mirror of
https://github.com/penpot/penpot.git
synced 2025-05-04 23:35:54 +02:00
452 lines
16 KiB
Clojure
452 lines
16 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/.
|
|
;;
|
|
;; Copyright (c) UXBOX Labs SL
|
|
|
|
(ns app.main.ui.viewer
|
|
(:require
|
|
[app.common.colors :as clr]
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.pages.helpers :as cph]
|
|
[app.common.text :as txt]
|
|
[app.main.data.comments :as dcm]
|
|
[app.main.data.viewer :as dv]
|
|
[app.main.data.viewer.shortcuts :as sc]
|
|
[app.main.fonts :as fonts]
|
|
[app.main.refs :as refs]
|
|
[app.main.store :as st]
|
|
[app.main.ui.context :as ctx]
|
|
[app.main.ui.hooks :as hooks]
|
|
[app.main.ui.icons :as i]
|
|
[app.main.ui.shapes.filters :as filters]
|
|
[app.main.ui.share-link]
|
|
[app.main.ui.static :as static]
|
|
[app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]]
|
|
[app.main.ui.viewer.handoff :as handoff]
|
|
[app.main.ui.viewer.header :refer [header]]
|
|
[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 [tr]]
|
|
[app.util.webapi :as wapi]
|
|
[cuerdas.core :as str]
|
|
[goog.events :as events]
|
|
[rumext.alpha :as mf]))
|
|
|
|
(defn- calculate-size
|
|
[frame zoom bounds]
|
|
(let [frame-bounds (filters/get-filters-bounds frame)
|
|
{:keys [x y width height]} (if (:show-content frame)
|
|
(gsh/join-rects [bounds frame-bounds])
|
|
frame-bounds)
|
|
padding (filters/calculate-padding frame)
|
|
x (- x (:horizontal padding))
|
|
y (- y (:vertical padding))
|
|
width (+ width (* 2 (:horizontal padding)))
|
|
height (+ height (* 2 (:vertical padding)))]
|
|
{:base-width width
|
|
:base-height height
|
|
:x x
|
|
:y y
|
|
:width (* width zoom)
|
|
:height (* height zoom)
|
|
:vbox (dm/fmt "% % % %" 0 0 width height)}))
|
|
|
|
(defn- calculate-wrapper
|
|
[size1 size2 zoom]
|
|
(cond
|
|
(nil? size1) size2
|
|
(nil? size2) size1
|
|
:else (let [width (max (:base-width size1) (:base-width size2))
|
|
height (max (:base-height size1) (:base-height size2))]
|
|
{:width (* width zoom)
|
|
:height (* height zoom)
|
|
:vbox (str "0 0 " width " " height)})))
|
|
|
|
|
|
(mf/defc viewer-pagination
|
|
[{:keys [index num-frames left-bar right-bar] :as props}]
|
|
[:*
|
|
(when (pos? index)
|
|
[:div.viewer-go-prev {:class (when left-bar "left-bar")}
|
|
[:div.arrow {:on-click #(st/emit! dv/select-prev-frame)} i/go-prev]])
|
|
(when (< (+ index 1) num-frames)
|
|
[:div.viewer-go-next {:class (when right-bar "right-bar")}
|
|
[:div.arrow {:on-click #(st/emit! dv/select-next-frame)} i/go-next]])
|
|
[:div.viewer-bottom {:class (when left-bar "left-bar")}
|
|
[:div.reset {:on-click #(st/emit! dv/select-first-frame)} i/reset]
|
|
[:div.counter (str/join " / " [(+ index 1) num-frames])]
|
|
[:span]]])
|
|
|
|
|
|
(mf/defc viewer-wrapper
|
|
[{:keys [wrapper-size scroll orig-frame orig-viewport-ref orig-size page file users current-viewport-ref
|
|
size frame interactions-mode overlays zoom close-overlay section index children-bounds] :as props}]
|
|
(let [{clist :list} (mf/deref refs/comments-local)
|
|
show-comments-list (and (= section :comments) (= :show clist))]
|
|
[:*
|
|
[:& viewer-pagination {:index index :num-frames (count (:frames page)) :right-bar show-comments-list}]
|
|
|
|
(when show-comments-list
|
|
[:& comments-sidebar {:users users :frame frame :page page}])
|
|
|
|
[:div.viewer-wrapper
|
|
{:style {:width (:width wrapper-size)
|
|
:height (:height wrapper-size)}}
|
|
[:& (mf/provider ctx/scroll-ctx) {:value @scroll}
|
|
[:div.viewer-clipper
|
|
[:*
|
|
(when orig-frame
|
|
[:div.viewport-container
|
|
{:ref orig-viewport-ref
|
|
:style {:width (:width orig-size)
|
|
:height (:height orig-size)
|
|
:position "relative"}}
|
|
|
|
[:& interactions/viewport
|
|
{:frame orig-frame
|
|
:base-frame orig-frame
|
|
:frame-offset (gpt/point 0 0)
|
|
:size orig-size
|
|
:page page
|
|
:file file
|
|
:users users
|
|
:interactions-mode :hide}]])
|
|
|
|
[:div.viewport-container
|
|
{:ref current-viewport-ref
|
|
:style {:width (:width size)
|
|
:height (:height size)
|
|
:position "relative"}}
|
|
|
|
[:& interactions/viewport
|
|
{:frame frame
|
|
:base-frame frame
|
|
:frame-offset (gpt/point 0 0)
|
|
:size size
|
|
:page page
|
|
:file file
|
|
:users users
|
|
:interactions-mode interactions-mode}]
|
|
|
|
(for [overlay overlays]
|
|
(let [size-over (calculate-size (:frame overlay) zoom children-bounds)]
|
|
[:*
|
|
(when (or (:close-click-outside overlay)
|
|
(:background-overlay overlay))
|
|
[:div.viewer-overlay-background
|
|
{:class (dom/classnames
|
|
:visible (:background-overlay overlay))
|
|
:style {:width (:width wrapper-size)
|
|
:height (:height wrapper-size)
|
|
:position "absolute"
|
|
:left 0
|
|
:top 0}
|
|
:on-click #(when (:close-click-outside overlay)
|
|
(close-overlay (:frame overlay)))}])
|
|
[:div.viewport-container.viewer-overlay
|
|
|
|
{:id (str "overlay-" (-> overlay :frame :id))
|
|
:style {:width (:width size-over)
|
|
:height (:height size-over)
|
|
:left (* (:x (:position overlay)) zoom)
|
|
:top (* (:y (:position overlay)) zoom)}}
|
|
[:& interactions/viewport
|
|
{:frame (:frame overlay)
|
|
:base-frame frame
|
|
:frame-offset (:position overlay)
|
|
:size size-over
|
|
:page page
|
|
:file file
|
|
:users users
|
|
:interactions-mode interactions-mode}]]]))]]
|
|
|
|
(when (= section :comments)
|
|
[:& comments-layer {:file file
|
|
:users users
|
|
:frame frame
|
|
:page page
|
|
:zoom zoom}])]]]]))
|
|
|
|
(mf/defc viewer
|
|
[{:keys [params data]}]
|
|
|
|
(let [{:keys [page-id section index]} params
|
|
{:keys [file users project permissions]} data
|
|
|
|
local (mf/deref refs/viewer-local)
|
|
|
|
nav-scroll (:nav-scroll local)
|
|
orig-viewport-ref (mf/use-ref nil)
|
|
current-viewport-ref (mf/use-ref nil)
|
|
viewer-section-ref (mf/use-ref nil)
|
|
current-animation (:current-animation local)
|
|
|
|
page-id (or page-id (-> file :data :pages first))
|
|
|
|
page (mf/use-memo
|
|
(mf/deps data page-id)
|
|
(fn []
|
|
(get-in data [:pages page-id])))
|
|
|
|
text-shapes
|
|
(hooks/use-equal-memo
|
|
(->> (:objects page)
|
|
(vals)
|
|
(filter cph/text-shape?)))
|
|
|
|
zoom (:zoom local)
|
|
frames (:frames page)
|
|
frame (get frames index)
|
|
|
|
children-bounds
|
|
(mf/use-memo
|
|
(mf/deps page (:id frame))
|
|
#(-> (cph/get-children (:objects page) (:id frame))
|
|
(gsh/selection-rect)))
|
|
|
|
fullscreen? (mf/deref refs/viewer-fullscreen?)
|
|
overlays (:overlays local)
|
|
scroll (mf/use-state nil)
|
|
|
|
orig-frame
|
|
(when (:orig-frame-id current-animation)
|
|
(d/seek #(= (:id %) (:orig-frame-id current-animation)) frames))
|
|
|
|
size (mf/use-memo
|
|
(mf/deps frame zoom children-bounds)
|
|
(fn [] (calculate-size frame zoom children-bounds)))
|
|
|
|
orig-size (mf/use-memo
|
|
(mf/deps orig-frame zoom)
|
|
(fn [] (when orig-frame
|
|
(calculate-size orig-frame zoom children-bounds))))
|
|
|
|
wrapper-size (mf/use-memo
|
|
(mf/deps size orig-size zoom)
|
|
(fn [] (calculate-wrapper size orig-size zoom)))
|
|
|
|
interactions-mode
|
|
(:interactions-mode local)
|
|
|
|
on-click
|
|
(mf/use-callback
|
|
(mf/deps section)
|
|
(fn [_]
|
|
(when (= section :comments)
|
|
(st/emit! (dcm/close-thread)))))
|
|
|
|
set-up-new-size
|
|
(mf/use-callback
|
|
(fn [_]
|
|
(let [viewer-section (dom/get-element "viewer-section")
|
|
size (dom/get-client-size viewer-section)]
|
|
(st/emit! (dv/set-viewport-size {:size size})))))
|
|
|
|
on-scroll
|
|
(fn [event]
|
|
(reset! scroll (dom/get-target-scroll event)))]
|
|
|
|
(hooks/use-shortcuts ::viewer sc/shortcuts)
|
|
|
|
(when (nil? page)
|
|
(ex/raise :type :not-found))
|
|
|
|
;; 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))))))
|
|
|
|
(mf/use-effect
|
|
(fn []
|
|
(dom/set-html-theme-color clr/gray-50 "dark")
|
|
(let [key1 (events/listen js/window "click" on-click)
|
|
key2 (events/listen (mf/ref-val viewer-section-ref) "scroll" on-scroll)]
|
|
(fn []
|
|
(events/unlistenByKey key1)
|
|
(events/unlistenByKey key2)))))
|
|
|
|
(mf/use-layout-effect
|
|
(fn []
|
|
(set-up-new-size)
|
|
(.addEventListener js/window "resize" set-up-new-size)
|
|
(fn []
|
|
(.removeEventListener js/window "resize" set-up-new-size))))
|
|
|
|
(mf/use-layout-effect
|
|
(mf/deps nav-scroll)
|
|
(fn []
|
|
;; Set scroll position after navigate
|
|
(when (number? nav-scroll)
|
|
(let [viewer-section (dom/get-element "viewer-section")]
|
|
(st/emit! (dv/reset-nav-scroll))
|
|
(dom/set-scroll-pos! viewer-section nav-scroll)))))
|
|
|
|
(mf/use-layout-effect
|
|
(mf/deps fullscreen?)
|
|
(fn []
|
|
;; Trigger dom fullscreen depending on our state
|
|
(let [wrapper (dom/get-element "viewer-layout")
|
|
fullscreen-dom? (dom/fullscreen?)]
|
|
(when (not= fullscreen? fullscreen-dom?)
|
|
(if fullscreen?
|
|
(wapi/request-fullscreen wrapper)
|
|
(wapi/exit-fullscreen))))))
|
|
|
|
(mf/use-layout-effect
|
|
(mf/deps index)
|
|
(fn []
|
|
;; Navigate animation needs to be started after navigation
|
|
;; is complete, and we have the next page index.
|
|
(when (and current-animation
|
|
(= (:kind current-animation) :go-to-frame))
|
|
(let [orig-viewport (mf/ref-val orig-viewport-ref)
|
|
current-viewport (mf/ref-val current-viewport-ref)]
|
|
(interactions/animate-go-to-frame
|
|
(:animation current-animation)
|
|
current-viewport
|
|
orig-viewport
|
|
size
|
|
orig-size
|
|
wrapper-size)))))
|
|
|
|
(mf/use-layout-effect
|
|
(mf/deps current-animation children-bounds)
|
|
(fn []
|
|
;; Overlay animations may be started when needed.
|
|
(when current-animation
|
|
(case (:kind current-animation)
|
|
|
|
:open-overlay
|
|
(let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation))))
|
|
overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation))
|
|
overlays)
|
|
overlay-size (calculate-size (:frame overlay) zoom children-bounds)
|
|
overlay-position {:x (* (:x (:position overlay)) zoom)
|
|
:y (* (:y (:position overlay)) zoom)}]
|
|
(interactions/animate-open-overlay
|
|
(:animation current-animation)
|
|
overlay-viewport
|
|
wrapper-size
|
|
overlay-size
|
|
overlay-position))
|
|
|
|
:close-overlay
|
|
(let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation))))
|
|
overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation))
|
|
overlays)
|
|
overlay-size (calculate-size (:frame overlay) zoom children-bounds)
|
|
overlay-position {:x (* (:x (:position overlay)) zoom)
|
|
:y (* (:y (:position overlay)) zoom)}]
|
|
(interactions/animate-close-overlay
|
|
(:animation current-animation)
|
|
overlay-viewport
|
|
wrapper-size
|
|
overlay-size
|
|
overlay-position
|
|
(:id (:frame overlay))))
|
|
|
|
nil))))
|
|
|
|
(mf/use-effect
|
|
(mf/deps text-shapes)
|
|
(fn []
|
|
(let [text-nodes (->> text-shapes (mapcat #(txt/node-seq txt/is-text-node? (:content %))))
|
|
fonts (into #{} (keep :font-id) text-nodes)]
|
|
(run! fonts/ensure-loaded! fonts))))
|
|
|
|
[:div#viewer-layout {:class (dom/classnames
|
|
:force-visible (:show-thumbnails local)
|
|
:viewer-layout (not= section :handoff)
|
|
:handoff-layout (= section :handoff)
|
|
:fullscreen fullscreen?)}
|
|
|
|
[:& header {:project project
|
|
:index index
|
|
:file file
|
|
:page page
|
|
:frame frame
|
|
:permissions permissions
|
|
:zoom zoom
|
|
:section section}]
|
|
|
|
[:div.viewer-content
|
|
[:div.thumbnail-close {:on-click #(st/emit! dv/close-thumbnails-panel)
|
|
:class (dom/classnames :invisible (not (:show-thumbnails local false)))}]
|
|
[:& thumbnails-panel {:frames frames
|
|
:show? (:show-thumbnails local false)
|
|
:page page
|
|
:index index}]
|
|
[:section.viewer-section {:id "viewer-section"
|
|
:ref viewer-section-ref
|
|
:class (if fullscreen? "fullscreen" "")}
|
|
(cond
|
|
(empty? frames)
|
|
[:section.empty-state
|
|
[:span (tr "viewer.empty-state")]]
|
|
|
|
(nil? frame)
|
|
[:section.empty-state
|
|
(when (some? index)
|
|
[:span (tr "viewer.frame-not-found")])]
|
|
|
|
(some? frame)
|
|
(if (= :handoff section)
|
|
[:& handoff/viewport
|
|
{:frame frame
|
|
:page page
|
|
:file file
|
|
:section section
|
|
:local local
|
|
:size size
|
|
:index index
|
|
:viewer-pagination viewer-pagination}]
|
|
|
|
|
|
[:& viewer-wrapper
|
|
{:wrapper-size wrapper-size
|
|
:scroll scroll
|
|
:orig-frame orig-frame
|
|
:orig-viewport-ref orig-viewport-ref
|
|
:orig-size orig-size
|
|
:page page
|
|
:file file
|
|
:users users
|
|
:current-viewport-ref current-viewport-ref
|
|
:size size
|
|
:frame frame
|
|
:interactions-mode interactions-mode
|
|
:overlays overlays
|
|
:zoom zoom
|
|
:section section
|
|
:index index
|
|
:children-bounds children-bounds}]))]]]))
|
|
|
|
;; --- Component: Viewer Page
|
|
|
|
(mf/defc viewer-page
|
|
[{:keys [file-id] :as props}]
|
|
(mf/use-effect
|
|
(mf/deps file-id)
|
|
(fn []
|
|
(st/emit! (dv/initialize props))
|
|
(fn []
|
|
(st/emit! (dv/finalize props)))))
|
|
|
|
(when-let [data (mf/deref refs/viewer-data)]
|
|
(let [key (str (get-in data [:file :id]))]
|
|
[:& viewer {:params props :data data :key key}])))
|
|
|
|
(mf/defc breaking-change-notice
|
|
[]
|
|
[:> static/static-header {}
|
|
[:div.image i/unchain]
|
|
[:div.main-message (tr "viewer.breaking-change.message")]
|
|
[:div.desc-message (tr "viewer.breaking-change.description")]])
|