♻️ Refactor: shape data structure, dashboard data loading...

This commit is contained in:
Andrey Antukh 2020-01-07 09:35:38 +01:00
parent 9f8936ea40
commit 1e058463b2
56 changed files with 1785 additions and 2316 deletions

View file

@ -56,7 +56,6 @@
(let [sql create-project-user-sql (let [sql create-project-user-sql
project-id (mk-uuid "project" project-index user-index) project-id (mk-uuid "project" project-index user-index)
user-id (mk-uuid "user" (dec user-index))] user-id (mk-uuid "user" (dec user-index))]
(println sql project-id user-id)
(db/query-one conn [sql project-id user-id]))) (db/query-one conn [sql project-id user-id])))
;; --- Projects creation ;; --- Projects creation
@ -114,10 +113,10 @@
(let [canvas {:id (mk-uuid "canvas" 1) (let [canvas {:id (mk-uuid "canvas" 1)
:name "Canvas-1" :name "Canvas-1"
:type :canvas :type :canvas
:x1 200 :x 200
:y1 200 :y 200
:x2 1224 :width 1024
:y2 968} :height 768}
data {:shapes [] data {:shapes []
:canvas [(:id canvas)] :canvas [(:id canvas)]
:shapes-by-id {(:id canvas) canvas}} :shapes-by-id {(:id canvas) canvas}}

View file

@ -56,7 +56,7 @@
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
(log/info "Creating or updating icons collection:" name) (log/info "Creating or updating icons collection:" name)
(let [id (uuid/namespaced +icons-uuid-ns+ name) (let [id (uuid/namespaced +icons-uuid-ns+ name)
sql "insert into icons_collections (id, user_id, name) sql "insert into icon_collections (id, user_id, name)
values ($1, '00000000-0000-0000-0000-000000000000'::uuid, $2) values ($1, '00000000-0000-0000-0000-000000000000'::uuid, $2)
on conflict (id) on conflict (id)
do update set name = $2 do update set name = $2
@ -122,8 +122,8 @@
"Create or replace image collection by its name." "Create or replace image collection by its name."
[conn {:keys [name] :as item}] [conn {:keys [name] :as item}]
(log/info "Creating or updating image collection:" name) (log/info "Creating or updating image collection:" name)
(let [id (uuid/namespaced +icons-uuid-ns+ name) (let [id (uuid/namespaced +images-uuid-ns+ name)
sql "insert into images_collections (id, user_id, name) sql "insert into image_collections (id, user_id, name)
values ($1, '00000000-0000-0000-0000-000000000000'::uuid, $2) values ($1, '00000000-0000-0000-0000-000000000000'::uuid, $2)
on conflict (id) on conflict (id)
do update set name = $2 do update set name = $2

View file

@ -45,7 +45,7 @@
(sm/defmutation ::create-icons-collection (sm/defmutation ::create-icons-collection
[{:keys [id user name] :as params}] [{:keys [id user name] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
sql "insert into icons_collections (id, user_id, name) sql "insert into icon_collections (id, user_id, name)
values ($1, $2, $3) returning *"] values ($1, $2, $3) returning *"]
(db/query-one db/pool [sql id user name]))) (db/query-one db/pool [sql id user name])))
@ -56,7 +56,7 @@
(sm/defmutation ::update-icons-collection (sm/defmutation ::update-icons-collection
[{:keys [id user name] :as params}] [{:keys [id user name] :as params}]
(let [sql "update icons_collections (let [sql "update icon_collections
set name = $3 set name = $3
where id = $1 where id = $1
and user_id = $2 and user_id = $2
@ -97,7 +97,7 @@
(sm/defmutation ::delete-icons-collection (sm/defmutation ::delete-icons-collection
[{:keys [user id] :as params}] [{:keys [user id] :as params}]
(let [sql "update icons_collections (let [sql "update icon_collections
set deleted_at = clock_timestamp() set deleted_at = clock_timestamp()
where id = $1 where id = $1
and user_id = $2 and user_id = $2

View file

@ -60,7 +60,7 @@
(sm/defmutation ::create-image-collection (sm/defmutation ::create-image-collection
[{:keys [id user name] :as params}] [{:keys [id user name] :as params}]
(let [sql "insert into images_collections (id, user_id, name) (let [sql "insert into image_collections (id, user_id, name)
values ($1, $2, $3) returning *;"] values ($1, $2, $3) returning *;"]
(db/query-one db/pool [sql (or id (uuid/next)) user name]))) (db/query-one db/pool [sql (or id (uuid/next)) user name])))
@ -71,7 +71,7 @@
(sm/defmutation ::update-images-collection (sm/defmutation ::update-images-collection
[{:keys [id user name] :as params}] [{:keys [id user name] :as params}]
(let [sql "update images_collections (let [sql "update image_collections
set name = $3 set name = $3
where id = $1 where id = $1
and user_id = $2 and user_id = $2
@ -85,7 +85,7 @@
(sm/defmutation ::delete-images-collection (sm/defmutation ::delete-images-collection
[{:keys [id user] :as params}] [{:keys [id user] :as params}]
(let [sql "update images_collections (let [sql "update image_collections
set deleted_at = clock_timestamp() set deleted_at = clock_timestamp()
where id = $1 where id = $1
and user_id = $2 and user_id = $2
@ -102,13 +102,13 @@
(-> (ds/save storage filename path) (-> (ds/save storage filename path)
(su/handle-on-context)))) (su/handle-on-context))))
(def ^:private create-image-sql (su/defstr sql:create-image
"insert into images (user_id, name, collection_id, path, width, height, mimetype) "insert into images (user_id, name, collection_id, path, width, height, mimetype)
values ($1, $2, $3, $4, $5, $6, $7) returning *") values ($1, $2, $3, $4, $5, $6, $7) returning *")
(defn- store-image-in-db (defn- store-image-in-db
[conn {:keys [id user name path collection-id height width mimetype]}] [conn {:keys [id user name path collection-id height width mimetype]}]
(let [sqlv [create-image-sql user name collection-id (let [sqlv [sql:create-image user name collection-id
path width height mimetype]] path width height mimetype]]
(-> (db/query-one conn sqlv) (-> (db/query-one conn sqlv)
(p/then populate-thumbnail) (p/then populate-thumbnail)

View file

@ -59,7 +59,7 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)))))) :code :not-authorized))))))
;; --- Mutation: Create Project ;; --- Mutation: Create Project File
(declare create-file) (declare create-file)
(declare create-page) (declare create-page)
@ -72,9 +72,9 @@
[{:keys [user project-id] :as params}] [{:keys [user project-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(proj/check-edition-permissions! conn user project-id) (proj/check-edition-permissions! conn user project-id)
(p/let [file (create-file conn params)] (p/let [file (create-file conn params)
(create-page conn (assoc params :file-id (:id file))) page (create-page conn (assoc params :file-id (:id file)))]
file))) (assoc file :pages [(:id page)]))))
(defn create-file (defn create-file
[conn {:keys [id user name project-id] :as params}] [conn {:keys [id user name project-id] :as params}]
@ -88,7 +88,10 @@
[conn {:keys [user file-id] :as params}] [conn {:keys [user file-id] :as params}]
(let [id (uuid/next) (let [id (uuid/next)
name "Page 1" name "Page 1"
data (blob/encode {}) data (blob/encode
{:shapes []
:canvas []
:shapes-by-id {}})
sql "insert into project_pages (id, user_id, file_id, name, version, sql "insert into project_pages (id, user_id, file_id, name, version,
ordering, data) ordering, data)
values ($1, $2, $3, $4, 0, 1, $5) returning id"] values ($1, $2, $3, $4, 0, 1, $5) returning id"]

View file

@ -31,7 +31,7 @@
(def ^:private icons-collections-sql (def ^:private icons-collections-sql
"select *, "select *,
(select count(*) from icons where collection_id = ic.id) as num_icons (select count(*) from icons where collection_id = ic.id) as num_icons
from icons_collections as ic from icon_collections as ic
where (ic.user_id = $1 or where (ic.user_id = $1 or
ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid) ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid)
and ic.deleted_at is null and ic.deleted_at is null

View file

@ -53,7 +53,7 @@
(def ^:private images-collections-sql (def ^:private images-collections-sql
"select *, "select *,
(select count(*) from images where collection_id = ic.id) as num_images (select count(*) from images where collection_id = ic.id) as num_images
from images_collections as ic from image_collections as ic
where (ic.user_id = $1 or where (ic.user_id = $1 or
ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid) ic.user_id = '00000000-0000-0000-0000-000000000000'::uuid)
and ic.deleted_at is null and ic.deleted_at is null
@ -86,23 +86,32 @@
;; --- Query Images by Collection (id) ;; --- Query Images by Collection (id)
(def images-by-collection-sql (su/defstr sql:images-by-collection
"select * from images "select * from images
where (user_id = $1 or where (user_id = $1 or
user_id = '00000000-0000-0000-0000-000000000000'::uuid) user_id = '00000000-0000-0000-0000-000000000000'::uuid)
and deleted_at is null and deleted_at is null
and case when $2::uuid is null then collection_id is null order by created_at desc")
else collection_id = $2::uuid
end
order by created_at desc;")
(s/def ::images-by-collection-query (su/defstr sql:images-by-collection1
"with images as (~{sql:images-by-collection})
select im.* from images as im
where im.collection_id is null")
(su/defstr sql:images-by-collection2
"with images as (~{sql:images-by-collection})
select im.* from images as im
where im.collection_id = $2")
(s/def ::images-by-collection
(s/keys :req-un [::user] (s/keys :req-un [::user]
:opt-un [::collection-id])) :opt-un [::collection-id]))
(sq/defquery ::images-by-collection (sq/defquery ::images-by-collection
[{:keys [user collection-id] :as params}] [{:keys [user collection-id] :as params}]
(let [sqlv [images-by-collection-sql user collection-id]] (let [sqlv (if (nil? collection-id)
[sql:images-by-collection1 user]
[sql:images-by-collection2 user collection-id])]
(-> (db/query db/pool sqlv) (-> (db/query db/pool sqlv)
(p/then populate-thumbnails) (p/then populate-thumbnails)
(p/then #(mapv populate-urls %))))) (p/then #(mapv populate-urls %)))))

View file

@ -99,13 +99,13 @@
{:enter (fn [data] {:enter (fn [data]
(let [context (get-in data [:request ::vw/routing-context]) (let [context (get-in data [:request ::vw/routing-context])
uploads (reduce (fn [acc ^FileUpload upload] uploads (reduce (fn [acc ^FileUpload upload]
(assoc acc (assoc! acc
(keyword (.name upload)) (keyword (.name upload))
{:type :uploaded-file {:type :uploaded-file
:mtype (.contentType upload) :mtype (.contentType upload)
:path (.uploadedFileName upload) :path (.uploadedFileName upload)
:name (.fileName upload) :name (.fileName upload)
:size (.size upload)})) :size (.size upload)}))
(transient {}) (transient {})
(.fileUploads ^RoutingContext context))] (.fileUploads ^RoutingContext context))]
(update data :request assoc attr (persistent! uploads))))})) (update data :request assoc attr (persistent! uploads))))}))

View file

@ -7,7 +7,13 @@
(ns uxbox.common.data (ns uxbox.common.data
"Data manipulation and query helper functions." "Data manipulation and query helper functions."
(:refer-clojure :exclude [concat]) (:refer-clojure :exclude [concat])
(:require [clojure.set :as set])) (:require [clojure.set :as set]
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Structures Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn concat (defn concat
[& colls] [& colls]
@ -18,6 +24,18 @@
(rest colls)) (rest colls))
result))) result)))
(defn enumerate
([items] (enumerate items 0))
([items start]
(loop [idx start
items items
res []]
(if (empty? items)
res
(recur (inc idx)
(rest items)
(conj res [idx (first items)]))))))
(defn seek (defn seek
([pred coll] ([pred coll]
(seek pred coll nil)) (seek pred coll nil))
@ -48,3 +66,75 @@
(recur (first r) (rest r) rs) (recur (first r) (rest r) rs)
(recur (first r) (rest r) (conj rs [:mod k vmb])))) (recur (first r) (rest r) (conj rs [:mod k vmb]))))
rs))))) rs)))))
(defn index-by
"Return a indexed map of the collection keyed by the result of
executing the getter over each element of the collection."
[getter coll]
(persistent!
(reduce #(assoc! %1 (getter %2) %2) (transient {}) coll)))
(defn remove-nil-vals
"Given a map, return a map removing key-value
pairs when value is `nil`."
[data]
(into {} (remove (comp nil? second) data)))
(defn without-keys
"Return a map without the keys provided
in the `keys` parameter."
[data keys]
(persistent!
(reduce #(dissoc! %1 %2) (transient data) keys)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- nan?
[v]
(not= v v))
(defn- impl-parse-integer
[v]
#?(:cljs (js/parseInt v 10)
:clj (try
(Integer/parseInt v)
(catch Throwable e
nil))))
(defn- impl-parse-double
[v]
#?(:cljs (js/parseFloat v)
:clj (try
(Double/parseDouble v)
(catch Throwable e
nil))))
(defn parse-integer
([v]
(parse-integer v nil))
([v default]
(let [v (impl-parse-integer v)]
(if (or (nil? v) (nan? v))
default
v))))
(defn parse-double
([v]
(parse-double v nil))
([v default]
(let [v (impl-parse-double v)]
(if (or (nil? v) (nan? v))
default
v))))
(defn read-string
[v]
(r/read-string v))
(defn coalesce-str
[val default]
(if (or (nil? val) (nan? val))
default
(str val)))

View file

@ -16,13 +16,12 @@
(s/def ::background string?) (s/def ::background string?)
(s/def ::background-opacity number?) (s/def ::background-opacity number?)
;; Page related (s/def ::metadata
(s/def ::file-id uuid?) (s/keys :opt-un [::grid-y-axis
(s/def ::user uuid?) ::grid-x-axis
(s/def ::created-at inst?) ::grid-color
(s/def ::modified-at inst?) ::background
(s/def ::version number?) ::background-opacity]))
(s/def ::ordering number?)
;; Page Data related ;; Page Data related
(s/def ::shape (s/def ::shape
@ -35,19 +34,9 @@
(s/def ::shapes-by-id (s/def ::shapes-by-id
(s/map-of uuid? ::shape)) (s/map-of uuid? ::shape))
;; Main
(s/def ::data (s/def ::data
(s/keys :req-un [::shapes ::canvas ::shapes-by-id])) (s/keys :req-un [::shapes ::canvas ::shapes-by-id]))
(s/def ::metadata
(s/keys :opt-un [::grid-y-axis
::grid-x-axis
::grid-color
::background
::background-opacity]))
(s/def ::shape-change (s/def ::shape-change
(s/tuple #{:add :mod :del} keyword? any?)) (s/tuple #{:add :mod :del} keyword? any?))

View file

@ -3,15 +3,15 @@
org.clojure/clojure {:mvn/version "1.10.1"} org.clojure/clojure {:mvn/version "1.10.1"}
com.cognitect/transit-cljs {:mvn/version "0.8.256"} com.cognitect/transit-cljs {:mvn/version "0.8.256"}
cljsjs/react {:mvn/version "16.11.0-0"} cljsjs/react {:mvn/version "16.12.0-1"}
cljsjs/react-dom {:mvn/version "16.11.0-0"} cljsjs/react-dom {:mvn/version "16.12.0-1"}
cljsjs/react-dom-server {:mvn/version "16.11.0-0"} cljsjs/react-dom-server {:mvn/version "16.12.0-1"}
environ/environ {:mvn/version "1.1.0"} environ/environ {:mvn/version "1.1.0"}
metosin/reitit-core {:mvn/version "0.3.10"} metosin/reitit-core {:mvn/version "0.3.10"}
expound/expound {:mvn/version "0.7.2"} expound/expound {:mvn/version "0.7.2"}
funcool/beicon {:mvn/version "5.1.0"} funcool/beicon {:mvn/version "6.0.0-SNAPSHOT"}
funcool/cuerdas {:mvn/version "2.2.0"} funcool/cuerdas {:mvn/version "2.2.0"}
funcool/lentes {:mvn/version "1.3.3"} funcool/lentes {:mvn/version "1.3.3"}
funcool/potok {:mvn/version "2.8.0-SNAPSHOT"} funcool/potok {:mvn/version "2.8.0-SNAPSHOT"}

View file

@ -242,7 +242,7 @@
// IMAGES SECTION // IMAGES SECTION
&.images-th { &.images-th {
background-color: $primary-ui-bg; border: 1px dashed $color-gray-light;
border-bottom: 2px solid lighten($color-gray-light, 12%); border-bottom: 2px solid lighten($color-gray-light, 12%);
&:hover { &:hover {

View file

@ -17,21 +17,14 @@
[uxbox.util.time :as dt] [uxbox.util.time :as dt]
[uxbox.util.uuid :as uuid])) [uxbox.util.uuid :as uuid]))
;; TODO: need a good refactor
;; --- Initialize ;; --- Initialize
(declare fetch-collections) (declare fetch-collections)
(declare persist-collections) (declare persist-collections)
(declare collections-fetched?) (declare collections-fetched?)
(defrecord Initialize []
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :colors] {:selected #{}})))
(defn initialize
[]
(Initialize.))
;; --- Collections Fetched ;; --- Collections Fetched
(defrecord CollectionsFetched [data] (defrecord CollectionsFetched [data]
@ -56,7 +49,7 @@
(defrecord FetchCollections [] (defrecord FetchCollections []
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
(->> (rp/query! :kvstore-entry {:key "color-collections"}) (->> (rp/query! :user-attr {:key "color-collections"})
(rx/map collections-fetched) (rx/map collections-fetched)
(rx/catch (fn [{:keys [type] :as error}] (rx/catch (fn [{:keys [type] :as error}]
(if (= type :not-found) (if (= type :not-found)
@ -99,10 +92,9 @@
version (or (get state ::version) -1) version (or (get state ::version) -1)
value (->> (get state :colors-collections) value (->> (get state :colors-collections)
(into {} xform)) (into {} xform))
data {:id "color-collections" data {:key "color-collections"
:version version :val value}]
:value value}] (->> (rp/mutation! :upsert-user-attr data)
(->> (rp/mutation! :upsert-kvstore data)
(rx/map collections-fetched))))) (rx/map collections-fetched)))))
(defn persist-collections (defn persist-collections

View file

@ -6,6 +6,7 @@
(ns uxbox.main.data.icons (ns uxbox.main.data.icons
(:require (:require
[cljs.spec.alpha :as s]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.core :as ptk] [potok.core :as ptk]
@ -18,30 +19,32 @@
[uxbox.util.router :as r] [uxbox.util.router :as r]
[uxbox.util.uuid :as uuid])) [uxbox.util.uuid :as uuid]))
;; --- Initialize (s/def ::id uuid?)
(s/def ::name string?)
(s/def ::created-at inst?)
(s/def ::modified-at inst?)
(s/def ::user-id uuid?)
(def initialize ;; (s/def ::collection-id (s/nilable ::us/uuid))
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :icons] {:selected #{}}))))
;; --- Select a Collection ;; (s/def ::mimetype string?)
;; (s/def ::thumbnail us/url-str?)
;; (s/def ::width number?)
;; (s/def ::height number?)
;; (s/def ::url us/url-str?)
(defn select-collection (s/def ::collection
([type] (s/keys :req-un [::id
(select-collection type nil)) ::name
([type id] ::created-at
{:pre [(keyword? type)]} ::modified-at
(ptk/reify ::select-collection ::user-id]))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (r/navigate :dashboard/icons {:type type :id id}))))))
;; --- Collections Fetched ;; --- Collections Fetched
(defn collections-fetched (defn collections-fetched
[items] [items]
(s/assert (s/every ::collection) items)
(ptk/reify ::collections-fetched (ptk/reify ::collections-fetched
cljs.core/IDeref cljs.core/IDeref
(-deref [_] items) (-deref [_] items)
@ -55,10 +58,6 @@
state state
items)))) items))))
(defn collections-fetched?
[v]
(= ::collections-fetched (ptk/type v)))
;; --- Fetch Collections ;; --- Fetch Collections
(def fetch-collections (def fetch-collections
@ -72,15 +71,12 @@
(defn collection-created (defn collection-created
[item] [item]
(s/assert ::collection item)
(ptk/reify ::collection-created (ptk/reify ::collection-created
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [{:keys [id] :as item} (assoc item :type :own)] (let [{:keys [id] :as item} (assoc item :type :own)]
(update state :icons-collections assoc id item))) (update state :icons-collections assoc id item)))))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (select-collection :own (:id item))))))
;; --- Create Collection ;; --- Create Collection
@ -100,7 +96,7 @@
(ptk/reify ::collection-updated (ptk/reify ::collection-updated
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update-in state [:icons-collections (:id item)] merge item)))) (update-in state [:icons-collections (:id item)] merge item))))
;; --- Update Collection ;; --- Update Collection
@ -141,7 +137,7 @@
(watch [_ state s] (watch [_ state s]
(let [type (get-in state [:dashboard :icons :type])] (let [type (get-in state [:dashboard :icons :type])]
(->> (rp/mutation! :delete-icons-collection {:id id}) (->> (rp/mutation! :delete-icons-collection {:id id})
(rx/map #(select-collection type)))))) (rx/map #(r/nav :dashboard-icons {:type type}))))))
(defn delete-collection (defn delete-collection
[id] [id]
@ -179,44 +175,42 @@
(dom/append-child! gc child)) (dom/append-child! gc child))
(recur (dom/get-first-child svg))) (recur (dom/get-first-child svg)))
(let [width (.. svg -width -baseVal -value) (let [width (.. svg -width -baseVal -value)
header (.. svg -height -baseVal -value) height (.. svg -height -baseVal -value)
view-box [(.. svg -viewBox -baseVal -x) view-box [(.. svg -viewBox -baseVal -x)
(.. svg -viewBox -baseVal -y) (.. svg -viewBox -baseVal -y)
(.. svg -viewBox -baseVal -width) (.. svg -viewBox -baseVal -width)
(.. svg -viewBox -baseVal -height)] (.. svg -viewBox -baseVal -height)]
props {:width width props {:width width
:mimetype "image/svg+xml" :mimetype "image/svg+xml"
:height header :height height
:view-box view-box}] :view-box view-box}]
[(dom/get-outer-html g) props]))))) [(dom/get-outer-html g) props])))))
(defrecord CreateIcons [id files]
ptk/WatchEvent
(watch [_ state s]
(letfn [(parse [file]
(->> (files/read-as-text file)
(rx/map parse-svg)))
(allowed? [file]
(= (.-type file) "image/svg+xml"))
(prepare [[content metadata]]
{:collection-id id
:content content
:id (uuid/random)
;; TODO Keep the name of the original icon
:name (str "Icon " (gensym "i"))
:metadata metadata})]
(->> (rx/from-coll files)
(rx/filter allowed?)
(rx/flat-map parse)
(rx/map prepare)
(rx/flat-map #(rp/mutation! :create-icon %))
(rx/map :payload)
(rx/map icon-created)))))
(defn create-icons (defn create-icons
[id files] [id files]
{:pre [(or (uuid? id) (nil? id))]} (s/assert (s/nilable uuid?) id)
(CreateIcons. id files)) (ptk/reify ::create-icons
ptk/WatchEvent
(watch [_ state s]
(letfn [(parse [file]
(->> (files/read-as-text file)
(rx/map parse-svg)))
(allowed? [file]
(= (.-type file) "image/svg+xml"))
(prepare [[content metadata]]
{:collection-id id
:content content
:id (uuid/random)
;; TODO Keep the name of the original icon
:name (str "Icon " (gensym "i"))
:metadata metadata})]
(->> (rx/from files)
(rx/filter allowed?)
(rx/flat-map parse)
(rx/map prepare)
(rx/flat-map #(rp/mutation! :create-icon %))
(rx/map icon-created))))))
;; --- Icon Persisted ;; --- Icon Persisted
@ -345,14 +339,14 @@
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])] (let [selected (get-in state [:dashboard :icons :selected])]
(rx/merge (rx/merge
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map #(get-in state [:icons %])) (rx/map #(get-in state [:icons %]))
(rx/map #(dissoc % :id)) (rx/map #(dissoc % :id))
(rx/map #(assoc % :collection-id id)) (rx/map #(assoc % :collection-id id))
(rx/flat-map #(rp/mutation :create-icon %)) (rx/flat-map #(rp/mutation :create-icon %))
(rx/map :payload) (rx/map :payload)
(rx/map icon-created)) (rx/map icon-created))
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map deselect-icon)))))) (rx/map deselect-icon))))))
(defn copy-selected (defn copy-selected
@ -375,9 +369,9 @@
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])] (let [selected (get-in state [:dashboard :icons :selected])]
(rx/merge (rx/merge
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map persist-icon)) (rx/map persist-icon))
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map deselect-icon)))))) (rx/map deselect-icon))))))
(defn move-selected (defn move-selected
@ -391,7 +385,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])] (let [selected (get-in state [:dashboard :icons :selected])]
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map delete-icon))))) (rx/map delete-icon)))))
(defn delete-selected (defn delete-selected

View file

@ -55,80 +55,50 @@
::url ::url
::user-id])) ::user-id]))
;; --- Initialize ;; --- Collections Fetched
(defrecord Initialize []
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :images] {:selected #{}})))
(defn initialize
[]
(Initialize.))
;; --- Color Collections Fetched
(defrecord CollectionsFetched [items]
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id user] :as item}]
(let [type (if (uuid/zero? (:user-id item)) :builtin :own)
item (assoc item :type type)]
(assoc-in state [:images-collections id] item)))
state
items)))
(defn collections-fetched (defn collections-fetched
[items] [items]
{:pre [(us/valid? (s/every ::collection-entity) items)]} (s/assert (s/every ::collection-entity) items)
(CollectionsFetched. items)) (ptk/reify ::collections-fetched
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id user] :as item}]
(let [type (if (uuid/zero? (:user-id item)) :builtin :own)
item (assoc item :type type)]
(assoc-in state [:images-collections id] item)))
state
items))))
;; --- Fetch Color Collections ;; --- Fetch Color Collections
(defrecord FetchCollections [] (def fetch-collections
ptk/WatchEvent (ptk/reify ::fetch-collections
(watch [_ state s] ptk/WatchEvent
(->> (rp/query! :images-collections) (watch [_ state s]
(rx/map collections-fetched)))) (->> (rp/query! :images-collections)
(rx/map collections-fetched)))))
(defn fetch-collections
[]
(FetchCollections.))
;; --- Collection Created ;; --- Collection Created
(defrecord CollectionCreated [item]
ptk/UpdateEvent
(update [_ state]
(let [{:keys [id] :as item} (assoc item :type :own)]
(update state :images-collections assoc id item)))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (rt/nav :dashboard/images nil {:type :own :id (:id item)}))))
(defn collection-created (defn collection-created
[item] [item]
{:pre [(us/valid? ::collection-entity item)]} (s/assert ::collection-entity item)
(CollectionCreated. item)) (ptk/reify ::collection-created
ptk/UpdateEvent
(update [_ state]
(let [{:keys [id] :as item} (assoc item :type :own)]
(update state :images-collections assoc id item)))))
;; --- Create Collection ;; --- Create Collection
(defrecord CreateCollection [] (def create-collection
ptk/WatchEvent (ptk/reify ::create-collection
(watch [_ state s] ptk/WatchEvent
(let [data {:name (tr "ds.default-library-title" (gensym "c"))}] (watch [_ state s]
(->> (rp/mutation! :create-image-collection data) (let [data {:name (tr "ds.default-library-title" (gensym "c"))}]
(rx/map :payload) (->> (rp/mutation! :create-image-collection data)
(rx/map collection-created))))) (rx/map collection-created))))))
(defn create-collection
[]
(CreateCollection.))
(defn collections-fetched?
[v]
(instance? CollectionsFetched v))
;; --- Collection Updated ;; --- Collection Updated
@ -189,61 +159,55 @@
;; --- Image Created ;; --- Image Created
(defrecord ImageCreated [item]
ptk/UpdateEvent
(update [_ state]
(update state :images assoc (:id item) item)))
(defn image-created (defn image-created
[item] [item]
{:pre [(us/valid? ::image-entity item)]} (s/assert ::image-entity item)
(ImageCreated. item)) (ptk/reify ::image-created
ptk/UpdateEvent
(update [_ state]
(update state :images assoc (:id item) item))))
;; --- Create Image ;; --- Create Image
(def allowed-file-types #{"image/jpeg" "image/png"}) (def allowed-file-types #{"image/jpeg" "image/png"})
(defrecord CreateImages [id files on-uploaded]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :images :uploading] true))
ptk/WatchEvent
(watch [_ state stream]
(letfn [(image-size [file]
(->> (files/get-image-size file)
(rx/map (partial vector file))))
(allowed-file? [file]
(contains? allowed-file-types (.-type file)))
(finalize-upload [state]
(assoc-in state [:dashboard :images :uploading] false))
(prepare [[file [width height]]]
(cond-> {:name (.-name file)
:mimetype (.-type file)
:id (uuid/random)
:file file
:width width
:height height}
id (assoc :collection-id id)))]
(->> (rx/from-coll files)
(rx/filter allowed-file?)
(rx/mapcat image-size)
(rx/map prepare)
(rx/mapcat #(rp/mutation! :create-image %))
(rx/map :payload)
(rx/reduce conj [])
(rx/do #(st/emit! finalize-upload))
(rx/do on-uploaded)
(rx/mapcat identity)
(rx/map image-created)))))
(defn create-images (defn create-images
([id files] ([id files] (create-images id files identity))
{:pre [(or (uuid? id) (nil? id))]}
(CreateImages. id files (constantly nil)))
([id files on-uploaded] ([id files on-uploaded]
{:pre [(or (uuid? id) (nil? id)) (fn? on-uploaded)]} (s/assert (s/nilable ::us/uuid) id)
(CreateImages. id files on-uploaded))) (s/assert fn? on-uploaded)
(ptk/reify ::create-images
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :images :uploading] true))
ptk/WatchEvent
(watch [_ state stream]
(letfn [(image-size [file]
(->> (files/get-image-size file)
(rx/map (partial vector file))))
(allowed-file? [file]
(contains? allowed-file-types (.-type file)))
(finalize-upload [state]
(assoc-in state [:dashboard :images :uploading] false))
(prepare [[file [width height]]]
(cond-> {:name (.-name file)
:mimetype (.-type file)
:id (uuid/random)
:file file
:width width
:height height}
id (assoc :collection-id id)))]
(->> (rx/from files)
(rx/filter allowed-file?)
(rx/mapcat image-size)
(rx/map prepare)
(rx/mapcat #(rp/mutation! :create-image %))
(rx/reduce conj [])
(rx/do #(st/emit! finalize-upload))
(rx/do on-uploaded)
(rx/mapcat identity)
(rx/map image-created)))))))
;; --- Update Image ;; --- Update Image
@ -259,32 +223,29 @@
;; --- Images Fetched ;; --- Images Fetched
(defrecord ImagesFetched [items]
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id] :as image}]
(assoc-in state [:images id] image))
state
items)))
(defn images-fetched (defn images-fetched
[items] [items]
(ImagesFetched. items)) (s/assert (s/every ::image-entity) items)
(ptk/reify ::images-fetched
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id] :as image}]
(assoc-in state [:images id] image))
state
items))))
;; --- Fetch Images ;; --- Fetch Images
(defrecord FetchImages [id]
ptk/WatchEvent
(watch [_ state s]
(let [params (cond-> {} id (assoc :collection-id id))]
(->> (rp/query! :images-by-collection params)
(rx/map images-fetched)))))
(defn fetch-images (defn fetch-images
"Fetch a list of images of the selected collection" "Fetch a list of images of the selected collection"
[id] [id]
{:pre [(or (uuid? id) (nil? id))]} (s/assert (s/nilable ::us/uuid) id)
(FetchImages. id)) (ptk/reify ::fetch-images
ptk/WatchEvent
(watch [_ state s]
(let [params (cond-> {} id (assoc :collection-id id))]
(->> (rp/query! :images-by-collection params)
(rx/map images-fetched))))))
;; --- Fetch Image ;; --- Fetch Image
@ -392,10 +353,10 @@
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])] (let [selected (get-in state [:dashboard :images :selected])]
(rx/merge (rx/merge
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/flat-map #(rp/mutation! :copy-image {:id % :collection-id id})) (rx/flat-map #(rp/mutation! :copy-image {:id % :collection-id id}))
(rx/map image-created)) (rx/map image-created))
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map deselect-image)))))) (rx/map deselect-image))))))
(defn copy-selected (defn copy-selected
@ -418,9 +379,9 @@
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])] (let [selected (get-in state [:dashboard :images :selected])]
(rx/merge (rx/merge
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map persist-image)) (rx/map persist-image))
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map deselect-image)))))) (rx/map deselect-image))))))
(defn move-selected (defn move-selected
@ -434,7 +395,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])] (let [selected (get-in state [:dashboard :images :selected])]
(->> (rx/from-coll selected) (->> (rx/from selected)
(rx/map delete-image))))) (rx/map delete-image)))))
(defn delete-selected (defn delete-selected

View file

@ -10,8 +10,8 @@
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.core :as ptk] [potok.core :as ptk]
[uxbox.common.pages :as cp]
[uxbox.main.repo.core :as rp] [uxbox.main.repo.core :as rp]
[uxbox.util.data :refer [index-by-id concatv]]
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.util.spec :as us] [uxbox.util.spec :as us]
[uxbox.util.time :as dt] [uxbox.util.time :as dt]
@ -25,12 +25,15 @@
(s/def ::user ::us/uuid) (s/def ::user ::us/uuid)
(s/def ::type ::us/keyword) (s/def ::type ::us/keyword)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::created-at ::us/inst) (s/def ::created-at ::us/inst)
(s/def ::modified-at ::us/inst) (s/def ::modified-at ::us/inst)
(s/def ::version ::us/number) (s/def ::version ::us/number)
(s/def ::ordering ::us/number) (s/def ::ordering ::us/number)
(s/def ::metadata (s/nilable ::cp/metadata))
(s/def ::data ::cp/data)
(s/def ::project-entity (s/def ::project
(s/keys ::req-un [::id (s/keys ::req-un [::id
::name ::name
::version ::version
@ -38,38 +41,18 @@
::created-at ::created-at
::modified-at])) ::modified-at]))
(s/def ::grid-x-axis ::us/number) (s/def ::file
(s/def ::grid-y-axis ::us/number) (s/keys :req-un [::id
(s/def ::grid-color ::us/string) ::name
(s/def ::background ::us/string) ::created-at
(s/def ::background-opacity ::us/number) ::modified-at
::project-id]))
(s/def ::metadata
(s/keys :opt-un [::grid-y-axis
::grid-x-axis
::grid-color
::background
::background-opacity]))
;; TODO: start using uxbox.common.pagedata/data spec ...
(s/def ::minimal-shape
(s/keys :req-un [::type ::name]
:opt-un [::id]))
(s/def ::shapes (s/coll-of ::us/uuid :kind vector?))
(s/def ::canvas (s/coll-of ::us/uuid :kind vector?))
(s/def ::shapes-by-id
(s/map-of ::us/uuid ::minimal-shape))
(s/def ::data
(s/keys :req-un [::shapes ::canvas ::shapes-by-id]))
(s/def ::page (s/def ::page
(s/keys :req-un [::id (s/keys :req-un [::id
::name ::name
::file-id ::file-id
::version
::created-at ::created-at
::modified-at ::modified-at
::user-id ::user-id
@ -77,23 +60,8 @@
::metadata ::metadata
::data])) ::data]))
(s/def ::pages
(s/every ::page :kind vector?))
;; --- Helpers ;; --- Helpers
(defn assoc-project
"A reduce function for assoc the project to the state map."
[state {:keys [id] :as project}]
(s/assert ::project-entity project)
(update-in state [:projects id] merge project))
(defn dissoc-project
"A reduce function for dissoc the project from the state map."
[state id]
(update state :projects dissoc id))
(defn unpack-page (defn unpack-page
[state {:keys [id data metadata] :as page}] [state {:keys [id data metadata] :as page}]
(-> state (-> state
@ -114,13 +82,10 @@
;; --- Initialize Dashboard ;; --- Initialize Dashboard
(declare fetch-projects) (declare fetch-projects)
(declare projects-fetched?)
(declare fetch-files) (declare fetch-files)
(declare initialized) (declare initialized)
;; TODO: rename to initialize dashboard
(defn initialize (defn initialize
[id] [id]
(ptk/reify ::initialize (ptk/reify ::initialize
@ -156,21 +121,8 @@
(when order {:order order}) (when order {:order order})
(when filter {:filter filter}))))) (when filter {:filter filter})))))
;; --- Projects Fetched
(defn projects-fetched
[projects]
(s/assert (s/every ::project-entity) projects)
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
(reduce assoc-project state projects))))
(defn projects-fetched?
[v]
(= ::projects-fetched (ptk/type v)))
;; --- Fetch Projects ;; --- Fetch Projects
(declare projects-fetched)
(def fetch-projects (def fetch-projects
(ptk/reify ::fetch-projects (ptk/reify ::fetch-projects
@ -179,6 +131,17 @@
(->> (rp/query :projects) (->> (rp/query :projects)
(rx/map projects-fetched))))) (rx/map projects-fetched)))))
;; --- Projects Fetched
(defn projects-fetched
[projects]
(s/assert (s/every ::project) projects)
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
(let [assoc-project #(update-in %1 [:projects (:id %2)] merge %2)]
(reduce assoc-project state projects)))))
;; --- Fetch Files ;; --- Fetch Files
(declare files-fetched) (declare files-fetched)
@ -205,11 +168,9 @@
;; --- Files Fetched ;; --- Files Fetched
(s/def ::files any?)
(defn files-fetched (defn files-fetched
[files] [files]
(s/assert ::files files) (s/assert (s/every ::file) files)
(ptk/reify ::files-fetched (ptk/reify ::files-fetched
cljs.core/IDeref cljs.core/IDeref
(-deref [_] files) (-deref [_] files)
@ -219,6 +180,34 @@
(let [assoc-file #(assoc-in %1 [:files (:id %2)] %2)] (let [assoc-file #(assoc-in %1 [:files (:id %2)] %2)]
(reduce assoc-file state files))))) (reduce assoc-file state files)))))
;; --- Create Project
(declare project-created)
(def create-project
(ptk/reify ::create-project
ptk/WatchEvent
(watch [this state stream]
(let [name (str "Project Name " (gensym "p"))]
(->> (rp/mutation! :create-project {:name name})
(rx/map (fn [data]
(projects-fetched [data]))))))))
;; --- Create File
(defn create-file
[{:keys [project-id] :as params}]
(ptk/reify ::create-file
ptk/WatchEvent
(watch [this state stream]
(let [name (str "File Name " (gensym "p"))
params {:name name :project-id project-id}]
(->> (rp/mutation! :create-project-file params)
(rx/mapcat
(fn [data]
(rx/of (files-fetched [data])
#(update-in % [:dashboard-projects :files project-id] conj (:id data))))))))))
;; --- Rename Project ;; --- Rename Project
(defn rename-project (defn rename-project
@ -243,7 +232,7 @@
(ptk/reify ::delete-project (ptk/reify ::delete-project
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(dissoc-project state id)) (update state :projects dissoc id))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state s] (watch [_ state s]
@ -265,32 +254,6 @@
(->> (rp/mutation :delete-project-file {:id id}) (->> (rp/mutation :delete-project-file {:id id})
(rx/ignore))))) (rx/ignore)))))
;; --- Create Project
(declare project-created)
(s/def ::create-project
(s/keys :req-un [::name]))
(defn create-project
[{:keys [name] :as params}]
(s/assert ::create-project params)
(ptk/reify ::create-project
ptk/WatchEvent
(watch [this state stream]
(->> (rp/mutation :create-project {:name name})
(rx/map project-created)))))
;; --- Project Created
(defn project-created
[data]
(ptk/reify ::project-created
ptk/UpdateEvent
(update [_ state]
(assoc-project state data))))
;; --- Rename Project ;; --- Rename Project
(defn rename-file (defn rename-file
@ -348,7 +311,7 @@
(defn pages-fetched (defn pages-fetched
[pages] [pages]
(s/assert ::pages pages) (s/assert (s/every ::page) pages)
(ptk/reify ::pages-fetched (ptk/reify ::pages-fetched
IDeref IDeref
(-deref [_] pages) (-deref [_] pages)

View file

@ -50,7 +50,6 @@
(s/def ::font-size number?) (s/def ::font-size number?)
(s/def ::font-style string?) (s/def ::font-style string?)
(s/def ::font-weight string?) (s/def ::font-weight string?)
(s/def ::height number?)
(s/def ::hidden boolean?) (s/def ::hidden boolean?)
(s/def ::id uuid?) (s/def ::id uuid?)
(s/def ::letter-spacing number?) (s/def ::letter-spacing number?)
@ -67,12 +66,13 @@
(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed}) (s/def ::stroke-style #{:none :solid :dotted :dashed :mixed})
(s/def ::stroke-width number?) (s/def ::stroke-width number?)
(s/def ::text-align #{"left" "right" "center" "justify"}) (s/def ::text-align #{"left" "right" "center" "justify"})
(s/def ::type #{:rect :path :circle :image :text}) (s/def ::type #{:rect :path :circle :image :text :canvas})
(s/def ::x number?)
(s/def ::y number?)
(s/def ::cx number?)
(s/def ::cy number?)
(s/def ::width number?) (s/def ::width number?)
(s/def ::x1 number?) (s/def ::height number?)
(s/def ::x2 number?)
(s/def ::y1 number?)
(s/def ::y2 number?)
(s/def ::attributes (s/def ::attributes
(s/keys :opt-un [::blocked (s/keys :opt-un [::blocked
@ -91,13 +91,14 @@
::proportion ::proportion
::proportion-lock ::proportion-lock
::rx ::ry ::rx ::ry
::cx ::cy
::x ::y
::stroke-color ::stroke-color
::stroke-opacity ::stroke-opacity
::stroke-style ::stroke-style
::stroke-width ::stroke-width
::text-align ::text-align
::x1 ::x2 ::width ::height]))
::y1 ::y2]))
(s/def ::minimal-shape (s/def ::minimal-shape
(s/keys :req-un [::id ::page ::type ::name])) (s/keys :req-un [::id ::page ::type ::name]))
@ -169,28 +170,6 @@
(ws/-close (get-in state [:ws file-id])) (ws/-close (get-in state [:ws file-id]))
(rx/of ::finalize)))) (rx/of ::finalize))))
;; --- Fetch Workspace Users
(declare users-fetched)
(defn fetch-users
[file-id]
(ptk/reify ::fetch-users
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :project-file-users {:file-id file-id})
(rx/map users-fetched)))))
(defn users-fetched
[users]
(ptk/reify ::users-fetched
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state user]
(update-in state [:workspace-users :by-id (:id user)] merge user))
state
users))))
;; --- Handle: Who ;; --- Handle: Who
;; TODO: assign color ;; TODO: assign color
@ -256,66 +235,119 @@
(defn initialize (defn initialize
"Initialize the workspace state." "Initialize the workspace state."
[file-id page-id] [file-id]
(s/assert ::us/uuid file-id) (s/assert ::us/uuid file-id)
(s/assert ::us/uuid page-id)
(ptk/reify ::initialize (ptk/reify ::initialize
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [local (assoc workspace-default (let [local (assoc workspace-default
:file-id file-id :file-id file-id)
:page-id page-id)] ;; :page-id page-id)
;; TODO: this need to be parametrized
uri (str "ws://localhost:6060/sub/" file-id)]
(-> state (-> state
(assoc :workspace-layout default-layout) (assoc :workspace-layout default-layout)
(assoc :workspace-local local)))) (assoc :workspace-local local)
(assoc-in [:ws file-id] (ws/open uri)))))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
#_(when-not (get-in state [:pages page-id]) (let [wsession (get-in state [:ws file-id])]
(reset! st/loader true)) (rx/merge
;; Stop possible previous watchers and re-fetch the main page
;; and all project related pages.
(rx/of ::stop-watcher
(dp/fetch-file file-id)
(dp/fetch-pages file-id)
(fetch-users file-id))
(rx/merge ;; When main page is fetched, schedule the main initialization.
;; Stop possible previous watchers and re-fetch the main page (->> (rx/zip (rx/filter (ptk/type? ::dp/pages-fetched) stream)
;; and all project related pages. (rx/filter (ptk/type? ::dp/files-fetched) stream))
(rx/of ::stop-watcher (rx/take 1)
(dp/fetch-file file-id) (rx/do #(reset! st/loader false))
(dp/fetch-pages file-id)) (rx/mapcat #(rx/of (initialized file-id)
#_(initialize-alignment page-id))))
;; When main page is fetched, schedule the main initialization. ;; WebSocket Incoming Messages Handling
(->> (rx/zip (rx/filter (ptk/type? ::dp/pages-fetched) stream) (->> (ws/-stream wsession)
(rx/filter (ptk/type? ::dp/files-fetched) stream)) (rx/filter #(= :message (:type %)))
(rx/take 1) (rx/map (comp t/decode :payload))
(rx/do #(reset! st/loader false)) (rx/filter #(s/valid? ::message %))
(rx/mapcat #(rx/of (initialized file-id page-id) (rx/map (fn [{:keys [type] :as msg}]
#_(initialize-alignment page-id)))) (case type
:who (handle-who msg)
(->> stream :pointer-update (handle-pointer-update msg)
(rx/filter uxbox.main.ui.workspace.streams/pointer-event?) :page-snapshot (handle-page-snapshot msg)
(rx/sample 150) ::unknown))))
(rx/tap (fn [{:keys [pt] :as event}]
(let [msg {:type :pointer-update
:page-id page-id
:x (:x pt)
:y (:y pt)}]
(ws/-send (get-in state [:ws file-id]) (t/encode msg)))))
(rx/ignore)
(rx/take-until (rx/filter #(= ::stop-watcher %) stream)))))))
#_(->> stream
;; TODO: this need to be rethinked
(rx/filter uxbox.main.ui.workspace.streams/pointer-event?)
(rx/sample 150)
(rx/tap (fn [{:keys [pt] :as event}]
(let [msg {:type :pointer-update
:page-id page-id
:x (:x pt)
:y (:y pt)}]
(ws/-send (get-in state [:ws file-id]) (t/encode msg)))))
(rx/ignore)
(rx/take-until (rx/filter #(= ::finalize %) stream))))))))
(defn- initialized (defn- initialized
[file-id page-id] [file-id]
(s/assert ::us/uuid file-id) (s/assert ::us/uuid file-id)
(s/assert ::us/uuid page-id)
(ptk/reify ::initialized (ptk/reify ::initialized
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [file (get-in state [:files file-id]) (let [file (get-in state [:files file-id])]
page (get-in state [:pages page-id]) (assoc state :workspace-file file)))))
(defn finalize
[file-id]
(ptk/reify ::finalize
cljs.core/IDeref
(-deref [_] file-id)
ptk/EffectEvent
(effect [_ state stream]
(ws/-close (get-in state [:ws file-id])))))
(defn initialize-page
[page-id]
(ptk/reify ::initialize-page
ptk/UpdateEvent
(update [_ state]
(let [page (get-in state [:pages page-id])
data (get-in state [:pages-data page-id])] data (get-in state [:pages-data page-id])]
(assoc state (assoc state
:workspace-file file
:workspace-data data :workspace-data data
:workspace-page page))))) :workspace-page page)))
ptk/EffectEvent
(effect [_ state stream])))
;; --- Fetch Workspace Users
(declare users-fetched)
(defn fetch-users
[file-id]
(ptk/reify ::fetch-users
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :project-file-users {:file-id file-id})
(rx/map users-fetched)))))
(defn users-fetched
[users]
(ptk/reify ::users-fetched
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state user]
(update-in state [:workspace-users :by-id (:id user)] merge user))
state
users))))
;; --- Toggle layout flag ;; --- Toggle layout flag
@ -576,14 +608,22 @@
(declare select-shape) (declare select-shape)
(declare recalculate-shape-canvas-relation) (declare recalculate-shape-canvas-relation)
(def shape-default-attrs
{:stroke-color "#000000"
:stroke-opacity 1
:fill-color "#000000"
:fill-opacity 1})
(defn add-shape (defn add-shape
[data] [data]
(s/assert ::attributes data)
(let [id (uuid/random)] (let [id (uuid/random)]
(ptk/reify ::add-shape (ptk/reify ::add-shape
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [shape (-> (geom/setup-proportions data) (let [shape (-> (geom/setup-proportions data)
(assoc :id id)) (assoc :id id))
shape (merge shape-default-attrs shape)
shape (recalculate-shape-canvas-relation state shape)] shape (recalculate-shape-canvas-relation state shape)]
(impl-assoc-shape state shape))) (impl-assoc-shape state shape)))
@ -593,6 +633,32 @@
(rx/of (commit-shapes-changes [[:add-shape id shape]]) (rx/of (commit-shapes-changes [[:add-shape id shape]])
(select-shape id))))))) (select-shape id)))))))
(def canvas-default-attrs
{:stroke-color "#000000"
:stroke-opacity 1
:fill-color "#ffffff"
:fill-opacity 1})
(defn add-canvas
[data]
(s/assert ::attributes data)
(let [id (uuid/random)]
(ptk/reify ::add-canvas
ptk/UpdateEvent
(update [_ state]
(let [shape (-> (geom/setup-proportions data)
(assoc :id id))
shape (merge canvas-default-attrs shape)
shape (recalculate-shape-canvas-relation state shape)]
(impl-assoc-shape state shape)))
ptk/WatchEvent
(watch [_ state stream]
(let [shape (get-in state [:workspace-data :shapes-by-id id])]
(rx/of (commit-shapes-changes [[:add-canvas id shape]])
(select-shape id)))))))
;; --- Duplicate Selected ;; --- Duplicate Selected
(defn impl-duplicate-shape (defn impl-duplicate-shape
@ -608,7 +674,7 @@
duplicate (partial impl-duplicate-shape state) duplicate (partial impl-duplicate-shape state)
shapes (map duplicate selected)] shapes (map duplicate selected)]
(rx/merge (rx/merge
(rx/from-coll (map (fn [s] #(impl-assoc-shape % s)) shapes)) (rx/from (map (fn [s] #(impl-assoc-shape % s)) shapes))
(rx/of (commit-shapes-changes (mapv #(vector :add-shape (:id %) %) shapes)))))))) (rx/of (commit-shapes-changes (mapv #(vector :add-shape (:id %) %) shapes))))))))
;; --- Toggle shape's selection status (selected or deselected) ;; --- Toggle shape's selection status (selected or deselected)
@ -675,41 +741,35 @@
;; --- Update Shape Attrs ;; --- Update Shape Attrs
(defn update-shape-attrs
[id attrs]
(s/assert ::us/uuid id)
(let [atts (s/conform ::attributes attrs)]
(ptk/reify ::update-shape-attrs
ptk/UpdateEvent
(update [_ state]
(if (map? attrs)
(update-in state [:workspace-data :shapes-by-id id] merge attrs)
state)))))
(defn update-shape (defn update-shape
[id & attrs] [id attrs]
(let [attrs' (->> (apply hash-map attrs) (s/assert ::attributes attrs)
(s/conform ::attributes))] (ptk/reify ::update-shape
(ptk/reify ::update-shape ptk/UpdateEvent
ptk/UpdateEvent (update [_ state]
(update [_ state] (let [shape-old (get-in state [:workspace-data :shapes-by-id id])
(cond-> state shape-new (merge shape-old attrs)
(not= attrs' ::s/invalid) diff (d/diff-maps shape-old shape-new)]
(update-in [:workspace-data :shapes-by-id id] merge attrs')))))) (-> state
(assoc-in [:workspace-data :shapes-by-id id] shape-new)
(assoc ::tmp-change (into [:mod-shape id] diff)))))
ptk/WatchEvent
(watch [_ state stream]
(let [change (::tmp-change state)]
(prn "update-shape" change)
(rx/of (commit-shapes-changes [change])
#(dissoc state ::tmp-change))))))
;; --- Update Selected Shapes attrs ;; --- Update Selected Shapes attrs
;; TODO: improve performance of this event (defn update-selected-shapes
[& attrs]
(defn update-selected-shapes-attrs
[attrs]
(s/assert ::attributes attrs)
(ptk/reify ::update-selected-shapes-attrs (ptk/reify ::update-selected-shapes-attrs
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])] (let [selected (get-in state [:workspace-local :selected])]
(rx/from-coll (map #(update-shape-attrs % attrs) selected)))))) (rx/from (map #(apply update-shape % attrs) selected))))))
;; --- Move Selected ;; --- Move Selected
@ -763,19 +823,6 @@
(rx/of (apply-temporal-displacement-in-bulk selected displacement)) (rx/of (apply-temporal-displacement-in-bulk selected displacement))
(rx/of (materialize-temporal-modifier-in-bulk selected))))))) (rx/of (materialize-temporal-modifier-in-bulk selected)))))))
;; --- Update Shape Position
(deftype UpdateShapePosition [id point]
ptk/UpdateEvent
(update [_ state]
(update-in state [:shapes id] geom/absolute-move point)))
(defn update-position
"Update the start position coordenate of the shape."
[id point]
{:pre [(uuid? id) (gpt/point? point)]}
(UpdateShapePosition. id point))
;; --- Delete Selected ;; --- Delete Selected
(defn impl-dissoc-shape (defn impl-dissoc-shape
@ -923,9 +970,10 @@
(defn- recalculate-shape-canvas-relation (defn- recalculate-shape-canvas-relation
[state shape] [state shape]
(let [xfmt (comp (map #(get-in state [:workspace-data :shapes-by-id %])) (let [shape' (geom/shape->rect-shape shape)
xfmt (comp (map #(get-in state [:workspace-data :shapes-by-id %]))
(map geom/shape->rect-shape) (map geom/shape->rect-shape)
(filter #(geom/overlaps? % shape)) (filter #(geom/overlaps? % shape'))
(map :id)) (map :id))
id (->> (get-in state [:workspace-data :canvas]) id (->> (get-in state [:workspace-data :canvas])
@ -1035,59 +1083,16 @@
(rx/map (constantly clear-drawing)) (rx/map (constantly clear-drawing))
(rx/take-until stoper))))))) (rx/take-until stoper)))))))
;; --- Shape Proportions
;; (defn toggle-shape-proportion-lock
;; [id]
;; (ptk/reify ::toggle-shape-proportion-lock
;; ptk/UpdateEvent
;; (update [_ state]
;; (let [shape (-> (get-in state [:workspace-data :shapes-by-id id])
;; (geom/size)
;; TODO: revisit
(deftype LockShapeProportions [id]
ptk/UpdateEvent
(update [_ state]
(let [[width height] (-> (get-in state [:shapes id])
(geom/size)
(keep [:width :height]))
proportion (/ width height)]
(update-in state [:shapes id] assoc
:proportion proportion
:proportion-lock true))))
(defn lock-proportions
"Mark proportions of the shape locked and save the current
proportion as additional precalculated property."
[id]
{:pre [(uuid? id)]}
(LockShapeProportions. id))
;; TODO: revisit
(deftype UnlockShapeProportions [id]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:shapes id :proportion-lock] false)))
(defn unlock-proportions
[id]
{:pre [(uuid? id)]}
(UnlockShapeProportions. id))
;; --- Update Dimensions ;; --- Update Dimensions
;; TODO: revisit (s/def ::width ::us/number)
(s/def ::height ::us/number)
(s/def ::width (s/and ::us/number ::us/positive))
(s/def ::height (s/and ::us/number ::us/positive))
(s/def ::update-dimensions (s/def ::update-dimensions
(s/keys :opt-un [::width ::height])) (s/keys :opt-un [::width ::height]))
;; TODO: emit commit-changes
(defn update-dimensions (defn update-dimensions
"A helper event just for update the position "A helper event just for update the position
of the shape using the width and height attrs of the shape using the width and height attrs
@ -1098,35 +1103,31 @@
(ptk/reify ::update-dimensions (ptk/reify ::update-dimensions
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update-in state [:shapes id] geom/resize-dim dimensions)))) (update-in state [:workspace-data :shapes-by-id id] geom/resize-dim dimensions))))
;; --- Update Interaction ;; --- Shape Proportions
;; TODO: revisit (defn toggle-shape-proportion-lock
(deftype UpdateInteraction [shape interaction] [id]
ptk/UpdateEvent (ptk/reify ::toggle-shape-proportion-lock
(update [_ state] ptk/UpdateEvent
(let [id (or (:id interaction) (update [_ state]
(uuid/random)) (let [shape (get-in state [:workspace-data :shapes-by-id id])]
data (assoc interaction :id id)] (if (:proportion-lock shape)
(assoc-in state [:shapes shape :interactions id] data)))) (assoc-in state [:workspace-data :shapes-by-id id :proportion-lock] false)
(->> (geom/assign-proportions (assoc shape :proportion-lock true))
(assoc-in state [:workspace-data :shapes-by-id id])))))))
(defn update-interaction ;; --- Update Shape Position
[shape interaction]
(UpdateInteraction. shape interaction))
;; --- Delete Interaction (defn update-position
[id point]
;; TODO: revisit (s/assert ::us/uuid id)
(deftype DeleteInteracton [shape id] (s/assert gpt/point? point)
ptk/UpdateEvent (ptk/reify ::update-position
(update [_ state] ptk/UpdateEvent
(update-in state [:shapes shape :interactions] dissoc id))) (update [_ state]
(update-in state [:workspace-data :shapes-by-id id] geom/absolute-move point))))
(defn delete-interaction
[shape id]
{:pre [(uuid? id) (uuid? shape)]}
(DeleteInteracton. shape id))
;; --- Path Modifications ;; --- Path Modifications

View file

@ -15,7 +15,6 @@
(declare move-rect) (declare move-rect)
(declare move-path) (declare move-path)
(declare move-circle) (declare move-circle)
(declare move-group)
(defn move (defn move
"Move the shape relativelly to its current "Move the shape relativelly to its current
@ -28,18 +27,15 @@
:text (move-rect shape dpoint) :text (move-rect shape dpoint)
:curve (move-path shape dpoint) :curve (move-path shape dpoint)
:path (move-path shape dpoint) :path (move-path shape dpoint)
:circle (move-circle shape dpoint) :circle (move-circle shape dpoint)))
:group (move-group shape dpoint)))
(defn- move-rect (defn- move-rect
"A specialized function for relative movement "A specialized function for relative movement
for rect-like shapes." for rect-like shapes."
[shape {dx :x dy :y}] [shape {dx :x dy :y}]
(assoc shape (assoc shape
:x1 (mth/round (+ (:x1 shape) dx)) :x (mth/round (+ (:x shape) dx))
:y1 (mth/round (+ (:y1 shape) dy)) :y (mth/round (+ (:y shape) dy))))
:x2 (mth/round (+ (:x2 shape) dx))
:y2 (mth/round (+ (:y2 shape) dy))))
(defn- move-circle (defn- move-circle
"A specialized function for relative movement "A specialized function for relative movement
@ -49,14 +45,6 @@
:cx (mth/round (+ (:cx shape) dx)) :cx (mth/round (+ (:cx shape) dx))
:cy (mth/round (+ (:cy shape) dy)))) :cy (mth/round (+ (:cy shape) dy))))
(defn- move-group
"A specialized function for relative movement
for group shapes."
[shape {dx :x dy :y}]
(assoc shape
:dx (mth/round (+ (:dx shape 0) dx))
:dy (mth/round (+ (:dy shape 0) dy))))
(defn- move-path (defn- move-path
"A specialized function for relative movement "A specialized function for relative movement
for path shapes." for path shapes."
@ -71,7 +59,6 @@
(declare absolute-move-rect) (declare absolute-move-rect)
(declare absolute-move-circle) (declare absolute-move-circle)
(declare absolute-move-group)
(defn absolute-move (defn absolute-move
"Move the shape to the exactly specified position." "Move the shape to the exactly specified position."
@ -80,31 +67,24 @@
:icon (absolute-move-rect shape point) :icon (absolute-move-rect shape point)
:image (absolute-move-rect shape point) :image (absolute-move-rect shape point)
:rect (absolute-move-rect shape point) :rect (absolute-move-rect shape point)
:circle (absolute-move-circle shape point) :circle (absolute-move-circle shape point)))
:group (absolute-move-group shape point)))
(defn- absolute-move-rect (defn- absolute-move-rect
"A specialized function for absolute moviment "A specialized function for absolute moviment
for rect-like shapes." for rect-like shapes."
[shape {:keys [x y] :as pos}] [shape {:keys [x y] :as pos}]
(let [dx (if x (- x (:x1 shape)) 0) (let [dx (if x (- x (:x shape)) 0)
dy (if y (- y (:y1 shape)) 0)] dy (if y (- y (:y shape)) 0)]
(move shape (gpt/point dx dy)))) (move shape (gpt/point dx dy))))
(defn- absolute-move-circle (defn- absolute-move-circle
"A specialized function for absolute moviment "A specialized function for absolute moviment
for rect-like shapes." for rect-like shapes."
[shape {:keys [x y] :as pos}] [shape {:keys [x y] :as pos}]
(let [dx (if x (- x(:cx shape)) 0) (let [dx (if x (- x (:cx shape)) 0)
dy (if y (- y (:cy shape)) 0)] dy (if y (- y (:cy shape)) 0)]
(move shape (gpt/point dx dy)))) (move shape (gpt/point dx dy))))
(defn- absolute-move-group
"A specialized function for absolute moviment
for rect-like shapes."
[shape {:keys [x y] :as pos}]
(throw (ex-info "Not implemented (TODO)" {})))
;; --- Rotation ;; --- Rotation
;; TODO: maybe we can consider apply the rotation ;; TODO: maybe we can consider apply the rotation
@ -159,6 +139,30 @@
(merge shape {:width (* rx 2) (merge shape {:width (* rx 2)
:height (* ry 2)})) :height (* ry 2)}))
;; --- Proportions
(declare assign-proportions-path)
(declare assign-proportions-circle)
(declare assign-proportions-rect)
(defn assign-proportions
[{:keys [type] :as shape}]
(case type
:circle (assign-proportions-circle shape)
:path (assign-proportions-path shape)
(assign-proportions-rect shape)))
(defn- assign-proportions-rect
[{:keys [width height] :as shape}]
(assoc shape :proportion (/ width height)))
(defn- assign-proportions-circle
[{:as shape}]
(prn "assign-proportions-circle" shape)
(assoc shape :proportion 1))
;; TODO: implement the rest of shapes
;; --- Paths ;; --- Paths
(defn update-path-point (defn update-path-point
@ -171,20 +175,16 @@
;; --- Setup Proportions ;; --- Setup Proportions
(declare setup-proportions-rect) (declare setup-proportions-const)
(declare setup-proportions-image) (declare setup-proportions-image)
(defn setup-proportions (defn setup-proportions
[shape] [shape]
(case (:type shape) (case (:type shape)
:canvas (setup-proportions-rect shape)
:rect (setup-proportions-rect shape)
:circle (setup-proportions-rect shape)
:icon (setup-proportions-image shape) :icon (setup-proportions-image shape)
:image (setup-proportions-image shape) :image (setup-proportions-image shape)
:text shape :text shape
:curve (setup-proportions-rect shape) (setup-proportions-const shape)))
:path (setup-proportions-rect shape)))
(defn setup-proportions-image (defn setup-proportions-image
[{:keys [metadata] :as shape}] [{:keys [metadata] :as shape}]
@ -193,12 +193,11 @@
:proportion (/ width height) :proportion (/ width height)
:proportion-lock false))) :proportion-lock false)))
(defn setup-proportions-rect (defn setup-proportions-const
[shape] [shape]
(let [{:keys [width height]} (size shape)] (assoc shape
(assoc shape :proportion 1
:proportion (/ width height) :proportion-lock false))
:proportion-lock false)))
;; --- Resize (Dimentsions) ;; --- Resize (Dimentsions)
@ -216,20 +215,19 @@
:circle (resize-dim-circle shape opts))) :circle (resize-dim-circle shape opts)))
(defn- resize-dim-rect (defn- resize-dim-rect
[{:keys [proportion proportion-lock x1 y1] :as shape} [{:keys [proportion proportion-lock x y] :as shape}
{:keys [width height]}] {:keys [width height] :as dimensions}]
{:pre [(not (and width height))]}
(if-not proportion-lock (if-not proportion-lock
(if width (if width
(assoc shape :x2 (+ x1 width)) (assoc shape :width width)
(assoc shape :y2 (+ y1 height))) (assoc shape :height height))
(if width (if width
(-> shape (-> shape
(assoc :x2 (+ x1 width)) (assoc :width width)
(assoc :y2 (+ y1 (/ width proportion)))) (assoc :height (/ width proportion)))
(-> shape (-> shape
(assoc :y2 (+ y1 height)) (assoc :height height)
(assoc :x2 (+ x1 (* height proportion))))))) (assoc :width (* height proportion))))))
(defn- resize-dim-circle (defn- resize-dim-circle
[{:keys [proportion proportion-lock] :as shape} [{:keys [proportion proportion-lock] :as shape}
@ -299,11 +297,11 @@
:bottom-right :bottom-right
(-> (gmt/matrix) (-> (gmt/matrix)
(gmt/translate (+ (:x1 shape)) (gmt/translate (+ (:x shape))
(+ (:y1 shape))) (+ (:y shape)))
(gmt/scale scalex scaley) (gmt/scale scalex scaley)
(gmt/translate (- (:x1 shape)) (gmt/translate (- (:x shape))
(- (:y1 shape)))) (- (:y shape))))
:bottom :bottom
(-> (gmt/matrix) (-> (gmt/matrix)
@ -373,8 +371,8 @@
:height (if lock? (/ width proportion) height))) :height (if lock? (/ width proportion) height)))
:bottom-right :bottom-right
(let [width (- x (:x1 shape)) (let [width (- x (:x shape))
height (- y (:y1 shape)) height (- y (:y shape))
proportion (:proportion shape 1)] proportion (:proportion shape 1)]
(assoc shape (assoc shape
:width width :width width
@ -421,38 +419,39 @@
(defn- setup-rect (defn- setup-rect
"A specialized function for setup rect-like shapes." "A specialized function for setup rect-like shapes."
[shape {:keys [x1 y1 x2 y2]}] [shape {:keys [x y width height]}]
(assoc shape (assoc shape
:x1 x1 :x x
:y1 y1 :y y
:x2 x2 :width width
:y2 y2)) :height height))
(defn- setup-circle (defn- setup-circle
"A specialized function for setup circle shapes." "A specialized function for setup circle shapes."
[shape {:keys [x1 y1 x2 y2]}] [shape {:keys [x y width height]}]
(assoc shape (assoc shape
:cx x1 :cx x
:cy y1 :cy y
:rx (mth/abs (- x2 x1)) :rx (mth/abs width)
:ry (mth/abs (- y2 y1)))) :ry (mth/abs height)))
(defn- setup-image (defn- setup-image
[{:keys [metadata] :as shape} {:keys [x1 y1 x2 y2] :as props}] [{:keys [metadata] :as shape} {:keys [x y width height] :as props}]
(let [{:keys [width height]} metadata] (assoc shape
(assoc shape :x x
:x1 x1 :y y
:y1 y1 :width width
:x2 x2 :height height
:y2 y2 :proportion (/ (:width metadata)
:proportion (/ width height) (:height metadata))
:proportion-lock true))) :proportion-lock true))
;; --- Coerce to Rect-like shape. ;; --- Coerce to Rect-like shape.
(declare circle->rect-shape) (declare circle->rect-shape)
(declare path->rect-shape) (declare path->rect-shape)
(declare group->rect-shape) (declare group->rect-shape)
(declare rect->rect-shape)
(defn shape->rect-shape (defn shape->rect-shape
"Coerce shape to rect like shape." "Coerce shape to rect like shape."
@ -461,7 +460,7 @@
:circle (circle->rect-shape shape) :circle (circle->rect-shape shape)
:path (path->rect-shape shape) :path (path->rect-shape shape)
:curve (path->rect-shape shape) :curve (path->rect-shape shape)
shape)) (rect->rect-shape shape)))
(defn shapes->rect-shape (defn shapes->rect-shape
[shapes] [shapes]
@ -474,29 +473,41 @@
:y1 miny :y1 miny
:x2 maxx :x2 maxx
:y2 maxy :y2 maxy
:x minx
:y miny
:width (- maxx minx)
:height (- maxy miny)
:type :rect})) :type :rect}))
(defn shapes->rect-shape' ;; (defn shapes->rect-shape'
[shapes] ;; [shapes]
(let [shapes (mapv shape->rect-shape shapes) ;; (let [shapes (mapv shape->rect-shape shapes)
total (count shapes)] ;; total (count shapes)]
(loop [idx (int 0) ;; (loop [idx (int 0)
minx js/Number.POSITIVE_INFINITY ;; minx js/Number.POSITIVE_INFINITY
miny js/Number.POSITIVE_INFINITY ;; miny js/Number.POSITIVE_INFINITY
maxx js/Number.NEGATIVE_INFINITY ;; maxx js/Number.NEGATIVE_INFINITY
maxy js/Number.NEGATIVE_INFINITY] ;; maxy js/Number.NEGATIVE_INFINITY]
(if (> total idx) ;; (if (> total idx)
(let [{:keys [x1 y1 x2 y2]} (nth shapes idx)] ;; (let [{:keys [x1 y1 x2 y2]} (nth shapes idx)]
(recur (inc idx) ;; (recur (inc idx)
(min minx x1) ;; (min minx x1)
(min miny y1) ;; (min miny y1)
(max maxx x2) ;; (max maxx x2)
(max maxy y2))) ;; (max maxy y2)))
{:x1 minx ;; {:x1 minx
:y1 miny ;; :y1 miny
:x2 maxx ;; :x2 maxx
:y2 maxy ;; :y2 maxy
:type :rect})))) ;; :type :rect}))))
(defn- rect->rect-shape
[{:keys [x y width height] :as shape}]
(assoc shape
:x1 x
:y1 y
:x2 (+ x width)
:y2 (+ y height)))
(defn- path->rect-shape (defn- path->rect-shape
[{:keys [segments] :as shape}] [{:keys [segments] :as shape}]
@ -508,7 +519,11 @@
:x1 minx :x1 minx
:y1 miny :y1 miny
:x2 maxx :x2 maxx
:y2 maxy))) :y2 maxy
:x minx
:y miny
:width (- maxx minx)
:height (- maxy miny))))
(defn- circle->rect-shape (defn- circle->rect-shape
[{:keys [cx cy rx ry] :as shape}] [{:keys [cx cy rx ry] :as shape}]
@ -520,7 +535,11 @@
:x1 x1 :x1 x1
:y1 y1 :y1 y1
:x2 (+ x1 width) :x2 (+ x1 width)
:y2 (+ y1 height)))) :y2 (+ y1 height)
:x x1
:y y1
:width width
:height height)))
;; --- Transform Shape ;; --- Transform Shape
@ -542,21 +561,23 @@
:circle (transform-circle shape xfmt))) :circle (transform-circle shape xfmt)))
(defn- transform-rect (defn- transform-rect
[{:keys [x1 y1] :as shape} mx] [{:keys [x y width height] :as shape} mx]
(let [{:keys [width height]} (size shape) (let [tl (gpt/transform [x y] mx)
tl (gpt/transform [x1 y1] mx) tr (gpt/transform [(+ x width) y] mx)
tr (gpt/transform [(+ x1 width) y1] mx) bl (gpt/transform [x (+ y height)] mx)
bl (gpt/transform [x1 (+ y1 height)] mx) br (gpt/transform [(+ x width) (+ y height)] mx)
br (gpt/transform [(+ x1 width) (+ y1 height)] mx)
minx (apply min (map :x [tl tr bl br])) minx (apply min (map :x [tl tr bl br]))
maxx (apply max (map :x [tl tr bl br])) maxx (apply max (map :x [tl tr bl br]))
miny (apply min (map :y [tl tr bl br])) miny (apply min (map :y [tl tr bl br]))
maxy (apply max (map :y [tl tr bl br]))] maxy (apply max (map :y [tl tr bl br]))]
(assoc shape (assoc shape
:x1 minx :x minx
:y1 miny :y miny
:x2 (+ minx (- maxx minx)) :width (- maxx minx)
:y2 (+ miny (- maxy miny))))) :height (- maxy miny))))
;; :x2 (+ minx (- maxx minx))
;; :y2 (+ miny (- maxy miny)))))
(defn- transform-circle (defn- transform-circle
[{:keys [cx cy rx ry] :as shape} xfmt] [{:keys [cx cy rx ry] :as shape} xfmt]
@ -585,8 +606,6 @@
;; --- Outer Rect ;; --- Outer Rect
(declare selection-rect-generic)
(defn rotation-matrix (defn rotation-matrix
"Generate a rotation matrix from shape." "Generate a rotation matrix from shape."
[{:keys [x1 y1 rotation] :as shape}] [{:keys [x1 y1 rotation] :as shape}]
@ -607,15 +626,12 @@
(defn selection-rect (defn selection-rect
"Return the selection rect for the shape." "Return the selection rect for the shape."
([shape] [shape]
(selection-rect @st/state shape)) (let [modifier (:modifier-mtx shape)]
([state shape] (-> (shape->rect-shape shape)
(let [modifier (:modifier-mtx shape)] (assoc :type :rect :id (:id shape))
(-> (shape->rect-shape shape) (transform (or modifier (gmt/matrix)))
(assoc :type :rect :id (:id shape)) #_(rotate-shape))))
(transform (or modifier (gmt/matrix)))
(rotate-shape)
(size)))))
;; --- Helpers ;; --- Helpers
@ -623,8 +639,8 @@
"Check if a shape is contained in the "Check if a shape is contained in the
provided selection rect." provided selection rect."
[shape selrect] [shape selrect]
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} selrect (let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (shape->rect-shape selrect)
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} shape] {rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (shape->rect-shape shape)]
(and (neg? (- sy1 ry1)) (and (neg? (- sy1 ry1))
(neg? (- sx1 rx1)) (neg? (- sx1 rx1))
(pos? (- sy2 ry2)) (pos? (- sy2 ry2))
@ -633,8 +649,8 @@
(defn overlaps? (defn overlaps?
"Check if a shape overlaps with provided selection rect." "Check if a shape overlaps with provided selection rect."
[shape selrect] [shape selrect]
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} selrect (let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (shape->rect-shape selrect)
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} shape] {rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (shape->rect-shape shape)]
(and (< rx1 sx2) (and (< rx1 sx2)
(> rx2 sx1) (> rx2 sx1)
(< ry1 sy2) (< ry1 sy2)

View file

@ -80,33 +80,8 @@
"ds.project.placeholder" "New project name" "ds.project.placeholder" "New project name"
"ds.project.new" "New project" "ds.project.new" "New project"
"ds.radius" "Radius" "common.accept" "Accept"
"ds.size" "Size" "common.cancel" "Cancel"
"ds.width" "Width"
"ds.height" "Height"
"ds.style" "Style"
"ds.none" "None"
"ds.solid" "Solid"
"ds.dotted" "Dotted"
"ds.dashed" "Dashed"
"ds.mixed" "Mixed"
"ds.position" "Position"
"ds.rotation" "Rotation"
"ds.opacity" "Opacity"
"ds.color" "Color"
"ds.background-color" "Background color"
"ds.font-family" "Font family"
"ds.size-weight" "Size and Weight"
"ds.font-size" "Font Size"
"ds.line-height-letter-spacing" "Line height and Letter spacing"
"ds.line-height" "Line height"
"ds.letter-spacing" "Letter spacing"
"ds.text-align" "Text align"
"ds.name" "Name"
"ds.go" "Go go go!"
"ds.accept" "Accept"
"ds.cancel" "Cancel"
"ds.settings.icons" "Icons" "ds.settings.icons" "Icons"
"ds.settings.element-options" "Element options" "ds.settings.element-options" "Element options"
@ -122,43 +97,69 @@
"ds.history.versions" "History" "ds.history.versions" "History"
"ds.history.pinned" "Pinned" "ds.history.pinned" "Pinned"
"ds.help.rect" "Box (Ctrl + B)" "workspace.header.rect" "Box (Ctrl + B)"
"ds.help.circle" "Circle (Ctrl + E)" "workspace.header.circle" "Circle (Ctrl + E)"
"ds.help.line" "Line (Ctrl + L)" "workspace.header.line" "Line (Ctrl + L)"
"ds.help.text" "Text" "workspace.header.text" "Text"
"ds.help.path" "Path" "workspace.header.path" "Path"
"ds.help.curve" "Curve" "workspace.header.curve" "Curve"
"ds.help.ruler" "Ruler" "workspace.header.ruler" "Ruler"
"ds.help.canvas" "Canvas" "workspace.header.canvas" "Canvas"
"ds.user.profile" "Profile" "ds.user.profile" "Profile"
"ds.user.password" "Password" "ds.user.password" "Password"
"ds.user.notifications" "Notifications" "ds.user.notifications" "Notifications"
"ds.user.exit" "Exit" "ds.user.exit" "Exit"
"header.sitemap" "Sitemap (Ctrl + Shift + M)" "workspace.header.sitemap" "Sitemap (Ctrl + Shift + M)"
"header.draw-tools" "Draw tools (Ctrl + Shift + S)" "workspace.header.draw-tools" "Draw tools (Ctrl + Shift + S)"
"header.color-palette" "Color Palette (---)" "workspace.header.color-palette" "Color Palette (---)"
"header.icons" "Icons (Ctrl + Shift + I)" "workspace.header.icons" "Icons (Ctrl + Shift + I)"
"header.layers" "Layers (Ctrl + Shift + L)" "workspace.header.layers" "Layers (Ctrl + Shift + L)"
"header.element-options" "Element options (Ctrl + Shift + O)" "workspace.header.element-options" "Element options (Ctrl + Shift + O)"
"header.document-history" "History (Ctrl + Shift + H)" "workspace.header.document-history" "History (Ctrl + Shift + H)"
"header.undo" "Undo (Ctrl + Z)" "workspace.header.undo" "Undo (Ctrl + Z)"
"header.redo" "Redo (Ctrl + Shift + Z)" "workspace.header.redo" "Redo (Ctrl + Shift + Z)"
"header.download" "Download (Ctrl + E)" "workspace.header.download" "Download (Ctrl + E)"
"header.image" "Image (Ctrl + I)" "workspace.header.image" "Image (Ctrl + I)"
"header.rules" "Rules" "workspace.header.rules" "Rules"
"header.grid" "Grid (Ctrl + G)" "workspace.header.grid" "Grid (Ctrl + G)"
"header.grid-snap" "Snap to grid" "workspace.header.grid-snap" "Snap to grid"
"header.align" "Align (Ctrl + A)" "workspace.header.align" "Align (Ctrl + A)"
"header.view-mode" "View mode (Ctrl + P)" "workspace.header.view-mode" "View mode (Ctrl + P)"
"workspace.options.radius" "Radius"
"workspace.options.size" "Size"
"workspace.options.width" "Width"
"workspace.options.height" "Height"
"workspace.options.stroke.style" "Style"
"workspace.options.stroke.none" "None"
"workspace.options.stroke.solid" "Solid"
"workspace.options.stroke.dotted" "Dotted"
"workspace.options.stroke.dashed" "Dashed"
"workspace.options.stroke.mixed" "Mixed"
"workspace.options.position" "Position"
"workspace.options.rotation" "Rotation"
"workspace.options.opacity" "Opacity"
"workspace.options.color" "Color"
"workspace.options.background-color" "Background color"
"workspace.options.font-family" "Font family"
"workspace.options.size-weight" "Size and Weight"
"workspace.options.font-size" "Font Size"
"workspace.options.line-height-letter-spacing" "Line height and Letter spacing"
"workspace.options.line-height" "Line height"
"workspace.options.letter-spacing" "Letter spacing"
"workspace.options.text-align" "Text align"
"workspace.options.name" "Name"
"workspace.options.go" "Go go go!"
"workspace.options.measures" "Size, position & rotation"
"workspace.options.rotation-radius" "Rotation & Radius"
"element.measures" "Size, position & rotation"
"element.fill" "Fill" "element.fill" "Fill"
"element.stroke" "Stroke" "workspace.options.stroke" "Stroke"
"element.text" "Text" "element.text" "Text"
"element.interactions" "Interactions" "element.interactions" "Interactions"
"element.page-measures" "Page settings" "workspace.options.page-measures" "Page settings"
"element.page-grid-options" "Grid settings" "element.page-grid-options" "Grid settings"
"image.new" "New image" "image.new" "New image"

View file

@ -82,9 +82,6 @@
;; Something else ;; Something else
:else :else
(do (do
(js/console.error "Unhandled Error:"
"\n - message:" (ex-message error)
"\n - data:" (pr-str (ex-data error)))
(js/console.error error) (js/console.error error)
(ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic"))))))) (ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic")))))))

View file

@ -17,28 +17,28 @@
id (when (uuid-str? id) (uuid id))] id (when (uuid-str? id) (uuid id))]
[:main.dashboard-main [:main.dashboard-main
[:& messages-widget] [:& messages-widget]
[:& header {:section :dashboard/projects}] [:& header {:section :dashboard-projects}]
[:& projects/projects-page {:id id}]])) [:& projects/projects-page {:id id}]]))
(mf/defc dashboard-assets (mf/defc dashboard-assets
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [section (:name route) (let [section (get-in route [:data :name])
{:keys [id type]} (get-in route [:params :query]) {:keys [id type]} (get-in route [:params :query])
id (cond id (cond
;; (str/digits? id) (parse-int id) ;; (str/digits? id) (parse-int id)
(uuid-str? id) (uuid id) (uuid-str? id) (uuid id)
(str/empty-or-nil? id) nil (str/empty-or-nil? id) nil
:else id) :else id)
type (when (str/alpha? type) (keyword type))] type (if (str/alpha? type) (keyword type) :own)]
[:main.dashboard-main [:main.dashboard-main
[:& messages-widget] [:& messages-widget]
[:& header {:section section}] [:& header {:section section}]
(case section (case section
:dashboard/icons :dashboard-icons
[:& icons/icons-page {:type type :id id}] [:& icons/icons-page {:type type :id id}]
:dashboard/images :dashboard-images
[:& images/images-page {:type type :id id}] [:& images/images-page {:type type :id id}]
:dashboard/colors :dashboard-colors
[:& colors/colors-page {:type type :id id}])])) [:& colors/colors-page {:type type :id id}])]))

View file

@ -61,7 +61,7 @@
(delete [] (delete []
(st/emit! (st/emit!
(dc/delete-collection (:id coll)) (dc/delete-collection (:id coll))
(rt/nav :dashboard/colors nil {:type (:type coll)}))) (rt/nav :dashboard-colors nil {:type (:type coll)})))
(on-delete [] (on-delete []
(modal/show! confirm-dialog {:on-accept delete}))] (modal/show! confirm-dialog {:on-accept delete}))]
@ -79,7 +79,7 @@
editable? (= type :own)] editable? (= type :own)]
(letfn [(on-click [event] (letfn [(on-click [event]
(let [type (or type :own)] (let [type (or type :own)]
(st/emit! (rt/nav :dashboard/colors nil {:type type :id id})))) (st/emit! (rt/nav :dashboard-colors nil {:type type :id id}))))
(on-input-change [event] (on-input-change [event]
(let [value (dom/get-target event) (let [value (dom/get-target event)
value (dom/get-value value)] value (dom/get-value value)]
@ -107,14 +107,14 @@
:on-key-down on-input-keyup}] :on-key-down on-input-keyup}]
[:span.close {:on-click on-cancel} i/close]] [:span.close {:on-click on-cancel} i/close]]
[:span.element-title name]) [:span.element-title name])
[:span.element-subtitle #_[:span.element-subtitle
(tr "ds.num-elements" (t/c colors))]]))) (tr "ds.num-elements" (t/c colors))]])))
(mf/defc nav (mf/defc nav
[{:keys [id type colls selected-coll] :as props}] [{:keys [id type colls selected-coll] :as props}]
(let [own? (= type :own) (let [own? (= type :own)
builtin? (= type :builtin) builtin? (= type :builtin)
select-tab #(st/emit! (rt/nav :dashboard/colors nil {:type %}))] select-tab #(st/emit! (rt/nav :dashboard-colors nil {:type %}))]
[:div.library-bar [:div.library-bar
[:div.library-bar-inside [:div.library-bar-inside
[:ul.library-tabs [:ul.library-tabs
@ -259,7 +259,8 @@
[{:keys [id type coll] :as props}] [{:keys [id type coll] :as props}]
(let [selected (mf/deref selected-colors-iref)] (let [selected (mf/deref selected-colors-iref)]
[:section.dashboard-grid.library [:section.dashboard-grid.library
[:& grid-header {:coll coll}] (when coll
[:& grid-header {:coll coll}])
[:& grid {:coll coll :id id :type type :selected selected}] [:& grid {:coll coll :id id :type type :selected selected}]
(when (seq selected) (when (seq selected)
[:& grid-options {:id id :type type [:& grid-options {:id id :type type
@ -282,14 +283,12 @@
(first colls)) (first colls))
id (:id selected-coll)] id (:id selected-coll)]
(mf/use-effect #(st/emit! (dc/initialize)) #js [id type])
(mf/use-effect #(st/emit! (dc/fetch-collections))) (mf/use-effect #(st/emit! (dc/fetch-collections)))
[:section.dashboard-content [:section.dashboard-content
[:& nav {:type type [:& nav {:type type
:id id :id id
:colls colls :colls colls}]
:selected-coll selected-coll}]
[:& content {:type type [:& content {:type type
:id id :id id
:coll selected-coll}]])) :coll selected-coll}]]))

View file

@ -25,26 +25,26 @@
(mf/defc header (mf/defc header
[{:keys [section] :as props}] [{:keys [section] :as props}]
(let [projects? (= section :dashboard/projects) (let [projects? (= section :dashboard-projects)
icons? (= section :dashboard/icons) icons? (= section :dashboard-icons)
images? (= section :dashboard/images) images? (= section :dashboard-images)
colors? (= section :dashboard/colors)] colors? (= section :dashboard-colors)]
[:header#main-bar.main-bar [:header#main-bar.main-bar
[:div.main-logo [:div.main-logo
[:& header-link {:section :dashboard/projects [:& header-link {:section :dashboard-projects
:content i/logo}]] :content i/logo}]]
[:ul.main-nav [:ul.main-nav
[:li {:class (when projects? "current")} [:li {:class (when projects? "current")}
[:& header-link {:section :dashboard/projects [:& header-link {:section :dashboard-projects
:content (tr "ds.projects")}]] :content (tr "ds.projects")}]]
[:li {:class (when icons? "current")} [:li {:class (when icons? "current")}
[:& header-link {:section :dashboard/icons [:& header-link {:section :dashboard-icons
:content (tr "ds.icons")}]] :content (tr "ds.icons")}]]
[:li {:class (when images? "current")} [:li {:class (when images? "current")}
[:& header-link {:section :dashboard/images [:& header-link {:section :dashboard-images
:content (tr "ds.images")}]] :content (tr "ds.images")}]]
[:li {:class (when colors? "current")} [:li {:class (when colors? "current")}
[:& header-link {:section :dashboard/colors [:& header-link {:section :dashboard-colors
:content (tr "ds.colors")}]]] :content (tr "ds.colors")}]]]
[:& user]])) [:& user]]))

View file

@ -79,29 +79,14 @@
;; --- Nav ;; --- Nav
(defn- make-num-icons-iref
[id]
(letfn [(selector [icons]
(->> (vals icons)
(filter #(= id (:collection-id %)))
(count)))]
(-> (comp (l/key :icons)
(l/lens selector))
(l/derive st/state))))
(mf/defc nav-item (mf/defc nav-item
[{:keys [coll selected?] :as props}] [{:keys [coll selected?] :as props}]
(let [local (mf/use-state {}) (let [local (mf/use-state {})
{:keys [id type name num-icons]} coll {:keys [id type name]} coll
;; TODO: recalculate the num-icons on crud operations for
;; avod doing this on UI.
;; num-icons-iref (mf/use-memo {:deps #js [id]
;; :fn #(make-num-icons-iref (:id coll))})
;; num-icons (mf/deref num-icons-iref)
editable? (= type :own)] editable? (= type :own)]
(letfn [(on-click [event] (letfn [(on-click [event]
(let [type (or type :own)] (let [type (or type :own)]
(st/emit! (rt/nav :dashboard/icons {} {:type type :id id})))) (st/emit! (rt/nav :dashboard-icons {} {:type type :id id}))))
(on-input-change [event] (on-input-change [event]
(-> (dom/get-target event) (-> (dom/get-target event)
(dom/get-value) (dom/get-value)
@ -127,15 +112,14 @@
:on-change on-input-change :on-change on-input-change
:on-key-down on-input-keyup}] :on-key-down on-input-keyup}]
[:span.close {:on-click on-cancel} i/close]] [:span.close {:on-click on-cancel} i/close]]
[:span.element-title (if id name "Storage")]) [:span.element-title (if id name "Storage")])])))
[:span.element-subtitle (tr "ds.num-elements" (t/c num-icons))]])))
(mf/defc nav (mf/defc nav
[{:keys [id type colls selected-coll] :as props}] [{:keys [id type colls selected-coll] :as props}]
(let [own? (= type :own) (let [own? (= type :own)
builtin? (= type :builtin) builtin? (= type :builtin)
select-tab #(st/emit! (rt/nav :dashboard/icons nil {:type %}))] select-tab #(st/emit! (rt/nav :dashboard-icons nil {:type %}))]
[:div.library-bar [:div.library-bar
[:div.library-bar-inside [:div.library-bar-inside
[:ul.library-tabs [:ul.library-tabs
@ -350,54 +334,12 @@
:selected (contains? (:selected opts) (:id icon)) :selected (contains? (:selected opts) (:id icon))
:edition? (= (:edition opts) (:id icon))}])]]])) :edition? (= (:edition opts) (:id icon))}])]]]))
;; --- Menu
(mf/defc menu
[{:keys [opts coll] :as props}]
(let [ordering (:order opts :name)
filtering (:filter opts "")
icount (count (:icons coll))]
(letfn [(on-term-change [event]
(let [term (-> (dom/get-target event)
(dom/get-value))]
(st/emit! (di/update-opts :filter term))))
(on-ordering-change [event]
(let [value (dom/event->value event)
value (read-string value)]
(st/emit! (di/update-opts :order value))))
(on-clear [event]
(st/emit! (di/update-opts :filter "")))]
[:section.dashboard-bar.library-gap
[:div.dashboard-info
;; Counter
[:span.dashboard-icons (tr "ds.num-icons" (t/c icount))]
;; Sorting
[:div
[:span (tr "ds.ordering")]
[:select.input-select {:on-change on-ordering-change
:value (pr-str ordering)}
(for [[key value] (seq +ordering-options+)]
[:option {:key key :value (pr-str key)} (tr value)])]]
;; Search
[:form.dashboard-search
[:input.input-text {:key :icons-search-box
:type "text"
:on-change on-term-change
:auto-focus true
:placeholder (tr "ds.search.placeholder")
:value filtering}]
[:div.clear-search {:on-click on-clear} i/close]]]])))
;; --- Content ;; --- Content
(mf/defc content (mf/defc content
[{:keys [id type coll] :as props}] [{:keys [id type coll] :as props}]
(let [opts (mf/deref opts-iref)] (let [opts (mf/deref opts-iref)]
[:* [:*
[:& menu {:opts opts :coll coll}]
[:section.dashboard-grid.library [:section.dashboard-grid.library
(when coll (when coll
[:& grid-header {:coll coll}]) [:& grid-header {:coll coll}])
@ -425,15 +367,13 @@
:else (first colls)) :else (first colls))
id (:id selected-coll)] id (:id selected-coll)]
(mf/use-effect #(st/emit! di/fetch-collections)) (mf/use-effect #(st/emit! di/fetch-collections))
(mf/use-effect {:fn #(st/emit! di/initialize (mf/use-effect {:fn #(st/emit! (di/fetch-icons id))
(di/fetch-icons id)) :deps #js [(str id)]})
:deps #js [id type]})
[:section.dashboard-content [:section.dashboard-content
[:& nav {:type type [:& nav {:type type
:id id :id id
:colls colls :colls colls}]
:selected-coll selected-coll}]
[:& content {:type type [:& content {:type type
:id id :id id
:coll selected-coll}]])) :coll selected-coll}]]))

View file

@ -26,30 +26,6 @@
[uxbox.util.router :as rt] [uxbox.util.router :as rt]
[uxbox.util.time :as dt])) [uxbox.util.time :as dt]))
;; --- Helpers & Constants
(def +ordering-options+
{:name "ds.ordering.by-name"
:created "ds.ordering.by-creation-date"})
(defn- sort-images-by
[ordering images]
(case ordering
:name (sort-by :name images)
:created (reverse (sort-by :created-at images))
images))
(defn- contains-term?
[phrase term]
(let [term (name term)]
(str/includes? (str/lower phrase) (str/trim (str/lower term)))))
(defn- filter-images-by
[term images]
(if (str/blank? term)
images
(filter #(contains-term? (:name %) term) images)))
;; --- Refs ;; --- Refs
(def collections-iref (def collections-iref
@ -80,61 +56,41 @@
;; --- Nav ;; --- Nav
(defn- make-num-images-iref
[id]
(letfn [(selector [images]
(->> (vals images)
(filter #(= id (:collection-id %)))
(count)))]
(-> (comp (l/key :images)
(l/lens selector))
(l/derive st/state))))
(mf/defc nav-item (mf/defc nav-item
[{:keys [coll selected?] :as props}] [{:keys [coll selected?] :as props}]
(let [local (mf/use-state {}) (let [local (mf/use-state {})
{:keys [id type name num-images]} coll {:keys [id type name num-images]} coll
;; TODO: recalculate the num-images on crud operations for editable? (= type :own)
;; avod doing this on UI. on-click
num-images-iref (mf/use-memo #(make-num-images-iref (:id coll)) #js [id]) (fn [event]
num-images (mf/deref num-images-iref) (let [type (or type :own)]
editable? (= type :own)] (st/emit! (rt/nav :dashboard-images {} {:type type :id id}))))
(letfn [(on-click [event]
(let [type (or type :own)] on-cancel-edition #(swap! local dissoc :edit)
(st/emit! (rt/nav :dashboard/images {} {:type type :id id})))) on-double-click #(when editable? (swap! local assoc :edit true))
(on-input-change [event] on-input-keyup
(-> (dom/get-target event) (fn [event]
(dom/get-value) (when (kbd/enter? event)
(swap! local assoc :name))) (let [value (-> (dom/get-target event)
(on-cancel [event] (dom/get-value)
(swap! local dissoc :name :edit)) (str/trim))]
(on-double-click [event] (st/emit! (di/rename-collection id value))
(when editable? (swap! local assoc :edit false))))]
(swap! local assoc :edit true))) [:li {:on-click on-click
(on-input-keyup [event] :on-double-click on-double-click
(when (kbd/enter? event) :class-name (when selected? "current")}
(let [value (-> (dom/get-target event) (dom/get-value))] (if (:edit @local)
(st/emit! (di/rename-collection id (str/trim (:name @local)))) [:div
(swap! local assoc :edit false))))] [:input.element-title {:default-value name
[:li {:on-click on-click :on-key-down on-input-keyup}]
:on-double-click on-double-click [:span.close {:on-click on-cancel-edition} i/close]]
:class-name (when selected? "current")} [:span.element-title (if id name "Storage")])]))
(if (:edit @local)
[:div
[:input.element-title {:value (if (:name @local)
(:name @local)
(if id name "Storage"))
:on-change on-input-change
:on-key-down on-input-keyup}]
[:span.close {:on-click on-cancel} i/close]]
[:span.element-title (if id name "Storage")])
[:span.element-subtitle (tr "ds.num-elements" (t/c num-images))]])))
(mf/defc nav (mf/defc nav
[{:keys [id type colls selected-coll] :as props}] [{:keys [id type colls] :as props}]
(let [own? (= type :own) (let [own? (= type :own)
builtin? (= type :builtin) builtin? (= type :builtin)
select-tab #(st/emit! (rt/nav :dashboard/images nil {:type %}))] select-tab #(st/emit! (rt/nav :dashboard-images nil {:type %}))]
[:div.library-bar [:div.library-bar
[:div.library-bar-inside [:div.library-bar-inside
[:ul.library-tabs [:ul.library-tabs
@ -148,7 +104,7 @@
[:ul.library-elements [:ul.library-elements
(when own? (when own?
[:li [:li
[:a.btn-primary {:on-click #(st/emit! (di/create-collection))} [:a.btn-primary {:on-click #(st/emit! di/create-collection)}
(tr "ds.images-collection.new")]]) (tr "ds.images-collection.new")]])
(when own? (when own?
[:& nav-item {:selected? (nil? id)}]) [:& nav-item {:selected? (nil? id)}])
@ -168,7 +124,7 @@
colls (->> (vals colls) colls (->> (vals colls)
(filter #(= :own (:type %))) (filter #(= :own (:type %)))
(remove #(= selected (:id %))) (remove #(= selected (:id %)))
(sort-by :name colls)) #_(sort-by :name colls))
on-select (fn [event id] on-select (fn [event id]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
@ -306,20 +262,21 @@
;; --- Grid ;; --- Grid
(defn- make-images-iref (defn- make-images-iref
[id] [id type]
(-> (comp (l/key :images) (letfn [(selector-fn [state]
(l/lens (fn [images] (let [images (vals (:images state))]
(->> (vals images) (filterv #(= id (:collection-id %)) images)))]
(filter #(= id (:collection-id %))))))) (-> (l/lens selector-fn)
(l/derive st/state))) (l/derive st/state))))
(mf/defc grid (mf/defc grid
[{:keys [id type coll opts] :as props}] [{:keys [id type coll opts] :as props}]
(let [editable? (or (= type :own) (nil? id)) (let [editable? (or (= type :own) (nil? id))
images-iref (mf/use-memo #(make-images-iref id) #js [id]) images-iref (mf/use-memo
{:fn #(make-images-iref id type)
:deps (mf/deps id type)})
images (->> (mf/deref images-iref) images (->> (mf/deref images-iref)
(filter-images-by (:filter opts "")) (sort-by :created-at))]
(sort-images-by (:order opts :name)))]
[:div.dashboard-grid-content [:div.dashboard-grid-content
[:div.dashboard-grid-row [:div.dashboard-grid-row
(when editable? (when editable?
@ -332,53 +289,53 @@
;; --- Menu ;; --- Menu
(mf/defc menu ;; (mf/defc menu
[{:keys [opts coll] :as props}] ;; [{:keys [opts coll] :as props}]
(let [ordering (:order opts :name) ;; (let [ordering (:order opts :name)
filtering (:filter opts "") ;; filtering (:filter opts "")
icount (count (:images coll))] ;; icount (count (:images coll))]
(letfn [(on-term-change [event] ;; (letfn [(on-term-change [event]
(let [term (-> (dom/get-target event) ;; (let [term (-> (dom/get-target event)
(dom/get-value))] ;; (dom/get-value))]
(st/emit! (di/update-opts :filter term)))) ;; (st/emit! (di/update-opts :filter term))))
(on-ordering-change [event] ;; (on-ordering-change [event]
(let [value (dom/event->value event) ;; (let [value (dom/event->value event)
value (read-string value)] ;; value (read-string value)]
(st/emit! (di/update-opts :order value)))) ;; (st/emit! (di/update-opts :order value))))
(on-clear [event] ;; (on-clear [event]
(st/emit! (di/update-opts :filter "")))] ;; (st/emit! (di/update-opts :filter "")))]
[:section.dashboard-bar.library-gap ;; [:section.dashboard-bar.library-gap
[:div.dashboard-info ;; [:div.dashboard-info
;; Counter ;; ;; Counter
[:span.dashboard-images (tr "ds.num-images" (t/c icount))] ;; [:span.dashboard-images (tr "ds.num-images" (t/c icount))]
;; Sorting ;; ;; Sorting
[:div ;; [:div
[:span (tr "ds.ordering")] ;; [:span (tr "ds.ordering")]
[:select.input-select {:on-change on-ordering-change ;; [:select.input-select {:on-change on-ordering-change
:value (pr-str ordering)} ;; :value (pr-str ordering)}
(for [[key value] (seq +ordering-options+)] ;; (for [[key value] (seq +ordering-options+)]
[:option {:key key :value (pr-str key)} (tr value)])]] ;; [:option {:key key :value (pr-str key)} (tr value)])]]
;; Search ;; ;; Search
[:form.dashboard-search ;; [:form.dashboard-search
[:input.input-text {:key :images-search-box ;; [:input.input-text {:key :images-search-box
:type "text" ;; :type "text"
:on-change on-term-change ;; :on-change on-term-change
:auto-focus true ;; :auto-focus true
:placeholder (tr "ds.search.placeholder") ;; :placeholder (tr "ds.search.placeholder")
:value filtering}] ;; :value filtering}]
[:div.clear-search {:on-click on-clear} i/close]]]]))) ;; [:div.clear-search {:on-click on-clear} i/close]]]])))
(mf/defc content (mf/defc content
[{:keys [id type coll] :as props}] [{:keys [id type coll] :as props}]
(let [opts (mf/deref opts-iref)] (let [opts (mf/deref opts-iref)]
[:* [:*
[:& menu {:opts opts :coll coll}]
[:section.dashboard-grid.library [:section.dashboard-grid.library
(when coll (when coll
[:& grid-header {:coll coll}]) [:& grid-header {:coll coll}])
[:& grid {:id id [:& grid {:id id
:type type :type type
:coll coll :coll coll
@ -390,28 +347,26 @@
(mf/defc images-page (mf/defc images-page
[{:keys [id type] :as props}] [{:keys [id type] :as props}]
(let [type (or type :own) (let [colls (mf/deref collections-iref)
colls (mf/deref collections-iref)
colls (cond->> (vals colls) colls (cond->> (vals colls)
(= type :own) (filter #(= :own (:type %))) (= type :own) (filter #(= :own (:type %)))
(= type :builtin) (filter #(= :builtin (:type %))) (= type :builtin) (filter #(= :builtin (:type %)))
true (sort-by :created-at)) true (sort-by :created-at))
selected-coll (cond
(and (= type :own) (nil? id)) nil
(uuid? id) (seek #(= id (:id %)) colls)
:else (first colls))
id (:id selected-coll)]
(mf/use-effect #(st/emit! (di/fetch-collections))) coll (cond
(mf/use-effect {:fn #(st/emit! (di/initialize) (and (= type :own) (nil? id)) nil
(di/fetch-images id)) (uuid? id) (seek #(= id (:id %)) colls)
:deps #js [id type]}) :else (first colls))
id (:id coll)]
(mf/use-effect #(st/emit! di/fetch-collections))
(mf/use-effect {:fn #(st/emit! (di/fetch-images (:id coll)))
:deps #js [(str (:id coll))]})
[:section.dashboard-content [:section.dashboard-content
[:& nav {:type type [:& nav {:type type
:id id :id id
:colls colls :colls colls}]
:selected-coll selected-coll}]
[:& content {:type type [:& content {:type type
:id id :id id
:coll selected-coll}]])) :coll coll}]]))

View file

@ -64,60 +64,6 @@
files files
(filter #(contains-term? (:name %) term) files))) (filter #(contains-term? (:name %) term) files)))
;; --- Menu (Filter & Sort)
(mf/defc menu
[{:keys [id opts files] :as props}]
(let [ordering (:order opts :modified)
filtering (:filter opts "")
on-term-change
(fn [event]
(let [term (-> (dom/get-target event)
(dom/get-value))]
(st/emit! (udp/update-opts :filter term))))
on-order-change
(fn [event]
(let [value (dom/event->value event)
value (read-string value)]
(st/emit! (udp/update-opts :order value))))
on-clear
(fn [event]
(st/emit! (udp/update-opts :filter "")))]
[:section.dashboard-bar.library-gap
[:div.dashboard-info
;; Counter
[:span.dashboard-images (tr "ds.num-files" (t/c (count files)))]
[:div
;; Sorting
;; TODO: convert to separate component?
(when id
[:*
[:span (tr "ds.ordering")]
[:select.input-select {:on-change on-order-change
:value (pr-str ordering)}
(for [[key value] (seq +ordering-options+)]
(let [key (pr-str key)]
[:option {:key key :value key} (tr value)]))]])]
;; Search
;; TODO: convert to separate component?
; [:form.dashboard-search
; [:input.input-text
; {:key :images-search-box
; :type "text"
; :on-change on-term-change
; :auto-focus true
; :placeholder (tr "ds.search.placeholder")
; :value (or filtering "")}]
; [:div.clear-search {:on-click on-clear} i/close]]
]]))
;; --- Grid Item Thumbnail ;; --- Grid Item Thumbnail
(mf/defc grid-item-thumbnail (mf/defc grid-item-thumbnail
@ -164,9 +110,9 @@
;; [:div.project-th-icon.pages ;; [:div.project-th-icon.pages
;; i/page ;; i/page
;; #_[:span (:total-pages project)]] ;; #_[:span (:total-pages project)]]
#_[:div.project-th-icon.comments ;; [:div.project-th-icon.comments
i/chat ;; i/chat
[:span "0"]] ;; [:span "0"]]
[:div.project-th-icon.edit [:div.project-th-icon.edit
{:on-click on-edit} {:on-click on-edit}
i/pencil] i/pencil]
@ -177,7 +123,7 @@
;; --- Grid ;; --- Grid
(mf/defc grid (mf/defc grid
[{:keys [opts files] :as props}] [{:keys [id opts files] :as props}]
(let [order (:order opts :modified) (let [order (:order opts :modified)
filter (:filter opts "") filter (:filter opts "")
files (->> files files (->> files
@ -185,15 +131,13 @@
(sort-by order)) (sort-by order))
on-click #(do on-click #(do
(dom/prevent-default %) (dom/prevent-default %)
#_(modal/show! create-project-dialog {}) (st/emit! (udp/create-file {:project-id id})))]
#_(udl/open! :create-project))
]
[:section.dashboard-grid [:section.dashboard-grid
;; [:h2 (tr "ds.projects.file-name")]
[:div.dashboard-grid-content [:div.dashboard-grid-content
[:div.dashboard-grid-row [:div.dashboard-grid-row
[:div.grid-item.add-project #_{:on-click on-click} (when id
[:span (tr "ds.new-file")]] [:div.grid-item.add-project {:on-click on-click}
[:span (tr "ds.new-file")]])
(for [item files] (for [item files]
[:& grid-item {:file item :key (:id item)}])]]])) [:& grid-item {:file item :key (:id item)}])]]]))
@ -245,10 +189,6 @@
:placeholder (tr "ds.search.placeholder")}] :placeholder (tr "ds.search.placeholder")}]
[:div.clear-search i/close]] [:div.clear-search i/close]]
[:ul.library-elements [:ul.library-elements
; [:li
; [:a.btn-primary #_{:on-click #(st/emit! di/create-collection)}
; "new project +"]]
[:li.recent-projects {:on-click #(st/emit! (udp/go-to-project nil)) [:li.recent-projects {:on-click #(st/emit! (udp/go-to-project nil))
:class-name (when (nil? id) "current")} :class-name (when (nil? id) "current")}
[:span.element-title "Recent"]] [:span.element-title "Recent"]]
@ -280,10 +220,8 @@
[{:keys [id] :as props}] [{:keys [id] :as props}]
(let [opts (mf/deref opts-iref) (let [opts (mf/deref opts-iref)
files (mf/deref files-ref)] files (mf/deref files-ref)]
[:* [:section.dashboard-grid.library
#_[:& menu {:id id :opts opts :files files}] [:& grid {:id id :opts opts :files files}]]))
[:section.dashboard-grid.library
[:& grid {:id id :opts opts :files files}]]]))
;; --- Projects Page ;; --- Projects Page

View file

@ -4,7 +4,43 @@
;; ;;
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.shapes.attrs) (ns uxbox.main.ui.shapes.attrs
(:require [cuerdas.core :as str]))
;; (defn camel-case
;; "Returns camel case version of the key, e.g. :http-equiv becomes :httpEquiv."
;; [k]
;; (if (or (keyword? k)
;; (string? k)
;; (symbol? k))
;; (let [[first-word & words] (str/split (name k) #"-")]
;; (if (or (empty? words)
;; (= "aria" first-word)
;; (= "data" first-word))
;; k
;; (-> (map str/capital words)
;; (conj first-word)
;; str/join
;; keyword)))
;; k))
(defn- process-key
[k]
(if (keyword? k)
(cond
(keyword-identical? k :stroke-color) :stroke
(keyword-identical? k :fill-color) :fill
(str/includes? (name k) "-") (str/camel k)
:else k)))
(defn- process-attrs
[m]
(persistent!
(reduce-kv (fn [m k v]
(assoc! m (process-key k) v))
(transient {})
m)))
(def shape-style-attrs (def shape-style-attrs
#{:fill-color #{:fill-color
@ -17,12 +53,6 @@
:rx :rx
:ry}) :ry})
(def shape-default-attrs
{:stroke-color "#000000"
:stroke-opacity 1
:fill-color "#000000"
:fill-opacity 1})
(defn- stroke-type->dasharray (defn- stroke-type->dasharray
[style] [style]
(case style (case style
@ -30,24 +60,23 @@
:dotted "5,5" :dotted "5,5"
:dashed "10,10")) :dashed "10,10"))
(defn- rename-attr ;; (defn- rename-attr
[[key value :as pair]] ;; [[key value :as pair]]
(case key ;; (case key
:stroke-color [:stroke value] ;; :stroke-color [:stroke value]
:fill-color [:fill value] ;; :fill-color [:fill value]
pair)) ;; pair))
(defn- rename-attrs ;; (defn- rename-attrs
[attrs] ;; [attrs]
(into {} (map rename-attr) attrs)) ;; (into {} (map rename-attr) attrs))
(defn- transform-stroke-attrs (defn- transform-stroke-attrs
[{:keys [stroke-style] :or {stroke-style :none} :as attrs}] [{:keys [stroke-style] :or {stroke-style :none} :as attrs}]
(case stroke-style (case stroke-style
:none (dissoc attrs :stroke-style :stroke-width :stroke-opacity :stroke-color) :none (dissoc attrs :stroke-style :stroke-width :stroke-opacity :stroke-color)
:solid (-> (merge shape-default-attrs attrs) :solid (dissoc attrs :stroke-style)
(dissoc :stroke-style)) (-> attrs
(-> (merge shape-default-attrs attrs)
(assoc :stroke-dasharray (stroke-type->dasharray stroke-style)) (assoc :stroke-dasharray (stroke-type->dasharray stroke-style))
(dissoc :stroke-style)))) (dissoc :stroke-style))))
@ -56,4 +85,4 @@
[shape] [shape]
(-> (select-keys shape shape-style-attrs) (-> (select-keys shape shape-style-attrs)
(transform-stroke-attrs) (transform-stroke-attrs)
(rename-attrs))) (process-attrs)))

View file

@ -47,18 +47,19 @@
(mf/defc image-shape (mf/defc image-shape
[{:keys [shape image] :as props}] [{:keys [shape image] :as props}]
(let [{:keys [id x1 y1 width height modifier-mtx]} (geom/size shape) (let [{:keys [id x y width height modifier-mtx]} shape
moving? (boolean modifier-mtx) moving? (boolean modifier-mtx)
transform (when (gmt/matrix? modifier-mtx) transform (when (gmt/matrix? modifier-mtx)
(str modifier-mtx)) (str modifier-mtx))
props {:x x1 :y y1 props (-> (attrs/extract-style-attrs shape)
:id (str "shape-" id) (assoc :x x
:preserve-aspect-ratio "none" :y y
:class (classnames :move-cursor moving?) :id (str "shape-" id)
:xlink-href (:url image) :preserveAspectRatio "none"
:transform transform :class (classnames :move-cursor moving?)
:width width :xlinkHref (:url image)
:height height} :transform transform
attrs (merge props (attrs/extract-style-attrs shape))] :width width
[:> :image (normalize-props attrs)])) :height height))]
[:& "image" props]))

View file

@ -7,6 +7,7 @@
(ns uxbox.main.ui.shapes.rect (ns uxbox.main.ui.shapes.rect
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.main.geom :as geom] [uxbox.main.geom :as geom]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.ui.shapes.attrs :as attrs] [uxbox.main.ui.shapes.attrs :as attrs]
@ -46,18 +47,19 @@
(gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx) (gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx)
:else shape) :else shape)
{:keys [x1 y1 width height] :as shape} (geom/size shape) {:keys [x y width height]} shape
transform (when (pos? rotation) transform (when (pos? rotation)
(str (rotate (gmt/matrix) shape))) (str (rotate (gmt/matrix) shape)))
moving? (boolean modifier-mtx) moving? (boolean modifier-mtx)
props {:x x1 :y y1 props (-> (attrs/extract-style-attrs shape)
:id (str "shape-" id) (assoc :x x
:className (classnames :move-cursor moving?) :y y
:width width :id (str "shape-" id)
:height height :className (classnames :move-cursor moving?)
:transform transform} :width width
attrs (merge (attrs/extract-style-attrs shape) props)] :height height
[:& "rect" attrs])) :transform transform))]
[:& "rect" props]))

View file

@ -122,7 +122,7 @@
style (make-style shape) style (make-style shape)
on-input (fn [ev] on-input (fn [ev]
(let [content (dom/event->inner-text ev)] (let [content (dom/event->inner-text ev)]
(st/emit! (udw/update-shape-attrs id {:content content}))))] #_(st/emit! (udw/update-shape-attrs id {:content content}))))]
[:foreignObject {:x x1 :y y1 :width width :height height} [:foreignObject {:x x1 :y y1 :width width :height height}
[:div {:style (normalize-props style) [:div {:style (normalize-props style)
:ref (::container own) :ref (::container own)

View file

@ -60,7 +60,7 @@
(scroll/scroll-to-point dom mouse-point scroll-position)))) (scroll/scroll-to-point dom mouse-point scroll-position))))
(mf/defc workspace-content (mf/defc workspace-content
[{:keys [layout page file] :as params}] [{:keys [layout page file flags] :as params}]
(let [canvas (mf/use-ref nil) (let [canvas (mf/use-ref nil)
left-sidebar? (not (empty? (keep layout [:layers :sitemap left-sidebar? (not (empty? (keep layout [:layers :sitemap
:document-history]))) :document-history])))
@ -93,28 +93,13 @@
(when right-sidebar? (when right-sidebar?
[:& right-sidebar {:page page :layout layout}])])) [:& right-sidebar {:page page :layout layout}])]))
(mf/defc workspace (mf/defc workspace-page
[{:keys [file-id page-id] :as props}] [{:keys [file-id page-id layout file flags] :as props}]
(mf/use-effect (mf/use-effect
{:deps #js [(str file-id)] {:deps (mf/deps file-id page-id)
:fn (fn [] :fn #(st/emit! (dw/initialize-page page-id))})
(st/emit! (dw/initialize-ws file-id))
#(st/emit! (dw/finalize-ws file-id)))})
(mf/use-effect
{:deps #js [(str file-id)
(str page-id)]
:fn (fn []
(let [sub (shortcuts/init)]
(st/emit! (dw/initialize file-id page-id))
#(rx/cancel! sub)))})
(let [layout (mf/deref refs/workspace-layout)
file (mf/deref refs/workspace-file)
page (mf/deref refs/workspace-page)
flags (mf/deref refs/selected-flags)]
(let [page (mf/deref refs/workspace-page)]
[:> rdnd/provider {:backend rdnd/html5} [:> rdnd/provider {:backend rdnd/html5}
[:& messages-widget] [:& messages-widget]
[:& header {:page page :layout layout :flags flags}] [:& header {:page page :layout layout :flags flags}]
@ -124,5 +109,34 @@
(when (and layout page) (when (and layout page)
[:& workspace-content {:layout layout [:& workspace-content {:layout layout
:flags flags
:file file :file file
:page page}])])) :page page}])]))
(mf/defc workspace
[{:keys [file-id page-id] :as props}]
(mf/use-effect
{:deps (mf/deps file-id)
:fn (fn []
(st/emit! (dw/initialize file-id))
#(st/emit! (dw/finalize file-id)))})
(mf/use-effect
{:deps (mf/deps file-id page-id)
:fn (fn []
(let [sub (shortcuts/init)]
#(rx/cancel! sub)))})
(let [layout (mf/deref refs/workspace-layout)
file (mf/deref refs/workspace-file)
flags (mf/deref refs/selected-flags)]
;; TODO: maybe loading state?
(when file
[:& workspace-page {:layout layout
:file file
:flags flags
:page-id page-id
:file-id file-id}])))

View file

@ -30,10 +30,9 @@
(mf/defc palette-item (mf/defc palette-item
[{:keys [color] :as props}] [{:keys [color] :as props}]
(letfn [(select-color [event] (letfn [(select-color [event]
(let [attrs (if (kbd/shift? event) (if (kbd/shift? event)
{:stroke-color color} (st/emit! (udw/update-selected-shapes :stroke-color color))
{:fill-color color})] (st/emit! (udw/update-selected-shapes :fill-color color))))]
(st/emit! (udw/update-selected-shapes-attrs attrs))))]
(let [rgb-vec (hex->rgb color) (let [rgb-vec (hex->rgb color)
rgb-color (apply str "" (interpose ", " rgb-vec))] rgb-color (apply str "" (interpose ", " rgb-vec))]
[:div.color-cell {:key (str color) [:div.color-cell {:key (str color)

View file

@ -75,7 +75,7 @@
(let [event (js/MouseEvent. "click") (let [event (js/MouseEvent. "click")
link (.createElement js/document "a") link (.createElement js/document "a")
now (dt/now) now (dt/now)
stream (->> (rx/from-coll (generate-files pages)) stream (->> (rx/from (generate-files pages))
(rx/reduce conj []) (rx/reduce conj [])
(rx/mapcat zip/build) (rx/mapcat zip/build)
(rx/map blob/create-uri) (rx/map blob/create-uri)

View file

@ -51,8 +51,7 @@
:fill-opacity 0 :fill-opacity 0
:segments []} :segments []}
{:type :canvas {:type :canvas
:name "Canvas" :name "Canvas"}
:stroke-color "#000000"}
{:type :curve {:type :curve
:name "Path" :name "Path"
:stroke-style :solid :stroke-style :solid
@ -109,18 +108,17 @@
(def handle-drawing-generic (def handle-drawing-generic
(letfn [(initialize-drawing [state point] (letfn [(initialize-drawing [state point]
(let [shape (get-in state [:workspace-local :drawing]) (let [shape (get-in state [:workspace-local :drawing])
shape (geom/setup shape {:x1 (:x point) shape (geom/setup shape {:x (:x point)
:y1 (:y point) :y (:y point)
:x2 (+ (:x point) 2) :width 2
:y2 (+ (:y point) 2)})] :height 2})]
(assoc-in state [:workspace-local :drawing] (assoc shape ::initialized? true)))) (assoc-in state [:workspace-local :drawing] (assoc shape ::initialized? true))))
(resize-shape [shape point lock?] (resize-shape [shape point lock?]
(let [shape (-> (geom/shape->rect-shape shape) (let [shape' (geom/shape->rect-shape shape)
(geom/size)) result (geom/resize-shape :bottom-right shape' point lock?)
result (geom/resize-shape :bottom-right shape point lock?) scale (geom/calculate-scale-ratio shape' result)
scale (geom/calculate-scale-ratio shape result) mtx (geom/generate-resize-matrix :bottom-right shape' scale)]
mtx (geom/generate-resize-matrix :bottom-right shape scale)]
(assoc shape :modifier-mtx mtx))) (assoc shape :modifier-mtx mtx)))
(update-drawing [state point lock?] (update-drawing [state point lock?]
@ -150,13 +148,13 @@
(def handle-drawing-path (def handle-drawing-path
(letfn [(stoper-event? [{:keys [type shift] :as event}] (letfn [(stoper-event? [{:keys [type shift] :as event}]
(or (= event :interrupt) (or (= event ::end-path-drawing)
(and (uws/mouse-event? event) (and (uws/mouse-event? event)
(or (and (= type :double-click) shift) (or (and (= type :double-click) shift)
(= type :context-menu))) (= type :context-menu)))
(and (uws/keyboard-event? event) (and (uws/keyboard-event? event)
(= type :down) (= type :down)
(= 13 (:key event))))) (= 13 (:key event)))))
(initialize-drawing [state point] (initialize-drawing [state point]
(-> state (-> state
@ -238,8 +236,7 @@
(def handle-drawing-curve (def handle-drawing-curve
(letfn [(stoper-event? [{:keys [type shift] :as event}] (letfn [(stoper-event? [{:keys [type shift] :as event}]
(or (= event :interrupt) (uws/mouse-event? event) (= type :up))
(and (uws/mouse-event? event) (= type :up))))
(initialize-drawing [state] (initialize-drawing [state]
(assoc-in state [:workspace-local :drawing ::initialized?] true)) (assoc-in state [:workspace-local :drawing ::initialized?] true))
@ -282,7 +279,9 @@
shape (dissoc shape ::initialized? :modifier-mtx)] shape (dissoc shape ::initialized? :modifier-mtx)]
;; Add & select the created shape to the workspace ;; Add & select the created shape to the workspace
(rx/of dw/deselect-all (rx/of dw/deselect-all
(dw/add-shape shape))))))))) (if (= :canvas (:type shape))
(dw/add-canvas shape)
(dw/add-shape shape))))))))))
(def close-drawing-path (def close-drawing-path
(ptk/reify ::close-drawing-path (ptk/reify ::close-drawing-path
@ -322,7 +321,8 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dw/set-tooltip nil) (st/emit! (dw/set-tooltip nil)
close-drawing-path close-drawing-path
:interrupt)) ::end-path-drawing))
(on-mouse-enter [event] (on-mouse-enter [event]
(st/emit! (dw/set-tooltip "Click to close the path"))) (st/emit! (dw/set-tooltip "Click to close the path")))
(on-mouse-leave [event] (on-mouse-leave [event]

View file

@ -7,22 +7,19 @@
(ns uxbox.main.ui.workspace.header (ns uxbox.main.ui.workspace.header
(:require (:require
[rumext.alpha :as mf]
[lentes.core :as l] [lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.main.data.history :as udh] [uxbox.main.data.history :as udh]
[uxbox.main.data.undo :as udu] [uxbox.main.data.undo :as udu]
[uxbox.main.data.workspace :as dw] [uxbox.main.data.workspace :as dw]
[uxbox.main.ui.workspace.images :refer [import-image-modal]]
[uxbox.main.ui.modal :as modal]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal]
[uxbox.main.ui.users :refer [user]] [uxbox.main.ui.users :refer [user]]
[uxbox.main.ui.workspace.clipboard] [uxbox.main.ui.workspace.images :refer [import-image-modal]]
[uxbox.util.data :refer [index-of]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.i18n :refer (tr)]
[uxbox.util.geom.point :as gpt]
[uxbox.util.math :as mth] [uxbox.util.math :as mth]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
@ -66,6 +63,7 @@
(mf/defc header (mf/defc header
[{:keys [page layout flags] :as props}] [{:keys [page layout flags] :as props}]
(let [toggle #(st/emit! (dw/toggle-flag %)) (let [toggle #(st/emit! (dw/toggle-flag %))
toggle-layout #(st/emit! (dw/toggle-layout-flag %))
on-undo #(st/emit! (udu/undo)) on-undo #(st/emit! (udu/undo))
on-redo #(st/emit! (udu/redo)) on-redo #(st/emit! (udu/redo))
on-image #(modal/show! import-image-modal {}) on-image #(modal/show! import-image-modal {})
@ -91,100 +89,100 @@
[:div.workspace-options [:div.workspace-options
[:ul.options-btn [:ul.options-btn
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "ds.help.canvas") {:alt (tr "workspace.header.canvas")
:class (when (= selected-drawtool :canvas) "selected") :class (when (= selected-drawtool :canvas) "selected")
:on-click (partial select-drawtool :canvas)} :on-click (partial select-drawtool :canvas)}
i/artboard] i/artboard]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "ds.help.rect") {:alt (tr "workspace.header.rect")
:class (when (= selected-drawtool :rect) "selected") :class (when (= selected-drawtool :rect) "selected")
:on-click (partial select-drawtool :rect)} :on-click (partial select-drawtool :rect)}
i/box] i/box]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "ds.help.circle") {:alt (tr "workspace.header.circle")
:class (when (= selected-drawtool :circle) "selected") :class (when (= selected-drawtool :circle) "selected")
:on-click (partial select-drawtool :circle)} :on-click (partial select-drawtool :circle)}
i/circle] i/circle]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "ds.help.text") {:alt (tr "workspace.header.text")
:class (when (= selected-drawtool :text) "selected") :class (when (= selected-drawtool :text) "selected")
:on-click (partial select-drawtool :text)} :on-click (partial select-drawtool :text)}
i/text] i/text]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "ds.help.path") {:alt (tr "workspace.header.path")
:class (when (= selected-drawtool :path) "selected") :class (when (= selected-drawtool :path) "selected")
:on-click (partial select-drawtool :path)} :on-click (partial select-drawtool :path)}
i/curve] i/curve]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "ds.help.curve") {:alt (tr "workspace.header.curve")
:class (when (= selected-drawtool :curve) "selected") :class (when (= selected-drawtool :curve) "selected")
:on-click (partial select-drawtool :curve)} :on-click (partial select-drawtool :curve)}
i/pencil] i/pencil]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.color-palette") {:alt (tr "workspace.header.color-palette")
:class (when (contains? layout :colorpalette) "selected") :class (when (contains? layout :colorpalette) "selected")
:on-click #(st/emit! (dw/toggle-layout-flag :colorpalette))} :on-click #(st/emit! (dw/toggle-layout-flag :colorpalette))}
i/palette] i/palette]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.icons") {:alt (tr "workspace.header.icons")
:class (when (contains? layout :icons) "selected") :class (when (contains? layout :icons) "selected")
:on-click #(st/emit! (dw/toggle-layout-flag :icons))} :on-click #(st/emit! (dw/toggle-layout-flag :icons))}
i/icon-set] i/icon-set]
; [:li.tooltip.tooltip-bottom ;; [:li.tooltip.tooltip-bottom
; {:alt (tr "header.layers") ;; {:alt (tr "header.layers")
; :class (when (contains? layout :layers) "selected") ;; :class (when (contains? layout :layers) "selected")
; :on-click #(st/emit! (dw/toggle-layout-flag :layers))} ;; :on-click #(st/emit! (dw/toggle-layout-flag :layers))}
; i/layers] ;; i/layers]
; [:li.tooltip.tooltip-bottom ;; [:li.tooltip.tooltip-bottom
; {:alt (tr "header.element-options") ;; {:alt (tr "header.element-options")
; :class (when (contains? layout :element-options) "selected") ;; :class (when (contains? layout :element-options) "selected")
; :on-click #(st/emit! (dw/toggle-layout-flag :element-options))} ;; :on-click #(st/emit! (dw/toggle-layout-flag :element-options))}
; i/options] ;; i/options]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.document-history") {:alt (tr "workspace.header.document-history")
:class (when (contains? layout :document-history) "selected") :class (when (contains? layout :document-history) "selected")
:on-click #(st/emit! (dw/toggle-layout-flag :document-history))} :on-click #(st/emit! (dw/toggle-layout-flag :document-history))}
i/undo-history] i/undo-history]
; [:li.tooltip.tooltip-bottom ;; [:li.tooltip.tooltip-bottom
; {:alt (tr "header.undo") ;; {:alt (tr "header.undo")
; :on-click on-undo} ;; :on-click on-undo}
; i/undo] ;; i/undo]
; [:li.tooltip.tooltip-bottom ;; [:li.tooltip.tooltip-bottom
; {:alt (tr "header.redo") ;; {:alt (tr "header.redo")
; :on-click on-redo} ;; :on-click on-redo}
; i/redo] ;; i/redo]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.download") {:alt (tr "workspace.header.download")
;; :on-click on-download ;; :on-click on-download
} }
i/download] i/download]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.image") {:alt (tr "workspace.header.image")
:on-click on-image} :on-click on-image}
i/image] i/image]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.rules") {:alt (tr "workspace.header.rules")
:class (when (contains? flags :rules) "selected") :class (when (contains? layout :rules) "selected")
:on-click (partial toggle :rules)} :on-click (partial toggle-layout :rules)}
i/ruler] i/ruler]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.grid") {:alt (tr "workspace.header.grid")
:class (when (contains? flags :grid) "selected") :class (when (contains? flags :grid) "selected")
:on-click (partial toggle :grid)} :on-click (partial toggle :grid)}
i/grid] i/grid]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "header.grid-snap") {:alt (tr "workspace.header.grid-snap")
:class (when (contains? flags :grid-snap) "selected") :class (when (contains? flags :grid-snap) "selected")
:on-click (partial toggle :grid-snap)} :on-click (partial toggle :grid-snap)}
i/grid-snap]]] i/grid-snap]]]
;; [:li.tooltip.tooltip-bottom ;; [:li.tooltip.tooltip-bottom
;; {:alt (tr "header.align")} ;; {:alt (tr "header.align")}
;; i/alignment]] ;; i/alignment]]
;;[:& user] ;; [:& user]
[:div.secondary-options [:div.secondary-options
[:& zoom-widget] [:& zoom-widget]
[:a.tooltip.tooltip-bottom.view-mode [:a.tooltip.tooltip-bottom.view-mode
{:alt (tr "header.view-mode") {:alt (tr "workspace.header.view-mode")
;; :on-click #(st/emit! (dw/->OpenView (:id page))) ;; :on-click #(st/emit! (dw/->OpenView (:id page)))
} }
i/play]] i/play]]

View file

@ -129,13 +129,9 @@
(read-string) (read-string)
(swap! local assoc :id))] (swap! local assoc :id))]
(mf/use-effect (mf/use-effect #(st/emit! udi/fetch-collections))
{:fn #(do (st/emit! (udi/fetch-collections)) (mf/use-effect {:deps #js [(str id)]
(st/emit! (udi/fetch-images nil)))}) :fn #(st/emit! (udi/fetch-images id))})
(mf/use-effect
{:deps #js [type id]
:fn #(st/emit! (udi/fetch-images id))})
[:div.lightbox-body.big-lightbox [:div.lightbox-body.big-lightbox
[:h3 (tr "image.import-library")] [:h3 (tr "image.import-library")]

View file

@ -99,10 +99,10 @@
(mf/defc controls (mf/defc controls
[{:keys [shape zoom on-click] :as props}] [{:keys [shape zoom on-click] :as props}]
(let [{:keys [x1 y1 width height]} shape (let [{:keys [x y width height]} shape
radius (if (> (max width height) handler-size-threshold) 6.0 4.0)] radius (if (> (max width height) handler-size-threshold) 6.0 4.0)]
[:g.controls [:g.controls
[:rect.main {:x x1 :y y1 [:rect.main {:x x :y y
:width width :width width
:height height :height height
:stroke-dasharray (str (/ 8.0 zoom) "," (/ 5 zoom)) :stroke-dasharray (str (/ 8.0 zoom) "," (/ 5 zoom))
@ -111,42 +111,42 @@
[:& control-item {:class "top" [:& control-item {:class "top"
:on-click #(on-click :top %) :on-click #(on-click :top %)
:r (/ radius zoom) :r (/ radius zoom)
:cx (+ x1 (/ width 2)) :cx (+ x (/ width 2))
:cy (- y1 2)}] :cy (- y 2)}]
[:& control-item {:on-click #(on-click :right %) [:& control-item {:on-click #(on-click :right %)
:r (/ radius zoom) :r (/ radius zoom)
:cy (+ y1 (/ height 2)) :cy (+ y (/ height 2))
:cx (+ x1 width 1) :cx (+ x width 1)
:class "right"}] :class "right"}]
[:& control-item {:on-click #(on-click :bottom %) [:& control-item {:on-click #(on-click :bottom %)
:r (/ radius zoom) :r (/ radius zoom)
:cx (+ x1 (/ width 2)) :cx (+ x (/ width 2))
:cy (+ y1 height 2) :cy (+ y height 2)
:class "bottom"}] :class "bottom"}]
[:& control-item {:on-click #(on-click :left %) [:& control-item {:on-click #(on-click :left %)
:r (/ radius zoom) :r (/ radius zoom)
:cy (+ y1 (/ height 2)) :cy (+ y (/ height 2))
:cx (- x1 3) :cx (- x 3)
:class "left"}] :class "left"}]
[:& control-item {:on-click #(on-click :top-left %) [:& control-item {:on-click #(on-click :top-left %)
:r (/ radius zoom) :r (/ radius zoom)
:cx x1 :cx x
:cy y1 :cy y
:class "top-left"}] :class "top-left"}]
[:& control-item {:on-click #(on-click :top-right %) [:& control-item {:on-click #(on-click :top-right %)
:r (/ radius zoom) :r (/ radius zoom)
:cx (+ x1 width) :cx (+ x width)
:cy y1 :cy y
:class "top-right"}] :class "top-right"}]
[:& control-item {:on-click #(on-click :bottom-left %) [:& control-item {:on-click #(on-click :bottom-left %)
:r (/ radius zoom) :r (/ radius zoom)
:cx x1 :cx x
:cy (+ y1 height) :cy (+ y height)
:class "bottom-left"}] :class "bottom-left"}]
[:& control-item {:on-click #(on-click :bottom-right %) [:& control-item {:on-click #(on-click :bottom-right %)
:r (/ radius zoom) :r (/ radius zoom)
:cx (+ x1 width) :cx (+ x width)
:cy (+ y1 height) :cy (+ y height)
:class "bottom-right"}]])) :class "bottom-right"}]]))
;; --- Selection Handlers (Component) ;; --- Selection Handlers (Component)
@ -203,9 +203,9 @@
(mf/defc text-edition-selection-handlers (mf/defc text-edition-selection-handlers
[{:keys [shape zoom] :as props}] [{:keys [shape zoom] :as props}]
(let [{:keys [x1 y1 width height] :as shape} (geom/selection-rect shape)] (let [{:keys [x y width height] :as shape} (geom/selection-rect shape)]
[:g.controls [:g.controls
[:rect.main {:x x1 :y y1 [:rect.main {:x x :y y
:width width :width width
:height height :height height
;; :stroke-dasharray (str (/ 5.0 zoom) "," (/ 5 zoom)) ;; :stroke-dasharray (str (/ 5.0 zoom) "," (/ 5 zoom))

View file

@ -205,7 +205,7 @@
:class (when-not collapsed? "inverse")} :class (when-not collapsed? "inverse")}
i/arrow-slide]] i/arrow-slide]]
[:ul [:ul
(for [[index shape] shapes] (for [[index shape] (reverse shapes)]
[:& layer-item {:shape shape [:& layer-item {:shape shape
:selected selected :selected selected
:index index :index index

View file

@ -9,28 +9,15 @@
(:require (:require
[lentes.core :as l] [lentes.core :as l]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[rumext.core :as mx]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.geom :as geom]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.ui.shapes.attrs :refer [shape-default-attrs]]
[uxbox.main.ui.workspace.sidebar.options.rect :as rect] [uxbox.main.ui.workspace.sidebar.options.rect :as rect]
[uxbox.main.ui.workspace.sidebar.options.circle :as circle] [uxbox.main.ui.workspace.sidebar.options.circle :as circle]
[uxbox.main.ui.workspace.sidebar.options.path :as path]
[uxbox.main.ui.workspace.sidebar.options.image :as image]
[uxbox.main.ui.workspace.sidebar.options.page :as page] [uxbox.main.ui.workspace.sidebar.options.page :as page]
;; [uxbox.main.ui.workspace.sidebar.options.circle-measures :as options-circlem]
;; [uxbox.main.ui.workspace.sidebar.options.fill :as options-fill]
;; [uxbox.main.ui.workspace.sidebar.options.icon-measures :as options-iconm]
;; [uxbox.main.ui.workspace.sidebar.options.image-measures :as options-imagem]
;; [uxbox.main.ui.workspace.sidebar.options.interactions :as options-interactions]
;; [uxbox.main.ui.workspace.sidebar.options.rect-measures :as options-rectm]
;; [uxbox.main.ui.workspace.sidebar.options.stroke :as options-stroke]
;; [uxbox.main.ui.workspace.sidebar.options.text :as options-text]
[uxbox.util.data :as data]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :refer [tr]]))
;; --- Constants ;; --- Constants
@ -44,43 +31,6 @@
;; :image [::image-measures] ;; :image [::image-measures]
;; ::page [::page-measures ::page-grid-options]}) ;; ::page [::page-measures ::page-grid-options]})
;; (def ^:private +menus+
;; [{:name "element.measures"
;; :id ::icon-measures
;; :icon i/infocard
;; :comp options-iconm/icon-measures-menu}
;; {:name "element.measures"
;; :id ::image-measures
;; :icon i/infocard
;; :comp options-imagem/image-measures-menu}
;; {:name "element.measures"
;; :id ::rect-measures
;; :icon i/infocard
;; :comp options-rectm/rect-measures-menu}
;; {:name "element.measures"
;; :id ::circle-measures
;; :icon i/infocard
;; :comp options-circlem/circle-measures-menu}
;; {:name "element.fill"
;; :id ::fill
;; :icon i/fill
;; :comp options-fill/fill-menu}
;; {:name "element.stroke"
;; :id ::stroke
;; :icon i/stroke
;; :comp options-stroke/stroke-menu}
;; {:name "element.text"
;; :id ::text
;; :icon i/text
;; :comp options-text/text-menu}
;; {:name "element.interactions"
;; :id ::interactions
;; :icon i/action
;; :comp options-interactions/interactions-menu}])
;; (def ^:private +menus-by-id+
;; (data/index-by-id +menus+))
;; --- Options ;; --- Options
(mf/defc shape-options (mf/defc shape-options
@ -93,6 +43,9 @@
(case (:type shape) (case (:type shape)
:rect [:& rect/options {:shape shape}] :rect [:& rect/options {:shape shape}]
:circle [:& circle/options {:shape shape}] :circle [:& circle/options {:shape shape}]
:path [:& path/options {:shape shape}]
:curve [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}]
nil)])) nil)]))
(mf/defc options-toolbox (mf/defc options-toolbox
@ -102,9 +55,9 @@
selected (mf/deref refs/selected-shapes)] selected (mf/deref refs/selected-shapes)]
[:div.elementa-options.tool-window [:div.elementa-options.tool-window
;; [:div.tool-window-bar ;; [:div.tool-window-bar
;; [:div.tool-window-icon i/options] ;; [:div.tool-window-icon i/options]
;; [:span (tr "ds.settings.element-options")] ;; [:span (tr "ds.settings.element-options")]
;; [:div.tool-window-close {:on-click close} i/close]] ;; [:div.tool-window-close {:on-click close} i/close]]
[:div.tool-window-content [:div.tool-window-content
[:div.element-options [:div.element-options
(if (= (count selected) 1) (if (= (count selected) 1)

View file

@ -8,126 +8,125 @@
(ns uxbox.main.ui.workspace.sidebar.options.circle (ns uxbox.main.ui.workspace.sidebar.options.circle
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.common.data :as d]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.geom :as geom] [uxbox.main.geom :as geom]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]] [uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]] [uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
[uxbox.util.data :refer (parse-int parse-float read-string)]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :refer [tr]]
[uxbox.util.math :refer (precision-or-0)])) [uxbox.util.math :as math :refer (precision-or-0)]))
(mf/defc size-options (mf/defc measures-menu
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
[:* (let [on-size-change
[:span (tr "ds.size")] (fn [event attr]
[:div.row-flex (let [value (-> (dom/get-target event)
[:div.input-element.pixels (dom/get-value)
[:input.input-text {:placeholder (tr "ds.width") (d/parse-integer 0))]
:type "number" (st/emit! (udw/update-dimensions (:id shape) {attr value}))))
:min "0"
;; :on-change #(on-size-change % shape :rx)
:value (precision-or-0 (:rx shape 0) 2)}]]
[:div.lock-size {:class (when (:proportion-lock shape) "selected")
;; :on-click #(on-proportion-lock-change % shape)
}
(if (:proportion-lock shape) i/lock i/unlock)]
[:div.input-element.pixels on-proportion-lock-change
[:input.input-text (fn [event]
{:placeholder (tr "ds.height") (st/emit! (udw/toggle-shape-proportion-lock (:id shape))))
:type "number"
:min "0"
;; :on-change #(on-size-change % shape :ry)
:value (precision-or-0 (:ry shape 0) 2)}]]]])
(mf/defc position-options on-size-rx-change #(on-size-change % :rx)
[{:keys [shape] :as props}] on-size-ry-change #(on-size-change % :ry)
[:*
[:span (tr "ds.position")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder "cx"
:type "number"
;; :on-change #(on-position-change % shape :x)
:value (precision-or-0 (:cx shape 0) 2)}]]
[:div.input-element.pixels
[:input.input-text
{:placeholder "cy"
:type "number"
;; :on-change #(on-position-change % shape :y)
:value (precision-or-0 (:cy shape 0) 2)}]]]])
(mf/defc rotation-options on-position-change
[{:keys [shape] :as props}] (fn [event attr]
[:* (let [value (-> (dom/get-target event)
[:span (tr "ds.rotation")] (dom/get-value)
[:div.row-flex (d/parse-integer))
[:input.slidebar point (gpt/point {attr value})]
{:type "range" (st/emit! (udw/update-position (:id shape) point))))
:min 0
:max 360
;; :on-change #(on-rotation-change % shape)
:value (:rotation shape 0)}]]
[:div.row-flex on-pos-cx-change #(on-position-change % :x)
[:div.input-element.degrees on-pos-cy-change #(on-position-change % :y)
[:input.input-text
{:placeholder ""
:type "number"
:min 0
:max 360
;; :on-change #(on-rotation-change % shape)
:value (precision-or-0 (:rotation shape 0) 2)}]]
[:input.input-text
{:style {:visibility "hidden"}}]]])
(mf/defc measures-options on-rotation-change
[{:keys [shape] :as props}] (fn [event]
[:div.element-set (let [value (-> (dom/get-target event)
[:div.element-set-title (tr "element.measures")] (dom/get-value)
[:div.element-set-content (d/parse-integer 0))]
[:& size-options {:shape shape}] (st/emit! (udw/update-shape (:id shape) {:rotation value}))))
[:& position-options {:shape shape}]
[:& rotation-options {:shape shape}]]])
;; (defn- on-size-change on-radius-change
;; [event shape attr] (fn [event]
;; (let [value (dom/event->value event) (let [value (-> (dom/get-target event)
;; value (parse-int value 0) (dom/get-value)
;; sid (:id shape) (d/parse-double 0))]
;; props {attr value}] (st/emit! (udw/update-shape (:id shape) {:rx value :ry value}))))]
;; (st/emit! (udw/update-dimensions sid props))))
;; (defn- on-rotation-change [:div.element-set
;; [event shape] [:div.element-set-title (tr "workspace.options.measures")]
;; (let [value (dom/event->value event) [:div.element-set-content
;; value (parse-int value 0)
;; sid (:id shape)]
;; (st/emit! (udw/update-shape-attrs sid {:rotation value}))))
;; (defn- on-position-change ;; SIZE
;; [event shape attr] [:span (tr "workspace.options.size")]
;; (let [value (dom/event->value event) [:div.row-flex
;; value (parse-int value nil) [:div.input-element.pixels
;; sid (:id shape) [:input.input-text {:type "number"
;; point (gpt/point {attr value})] :min "0"
;; (st/emit! (udw/update-position sid point)))) :on-change on-size-rx-change
:value (-> (:rx shape)
(math/precision 2)
(d/coalesce-str "0"))}]]
[:div.lock-size {:class (when (:proportion-lock shape) "selected")
:on-click on-proportion-lock-change}
(if (:proportion-lock shape)
i/lock
i/unlock)]
;; (defn- on-proportion-lock-change [:div.input-element.pixels
;; [event shape] [:input.input-text {:type "number"
;; (if (:proportion-lock shape) :min "0"
;; (st/emit! (udw/unlock-proportions (:id shape))) :on-change on-size-ry-change
;; (st/emit! (udw/lock-proportions (:id shape))))) :value (-> (:ry shape)
(math/precision 2)
(d/coalesce-str "0"))}]]]
;; POSITION
[:span (tr "workspace.options.position")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text {:type "number"
:on-change on-pos-cx-change
:value (-> (:cx shape)
(math/precision 2)
(d/coalesce-str "0"))}]]
[:div.input-element.pixels
[:input.input-text {:type "number"
:on-change on-pos-cy-change
:value (-> (:cy shape)
(math/precision 2)
(d/coalesce-str "0"))}]]]
;; ROTATION & RADIUS
[:span (tr "workspace.options.rotation-radius")]
[:div.row-flex
[:div.input-element.degrees
[:input.input-text {:placeholder ""
:type "number"
:min 0
:max 360
:on-change on-rotation-change
:value (-> (:rotation shape)
(math/precision 2)
(d/coalesce-str "0"))}]]
[:div.input-element.pixels
[:input.input-text {:type "number"
:on-change on-radius-change
:value (-> (:rx shape)
(math/precision 2)
(d/coalesce-str "0"))}]]]]]))
(mf/defc options (mf/defc options
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
[:div [:div
[:& measures-options {:shape shape}] [:& measures-menu {:shape shape}]
[:& fill-menu {:shape shape}] [:& fill-menu {:shape shape}]
[:& stroke-menu {:shape shape}]]) [:& stroke-menu {:shape shape}]])

View file

@ -1,117 +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) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.workspace.sidebar.options.circle-measures
(:require
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as udw]
[uxbox.main.geom :as geom]
[uxbox.main.store :as st]
[uxbox.util.data :refer (parse-int parse-float read-string)]
[uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :refer (tr)]
[uxbox.util.math :refer (precision-or-0)]))
(declare on-size-change)
(declare on-rotation-change)
(declare on-position-change)
(declare on-proportion-lock-change)
(mf/defc circle-measures-menu
[{:keys [menu shape] :as props}]
[:div.element-set {:key (str (:id menu))}
[:div.element-set-title (:name menu)]
[:div.element-set-content
;; SLIDEBAR FOR ROTATION AND OPACITY
[:span (tr "ds.size")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder (tr "ds.width")
:type "number"
:min "0"
:value (precision-or-0 (:rx shape 0) 2)
:on-change #(on-size-change % shape :rx)}]]
[:div.lock-size {:class (when (:proportion-lock shape) "selected")
:on-click #(on-proportion-lock-change % shape)}
(if (:proportion-lock shape) i/lock i/unlock)]
[:div.input-element.pixels
[:input.input-text
{:placeholder (tr "ds.height")
:type "number"
:min "0"
:value (precision-or-0 (:ry shape 0) 2)
:on-change #(on-size-change % shape :ry)}]]]
[:span (tr "ds.position")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder "cx"
:type "number"
:value (precision-or-0 (:cx shape 0) 2)
:on-change #(on-position-change % shape :x)}]]
[:div.input-element.pixels
[:input.input-text
{:placeholder "cy"
:type "number"
:value (precision-or-0 (:cy shape 0) 2)
:on-change #(on-position-change % shape :y)}]]]
[:span (tr "ds.rotation")]
[:div.row-flex
[:input.slidebar
{:type "range"
:min 0
:max 360
:value (:rotation shape 0)
:on-change #(on-rotation-change % shape)}]]
[:div.row-flex
[:div.input-element.degrees
[:input.input-text
{:placeholder ""
:type "number"
:min 0
:max 360
:value (precision-or-0 (:rotation shape 0) 2)
:on-change #(on-rotation-change % shape)}]]
[:input.input-text
{:style {:visibility "hidden"}}]]]])
(defn- on-size-change
[event shape attr]
(let [value (dom/event->value event)
value (parse-int value 0)
sid (:id shape)
props {attr value}]
(st/emit! (udw/update-dimensions sid props))))
(defn- on-rotation-change
[event shape]
(let [value (dom/event->value event)
value (parse-int value 0)
sid (:id shape)]
(st/emit! (udw/update-shape-attrs sid {:rotation value}))))
(defn- on-position-change
[event shape attr]
(let [value (dom/event->value event)
value (parse-int value nil)
sid (:id shape)
point (gpt/point {attr value})]
(st/emit! (udw/update-position sid point))))
(defn- on-proportion-lock-change
[event shape]
(if (:proportion-lock shape)
(st/emit! (udw/unlock-proportions (:id shape)))
(st/emit! (udw/lock-proportions (:id shape)))))

View file

@ -9,31 +9,33 @@
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.common.data :as d]
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal] [uxbox.main.ui.modal :as modal]
[uxbox.main.ui.workspace.colorpicker :refer [colorpicker-modal]] [uxbox.main.ui.workspace.colorpicker :refer [colorpicker-modal]]
[uxbox.util.data :refer [parse-float]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.i18n :refer [tr]])) [uxbox.util.i18n :refer [tr]]))
(mf/defc fill-menu (mf/defc fill-menu
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(letfn [(change-attrs [attrs] (letfn [(update-shape! [attr value]
(st/emit! (udw/update-shape-attrs (:id shape) attrs))) (st/emit! (udw/update-shape (:id shape) {attr value})))
(on-color-change [event] (on-color-change [event]
(let [value (dom/event->value event)] (let [value (-> (dom/get-target event)
(change-attrs {:fill-color value}))) (dom/get-value))]
(update-shape! :fill-color value)))
(on-opacity-change [event] (on-opacity-change [event]
(let [value (dom/event->value event) (let [value (-> (dom/get-target event)
value (parse-float value 1) (dom/get-value)
value (/ value 10000)] (d/parse-double 1)
(change-attrs {:fill-opacity value}))) (/ 10000))]
(update-shape! :fill-opacity value)))
(show-color-picker [event] (show-color-picker [event]
(let [x (.-clientX event) (let [x (.-clientX event)
y (.-clientY event) y (.-clientY event)
props {:x x :y y props {:x x :y y
:on-change #(change-attrs {:fill-color %}) :on-change #(update-shape! :fill-color %)
:default "#ffffff" :default "#ffffff"
:value (:fill-color shape) :value (:fill-color shape)
:transparent? true}] :transparent? true}]
@ -42,10 +44,10 @@
[:div.element-set-title (tr "element.fill")] [:div.element-set-title (tr "element.fill")]
[:div.element-set-content [:div.element-set-content
[:span (tr "ds.color")] [:span (tr "workspace.options.color")]
[:div.row-flex.color-data [:div.row-flex.color-data
[:span.color-th [:span.color-th
{:style {:background-color (:fill-color shape)} {:style {:background-color (:fill-color shape "#000000")}
:on-click show-color-picker}] :on-click show-color-picker}]
[:div.color-info [:div.color-info
[:input [:input
@ -53,7 +55,7 @@
:value (:fill-color shape "")}]]] :value (:fill-color shape "")}]]]
;; SLIDEBAR FOR ROTATION AND OPACITY ;; SLIDEBAR FOR ROTATION AND OPACITY
[:span (tr "ds.opacity")] [:span (tr "workspace.options.opacity")]
[:div.row-flex [:div.row-flex
[:input.slidebar [:input.slidebar
{:type "range" {:type "range"

View file

@ -95,7 +95,7 @@
[event shape] [event shape]
(let [value (dom/event->value event) (let [value (dom/event->value event)
value (parse-int value 0)] value (parse-int value 0)]
(st/emit! (udw/update-shape-attrs (:id shape) {:rotation value})))) #_(st/emit! (udw/update-shape-attrs (:id shape) {:rotation value}))))
(defn- on-position-change (defn- on-position-change
[event shape attr] [event shape attr]
@ -107,7 +107,7 @@
(defn- on-proportion-lock-change (defn- on-proportion-lock-change
[event shape] [event shape]
(if (:proportion-lock shape) #_(if (:proportion-lock shape)
(st/emit! (udw/unlock-proportions (:id shape))) (st/emit! (udw/unlock-proportions (:id shape)))
(st/emit! (udw/lock-proportions (:id shape))))) (st/emit! (udw/lock-proportions (:id shape)))))

View file

@ -1,134 +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) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.workspace.sidebar.options.image-measures
(:require
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as dw]
[uxbox.main.geom :as geom]
[uxbox.main.store :as st]
[uxbox.util.data :refer (parse-int parse-float read-string)]
[uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :refer (tr)]
[uxbox.util.math :refer (precision-or-0)]))
(declare on-size-change)
(declare on-rotation-change)
(declare on-opacity-change)
(declare on-position-change)
(declare on-proportion-lock-change)
(mf/defc image-measures-menu
[{:keys [menu shape] :as props}]
(let [size (geom/size shape)]
[:div.element-set
[:div.element-set-title (:name menu)]
[:div.element-set-content
;; SLIDEBAR FOR ROTATION AND OPACITY
[:span (tr "ds.size")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder (tr "ds.width")
:type "number"
:min "0"
:value (precision-or-0 (:width size) 2)
:on-change #(on-size-change % shape :width)}]]
[:div.lock-size
{:class (when (:proportion-lock shape) "selected")
:on-click #(on-proportion-lock-change % shape)}
(if (:proportion-lock shape) i/lock i/unlock)]
[:div.input-element.pixels
[:input.input-text
{:placeholder (tr "ds.height")
:type "number"
:min "0"
:value (precision-or-0 (:height size) 2)
:on-change #(on-size-change % shape :height)}]]]
[:span (tr "ds.position")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder "X"
:type "number"
:value (precision-or-0 (:x1 shape 0) 2)
:on-change #(on-position-change % shape :x)}]]
[:div.input-element.pixels
[:input.input-text
{:placeholder "Y"
:type "number"
:value (precision-or-0 (:y1 shape 0) 2)
:on-change #(on-position-change % shape :y)}]]]
;; [:span (tr "ds.rotation")]
;; [:div.row-flex
;; [:input.slidebar
;; {:type "range"
;; :min 0
;; :max 360
;; :value (:rotation shape 0)
;; :on-change on-rotation-change}]]
;; [:div.row-flex
;; [:div.input-element.degrees
;; [:input.input-text
;; {:placeholder ""
;; :type "number"
;; :min 0
;; :max 360
;; :value (precision-or-0 (:rotation shape 0) 2)
;; :on-change on-rotation-change
;; }]]
;; [:input.input-text
;; {:style {:visibility "hidden"}}]]
[:span (tr "ds.opacity")]
[:div.row-flex
[:input.slidebar
{:type "range"
:min "0"
:max "10000"
:value (* 10000 (:opacity shape 1))
:step "1"
:on-change #(on-opacity-change % shape)}]]]]))
(defn- on-size-change
[event shape attr]
(let [value (dom/event->value event)
value (parse-int value 0)
props {attr value}]
(st/emit! (dw/update-dimensions (:id shape) props))))
(defn- on-rotation-change
[event shape]
(let [value (dom/event->value event)
value (parse-int value 0)]
(st/emit! (dw/update-shape-attrs (:id shape) {:rotation value}))))
(defn- on-opacity-change
[event shape]
(let [value (dom/event->value event)
value (parse-float value 1)
value (/ value 10000)]
(st/emit! (dw/update-shape-attrs (:id shape) {:opacity value}))))
(defn- on-position-change
[event shape attr]
(let [value (dom/event->value event)
value (parse-int value nil)
point (gpt/point {attr value})]
(st/emit! (dw/update-position (:id shape) point))))
(defn- on-proportion-lock-change
[event shape]
(if (:proportion-lock shape)
(st/emit! (dw/unlock-proportions (:id shape)))
(st/emit! (dw/lock-proportions (:id shape)))))

View file

@ -22,452 +22,452 @@
;; --- Helpers ;; --- Helpers
(defn- on-change ;; (defn- on-change
([form attr event] ;; ([form attr event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(let [value (dom/event->value event) ;; (let [value (dom/event->value event)
value (read-string value)] ;; value (read-string value)]
(swap! form assoc attr value))) ;; (swap! form assoc attr value)))
([form attr keep event] ;; ([form attr keep event]
(let [data (select-keys @form keep)] ;; (let [data (select-keys @form keep)]
(reset! form data) ;; (reset! form data)
(on-change form attr event)))) ;; (on-change form attr event))))
;; --- Interactions List ;; ;; --- Interactions List
(defn- translate-trigger-name ;; (defn- translate-trigger-name
[trigger] ;; [trigger]
(case trigger ;; (case trigger
:click "Click" ;; :click "Click"
:doubleclick "Double Click" ;; :doubleclick "Double Click"
:rightclick "Right Click" ;; :rightclick "Right Click"
:hover "Hover" ;; :hover "Hover"
:mousein "Mouse In" ;; :mousein "Mouse In"
:mouseout "Mouse Out" ;; :mouseout "Mouse Out"
;; :swiperight "Swipe Right" ;; ;; :swiperight "Swipe Right"
;; :swipeleft "Swipe Left" ;; ;; :swipeleft "Swipe Left"
;; :swipedown "Swipe Down" ;; ;; :swipedown "Swipe Down"
;; :touchandhold "Touch and Hold" ;; ;; :touchandhold "Touch and Hold"
;; :holdrelease "Hold release" ;; ;; :holdrelease "Hold release"
(pr-str trigger))) ;; (pr-str trigger)))
(mf/defc interactions-list ;; (mf/defc interactions-list
[{:keys [shape form] :as props}] ;; [{:keys [shape form] :as props}]
(letfn [(on-edit [item event] ;; (letfn [(on-edit [item event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(reset! form item)) ;; (reset! form item))
(delete [item] ;; (delete [item]
(let [sid (:id shape) ;; (let [sid (:id shape)
id (:id item)] ;; id (:id item)]
(st/emit! (dw/delete-interaction sid id)))) ;; (st/emit! (dw/delete-interaction sid id))))
(on-delete [item event] ;; (on-delete [item event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(let [delete (partial delete item)] ;; (let [delete (partial delete item)]
(udl/open! :confirm {:on-accept delete})))] ;; (udl/open! :confirm {:on-accept delete})))]
[:ul.element-list ;; [:ul.element-list
(for [item (vals (:interactions shape)) ;; (for [item (vals (:interactions shape))
:let [key (pr-str (:id item))]] ;; :let [key (pr-str (:id item))]]
[:li {:key key} ;; [:li {:key key}
[:div.list-icon i/action] ;; [:div.list-icon i/action]
[:span (translate-trigger-name (:trigger item))] ;; [:span (translate-trigger-name (:trigger item))]
[:div.list-actions ;; [:div.list-actions
[:a {:on-click (partial on-edit item)} i/pencil] ;; [:a {:on-click (partial on-edit item)} i/pencil]
[:a {:on-click (partial on-delete item)} i/trash]]])])) ;; [:a {:on-click (partial on-delete item)} i/trash]]])]))
;; --- Trigger Input ;; ;; --- Trigger Input
(mf/defc trigger-input ;; (mf/defc trigger-input
[{:keys [form] :as props}] ;; [{:keys [form] :as props}]
;; (mf/use-effect ;; ;; (mf/use-effect
;; {:init #(when-not (:trigger @form) (swap! form assoc :trigger :click)) ;; ;; {:init #(when-not (:trigger @form) (swap! form assoc :trigger :click))
;; :deps true}) ;; ;; :deps true})
[:div ;; [:div
[:span "Trigger"] ;; [:span "Trigger"]
[:div.row-flex ;; [:div.row-flex
[:select.input-select {:placeholder "Choose a trigger" ;; [:select.input-select {:placeholder "Choose a trigger"
:on-change (partial on-change form :trigger) ;; :on-change (partial on-change form :trigger)
:value (pr-str (:trigger @form))} ;; :value (pr-str (:trigger @form))}
[:option {:value ":click"} "Click"] ;; [:option {:value ":click"} "Click"]
[:option {:value ":doubleclick"} "Double-click"] ;; [:option {:value ":doubleclick"} "Double-click"]
[:option {:value ":rightclick"} "Right-click"] ;; [:option {:value ":rightclick"} "Right-click"]
[:option {:value ":hover"} "Hover"] ;; [:option {:value ":hover"} "Hover"]
[:option {:value ":mousein"} "Mouse in"] ;; [:option {:value ":mousein"} "Mouse in"]
[:option {:value ":mouseout"} "Mouse out"] ;; [:option {:value ":mouseout"} "Mouse out"]
#_[:option {:value ":swiperight"} "Swipe right"] ;; #_[:option {:value ":swiperight"} "Swipe right"]
#_[:option {:value ":swipeleft"} "Swipe left"] ;; #_[:option {:value ":swipeleft"} "Swipe left"]
#_[:option {:value ":swipedown"} "Swipe dpwn"] ;; #_[:option {:value ":swipedown"} "Swipe dpwn"]
#_[:option {:value ":touchandhold"} "Touch and hold"] ;; #_[:option {:value ":touchandhold"} "Touch and hold"]
#_[:option {:value ":holdrelease"} "Hold release"] ;; #_[:option {:value ":holdrelease"} "Hold release"]
#_[:option {:value ":keypress"} "Key press"] ;; #_[:option {:value ":keypress"} "Key press"]
#_[:option {:value ":pageisloaded"} "Page is loaded"] ;; #_[:option {:value ":pageisloaded"} "Page is loaded"]
#_[:option {:value ":windowscroll"} "Window is scrolled to"]]]]) ;; #_[:option {:value ":windowscroll"} "Window is scrolled to"]]]])
;; --- URL Input ;; ;; --- URL Input
(mf/defc url-input ;; (mf/defc url-input
[form]
[:div
[:span "Url"]
[:div.row-flex
[:input.input-text
{:placeholder "http://"
:on-change (partial on-change form :url)
:value (:url @form "")
:type "url"}]]])
;; --- Elements Input
(defn- collect-shapes
[state page]
(let [shapes-by-id (:shapes state)
shapes (get-in state [:pages page :shapes])]
(letfn [(resolve-shape [acc id]
(let [shape (get shapes-by-id id)]
(if (= (:type shape) :group)
(reduce resolve-shape (conj acc shape) (:items shape))
(conj acc shape))))]
(reduce resolve-shape [] shapes))))
(mf/defc elements-input
[{:keys [page-id form] :as props}]
(let [shapes (collect-shapes @st/state page-id)]
[:div
[:span "Element"]
[:div.row-flex
[:select.input-select
{:placeholder "Choose an element"
:on-change (partial on-change form :element)
:value (pr-str (:element @form))}
[:option {:value "nil"} "---"]
(for [shape shapes
:let [key (pr-str (:id shape))]]
[:option {:key key :value key} (:name shape)])]]]))
;; --- Page Input
(mf/defc pages-input
[form-ref path]
;; FIXME: react on ref
#_(let [pages (mx/react refs/selected-project-pages)]
(when (and (not (:page @form-ref))
(pos? (count pages)))
(swap! form-ref assoc :page (:id (first pages))))
[:div
[:span "Page"]
[:div.row-flex
[:select.input-select {:placeholder "Choose a page"
:on-change (partial on-change form-ref :page)
:value (pr-str (:page @form-ref))}
(for [page pages
:let [key (pr-str (:id page))]]
[:option {:key key :value key} (:name page)])]]]))
;; --- Animation
(mf/defc animation-input
[{:keys [form] :as props}]
(when-not (:action @form)
(swap! form assoc :animation :none))
[:div
[:span "Animation"]
[:div.row-flex
[:select.input-select
{:placeholder "Animation"
:on-change (partial on-change form :animation)
:value (pr-str (:animation @form))}
[:option {:value ":none"} "None"]
[:option {:value ":fade"} "Fade"]
[:option {:value ":slide"} "Slide"]]]])
;; --- MoveTo Input
(mf/defc moveto-input
[{:keys [form] :as props}]
(when-not (:moveto-x @form)
(swap! form assoc :moveto-x 0))
(when-not (:moveto-y @form)
(swap! form assoc :moveto-y 0))
[:div
[:span "Move to position"]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder "X"
:on-change (partial on-change form :moveto-x)
:type "number"
:value (:moveto-x @form "")}]]
[:div.input-element.pixels
[:input.input-text
{:placeholder "Y"
:on-change (partial on-change form :moveto-y)
:type "number"
:value (:moveto-y @form "")}]]]])
;; --- MoveBy Input
(mf/defc moveby-input
[{:keys [form] :as props}]
(when-not (:moveby-x @form)
(swap! form assoc :moveby-x 0))
(when-not (:moveby-y @form)
(swap! form assoc :moveby-y 0))
[:div
[:span "Move to position"]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text
{:placeholder "X"
:on-change (partial on-change form :moveby-x)
:type "number"
:value (:moveby-x @form "")}]]
[:div.input-element.pixels
[:input.input-text
{:placeholder "Y"
:on-change (partial on-change form :moveby-y)
:type "number"
:value (:moveby-y @form "")}]]]])
;; --- Opacity Input
(mf/defc opacity-input
[{:keys [form] :as props}]
(when-not (:opacity @form)
(swap! form assoc :opacity 100))
[:div
[:span "Opacity"]
[:div.row-flex
[:div.input-element.percentail
[:input.input-text
{:placeholder "%"
:on-change (partial on-change form :opacity)
:min "0"
:max "100"
:type "number"
:value (:opacity @form "")}]]]])
;; --- Rotate Input
;; (mx/defc rotate-input
;; [form] ;; [form]
;; [:div ;; [:div
;; [:span "Rotate (dg)"] ;; [:span "Url"]
;; [:div.row-flex ;; [:div.row-flex
;; [:div.input-element.degrees ;; [:input.input-text
;; {:placeholder "http://"
;; :on-change (partial on-change form :url)
;; :value (:url @form "")
;; :type "url"}]]])
;; ;; --- Elements Input
;; (defn- collect-shapes
;; [state page]
;; (let [shapes-by-id (:shapes state)
;; shapes (get-in state [:pages page :shapes])]
;; (letfn [(resolve-shape [acc id]
;; (let [shape (get shapes-by-id id)]
;; (if (= (:type shape) :group)
;; (reduce resolve-shape (conj acc shape) (:items shape))
;; (conj acc shape))))]
;; (reduce resolve-shape [] shapes))))
;; (mf/defc elements-input
;; [{:keys [page-id form] :as props}]
;; (let [shapes (collect-shapes @st/state page-id)]
;; [:div
;; [:span "Element"]
;; [:div.row-flex
;; [:select.input-select
;; {:placeholder "Choose an element"
;; :on-change (partial on-change form :element)
;; :value (pr-str (:element @form))}
;; [:option {:value "nil"} "---"]
;; (for [shape shapes
;; :let [key (pr-str (:id shape))]]
;; [:option {:key key :value key} (:name shape)])]]]))
;; ;; --- Page Input
;; (mf/defc pages-input
;; [form-ref path]
;; ;; FIXME: react on ref
;; #_(let [pages (mx/react refs/selected-project-pages)]
;; (when (and (not (:page @form-ref))
;; (pos? (count pages)))
;; (swap! form-ref assoc :page (:id (first pages))))
;; [:div
;; [:span "Page"]
;; [:div.row-flex
;; [:select.input-select {:placeholder "Choose a page"
;; :on-change (partial on-change form-ref :page)
;; :value (pr-str (:page @form-ref))}
;; (for [page pages
;; :let [key (pr-str (:id page))]]
;; [:option {:key key :value key} (:name page)])]]]))
;; ;; --- Animation
;; (mf/defc animation-input
;; [{:keys [form] :as props}]
;; (when-not (:action @form)
;; (swap! form assoc :animation :none))
;; [:div
;; [:span "Animation"]
;; [:div.row-flex
;; [:select.input-select
;; {:placeholder "Animation"
;; :on-change (partial on-change form :animation)
;; :value (pr-str (:animation @form))}
;; [:option {:value ":none"} "None"]
;; [:option {:value ":fade"} "Fade"]
;; [:option {:value ":slide"} "Slide"]]]])
;; ;; --- MoveTo Input
;; (mf/defc moveto-input
;; [{:keys [form] :as props}]
;; (when-not (:moveto-x @form)
;; (swap! form assoc :moveto-x 0))
;; (when-not (:moveto-y @form)
;; (swap! form assoc :moveto-y 0))
;; [:div
;; [:span "Move to position"]
;; [:div.row-flex
;; [:div.input-element.pixels
;; [:input.input-text ;; [:input.input-text
;; {:placeholder "dg" ;; {:placeholder "X"
;; :on-change (partial on-change form :rotation) ;; :on-change (partial on-change form :moveto-x)
;; :type "number" ;; :type "number"
;; :value (:rotation @form "")}]]]]) ;; :value (:moveto-x @form "")}]]
;; [:div.input-element.pixels
;; [:input.input-text
;; {:placeholder "Y"
;; :on-change (partial on-change form :moveto-y)
;; :type "number"
;; :value (:moveto-y @form "")}]]]])
;; --- Resize Input ;; ;; --- MoveBy Input
(mf/defc resize-input ;; (mf/defc moveby-input
[{:keys [form] :as props}] ;; [{:keys [form] :as props}]
[:div ;; (when-not (:moveby-x @form)
[:span "Resize"] ;; (swap! form assoc :moveby-x 0))
[:div.row-flex ;; (when-not (:moveby-y @form)
[:div.input-element.pixels ;; (swap! form assoc :moveby-y 0))
[:input.input-text ;; [:div
{:placeholder "Width" ;; [:span "Move to position"]
:on-change (partial on-change form :resize-width) ;; [:div.row-flex
:type "number" ;; [:div.input-element.pixels
:value (:resize-width @form "")}]] ;; [:input.input-text
[:div.input-element.pixels ;; {:placeholder "X"
[:input.input-text ;; :on-change (partial on-change form :moveby-x)
{:placeholder "Height" ;; :type "number"
:on-change (partial on-change form :resize-height) ;; :value (:moveby-x @form "")}]]
:type "number" ;; [:div.input-element.pixels
:value (:resize-height @form "")}]]]]) ;; [:input.input-text
;; {:placeholder "Y"
;; :on-change (partial on-change form :moveby-y)
;; :type "number"
;; :value (:moveby-y @form "")}]]]])
;; --- Color Input ;; ;; --- Opacity Input
(mf/defc colorpicker ;; (mf/defc opacity-input
[{:keys [x y on-change value]}] ;; [{:keys [form] :as props}]
(let [left (- x 260) ;; (when-not (:opacity @form)
top (- y 50)] ;; (swap! form assoc :opacity 100))
[:div.colorpicker-tooltip ;; [:div
{:style {:left (str left "px") ;; [:span "Opacity"]
:top (str top "px")}} ;; [:div.row-flex
;; [:div.input-element.percentail
;; [:input.input-text
;; {:placeholder "%"
;; :on-change (partial on-change form :opacity)
;; :min "0"
;; :max "100"
;; :type "number"
;; :value (:opacity @form "")}]]]])
(cp/colorpicker ;; ;; --- Rotate Input
:theme :small
:value value
:on-change on-change)]))
(defmethod lbx/render-lightbox :interactions/colorpicker ;; ;; (mx/defc rotate-input
[params] ;; ;; [form]
(colorpicker params)) ;; ;; [:div
;; ;; [:span "Rotate (dg)"]
;; ;; [:div.row-flex
;; ;; [:div.input-element.degrees
;; ;; [:input.input-text
;; ;; {:placeholder "dg"
;; ;; :on-change (partial on-change form :rotation)
;; ;; :type "number"
;; ;; :value (:rotation @form "")}]]]])
(mf/defc color-input ;; ;; --- Resize Input
[{:keys [form] :as props}]
(when-not (:fill-color @form)
(swap! form assoc :fill-color "#000000"))
(when-not (:stroke-color @form)
(swap! form assoc :stroke-color "#000000"))
(letfn [(on-change [attr color]
(swap! form assoc attr color))
(on-change-fill-color [event]
(let [value (dom/event->value event)]
(when (color? value)
(on-change :fill-color value))))
(on-change-stroke-color [event]
(let [value (dom/event->value event)]
(when (color? value)
(on-change :stroke-color value))))
(show-picker [attr event]
(let [x (.-clientX event)
y (.-clientY event)
opts {:x x :y y
:on-change (partial on-change attr)
:value (get @form attr)
:transparent? true}]
(udl/open! :interactions/colorpicker opts)))]
(let [stroke-color (:stroke-color @form)
fill-color (:fill-color @form)]
[:div
[:div.row-flex
[:div.column-half
[:span "Fill"]
[:div.color-data
[:span.color-th
{:style {:background-color fill-color}
:on-click (partial show-picker :fill-color)}]
[:div.color-info
[:input
{:on-change on-change-fill-color
:value fill-color}]]]]
[:div.column-half
[:span "Stroke"]
[:div.color-data
[:span.color-th
{:style {:background-color stroke-color}
:on-click (partial show-picker :stroke-color)}]
[:div.color-info
[:input
{:on-change on-change-stroke-color
:value stroke-color}]]]]]])))
;; --- Easing Input ;; (mf/defc resize-input
;; [{:keys [form] :as props}]
;; [:div
;; [:span "Resize"]
;; [:div.row-flex
;; [:div.input-element.pixels
;; [:input.input-text
;; {:placeholder "Width"
;; :on-change (partial on-change form :resize-width)
;; :type "number"
;; :value (:resize-width @form "")}]]
;; [:div.input-element.pixels
;; [:input.input-text
;; {:placeholder "Height"
;; :on-change (partial on-change form :resize-height)
;; :type "number"
;; :value (:resize-height @form "")}]]]])
(mf/defc easing-input ;; ;; --- Color Input
[{:keys [form] :as props}]
(when-not (:easing @form)
(swap! form assoc :easing :linear))
[:div
[:span "Easing"]
[:div.row-flex
[:select.input-select
{:placeholder "Easing"
:on-change (partial on-change form :easing)
:value (pr-str (:easing @form))}
[:option {:value ":linear"} "Linear"]
[:option {:value ":easein"} "Ease in"]
[:option {:value ":easeout"} "Ease out"]
[:option {:value ":easeinout"} "Ease in out"]]]])
;; --- Duration Input ;; (mf/defc colorpicker
;; [{:keys [x y on-change value]}]
;; (let [left (- x 260)
;; top (- y 50)]
;; [:div.colorpicker-tooltip
;; {:style {:left (str left "px")
;; :top (str top "px")}}
(mf/defc duration-input ;; (cp/colorpicker
[{:keys [form] :as props}] ;; :theme :small
(when-not (:duration @form) ;; :value value
(swap! form assoc :duration 300)) ;; :on-change on-change)]))
(when-not (:delay @form)
(swap! form assoc :delay 0))
[:div
[:span "Duration | Delay"]
[:div.row-flex
[:div.input-element.miliseconds
[:input.input-text
{:placeholder "Duration"
:type "number"
:on-change (partial on-change form :duration)
:value (pr-str (:duration @form))}]]
[:div.input-element.miliseconds
[:input.input-text {:placeholder "Delay"
:type "number"
:on-change (partial on-change form :delay)
:value (pr-str (:delay @form))}]]]])
;; --- Action Input ;; (defmethod lbx/render-lightbox :interactions/colorpicker
;; [params]
;; (colorpicker params))
(mf/defc action-input ;; (mf/defc color-input
[{:keys [shape form] :as props}] ;; [{:keys [form] :as props}]
;; (when-not (:action @form) ;; (when-not (:fill-color @form)
;; (swap! form assoc :action :show)) ;; (swap! form assoc :fill-color "#000000"))
(let [form-data (deref form) ;; (when-not (:stroke-color @form)
simple? #{:gotourl :gotopage} ;; (swap! form assoc :stroke-color "#000000"))
elements? (complement simple?) ;; (letfn [(on-change [attr color]
animation? #{:show :hide :toggle} ;; (swap! form assoc attr color))
only-easing? (complement animation?)] ;; (on-change-fill-color [event]
[:div ;; (let [value (dom/event->value event)]
[:span "Action"] ;; (when (color? value)
[:div.row-flex ;; (on-change :fill-color value))))
[:select.input-select ;; (on-change-stroke-color [event]
{:placeholder "Choose an action" ;; (let [value (dom/event->value event)]
:on-change (partial on-change form :action [:trigger]) ;; (when (color? value)
:value (pr-str (:action form-data))} ;; (on-change :stroke-color value))))
[:option {:value ":show"} "Show"] ;; (show-picker [attr event]
[:option {:value ":hide"} "Hide"] ;; (let [x (.-clientX event)
[:option {:value ":toggle"} "Toggle"] ;; y (.-clientY event)
;; [:option {:value ":moveto"} "Move to"] ;; opts {:x x :y y
[:option {:value ":moveby"} "Move by"] ;; :on-change (partial on-change attr)
[:option {:value ":opacity"} "Opacity"] ;; :value (get @form attr)
[:option {:value ":size"} "Size"] ;; :transparent? true}]
[:option {:value ":color"} "Color"] ;; (udl/open! :interactions/colorpicker opts)))]
;; [:option {:value ":rotate"} "Rotate"] ;; (let [stroke-color (:stroke-color @form)
[:option {:value ":gotopage"} "Go to page"] ;; fill-color (:fill-color @form)]
[:option {:value ":gotourl"} "Go to URL"] ;; [:div
#_[:option {:value ":goback"} "Go back"] ;; [:div.row-flex
[:option {:value ":scrolltoelement"} "Scroll to element"]]] ;; [:div.column-half
;; [:span "Fill"]
;; [:div.color-data
;; [:span.color-th
;; {:style {:background-color fill-color}
;; :on-click (partial show-picker :fill-color)}]
;; [:div.color-info
;; [:input
;; {:on-change on-change-fill-color
;; :value fill-color}]]]]
;; [:div.column-half
;; [:span "Stroke"]
;; [:div.color-data
;; [:span.color-th
;; {:style {:background-color stroke-color}
;; :on-click (partial show-picker :stroke-color)}]
;; [:div.color-info
;; [:input
;; {:on-change on-change-stroke-color
;; :value stroke-color}]]]]]])))
(case (:action form-data) ;; ;; --- Easing Input
:gotourl [:& url-input {:form form}]
;; :gotopage (pages-input form)
:color [:& color-input {:form form}]
;; :rotate (rotate-input form)
:size [:& resize-input {:form form}]
:moveto [:& moveto-input {:form form}]
:moveby [:& moveby-input {:form form}]
:opacity [:& opacity-input {:form form}]
nil)
(when (elements? (:action form-data)) ;; (mf/defc easing-input
[:& elements-input {:page-id (:page shape) ;; [{:keys [form] :as props}]
:form form}]) ;; (when-not (:easing @form)
;; (swap! form assoc :easing :linear))
;; [:div
;; [:span "Easing"]
;; [:div.row-flex
;; [:select.input-select
;; {:placeholder "Easing"
;; :on-change (partial on-change form :easing)
;; :value (pr-str (:easing @form))}
;; [:option {:value ":linear"} "Linear"]
;; [:option {:value ":easein"} "Ease in"]
;; [:option {:value ":easeout"} "Ease out"]
;; [:option {:value ":easeinout"} "Ease in out"]]]])
(when (and (animation? (:action form-data)) ;; ;; --- Duration Input
(:element form-data))
[:& animation-input {:form form}])
(when (or (not= (:animation form-data :none) :none) ;; (mf/defc duration-input
(and (only-easing? (:action form-data)) ;; [{:keys [form] :as props}]
(:element form-data))) ;; (when-not (:duration @form)
[:* ;; (swap! form assoc :duration 300))
[:& easing-input {:form form}] ;; (when-not (:delay @form)
[:& duration-input {:form form}]])])) ;; (swap! form assoc :delay 0))
;; [:div
;; [:span "Duration | Delay"]
;; [:div.row-flex
;; [:div.input-element.miliseconds
;; [:input.input-text
;; {:placeholder "Duration"
;; :type "number"
;; :on-change (partial on-change form :duration)
;; :value (pr-str (:duration @form))}]]
;; [:div.input-element.miliseconds
;; [:input.input-text {:placeholder "Delay"
;; :type "number"
;; :on-change (partial on-change form :delay)
;; :value (pr-str (:delay @form))}]]]])
;; ;; --- Action Input
;; (mf/defc action-input
;; [{:keys [shape form] :as props}]
;; ;; (when-not (:action @form)
;; ;; (swap! form assoc :action :show))
;; (let [form-data (deref form)
;; simple? #{:gotourl :gotopage}
;; elements? (complement simple?)
;; animation? #{:show :hide :toggle}
;; only-easing? (complement animation?)]
;; [:div
;; [:span "Action"]
;; [:div.row-flex
;; [:select.input-select
;; {:placeholder "Choose an action"
;; :on-change (partial on-change form :action [:trigger])
;; :value (pr-str (:action form-data))}
;; [:option {:value ":show"} "Show"]
;; [:option {:value ":hide"} "Hide"]
;; [:option {:value ":toggle"} "Toggle"]
;; ;; [:option {:value ":moveto"} "Move to"]
;; [:option {:value ":moveby"} "Move by"]
;; [:option {:value ":opacity"} "Opacity"]
;; [:option {:value ":size"} "Size"]
;; [:option {:value ":color"} "Color"]
;; ;; [:option {:value ":rotate"} "Rotate"]
;; [:option {:value ":gotopage"} "Go to page"]
;; [:option {:value ":gotourl"} "Go to URL"]
;; #_[:option {:value ":goback"} "Go back"]
;; [:option {:value ":scrolltoelement"} "Scroll to element"]]]
;; (case (:action form-data)
;; :gotourl [:& url-input {:form form}]
;; ;; :gotopage (pages-input form)
;; :color [:& color-input {:form form}]
;; ;; :rotate (rotate-input form)
;; :size [:& resize-input {:form form}]
;; :moveto [:& moveto-input {:form form}]
;; :moveby [:& moveby-input {:form form}]
;; :opacity [:& opacity-input {:form form}]
;; nil)
;; (when (elements? (:action form-data))
;; [:& elements-input {:page-id (:page shape)
;; :form form}])
;; (when (and (animation? (:action form-data))
;; (:element form-data))
;; [:& animation-input {:form form}])
;; (when (or (not= (:animation form-data :none) :none)
;; (and (only-easing? (:action form-data))
;; (:element form-data)))
;; [:*
;; [:& easing-input {:form form}]
;; [:& duration-input {:form form}]])]))
;; --- Form ;; ;; --- Form
(mf/defc interactions-form ;; (mf/defc interactions-form
[{:keys [shape form] :as props}] ;; [{:keys [shape form] :as props}]
(letfn [(on-submit [event] ;; (letfn [(on-submit [event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(let [sid (:id shape) ;; (let [sid (:id shape)
data (deref form)] ;; data (deref form)]
(st/emit! (dw/update-interaction sid data)) ;; (st/emit! (dw/update-interaction sid data))
(reset! form nil))) ;; (reset! form nil)))
(on-cancel [event] ;; (on-cancel [event]
(dom/prevent-default event) ;; (dom/prevent-default event)
(reset! form nil))] ;; (reset! form nil))]
[:form {:on-submit on-submit} ;; [:form {:on-submit on-submit}
[:& trigger-input {:form form}] ;; [:& trigger-input {:form form}]
[:& action-input {:shape shape :form form}] ;; [:& action-input {:shape shape :form form}]
[:div.row-flex ;; [:div.row-flex
[:input.btn-primary.btn-small.save-btn ;; [:input.btn-primary.btn-small.save-btn
{:value "Save" :type "submit"}] ;; {:value "Save" :type "submit"}]
[:a.cancel-btn {:on-click on-cancel} ;; [:a.cancel-btn {:on-click on-cancel}
"Cancel"]]])) ;; "Cancel"]]]))
;; --- Interactions Menu ;; --- Interactions Menu
@ -477,7 +477,7 @@
(mf/defc interactions-menu (mf/defc interactions-menu
[{:keys [menu shape] :as props}] [{:keys [menu shape] :as props}]
(let [form (mf/use-state nil) #_(let [form (mf/use-state nil)
interactions (:interactions shape)] interactions (:interactions shape)]
[:div.element-set {:key (str (:id menu))} [:div.element-set {:key (str (:id menu))}
[:div.element-set-title (:name menu)] [:div.element-set-title (:name menu)]

View file

@ -46,9 +46,9 @@
(modal/show! colorpicker-modal props)))] (modal/show! colorpicker-modal props)))]
[:div.element-set [:div.element-set
[:div.element-set-title (tr "element.page-measures")] [:div.element-set-title (tr "workspace.options.page-measures")]
[:div.element-set-content [:div.element-set-content
[:span (tr "ds.background-color")] [:span (tr "workspace.options.background-color")]
[:div.row-flex.color-data [:div.row-flex.color-data
[:span.color-th [:span.color-th
{:style {:background-color (:background metadata "#ffffff")} {:style {:background-color (:background metadata "#ffffff")}
@ -92,7 +92,7 @@
[:div.element-set [:div.element-set
[:div.element-set-title (tr "element.page-grid-options")] [:div.element-set-title (tr "element.page-grid-options")]
[:div.element-set-content [:div.element-set-content
[:span (tr "ds.size")] [:span (tr "workspace.options.size")]
[:div.row-flex [:div.row-flex
[:div.input-element.pixels [:div.input-element.pixels
[:input.input-text [:input.input-text
@ -106,7 +106,7 @@
:value (:grid-y-axis metadata) :value (:grid-y-axis metadata)
:on-change on-y-change :on-change on-y-change
:placeholder "y"}]]] :placeholder "y"}]]]
[:span (tr "ds.color")] [:span (tr "workspace.options.color")]
[:div.row-flex.color-data [:div.row-flex.color-data
[:span.color-th [:span.color-th
{:style {:background-color (:grid-color metadata)} {:style {:background-color (:grid-color metadata)}

View file

@ -8,111 +8,130 @@
(ns uxbox.main.ui.workspace.sidebar.options.rect (ns uxbox.main.ui.workspace.sidebar.options.rect
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.common.data :as d]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.geom :as geom] [uxbox.main.geom :as geom]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.util.data :refer [parse-int parse-float read-string]]
[uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]] [uxbox.main.ui.workspace.sidebar.options.fill :refer [fill-menu]]
[uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]] [uxbox.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.math :refer [precision-or-0]])) [uxbox.util.math :as math]))
(declare on-size-change) (mf/defc measures-menu
(declare on-rotation-change) [{:keys [shape] :as props}]
(declare on-position-change) (let [on-size-change
(declare on-proportion-lock-change) (fn [event attr]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-integer 0))]
(st/emit! (udw/update-dimensions (:id shape) {attr value}))))
on-proportion-lock-change
(fn [event]
(st/emit! (udw/toggle-shape-proportion-lock (:id shape))))
on-position-change
(fn [event attr]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-integer))
point (gpt/point {attr value})]
(st/emit! (udw/update-position (:id shape) point))))
on-rotation-change
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-integer 0))]
(st/emit! (udw/update-shape (:id shape) {:rotation value}))))
on-radius-change
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-double 0))]
(st/emit! (udw/update-shape (:id shape) {:rx value :ry value}))))
on-width-change #(on-size-change % :width)
on-height-change #(on-size-change % :height)
on-pos-x-change #(on-position-change % :x)
on-pos-y-change #(on-position-change % :y)]
(mf/defc measures
[{:keys [menu shape] :as props}]
(let [size (geom/size shape)]
[:div.element-set [:div.element-set
[:div.element-set-title (tr "element.measures")] [:div.element-set-title (tr "workspace.options.measures")]
[:div.element-set-content [:div.element-set-content
[:span (tr "ds.size")] [:span (tr "workspace.options.size")]
;; WIDTH & HEIGHT ;; WIDTH & HEIGHT
[:div.row-flex [:div.row-flex
[:div.input-element.pixels [:div.input-element.pixels
[:input.input-text {:placeholder (tr "ds.width") [:input.input-text {:type "number"
:type "number"
:min "0" :min "0"
:value (precision-or-0 (:width size) 2)}]] :on-change on-width-change
:value (-> (:width shape)
(math/precision 2)
(d/coalesce-str "0"))}]]
[:div.lock-size {:class (when (:proportion-lock shape) "selected") [:div.lock-size {:class (when (:proportion-lock shape) "selected")
:on-click #(on-proportion-lock-change % shape)} :on-click on-proportion-lock-change}
(if (:proportion-lock shape) i/lock i/unlock)] (if (:proportion-lock shape)
i/lock
i/unlock)]
[:div.input-element.pixels [:div.input-element.pixels
[:input.input-text {:placeholder (tr "ds.height") [:input.input-text {:type "number"
:type "number"
:min "0" :min "0"
:value (precision-or-0 (:height size) 2)}]]] :on-change on-height-change
:value (-> (:height shape)
(math/precision 2)
(d/coalesce-str "0"))}]]]
;; POSITION ;; POSITION
[:span (tr "ds.position")] [:span (tr "workspace.options.position")]
[:div.row-flex [:div.row-flex
[:div.input-element.pixels [:div.input-element.pixels
[:input.input-text {:placeholder "x" [:input.input-text {:placeholder "x"
:type "number" :type "number"
:value (precision-or-0 (:x1 shape 0) 2)}]] :on-change on-pos-x-change
:value (-> (:x shape)
(math/precision 2)
(d/coalesce-str "0"))}]]
[:div.input-element.pixels [:div.input-element.pixels
[:input.input-text {:placeholder "y" [:input.input-text {:placeholder "y"
:type "number" :type "number"
:value (precision-or-0 (:y1 shape 0) 2)}]]] :on-change on-pos-y-change
:value (-> (:y shape)
;; ROTATION (math/precision 2)
[:span (tr "ds.rotation")] (d/coalesce-str "0"))}]]]
[:div.row-flex
[:input.slidebar {:type "range"
:min 0
:max 360
;; :on-change #(on-rotation-change % shape)
:value (:rotation shape 0)}]]
[:span (tr "workspace.options.rotation-radius")]
[:div.row-flex [:div.row-flex
[:div.input-element.degrees [:div.input-element.degrees
[:input.input-text {:placeholder "" [:input.input-text {:placeholder ""
:type "number" :type "number"
:min 0 :min 0
:max 360 :max 360
:on-change #(on-rotation-change % shape) :on-change on-rotation-change
:value (precision-or-0 (:rotation shape "0") 2)}]] :value (-> (:rotation shape 0)
[:input.input-text {:style {:visibility "hidden"}}]]]])) (math/precision 2)
(d/coalesce-str "0"))}]]
;; (defn- on-size-change [:div.input-element.pixels
;; [event shape attr] [:input.input-text
;; (let [value (-> (dom/event->value event) {:placeholder "rx"
;; (parse-int 0))] :type "number"
;; (st/emit! (udw/update-dimensions (:id shape) {attr value})))) :on-change on-radius-change
:value (-> (:rx shape)
(math/precision 2)
(d/coalesce-str "0"))}]]]]]))
;; (defn- on-rotation-change
;; [event shape]
;; (let [value (dom/event->value event)
;; value (parse-int value 0)]
;; (st/emit! (udw/update-shape-attrs (:id shape) {:rotation value}))))
;; (defn- on-position-change
;; [event shape attr]
;; (let [value (-> (dom/event->value event)
;; (parse-int nil))
;; point (gpt/point {attr value})]
;; (st/emit! (udw/update-position (:id shape) point))))
;; (defn- on-proportion-lock-change
;; [event shape]
;; (if (:proportion-lock shape)
;; (st/emit! (udw/unlock-proportions (:id shape)))
;; (st/emit! (udw/lock-proportions (:id shape)))))
;; :rect [::rect-measures ::fill ::stroke]
(mf/defc options (mf/defc options
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
[:div [:div
[:& measures {:shape shape}] [:& measures-menu {:shape shape}]
[:& fill-menu {:shape shape}] [:& fill-menu {:shape shape}]
[:& stroke-menu {:shape shape}]]) [:& stroke-menu {:shape shape}]])

View file

@ -1,108 +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) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.workspace.sidebar.options.rect-measures
(:require
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as udw]
[uxbox.main.geom :as geom]
[uxbox.main.store :as st]
[uxbox.util.data :refer [parse-int parse-float read-string]]
[uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.math :refer [precision-or-0]]))
(declare on-size-change)
(declare on-rotation-change)
(declare on-position-change)
(declare on-proportion-lock-change)
(mf/defc rect-measures-menu
[{:keys [menu shape] :as props}]
(let [size (geom/size shape)]
[:div.element-set
[:div.element-set-title (:name menu)]
[:div.element-set-content
;; SLIDEBAR FOR ROTATION AND OPACITY
[:span (tr "ds.size")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text {:placeholder (tr "ds.width")
:type "number"
:min "0"
:value (precision-or-0 (:width size) 2)
:on-change #(on-size-change % shape :width)}]]
[:div.lock-size {:class (when (:proportion-lock shape) "selected")
:on-click #(on-proportion-lock-change % shape)}
(if (:proportion-lock shape) i/lock i/unlock)]
[:div.input-element.pixels
[:input.input-text {:placeholder (tr "ds.height")
:type "number"
:min "0"
:value (precision-or-0 (:height size) 2)
:on-change #(on-size-change % shape :height)}]]]
[:span (tr "ds.position")]
[:div.row-flex
[:div.input-element.pixels
[:input.input-text {:placeholder "x"
:type "number"
:value (precision-or-0 (:x1 shape 0) 2)
:on-change #(on-position-change % shape :x)}]]
[:div.input-element.pixels
[:input.input-text {:placeholder "y"
:type "number"
:value (precision-or-0 (:y1 shape 0) 2)
:on-change #(on-position-change % shape :y)}]]]
[:span (tr "ds.rotation")]
[:div.row-flex
[:input.slidebar {:type "range"
:min 0
:max 360
:value (:rotation shape 0)
:on-change #(on-rotation-change % shape)}]]
[:div.row-flex
[:div.input-element.degrees
[:input.input-text {:placeholder ""
:type "number"
:min 0
:max 360
:value (precision-or-0 (:rotation shape "0") 2)
:on-change #(on-rotation-change % shape)}]]
[:input.input-text {:style {:visibility "hidden"}}]]]]))
(defn- on-size-change
[event shape attr]
(let [value (-> (dom/event->value event)
(parse-int 0))]
(st/emit! (udw/update-dimensions (:id shape) {attr value}))))
(defn- on-rotation-change
[event shape]
(let [value (dom/event->value event)
value (parse-int value 0)]
(st/emit! (udw/update-shape-attrs (:id shape) {:rotation value}))))
(defn- on-position-change
[event shape attr]
(let [value (-> (dom/event->value event)
(parse-int nil))
point (gpt/point {attr value})]
(st/emit! (udw/update-position (:id shape) point))))
(defn- on-proportion-lock-change
[event shape]
(if (:proportion-lock shape)
(st/emit! (udw/unlock-proportions (:id shape)))
(st/emit! (udw/lock-proportions (:id shape)))))

View file

@ -8,6 +8,7 @@
(ns uxbox.main.ui.workspace.sidebar.options.stroke (ns uxbox.main.ui.workspace.sidebar.options.stroke
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.common.data :as d]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.store :as st] [uxbox.main.store :as st]
@ -16,126 +17,82 @@
[uxbox.util.data :refer [parse-int parse-float read-string]] [uxbox.util.data :refer [parse-int parse-float read-string]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.math :refer [precision-or-0]])) [uxbox.util.math :as math]))
(declare on-width-change)
(declare on-opacity-change)
(declare on-stroke-style-change)
(declare on-stroke-color-change)
(declare on-border-change)
(declare show-color-picker)
(mf/defc stroke-menu (mf/defc stroke-menu
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(let [local (mf/use-state {}) (let [on-stroke-style-change
on-border-lock #(swap! local update :border-lock not) (fn [event]
on-stroke-style-change #(on-stroke-style-change % shape) (let [value (-> (dom/get-target event)
on-width-change #(on-width-change % shape) (dom/get-value)
on-stroke-color-change #(on-stroke-color-change % shape) (d/read-string))]
on-border-change-rx #(on-border-change % shape local :rx) (st/emit! (udw/update-shape (:id shape) {:stroke-style value}))))
on-border-change-ry #(on-border-change % shape local :ry)
on-opacity-change #(on-opacity-change % shape) on-stroke-width-change
show-color-picker #(show-color-picker % shape)] (fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-double 1))]
(st/emit! (udw/update-shape (:id shape) {:stroke-width value}))))
on-stroke-opacity-change
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-double 1)
(/ 10000))]
(st/emit! (udw/update-shape (:id shape) {:stroke-opacity value}))))
show-color-picker
(fn [event]
(let [x (.-clientX event)
y (.-clientY event)
props {:x x :y y
:default "#ffffff"
:value (:stroke-color shape)
:on-change #(st/emit! (udw/update-shape (:id shape) {:stroke-color %}))
:transparent? true}]
(modal/show! colorpicker-modal props)))]
[:div.element-set [:div.element-set
[:div.element-set-title (tr "element.stroke")] [:div.element-set-title (tr "workspace.options.stroke")]
[:div.element-set-content [:div.element-set-content
[:span (tr "ds.style")]
;; Stroke Style & Width
[:span (tr "workspace.options.stroke.style")]
[:div.row-flex [:div.row-flex
[:select#style.input-select {:placeholder (tr "ds.style") [:select#style.input-select {:value (pr-str (:stroke-style shape))
:value (pr-str (:stroke-style shape))
:on-change on-stroke-style-change} :on-change on-stroke-style-change}
[:option {:value ":none"} (tr "ds.none")] [:option {:value ":none"} (tr "workspace.options.stroke.none")]
[:option {:value ":solid"} (tr "ds.solid")] [:option {:value ":solid"} (tr "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (tr "ds.dotted")] [:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (tr "ds.dashed")] [:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (tr "ds.mixed")]] [:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]
[:div.input-element.pixels
[:input.input-text
{:placeholder (tr "ds.width")
:type "number"
:min "0"
:value (precision-or-0 (:stroke-width shape 1) 2)
:on-change on-width-change}]]]
[:span (tr "ds.color")] [:div.input-element.pixels
[:input.input-text {:type "number"
:min "0"
:value (-> (:stroke-width shape)
(math/precision 2)
(d/coalesce-str "1"))
:on-change on-stroke-width-change}]]]
;; Stroke Color
[:span (tr "workspace.options.color")]
[:div.row-flex.color-data [:div.row-flex.color-data
[:span.color-th [:span.color-th {:style {:background-color (:stroke-color shape)}
{:style {:background-color (:stroke-color shape)} :on-click show-color-picker}]
:on-click show-color-picker}]
[:div.color-info [:div.color-info
[:input [:input {:read-only true
{:on-change on-stroke-color-change :default-value (:stroke-color shape "")}]]]
:value (:stroke-color shape "")}]]]
[:span (tr "ds.radius")] [:span (tr "workspace.options.opacity")]
[:div.row-flex [:div.row-flex
[:div.input-element.pixels [:input.slidebar {:type "range"
[:input.input-text :min "0"
{:placeholder "rx" :max "10000"
:type "number" :value (-> (:stroke-opacity shape 1)
:value (precision-or-0 (:rx shape 0) 2) (* 10000)
:on-change on-border-change-rx}]] (d/coalesce-str "1"))
[:div.lock-size :step "1"
{:class (when (:border-lock @local) "selected") :on-change on-stroke-opacity-change}]]]]))
:on-click on-border-lock}
i/lock]
[:div.input-element.pixels
[:input.input-text
{:placeholder "ry"
:type "number"
:value (precision-or-0 (:ry shape 0) 2)
:on-change on-border-change-ry}]]]
[:span (tr "ds.opacity")]
[:div.row-flex
[:input.slidebar
{:type "range"
:min "0"
:max "10000"
:value (* 10000 (:stroke-opacity shape 1))
:step "1"
:on-change on-opacity-change}]]]]))
(defn- on-width-change
[event shape]
(let [value (-> (dom/event->value event)
(parse-float 1))]
(st/emit! (udw/update-shape-attrs (:id shape) {:stroke-width value}))))
(defn- on-opacity-change
[event shape]
(let [value (-> (dom/event->value event)
(parse-float 1)
(/ 10000))]
(st/emit! (udw/update-shape-attrs (:id shape) {:stroke-opacity value}))))
(defn- on-stroke-style-change
[event shape]
(let [value (-> (dom/event->value event)
(read-string))]
(st/emit! (udw/update-shape-attrs (:id shape) {:stroke-style value}))))
(defn- on-stroke-color-change
[event shape]
(let [value (dom/event->value event)]
(st/emit! (udw/update-shape-attrs (:id shape) {:stroke-color value}))))
(defn- on-border-change
[event shape local attr]
(let [value (-> (dom/event->value event)
(parse-int nil))
id (:id shape)]
(if (:border-lock @local)
(st/emit! (udw/update-shape-attrs id {:rx value :ry value}))
(st/emit! (udw/update-shape-attrs id {attr value})))))
(defn- show-color-picker
[event shape]
(let [x (.-clientX event)
y (.-clientY event)
props {:x x :y y
:default "#ffffff"
:value (:stroke-color shape)
:on-change #(st/emit! (udw/update-shape-attrs (:id shape) {:stroke-color %}))
:transparent? true}]
(modal/show! colorpicker-modal props)))

View file

@ -29,7 +29,7 @@
{:mixins [mx/static]} {:mixins [mx/static]}
[menu {:keys [id] :as shape}] [menu {:keys [id] :as shape}]
(letfn [(update-attrs [attrs] (letfn [(update-attrs [attrs]
(st/emit! (udw/update-shape-attrs id attrs))) #_(st/emit! (udw/update-shape-attrs id attrs)))
(on-font-family-change [event] (on-font-family-change [event]
(let [value (dom/event->value event) (let [value (dom/event->value event)
attrs {:font-family (read-string value) attrs {:font-family (read-string value)

View file

@ -82,11 +82,11 @@
end-x (max (:x start) (:x stop)) end-x (max (:x start) (:x stop))
end-y (max (:y start) (:y stop))] end-y (max (:y start) (:y stop))]
(assoc data (assoc data
:x1 start-x :type :rect
:y1 start-y :x start-x
:x2 end-x :y start-y
:y2 end-y :width (- end-x start-x)
:type :rect))) :height (- end-y start-y))))
(def ^:private handle-selrect (def ^:private handle-selrect
(letfn [(update-state [state position] (letfn [(update-state [state position]
@ -115,13 +115,11 @@
{:wrap [mf/wrap-memo]} {:wrap [mf/wrap-memo]}
[{:keys [data] :as props}] [{:keys [data] :as props}]
(when data (when data
(let [{:keys [x1 y1 width height]} (geom/size data)] [:rect.selection-rect
[:rect.selection-rect {:x (:x data)
{:x x1 :y (:y data)
:y y1 :width (:width data)
:width width :height (:height data)}]))
:height height}])))
;; --- Viewport Positioning ;; --- Viewport Positioning
@ -155,7 +153,7 @@
[:* [:*
(for [item canvas] (for [item canvas]
[:& shape-wrapper {:shape item :key (:id item)}]) [:& shape-wrapper {:shape item :key (:id item)}])
(for [item (reverse shapes)] (for [item shapes]
[:& shape-wrapper {:shape item :key (:id item)}])])) [:& shape-wrapper {:shape item :key (:id item)}])]))
(mf/defc viewport (mf/defc viewport

View file

@ -201,6 +201,14 @@
(str/camel (name key)))))) (str/camel (name key))))))
;; (defn coalesce
;; [^number v ^number n]
;; (if (.-toFixed v)
;; (js/parseFloat (.toFixed v n))
;; 0))
;; (defmacro mirror-map [& fields] ;; (defmacro mirror-map [& fields]
;; (let [keys# (map #(keyword (name %)) fields) ;; (let [keys# (map #(keyword (name %)) fields)
;; vals# fields] ;; vals# fields]

View file

@ -81,7 +81,8 @@
(defn precision (defn precision
[^number v ^number n] [^number v ^number n]
(js/parseFloat (.toFixed v n))) (when (and (number? v) (number? n))
(js/parseFloat (.toFixed v n))))
(defn precision-or-0 (defn precision-or-0
[^number v ^number n] [^number v ^number n]

View file

@ -5,12 +5,15 @@
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.router (ns uxbox.util.router
(:require [reitit.core :as r] (:require
[cuerdas.core :as str] [reitit.core :as r]
[potok.core :as ptk] [cuerdas.core :as str]
[uxbox.util.html.history :as html-history]) [potok.core :as ptk]
(:import goog.Uri [uxbox.common.data :as d]
goog.Uri.QueryData)) [uxbox.util.html.history :as html-history])
(:import
goog.Uri
goog.Uri.QueryData))
(defonce +router+ nil) (defonce +router+ nil)
@ -42,7 +45,9 @@
(if (empty? qparams) (if (empty? qparams)
(r/match->path match) (r/match->path match)
(let [uri (.parse goog.Uri (r/match->path match)) (let [uri (.parse goog.Uri (r/match->path match))
qdt (.createFromMap QueryData (clj->js qparams))] qdt (.createFromMap QueryData (-> qparams
(d/remove-nil-vals)
(clj->js)))]
(.setQueryData uri qdt) (.setQueryData uri qdt)
(.toString uri)))))) (.toString uri))))))

View file

@ -12,20 +12,20 @@
(schedule 0 func)) (schedule 0 func))
([ms func] ([ms func]
(let [sem (js/setTimeout #(func) ms)] (let [sem (js/setTimeout #(func) ms)]
(reify rx/ICancellable (reify rx/IDisposable
(-cancel [_] (-dispose [_]
(js/clearTimeout sem)))))) (js/clearTimeout sem))))))
(defn interval (defn interval
[ms func] [ms func]
(let [sem (js/setInterval #(func) ms)] (let [sem (js/setInterval #(func) ms)]
(reify rx/ICancellable (reify rx/IDisposable
(-cancel [_] (-dispose [_]
(js/clearInterval sem))))) (js/clearInterval sem)))))
(defn schedule-on-idle (defn schedule-on-idle
[func] [func]
(let [sem (js/requestIdleCallback #(func))] (let [sem (js/requestIdleCallback #(func))]
(reify rx/ICancellable (reify rx/IDisposable
(-cancel [_] (-dispose [_]
(js/cancelIdleCallback sem))))) (js/cancelIdleCallback sem)))))

View file

@ -16,4 +16,4 @@
(let [zobj (js/JSZip.)] (let [zobj (js/JSZip.)]
(run! (partial attach-file zobj) files) (run! (partial attach-file zobj) files)
(->> (.generateAsync zobj #js {:type "blob"}) (->> (.generateAsync zobj #js {:type "blob"})
(rx/from-promise))))) (rx/from)))))