♻️ Refactor images storage.

This commit is contained in:
Andrey Antukh 2020-02-03 22:29:59 +01:00
parent b98d8519d4
commit 2cebbbc2f8
34 changed files with 2032 additions and 1630 deletions

View file

@ -10,12 +10,13 @@
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]
[uxbox.common.spec :as us]
[uxbox.common.data :as d]
[uxbox.main.repo :as rp]
[uxbox.main.store :as st]
[uxbox.util.data :refer (jscoll->vec)]
[uxbox.util.dom :as dom]
[uxbox.util.files :as files]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.webapi :as wapi]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.router :as r]
[uxbox.util.uuid :as uuid]))
@ -24,14 +25,7 @@
(s/def ::created-at inst?)
(s/def ::modified-at inst?)
(s/def ::user-id uuid?)
;; (s/def ::collection-id (s/nilable ::us/uuid))
;; (s/def ::mimetype string?)
;; (s/def ::thumbnail us/url-str?)
;; (s/def ::width number?)
;; (s/def ::height number?)
;; (s/def ::url us/url-str?)
(s/def ::collection-id ::us/uuid)
(s/def ::collection
(s/keys :req-un [::id
@ -40,6 +34,32 @@
::modified-at
::user-id]))
(declare fetch-icons)
(defn initialize
[collection-id]
(s/assert ::us/uuid collection-id)
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-icons :selected] #{}))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (fetch-icons collection-id)))))
;; --- Fetch Collections
(declare collections-fetched)
(def fetch-collections
(ptk/reify ::fetch-collections
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :icons-collections)
(rx/map collections-fetched)))))
;; --- Collections Fetched
(defn collections-fetched
@ -58,14 +78,20 @@
state
items))))
;; --- Fetch Collections
(def fetch-collections
(ptk/reify ::fetch-collections
;; --- Create Collection
(declare collection-created)
(def create-collection
(ptk/reify ::create-collection
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :icons-collections)
(rx/map collections-fetched)))))
(let [name (tr "ds.default-library-title" (gensym "c"))
data {:name name}]
(->> (rp/mutation! :create-icons-collection data)
(rx/map collection-created))))))
;; --- Collection Created
@ -78,70 +104,35 @@
(let [{:keys [id] :as item} (assoc item :type :own)]
(update state :icons-collections assoc id item)))))
;; --- Create Collection
(def create-collection
(ptk/reify ::create-collection
ptk/WatchEvent
(watch [_ state s]
(let [name (tr "ds.default-library-title" (gensym "c"))
data {:name name}]
(->> (rp/mutation! :create-icons-collection data)
(rx/map collection-created))))))
;; --- Collection Updated
(defn collection-updated
[item]
(ptk/reify ::collection-updated
ptk/UpdateEvent
(update [_ state]
(update-in state [:icons-collections (:id item)] merge item))))
;; --- Update Collection
(defrecord UpdateCollection [id]
ptk/WatchEvent
(watch [_ state s]
(let [data (get-in state [:icons-collections id])]
(->> (rp/mutation! :update-icons-collection data)
(rx/map collection-updated)))))
(defn update-collection
[id]
(UpdateCollection. id))
;; --- Rename Collection
(defrecord RenameCollection [id name]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:icons-collections id :name] name))
ptk/WatchEvent
(watch [_ state s]
(rx/of (update-collection id))))
(defn rename-collection
[id name]
(RenameCollection. id name))
(ptk/reify ::rename-collection
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:icons-collections id :name] name))
ptk/WatchEvent
(watch [_ state s]
(let [params {:id id :name name}]
(->> (rp/mutation! :rename-icons-collection params)
(rx/ignore))))))
;; --- Delete Collection
(defrecord DeleteCollection [id]
ptk/UpdateEvent
(update [_ state]
(update state :icons-collections dissoc id))
ptk/WatchEvent
(watch [_ state s]
(let [type (get-in state [:dashboard :icons :type])]
(->> (rp/mutation! :delete-icons-collection {:id id})
(rx/map #(r/nav :dashboard-icons {:type type}))))))
(defn delete-collection
[id]
(DeleteCollection. id))
[id on-success]
(ptk/reify ::delete-collection
ptk/UpdateEvent
(update [_ state]
(update state :icons-collections dissoc id))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation! :delete-icons-collection {:id id})
(rx/tap on-success)
(rx/ignore)))))
;; --- Icon Created
@ -157,9 +148,11 @@
;; --- Create Icon
(declare icon-created)
(defn- parse-svg
[data]
{:pre [(string? data)]}
(s/assert ::us/string data)
(let [valid-tags #{"defs" "path" "circle" "rect" "metadata" "g"
"radialGradient" "stop"}
div (dom/create-element "div")
@ -194,7 +187,7 @@
ptk/WatchEvent
(watch [_ state s]
(letfn [(parse [file]
(->> (files/read-as-text file)
(->> (wapi/read-file-as-text file)
(rx/map parse-svg)))
(allowed? [file]
(= (.-type file) "image/svg+xml"))
@ -207,7 +200,7 @@
:metadata metadata})]
(->> (rx/from files)
(rx/filter allowed?)
(rx/flat-map parse)
(rx/merge-map parse)
(rx/map prepare)
(rx/flat-map #(rp/mutation! :create-icon %))
(rx/map icon-created))))))
@ -226,184 +219,158 @@
;; --- Persist Icon
(defrecord PersistIcon [id]
ptk/WatchEvent
(watch [_ state stream]
(let [data (get-in state [:icons id])]
(->> (rp/mutation! :update-icon data)
(rx/map icon-persisted)))))
(defn persist-icon
[id]
{:pre [(uuid? id)]}
(PersistIcon. id))
;; --- Icons Fetched
(defrecord IconsFetched [items]
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id] :as icon}]
(let [icon (assoc icon :type :icon)]
(assoc-in state [:icons id] icon)))
state
items)))
(defn icons-fetched
[items]
(IconsFetched. items))
(s/assert ::us/uuid id)
(ptk/reify ::persist-icon
ptk/WatchEvent
(watch [_ state stream]
(let [data (get-in state [:icons id])]
(->> (rp/mutation! :update-icon data)
(rx/ignore))))))
;; --- Load Icons
(defrecord FetchIcons [id]
ptk/WatchEvent
(watch [_ state s]
(let [params (cond-> {} id (assoc :collection-id id))]
(->> (rp/query! :icons-by-collection params)
(rx/map icons-fetched)))))
(declare icons-fetched)
(defn fetch-icons
[id]
{:pre [(or (uuid? id) (nil? id))]}
(FetchIcons. id))
(ptk/reify ::fetch-icons
ptk/WatchEvent
(watch [_ state s]
(let [params (cond-> {} id (assoc :collection-id id))]
(->> (rp/query! :icons-by-collection params)
(rx/map icons-fetched))))))
;; --- Delete Icons
;; --- Icons Fetched
(defrecord DeleteIcon [id]
ptk/UpdateEvent
(update [_ state]
(-> state
(update :icons dissoc id)
(update-in [:dashboard :icons :selected] disj id)))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation! :delete-icon {:id id})
(rx/ignore))))
(defn delete-icon
[id]
{:pre [(uuid? id)]}
(DeleteIcon. id))
(defn icons-fetched
[items]
;; TODO: specs
(ptk/reify ::icons-fetched
ptk/UpdateEvent
(update [_ state]
(let [icons (d/index-by :id items)]
(assoc state :icons icons)))))
;; --- Rename Icon
(defrecord RenameIcon [id name]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:icons id :name] name))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (persist-icon id))))
(defn rename-icon
[id name]
{:pre [(uuid? id) (string? name)]}
(RenameIcon. id name))
(s/assert ::us/uuid id)
(s/assert ::us/string name)
(ptk/reify ::rename-icon
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:icons id :name] name))
;; --- Select icon
ptk/WatchEvent
(watch [_ state stream]
(rx/of (persist-icon id)))))
(defrecord SelectIcon [id]
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard :icons :selected] conj id)))
;; --- Icon Selection
(defrecord DeselectIcon [id]
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard :icons :selected] disj id)))
(defrecord ToggleIconSelection [id]
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])]
(rx/of
(if (selected id)
(DeselectIcon. id)
(SelectIcon. id))))))
(defn select-icon
[id]
(ptk/reify ::select-icon
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard-icons :selected] (fnil conj #{}) id))))
(defn deselect-icon
[id]
{:pre [(uuid? id)]}
(DeselectIcon. id))
(ptk/reify ::deselect-icon
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard-icons :selected] (fnil disj #{}) id))))
(defn toggle-icon-selection
(def deselect-all-icons
(ptk/reify ::deselect-all-icons
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-icons :selected] #{}))))
;; --- Delete Icons
(defn delete-icon
[id]
(ToggleIconSelection. id))
(ptk/reify ::delete-icon
ptk/UpdateEvent
(update [_ state]
(update state :icons dissoc id))
;; --- Copy Selected Icon
(defrecord CopySelected [id]
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])]
ptk/WatchEvent
(watch [_ state s]
(rx/merge
(->> (rx/from selected)
(rx/map #(get-in state [:icons %]))
(rx/map #(dissoc % :id))
(rx/map #(assoc % :collection-id id))
(rx/flat-map #(rp/mutation :create-icon %))
(rx/map :payload)
(rx/map icon-created))
(->> (rx/from selected)
(rx/map deselect-icon))))))
(defn copy-selected
[id]
{:pre [(or (uuid? id) (nil? id))]}
(CopySelected. id))
;; --- Move Selected Icon
(defrecord MoveSelected [id]
ptk/UpdateEvent
(update [_ state]
(let [selected (get-in state [:dashboard :icons :selected])]
(reduce (fn [state icon]
(assoc-in state [:icons icon :collection] id))
state
selected)))
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])]
(rx/merge
(->> (rx/from selected)
(rx/map persist-icon))
(->> (rx/from selected)
(rx/map deselect-icon))))))
(defn move-selected
[id]
{:pre [(or (uuid? id) (nil? id))]}
(MoveSelected. id))
(rx/of deselect-all-icons)
(->> (rp/mutation! :delete-icon {:id id})
(rx/ignore))))))
;; --- Delete Selected
(defrecord DeleteSelected []
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :icons :selected])]
(->> (rx/from selected)
(rx/map delete-icon)))))
(defn delete-selected
[]
(DeleteSelected.))
(def delete-selected
(ptk/reify ::delete-selected
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard-icons :selected])]
(->> (rx/from selected)
(rx/map delete-icon))))))
;; --- Update Opts (Filtering & Ordering)
(defrecord UpdateOpts [order filter edition]
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard :icons] merge
{:edition edition}
(when order {:order order})
(when filter {:filter filter}))))
(defn update-opts
[& {:keys [order filter edition]
:or {edition false}
:as opts}]
(UpdateOpts. order filter edition))
:or {edition false}}]
(ptk/reify ::update-opts
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-icons merge
{:edition edition}
(when order {:order order})
(when filter {:filter filter})))))
;; --- Copy Selected Icon
;; (defrecord CopySelected [id]
;; ptk/WatchEvent
;; (watch [_ state stream]
;; (let [selected (get-in state [:dashboard :icons :selected])]
;; (rx/merge
;; (->> (rx/from selected)
;; (rx/map #(get-in state [:icons %]))
;; (rx/map #(dissoc % :id))
;; (rx/map #(assoc % :collection-id id))
;; (rx/flat-map #(rp/mutation :create-icon %))
;; (rx/map :payload)
;; (rx/map icon-created))
;; (->> (rx/from selected)
;; (rx/map deselect-icon))))))
;; (defn copy-selected
;; [id]
;; {:pre [(or (uuid? id) (nil? id))]}
;; (CopySelected. id))
;; --- Move Selected Icon
;; (defrecord MoveSelected [id]
;; ptk/UpdateEvent
;; (update [_ state]
;; (let [selected (get-in state [:dashboard :icons :selected])]
;; (reduce (fn [state icon]
;; (assoc-in state [:icons icon :collection] id))
;; state
;; selected)))
;; ptk/WatchEvent
;; (watch [_ state stream]
;; (let [selected (get-in state [:dashboard :icons :selected])]
;; (rx/merge
;; (->> (rx/from selected)
;; (rx/map persist-icon))
;; (->> (rx/from selected)
;; (rx/map deselect-icon))))))
;; (defn move-selected
;; [id]
;; {:pre [(or (uuid? id) (nil? id))]}
;; (MoveSelected. id))

View file

@ -11,11 +11,11 @@
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.common.spec :as us]
[uxbox.common.data :as d]
[uxbox.main.store :as st]
[uxbox.main.repo :as rp]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]
[uxbox.util.data :refer (jscoll->vec)]
[uxbox.util.uuid :as uuid]
[uxbox.util.time :as ts]
[uxbox.util.router :as r]
@ -28,38 +28,66 @@
(s/def ::height number?)
(s/def ::modified-at inst?)
(s/def ::created-at inst?)
(s/def ::mimetype string?)
(s/def ::mtype string?)
(s/def ::thumbnail string?)
(s/def ::id uuid?)
(s/def ::url string?)
(s/def ::collection-id (s/nilable uuid?))
(s/def ::collection-id uuid?)
(s/def ::user-id uuid?)
(s/def ::collection-entity
(s/def ::collection
(s/keys :req-un [::id
::name
::created-at
::modified-at
::user-id]))
(s/def ::image-entity
(s/keys :opt-un [::collection-id]
:req-un [::id
(s/def ::image
(s/keys :req-un [::id
::name
::width
::height
::mtype
::collection-id
::created-at
::modified-at
::mimetype
::thumbnail
::url
::uri
::thumb-uri
::user-id]))
;; --- Initialize Collection Page
(declare fetch-images)
(defn initialize
[collection-id]
(us/verify ::us/uuid collection-id)
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-images :selected] #{}))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (fetch-images collection-id)))))
;; --- Fetch Collections
(declare collections-fetched)
(def fetch-collections
(ptk/reify ::fetch-collections
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :images-collections)
(rx/map collections-fetched)))))
;; --- Collections Fetched
(defn collections-fetched
[items]
(us/verify (s/every ::collection-entity) items)
(us/verify (s/every ::collection) items)
(ptk/reify ::collections-fetched
ptk/UpdateEvent
(update [_ state]
@ -70,105 +98,63 @@
state
items))))
;; --- Fetch Color Collections
(def fetch-collections
(ptk/reify ::fetch-collections
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :images-collections)
(rx/map collections-fetched)))))
;; --- Collection Created
(defn collection-created
[item]
(us/verify ::collection-entity 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
(declare collection-created)
(def create-collection
(ptk/reify ::create-collection
ptk/WatchEvent
(watch [_ state s]
(let [data {:name (tr "ds.default-library-title" (gensym "c"))}]
(->> (rp/mutation! :create-image-collection data)
(->> (rp/mutation! :create-images-collection data)
(rx/map collection-created))))))
;; --- Collection Updated
;; --- Collection Created
(defrecord CollectionUpdated [item]
ptk/UpdateEvent
(update [_ state]
(update-in state [:images-collections (:id item)] merge item)))
(defn collection-updated
(defn collection-created
[item]
(us/verify ::collection-entity item)
(CollectionUpdated. item))
;; --- Update Collection
(defrecord UpdateCollection [id]
ptk/WatchEvent
(watch [_ state s]
(let [item (get-in state [:images-collections id])]
(->> (rp/mutation! :update-images-collection item)
(rx/map collection-updated)))))
(defn update-collection
[id]
(UpdateCollection. id))
(us/verify ::collection 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)))))
;; --- Rename Collection
(defrecord RenameCollection [id name]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:images-collections id :name] name))
ptk/WatchEvent
(watch [_ state s]
(rx/of (update-collection id))))
(defn rename-collection
[id name]
(RenameCollection. id name))
(ptk/reify ::rename-collection
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:images-collections id :name] name))
ptk/WatchEvent
(watch [_ state s]
(let [params {:id id :name name}]
(->> (rp/mutation! :rename-images-collection params)
(rx/ignore))))))
;; --- Delete Collection
(defrecord DeleteCollection [id]
ptk/UpdateEvent
(update [_ state]
(update state :images-collections dissoc id))
ptk/WatchEvent
(watch [_ state s]
(let [type (get-in state [:dashboard :images :type])]
(->> (rp/mutation! :delete-images-collection {:id id})
(rx/map #(rt/nav :dashboard/images nil {:type type}))))))
(defn delete-collection
[id]
(DeleteCollection. id))
;; --- Image Created
(defn image-created
[item]
(us/verify ::image-entity item)
(ptk/reify ::image-created
[id on-success]
(ptk/reify ::delete-collection
ptk/UpdateEvent
(update [_ state]
(update state :images assoc (:id item) item))))
(update state :images-collections dissoc id))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation! :delete-images-collection {:id id})
(rx/tap on-success)
(rx/ignore)))))
;; --- Create Image
(declare image-created)
(def allowed-file-types #{"image/jpeg" "image/png"})
(defn create-images
@ -179,41 +165,49 @@
(ptk/reify ::create-images
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard :images :uploading] true))
(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]
(letfn [(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/next)
:file file
:width width
:height height}
id (assoc :collection-id id)))]
(assoc-in state [:dashboard-images :uploading] false))
(on-success [_]
(st/emit! finalize-upload)
(on-uploaded))
(on-error [e]
(st/emit! finalize-upload)
(rx/throw e))
(prepare [file]
{:name (.-name file)
:collection-id id
:content file})]
(->> (rx/from files)
(rx/filter allowed-file?)
(rx/mapcat image-size)
(rx/map prepare)
(rx/mapcat #(rp/mutation! :create-image %))
(rx/mapcat #(rp/mutation! :upload-image %))
(rx/reduce conj [])
(rx/do #(st/emit! finalize-upload))
(rx/do on-uploaded)
(rx/do on-success)
(rx/mapcat identity)
(rx/map image-created)))))))
(rx/map image-created)
(rx/catch on-error)))))))
;; --- Image Created
(defn image-created
[item]
(us/verify ::image item)
(ptk/reify ::image-created
ptk/UpdateEvent
(update [_ state]
(update state :images assoc (:id item) item))))
;; --- Update Image
(defn persist-image
[id]
{:pre [(uuid? id)]}
(us/verify ::us/uuid id)
(ptk/reify ::persist-image
ptk/WatchEvent
(watch [_ state stream]
@ -221,31 +215,34 @@
(->> (rp/mutation! :update-image data)
(rx/ignore))))))
;; --- Images Fetched
(defn images-fetched
[items]
(us/verify (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
(declare images-fetched)
(defn fetch-images
"Fetch a list of images of the selected collection"
[id]
(us/verify (s/nilable ::us/uuid) id)
(us/verify ::us/uuid id)
(ptk/reify ::fetch-images
ptk/WatchEvent
(watch [_ state s]
(let [params (cond-> {} id (assoc :collection-id id))]
(let [params {:collection-id id}]
(->> (rp/query! :images-by-collection params)
(rx/map images-fetched))))))
(rx/map (partial images-fetched id)))))))
;; --- Images Fetched
(s/def ::images (s/every ::image))
(defn images-fetched
[collection-id items]
(us/verify ::us/uuid collection-id)
(us/verify ::images items)
(ptk/reify ::images-fetched
ptk/UpdateEvent
(update [_ state]
(let [images (d/index-by :id items)]
(assoc state :images images)))))
;; --- Fetch Image
@ -281,139 +278,123 @@
{:pre [(map? image)]}
(ImageFetched. image))
;; --- Delete Images
(defrecord DeleteImage [id]
ptk/UpdateEvent
(update [_ state]
(-> state
(update :images dissoc id)
(update-in [:dashboard :images :selected] disj id)))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation! :delete-image {:id id})
(rx/ignore))))
(defn delete-image
[id]
{:pre [(uuid? id)]}
(DeleteImage. id))
;; --- Rename Image
(defrecord RenameImage [id name]
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:images id :name] name))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (persist-image id))))
(defn rename-image
[id name]
{:pre [(uuid? id) (string? name)]}
(RenameImage. id name))
(us/verify ::us/uuid id)
(us/verify ::us/string name)
(ptk/reify ::rename-image
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:images id :name] name))
;; --- Select image
ptk/WatchEvent
(watch [_ state stream]
(rx/of (persist-image id)))))
(defrecord SelectImage [id]
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard :images :selected] conj id)))
;; --- Image Selection
(defrecord DeselectImage [id]
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard :images :selected] disj id)))
(defrecord ToggleImageSelection [id]
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])]
(rx/of
(if (selected id)
(DeselectImage. id)
(SelectImage. id))))))
(defn select-image
[id]
(ptk/reify ::select-image
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard-images :selected] (fnil conj #{}) id))))
(defn deselect-image
[id]
{:pre [(uuid? id)]}
(DeselectImage. id))
(ptk/reify ::deselect-image
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard-images :selected] (fnil disj #{}) id))))
(defn toggle-image-selection
(def deselect-all-images
(ptk/reify ::deselect-all-images
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:dashboard-images :selected] #{}))))
;; --- Delete Images
(defn delete-image
[id]
(ToggleImageSelection. id))
(us/verify ::us/uuid id)
(ptk/reify ::delete-image
ptk/UpdateEvent
(update [_ state]
(update state :images dissoc id))
;; --- Copy Selected Image
(defrecord CopySelected [id]
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])]
ptk/WatchEvent
(watch [_ state s]
(rx/merge
(->> (rx/from selected)
(rx/flat-map #(rp/mutation! :copy-image {:id % :collection-id id}))
(rx/map image-created))
(->> (rx/from selected)
(rx/map deselect-image))))))
(defn copy-selected
[id]
{:pre [(or (uuid? id) (nil? id))]}
(CopySelected. id))
;; --- Move Selected Image
(defrecord MoveSelected [id]
ptk/UpdateEvent
(update [_ state]
(let [selected (get-in state [:dashboard :images :selected])]
(reduce (fn [state image]
(assoc-in state [:images image :collection] id))
state
selected)))
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])]
(rx/merge
(->> (rx/from selected)
(rx/map persist-image))
(->> (rx/from selected)
(rx/map deselect-image))))))
(defn move-selected
[id]
{:pre [(or (uuid? id) (nil? id))]}
(MoveSelected. id))
(rx/of deselect-all-images)
(->> (rp/mutation! :delete-image {:id id})
(rx/ignore))))))
;; --- Delete Selected
(defrecord DeleteSelected []
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard :images :selected])]
(->> (rx/from selected)
(rx/map delete-image)))))
(defn delete-selected
[]
(DeleteSelected.))
(def delete-selected
(ptk/reify ::delete-selected
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:dashboard-images :selected])]
(->> (rx/from selected)
(rx/map delete-image))))))
;; --- Update Opts (Filtering & Ordering)
(defrecord UpdateOpts [order filter edition]
ptk/UpdateEvent
(update [_ state]
(update-in state [:dashboard :images] merge
{:edition edition}
(when order {:order order})
(when filter {:filter filter}))))
(defn update-opts
[& {:keys [order filter edition]
:or {edition false}
:as opts}]
(UpdateOpts. order filter edition))
:or {edition false}}]
(ptk/reify ::update-opts
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-images merge
{:edition edition}
(when order {:order order})
(when filter {:filter filter})))))
;; --- Copy Selected Image
;; (defrecord CopySelected [id]
;; ptk/WatchEvent
;; (watch [_ state stream]
;; (let [selected (get-in state [:dashboard-images :selected])]
;; (rx/merge
;; (->> (rx/from selected)
;; (rx/flat-map #(rp/mutation! :copy-image {:id % :collection-id id}))
;; (rx/map image-created))
;; (->> (rx/from selected)
;; (rx/map deselect-image))))))
;; (defn copy-selected
;; [id]
;; {:pre [(or (uuid? id) (nil? id))]}
;; (CopySelected. id))
;; --- Move Selected Image
;; (defrecord MoveSelected [id]
;; ptk/UpdateEvent
;; (update [_ state]
;; (let [selected (get-in state [:dashboard-images :selected])]
;; (reduce (fn [state image]
;; (assoc-in state [:images image :collection] id))
;; state
;; selected)))
;; ptk/WatchEvent
;; (watch [_ state stream]
;; (let [selected (get-in state [:dashboard-images :selected])]
;; (rx/merge
;; (->> (rx/from selected)
;; (rx/map persist-image))
;; (->> (rx/from selected)
;; (rx/map deselect-image))))))
;; (defn move-selected
;; [id]
;; {:pre [(or (uuid? id) (nil? id))]}
;; (MoveSelected. id))

View file

@ -54,6 +54,7 @@
;; --- Declarations
(declare fetch-users)
(declare fetch-images)
(declare handle-who)
(declare handle-pointer-update)
(declare handle-pointer-send)
@ -289,7 +290,8 @@
(rx/of (dp/fetch-file file-id)
(dp/fetch-pages file-id)
(initialize-layout file-id)
(fetch-users file-id))
(fetch-users file-id)
(fetch-images file-id))
(->> (rx/zip (rx/filter (ptk/type? ::dp/pages-fetched) stream)
(rx/filter (ptk/type? ::dp/files-fetched) stream))
(rx/take 1)
@ -399,6 +401,7 @@
state
users))))
;; --- Toggle layout flag
(defn toggle-layout-flag
@ -1303,6 +1306,94 @@
query-params {:page-id page-id}]
(rx/of (rt/nav :workspace path-params query-params))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Images
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Fetch Workspace Images
(declare images-fetched)
(defn fetch-images
[file-id]
(ptk/reify ::fetch-images
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :project-file-images {:file-id file-id})
(rx/map images-fetched)))))
(defn images-fetched
[images]
(ptk/reify ::images-fetched
ptk/UpdateEvent
(update [_ state]
(let [images (d/index-by :id images)]
(assoc state :workspace-images images)))))
;; --- Upload Image
(declare image-uploaded)
(def allowed-file-types #{"image/jpeg" "image/png"})
(defn upload-image
([file] (upload-image file identity))
([file on-uploaded]
(us/verify fn? on-uploaded)
(ptk/reify ::upload-image
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :uploading] true))
ptk/WatchEvent
(watch [_ state stream]
(let [allowed-file? #(contains? allowed-file-types (.-type %))
finalize-upload #(assoc-in % [:workspace-local :uploading] false)
file-id (get-in state [:workspace-page :file-id])
on-success #(do (st/emit! finalize-upload)
(on-uploaded %))
on-error #(do (st/emit! finalize-upload)
(rx/throw %))
prepare
(fn [file]
{:name (.-name file)
:file-id file-id
:content file})]
(->> (rx/of file)
(rx/filter allowed-file?)
(rx/map prepare)
(rx/mapcat #(rp/mutation! :upload-project-file-image %))
(rx/do on-success)
(rx/map image-uploaded)
(rx/catch on-error)))))))
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::width ::us/number)
(s/def ::height ::us/number)
(s/def ::mtype ::us/string)
(s/def ::uri ::us/string)
(s/def ::thumb-uri ::us/string)
(s/def ::image
(s/keys :req-un [::id
::name
::width
::height
::uri
::thumb-uri]))
(defn image-uploaded
[item]
(us/verify ::image item)
(ptk/reify ::image-created
ptk/UpdateEvent
(update [_ state]
(update state :workspace-images assoc (:id item) item))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Page Changes Reactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -2,6 +2,9 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2017-2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.refs
@ -35,6 +38,10 @@
(-> (l/key :workspace-file)
(l/derive st/state)))
(def workspace-images
(-> (l/key :workspace-images)
(l/derive st/state)))
(def workspace-users
(-> (l/key :workspace-users)
(l/derive st/state)))

View file

@ -112,7 +112,15 @@
([id] (mutation id {}))
([id params] (mutation id params)))
(defmethod mutation :create-image
(defmethod mutation :upload-image
[id params]
(let [form (js/FormData.)]
(run! (fn [[key val]]
(.append form (name key) val))
(seq params))
(send-mutation! id form)))
(defmethod mutation :upload-project-file-image
[id params]
(let [form (js/FormData.)]
(run! (fn [[key val]]

View file

@ -7,9 +7,12 @@
(ns uxbox.main.ui.dashboard.icons
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.common.data :as d]
[uxbox.common.spec :as us]
[uxbox.builtins.icons :as i]
[uxbox.main.data.icons :as di]
[uxbox.main.store :as st]
@ -21,7 +24,7 @@
[uxbox.util.components :refer [chunked-list]]
[uxbox.util.data :refer [read-string jscoll->vec seek]]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :as t :refer [tr]]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]
[uxbox.util.time :as dt]))
@ -51,111 +54,200 @@
icons
(filter #(contains-term? (:name %) term) icons)))
;; --- Refs
(def collections-iref
(-> (l/key :icons-collections)
(l/derive st/state)))
(def opts-iref
(-> (l/in [:dashboard :icons])
(l/derive st/state)))
;; --- Component: Grid Header
(mf/defc grid-header
[{:keys [coll] :as props}]
(letfn [(on-change [name]
(st/emit! (di/rename-collection (:id coll) name)))
(delete []
(st/emit!
(di/delete-collection (:id coll))
(rt/nav :dashboard/icons nil {:type (:type coll)})))
(on-delete []
(modal/show! confirm-dialog {:on-accept delete}))]
[:& common/grid-header {:value (:name coll)
[{:keys [collection] :as props}]
(let [{:keys [id type]} collection
on-change #(st/emit! (di/rename-collection id %))
on-deleted #(st/emit! (rt/nav :dashboard-icons nil {:type type}))
delete #(st/emit! (di/delete-collection id on-deleted))
on-delete #(modal/show! confirm-dialog {:on-accept delete})]
[:& common/grid-header {:value (:name collection)
:on-change on-change
:on-delete on-delete}]))
;; --- Nav
(mf/defc nav-item
[{:keys [coll selected?] :as props}]
[{:keys [collection selected?] :as props}]
(let [local (mf/use-state {})
{:keys [id type name]} coll
editable? (= type :own)]
(letfn [(on-click [event]
(let [type (or type :own)]
(st/emit! (rt/nav :dashboard-icons {} {:type type :id id}))))
(on-input-change [event]
(-> (dom/get-target event)
(dom/get-value)
(swap! local assoc :name)))
(on-cancel [event]
(swap! local dissoc :name :edit))
(on-double-click [event]
(when editable?
(swap! local assoc :edit true)))
(on-input-keyup [event]
(when (kbd/enter? event)
(let [value (-> (dom/get-target event) (dom/get-value))]
(st/emit! (di/rename-collection id (str/trim (:name @local))))
(swap! local assoc :edit false))))]
[:li {:on-click on-click
:on-double-click on-double-click
:class-name (when selected? "current")}
(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")])])))
{:keys [id type name]} collection
editable? (= type :own)
on-click
(fn [event]
(let [type (or type :own)]
(st/emit! (rt/nav :dashboard-icons {} {:type type :id id}))))
on-input-change
(fn [event]
(-> (dom/get-target event)
(dom/get-value)
(swap! local assoc :name)))
on-cancel #(swap! local dissoc :name :edit)
on-double-click #(when editable? (swap! local assoc :edit true))
on-input-keyup
(fn [event]
(when (kbd/enter? event)
(let [value (-> (dom/get-target event) (dom/get-value))]
(st/emit! (di/rename-collection id (str/trim (:name @local))))
(swap! local assoc :edit false))))]
[:li {:on-click on-click
:on-double-click on-double-click
:class-name (when selected? "current")}
(if (:edit @local)
[:div
[:input.element-title {:value (or (:name @local) name)
:on-change on-input-change
:on-key-down on-input-keyup}]
[:span.close {:on-click on-cancel} i/close]]
[:span.element-title name])]))
(mf/defc nav
[{:keys [id type colls selected-coll] :as props}]
(let [own? (= type :own)
[{:keys [id type collections] :as props}]
(let [locale (i18n/use-locale)
own? (= type :own)
builtin? (= type :builtin)
select-tab #(st/emit! (rt/nav :dashboard-icons nil {:type %}))]
create-collection #(st/emit! di/create-collection)
select-own-tab #(st/emit! (rt/nav :dashboard-icons nil {:type :own}))
select-buitin-tab #(st/emit! (rt/nav :dashboard-icons nil {:type :builtin}))]
[:div.library-bar
[:div.library-bar-inside
;; Tabs
[:ul.library-tabs
[:li {:class-name (when own? "current")
:on-click (partial select-tab :own)}
(tr "ds.your-icons-title")]
[:li {:class-name (when builtin? "current")
:on-click (partial select-tab :builtin)}
(tr "ds.store-icons-title")]]
[:li {:class (when own? "current")
:on-click select-own-tab}
(t locale "ds.your-icons-title")]
[:li {:class (when builtin? "current")
:on-click select-buitin-tab}
(t locale "ds.store-icons-title")]]
;; Collections List
[:ul.library-elements
(when own?
[:li
[:a.btn-primary {:on-click #(st/emit! di/create-collection)}
(tr "ds.icons-collection.new")]])
(when own?
[:& nav-item {:selected? (nil? id)}])
(for [item colls]
[:& nav-item {:coll item
(for [item collections]
[:& nav-item {:collection item
:selected? (= (:id item) id)
:key (:id item)}])]]]))
;; --- Grid
;; (mf/def grid-options-tooltip
;; :mixins [mf/reactive mf/memo]
;; :render
;; (fn [own {:keys [selected on-select title]}]
;; {:pre [(uuid? selected)
;; (fn? on-select)
;; (string? title)]}
;; (let [colls (mf/react collections-iref)
;; colls (->> (vals colls)
;; (filter #(= :own (:type %)))
;; (remove #(= selected (:id %)))
;; (sort-by :name colls))
;; on-select (fn [event id]
;; (dom/prevent-default event)
;; (dom/stop-propagation event)
;; (on-select id))]
;; [:ul.move-list
;; [:li.title title]
;; [:li
;; [:a {:href "#" :on-click #(on-select % nil)} "Storage"]]
;; (for [{:keys [id name] :as coll} colls]
;; [:li {:key (pr-str id)}
;; [:a {:on-click #(on-select % id)} name]])])))
(mf/defc grid-options
[{:keys [id type selected] :as props}]
(let [local (mf/use-state {})
delete #(st/emit! di/delete-selected)
on-delete #(modal/show! confirm-dialog {:on-accept delete})
;; (on-toggle-copy [event]
;; (swap! local update :show-copy-tooltip not))
;; (on-toggle-move [event]
;; (swap! local update :show-move-tooltip not))
;; (on-copy [selected]
;; (swap! local assoc
;; :show-move-tooltip false
;; :show-copy-tooltip false)
;; (st/emit! (di/copy-selected selected)))
;; (on-move [selected]
;; (swap! local assoc
;; :show-move-tooltip false
;; :show-copy-tooltip false)
;; (st/emit! (di/move-selected selected)))
;; (on-rename [event]
;; (let [selected (first selected)]
;; (st/emit! (di/update-opts :edition selected))))
]
;; MULTISELECT OPTIONS BAR
[:div.multiselect-bar
(when (= type :own)
;; If editable
[:div.multiselect-nav
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.copy")
;; :on-click on-toggle-copy}
;; (when (:show-copy-tooltip @local)
;; [:& grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.copy-to-library")
;; :on-select on-copy}])
;; i/copy]
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.move")
;; :on-click on-toggle-move}
;; (when (:show-move-tooltip @local)
;; [:& grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.move-to-library")
;; :on-select on-move}])
;; i/move]
;; (when (= 1 (count selected))
;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.rename")
;; :on-click on-rename}
;; i/pencil])
[:span.delete.tooltip.tooltip-top
{:alt (tr "ds.multiselect-bar.delete")
:on-click on-delete}
i/trash]]
;; If not editable
;; [:div.multiselect-nav
;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.copy")
;; :on-click on-toggle-copy}
;; (when (:show-copy-tooltip @local)
;; [:& grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.copy-to-library")
;; :on-select on-copy}])
;; i/organize]]
)]))
;; --- Grid Form
(mf/defc grid-form
[{:keys [id type uploading?] :as props}]
(let [input (mf/use-ref nil)
(let [locale (i18n/use-locale)
input (mf/use-ref nil)
on-click #(dom/click (mf/ref-node input))
on-select #(st/emit! (->> (dom/get-event-files %)
(jscoll->vec)
on-select #(st/emit! (->> (dom/get-target %)
(dom/get-files)
(array-seq)
(di/create-icons id)))]
[:div.grid-item.add-project {:on-click on-click}
(if uploading?
[:div i/loader-pencil]
[:span (tr "ds.icon-new")])
[:span (t locale "ds.icon-new")])
[:input.upload-icon-input
{:style {:display "none"}
:multiple true
@ -165,122 +257,36 @@
:type "file"
:on-change on-select}]]))
(mf/def grid-options-tooltip
:mixins [mf/reactive mf/memo]
:render
(fn [own {:keys [selected on-select title]}]
{:pre [(uuid? selected)
(fn? on-select)
(string? title)]}
(let [colls (mf/react collections-iref)
colls (->> (vals colls)
(filter #(= :own (:type %)))
(remove #(= selected (:id %)))
(sort-by :name colls))
on-select (fn [event id]
(dom/prevent-default event)
(dom/stop-propagation event)
(on-select id))]
[:ul.move-list
[:li.title title]
[:li
[:a {:href "#" :on-click #(on-select % nil)} "Storage"]]
(for [{:keys [id name] :as coll} colls]
[:li {:key (pr-str id)}
[:a {:on-click #(on-select % id)} name]])])))
;; (mf/def grid-options
;; :mixins [(mf/local) mf/memo]
;; :render
;; (fn [{:keys [::mf/local] :as own}
;; {:keys [id type selected] :as props}]
;; (letfn [(delete []
;; (st/emit! (di/delete-selected)))
;; (on-delete [event]
;; (modal/show! confirm-dialog {:on-accept delete}))
;; (on-toggle-copy [event]
;; (swap! local update :show-copy-tooltip not))
;; (on-toggle-move [event]
;; (swap! local update :show-move-tooltip not))
;; (on-copy [selected]
;; (swap! local assoc
;; :show-move-tooltip false
;; :show-copy-tooltip false)
;; (st/emit! (di/copy-selected selected)))
;; (on-move [selected]
;; (swap! local assoc
;; :show-move-tooltip false
;; :show-copy-tooltip false)
;; (st/emit! (di/move-selected selected)))
;; (on-rename [event]
;; (let [selected (first selected)]
;; (st/emit! (di/update-opts :edition selected))))]
;; ;; MULTISELECT OPTIONS BAR
;; [:div.multiselect-bar
;; (if (or (= type :own) (nil? id))
;; ;; if editable
;; [:div.multiselect-nav {}
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.copy")
;; :on-click on-toggle-copy}
;; (when (:show-copy-tooltip @local)
;; (grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.copy-to-library")
;; :on-select on-copy}))
;; i/copy]
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.move")
;; :on-click on-toggle-move}
;; (when (:show-move-tooltip @local)
;; (grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.move-to-library")
;; :on-select on-move}))
;; i/move]
;; (when (= 1 (count selected))
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.rename")
;; :on-click on-rename}
;; i/pencil])
;; [:span.delete.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.delete")
;; :on-click on-delete}
;; i/trash]]
;; ;; if not editable
;; [:div.multiselect-nav
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.copy")
;; :on-click on-toggle-copy}
;; (when (:show-copy-tooltip @local)
;; (grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.copy-to-library")
;; :on-select on-copy}))
;; i/organize]])])))
;; --- Grid Item
(mf/defc grid-item
[{:keys [icon selected? edition?] :as props}]
(letfn [(toggle-selection [event]
(st/emit! (di/toggle-icon-selection (:id icon))))
(on-key-down [event]
(when (kbd/enter? event)
(on-blur event)))
(on-blur [event]
(let [target (dom/event->target event)
name (dom/get-value target)]
(st/emit! (di/update-opts :edition false)
(di/rename-icon (:id icon) name))))
(ignore-click [event]
(dom/stop-propagation event)
(dom/prevent-default event))
(on-edit [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (di/update-opts :edition (:id icon))))]
(let [toggle-selection #(st/emit! (if selected?
(di/deselect-icon (:id icon))
(di/select-icon (:id icon))))
on-blur
(fn [event]
(let [target (dom/get-target event)
name (dom/get-value target)]
(st/emit! (di/update-opts :edition false)
(di/rename-icon (:id icon) name))))
on-key-down
(fn [event]
(when (kbd/enter? event)
(on-blur event)))
ignore-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event))
on-edit
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (di/update-opts :edition (:id icon))))]
[:div.grid-item.small-item.project-th
[:div.input-checkbox.check-primary
[:input {:type "checkbox"
@ -304,18 +310,13 @@
;; --- Grid
(defn make-icons-iref
[id]
(-> (comp (l/key :icons)
(l/lens (fn [icons]
(->> (vals icons)
(filter #(= id (:collection-id %)))))))
(def icons-iref
(-> (comp (l/key :icons) (l/lens vals))
(l/derive st/state)))
(mf/defc grid
[{:keys [id type coll opts] :as props}]
(let [editable? (or (= type :own) (nil? id))
icons-iref (mf/use-memo #(make-icons-iref id) #js [id])
[{:keys [id type collection opts] :as props}]
(let [editable? (= type :own)
icons (->> (mf/deref icons-iref)
(filter-icons-by (:filter opts ""))
(sort-icons-by (:order opts :name)))]
@ -336,44 +337,48 @@
;; --- Content
(def opts-iref
(-> (l/key :dashboard-icons)
(l/derive st/state)))
(mf/defc content
[{:keys [id type coll] :as props}]
(let [opts (mf/deref opts-iref)]
[:*
[:section.dashboard-grid.library
(when coll
[:& grid-header {:coll coll}])
[:& grid {:id id
:key [id type]
:type type
:coll coll
:opts opts}]
(when (seq (:selected opts))
#_[:& grid-options {:id id :type type :selected (:selected opts)}])]]))
[{:keys [id type collection] :as props}]
(let [{:keys [selected] :as opts} (mf/deref opts-iref)]
[:section.dashboard-grid.library
(when collection
[:& grid-header {:collection collection}])
(if collection
[:& grid {:id id :type type :collection collection :opts opts}]
[:span "EMPTY STATE TODO"])
(when-not (empty? selected)
#_[:& grid-options {:id id :type type :selected (:selected opts)}])]))
;; --- Icons Page
(def collections-iref
(-> (l/key :icons-collections)
(l/derive st/state)))
(mf/defc icons-page
[{:keys [id type] :as props}]
(let [type (or type :own)
colls (mf/deref collections-iref)
colls (cond->> (vals colls)
(= type :own) (filter #(= :own (:type %)))
(= type :builtin) (filter #(= :builtin (:type %)))
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)]
collections (mf/deref collections-iref)
collections (cond->> (vals collections)
(= type :own) (filter #(= :own (:type %)))
(= type :builtin) (filter #(= :builtin (:type %)))
true (sort-by :created-at))
collection (cond
(uuid? id) (seek #(= id (:id %)) collections)
:else (first collections))
id (:id collection)]
(mf/use-effect #(st/emit! di/fetch-collections))
(mf/use-effect {:fn #(st/emit! (di/fetch-icons id))
:deps #js [(str id)]})
(mf/use-effect
{:fn #(when id (st/emit! (di/initialize id)))
:deps (mf/deps id)})
[:section.dashboard-content
[:& nav {:type type
:id id
:colls colls}]
[:& content {:type type
:id id
:coll selected-coll}]]))
[:& nav {:type type :id id :collections collections}]
[:& content {:type type :id id :collection collection}]]))

View file

@ -2,55 +2,42 @@
;; 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 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.dashboard.images
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.core :as mx]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.common.data :as d]
[uxbox.common.spec :as us]
[uxbox.main.data.images :as di]
[uxbox.main.data.lightbox :as udl]
[uxbox.main.store :as st]
[uxbox.main.ui.confirm :refer [confirm-dialog]]
[uxbox.main.ui.dashboard.common :as common]
[uxbox.main.ui.keyboard :as kbd]
[uxbox.main.ui.lightbox :as lbx]
[uxbox.main.ui.confirm :refer [confirm-dialog]]
[uxbox.main.ui.modal :as modal]
[uxbox.util.data :refer [read-string jscoll->vec seek]]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :as t :refer [tr]]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.router :as rt]
[uxbox.util.time :as dt]))
;; --- Refs
(def collections-iref
(-> (l/key :images-collections)
(l/derive st/state)))
(def opts-iref
(-> (l/in [:dashboard :images])
(l/derive st/state)))
;; --- Page Title
(mf/defc grid-header
[{:keys [coll] :as props}]
(letfn [(on-change [name]
(st/emit! (di/rename-collection (:id coll) name)))
(delete []
(st/emit!
(di/delete-collection (:id coll))
(rt/nav :dashboard/images nil {:type (:type coll)})))
(on-delete []
(modal/show! confirm-dialog {:on-accept delete}))]
[:& common/grid-header {:value (:name coll)
[{:keys [collection] :as props}]
(let [{:keys [id type]} collection
on-change #(st/emit! (di/rename-collection id %))
on-deleted #(st/emit! (rt/nav :dashboard-images nil {:type type}))
delete #(st/emit! (di/delete-collection id on-deleted))
on-delete #(modal/show! confirm-dialog {:on-accept delete})]
[:& common/grid-header {:value (:name collection)
:on-change on-change
:on-delete on-delete}]))
@ -61,6 +48,7 @@
(let [local (mf/use-state {})
{:keys [id type name num-images]} coll
editable? (= type :own)
on-click
(fn [event]
(let [type (or type :own)]
@ -68,6 +56,7 @@
on-cancel-edition #(swap! local dissoc :edit)
on-double-click #(when editable? (swap! local assoc :edit true))
on-input-keyup
(fn [event]
(when (kbd/enter? event)
@ -76,6 +65,7 @@
(str/trim))]
(st/emit! (di/rename-collection id value))
(swap! local assoc :edit false))))]
[:li {:on-click on-click
:on-double-click on-double-click
:class-name (when selected? "current")}
@ -87,155 +77,128 @@
[:span.element-title (if id name "Storage")])]))
(mf/defc nav
[{:keys [id type colls] :as props}]
(let [own? (= type :own)
[{:keys [id type collections] :as props}]
(let [locale (i18n/use-locale)
own? (= type :own)
builtin? (= type :builtin)
select-tab #(st/emit! (rt/nav :dashboard-images nil {:type %}))]
create-collection #(st/emit! di/create-collection)
select-own-tab #(st/emit! (rt/nav :dashboard-images nil {:type :own}))
select-buitin-tab #(st/emit! (rt/nav :dashboard-images nil {:type :builtin}))]
[:div.library-bar
[:div.library-bar-inside
[:ul.library-tabs
[:li {:class-name (when own? "current")
:on-click (partial select-tab :own)}
(tr "ds.your-images-title")]
[:li {:class-name (when builtin? "current")
:on-click (partial select-tab :builtin)}
(tr "ds.store-images-title")]]
;; Tabs
[:ul.library-tabs
[:li {:class (when own? "current")
:on-click select-own-tab}
(t locale "ds.your-images-title")]
[:li {:class (when builtin? "current")
:on-click select-buitin-tab}
(t locale "ds.store-images-title")]]
;; Collections List
[:ul.library-elements
(when own?
[:li
[:a.btn-primary {:on-click #(st/emit! di/create-collection)}
(tr "ds.images-collection.new")]])
(when own?
[:& nav-item {:selected? (nil? id)}])
(for [item colls]
[:a.btn-primary {:on-click create-collection}
(t locale "ds.images-collection.new")]])
(for [item collections]
[:& nav-item {:coll item
:selected? (= (:id item) id)
:key (:id item)}])]]]))
;; --- Grid
(mf/defc grid-options-tooltip
[{:keys [selected on-select title] :as props}]
{:pre [(uuid? selected)
(fn? on-select)
(string? title)]}
(let [colls (mf/deref collections-iref)
colls (->> (vals colls)
(filter #(= :own (:type %)))
(remove #(= selected (:id %)))
#_(sort-by :name colls))
on-select (fn [event id]
(dom/prevent-default event)
(dom/stop-propagation event)
(on-select id))]
[:ul.move-list
[:li.title title]
[:li
(when (not (nil? selected))
[:a {:href "#" :on-click #(on-select % nil)} "Storage"])]
(for [{:keys [id name] :as coll} colls]
[:li {:key (pr-str id)}
[:a {:on-click #(on-select % id)} name]])]))
;; (mf/defc grid-options-tooltip
;; [{:keys [selected on-select title] :as props}]
;; {:pre [(uuid? selected)
;; (fn? on-select)
;; (string? title)]}
;; (let [colls (mf/deref collections-iref)
;; colls (->> (vals colls)
;; (filter #(= :own (:type %)))
;; (remove #(= selected (:id %)))
;; #_(sort-by :name colls))
;; on-select (fn [event id]
;; (dom/prevent-default event)
;; (dom/stop-propagation event)
;; (on-select id))]
;; [:ul.move-list
;; [:li.title title]
;; [:li
;; (when (not (nil? selected))
;; [:a {:href "#" :on-click #(on-select % nil)} "Storage"])]
;; (for [{:keys [id name] :as coll} colls]
;; [:li {:key (pr-str id)}
;; [:a {:on-click #(on-select % id)} name]])]))
(mf/defc grid-options
[{:keys [id type selected] :as props}]
(let [local (mf/use-state {})]
(letfn [(delete []
(st/emit! (di/delete-selected)))
(on-delete [event]
(modal/show! confirm-dialog {:on-accept delete}))
(on-toggle-copy [event]
(swap! local update :show-copy-tooltip not))
(on-toggle-move [event]
(swap! local update :show-move-tooltip not))
(on-copy [selected]
(swap! local assoc
:show-move-tooltip false
:show-copy-tooltip false)
(st/emit! (di/copy-selected selected)))
(on-move [selected]
(swap! local assoc
:show-move-tooltip false
:show-copy-tooltip false)
(st/emit! (di/move-selected selected)))
(on-rename [event]
(let [selected (first selected)]
(st/emit! (di/update-opts :edition selected))))]
;; MULTISELECT OPTIONS BAR
[:div.multiselect-bar
(if (or (= type :own) (nil? id))
;; If editable
[:div.multiselect-nav
[:span.move-item.tooltip.tooltip-top
{:alt (tr "ds.multiselect-bar.copy")
:on-click on-toggle-copy}
(when (:show-copy-tooltip @local)
[:& grid-options-tooltip {:selected id
:title (tr "ds.multiselect-bar.copy-to-library")
:on-select on-copy}])
i/copy]
[:span.move-item.tooltip.tooltip-top
{:alt (tr "ds.multiselect-bar.move")
:on-click on-toggle-move}
(when (:show-move-tooltip @local)
[:& grid-options-tooltip {:selected id
:title (tr "ds.multiselect-bar.move-to-library")
:on-select on-move}])
i/move]
(when (= 1 (count selected))
[:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.rename")
:on-click on-rename}
i/pencil])
[:span.delete.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.delete")
:on-click on-delete}
i/trash]]
(let [local (mf/use-state {})
delete #(st/emit! di/delete-selected)
on-delete #(modal/show! confirm-dialog {:on-accept delete})
;; If not editable
[:div.multiselect-nav
[:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.copy")
:on-click on-toggle-copy}
(when (:show-copy-tooltip @local)
[:& grid-options-tooltip {:selected id
:title (tr "ds.multiselect-bar.copy-to-library")
:on-select on-copy}])
i/organize]])])))
;; (on-toggle-copy [event]
;; (swap! local update :show-copy-tooltip not))
;; (on-toggle-move [event]
;; (swap! local update :show-move-tooltip not))
;; (on-copy [selected]
;; (swap! local assoc
;; :show-move-tooltip false
;; :show-copy-tooltip false)
;; (st/emit! (di/copy-selected selected)))
;; (on-move [selected]
;; (swap! local assoc
;; :show-move-tooltip false
;; :show-copy-tooltip false)
;; (st/emit! (di/move-selected selected)))
;; (on-rename [event]
;; (let [selected (first selected)]
;; (st/emit! (di/update-opts :edition selected))))
]
;; MULTISELECT OPTIONS BAR
[:div.multiselect-bar
(when (= type :own)
;; If editable
[:div.multiselect-nav
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.copy")
;; :on-click on-toggle-copy}
;; (when (:show-copy-tooltip @local)
;; [:& grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.copy-to-library")
;; :on-select on-copy}])
;; i/copy]
;; [:span.move-item.tooltip.tooltip-top
;; {:alt (tr "ds.multiselect-bar.move")
;; :on-click on-toggle-move}
;; (when (:show-move-tooltip @local)
;; [:& grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.move-to-library")
;; :on-select on-move}])
;; i/move]
;; (when (= 1 (count selected))
;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.rename")
;; :on-click on-rename}
;; i/pencil])
[:span.delete.tooltip.tooltip-top
{:alt (tr "ds.multiselect-bar.delete")
:on-click on-delete}
i/trash]]
;; If not editable
;; [:div.multiselect-nav
;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.copy")
;; :on-click on-toggle-copy}
;; (when (:show-copy-tooltip @local)
;; [:& grid-options-tooltip {:selected id
;; :title (tr "ds.multiselect-bar.copy-to-library")
;; :on-select on-copy}])
;; i/organize]]
)]))
(mf/defc grid-item
[{:keys [image selected? edition?] :as props}]
(letfn [(toggle-selection [event]
(st/emit! (di/toggle-image-selection (:id image))))
(on-key-down [event]
(when (kbd/enter? event)
(on-blur event)))
(on-blur [event]
(let [target (dom/event->target event)
name (dom/get-value target)]
(st/emit! (di/update-opts :edition false)
(di/rename-image (:id image) name))))
(on-edit [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (di/update-opts :edition (:id image))))]
[:div.grid-item.images-th
[:div.grid-item-th {:style {:background-image (str "url('" (:thumbnail image) "')")}}
[:div.input-checkbox.check-primary
[:input {:type "checkbox"
:id (:id image)
:on-change toggle-selection
:checked selected?}]
[:label {:for (:id image)}]]]
[:div.item-info
(if edition?
[:input.element-name {:type "text"
:auto-focus true
:on-key-down on-key-down
:on-blur on-blur
:on-click on-edit
:default-value (:name image)}]
[:h3 {:on-double-click on-edit} (:name image)])
[:span.date (str (tr "ds.uploaded-at"
(dt/format (:created-at image) "dd/MM/yyyy")))]]]))
;; --- Grid Form
@ -243,8 +206,9 @@
[{:keys [id type uploading?] :as props}]
(let [input (mf/use-ref nil)
on-click #(dom/click (mf/ref-node input))
on-select #(st/emit! (->> (dom/get-event-files %)
(jscoll->vec)
on-select #(st/emit! (->> (dom/get-target %)
(dom/get-files)
(array-seq)
(di/create-images id)))]
[:div.grid-item.add-project {:on-click on-click}
(if uploading?
@ -255,26 +219,76 @@
:multiple true
:ref input
:value ""
:accept "image/jpeg,image/png"
:accept "image/jpeg,image/png,image/webp"
:type "file"
:on-change on-select}]]))
;; --- Grid Item
(mf/defc grid-item
[{:keys [image selected? edition?] :as props}]
(let [toggle-selection #(st/emit! (if selected?
(di/deselect-image (:id image))
(di/select-image (:id image))))
on-blur
(fn [event]
(let [target (dom/get-target event)
name (dom/get-value target)]
(st/emit! (di/update-opts :edition false)
(di/rename-image (:id image) name))))
on-key-down
(fn [event]
(when (kbd/enter? event)
(on-blur event)))
on-edit
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (di/update-opts :edition (:id image))))
background (str "url('" (:thumb-uri image) "')")]
[:div.grid-item.images-th
[:div.grid-item-th {:style {:background-image background}}
[:div.input-checkbox.check-primary
[:input {:type "checkbox"
:id (:id image)
:on-change toggle-selection
:checked selected?}]
[:label {:for (:id image)}]]]
[:div.item-info
(if edition?
[:input.element-name {:type "text"
:auto-focus true
:on-key-down on-key-down
:on-blur on-blur
:on-click on-edit
:default-value (:name image)}]
[:h3 {:on-double-click on-edit} (:name image)])
[:span.date (tr "ds.uploaded-at" (dt/format (:created-at image) "dd/MM/yyyy"))]]]))
;; --- Grid
(defn- make-images-iref
[id type]
(letfn [(selector-fn [state]
(let [images (vals (:images state))]
(filterv #(= id (:collection-id %)) images)))]
(-> (l/lens selector-fn)
(l/derive st/state))))
;; (defn- make-images-iref
;; [collection-id]
;; (letfn [(selector [state]
;; (->> (vals (:images state))
;; (filterv #(= (:collection-id %) collection-id))))]
;; (-> (l/lens selector)
;; (l/derive st/state))))
(def images-iref
(-> (comp (l/key :images) (l/lens vals))
(l/derive st/state)))
(mf/defc grid
[{:keys [id type coll opts] :as props}]
(let [editable? (or (= type :own) (nil? id))
images-iref (mf/use-memo
{:fn #(make-images-iref id type)
:deps (mf/deps id type)})
[{:keys [id type collection opts] :as props}]
(let [editable? (= type :own)
;; images-iref (mf/use-memo {:fn #(make-images-iref id)
;; :deps (mf/deps id)})
images (->> (mf/deref images-iref)
(sort-by :created-at))]
[:div.dashboard-grid-content
@ -328,45 +342,46 @@
;; :value filtering}]
;; [:div.clear-search {:on-click on-clear} i/close]]]])))
(mf/defc content
[{:keys [id type coll] :as props}]
(let [opts (mf/deref opts-iref)]
[:*
[:section.dashboard-grid.library
(when coll
[:& grid-header {:coll coll}])
(def opts-iref
(-> (l/key :dashboard-images)
(l/derive st/state)))
[:& grid {:id id
:type type
:coll coll
:opts opts}]
(when (seq (:selected opts))
[:& grid-options {:id id :type type :selected (:selected opts)}])]]))
(mf/defc content
[{:keys [id type collection] :as props}]
(let [{:keys [selected] :as opts} (mf/deref opts-iref)]
[:section.dashboard-grid.library
(when collection
[:& grid-header {:collection collection}])
(if collection
[:& grid {:id id :type type :collection collection :opts opts}]
[:span "EMPTY STATE TODO"])
(when-not (empty? selected)
[:& grid-options {:id id :type type :selected selected}])]))
;; --- Images Page
(def collections-iref
(-> (l/key :images-collections)
(l/derive st/state)))
(mf/defc images-page
[{:keys [id type] :as props}]
(let [colls (mf/deref collections-iref)
colls (cond->> (vals colls)
(= type :own) (filter #(= :own (:type %)))
(= type :builtin) (filter #(= :builtin (:type %)))
true (sort-by :created-at))
(let [collections (mf/deref collections-iref)
collections (cond->> (vals collections)
(= type :own) (filter #(= :own (:type %)))
(= type :builtin) (filter #(= :builtin (:type %)))
true (sort-by :created-at))
coll (cond
(and (= type :own) (nil? id)) nil
(uuid? id) (seek #(= id (:id %)) colls)
:else (first colls))
id (:id coll)]
collection (cond
(uuid? id) (d/seek #(= id (:id %)) collections)
:else (first collections))
id (:id collection)]
(mf/use-effect #(st/emit! di/fetch-collections))
(mf/use-effect {:fn #(st/emit! (di/fetch-images (:id coll)))
:deps #js [(str (:id coll))]})
(mf/use-effect
{:fn #(when id (st/emit! (di/initialize id)))
:deps (mf/deps id)})
[:section.dashboard-content
[:& nav {:type type
:id id
:colls colls}]
[:& content {:type type
:id id
:coll coll}]]))
[:& nav {:type type :id id :collections collections}]
[:& content {:type type :id id :collection collection}]]))

View file

@ -6,7 +6,6 @@
(ns uxbox.main.ui.shapes.image
(:require
[lentes.core :as l]
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.main.data.images :as udi]
@ -17,13 +16,6 @@
[uxbox.main.ui.shapes.common :as common]
[uxbox.util.geom.matrix :as gmt]))
;; --- Refs
(defn image-ref
[id]
(-> (l/in [:images id])
(l/derive st/state)))
;; --- Image Wrapper
(declare image-shape)
@ -31,23 +23,17 @@
(mf/defc image-wrapper
[{:keys [shape] :as props}]
(let [selected (mf/deref refs/selected-shapes)
image (mf/deref (image-ref (:image shape)))
selected? (contains? selected (:id shape))
on-mouse-down #(common/on-mouse-down % shape selected)]
(mf/use-effect #(st/emit! (udi/fetch-image (:image shape))))
(when image
[:g.shape {:class (when selected? "selected")
:on-mouse-down on-mouse-down}
[:& image-shape {:shape shape
:image image}]])))
[:g.shape {:class (when selected? "selected")
:on-mouse-down on-mouse-down}
[:& image-shape {:shape shape}]]))
;; --- Image Shape
(mf/defc image-shape
[{:keys [shape image] :as props}]
(let [{:keys [id rotation modifier-mtx]} shape
[{:keys [shape] :as props}]
(let [{:keys [id rotation modifier-mtx metadata]} shape
shape (cond
(gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx)
@ -60,13 +46,18 @@
rotation
(+ x (/ width 2))
(+ y (/ height 2))))
uri (if (or (> (:thumb-width metadata) width)
(> (:thumb-height metadata) height))
(:thumb-uri metadata)
(:uri metadata))
props (-> (attrs/extract-style-attrs shape)
(assoc :x x
:y y
:id (str "shape-" id)
:preserveAspectRatio "none"
:xlinkHref (:url image)
:xlinkHref uri
:transform transform
:width width
:height height))]

View file

@ -11,13 +11,14 @@
[rumext.alpha :as mf]
[rumext.core :as mx]
[uxbox.builtins.icons :as i]
[uxbox.common.data :as d]
[uxbox.main.data.images :as udi]
[uxbox.main.data.workspace :as dw]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
[uxbox.main.ui.modal :as modal]
[uxbox.util.data :refer [read-string jscoll->vec]]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :as t :refer [tr]]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.uuid :as uuid]))
;; --- Refs
@ -30,8 +31,13 @@
(-> (l/key :images)
(l/derive st/state)))
(def ^:private workspace-images-iref
(-> (comp (l/key :workspace-images)
(l/lens vals))
(l/derive st/state)))
(def ^:private uploading-iref
(-> (l/in [:dashboard :images :uploading])
(-> (l/in [:workspace-local :uploading])
(l/derive st/state)))
;; --- Import Image Modal
@ -41,116 +47,152 @@
(mf/defc import-image-modal
[props]
(let [input (mf/use-ref nil)
uploading? (mf/deref uploading-iref)]
(letfn [(on-upload-click [event]
(let [input-el (mf/ref-node input)]
(dom/click input-el)))
uploading? (mf/deref uploading-iref)
(on-uploaded [[image]]
(let [{:keys [id name width height]} image
shape {:name name
:metadata {:width width
:height height}
:image id}]
(st/emit! (dw/select-for-drawing :image shape))
(modal/hide!)))
on-upload-click #(dom/click (mf/ref-node input))
(on-files-selected [event]
(let [files (dom/get-event-files event)
files (jscoll->vec files)]
(st/emit! (udi/create-images nil files on-uploaded))))
on-uploaded
(fn [{:keys [id name] :as image}]
(let [shape {:name name
:metadata {:width (:width image)
:height (:height image)
:uri (:uri image)
:thumb-width (:thumb-width image)
:thumb-height (:thumb-height image)
:thumb-uri (:thumb-uri image)}}]
(st/emit! (dw/select-for-drawing :image shape))
(modal/hide!)))
(on-select-from-library [event]
(dom/prevent-default event)
(modal/show! import-image-from-coll-modal {}))
on-files-selected
(fn [event]
(st/emit! (-> (dom/get-target event)
(dom/get-files)
(array-seq)
(first)
(dw/upload-image on-uploaded))))
(on-close [event]
(dom/prevent-default event)
(modal/hide!))]
[:div.lightbox-body
[:h3 (tr "image.new")]
[:div.row-flex
[:div.lightbox-big-btn {:on-click on-select-from-library}
[:span.big-svg i/image]
[:span.text (tr "image.select")]]
[:div.lightbox-big-btn {:on-click on-upload-click}
(if uploading?
[:span.big-svg.upload i/loader-pencil]
[:span.big-svg.upload i/exit])
[:span.text (tr "image.upload")]
[:input.upload-image-input
{:style {:display "none"}
:accept "image/jpeg,image/png"
:type "file"
:ref input
:on-change on-files-selected}]]]
[:a.close {:on-click on-close} i/close]])))
on-select-from-library
(fn [event]
(dom/prevent-default event)
(modal/show! import-image-from-coll-modal {}))
on-close
(fn [event]
(dom/prevent-default event)
(modal/hide!))]
[:div.lightbox-body
[:h3 (tr "image.new")]
[:div.row-flex
;; Select from collections
[:div.lightbox-big-btn {:on-click on-select-from-library}
[:span.big-svg i/image]
[:span.text (tr "image.select")]]
;; Select from workspace
[:div.lightbox-big-btn {:on-click on-select-from-library}
[:span.big-svg i/image]
[:span.text (tr "image.select")]]
;; Direct image upload
[:div.lightbox-big-btn {:on-click on-upload-click}
(if uploading?
[:span.big-svg.upload i/loader-pencil]
[:span.big-svg.upload i/exit])
[:span.text (tr "image.upload")]
[:input.upload-image-input
{:style {:display "none"}
:multiple false
:accept "image/jpeg,image/png,image/webp"
:type "file"
:ref input
:on-change on-files-selected}]]]
[:a.close {:on-click on-close} i/close]]))
;; --- Import Image from Collection Modal
(mf/defc image-item
[{:keys [image] :as props}]
(letfn [(on-click [event]
;; TODO: deduplicate this code...
(let [shape {:name (:name image)
:metadata {:width (:width image)
:height (:height image)}
:image (:id image)}]
:height (:height image)
:uri (:uri image)
:thumb-width (:thumb-width image)
:thumb-height (:thumb-height image)
:thumb-uri (:thumb-uri image)}}]
(st/emit! (dw/select-for-drawing :image shape))
(modal/hide!)))]
[:div.library-item {:on-click on-click}
[:div.library-item-th
{:style {:background-image (str "url('" (:thumbnail image) "')")}}]
{:style {:background-image (str "url('" (:thumb-uri image) "')")}}]
[:span (:name image)]]))
(mf/defc image-collection
[{:keys [images] :as props}]
[:div.library-content
(for [image images]
[:& image-item {:image image :key (:id image)}])])
(mf/defc import-image-from-coll-modal
[props]
(let [local (mf/use-state {:id nil :type :own})
id (:id @local)
type (:type @local)
own? (= type :own)
builtin? (= type :builtin)
colls (mf/deref collections-iref)
colls (->> (vals colls)
(filter #(= type (:type %)))
(sort-by :name))
(let [locale (i18n/use-locale)
local (mf/use-state {:collection-id nil :tab :file})
collections (mf/deref collections-iref)
collections (->> (vals collections)
(sort-by :name))
select-tab #(swap! local assoc :tab %)
collection-id (or (:collection-id @local)
(:id (first collections)))
tab (:tab @local)
images (mf/deref images-iref)
images (->> (vals images)
(filter #(= id (:collection %))))
(filter #(= collection-id (:collection-id %))))
workspace-images (mf/deref workspace-images-iref)
on-close #(do (dom/prevent-default %)
(modal/hide!))
select-type #(swap! local assoc :type %)
on-change #(-> (dom/event->value %)
(read-string)
(swap! local assoc :id))]
on-change #(->> (dom/get-target %)
(dom/get-value)
(d/read-string)
(swap! local assoc :collection-id))]
(mf/use-effect #(st/emit! udi/fetch-collections))
(mf/use-effect {:deps #js [(str id)]
:fn #(st/emit! (udi/fetch-images id))})
(mf/use-effect
{:deps (mf/deps collection-id)
:fn #(when collection-id
(st/emit! (udi/fetch-images collection-id)))})
[:div.lightbox-body.big-lightbox
[:h3 (tr "image.import-library")]
[:div.import-img-library
[:div.library-actions
[:ul.toggle-library
[:li.your-images {:class (when own? "current")
:on-click #(select-type :own)}
(tr "ds.your-images-title")]
[:li.standard {:class (when builtin? "current")
:on-click #(select-type :builtin)}
(tr "ds.store-images-title")]]
[:select.input-select {:on-change on-change}
(when own?
[:option {:value (pr-str nil)} "Storage"])
(for [coll colls]
(let [id (:id coll)
name (:name coll)]
[:option {:key (str id) :value (pr-str id)} name]))]]
[:& image-collection {:images images}]]
;; Tabs
[:ul.toggle-library
[:li.your-images {:class (when (= tab :file) "current")
:on-click #(select-tab :file)}
(t locale "ds.your-images-title")]
[:li.standard {:class (when (not= tab :file) "current")
:on-click #(select-tab :collection)}
(t locale "ds.store-images-title")]]
;; Collections dropdown
(when (= tab :collection)
[:select.input-select {:on-change on-change}
(for [coll collections]
(let [id (:id coll)
name (:name coll)]
[:option {:key (str id) :value (pr-str id)} name]))])]
(if (= tab :collection)
[:div.library-content
(for [image images]
[:& image-item {:image image :key (:id image)}])]
[:div.library-content
(for [image workspace-images]
[:& image-item {:image image :key (:id image)}])])]
[:a.close {:href "#" :on-click on-close} i/close]]))

View file

@ -40,10 +40,7 @@
(mf/defc icons-list
[{:keys [collection-id] :as props}]
(let [icons-iref (mf/use-memo
{:fn #(icons/make-icons-iref collection-id)
:deps (mf/deps collection-id)})
icons (mf/deref icons-iref)
(let [icons (mf/deref icons/icons-iref)
on-select
(fn [event data]

View file

@ -7,6 +7,8 @@
(ns uxbox.util.blob
"Helpers for work with HTML5 Blob objects.")
;; TODO: DEPRECATED
(defn ^boolean blob?
[v]
(instance? js/Blob v))

View file

@ -11,7 +11,10 @@
(ns uxbox.util.dom
(:require
[goog.dom :as dom]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[beicon.core :as rx]
[cuerdas.core :as str]
[uxbox.util.blob :as blob]))
;; --- Deprecated methods
@ -131,3 +134,5 @@
(defn query
[el query]
(.querySelector el query))

View file

@ -10,6 +10,8 @@
[cuerdas.core :as str]
[uxbox.util.blob :as blob]))
;; TODO: DEPRECATED
(defn read-as-text
[file]
(rx/create

View file

@ -0,0 +1,68 @@
;; 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) 2020 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.webapi
"HTML5 web api helpers."
(:require
[beicon.core :as rx]
[cuerdas.core :as str]))
(defn read-file-as-text
[file]
(rx/create
(fn [sink]
(let [fr (js/FileReader.)]
(aset fr "onload" #(sink (rx/end (.-result fr))))
(.readAsText fr file)
(constantly nil)))))
(defn read-file-as-dataurl
[file]
(rx/create
(fn [sick]
(let [fr (js/FileReader.)]
(aset fr "onload" #(sick (rx/end (.-result fr))))
(.readAsDataURL fr file))
(constantly nil))))
(defn ^boolean blob?
[v]
(instance? js/Blob v))
(defn create-blob
"Create a blob from content."
([content]
(create-blob content "application/octet-stream"))
([content mtype]
(js/Blob. #js [content] #js {:type mtype})))
(defn revoke-uri
[url]
(assert (string? url) "invalid arguments")
(js/URL.revokeObjectURL url))
(defn create-uri
"Create a url from blob."
[b]
(assert (blob? b) "invalid arguments")
(js/URL.createObjectURL b))
;; (defn get-image-size
;; [file]
;; (letfn [(on-load [sink img]
;; (let [size [(.-width img) (.-height img)]]
;; (sink (rx/end size))))
;; (on-subscribe [sink]
;; (let [img (js/Image.)
;; uri (blob/create-uri file)]
;; (set! (.-onload img) (partial on-load sink img))
;; (set! (.-src img) uri)
;; #(blob/revoke-uri uri)))]
;; (rx/create on-subscribe)))