0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

🎉 Add initial version of viewer.

This commit is contained in:
Andrey Antukh 2020-04-02 17:08:24 +02:00
parent b3e6566bd8
commit 1a3a48e4de
24 changed files with 1109 additions and 92 deletions

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500.00001" width="500" height="500">
<path d="M449.67773 20c-9.59809 3.53351-14.43153 13.472828-22.44921 19.304688-36.68366 35.782049-72.17331 72.786642-109.5879 107.818362-.76612-28.84172-.67096-57.696805-.96874-86.544925h-47.55274V231.42383h170.84375v-47.55664c-28.84878-.29635-57.70268-.19723-86.54492-.97071 41.14084-44.14516 85.49381-85.257142 126.57031-129.404292.3673-8.154531-8.63701-11.847784-12.75195-17.839844-5.42547-5.6539-10.9029-11.427284-17.5586-15.652344zM60.037109 268.57617v47.55664c28.84878.29635 57.702691.19723 86.544921.97071-41.14082 44.14516-85.493811 85.25714-126.570311 129.40429-.3673 8.15453 8.637013 11.84779 12.751953 17.83985 5.42547 5.6539 10.902894 11.42728 17.558594 15.65234 9.59809-3.53351 14.431538-13.47283 22.449218-19.30469 36.683666-35.78205 72.173316-72.78663 109.587896-107.81836.76612 28.84173.67096 57.6968.96874 86.54493h47.55274V268.57617H60.037109z"/>
</svg>

After

Width:  |  Height:  |  Size: 966 B

View file

@ -30,6 +30,8 @@
@import 'main/layouts/projects-page';
@import 'main/layouts/recent-files-page';
@import 'main/layouts/library-page';
@import "main/layouts/not-found";
@import "main/layouts/viewer";
//#################################################
// Commons
@ -69,6 +71,9 @@
@import 'main/partials/debug-icons-preview';
@import 'main/partials/editable-label';
@import 'main/partials/tab-container';
@import "main/partials/viewer-header";
@import "main/partials/viewer-thumbnails";
@import "main/partials/viewer";
//#################################################
// Resources

View file

@ -0,0 +1,43 @@
.not-found-layout {
display: grid;
grid-template-rows: 120px auto;
grid-template-columns: 1fr;
}
.not-found-header {
grid-column: 1 / span 1;
grid-row: 1 / span 1;
display: flex;
align-items: center;
padding: 32px;
svg {
height: 55px;
width: 170px;
}
}
.not-found-content {
grid-column: 1 / span 1;
grid-row: 1 / span 2;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.main-message {
font-size: 18rem;
color: $color-black;
line-height: 226px;
}
.desc-message {
font-size: 3rem;
color: $color-black;
}
}

View file

@ -0,0 +1,25 @@
.viewer-layout {
display: grid;
grid-template-rows: 40px auto;
grid-template-columns: 1fr;
&.fullscreen {
.viewer-header {
display: none;
}
.viewer-content {
grid-row: 1 / span 2;
}
}
.viewer-header {
grid-column: 1 / span 1;
grid-row: 1 / span 1;
}
.viewer-content {
grid-column: 1 / span 1;
grid-row: 2 / span 1;
}
}

View file

@ -0,0 +1,224 @@
.viewer-header {
align-items: center;
background-color: $color-gray-50;
border-bottom: 1px solid $color-gray-60;
display: flex;
height: 40px;
padding: $x-small $medium $x-small 55px;
position: relative;
z-index: 12;
justify-content: space-between;
.main-icon {
align-items: center;
background-color: $color-gray-60;
cursor: pointer;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 40px;
a {
height: 30px;
svg {
fill: $color-gray-30;
height: 30px;
width: 28px;
}
&:hover {
svg {
fill: $color-primary;
}
}
}
}
.sitemap-zone {
align-items: center;
cursor: pointer;
display: flex;
padding: $x-small;
svg {
fill: $color-gray-20;
height: 20px;
margin-right: $small;
width: 20px;
}
span {
color: $color-gray-20;
margin-right: $x-small;
font-size: $fs14;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.frame-name {
color: $color-white;
}
}
.dropdown-button {
svg {
fill: $color-white;
height: 10px;
width: 10px;
}
}
.page-name {
color: $color-white;
}
.counters {
margin-left: $size-3;
}
}
.options-zone {
align-items: center;
display: flex;
width: 250px;
justify-content: space-between;
.btn-primary {
padding: 0.4rem 1rem;
}
.btn-fullscreen {
align-items: center;
background-color: $color-gray-60;
border-radius: $br-small;
cursor: pointer;
display: flex;
height: 25px;
justify-content: center;
width: 25px;
svg {
fill: $color-gray-20;
width: 15px;
height: 15px;
}
&:hover {
background-color: $color-primary;
svg {
fill: $color-gray-60;
}
}
}
}
.zoom-widget {
cursor: pointer;
align-items: center;
display: flex;
position: relative;
.input-container {
display: flex;
}
span {
color: $color-gray-10;
font-size: $fs15;
margin-left: $x-small;
}
.dropdown-button svg {
fill: $color-gray-10;
height: 10px;
width: 10px;
}
.zoom-dropdown {
position: absolute;
right: -25px;
top: 45px;
z-index: 12;
width: 150px;
background-color: $color-white;
border-radius: $br-small;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
li {
color: $color-gray-60;
cursor: pointer;
font-size: $fs12;
display: flex;
padding: $small;
span {
color: $color-gray-40;
font-size: $fs12;
margin-left: auto;
}
&:hover {
background-color: $color-primary-lighter;
}
}
}
.add-zoom,
.remove-zoom {
align-items: center;
background-color: $color-gray-60;
border-radius: $br-small;
cursor: pointer;
color: $color-gray-20;
display: flex;
opacity: 0;
flex-shrink: 0;
font-size: $fs20;
font-weight: bold;
height: 20px;
justify-content: center;
width: 20px;
&:hover {
color: $color-primary;
}
}
&:hover {
.add-zoom,
.remove-zoom {
opacity: 100%;
}
}
}
.users-zone {
align-items: center;
cursor: pointer;
display: flex;
margin: 0;
li {
margin-left: $small;
position: relative;
img {
border: 3px solid #f3dd14;
border-radius: 50%;
flex-shrink: 0;
height: 25px;
width: 25px;
}
}
}
}

View file

@ -0,0 +1,173 @@
.viewer-thumbnails {
grid-row: 1 / span 1;
grid-column: 1 / span 1;
background-color: $color-gray-50;
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 12;
&.expanded {
grid-row: 1 / span 2;
.btn-expand svg {
transform: rotate(180deg);
}
}
.thumbnails-summary {
padding: 0.5rem 1rem;
display: flex;
justify-content: space-between;
.buttons {
display: flex;
justify-content: space-between;
width: 50px;
span {
cursor: pointer;
}
svg {
fill: $color-gray-30;
height: 20px;
width: 20px;
&:hover {
fill: $color-white;
}
}
.btn-close {
transform: rotate(45deg);
}
}
.counter {
color: $color-gray-10;
}
}
.thumbnails-content {
display: grid;
grid-template-columns: 40px auto 40px;
grid-template-rows: auto;
}
.left-scroll-handler {
grid-column: 1 / span 1;
grid-row: 1 / span 1;
background-color: $color-gray-50;
opacity: 0;
display: flex;
z-index: 12;
cursor: pointer;
flex-direction: column;
justify-content: center;
align-items: center;
&:hover {
opacity: 0.5;
}
svg {
transform: rotate(180deg);
width: 30px;
height: 30px;
}
}
.right-scroll-handler {
grid-column: 3 / span 1;
grid-row: 1 / span 1;
background-color: $color-gray-50;
opacity: 0;
display: flex;
z-index: 12;
cursor: pointer;
flex-direction: column;
justify-content: center;
align-items: center;
&:hover {
opacity: 0.5;
}
svg {
width: 30px;
height: 30px;
}
}
.thumbnails-list {
grid-column: 1 / span 3;
grid-row: 1 / span 1;
display: flex;
flex-wrap: nowrap;
overflow: hidden;
.thumbnails-list-inside {
display: flex;
position: relative;
}
}
.thumbnails-list-expanded {
grid-column: 1 / span 3;
grid-row: 1 / span 1;
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
.thumbnail-item {
display: flex;
flex-direction: column;
padding: 1rem;
cursor: pointer;
}
.thumbnail-preview {
background-color: $color-gray-40;
width: 120px;
min-height: 120px;
height: 120px;
border: 1px solid $color-gray-20;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 100%;
height: 100%;
}
&.selected {
border-color: $color-primary;
}
&:hover {
border-color: $color-primary;
border-width: 2px;
}
}
.thumbnail-info {
padding: 0.5rem 0;
span {
font-size: $fs13;
}
}
}

View file

@ -0,0 +1,20 @@
.viewer-content {
background-color: black;
display: grid;
grid-template-rows: 232px auto;
grid-template-columns: 1fr;
}
.viewer-preview {
height: 100vh;
grid-row: 1 / span 2;
grid-column: 1 / span 1;
overflow: scroll;
display: flex;
justify-content: center;
align-items: center;
}

View file

@ -15,6 +15,31 @@
position: relative;
z-index: 12;
.preview {
align-items: center;
background-color: $color-gray-60;
border-radius: $br-small;
cursor: pointer;
display: flex;
height: 25px;
justify-content: center;
width: 25px;
svg {
fill: $color-gray-20;
width: 15px;
height: 15px;
}
&:hover {
background-color: $color-primary;
svg {
fill: $color-gray-60;
}
}
}
.workspace-menu {
position: absolute;
top: 40px;

View file

@ -17,7 +17,6 @@
window.uxboxConfig = JSON.parse({{& config }});
window.uxboxTranslations = JSON.parse({{& translations }});
</script>
<script src="/js/shared.js?ts={{& ts}}"></script>
<script src="/js/main.js?ts={{& ts}}"></script>
<script>uxbox.main.init()</script>
</body>

View file

@ -38,6 +38,7 @@
(def fill (icon-xref :fill))
(def folder (icon-xref :folder))
(def folder-zip (icon-xref :folder-zip))
(def full-screen (icon-xref :full-screen))
(def grid (icon-xref :grid))
(def grid-snap (icon-xref :grid-snap))
(def icon-set (icon-xref :icon-set))

View file

@ -40,7 +40,7 @@
(st/emit! (rt/nav :login))
(nil? match)
(prn "TODO 404 main")
(st/emit! (rt/nav :not-found))
:else
(st/emit! #(assoc % :route match)))))

View file

@ -0,0 +1,131 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.data.viewer
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.main.constants :as c]
[uxbox.main.repo :as rp]
[uxbox.common.spec :as us]
[uxbox.common.pages :as cp]
[uxbox.common.data :as d]
[uxbox.common.exceptions :as ex]
[uxbox.util.uuid :as uuid]))
;; --- 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 (s/keys :req-un [::id ::name ::cp/data]))
(s/def ::bundle
(s/keys :req-un [::project ::file ::page]))
;; --- Initialization
(declare fetch-bundle)
(declare bundle-fetched)
(defn initialize
[page-id]
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc state :viewer-local {:zoom 1}))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (fetch-bundle page-id)))))
;; --- Data Fetching
(defn fetch-bundle
[page-id]
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :viewer-bundle-by-page-id {:page-id page-id})
(rx/map bundle-fetched)))))
(defn- extract-frames
[page]
(let [objects (get-in page [:data :objects])
root (get objects uuid/zero)]
(->> (:shapes root)
(map #(get objects %))
(filter #(= :frame (:type %)))
(vec))))
(defn bundle-fetched
[{:keys [project file page images] :as bundle}]
(us/verify ::bundle bundle)
(ptk/reify ::file-fetched
ptk/UpdateEvent
(update [_ state]
(let [frames (extract-frames page)
objects (get-in page [:data :objects])]
(assoc state :viewer-data {:project project
:objects objects
:file file
:page page
:images images
:frames frames})))))
;; --- Zoom Management
(def increase-zoom
(ptk/reify ::increase-zoom
ptk/UpdateEvent
(update [_ state]
(let [increase #(nth c/zoom-levels
(+ (d/index-of c/zoom-levels %) 1)
(last c/zoom-levels))]
(update-in state [:viewer-local :zoom] (fnil increase 1))))))
(def decrease-zoom
(ptk/reify ::decrease-zoom
ptk/UpdateEvent
(update [_ state]
(let [decrease #(nth c/zoom-levels
(- (d/index-of c/zoom-levels %) 1)
(first c/zoom-levels))]
(update-in state [:viewer-local :zoom] (fnil decrease 1))))))
(def reset-zoom
(ptk/reify ::reset-zoom
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :zoom] 1))))
(def zoom-to-50
(ptk/reify ::zoom-to-50
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :zoom] 0.5))))
(def zoom-to-200
(ptk/reify ::zoom-to-200
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :zoom] 2))))
;; --- Local State Management
(def toggle-thumbnails-panel
(ptk/reify ::toggle-thumbnails-panel
ptk/UpdateEvent
(update [_ state]
(update-in state [:viewer-local :show-thumbnails] not))))

View file

@ -1955,7 +1955,7 @@
(watch [_ state stream]
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-page :file-id])
path-params {:project-id project-id :file-id file-id}
path-params {:file-id file-id :project-id project-id}
query-params {:page-id page-id}]
(rx/of (rt/nav :workspace path-params query-params))))))
@ -2199,7 +2199,7 @@
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
parent (get-parent (first selected) (vals objects))
parent-id (:id parent)
parent-id (:id parent)
selected-objects (map (partial get objects) selected)
selection-rect (geom/selection-rect selected-objects)
frame-id (-> selected-objects first :frame-id)
@ -2254,7 +2254,7 @@
:obj group}
{:type :mod-obj
:id parent-id
:operations [{:type :set :attr :shapes :val (:shapes parent)}]}]]
:operations [{:type :set :attr :shapes :val (:shapes parent)}]}]]
(rx/of (commit-changes rchanges uchanges {:commit-local? true}))))
rx/empty)))))

View file

@ -12,6 +12,8 @@
[uxbox.util.uuid :as uuid]
[uxbox.util.math :as mth]
[uxbox.main.geom :as geom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.geom.matrix :as gmt]
[uxbox.main.ui.shapes.frame :as frame]
[uxbox.main.ui.shapes.circle :as circle]
[uxbox.main.ui.shapes.icon :as icon]
@ -51,6 +53,9 @@
(let [children (mapv #(get objects %) (:shapes shape))]
[:& group-shape {:shape shape :children children}]))
(declare group-shape)
(declare frame-shape)
(mf/defc shape-wrapper
[{:keys [shape objects] :as props}]
(when (and shape (not (:hidden shape)))
@ -63,7 +68,7 @@
:path [:& path/path-shape {:shape shape}]
:image [:& image/image-shape {:shape shape}]
:circle [:& circle/circle-shape {:shape shape}]
:group [:& (group/group-shape shape-wrapper) {:shape shape :shape-wrapper shape-wrapper :objects objects}]
:group [:& group-shape {:shape shape :objects objects}]
nil)))
(def group-shape (group/group-shape shape-wrapper))
@ -90,15 +95,3 @@
:key (:id item)
:objects objects}]))]))
;; (defn- render-html
;; [component]
;; (.renderToStaticMarkup js/ReactDOMServer component))
;; (defn render
;; [{:keys [data] :as page}]
;; (try
;; (-> (mf/element page-svg #js {:data data})
;; (render-html))
;; (catch :default e
;; (js/console.log e)
;; nil)))

View file

@ -38,6 +38,10 @@
(-> (l/key :workspace-file)
(l/derive st/state)))
(def workspace-project
(-> (l/key :workspace-project)
(l/derive st/state)))
(def workspace-images
(-> (l/key :workspace-images)
(l/derive st/state)))

View file

@ -11,6 +11,8 @@
[uxbox.util.uuid :as uuid]
[uxbox.util.storage :refer [storage]]))
;; TODO: move outside uxbox.main
(enable-console-print!)
(def ^:dynamic *on-error* identity)
@ -47,6 +49,7 @@
(l/derive state)))
(defn emit!
([] nil)
([event]
(ptk/emit! store event)
nil)

View file

@ -16,6 +16,7 @@
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.common.exceptions :as ex]
[uxbox.common.data :as d]
[uxbox.main.data.auth :refer [logout]]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
@ -25,12 +26,13 @@
[uxbox.main.ui.profile.recovery :refer [profile-recovery-page]]
[uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]]
[uxbox.main.ui.profile.register :refer [profile-register-page]]
[uxbox.main.ui.viewer :refer [viewer-page]]
[uxbox.main.ui.settings :as settings]
[uxbox.main.ui.not-found :refer [not-found-page]]
[uxbox.main.ui.shapes]
[uxbox.main.ui.workspace :as workspace]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.messages :as uum]
[uxbox.util.router :as rt]
[uxbox.util.timers :as ts]))
(def route-iref
@ -49,6 +51,9 @@
["/profile" :settings-profile]
["/password" :settings-password]]
["/view/:page-id/:index" :viewer]
["/not-found" :not-found]
(when *assert*
["/debug/icons-preview" :debug-icons-preview])
@ -78,54 +83,63 @@
[{:keys [error] :as props}]
(let [data (ex-data error)]
(case (:type data)
:not-found [:span "404"]
:not-found [:& not-found-page {:error data}]
[:span "Internal application errror"])))
(mf/defc app
{:wrap [#(wrap-catch % {:fallback app-error})]}
[props]
(let [route (mf/deref route-iref)]
(case (get-in route [:data :name])
:login
(mf/element login-page)
(when route
(case (get-in route [:data :name])
:login
(mf/element login-page)
:profile-register
(mf/element profile-register-page)
:profile-register
(mf/element profile-register-page)
:profile-recovery-request
(mf/element profile-recovery-request-page)
:profile-recovery-request
(mf/element profile-recovery-request-page)
:profile-recovery
(mf/element profile-recovery-page)
:profile-recovery
(mf/element profile-recovery-page)
(:settings-profile
:settings-password)
(mf/element settings/settings #js {:route route})
:viewer
(let [index (d/parse-integer (get-in route [:params :path :index]))
page-id (uuid (get-in route [:params :path :page-id]))]
[:& viewer-page {:page-id page-id
:index index}])
:debug-icons-preview
(when *assert*
(mf/element i/debug-icons-preview))
(:settings-profile
:settings-password)
(mf/element settings/settings #js {:route route})
(:dashboard-search
:dashboard-team
:dashboard-project
:dashboard-library-icons
:dashboard-library-icons-index
:dashboard-library-images
:dashboard-library-images-index
:dashboard-library-palettes
:dashboard-library-palettes-index)
(mf/element dashboard #js {:route route})
:debug-icons-preview
(when *assert*
(mf/element i/debug-icons-preview))
:workspace
(let [project-id (uuid (get-in route [:params :path :project-id]))
file-id (uuid (get-in route [:params :path :file-id]))
page-id (uuid (get-in route [:params :query :page-id]))]
[:& workspace/workspace {:project-id project-id
:file-id file-id
:page-id page-id
:key file-id}])
nil)))
(:dashboard-search
:dashboard-team
:dashboard-project
:dashboard-library-icons
:dashboard-library-icons-index
:dashboard-library-images
:dashboard-library-images-index
:dashboard-library-palettes
:dashboard-library-palettes-index)
(mf/element dashboard #js {:route route})
:workspace
(let [project-id (uuid (get-in route [:params :path :project-id]))
file-id (uuid (get-in route [:params :path :file-id]))
page-id (uuid (get-in route [:params :query :page-id]))]
[:& workspace/workspace {:project-id project-id
:file-id file-id
:page-id page-id
:key file-id}])
:not-found
[:& not-found-page {}]))))
;; --- Error Handling

View file

@ -0,0 +1,24 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.not-found
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]))
(mf/defc not-found-page
[{:keys [error] :as props}]
(js/console.log "not-found" error)
[:section.not-found-layout
[:div.not-found-header i/logo]
[:div.not-found-content
[:div.message-container
[:div.main-message "404"]
[:div.desc-message "Oops! Page not found"]]]])

View file

@ -11,7 +11,9 @@
"A collection of general purpose react hooks."
(:require
[beicon.core :as rx]
[rumext.alpha :as mf]))
[goog.events :as events]
[rumext.alpha :as mf])
(:import goog.events.EventType))
(defn use-rxsub
[ob]
@ -22,4 +24,3 @@
#(rx/cancel! sub)))
#js [ob])
state))

View file

@ -0,0 +1,96 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.viewer
(:require
[beicon.core :as rx]
[goog.events :as events]
[goog.object :as gobj]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.store :as st]
[uxbox.common.exceptions :as ex]
[uxbox.main.ui.keyboard :as kbd]
[uxbox.main.ui.components.dropdown :refer [dropdown]]
[uxbox.main.data.viewer :as vd]
[uxbox.main.ui.viewer.header :refer [header]]
[uxbox.main.ui.viewer.thumbnails :refer [thumbnails-panel frame-svg]]
[uxbox.util.dom :as dom]
[uxbox.util.data :refer [classnames]]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.math :as mth]
[uxbox.util.router :as rt])
(:import goog.events.EventType
goog.events.KeyCodes))
(mf/defc main-panel
[{:keys [data zoom index]}]
(let [frames (:frames data [])
objects (:objects data)
frame (get frames index)]
(when-not frame
(ex/raise :type :not-found
:hint "Frame not found"))
[:section.viewer-preview
[:& frame-svg {:frame frame :zoom zoom :objects objects}]]))
(mf/defc viewer-content
[{:keys [data local index] :as props}]
(let [on-mouse-wheel
(fn [event]
(when (kbd/ctrl? event)
;; Disable browser zoom with ctrl+mouse wheel
(dom/prevent-default event)))
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)
[:div.viewer-layout
[:& header {:data data
:local local
:index index}]
[:div.viewer-content
(when (:show-thumbnails local)
[:& thumbnails-panel {:index index
:data data}])
[:& main-panel {:data data
:zoom (:zoom local)
:index index}]]]))
;; --- Component: Viewer Page
(def viewer-data-ref
(-> (l/key :viewer-data)
(l/derive st/state)))
(def viewer-local-ref
(-> (l/key :viewer-local)
(l/derive st/state)))
(mf/defc viewer-page
[{:keys [page-id index] :as props}]
(mf/use-effect (mf/deps page-id) #(st/emit! (vd/initialize page-id)))
(let [data (mf/deref viewer-data-ref)
local (mf/deref viewer-local-ref)]
(when data
[:& viewer-content {:index index
:local local
:data data}])))

View file

@ -0,0 +1,85 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.viewer.header
(:require
[beicon.core :as rx]
[goog.events :as events]
[goog.object :as gobj]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.store :as st]
[uxbox.main.ui.components.dropdown :refer [dropdown]]
[uxbox.main.data.viewer :as dv]
[uxbox.util.data :refer [classnames]]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.math :as mth]
[uxbox.util.router :as rt])
(:import goog.events.EventType
goog.events.KeyCodes))
(mf/defc zoom-widget
{:wrap [mf/memo]}
[{:keys [zoom] :as props}]
(let [show-dropdown? (mf/use-state false)
increase #(st/emit! dv/increase-zoom)
decrease #(st/emit! dv/decrease-zoom)
zoom-to-50 #(st/emit! dv/zoom-to-50)
zoom-to-100 #(st/emit! dv/reset-zoom)
zoom-to-200 #(st/emit! dv/zoom-to-200)]
[:div.zoom-widget
[:span.add-zoom {:on-click decrease} "-"]
[:div.input-container {: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.zoom-dropdown
[:li {:on-click increase}
"Zoom in" [:span "+"]]
[:li {:on-click decrease}
"Zoom out" [:span "-"]]
[:li {:on-click zoom-to-50}
"Zoom to 50%"]
[:li {:on-click zoom-to-100}
"Zoom to 100%" [:span "Shift + 0"]]
[:li {:on-click zoom-to-200}
"Zoom to 200%"]]]]
[:span.remove-zoom {:on-click increase} "+"]]))
(mf/defc header
[{:keys [data index local] :as props}]
(let [{:keys [project file page frames]} data
total (count frames)
on-click #(st/emit! dv/toggle-thumbnails-panel)
on-edit #(st/emit! (rt/nav :workspace
{:project-id (get-in data [:project :id])
:file-id (get-in data [:file :id])}
{:page-id (get-in data [:page :id])}))]
[:header.viewer-header
[:div.main-icon
[:a i/logo-icon]]
[:div.sitemap-zone {:alt (tr "header.sitemap")
:on-click on-click}
[:span.project-name (:name project)]
[:span "/"]
[:span.file-name (:name file)]
[:span "/"]
[:span.page-name (:name page)]
[:span.dropdown-button i/arrow-down]
[:span.counters (str (inc index) " / " total)]]
[:div.options-zone
[:span.btn-primary {:on-click on-edit} "Edit page"]
[:& zoom-widget {:zoom (:zoom local)}]
[:span.btn-fullscreen.tooltip.tooltip-bottom {:alt "Full screen"} i/full-screen]]]))

View file

@ -0,0 +1,146 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.viewer.thumbnails
(:require
[goog.events :as events]
[goog.object :as gobj]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.common.data :as d]
[uxbox.main.store :as st]
[uxbox.main.data.viewer :as dv]
[uxbox.main.ui.components.dropdown :refer [dropdown']]
[uxbox.main.ui.shapes.frame :as frame]
[uxbox.main.exports :as exports]
[uxbox.util.data :refer [classnames]]
[uxbox.util.dom :as dom]
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.math :as mth]
[uxbox.util.router :as rt]
[uxbox.main.data.viewer :as vd])
(:import goog.events.EventType
goog.events.KeyCodes))
(mf/defc thumbnails-content
[{:keys [children expanded? total] :as props}]
(let [container (mf/use-ref)
width (mf/use-var (.. js/document -documentElement -clientWidth))
element-width (mf/use-var 152)
offset (mf/use-state 0)
on-left-arrow-click
(fn [event]
(swap! offset (fn [v]
(if (pos? v)
(dec v)
v))))
on-right-arrow-click
(fn [event]
(let [visible (/ @width @element-width)
max-val (- total visible)]
(swap! offset (fn [v]
(if (< v max-val)
(inc v)
v)))))
on-scroll
(fn [event]
(if (pos? (.. event -nativeEvent -deltaY))
(on-right-arrow-click event)
(on-left-arrow-click event)))
on-mount
(fn []
(let [dom (mf/ref-val container)]
(reset! width (gobj/get dom "clientWidth"))))]
(mf/use-effect on-mount)
(if expanded?
[:div.thumbnails-content
[:div.thumbnails-list-expanded children]]
[:div.thumbnails-content
[:div.left-scroll-handler {:on-click on-left-arrow-click} i/arrow-slide]
[:div.right-scroll-handler {:on-click on-right-arrow-click} i/arrow-slide]
[:div.thumbnails-list {:ref container :on-wheel on-scroll}
[:div.thumbnails-list-inside {:style {:right (str (* @offset 152) "px")}}
children]]])))
(mf/defc frame-svg
{::mf/wrap [mf/wrap-memo]}
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
(let [childs (mapv #(get objects %) (:shapes frame))
modifier (-> (gpt/point (:x frame) (:y frame))
(gpt/negate)
(gmt/translate-matrix))
frame (assoc frame :displacement-modifier modifier)
transform (str "scale(" zoom ")")]
[:svg {:view-box (str "0 0 " (:width frame 0) " " (:height frame 0))
:width (:width frame)
:height (:height frame)
:transform transform
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& exports/frame-shape {:shape frame :childs childs}]]))
(mf/defc thumbnails-summary
[{:keys [on-toggle-expand on-close total] :as props}]
[:div.thumbnails-summary
[:span.counter (str total " frames")]
[:span.buttons
[:span.btn-expand {:on-click on-toggle-expand} i/arrow-down]
[:span.btn-close {:on-click on-close} i/close]]])
(mf/defc thumbnail-item
[{:keys [selected? frame on-click index objects] :as props}]
[:div.thumbnail-item {:on-click #(on-click % index)}
[:div.thumbnail-preview
{:class (classnames :selected selected?)}
[:& frame-svg {:frame frame :objects objects}]]
[:div.thumbnail-info
[:span.name (:name frame)]]])
(mf/defc thumbnails-panel
[{:keys [data index] :as props}]
(let [expanded? (mf/use-state false)
container (mf/use-ref)
page-id (get-in data [:page :id])
on-close #(st/emit! dv/toggle-thumbnails-panel)
on-item-click
(fn [event index]
(st/emit! (rt/nav :viewer {:page-id page-id
:index index})))]
[:& dropdown' {:on-close on-close
:container container
:show true}
[:section.viewer-thumbnails {:class (classnames :expanded @expanded?)
:ref container}
[:& 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)}])]]]))

View file

@ -121,11 +121,13 @@
(let [file (mf/deref refs/workspace-file)
page (mf/deref refs/workspace-page)
project (mf/deref refs/workspace-project)
layout (mf/deref refs/workspace-layout)]
[:> rdnd/provider {:backend rdnd/html5}
[:& messages-widget]
[:& header {:page page
:file file
:project project
:layout layout}]
(when page

View file

@ -22,6 +22,7 @@
[uxbox.main.ui.workspace.images :refer [import-image-modal]]
[uxbox.main.ui.components.dropdown :refer [dropdown]]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.data :refer [classnames]]
[uxbox.util.math :as mth]
[uxbox.util.router :as rt]))
@ -40,21 +41,21 @@
[:div.zoom-input
[:span.add-zoom {:on-click decrease} "-"]
[:div {: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.zoom-dropdown
[:li {:on-click increase}
"Zoom in" [:span "+"]]
[:li {:on-click decrease}
"Zoom out" [:span "-"]]
[:li {:on-click zoom-to-50}
"Zoom to 50%" [:span "Shift + 0"]]
[:li {:on-click zoom-to-100}
"Zoom to 100%" [:span "Shift + 1"]]
[:li {:on-click zoom-to-200}
"Zoom to 200%" [:span "Shift + 2"]]]]]
[:span {} (str (mth/round (* 100 zoom)) "%")]
[:span.dropdown-button i/arrow-down]
[:& dropdown {:show @show-dropdown?
:on-close #(reset! show-dropdown? false)}
[:ul.zoom-dropdown
[:li {:on-click increase}
"Zoom in" [:span "+"]]
[:li {:on-click decrease}
"Zoom out" [:span "-"]]
[:li {:on-click zoom-to-50}
"Zoom to 50%" [:span "Shift + 0"]]
[:li {:on-click zoom-to-100}
"Zoom to 100%" [:span "Shift + 1"]]
[:li {:on-click zoom-to-200}
"Zoom to 200%" [:span "Shift + 2"]]]]]
[:span.remove-zoom {:on-click increase} "+"]]))
;; --- Header Users
@ -132,35 +133,34 @@
;; --- Header Component
(def router-ref
(-> (l/key :router)
(l/derive st/state)))
(mf/defc header
[{:keys [page file layout] :as props}]
(let [toggle-layout #(st/emit! (dw/toggle-layout-flag %))
on-undo (constantly nil)
on-redo (constantly nil)
[{:keys [page file layout project] :as props}]
(let [go-to-dashboard #(st/emit! (rt/nav :dashboard-team {:team-id "self"}))
toggle-sitemap #(st/emit! (dw/toggle-layout-flag :sitemap))
locale (i18n/use-locale)
on-image #(modal/show! import-image-modal {})
;;on-download #(udl/open! :download)
selected-drawtool (mf/deref refs/selected-drawing-tool)
select-drawtool #(st/emit! :interrupt
#_(dw/deactivate-ruler)
(dw/select-for-drawing %))]
router (mf/deref router-ref)
view-url (rt/resolve router :viewer {:page-id (:id page) :index 0})]
[:header.workspace-bar
[:div.main-icon
[:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id "self"}))}
i/logo-icon]]
[:a {:on-click go-to-dashboard} i/logo-icon]]
[:& menu {:layout layout}]
[:div.project-tree-btn
{:alt (tr "header.sitemap")
:class (when (contains? layout :sitemap) "selected")
:on-click #(st/emit! (dw/toggle-layout-flag :sitemap))}
[:span.project-name "Project name /"]
[:div.project-tree-btn {:alt (tr "header.sitemap")
:class (classnames :selected (contains? layout :sitemap))
:on-click toggle-sitemap}
[:span.project-name (:name project) " /"]
[:span (:name file)]]
[:div.workspace-options
[:& active-users]]
[:& zoom-widget]]))
[:& zoom-widget]
[:a.preview {
;; :target "__blank"
:href (str "#" view-url)} i/play]]))