0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-12 15:51:37 -05:00
penpot/frontend/src/app/main/data/viewer.cljs
2020-11-13 11:55:28 +01:00

334 lines
10 KiB
Clojure

;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.viewer
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk]
[app.main.constants :as c]
[app.main.repo :as rp]
[app.main.store :as st]
[app.common.spec :as us]
[app.common.pages :as cp]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.util.router :as rt]
[app.common.uuid :as uuid]
[app.common.pages-helpers :as cph]))
;; --- Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::project (s/keys ::req-un [::id ::name]))
(s/def ::file (s/keys :req-un [::id ::name]))
(s/def ::page ::cp/page)
(s/def ::interactions-mode #{:hide :show :show-on-click})
(s/def ::bundle
(s/keys :req-un [::project ::file ::page]))
;; --- Initialization
(declare fetch-bundle)
(declare bundle-fetched)
(defn initialize
[{:keys [page-id file-id] :as params}]
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc state :viewer-local {:zoom 1
:page-id page-id
:file-id file-id
:interactions-mode :hide
:show-interactions? false
:selected #{}
:collapsed #{}
:hover nil}))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (fetch-bundle params)))))
;; --- Data Fetching
(defn fetch-bundle
[{:keys [page-id file-id token]}]
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ state stream]
(let [params (cond-> {:page-id page-id
:file-id file-id}
(string? token) (assoc :token token))]
(->> (rp/query :viewer-bundle params)
(rx/first)
(rx/map bundle-fetched))))))
(defn- extract-frames
[objects]
(let [root (get objects uuid/zero)]
(->> (:shapes root)
(map #(get objects %))
(filter #(= :frame (:type %)))
(reverse)
(vec))))
(defn bundle-fetched
[{:keys [project file page share-token token libraries] :as bundle}]
(us/verify ::bundle bundle)
(ptk/reify ::file-fetched
ptk/UpdateEvent
(update [_ state]
(let [objects (:objects page)
frames (extract-frames objects)]
(-> state
(assoc :viewer-libraries (into {} (map #(vector (:id %) %) libraries))
:viewer-data {:project project
:objects objects
:file file
:page page
:frames frames
:token token
:share-token share-token}))))))
(def create-share-link
(ptk/reify ::create-share-link
ptk/WatchEvent
(watch [_ state stream]
(let [file-id (get-in state [:viewer-local :file-id])
page-id (get-in state [:viewer-local :page-id])]
(->> (rp/mutation! :create-file-share-token {:file-id file-id
:page-id page-id})
(rx/map (fn [{:keys [token]}]
#(assoc-in % [:viewer-data :share-token] token))))))))
(def delete-share-link
(ptk/reify ::delete-share-link
ptk/WatchEvent
(watch [_ state stream]
(let [file-id (get-in state [:viewer-local :file-id])
page-id (get-in state [:viewer-local :page-id])
token (get-in state [:viewer-data :share-token])]
(->> (rp/mutation :delete-file-share-token {:file-id file-id
:page-id page-id
:token token})
(rx/map (fn [_] #(update % :viewer-data dissoc :share-token))))))))
;; --- Zoom Management
(def increase-zoom
(ptk/reify ::increase-zoom
ptk/UpdateEvent
(update [_ state]
(let [increase #(nth c/zoom-levels
(+ (d/index-of c/zoom-levels %) 1)
(last c/zoom-levels))]
(update-in state [:viewer-local :zoom] (fnil increase 1))))))
(def decrease-zoom
(ptk/reify ::decrease-zoom
ptk/UpdateEvent
(update [_ state]
(let [decrease #(nth c/zoom-levels
(- (d/index-of c/zoom-levels %) 1)
(first c/zoom-levels))]
(update-in state [:viewer-local :zoom] (fnil decrease 1))))))
(def reset-zoom
(ptk/reify ::reset-zoom
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :zoom] 1))))
(def zoom-to-50
(ptk/reify ::zoom-to-50
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :zoom] 0.5))))
(def zoom-to-200
(ptk/reify ::zoom-to-200
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :zoom] 2))))
;; --- Local State Management
(def toggle-thumbnails-panel
(ptk/reify ::toggle-thumbnails-panel
ptk/UpdateEvent
(update [_ state]
(update-in state [:viewer-local :show-thumbnails] not))))
(def select-prev-frame
(ptk/reify ::select-prev-frame
ptk/WatchEvent
(watch [_ state stream]
(let [route (:route state)
screen (-> route :data :name keyword)
qparams (get-in route [:params :query])
pparams (get-in route [:params :path])
index (d/parse-integer (:index qparams))]
(when (pos? index)
(rx/of (rt/nav screen pparams (assoc qparams :index (dec index)))))))))
(def select-next-frame
(ptk/reify ::select-prev-frame
ptk/WatchEvent
(watch [_ state stream]
(let [route (:route state)
screen (-> route :data :name keyword)
qparams (get-in route [:params :query])
pparams (get-in route [:params :path])
index (d/parse-integer (:index qparams))
total (count (get-in state [:viewer-data :frames]))]
(when (< index (dec total))
(rx/of (rt/nav screen pparams (assoc qparams :index (inc index)))))))))
(defn set-interactions-mode
[mode]
(us/verify ::interactions-mode mode)
(ptk/reify ::set-interactions-mode
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:viewer-local :interactions-mode] mode)
(assoc-in [:viewer-local :show-interactions?] (case mode
:hide false
:show true
:show-on-click false))))))
(declare flash-done)
(def flash-interactions
(ptk/reify ::flash-interactions
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :show-interactions?] true))
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (rx/filter (ptk/type? ::flash-interactions) stream)]
(->> (rx/of flash-done)
(rx/delay 500)
(rx/take-until stopper))))))
(def flash-done
(ptk/reify ::flash-done
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :show-interactions?] false))))
;; --- Navigation
(defn go-to-frame
[frame-id]
(us/verify ::us/uuid frame-id)
(ptk/reify ::go-to-frame
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:viewer-local :page-id])
file-id (get-in state [:viewer-local :file-id])
frames (get-in state [:viewer-data :frames])
token (get-in state [:viewer-data :token])
index (d/index-of-pred frames #(= (:id %) frame-id))]
(rx/of (rt/nav :viewer
{:page-id page-id
:file-id file-id}
{:token token
:index index}))))))
(defn set-current-frame [frame-id]
(ptk/reify ::current-frame
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-data :current-frame-id] frame-id))))
(defn deselect-all []
(ptk/reify ::deselect-all
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :selected] #{}))))
(defn select-shape
([id]
(ptk/reify ::select-shape
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:viewer-local :selected] #{id}))))))
(defn toggle-selection
[id]
(ptk/reify ::toggle-selection
ptk/UpdateEvent
(update [_ state]
(let [selected (get-in state [:viewer-local :selected])]
(cond-> state
(not (selected id)) (update-in [:viewer-local :selected] conj id)
(selected id) (update-in [:viewer-local :selected] disj id))))))
(defn shift-select-to
[id]
(ptk/reify ::shift-select-to
ptk/UpdateEvent
(update [_ state]
(let [objects (get-in state [:viewer-data :objects])
selection (-> state
(get-in [:viewer-local :selected] #{})
(conj id))]
(-> state
(assoc-in [:viewer-local :selected]
(cph/expand-region-selection objects selection)))))))
(defn select-all
[]
(ptk/reify ::select-all
ptk/UpdateEvent
(update [_ state]
(let [objects (get-in state [:viewer-data :objects])
frame-id (get-in state [:viewer-data :current-frame-id])
selection (->> objects
(filter #(= (:frame-id (second %)) frame-id))
(map first)
(into #{frame-id}))]
(-> state
(assoc-in [:viewer-local :selected] selection))))))
(defn toggle-collapse [id]
(ptk/reify ::toggle-collapse
ptk/UpdateEvent
(update [_ state]
(let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)]
(update-in state [:viewer-local :collapsed] (if toggled? disj conj) id)))))
(defn hover-shape [id hover?]
(ptk/reify ::hover-shape
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :hover] (when hover? id)))))
;; --- Shortcuts
(def shortcuts
{"+" #(st/emit! increase-zoom)
"-" #(st/emit! decrease-zoom)
"ctrl+a" #(st/emit! (select-all))
"shift+0" #(st/emit! zoom-to-50)
"shift+1" #(st/emit! reset-zoom)
"shift+2" #(st/emit! zoom-to-200)
"left" #(st/emit! select-prev-frame)
"right" #(st/emit! select-next-frame)})