mirror of
https://github.com/penpot/penpot.git
synced 2025-05-24 07:16:11 +02:00
🎉 Share link & pages on viewer.
This commit is contained in:
parent
3532263af4
commit
c8102f4bff
58 changed files with 1837 additions and 1245 deletions
46
frontend/src/app/main/data/common.cljs
Normal file
46
frontend/src/app/main/data/common.cljs
Normal file
|
@ -0,0 +1,46 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.common
|
||||
"A general purpose events."
|
||||
(:require
|
||||
[app.main.repo :as rp]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SHARE LINK
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn share-link-created
|
||||
[link]
|
||||
(ptk/reify ::share-link-created
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :share-links (fnil conj []) link))))
|
||||
|
||||
(defn create-share-link
|
||||
[params]
|
||||
(ptk/reify ::create-share-link
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation! :create-share-link params)
|
||||
(rx/map share-link-created)))))
|
||||
|
||||
(defn delete-share-link
|
||||
[{:keys [id] :as link}]
|
||||
(ptk/reify ::delete-share-link
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :share-links
|
||||
(fn [links]
|
||||
(filterv #(not= id (:id %)) links))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/mutation! :delete-share-link {:id id})
|
||||
(rx/ignore)))))
|
||||
|
|
@ -14,24 +14,12 @@
|
|||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; --- General 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 ::bundle
|
||||
(s/keys :req-un [::project ::file ::page]))
|
||||
|
||||
|
||||
;; --- Local State Initialization
|
||||
|
||||
(def ^:private
|
||||
|
@ -49,25 +37,24 @@
|
|||
(declare fetch-bundle)
|
||||
(declare bundle-fetched)
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::index ::us/integer)
|
||||
(s/def ::token (s/nilable ::us/string))
|
||||
(s/def ::page-id (s/nilable ::us/uuid))
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
(s/def ::section ::us/string)
|
||||
|
||||
(s/def ::initialize-params
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-un [::token]))
|
||||
(s/keys :req-un [::file-id]
|
||||
:opt-un [::share-id ::page-id]))
|
||||
|
||||
(defn initialize
|
||||
[{:keys [page-id file-id] :as params}]
|
||||
[{:keys [file-id] :as params}]
|
||||
(us/assert ::initialize-params params)
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :current-file-id file-id)
|
||||
(assoc :current-page-id page-id)
|
||||
(update :viewer-local
|
||||
(fn [lstate]
|
||||
(if (nil? lstate)
|
||||
|
@ -77,55 +64,72 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (fetch-bundle params)
|
||||
(fetch-comment-threads params)))))
|
||||
(fetch-comment-threads params)))
|
||||
|
||||
;; --- Data Fetching
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; Set the window name, the window name is used on inter-tab
|
||||
;; navigation; in other words: when a user opens a tab with a
|
||||
;; name, if there are already opened tab with that name, the
|
||||
;; browser just focus the opened tab instead of creating new
|
||||
;; tab.
|
||||
(let [name (str "viewer-" file-id)]
|
||||
(unchecked-set ug/global "name" name)))))
|
||||
|
||||
(s/def ::fetch-bundle-params
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-un [::token]))
|
||||
(defn finalize
|
||||
[_]
|
||||
(ptk/reify ::finalize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(dissoc state :viewer))))
|
||||
|
||||
(defn fetch-bundle
|
||||
[{:keys [page-id file-id token] :as params}]
|
||||
(us/assert ::fetch-bundle-params params)
|
||||
(ptk/reify ::fetch-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [params (cond-> {:page-id page-id
|
||||
:file-id file-id}
|
||||
(string? token) (assoc :token token))]
|
||||
(->> (rp/query :viewer-bundle params)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [fonts] :as bundle}]
|
||||
(rx/of (df/fonts-fetched fonts)
|
||||
(bundle-fetched bundle)))))))))
|
||||
|
||||
(defn- extract-frames
|
||||
[objects]
|
||||
(defn select-frames
|
||||
[{:keys [objects] :as page}]
|
||||
(let [root (get objects uuid/zero)]
|
||||
(into [] (comp (map #(get objects %))
|
||||
(filter #(= :frame (:type %))))
|
||||
(reverse (:shapes root)))))
|
||||
|
||||
;; --- Data Fetching
|
||||
|
||||
(s/def ::fetch-bundle-params
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-un [::share-id]))
|
||||
|
||||
(defn fetch-bundle
|
||||
[{:keys [file-id share-id] :as params}]
|
||||
(us/assert ::fetch-bundle-params params)
|
||||
(ptk/reify ::fetch-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [params' (cond-> {:file-id file-id}
|
||||
(uuid? share-id) (assoc :share-id share-id))]
|
||||
(->> (rp/query :view-only-bundle params')
|
||||
(rx/mapcat
|
||||
(fn [{:keys [fonts] :as bundle}]
|
||||
(rx/of (df/fonts-fetched fonts)
|
||||
(bundle-fetched (merge bundle params))))))))))
|
||||
|
||||
|
||||
(defn bundle-fetched
|
||||
[{:keys [project file page share-token token libraries users] :as bundle}]
|
||||
(us/verify ::bundle bundle)
|
||||
(ptk/reify ::bundle-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (:objects page)
|
||||
frames (extract-frames objects)]
|
||||
[{:keys [project file share-links libraries users permissions] :as bundle}]
|
||||
(let [pages (->> (get-in file [:data :pages])
|
||||
(map (fn [page-id]
|
||||
(let [data (get-in file [:data :pages-index page-id])]
|
||||
[page-id (assoc data :frames (select-frames data))])))
|
||||
(into {}))]
|
||||
|
||||
(ptk/reify ::bundle-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :viewer-libraries (d/index-by :id libraries))
|
||||
(update :viewer-data assoc
|
||||
:project project
|
||||
:objects objects
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
:token token
|
||||
:share-token share-token))))))
|
||||
(assoc :share-links share-links)
|
||||
(assoc :viewer {:libraries (d/index-by :id libraries)
|
||||
:users (d/index-by :id users)
|
||||
:permissions permissions
|
||||
:project project
|
||||
:pages pages
|
||||
:file file}))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
|
@ -168,32 +172,6 @@
|
|||
(->> (rp/query :comments {:thread-id thread-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn create-share-link
|
||||
[]
|
||||
(ptk/reify ::create-share-link
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)]
|
||||
(->> (rp/mutation! :create-file-share-token {:file-id file-id
|
||||
:page-id page-id})
|
||||
(rx/map (fn [{:keys [token]}]
|
||||
#(assoc-in % [:viewer-data :token] token))))))))
|
||||
|
||||
(defn delete-share-link
|
||||
[]
|
||||
(ptk/reify ::delete-share-link
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
token (get-in state [:viewer-data :token])
|
||||
params {:file-id file-id
|
||||
:page-id page-id
|
||||
:token token}]
|
||||
(->> (rp/mutation :delete-file-share-token params)
|
||||
(rx/map (fn [_] #(update % :viewer-data dissoc :token))))))))
|
||||
|
||||
;; --- Zoom Management
|
||||
|
||||
(def increase-zoom
|
||||
|
@ -245,29 +223,32 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)
|
||||
index (:index qparams)]
|
||||
(when (pos? index)
|
||||
(rx/of
|
||||
(dcm/close-thread)
|
||||
(rt/nav screen pparams (assoc qparams :index (dec index)))))))))
|
||||
(rt/nav :viewer pparams (assoc qparams :index (dec index)))))))))
|
||||
|
||||
(def select-next-frame
|
||||
(ptk/reify ::select-prev-frame
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(prn "select-next-frame")
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)
|
||||
qparams (:query-params route)
|
||||
|
||||
page-id (:page-id pparams)
|
||||
index (:index qparams)
|
||||
total (count (get-in state [:viewer-data :frames]))]
|
||||
|
||||
total (count (get-in state [:viewer :pages page-id :frames]))]
|
||||
|
||||
(when (< index (dec total))
|
||||
(rx/of
|
||||
(dcm/close-thread)
|
||||
(rt/nav screen pparams (assoc qparams :index (inc index)))))))))
|
||||
(rt/nav :viewer pparams (assoc qparams :index (inc index)))))))))
|
||||
|
||||
(s/def ::interactions-mode #{:hide :show :show-on-click})
|
||||
|
||||
|
@ -329,7 +310,6 @@
|
|||
(when index
|
||||
(rx/of (go-to-frame-by-index index)))))))
|
||||
|
||||
|
||||
(defn go-to-section
|
||||
[section]
|
||||
(ptk/reify ::go-to-section
|
||||
|
@ -340,7 +320,6 @@
|
|||
qparams (:query-params route)]
|
||||
(rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
|
||||
|
||||
|
||||
(defn set-current-frame [frame-id]
|
||||
(ptk/reify ::set-current-frame
|
||||
ptk/UpdateEvent
|
||||
|
@ -405,18 +384,50 @@
|
|||
(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?]
|
||||
(defn hover-shape
|
||||
[id hover?]
|
||||
(ptk/reify ::hover-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
;; --- NAV
|
||||
|
||||
(defn go-to-dashboard
|
||||
([] (go-to-dashboard nil))
|
||||
([{:keys [team-id]}]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
ptk/WatchEvent
|
||||
[]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (get-in state [:viewer :project :team-id])
|
||||
params {:team-id team-id}]
|
||||
(rx/of (rt/nav :dashboard-projects params))))))
|
||||
|
||||
(defn go-to-page
|
||||
[page-id]
|
||||
(ptk/reify ::go-to-page
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id})))))))
|
||||
(let [route (:route state)
|
||||
pparams (:path-params route)
|
||||
qparams (-> (:query-params route)
|
||||
(assoc :index 0)
|
||||
(assoc :page-id page-id))
|
||||
rname (get-in route [:data :name])]
|
||||
(rx/of (rt/nav rname pparams qparams))))))
|
||||
|
||||
(defn go-to-workspace
|
||||
[page-id]
|
||||
(ptk/reify ::go-to-workspace
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [project-id (get-in state [:viewer :project :id])
|
||||
file-id (get-in state [:viewer :file :id])
|
||||
pparams {:project-id project-id :file-id file-id}
|
||||
qparams {:page-id page-id}]
|
||||
(rx/of (rt/nav-new-window*
|
||||
{:rname :workspace
|
||||
:path-params pparams
|
||||
:query-params qparams
|
||||
:name (str "workspace-" file-id)}))))))
|
||||
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
[app.main.repo :as rp]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.router :as rt]
|
||||
|
@ -171,7 +172,12 @@
|
|||
(->> stream
|
||||
(rx/filter #(= ::dwc/index-initialized %))
|
||||
(rx/first)
|
||||
(rx/map #(file-initialized bundle)))))))))))
|
||||
(rx/map #(file-initialized bundle)))))))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(let [name (str "workspace-" file-id)]
|
||||
(unchecked-set ug/global "name" name)))))
|
||||
|
||||
(defn- file-initialized
|
||||
[{:keys [file users project libraries] :as bundle}]
|
||||
|
@ -1273,10 +1279,14 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [{:keys [current-file-id current-page-id]} state
|
||||
params {:file-id (or file-id current-file-id)
|
||||
:page-id (or page-id current-page-id)}]
|
||||
pparams {:file-id (or file-id current-file-id)}
|
||||
qparams {:page-id (or page-id current-page-id)
|
||||
:index 0}]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav-new-window :viewer params {:index 0})))))))
|
||||
(rt/nav-new-window* {:rname :viewer
|
||||
:path-params pparams
|
||||
:query-params qparams
|
||||
:name (str "viewer-" (:file-id pparams))})))))))
|
||||
|
||||
(defn go-to-dashboard
|
||||
([] (go-to-dashboard nil))
|
||||
|
|
|
@ -38,6 +38,9 @@
|
|||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def share-links
|
||||
(l/derived :share-links st/state))
|
||||
|
||||
;; ---- Dashboard refs
|
||||
|
||||
(def dashboard-local
|
||||
|
@ -287,8 +290,17 @@
|
|||
|
||||
;; ---- Viewer refs
|
||||
|
||||
(def viewer-file
|
||||
(l/derived :viewer-file st/state))
|
||||
|
||||
(def viewer-project
|
||||
(l/derived :viewer-file st/state))
|
||||
|
||||
(def viewer-data
|
||||
(l/derived :viewer-data st/state))
|
||||
(l/derived :viewer st/state))
|
||||
|
||||
(def viewer-state
|
||||
(l/derived :viewer st/state))
|
||||
|
||||
(def viewer-local
|
||||
(l/derived :viewer-local st/state))
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.cursors :as c]
|
||||
[app.main.ui.dashboard :refer [dashboard]]
|
||||
[app.main.ui.handoff :refer [handoff]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.messages :as msgs]
|
||||
[app.main.ui.onboarding]
|
||||
|
@ -41,16 +40,17 @@
|
|||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::viewer-path-params
|
||||
(s/keys :req-un [::file-id ::page-id]))
|
||||
|
||||
(s/def ::section ::us/keyword)
|
||||
(s/def ::index ::us/integer)
|
||||
(s/def ::token (s/nilable ::us/string))
|
||||
(s/def ::token (s/nilable ::us/not-empty-string))
|
||||
(s/def ::share-id ::us/uuid)
|
||||
|
||||
(s/def ::viewer-path-params
|
||||
(s/keys :req-un [::file-id]))
|
||||
|
||||
(s/def ::viewer-query-params
|
||||
(s/keys :req-un [::index]
|
||||
:opt-un [::token ::section]))
|
||||
:opt-un [::share-id ::section ::page-id]))
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
|
@ -71,7 +71,7 @@
|
|||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]]
|
||||
|
||||
["/view/:file-id/:page-id"
|
||||
["/view/:file-id"
|
||||
{:name :viewer
|
||||
:conform
|
||||
{:path-params ::viewer-path-params
|
||||
|
@ -147,22 +147,15 @@
|
|||
[:& dashboard {:route route}]]
|
||||
|
||||
:viewer
|
||||
(let [index (get-in route [:query-params :index])
|
||||
token (get-in route [:query-params :token])
|
||||
section (get-in route [:query-params :section] :interactions)
|
||||
file-id (get-in route [:path-params :file-id])
|
||||
page-id (get-in route [:path-params :page-id])]
|
||||
(let [{:keys [query-params path-params]} route
|
||||
{:keys [index share-id section page-id] :or {section :interactions}} query-params
|
||||
{:keys [file-id]} path-params]
|
||||
[:& fs/fullscreen-wrapper {}
|
||||
(if (= section :handoff)
|
||||
[:& handoff {:page-id page-id
|
||||
:file-id file-id
|
||||
:index index
|
||||
:token token}]
|
||||
[:& viewer-page {:page-id page-id
|
||||
:file-id file-id
|
||||
:section section
|
||||
:index index
|
||||
:token token}])])
|
||||
[:& viewer-page {:page-id page-id
|
||||
:file-id file-id
|
||||
:section section
|
||||
:index index
|
||||
:share-id share-id}]])
|
||||
|
||||
:render-object
|
||||
(do
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff
|
||||
(:require
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.handoff.left-sidebar :refer [left-sidebar]]
|
||||
[app.main.ui.handoff.render :refer [render-frame-svg]]
|
||||
[app.main.ui.handoff.right-sidebar :refer [right-sidebar]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.viewer.header :refer [header]]
|
||||
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn handle-select-frame [frame]
|
||||
#(do (dom/prevent-default %)
|
||||
(dom/stop-propagation %)
|
||||
(st/emit! (dv/select-shape (:id frame)))))
|
||||
|
||||
(mf/defc render-panel
|
||||
[{:keys [data state index page-id file-id]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
frames (:frames data [])
|
||||
objects (:objects data)
|
||||
frame (get frames index)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps index)
|
||||
(fn []
|
||||
(st/emit! (dv/set-current-frame (:id frame))
|
||||
(dv/select-shape (:id frame)))))
|
||||
|
||||
[:section.viewer-preview
|
||||
(cond
|
||||
(empty? frames)
|
||||
[:section.empty-state
|
||||
[:span (t locale "viewer.empty-state")]]
|
||||
|
||||
(nil? frame)
|
||||
[:section.empty-state
|
||||
[:span (t locale "viewer.frame-not-found")]]
|
||||
|
||||
:else
|
||||
[:*
|
||||
[:& left-sidebar {:frame frame}]
|
||||
[:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)}
|
||||
[:div.handoff-svg-container
|
||||
[:& render-frame-svg {:frame-id (:id frame)
|
||||
:zoom (:zoom state)
|
||||
:objects objects}]]]
|
||||
[:& right-sidebar {:frame frame
|
||||
:page-id page-id
|
||||
:file-id file-id}]])]))
|
||||
|
||||
(mf/defc handoff-content
|
||||
[{:keys [data state index page-id file-id] :as props}]
|
||||
(let [on-mouse-wheel
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event)
|
||||
(.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom))))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global EventType.WHEEL
|
||||
on-mouse-wheel #js {"passive" false})]
|
||||
(fn []
|
||||
(events/unlistenByKey key1))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(hooks/use-shortcuts ::handoff sc/shortcuts)
|
||||
|
||||
[:div.handoff-layout {:class (dom/classnames :force-visible
|
||||
(:show-thumbnails state))}
|
||||
[:& header
|
||||
{:data data
|
||||
:state state
|
||||
:index index
|
||||
:section :handoff}]
|
||||
[:div.viewer-content
|
||||
(when (:show-thumbnails state)
|
||||
[:& thumbnails-panel {:index index
|
||||
:data data
|
||||
:screen :handoff}])
|
||||
[:& render-panel {:data data
|
||||
:state state
|
||||
:index index
|
||||
:page-id page-id
|
||||
:file-id file-id}]]]))
|
||||
|
||||
(mf/defc handoff
|
||||
[{:keys [file-id page-id index token] :as props}]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id page-id token)
|
||||
(fn []
|
||||
(st/emit! (dv/initialize props))))
|
||||
|
||||
(let [data (mf/deref refs/viewer-data)
|
||||
state (mf/deref refs/viewer-local)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:file data))
|
||||
#(when (:file data)
|
||||
(dom/set-html-title (tr "title.viewer"
|
||||
(get-in data [:file :name])))))
|
||||
|
||||
(when (and data state)
|
||||
[:& handoff-content
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:index index
|
||||
:state state
|
||||
:data data}])))
|
232
frontend/src/app/main/ui/share_link.cljs
Normal file
232
frontend/src/app/main/ui/share_link.cljs
Normal file
|
@ -0,0 +1,232 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.share-link
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dc]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.logging :as log]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
|
||||
(defn prepare-params
|
||||
[{:keys [sections pages pages-mode]}]
|
||||
{:pages pages
|
||||
:flags (-> #{}
|
||||
(into (map #(str "section-" %)) sections)
|
||||
(into (map #(str "pages-" %)) [pages-mode]))})
|
||||
|
||||
(mf/defc share-link-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :share-link}
|
||||
[{:keys [file page]}]
|
||||
(let [slinks (mf/deref refs/share-links)
|
||||
router (mf/deref refs/router)
|
||||
route (mf/deref refs/route)
|
||||
|
||||
link (mf/use-state nil)
|
||||
confirm (mf/use-state false)
|
||||
|
||||
opts (mf/use-state
|
||||
{:sections #{"viewer"}
|
||||
:pages-mode "current"
|
||||
:pages #{(:id page)}})
|
||||
|
||||
close
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide)))
|
||||
|
||||
select-pages-mode
|
||||
(fn [mode]
|
||||
(reset! confirm false)
|
||||
(swap! opts
|
||||
(fn [state]
|
||||
(-> state
|
||||
(assoc :pages-mode mode)
|
||||
(cond-> (= mode "current") (assoc :pages #{(:id page)}))
|
||||
(cond-> (= mode "all") (assoc :pages (into #{} (get-in file [:data :pages]))))))))
|
||||
|
||||
mark-checked-page
|
||||
(fn [event id]
|
||||
(let [target (dom/get-target event)
|
||||
checked? (.-checked ^js target)]
|
||||
(reset! confirm false)
|
||||
(swap! opts update :pages
|
||||
(fn [pages]
|
||||
(if checked?
|
||||
(conj pages id)
|
||||
(disj pages id))))))
|
||||
|
||||
create-link
|
||||
(fn [_]
|
||||
(let [params (prepare-params @opts)
|
||||
params (assoc params :file-id (:id file))]
|
||||
(st/emit! (dc/create-share-link params))))
|
||||
|
||||
copy-link
|
||||
(fn [_]
|
||||
(wapi/write-to-clipboard @link)
|
||||
(st/emit! (dm/show {:type :info
|
||||
:content (tr "common.share-link.link-copied-success")
|
||||
:timeout 3000})))
|
||||
|
||||
try-delete-link
|
||||
(fn [_]
|
||||
(reset! confirm true))
|
||||
|
||||
delete-link
|
||||
(fn [_]
|
||||
(let [params (prepare-params @opts)
|
||||
slink (d/seek #(= (:flags %) (:flags params)) slinks)]
|
||||
(reset! confirm false)
|
||||
(st/emit! (dc/delete-share-link slink)
|
||||
(dm/show {:type :info
|
||||
:content (tr "common.share-link.link-deleted-success")
|
||||
:timeout 3000}))))
|
||||
]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file slinks @opts)
|
||||
(fn []
|
||||
(let [{:keys [flags pages] :as params} (prepare-params @opts)
|
||||
slink (d/seek #(and (= (:flags %) flags) (= (:pages %) pages)) slinks)
|
||||
href (when slink
|
||||
(let [pparams (:path-params route)
|
||||
qparams (-> (:query-params route)
|
||||
(assoc :share-id (:id slink))
|
||||
(assoc :index "0"))
|
||||
href (rt/resolve router :viewer pparams qparams)]
|
||||
(assoc cf/public-uri :fragment href)))]
|
||||
(reset! link (some-> href str)))))
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.share-link-dialog
|
||||
[:div.modal-content
|
||||
[:div.title
|
||||
[:h2 (tr "common.share-link.title")]
|
||||
[:div.modal-close-button
|
||||
{:on-click close
|
||||
:title (tr "labels.close")}
|
||||
i/close]]
|
||||
|
||||
[:div.share-link-section
|
||||
[:label (tr "labels.link")]
|
||||
[:div.custom-input.with-icon
|
||||
[:input {:type "text" :value (or @link "") :read-only true}]
|
||||
[:div.help-icon {:title (tr "labels.copy")
|
||||
:on-click copy-link}
|
||||
i/copy]]
|
||||
|
||||
[:div.hint (tr "common.share-link.permissions-hint")]]]
|
||||
|
||||
[:div.modal-content
|
||||
(let [sections (:sections @opts)]
|
||||
[:div.access-mode
|
||||
[:div.title (tr "common.share-link.permissions-can-access")]
|
||||
[:div.items
|
||||
[:div.input-checkbox.check-primary.disabled
|
||||
[:input.check-primary.input-checkbox {:type "checkbox" :disabled true}]
|
||||
[:label (tr "labels.workspace")]]
|
||||
|
||||
[:div.input-checkbox.check-primary
|
||||
[:input {:type "checkbox"
|
||||
:default-checked (contains? sections "viewer")}]
|
||||
[:label (tr "labels.viewer")
|
||||
[:span.hint "(" (tr "labels.default") ")"]]]
|
||||
|
||||
;; [:div.input-checkbox.check-primary
|
||||
;; [:input.check-primary.input-checkbox {:type "checkbox"}]
|
||||
;; [:label "Handsoff" ]]
|
||||
]])
|
||||
|
||||
(let [mode (:pages-mode @opts)]
|
||||
[:*
|
||||
[:div.view-mode
|
||||
[:div.title (tr "common.share-link.permissions-can-view")]
|
||||
[:div.items
|
||||
[:div.input-radio.radio-primary
|
||||
[:input {:type "radio"
|
||||
:id "view-all"
|
||||
:checked (= "all" mode)
|
||||
:name "pages-mode"
|
||||
:on-change #(select-pages-mode "all")}]
|
||||
[:label {:for "view-all"} (tr "common.share-link.view-all-pages")]]
|
||||
|
||||
[:div.input-radio.radio-primary
|
||||
[:input {:type "radio"
|
||||
:id "view-current"
|
||||
:name "pages-mode"
|
||||
:checked (= "current" mode)
|
||||
:on-change #(select-pages-mode "current")}]
|
||||
[:label {:for "view-current"} (tr "common.share-link.view-current-page")]]
|
||||
|
||||
[:div.input-radio.radio-primary
|
||||
[:input {:type "radio"
|
||||
:id "view-selected"
|
||||
:name "pages-mode"
|
||||
:checked (= "selected" mode)
|
||||
:on-change #(select-pages-mode "selected")}]
|
||||
[:label {:for "view-selected"} (tr "common.share-link.view-selected-pages")]]]]
|
||||
|
||||
(when (= "selected" mode)
|
||||
(let [pages (->> (get-in file [:data :pages])
|
||||
(map #(get-in file [:data :pages-index %])))
|
||||
selected (:pages @opts)]
|
||||
[:ul.pages-selection
|
||||
(for [page pages]
|
||||
[:li.input-checkbox.check-primary {:key (str (:id page))}
|
||||
[:input {:type "checkbox"
|
||||
:id (str "page-" (:id page))
|
||||
:on-change #(mark-checked-page % (:id page))
|
||||
:checked (contains? selected (:id page))}]
|
||||
[:label {:for (str "page-" (:id page))} (:name page)]])]))])]
|
||||
|
||||
[:div.modal-footer
|
||||
(cond
|
||||
(true? @confirm)
|
||||
[:div.confirm-dialog
|
||||
[:div.description (tr "common.share-link.confirm-deletion-link-description")]
|
||||
[:div.actions
|
||||
[:input.btn-secondary
|
||||
{:type "button"
|
||||
:on-click #(reset! confirm false)
|
||||
:value (tr "labels.cancel")}]
|
||||
[:input.btn-warning
|
||||
{:type "button"
|
||||
:on-click delete-link
|
||||
:value (tr "common.share-link.remove-link")
|
||||
}]]]
|
||||
|
||||
(some? @link)
|
||||
[:input.btn-secondary
|
||||
{:type "button"
|
||||
:class "primary"
|
||||
:on-click try-delete-link
|
||||
:value (tr "common.share-link.remove-link")}]
|
||||
|
||||
:else
|
||||
[:input.btn-primary
|
||||
{:type "button"
|
||||
:class "primary"
|
||||
:on-click create-link
|
||||
:value (tr "common.share-link.get-link")}])]
|
||||
|
||||
]]))
|
||||
|
||||
|
||||
|
|
@ -6,276 +6,147 @@
|
|||
|
||||
(ns app.main.ui.viewer
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.share-link]
|
||||
[app.main.ui.viewer.comments :refer [comments-layer]]
|
||||
[app.main.ui.viewer.handoff :as handoff]
|
||||
[app.main.ui.viewer.header :refer [header]]
|
||||
[app.main.ui.viewer.shapes :as shapes]
|
||||
[app.main.ui.viewer.interactions :as interactions]
|
||||
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[goog.events :as events]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- frame-contains?
|
||||
[{:keys [x y width height]} {px :x py :y}]
|
||||
(let [x2 (+ x width)
|
||||
y2 (+ y height)]
|
||||
(and (<= x px x2)
|
||||
(<= y py y2))))
|
||||
(defn- calculate-size
|
||||
[frame zoom]
|
||||
{:width (* (:width frame) zoom)
|
||||
:height (* (:height frame) zoom)
|
||||
:vbox (str "0 0 " (:width frame 0) " " (:height frame 0))})
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
(mf/defc viewer
|
||||
[{:keys [params data]}]
|
||||
|
||||
(def comments-local-ref
|
||||
(l/derived :comments-local st/state))
|
||||
(let [{:keys [page-id section index]} params
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [zoom frame data] :as props}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
local (mf/deref refs/viewer-local)
|
||||
|
||||
modifier1 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
file (:file data)
|
||||
users (:users data)
|
||||
project (:project data)
|
||||
perms (:permissions data)
|
||||
|
||||
modifier2 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gmt/translate-matrix))
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
|
||||
threads-map (->> (mf/deref threads-ref)
|
||||
(d/mapm #(update %2 :position gpt/transform modifier1)))
|
||||
page (mf/use-memo
|
||||
(mf/deps data page-id)
|
||||
(fn []
|
||||
(get-in data [:pages page-id])))
|
||||
|
||||
cstate (mf/deref refs/comments-local)
|
||||
zoom (:zoom local)
|
||||
frames (:frames page)
|
||||
frame (get frames index)
|
||||
|
||||
mframe (geom/transform-shape frame)
|
||||
threads (->> (vals threads-map)
|
||||
(dcm/apply-filters cstate profile)
|
||||
(filter (fn [{:keys [position]}]
|
||||
(frame-contains? mframe position))))
|
||||
|
||||
on-bubble-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(fn [thread]
|
||||
(if (= (:open cstate) (:id thread))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread)))))
|
||||
size (mf/use-memo
|
||||
(mf/deps frame zoom)
|
||||
(fn [] (calculate-size frame zoom)))
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate data frame)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (some? (:open cstate))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(let [event (.-nativeEvent ^js event)
|
||||
position (-> (dom/get-offset-position event)
|
||||
(gpt/transform modifier2))
|
||||
params {:position position
|
||||
:page-id (get-in data [:page :id])
|
||||
:file-id (get-in data [:file :id])}]
|
||||
(st/emit! (dcm/create-draft params))))))
|
||||
(mf/deps section)
|
||||
(fn [_]
|
||||
(when (= section :comments)
|
||||
(st/emit! (dcm/close-thread)))))]
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(st/emitf (dcm/close-thread)))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(mf/deps frame)
|
||||
(fn [draft]
|
||||
(let [params (update draft :position gpt/transform modifier2)]
|
||||
(st/emit! (dcm/create-thread params)
|
||||
(dcm/close-thread)))))]
|
||||
|
||||
[:div.comments-section {:on-click on-click}
|
||||
[:div.viewer-comments-container
|
||||
[:div.threads
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open cstate))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open cstate)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users (:users data)
|
||||
:zoom zoom}]))
|
||||
|
||||
(when-let [draft (:draft cstate)]
|
||||
[:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1)
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
||||
|
||||
|
||||
|
||||
(mf/defc viewport
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [state data index section] :as props}]
|
||||
(let [zoom (:zoom state)
|
||||
objects (:objects data)
|
||||
|
||||
frame (get-in data [:frames index])
|
||||
frame-id (:id frame)
|
||||
|
||||
modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)
|
||||
|
||||
objects (->> (d/concat [frame-id] (cp/get-children frame-id objects))
|
||||
(reduce update-fn objects))
|
||||
|
||||
interactions? (:interactions-show? state)
|
||||
wrapper (mf/use-memo (mf/deps objects) #(shapes/frame-container-factory objects interactions?))
|
||||
|
||||
;; Retrieve frame again with correct modifier
|
||||
frame (get objects frame-id)
|
||||
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
vbox (str "0 0 " (:width frame 0) " " (:height frame 0))]
|
||||
|
||||
[:div.viewport-container
|
||||
{:style {:width width
|
||||
:height height
|
||||
:state state
|
||||
:position "relative"}}
|
||||
|
||||
(when (= section :comments)
|
||||
[:& comments-layer {:width width
|
||||
:height height
|
||||
:frame frame
|
||||
:data data
|
||||
:zoom zoom}])
|
||||
|
||||
[:svg {:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
[:& wrapper {:shape frame
|
||||
:show-interactions? interactions?
|
||||
:view-box vbox}]]]))
|
||||
|
||||
(mf/defc main-panel
|
||||
[{:keys [data state index section]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
frames (:frames data)
|
||||
frame (get frames index)]
|
||||
[:section.viewer-preview
|
||||
(cond
|
||||
(empty? frames)
|
||||
[:section.empty-state
|
||||
[:span (t locale "viewer.empty-state")]]
|
||||
|
||||
(nil? frame)
|
||||
[:section.empty-state
|
||||
[:span (t locale "viewer.frame-not-found")]]
|
||||
|
||||
(some? state)
|
||||
[:& viewport
|
||||
{:data data
|
||||
:section section
|
||||
:index index
|
||||
:state state
|
||||
}])]))
|
||||
|
||||
(mf/defc viewer-content
|
||||
[{:keys [data state index section] :as props}]
|
||||
(let [on-click
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dcm/close-thread))
|
||||
(let [mode (get state :interactions-mode)]
|
||||
(when (= mode :show-on-click)
|
||||
(st/emit! dv/flash-interactions))))
|
||||
|
||||
on-mouse-wheel
|
||||
(fn [event]
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event) (.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
|
||||
on-key-down
|
||||
(fn [event]
|
||||
(when (kbd/esc? event)
|
||||
(st/emit! (dcm/close-thread))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false})
|
||||
key2 (events/listen js/window "keydown" on-key-down)
|
||||
key3 (events/listen js/window "click" on-click)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2)
|
||||
(events/unlistenByKey key3))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(hooks/use-shortcuts ::viewer sc/shortcuts)
|
||||
|
||||
[:div.viewer-layout {:class (dom/classnames :force-visible
|
||||
(:show-thumbnails state))}
|
||||
[:& header
|
||||
{:data data
|
||||
:state state
|
||||
:section section
|
||||
:index index}]
|
||||
;; Set the page title
|
||||
(mf/use-effect
|
||||
(mf/deps (:name file))
|
||||
(fn []
|
||||
(let [name (:name file)]
|
||||
(dom/set-html-title (str "\u25b6 " (tr "title.viewer" name))))))
|
||||
|
||||
[:div.viewer-content {:on-click on-click}
|
||||
(when (:show-thumbnails state)
|
||||
[:& thumbnails-panel {:screen :viewer
|
||||
:index index
|
||||
:data data}])
|
||||
[:& main-panel {:data data
|
||||
:section section
|
||||
:state state
|
||||
:index index}]]]))
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [key1 (events/listen js/window "click" on-click)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)))))
|
||||
|
||||
[:div {:class (dom/classnames
|
||||
:force-visible (:show-thumbnails local)
|
||||
:viewer-layout (not= section :handoff)
|
||||
:handoff-layout (= section :handoff))}
|
||||
|
||||
[:& header {:project project
|
||||
:file file
|
||||
:page page
|
||||
:frame frame
|
||||
:permissions perms
|
||||
:zoom (:zoom local)
|
||||
:section section}]
|
||||
|
||||
[:div.viewer-content
|
||||
[:& thumbnails-panel {:frames frames
|
||||
:show? (:show-thumbnails local false)
|
||||
:page page
|
||||
:index index}]
|
||||
[:section.viewer-preview
|
||||
(cond
|
||||
(empty? frames)
|
||||
[:section.empty-state
|
||||
[:span (tr "viewer.empty-state")]]
|
||||
|
||||
(nil? frame)
|
||||
[:section.empty-state
|
||||
[:span (tr "viewer.frame-not-found")]]
|
||||
|
||||
(some? frame)
|
||||
(if (= :handoff section)
|
||||
[:& handoff/viewport
|
||||
{:frame frame
|
||||
:page page
|
||||
:file file
|
||||
:section section
|
||||
:local local}]
|
||||
|
||||
|
||||
[:div.viewport-container
|
||||
{:style {:width (:width size)
|
||||
:height (:height size)
|
||||
:position "relative"}}
|
||||
|
||||
(when (= section :comments)
|
||||
[:& comments-layer {:file file
|
||||
:users users
|
||||
:frame frame
|
||||
:page page
|
||||
:zoom zoom}])
|
||||
|
||||
[:& interactions/viewport
|
||||
{:frame frame
|
||||
:size size
|
||||
:page page
|
||||
:file file
|
||||
:users users
|
||||
:local local}]]))]]]))
|
||||
|
||||
;; --- Component: Viewer Page
|
||||
|
||||
(mf/defc viewer-page
|
||||
[{:keys [file-id page-id index token section] :as props}]
|
||||
(let [data (mf/deref refs/viewer-data)
|
||||
state (mf/deref refs/viewer-local)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id page-id token)
|
||||
[{:keys [file-id] :as props}]
|
||||
(mf/use-effect
|
||||
(mf/deps file-id)
|
||||
(fn []
|
||||
(st/emit! (dv/initialize props))
|
||||
(fn []
|
||||
(st/emit! (dv/initialize props))))
|
||||
(st/emit! (dv/finalize props)))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:file data))
|
||||
#(when-let [name (get-in data [:file :name])]
|
||||
(dom/set-html-title (str "\u25b6 " (tr "title.viewer" name)))))
|
||||
|
||||
(when (and data state)
|
||||
[:& viewer-content
|
||||
{:index index
|
||||
:section section
|
||||
:state state
|
||||
:data data}])))
|
||||
(when-let [data (mf/deref refs/viewer-data)]
|
||||
(let [key (str (get-in data [:file :id]))]
|
||||
[:& viewer {:params props :data data :key key}])))
|
||||
|
|
158
frontend/src/app/main/ui/viewer/comments.cljs
Normal file
158
frontend/src/app/main/ui/viewer/comments.cljs
Normal file
|
@ -0,0 +1,158 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.comments
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
||||
(mf/defc comments-menu
|
||||
[]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:div.view-options {:on-click toggle-dropdown}
|
||||
[:span.label (tr "labels.comments")]
|
||||
[:span.icon i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= :all cmode))
|
||||
:on-click #(update-mode :all)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-all-comments")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :yours cmode))
|
||||
:on-click #(update-mode :yours)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-your-comments")]]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :pending cshow))
|
||||
:on-click #(update-show (if (= :pending cshow) :all :pending))}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.hide-resolved-comments")]]]]]))
|
||||
|
||||
|
||||
(defn- frame-contains?
|
||||
[{:keys [x y width height]} {px :x py :y}]
|
||||
(let [x2 (+ x width)
|
||||
y2 (+ y height)]
|
||||
(and (<= x px x2)
|
||||
(<= y py y2))))
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def comments-local-ref
|
||||
(l/derived :comments-local st/state))
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [zoom file users frame page] :as props}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
|
||||
modifier1 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
modifier2 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gmt/translate-matrix))
|
||||
|
||||
threads-map (->> (mf/deref threads-ref)
|
||||
(d/mapm #(update %2 :position gpt/transform modifier1)))
|
||||
|
||||
cstate (mf/deref refs/comments-local)
|
||||
|
||||
mframe (geom/transform-shape frame)
|
||||
threads (->> (vals threads-map)
|
||||
(dcm/apply-filters cstate profile)
|
||||
(filter (fn [{:keys [position]}]
|
||||
(frame-contains? mframe position))))
|
||||
|
||||
on-bubble-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(fn [thread]
|
||||
(if (= (:open cstate) (:id thread))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread)))))
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate frame page file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (some? (:open cstate))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(let [event (.-nativeEvent ^js event)
|
||||
position (-> (dom/get-offset-position event)
|
||||
(gpt/transform modifier2))
|
||||
params {:position position
|
||||
:page-id (:id page)
|
||||
:file-id (:id file)}]
|
||||
(st/emit! (dcm/create-draft params))))))
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(st/emitf (dcm/close-thread)))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(mf/deps frame)
|
||||
(fn [draft]
|
||||
(let [params (update draft :position gpt/transform modifier2)]
|
||||
(st/emit! (dcm/create-thread params)
|
||||
(dcm/close-thread)))))]
|
||||
|
||||
[:div.comments-section {:on-click on-click}
|
||||
[:div.viewer-comments-container
|
||||
[:div.threads
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open cstate))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open cstate)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users users
|
||||
:zoom zoom}]))
|
||||
|
||||
(when-let [draft (:draft cstate)]
|
||||
[:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1)
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
68
frontend/src/app/main/ui/viewer/handoff.cljs
Normal file
68
frontend/src/app/main/ui/viewer/handoff.cljs
Normal 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) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff
|
||||
(:require
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]]
|
||||
[app.main.ui.viewer.handoff.render :refer [render-frame-svg]]
|
||||
[app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn handle-select-frame
|
||||
[frame]
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/select-shape (:id frame)))))
|
||||
|
||||
(mf/defc viewport
|
||||
[{:keys [local file page frame]}]
|
||||
(let [on-mouse-wheel
|
||||
(fn [event]
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event)
|
||||
(.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global EventType.WHEEL
|
||||
on-mouse-wheel #js {"passive" false})]
|
||||
(fn []
|
||||
(events/unlistenByKey key1))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:id frame))
|
||||
(fn []
|
||||
(st/emit! (dv/set-current-frame (:id frame))
|
||||
(dv/select-shape (:id frame)))))
|
||||
|
||||
[:*
|
||||
[:& left-sidebar {:frame frame
|
||||
:local local
|
||||
:page page}]
|
||||
[:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)}
|
||||
[:div.handoff-svg-container
|
||||
[:& render-frame-svg {:frame frame :page page :local local}]]]
|
||||
|
||||
[:& right-sidebar {:frame frame
|
||||
:selected (:selected local)
|
||||
:page page
|
||||
:file file}]]))
|
|
@ -4,18 +4,18 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes
|
||||
(ns app.main.ui.viewer.handoff.attributes
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.handoff.attributes.blur :refer [blur-panel]]
|
||||
[app.main.ui.handoff.attributes.fill :refer [fill-panel]]
|
||||
[app.main.ui.handoff.attributes.image :refer [image-panel]]
|
||||
[app.main.ui.handoff.attributes.layout :refer [layout-panel]]
|
||||
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
|
||||
[app.main.ui.handoff.attributes.stroke :refer [stroke-panel]]
|
||||
[app.main.ui.handoff.attributes.svg :refer [svg-panel]]
|
||||
[app.main.ui.handoff.attributes.text :refer [text-panel]]
|
||||
[app.main.ui.handoff.exports :refer [exports]]
|
||||
[app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.svg :refer [svg-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.text :refer [text-panel]]
|
||||
[app.main.ui.viewer.handoff.exports :refer [exports]]
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.alpha :as mf]))
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.blur
|
||||
(ns app.main.ui.viewer.handoff.attributes.blur
|
||||
(:require
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.common
|
||||
(ns app.main.ui.viewer.handoff.attributes.common
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.main.store :as st]
|
|
@ -4,10 +4,10 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.fill
|
||||
(ns app.main.ui.viewer.handoff.attributes.fill
|
||||
(:require
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.image
|
||||
(ns app.main.ui.viewer.handoff.attributes.image
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.layout
|
||||
(ns app.main.ui.viewer.handoff.attributes.layout
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
|
@ -4,11 +4,11 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.shadow
|
||||
(ns app.main.ui.viewer.handoff.attributes.shadow
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
|
@ -4,12 +4,12 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.stroke
|
||||
(ns app.main.ui.viewer.handoff.attributes.stroke
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [t]]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.svg
|
||||
(ns app.main.ui.viewer.handoff.attributes.svg
|
||||
(:require
|
||||
#_[app.common.math :as mth]
|
||||
#_[app.main.ui.icons :as i]
|
|
@ -4,13 +4,13 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.attributes.text
|
||||
(ns app.main.ui.viewer.handoff.attributes.text
|
||||
(:require
|
||||
[app.common.text :as txt]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.code
|
||||
(ns app.main.ui.viewer.handoff.code
|
||||
(:require
|
||||
["js-beautify" :as beautify]
|
||||
[app.common.geom.shapes :as gsh]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.exports
|
||||
(ns app.main.ui.viewer.handoff.exports
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.messages :as dm]
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.left-sidebar
|
||||
(ns app.main.ui.viewer.handoff.left-sidebar
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.viewer :as dv]
|
||||
|
@ -16,12 +16,6 @@
|
|||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def selected-shapes
|
||||
(l/derived (comp :selected :viewer-local) st/state))
|
||||
|
||||
(def page-ref
|
||||
(l/derived (comp :page :viewer-data) st/state))
|
||||
|
||||
(defn- make-collapsed-iref
|
||||
[id]
|
||||
#(-> (l/in [:viewer-local :collapsed id])
|
||||
|
@ -31,7 +25,9 @@
|
|||
[{:keys [item selected objects disable-collapse?] :as props}]
|
||||
(let [id (:id item)
|
||||
selected? (contains? selected id)
|
||||
item-ref (mf/use-ref nil)
|
||||
item-ref (mf/use-ref nil)
|
||||
|
||||
|
||||
collapsed-iref (mf/use-memo
|
||||
(mf/deps id)
|
||||
(make-collapsed-iref id))
|
||||
|
@ -94,10 +90,10 @@
|
|||
:objects objects
|
||||
:key (:id item)}]))])]))
|
||||
|
||||
(mf/defc left-sidebar [{:keys [frame]}]
|
||||
(let [page (mf/deref page-ref)
|
||||
selected (mf/deref selected-shapes)
|
||||
objects (:objects page)]
|
||||
(mf/defc left-sidebar
|
||||
[{:keys [frame page local]}]
|
||||
(let [selected (:selected local)
|
||||
objects (:objects page)]
|
||||
|
||||
[:aside.settings-bar.settings-bar-left
|
||||
[:div.settings-bar-inside
|
|
@ -4,17 +4,12 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.render
|
||||
(ns app.main.ui.viewer.handoff.render
|
||||
"The main container for a frame in handoff mode"
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.handoff.selection-feedback :refer [selection-feedback]]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.group :as group]
|
||||
|
@ -24,17 +19,21 @@
|
|||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.viewer.handoff.selection-feedback :refer [selection-feedback]]
|
||||
[app.main.ui.viewer.interactions :refer [prepare-objects]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(declare shape-container-factory)
|
||||
|
||||
(defn handle-hover-shape [{:keys [type id]} hover?]
|
||||
#(when-not (#{:group :frame} type)
|
||||
(dom/prevent-default %)
|
||||
(dom/stop-propagation %)
|
||||
(st/emit! (dv/hover-shape id hover?))))
|
||||
(defn handle-hover-shape
|
||||
[{:keys [type id]} hover?]
|
||||
(fn [event]
|
||||
(when-not (#{:group :frame} type)
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dv/hover-shape id hover?)))))
|
||||
|
||||
(defn select-shape [{:keys [type id]}]
|
||||
(fn [event]
|
||||
|
@ -42,7 +41,7 @@
|
|||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(cond
|
||||
(.-shiftKey event)
|
||||
(.-shiftKey ^js event)
|
||||
(st/emit! (dv/toggle-selection id))
|
||||
|
||||
:else
|
||||
|
@ -154,42 +153,37 @@
|
|||
:group [:> group-container opts]
|
||||
:svg-raw [:> svg-raw-container opts])))))))
|
||||
|
||||
(defn adjust-frame-position [frame-id objects]
|
||||
(let [frame (get objects frame-id)
|
||||
modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
|
||||
modifier-ids (d/concat [frame-id] (cp/get-children frame-id objects))]
|
||||
(reduce update-fn objects modifier-ids)))
|
||||
|
||||
(defn make-vbox [frame]
|
||||
(str "0 0 " (:width frame 0) " " (:height frame 0)))
|
||||
|
||||
(mf/defc render-frame-svg
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects frame-id zoom] :or {zoom 1} :as props}]
|
||||
[{:keys [page frame local]}]
|
||||
(let [objects (mf/use-memo
|
||||
(mf/deps page frame)
|
||||
(prepare-objects page frame))
|
||||
|
||||
(let [objects (adjust-frame-position frame-id objects)
|
||||
frame (get objects frame-id)
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
vbox (make-vbox frame)
|
||||
render-frame (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(frame-container-factory objects))]
|
||||
|
||||
[:svg {:id "svg-frame"
|
||||
:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
;; Retrieve frame again with correct modifier
|
||||
frame (get objects (:id frame))
|
||||
|
||||
[:& render-frame {:shape frame
|
||||
:view-box vbox}]
|
||||
zoom (:zoom local 1)
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
vbox (str "0 0 " (:width frame 0) " " (:height frame 0))
|
||||
|
||||
[:& selection-feedback {:frame frame}]]))
|
||||
render (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(frame-container-factory objects))]
|
||||
|
||||
[:svg
|
||||
{:id "svg-frame"
|
||||
:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
|
||||
[:& render {:shape frame :view-box vbox}]
|
||||
[:& selection-feedback
|
||||
{:frame frame
|
||||
:objects objects
|
||||
:local local}]]))
|
||||
|
|
@ -4,37 +4,26 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.right-sidebar
|
||||
(ns app.main.ui.viewer.handoff.right-sidebar
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||
[app.main.ui.handoff.attributes :refer [attributes]]
|
||||
[app.main.ui.handoff.code :refer [code]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.handoff.attributes :refer [attributes]]
|
||||
[app.main.ui.viewer.handoff.code :refer [code]]
|
||||
[app.main.ui.viewer.handoff.selection-feedback :refer [resolve-shapes]]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon]]
|
||||
[app.util.i18n :refer [t] :as i18n]
|
||||
[okulary.core :as l]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn make-selected-shapes-iref
|
||||
[]
|
||||
(let [selected->shapes
|
||||
(fn [state]
|
||||
(let [selected (get-in state [:viewer-local :selected])
|
||||
objects (get-in state [:viewer-data :page :objects])
|
||||
resolve-shape #(get objects %)]
|
||||
(mapv resolve-shape selected)))]
|
||||
#(l/derived selected->shapes st/state)))
|
||||
|
||||
(mf/defc right-sidebar
|
||||
[{:keys [frame page-id file-id]}]
|
||||
(let [expanded (mf/use-state false)
|
||||
locale (mf/deref i18n/locale)
|
||||
section (mf/use-state :info #_:code)
|
||||
selected-ref (mf/use-memo (make-selected-shapes-iref))
|
||||
shapes (mf/deref selected-ref)
|
||||
selected-type (-> shapes first (:type :not-found))]
|
||||
[{:keys [frame page file selected]}]
|
||||
(let [expanded (mf/use-state false)
|
||||
section (mf/use-state :info #_:code)
|
||||
|
||||
shapes (resolve-shapes (:objects page) selected)
|
||||
selected-type (or (-> shapes first :type) :not-found)]
|
||||
|
||||
[:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")}
|
||||
[:div.settings-bar-inside
|
||||
(when (seq shapes)
|
||||
|
@ -43,24 +32,24 @@
|
|||
(if (> (count shapes) 1)
|
||||
[:*
|
||||
[:span.tool-window-bar-icon i/layers]
|
||||
[:span.tool-window-bar-title (t locale "handoff.tabs.code.selected.multiple" (count shapes))]]
|
||||
[:span.tool-window-bar-title (tr "handoff.tabs.code.selected.multiple" (count shapes))]]
|
||||
[:*
|
||||
[:span.tool-window-bar-icon
|
||||
[:& element-icon {:shape (-> shapes first)}]]
|
||||
[:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (t locale))]])
|
||||
[:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (tr))]])
|
||||
]
|
||||
[:div.tool-window-content
|
||||
[:& tab-container {:on-change-tab #(do
|
||||
(reset! expanded false)
|
||||
(reset! section %))
|
||||
:selected @section}
|
||||
[:& tab-element {:id :info :title (t locale "handoff.tabs.info")}
|
||||
[:& attributes {:page-id page-id
|
||||
:file-id file-id
|
||||
[:& tab-element {:id :info :title (tr "handoff.tabs.info")}
|
||||
[:& attributes {:page-id (:id page)
|
||||
:file-id (:id file)
|
||||
:frame frame
|
||||
:shapes shapes}]]
|
||||
|
||||
[:& tab-element {:id :code :title (t locale "handoff.tabs.code")}
|
||||
[:& tab-element {:id :code :title (tr "handoff.tabs.code")}
|
||||
[:& code {:frame frame
|
||||
:shapes shapes
|
||||
:on-expand #(swap! expanded not)}]]]]])]]))
|
|
@ -4,12 +4,10 @@
|
|||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.handoff.selection-feedback
|
||||
(ns app.main.ui.viewer.handoff.selection-feedback
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.measurements :refer [selection-guides size-display measurement]]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; ------------------------------------------------
|
||||
|
@ -21,33 +19,12 @@
|
|||
(def select-guide-width 1)
|
||||
(def select-guide-dasharray 5)
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; LENSES
|
||||
;; ------------------------------------------------
|
||||
|
||||
(defn make-selected-shapes-iref
|
||||
"Creates a lens to the current selected shapes"
|
||||
[]
|
||||
(let [selected->shapes
|
||||
(fn [state]
|
||||
(let [selected (get-in state [:viewer-local :selected])
|
||||
objects (get-in state [:viewer-data :page :objects])
|
||||
resolve-shape #(get objects %)]
|
||||
(->> selected (map resolve-shape) (filterv (comp not nil?)))))]
|
||||
#(l/derived selected->shapes st/state)))
|
||||
|
||||
(defn make-hover-shapes-iref
|
||||
"Creates a lens to the shapes the user is making hover"
|
||||
[]
|
||||
(let [hover->shapes
|
||||
(fn [state]
|
||||
(let [hover (get-in state [:viewer-local :hover])
|
||||
objects (get-in state [:viewer-data :page :objects])]
|
||||
(get objects hover)))]
|
||||
#(l/derived hover->shapes st/state)))
|
||||
|
||||
(def selected-zoom
|
||||
(l/derived (l/in [:viewer-local :zoom]) st/state))
|
||||
(defn resolve-shapes
|
||||
[objects ids]
|
||||
(let [resolve-shape #(get objects %)]
|
||||
(into [] (comp (map resolve-shape)
|
||||
(filter some?))
|
||||
ids)))
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; HELPERS
|
||||
|
@ -75,19 +52,17 @@
|
|||
:stroke select-color
|
||||
:stroke-width selection-rect-width}}]]))
|
||||
|
||||
(mf/defc selection-feedback [{:keys [frame]}]
|
||||
(let [zoom (mf/deref selected-zoom)
|
||||
|
||||
hover-shapes-ref (mf/use-memo (make-hover-shapes-iref))
|
||||
hover-shape (-> (or (mf/deref hover-shapes-ref) frame)
|
||||
(gsh/translate-to-frame frame))
|
||||
|
||||
selected-shapes-ref (mf/use-memo (make-selected-shapes-iref))
|
||||
selected-shapes (->> (mf/deref selected-shapes-ref)
|
||||
(mf/defc selection-feedback
|
||||
[{:keys [frame local objects]}]
|
||||
(let [{:keys [hover selected zoom]} local
|
||||
hover-shape (-> (or (first (resolve-shapes objects [hover])) frame)
|
||||
(gsh/translate-to-frame frame))
|
||||
selected-shapes (->> (resolve-shapes objects selected)
|
||||
(map #(gsh/translate-to-frame % frame)))
|
||||
|
||||
selrect (gsh/selection-rect selected-shapes)
|
||||
bounds (frame->bounds frame)]
|
||||
selrect (gsh/selection-rect selected-shapes)
|
||||
bounds (frame->bounds frame)]
|
||||
|
||||
|
||||
(when (seq selected-shapes)
|
||||
[:g.selection-feedback {:pointer-events "none"}
|
|
@ -6,304 +6,155 @@
|
|||
|
||||
(ns app.main.ui.viewer.header
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.viewer.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.fullscreen :as fs]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.comments :refer [comments-menu]]
|
||||
[app.main.ui.viewer.interactions :refer [interactions-menu]]
|
||||
[app.main.ui.workspace.header :refer [zoom-widget]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc zoom-widget
|
||||
{:wrap [mf/memo]}
|
||||
[{:keys [zoom
|
||||
on-increase
|
||||
on-decrease
|
||||
on-zoom-to-50
|
||||
on-zoom-to-100
|
||||
on-zoom-to-200
|
||||
on-fullscreen]
|
||||
:as props}]
|
||||
(let [show-dropdown? (mf/use-state false)]
|
||||
[:div.zoom-widget {:on-click #(reset! show-dropdown? true)}
|
||||
[:span {} (str (mth/round (* 100 zoom)) "%")]
|
||||
[:span.dropdown-button i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(reset! show-dropdown? false)}
|
||||
[:ul.dropdown.zoom-dropdown
|
||||
[:li {:on-click on-increase}
|
||||
"Zoom in" [:span (sc/get-tooltip :increase-zoom)]]
|
||||
[:li {:on-click on-decrease}
|
||||
"Zoom out" [:span (sc/get-tooltip :decrease-zoom)]]
|
||||
[:li {:on-click on-zoom-to-50}
|
||||
"Zoom to 50%" [:span (sc/get-tooltip :zoom-50)]]
|
||||
[:li {:on-click on-zoom-to-100}
|
||||
"Zoom to 100%" [:span (sc/get-tooltip :reset-zoom)]]
|
||||
[:li {:on-click on-zoom-to-200}
|
||||
"Zoom to 200%" [:span (sc/get-tooltip :zoom-200)]]
|
||||
[:li {:on-click on-fullscreen}
|
||||
"Full screen"]]]]))
|
||||
;; "Full screen" [:span (sc/get-tooltip :full-screen)]]]]]))
|
||||
|
||||
(mf/defc share-link
|
||||
[{:keys [token] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
dropdown-ref (mf/use-ref)
|
||||
create (st/emitf (dv/create-share-link))
|
||||
delete (st/emitf (dv/delete-share-link))
|
||||
|
||||
router (mf/deref refs/router)
|
||||
route (mf/deref refs/route)
|
||||
link (rt/resolve router
|
||||
:viewer
|
||||
(:path-params route)
|
||||
{:token token :index "0"})
|
||||
link (assoc cfg/public-uri :fragment link)
|
||||
|
||||
copy-link
|
||||
(fn [_]
|
||||
(wapi/write-to-clipboard (str link))
|
||||
(st/emit! (dm/show {:type :info
|
||||
:content "Link copied successfuly!"
|
||||
:timeout 3000})))]
|
||||
[:*
|
||||
[:span.btn-primary.btn-small
|
||||
{:alt (tr "viewer.header.share.title")
|
||||
:on-click #(swap! show-dropdown? not)}
|
||||
(tr "viewer.header.share.title")]
|
||||
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)
|
||||
:container dropdown-ref}
|
||||
[:div.dropdown.share-link-dropdown {:ref dropdown-ref}
|
||||
[:span.share-link-title (tr "viewer.header.share.title")]
|
||||
[:div.share-link-input
|
||||
(if (string? token)
|
||||
[:*
|
||||
[:span.link (str link)]
|
||||
[:span.link-button {:on-click copy-link}
|
||||
(tr "viewer.header.share.copy-link")]]
|
||||
[:span.link-placeholder (tr "viewer.header.share.placeholder")])]
|
||||
|
||||
[:span.share-link-subtitle (tr "viewer.header.share.subtitle")]
|
||||
[:div.share-link-buttons
|
||||
(if (string? token)
|
||||
[:button.btn-warning {:on-click delete}
|
||||
(tr "viewer.header.share.remove-link")]
|
||||
[:button.btn-primary {:on-click create}
|
||||
(tr "viewer.header.share.create-link")])]]]]))
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[{:keys [state] :as props}]
|
||||
(let [imode (:interactions-mode state)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
select-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dv/set-interactions-mode mode))))]
|
||||
|
||||
[:div.view-options
|
||||
[:div.view-options-dropdown {:on-click toggle-dropdown}
|
||||
[:span (tr "viewer.header.interactions")]
|
||||
i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= imode :hide))
|
||||
:on-click #(select-mode :hide)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.dont-show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= imode :show))
|
||||
:on-click #(select-mode :show)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= imode :show-on-click))
|
||||
:on-click #(select-mode :show-on-click)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions-on-click")]]]]]))
|
||||
|
||||
(mf/defc comments-menu
|
||||
[]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:div.view-options
|
||||
[:div.icon {:on-click toggle-dropdown} i/eye]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= :all cmode))
|
||||
:on-click #(update-mode :all)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-all-comments")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :yours cmode))
|
||||
:on-click #(update-mode :yours)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.show-your-comments")]]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :pending cshow))
|
||||
:on-click #(update-show (if (= :pending cshow) :all :pending))}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "labels.hide-resolved-comments")]]]]]))
|
||||
|
||||
(mf/defc file-menu
|
||||
[{:keys [project-id file-id page-id] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
on-edit
|
||||
(mf/use-callback
|
||||
(mf/deps project-id file-id page-id)
|
||||
(st/emitf (rt/nav :workspace
|
||||
{:project-id project-id
|
||||
:file-id file-id}
|
||||
{:page-id page-id})))]
|
||||
[:div.file-menu
|
||||
[:span.btn-icon-dark.btn-small {:on-click toggle-dropdown}
|
||||
i/actions
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown
|
||||
[:li {:on-click on-edit}
|
||||
[:span.label (tr "viewer.header.edit-file")]]]]]]))
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [data index section state] :as props}]
|
||||
(let [{:keys [project file page frames]} data
|
||||
|
||||
fullscreen (mf/use-ctx fs/fullscreen-context)
|
||||
|
||||
total (count frames)
|
||||
profile (mf/deref refs/profile)
|
||||
teams (mf/deref refs/teams)
|
||||
|
||||
team-id (get-in data [:project :team-id])
|
||||
|
||||
has-permission? (and (not= uuid/zero (:id profile))
|
||||
(contains? teams team-id))
|
||||
|
||||
project-id (get-in data [:project :id])
|
||||
file-id (get-in data [:file :id])
|
||||
page-id (get-in data [:page :id])
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(st/emitf dv/toggle-thumbnails-panel))
|
||||
|
||||
on-goback
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
(st/emitf (dv/go-to-dashboard project)))
|
||||
|
||||
navigate
|
||||
(mf/use-callback
|
||||
(mf/deps file-id page-id)
|
||||
(fn [section]
|
||||
(st/emit! (dv/go-to-section section))))
|
||||
(mf/defc header-options
|
||||
[{:keys [section zoom page file permissions]}]
|
||||
(let [fullscreen (mf/use-ctx fs/fullscreen-context)
|
||||
|
||||
toggle-fullscreen
|
||||
(mf/use-callback
|
||||
(mf/deps fullscreen)
|
||||
(fn []
|
||||
(if @fullscreen (fullscreen false) (fullscreen true))))]
|
||||
(mf/deps fullscreen)
|
||||
(fn []
|
||||
(if @fullscreen (fullscreen false) (fullscreen true))))
|
||||
|
||||
go-to-workspace
|
||||
(mf/use-callback
|
||||
(mf/deps page)
|
||||
(fn []
|
||||
(st/emit! (dv/go-to-workspace (:id page)))))
|
||||
|
||||
open-share-dialog
|
||||
(mf/use-callback
|
||||
(mf/deps page)
|
||||
(fn []
|
||||
(modal/show! :share-link {:page page :file file})))]
|
||||
|
||||
[:div.options-zone
|
||||
(case section
|
||||
:interactions [:& interactions-menu]
|
||||
:comments [:& comments-menu]
|
||||
|
||||
[:div.view-options])
|
||||
|
||||
[:& zoom-widget
|
||||
{:zoom zoom
|
||||
:on-increase (st/emitf dv/increase-zoom)
|
||||
:on-decrease (st/emitf dv/decrease-zoom)
|
||||
:on-zoom-to-50 (st/emitf dv/zoom-to-50)
|
||||
:on-zoom-to-100 (st/emitf dv/reset-zoom)
|
||||
:on-zoom-to-200 (st/emitf dv/zoom-to-200)
|
||||
:on-fullscreen toggle-fullscreen}]
|
||||
|
||||
[:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
|
||||
{:alt (tr "viewer.header.fullscreen")
|
||||
:on-click toggle-fullscreen}
|
||||
(if @fullscreen
|
||||
i/full-screen-off
|
||||
i/full-screen)]
|
||||
|
||||
(when (:edit permissions)
|
||||
[:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")])
|
||||
|
||||
(when (:edit permissions)
|
||||
[:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])]))
|
||||
|
||||
(mf/defc header-sitemap
|
||||
[{:keys [project file page frame] :as props}]
|
||||
(let [project-name (:name project)
|
||||
file-name (:name file)
|
||||
page-name (:name page)
|
||||
frame-name (:name frame)
|
||||
|
||||
toggle-thumbnails
|
||||
(fn []
|
||||
(st/emit! dv/toggle-thumbnails-panel))
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
|
||||
navigate-to
|
||||
(fn [page-id]
|
||||
(st/emit! (dv/go-to-page page-id))
|
||||
(reset! show-dropdown? false))
|
||||
]
|
||||
|
||||
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")}
|
||||
[:div.breadcrumb
|
||||
{:on-click #(swap! show-dropdown? not)}
|
||||
[:span.project-name project-name]
|
||||
[:span "/"]
|
||||
[:span.file-name file-name]
|
||||
[:span "/"]
|
||||
[:span.page-name page-name]
|
||||
[:span.icon i/arrow-down]
|
||||
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)}
|
||||
[:ul.dropdown
|
||||
(for [id (get-in file [:data :pages])]
|
||||
[:li {:id (str id)
|
||||
:on-click (partial navigate-to id)}
|
||||
(get-in file [:data :pages-index id :name])])]]]
|
||||
|
||||
[:div.current-frame
|
||||
{:on-click toggle-thumbnails}
|
||||
[:span.label "/"]
|
||||
[:span.label frame-name]
|
||||
[:span.icon i/arrow-down]]]))
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [project file page frame zoom section permissions]}]
|
||||
(let [go-to-dashboard
|
||||
(st/emitf (dv/go-to-dashboard))
|
||||
|
||||
navigate
|
||||
(fn [section]
|
||||
(st/emit! (dv/go-to-section section)))]
|
||||
|
||||
|
||||
[:header.viewer-header
|
||||
[:div.main-icon
|
||||
[:a {:on-click on-goback
|
||||
[:a {:on-click go-to-dashboard
|
||||
;; If the user doesn't have permission we disable the link
|
||||
:style {:pointer-events (when-not has-permission? "none")}} i/logo-icon]]
|
||||
:style {:pointer-events (when-not permissions "none")}} i/logo-icon]]
|
||||
|
||||
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")
|
||||
:on-click on-click}
|
||||
[:span.project-name (:name project)]
|
||||
[:span "/"]
|
||||
[:span.file-name (:name file)]
|
||||
[:span "/"]
|
||||
[:span.page-name (:name page)]
|
||||
[:span.show-thumbnails-button i/arrow-down]
|
||||
[:span.counters (str (inc index) " / " total)]]
|
||||
[:& header-sitemap {:project project :file file :page page :frame frame}]
|
||||
|
||||
[:div.mode-zone
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :interactions)
|
||||
:class (dom/classnames :active (= section :interactions))
|
||||
:alt "View mode"}
|
||||
:alt (tr "viewer.header.interactions-section")}
|
||||
i/play]
|
||||
|
||||
(when has-permission?
|
||||
(when (:edit permissions)
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :comments)
|
||||
:class (dom/classnames :active (= section :comments))
|
||||
:alt "Comments"}
|
||||
:alt (tr "viewer.header.comments-section")}
|
||||
i/chat])
|
||||
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :handoff)
|
||||
:class (dom/classnames :active (= section :handoff))
|
||||
:alt "Code mode"}
|
||||
i/code]]
|
||||
(when (:read permissions)
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :handoff)
|
||||
:class (dom/classnames :active (= section :handoff))
|
||||
:alt (tr "viewer.header.handsoff-section")}
|
||||
i/code])]
|
||||
|
||||
[:div.options-zone
|
||||
(case section
|
||||
:interactions [:& interactions-menu {:state state}]
|
||||
:comments [:& comments-menu]
|
||||
nil)
|
||||
|
||||
(when has-permission?
|
||||
[:& share-link {:token (:token data)
|
||||
:page (:page data)}])
|
||||
|
||||
[:& zoom-widget
|
||||
{:zoom (:zoom state)
|
||||
:on-increase (st/emitf dv/increase-zoom)
|
||||
:on-decrease (st/emitf dv/decrease-zoom)
|
||||
:on-zoom-to-50 (st/emitf dv/zoom-to-50)
|
||||
:on-zoom-to-100 (st/emitf dv/reset-zoom)
|
||||
:on-zoom-to-200 (st/emitf dv/zoom-to-200)
|
||||
:on-fullscreen toggle-fullscreen}]
|
||||
|
||||
[:span.btn-icon-basic.btn-small.tooltip.tooltip-bottom-left
|
||||
{:alt (tr "viewer.header.fullscreen")
|
||||
:on-click toggle-fullscreen}
|
||||
(if @fullscreen
|
||||
i/full-screen-off
|
||||
i/full-screen)]
|
||||
|
||||
(when has-permission?
|
||||
[:& file-menu {:project-id project-id
|
||||
:file-id file-id
|
||||
:page-id page-id}])]]))
|
||||
[:& header-options {:section section
|
||||
:permissions permissions
|
||||
:page page
|
||||
:file file
|
||||
:zoom zoom}]]))
|
||||
|
||||
|
|
136
frontend/src/app/main/ui/viewer/interactions.cljs
Normal file
136
frontend/src/app/main/ui/viewer/interactions.cljs
Normal file
|
@ -0,0 +1,136 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.interactions
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.shapes :as shapes]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn prepare-objects
|
||||
[page frame]
|
||||
(fn []
|
||||
(let [objects (:objects page)
|
||||
frame-id (:id frame)
|
||||
modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)]
|
||||
|
||||
(->> (cp/get-children frame-id objects)
|
||||
(d/concat [frame-id])
|
||||
(reduce update-fn objects)))))
|
||||
|
||||
|
||||
(mf/defc viewport
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [local page frame size]}]
|
||||
(let [interactions? (:interactions-show? local)
|
||||
|
||||
objects (mf/use-memo
|
||||
(mf/deps page frame)
|
||||
(prepare-objects page frame))
|
||||
|
||||
wrapper (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(shapes/frame-container-factory objects interactions?))
|
||||
|
||||
;; Retrieve frame again with correct modifier
|
||||
frame (get objects (:id frame))
|
||||
|
||||
on-click
|
||||
(fn [_]
|
||||
(let [mode (:interactions-mode local)]
|
||||
(when (= mode :show-on-click)
|
||||
(st/emit! dv/flash-interactions))))
|
||||
|
||||
on-mouse-wheel
|
||||
(fn [event]
|
||||
(when (or (kbd/ctrl? event) (kbd/meta? event))
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
delta (+ (.-deltaY ^js event) (.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
|
||||
on-key-down
|
||||
(fn [event]
|
||||
(when (kbd/esc? event)
|
||||
(st/emit! (dcm/close-thread))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false})
|
||||
key2 (events/listen js/window "keydown" on-key-down)
|
||||
key3 (events/listen js/window "click" on-click)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2)
|
||||
(events/unlistenByKey key3)))))
|
||||
|
||||
[:svg {:view-box (:vbox size)
|
||||
:width (:width size)
|
||||
:height (:height size)
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
[:& wrapper {:shape frame
|
||||
:show-interactions? interactions?
|
||||
:view-box (:vbox size)}]]))
|
||||
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[]
|
||||
(let [local (mf/deref refs/viewer-local)
|
||||
mode (:interactions-mode local)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
select-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dv/set-interactions-mode mode))))]
|
||||
|
||||
[:div.view-options {:on-click toggle-dropdown}
|
||||
[:span.label (tr "viewer.header.interactions")]
|
||||
[:span.icon i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= mode :hide))
|
||||
:on-click #(select-mode :hide)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.dont-show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= mode :show))
|
||||
:on-click #(select-mode :show)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= mode :show-on-click))
|
||||
:on-click #(select-mode :show-on-click)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (tr "viewer.header.show-interactions-on-click")]]]]]))
|
||||
|
|
@ -10,11 +10,11 @@
|
|||
[app.main.data.viewer :as dv]
|
||||
[app.main.exports :as exports]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[goog.object :as gobj]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as ts]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc thumbnails-content
|
||||
|
@ -50,7 +50,7 @@
|
|||
on-mount
|
||||
(fn []
|
||||
(let [dom (mf/ref-val container)]
|
||||
(reset! width (gobj/get dom "clientWidth"))))]
|
||||
(reset! width (obj/get dom "clientWidth"))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(if expanded?
|
||||
|
@ -72,7 +72,8 @@
|
|||
[:span.btn-close {:on-click on-close} i/close]]])
|
||||
|
||||
(mf/defc thumbnail-item
|
||||
[{:keys [selected? frame on-click index objects] :as props}]
|
||||
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||
[{:keys [selected? frame on-click index objects]}]
|
||||
[:div.thumbnail-item {:on-click #(on-click % index)}
|
||||
[:div.thumbnail-preview
|
||||
{:class (dom/classnames :selected selected?)}
|
||||
|
@ -81,42 +82,39 @@
|
|||
[:span.name {:title (:name frame)} (:name frame)]]])
|
||||
|
||||
(mf/defc thumbnails-panel
|
||||
[{:keys [data index] :as props}]
|
||||
[{:keys [frames page index show?] :as props}]
|
||||
(let [expanded? (mf/use-state false)
|
||||
container (mf/use-ref)
|
||||
|
||||
on-close #(st/emit! dv/toggle-thumbnails-panel)
|
||||
selected (mf/use-var false)
|
||||
objects (:objects page)
|
||||
|
||||
on-mouse-leave
|
||||
(fn [_]
|
||||
(when @selected
|
||||
(on-close)))
|
||||
on-close #(st/emit! dv/toggle-thumbnails-panel)
|
||||
selected (mf/use-var false)
|
||||
|
||||
on-item-click
|
||||
(fn [_ index]
|
||||
(compare-and-set! selected false true)
|
||||
(st/emit! (dv/go-to-frame-by-index index))
|
||||
(when @expanded?
|
||||
(on-close)))]
|
||||
(mf/use-callback
|
||||
(mf/deps @expanded?)
|
||||
(fn [_ index]
|
||||
(compare-and-set! selected false true)
|
||||
(st/emit! (dv/go-to-frame-by-index index))
|
||||
(when @expanded?
|
||||
(on-close))))]
|
||||
|
||||
[:& dropdown' {:on-close on-close
|
||||
:container container
|
||||
:show true}
|
||||
[:section.viewer-thumbnails
|
||||
{:class (dom/classnames :expanded @expanded?)
|
||||
:ref container
|
||||
:on-mouse-leave on-mouse-leave}
|
||||
[:section.viewer-thumbnails
|
||||
{:class (dom/classnames :expanded @expanded?
|
||||
:invisible (not show?))
|
||||
|
||||
[:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not)
|
||||
:on-close on-close
|
||||
:total (count (:frames data))}]
|
||||
[:& thumbnails-content {:expanded? @expanded?
|
||||
:total (count (:frames data))}
|
||||
(for [[i frame] (d/enumerate (:frames data))]
|
||||
[:& thumbnail-item {:key i
|
||||
:index i
|
||||
:frame frame
|
||||
:objects (:objects data)
|
||||
:on-click on-item-click
|
||||
:selected? (= i index)}])]]]))
|
||||
:ref container
|
||||
}
|
||||
|
||||
[:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not)
|
||||
:on-close on-close
|
||||
:total (count frames)}]
|
||||
[:& thumbnails-content {:expanded? @expanded?
|
||||
:total (count frames)}
|
||||
(for [[i frame] (d/enumerate frames)]
|
||||
[:& thumbnail-item {:index i
|
||||
:frame frame
|
||||
:objects objects
|
||||
:on-click on-item-click
|
||||
:selected? (= i index)}])]]))
|
||||
|
|
|
@ -64,15 +64,16 @@
|
|||
on-decrease
|
||||
on-zoom-reset
|
||||
on-zoom-fit
|
||||
on-zoom-selected]
|
||||
on-zoom-selected
|
||||
on-fullscreen]
|
||||
:as props}]
|
||||
(let [show-dropdown? (mf/use-state false)]
|
||||
[:div.zoom-widget {:on-click #(reset! show-dropdown? true)}
|
||||
[:span {} (str (mth/round (* 100 zoom)) "%")]
|
||||
[:span.dropdown-button i/arrow-down]
|
||||
[:span.label {} (str (mth/round (* 100 zoom)) "%")]
|
||||
[:span.icon i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(reset! show-dropdown? false)}
|
||||
[:ul.zoom-dropdown
|
||||
[:ul.dropdown
|
||||
[:li {:on-click on-increase}
|
||||
"Zoom in" [:span (sc/get-tooltip :increase-zoom)]]
|
||||
[:li {:on-click on-decrease}
|
||||
|
@ -82,7 +83,11 @@
|
|||
[:li {:on-click on-zoom-fit}
|
||||
"Zoom to fit all" [:span (sc/get-tooltip :fit-all)]]
|
||||
[:li {:on-click on-zoom-selected}
|
||||
"Zoom to selected" [:span (sc/get-tooltip :zoom-selected)]]]]]))
|
||||
"Zoom to selected" [:span (sc/get-tooltip :zoom-selected)]]
|
||||
(when on-fullscreen
|
||||
[:li {:on-click on-fullscreen}
|
||||
"Full screen"])]]]))
|
||||
|
||||
|
||||
;; --- Header Users
|
||||
|
||||
|
|
|
@ -108,14 +108,30 @@
|
|||
(let [router (:router state)
|
||||
path (resolve router id params qparams)
|
||||
uri (-> (u/uri cfg/public-uri)
|
||||
(assoc :fragment path))]
|
||||
(js/window.open (str uri) "_blank"))))
|
||||
(assoc :fragment path))
|
||||
name (str (name id) "-" (:file-id params))]
|
||||
(js/window.open (str uri) name))))
|
||||
|
||||
(defn nav-new-window
|
||||
([id] (nav-new-window id nil nil))
|
||||
([id params] (nav-new-window id params nil))
|
||||
([id params qparams] (NavigateNewWindow. id params qparams)))
|
||||
|
||||
|
||||
(defn nav-new-window*
|
||||
[{:keys [rname path-params query-params name]}]
|
||||
(ptk/reify ::nav-new-window
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [router (:router state)
|
||||
path (resolve router rname path-params query-params)
|
||||
uri (-> (u/uri cfg/public-uri)
|
||||
(assoc :fragment path))]
|
||||
|
||||
|
||||
|
||||
(js/window.open (str uri) name)))))
|
||||
|
||||
;; --- History API
|
||||
|
||||
(defn initialize-history
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue