diff --git a/frontend/resources/images/icons/full-screen.svg b/frontend/resources/images/icons/full-screen.svg new file mode 100644 index 000000000..47e7db42c --- /dev/null +++ b/frontend/resources/images/icons/full-screen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/styles/main.scss b/frontend/resources/styles/main.scss index cb43312a5..545e75779 100644 --- a/frontend/resources/styles/main.scss +++ b/frontend/resources/styles/main.scss @@ -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 diff --git a/frontend/resources/styles/main/layouts/not-found.scss b/frontend/resources/styles/main/layouts/not-found.scss new file mode 100644 index 000000000..359d7ab53 --- /dev/null +++ b/frontend/resources/styles/main/layouts/not-found.scss @@ -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; + } +} + diff --git a/frontend/resources/styles/main/layouts/viewer.scss b/frontend/resources/styles/main/layouts/viewer.scss new file mode 100644 index 000000000..eafd9911f --- /dev/null +++ b/frontend/resources/styles/main/layouts/viewer.scss @@ -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; + } +} diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss new file mode 100644 index 000000000..210d244d7 --- /dev/null +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -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; + } + } + } +} diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss new file mode 100644 index 000000000..bd92f3de9 --- /dev/null +++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss @@ -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; + } + } +} diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss new file mode 100644 index 000000000..15f3fd48d --- /dev/null +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -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; +} diff --git a/frontend/resources/styles/main/partials/workspace-bar.scss b/frontend/resources/styles/main/partials/workspace-bar.scss index 6a075a47f..52c129cd5 100644 --- a/frontend/resources/styles/main/partials/workspace-bar.scss +++ b/frontend/resources/styles/main/partials/workspace-bar.scss @@ -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; diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index cbb80b00f..8fabc07c4 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -17,7 +17,6 @@ window.uxboxConfig = JSON.parse({{& config }}); window.uxboxTranslations = JSON.parse({{& translations }}); - diff --git a/frontend/src/uxbox/builtins/icons.cljs b/frontend/src/uxbox/builtins/icons.cljs index a000cbdec..069f01f84 100644 --- a/frontend/src/uxbox/builtins/icons.cljs +++ b/frontend/src/uxbox/builtins/icons.cljs @@ -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)) diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index f89325453..63f222f91 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -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))))) diff --git a/frontend/src/uxbox/main/data/viewer.cljs b/frontend/src/uxbox/main/data/viewer.cljs new file mode 100644 index 000000000..babee28e4 --- /dev/null +++ b/frontend/src/uxbox/main/data/viewer.cljs @@ -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)))) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 7a6b28137..72fbb56f1 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -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))))) diff --git a/frontend/src/uxbox/main/exports.cljs b/frontend/src/uxbox/main/exports.cljs index 008c4661c..092b9d4f6 100644 --- a/frontend/src/uxbox/main/exports.cljs +++ b/frontend/src/uxbox/main/exports.cljs @@ -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))) diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index c4abf742e..19da8d924 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -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))) diff --git a/frontend/src/uxbox/main/store.cljs b/frontend/src/uxbox/main/store.cljs index 8905cc39d..a6cced39e 100644 --- a/frontend/src/uxbox/main/store.cljs +++ b/frontend/src/uxbox/main/store.cljs @@ -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) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index d9f95d47e..fe4fba293 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -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 diff --git a/frontend/src/uxbox/main/ui/not_found.cljs b/frontend/src/uxbox/main/ui/not_found.cljs new file mode 100644 index 000000000..85d8f6bbc --- /dev/null +++ b/frontend/src/uxbox/main/ui/not_found.cljs @@ -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"]]]]) diff --git a/frontend/src/uxbox/main/ui/react_hooks.cljs b/frontend/src/uxbox/main/ui/react_hooks.cljs index 6929a71ae..35da9ee1d 100644 --- a/frontend/src/uxbox/main/ui/react_hooks.cljs +++ b/frontend/src/uxbox/main/ui/react_hooks.cljs @@ -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)) - diff --git a/frontend/src/uxbox/main/ui/viewer.cljs b/frontend/src/uxbox/main/ui/viewer.cljs new file mode 100644 index 000000000..8b41c4ac7 --- /dev/null +++ b/frontend/src/uxbox/main/ui/viewer.cljs @@ -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}]))) diff --git a/frontend/src/uxbox/main/ui/viewer/header.cljs b/frontend/src/uxbox/main/ui/viewer/header.cljs new file mode 100644 index 000000000..ac9793c11 --- /dev/null +++ b/frontend/src/uxbox/main/ui/viewer/header.cljs @@ -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]]])) + diff --git a/frontend/src/uxbox/main/ui/viewer/thumbnails.cljs b/frontend/src/uxbox/main/ui/viewer/thumbnails.cljs new file mode 100644 index 000000000..f17057f2d --- /dev/null +++ b/frontend/src/uxbox/main/ui/viewer/thumbnails.cljs @@ -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)}])]]])) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 108ff1f9a..74aac1c2a 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -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 diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index afdfca654..0979c6ae3 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -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]]))