mirror of
https://github.com/penpot/penpot.git
synced 2025-05-05 21:36:04 +02:00
🎉 Add assets exportation in bulk (multiple)
And adapt to the websocket changes on backend and exporter.
This commit is contained in:
parent
f60d8c6c96
commit
0e0fb68c38
39 changed files with 1497 additions and 411 deletions
|
@ -10,10 +10,13 @@
|
||||||
(def black "#000000")
|
(def black "#000000")
|
||||||
(def canvas "#E8E9EA")
|
(def canvas "#E8E9EA")
|
||||||
(def default-layout "#DE4762")
|
(def default-layout "#DE4762")
|
||||||
|
(def gray-10 "#E3E3E3")
|
||||||
(def gray-20 "#B1B2B5")
|
(def gray-20 "#B1B2B5")
|
||||||
(def gray-30 "#7B7D85")
|
(def gray-30 "#7B7D85")
|
||||||
|
(def gray-40 "#64666A")
|
||||||
(def info "#59B9E2")
|
(def info "#59B9E2")
|
||||||
(def test "#fabada")
|
(def test "#fabada")
|
||||||
(def white "#FFFFFF")
|
(def white "#FFFFFF")
|
||||||
(def primary "#31EFB8")
|
(def primary "#31EFB8")
|
||||||
|
(def danger "#E65244")
|
||||||
|
(def warning "#FC8802")
|
||||||
|
|
|
@ -65,7 +65,8 @@
|
||||||
:masked-group? :mask-group
|
:masked-group? :mask-group
|
||||||
:constraints-h :constraints-group
|
:constraints-h :constraints-group
|
||||||
:constraints-v :constraints-group
|
:constraints-v :constraints-group
|
||||||
:fixed-scroll :constraints-group})
|
:fixed-scroll :constraints-group
|
||||||
|
:exports :exports-group})
|
||||||
|
|
||||||
;; Attributes that may directly be edited by the user with forms
|
;; Attributes that may directly be edited by the user with forms
|
||||||
(def editable-attrs
|
(def editable-attrs
|
||||||
|
@ -99,7 +100,9 @@
|
||||||
:stroke-opacity
|
:stroke-opacity
|
||||||
:stroke-color-gradient
|
:stroke-color-gradient
|
||||||
:stroke-cap-start
|
:stroke-cap-start
|
||||||
:stroke-cap-end}
|
:stroke-cap-end
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:group #{:proportion-lock
|
:group #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -120,7 +123,9 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}
|
:blur
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:rect #{:proportion-lock
|
:rect #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -162,7 +167,9 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}
|
:blur
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:circle #{:proportion-lock
|
:circle #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -202,7 +209,9 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}
|
:blur
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:path #{:proportion-lock
|
:path #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -242,7 +251,9 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}
|
:blur
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:text #{:proportion-lock
|
:text #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -305,7 +316,9 @@
|
||||||
|
|
||||||
:text-transform
|
:text-transform
|
||||||
|
|
||||||
:grow-type}
|
:grow-type
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:image #{:proportion-lock
|
:image #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -328,7 +341,9 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}
|
:blur
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:svg-raw #{:proportion-lock
|
:svg-raw #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -370,7 +385,9 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}
|
:blur
|
||||||
|
|
||||||
|
:exports}
|
||||||
|
|
||||||
:bool #{:proportion-lock
|
:bool #{:proportion-lock
|
||||||
:width :height
|
:width :height
|
||||||
|
@ -410,5 +427,7 @@
|
||||||
|
|
||||||
:shadow
|
:shadow
|
||||||
|
|
||||||
:blur}})
|
:blur
|
||||||
|
|
||||||
|
:exports}})
|
||||||
|
|
||||||
|
|
BIN
frontend/resources/images/export-no-shapes.png
Normal file
BIN
frontend/resources/images/export-no-shapes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg id="screenshot" viewBox="10064.99280029184 896.9999999999992 40.00000000000364 40.000000000000796" width="40" height="40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="-webkit-print-color-adjust: exact;"><g id="shape-3042ec00-9b94-11ec-b905-cb847d55c7a9"><path d="M10064.99280029184,897.0000000000006L10064.99280029184,937L10104.992800291844,936.9999999999995L10104.992800291842,896.9999999999992L10064.99280029184,897.0000000000006ZL10064.99280029184,897.0000000000006ZM10067.920800291844,899.9319999999997L10102.064800291844,899.9319999999999L10102.064800291844,934.0679999999994L10067.920800291844,934.068L10067.920800291844,899.9319999999997ZL10067.920800291844,899.9319999999997ZM10073,915L10097,915L10097,919L10073,919L10073,915Z"/></g></svg>
|
After Width: | Height: | Size: 807 B |
|
@ -96,7 +96,6 @@
|
||||||
height: 30px;
|
height: 30px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
width: 30px;
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
|
@ -140,7 +139,7 @@
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 63px;
|
height: 63px;
|
||||||
padding: 0px 16px;
|
padding: 0px 18px;
|
||||||
border-top: 1px solid $color-gray-10;
|
border-top: 1px solid $color-gray-10;
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
|
@ -235,12 +234,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-dialog,
|
.import-dialog,
|
||||||
.export-dialog {
|
.export-dialog,
|
||||||
|
.export-shapes-dialog {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
border: 1px solid $color-gray-20;
|
border: 1px solid $color-gray-20;
|
||||||
width: 30rem;
|
width: 30rem;
|
||||||
min-height: 14rem;
|
min-height: 14rem;
|
||||||
|
|
||||||
|
&.no-shapes {
|
||||||
|
width: 39rem;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: $fs14;
|
font-size: $fs14;
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
|
@ -259,9 +263,9 @@
|
||||||
border: 1px solid $color-gray-20;
|
border: 1px solid $color-gray-20;
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 0.3rem 1.25rem;
|
padding: 0.5rem 2.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 8px;
|
margin-right: 18px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $color-gray-20;
|
background: $color-gray-20;
|
||||||
|
@ -274,7 +278,7 @@
|
||||||
border: 1px solid $color-primary;
|
border: 1px solid $color-primary;
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.3rem 1.25rem;
|
padding: 0.5rem 2.25rem;
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
border: 1px solid #e3e3e3;
|
border: 1px solid #e3e3e3;
|
||||||
|
@ -295,7 +299,7 @@
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: $fs14;
|
font-size: $fs18;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,11 +307,6 @@
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
max-width: 18px;
|
|
||||||
max-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-entry {
|
.file-entry {
|
||||||
margin: 0.75rem 1rem;
|
margin: 0.75rem 1rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -515,6 +514,38 @@
|
||||||
&.selected {
|
&.selected {
|
||||||
border: 1px solid $color-primary;
|
border: 1px solid $color-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.table-row {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 45px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0px 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-field {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 0px 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 50px;
|
||||||
|
|
||||||
|
&.check {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scale {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-container {
|
.option-container {
|
||||||
|
@ -1306,3 +1337,216 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export shapes
|
||||||
|
|
||||||
|
.export-progress-modal-overlay {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
right: 1rem;
|
||||||
|
top: 3rem;
|
||||||
|
padding: 16px 18px;
|
||||||
|
background-color: $color-white;
|
||||||
|
border: 1px solid $color-gray-20;
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
&.transparent {
|
||||||
|
background-color: rgba($color-white, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-progress-modal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-progress-modal-header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: stretch;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
|
||||||
|
.modal-close-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-progress-modal-title {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: $color-black;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: $fs16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
color: $color-gray-30;
|
||||||
|
font-size: $fs16;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry {
|
||||||
|
font-size: $fs12;
|
||||||
|
margin-right: 16px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-shapes-dialog {
|
||||||
|
.modal-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0.5rem 0.5rem 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
flex-grow: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0px 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 50px;
|
||||||
|
|
||||||
|
&.image {
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid $color-gray-10;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 20px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.check {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
width: 30px;
|
||||||
|
svg {
|
||||||
|
fill: $color-white;
|
||||||
|
max-width: 18px;
|
||||||
|
max-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .checked {
|
||||||
|
svg {
|
||||||
|
background-color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .unchecked {
|
||||||
|
svg {
|
||||||
|
background-color: $color-gray-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.title {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: $fs12;
|
||||||
|
color: $color-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: $fs16;
|
||||||
|
color: $color-black;
|
||||||
|
width: 45%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scale {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scale,
|
||||||
|
&.extension {
|
||||||
|
color: $color-gray-30;
|
||||||
|
font-size: $fs12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid $color-gray-10;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 32px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 3rem;
|
||||||
|
margin: 0 0.5rem 0 2rem;
|
||||||
|
width: calc(100% - 2.5rem);
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-top: 1px solid $color-gray-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-selection {
|
||||||
|
padding: 2rem 1rem 2rem 2rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
color: $color-primary-dark;
|
||||||
|
float: right;
|
||||||
|
margin-left: 4rem;
|
||||||
|
width: 176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: $fs12;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $color-gray-40;
|
||||||
|
font-size: $fs16;
|
||||||
|
padding: 1rem 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -298,8 +298,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-progress-widget {
|
||||||
|
cursor: pointer;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.persistence-status-widget {
|
.persistence-status-widget {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-left: 0px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
/* border: 1px solid red; */
|
/* border: 1px solid red; */
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
[app.main.data.users :as du]
|
[app.main.data.users :as du]
|
||||||
|
[app.main.data.websocket :as ws]
|
||||||
[app.main.errors]
|
[app.main.errors]
|
||||||
[app.main.sentry :as sentry]
|
[app.main.sentry :as sentry]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
@ -58,7 +59,15 @@
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter du/profile-fetched?)
|
(rx/filter du/profile-fetched?)
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/map #(rt/init-routes)))))))
|
(rx/map #(rt/init-routes)))
|
||||||
|
|
||||||
|
(->> stream
|
||||||
|
(rx/filter du/profile-fetched?)
|
||||||
|
(rx/map deref)
|
||||||
|
(rx/filter du/is-authenticated?)
|
||||||
|
(rx/take 1)
|
||||||
|
(rx/map #(ws/initialize)))))))
|
||||||
|
|
||||||
|
|
||||||
(def essential-only?
|
(def essential-only?
|
||||||
(let [href (.-href ^js glob/location)]
|
(let [href (.-href ^js glob/location)]
|
||||||
|
|
225
frontend/src/app/main/data/exports.cljs
Normal file
225
frontend/src/app/main/data/exports.cljs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
;; 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.exports
|
||||||
|
(:require
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.data.workspace.persistence :as dwp]
|
||||||
|
[app.main.data.workspace.state-helpers :as wsh]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[app.util.websocket :as ws]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[potok.core :as ptk]))
|
||||||
|
|
||||||
|
(def default-timeout 5000)
|
||||||
|
|
||||||
|
(defn toggle-detail-visibililty
|
||||||
|
[]
|
||||||
|
(ptk/reify ::toggle-detail-visibililty
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update-in state [:export :detail-visible] not))))
|
||||||
|
|
||||||
|
(defn toggle-widget-visibililty
|
||||||
|
[]
|
||||||
|
(ptk/reify ::toggle-widget-visibility
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update-in state [:export :widget-visible] not))))
|
||||||
|
|
||||||
|
(defn clear-export-state
|
||||||
|
[id]
|
||||||
|
(ptk/reify ::clear-export-state
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
;; only clear if the existing export is the same
|
||||||
|
(let [existing-id (-> state :export :id)]
|
||||||
|
(if (and (some? existing-id)
|
||||||
|
(not= id existing-id))
|
||||||
|
state
|
||||||
|
(dissoc state :export))))))
|
||||||
|
|
||||||
|
(defn show-workspace-export-dialog
|
||||||
|
([] (show-workspace-export-dialog nil))
|
||||||
|
([{:keys [selected]}]
|
||||||
|
(ptk/reify ::show-workspace-export-dialog
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [file-id (:current-file-id state)
|
||||||
|
page-id (:current-page-id state)
|
||||||
|
|
||||||
|
filename (-> (wsh/lookup-page state page-id) :name)
|
||||||
|
selected (or selected (wsh/lookup-selected state page-id {}))
|
||||||
|
|
||||||
|
shapes (if (seq selected)
|
||||||
|
(wsh/lookup-shapes state selected)
|
||||||
|
(wsh/filter-shapes state #(pos? (count (:exports %)))))
|
||||||
|
|
||||||
|
exports (for [shape shapes
|
||||||
|
export (:exports shape)]
|
||||||
|
(-> export
|
||||||
|
(assoc :enabled true)
|
||||||
|
(assoc :page-id page-id)
|
||||||
|
(assoc :file-id file-id)
|
||||||
|
(assoc :object-id (:id shape))
|
||||||
|
(assoc :shape (dissoc shape :exports))
|
||||||
|
(assoc :name (:name shape))))]
|
||||||
|
|
||||||
|
(rx/of (modal/show :export-shapes
|
||||||
|
{:exports (vec exports)
|
||||||
|
:filename filename})))))))
|
||||||
|
|
||||||
|
(defn show-viewer-export-dialog
|
||||||
|
[{:keys [shapes filename page-id file-id exports]}]
|
||||||
|
(ptk/reify ::show-viewer-export-dialog
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(let [exports (for [shape shapes
|
||||||
|
export exports]
|
||||||
|
(-> export
|
||||||
|
(assoc :enabled true)
|
||||||
|
(assoc :page-id page-id)
|
||||||
|
(assoc :file-id file-id)
|
||||||
|
(assoc :object-id (:id shape))
|
||||||
|
(assoc :shape (dissoc shape :exports))
|
||||||
|
(assoc :name (:name shape))))]
|
||||||
|
(rx/of (modal/show :export-shapes {:exports (vec exports)
|
||||||
|
:filename filename}))))))
|
||||||
|
|
||||||
|
(defn- initialize-export-status
|
||||||
|
[exports filename resource-id]
|
||||||
|
(ptk/reify ::initialize-export-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(assoc state :export {:in-progress true
|
||||||
|
:resource-id resource-id
|
||||||
|
:healthy? true
|
||||||
|
:error false
|
||||||
|
:progress 0
|
||||||
|
:widget-visible true
|
||||||
|
:detail-visible true
|
||||||
|
:exports exports
|
||||||
|
:filename filename
|
||||||
|
:last-update (dt/now)}))))
|
||||||
|
|
||||||
|
(defn- update-export-status
|
||||||
|
[{:keys [progress status resource-id name] :as data}]
|
||||||
|
(ptk/reify ::update-export-status
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [time-diff (dt/diff (dt/now)
|
||||||
|
(get-in state [:export :last-update]))
|
||||||
|
healthy? (< time-diff (dt/duration {:seconds 6}))]
|
||||||
|
(cond-> state
|
||||||
|
(= status "running")
|
||||||
|
(update :export assoc :progress (:done progress) :last-update (dt/now) :healthy? healthy?)
|
||||||
|
|
||||||
|
(= status "error")
|
||||||
|
(update :export assoc :error (:cause data) :last-update (dt/now) :healthy? healthy?)
|
||||||
|
|
||||||
|
(= status "ended")
|
||||||
|
(update :export assoc :in-progress false :last-update (dt/now) :healthy? healthy?))))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ _ _]
|
||||||
|
(when (= status "ended")
|
||||||
|
(->> (rp/query! :download-export-resource resource-id)
|
||||||
|
(rx/delay 500)
|
||||||
|
(rx/map #(dom/trigger-download name %)))))))
|
||||||
|
|
||||||
|
(defn request-simple-export
|
||||||
|
[{:keys [export filename]}]
|
||||||
|
(ptk/reify ::request-simple-export
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :export assoc :in-progress true))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [profile-id (:profile-id state)
|
||||||
|
params {:exports [export]
|
||||||
|
:profile-id profile-id}]
|
||||||
|
(rx/concat
|
||||||
|
(rx/of ::dwp/force-persist)
|
||||||
|
(->> (rp/query! :export-shapes-simple params)
|
||||||
|
(rx/map (fn [data]
|
||||||
|
(dom/trigger-download filename data)
|
||||||
|
(fn [state]
|
||||||
|
(dissoc state :export))))))))))
|
||||||
|
|
||||||
|
(defn request-multiple-export
|
||||||
|
[{:keys [filename exports] :as params}]
|
||||||
|
(ptk/reify ::request-multiple-export
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [resource-id (volatile! nil)
|
||||||
|
profile-id (:profile-id state)
|
||||||
|
ws-conn (:ws-conn state)
|
||||||
|
params {:exports exports
|
||||||
|
:name filename
|
||||||
|
:profile-id profile-id
|
||||||
|
:wait false}
|
||||||
|
|
||||||
|
progress-stream
|
||||||
|
(->> (ws/get-rcv-stream ws-conn)
|
||||||
|
(rx/filter ws/message-event?)
|
||||||
|
(rx/map :payload)
|
||||||
|
(rx/filter #(= :export-update (:type %)))
|
||||||
|
(rx/filter #(= @resource-id (:resource-id %)))
|
||||||
|
(rx/share))
|
||||||
|
|
||||||
|
stoper
|
||||||
|
(->> progress-stream
|
||||||
|
(rx/filter #(or (= "ended" (:status %))
|
||||||
|
(= "error" (:status %)))))]
|
||||||
|
|
||||||
|
(swap! st/ongoing-tasks conj :export)
|
||||||
|
|
||||||
|
(rx/merge
|
||||||
|
;; Force that all data is persisted; best effort.
|
||||||
|
(rx/of ::dwp/force-persist)
|
||||||
|
|
||||||
|
;; Launch the exportation process and stores the resource id
|
||||||
|
;; locally.
|
||||||
|
(->> (rp/query! :export-shapes-multiple params)
|
||||||
|
(rx/tap (fn [{:keys [id]}]
|
||||||
|
(vreset! resource-id id)))
|
||||||
|
(rx/map (fn [{:keys [id]}]
|
||||||
|
(initialize-export-status exports filename id))))
|
||||||
|
|
||||||
|
;; We proceed to update the export state with incoming
|
||||||
|
;; progress updates. We delay the stoper for give some time
|
||||||
|
;; to update the status with ended or errored status before
|
||||||
|
;; close the stream.
|
||||||
|
(->> progress-stream
|
||||||
|
(rx/map update-export-status)
|
||||||
|
(rx/take-until (rx/delay 500 stoper))
|
||||||
|
(rx/finalize (fn []
|
||||||
|
(swap! st/ongoing-tasks disj :export))))
|
||||||
|
|
||||||
|
;; We hide need to hide the ui elements of the export after
|
||||||
|
;; some interval. We also delay a litle bit more the stopper
|
||||||
|
;; for ensure that after some security time, the stream is
|
||||||
|
;; completelly closed.
|
||||||
|
(->> progress-stream
|
||||||
|
(rx/filter #(= "ended" (:status %)))
|
||||||
|
(rx/take 1)
|
||||||
|
(rx/delay default-timeout)
|
||||||
|
(rx/map #(clear-export-state @resource-id))
|
||||||
|
(rx/take-until (rx/delay 6000 stoper))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn retry-last-export
|
||||||
|
[]
|
||||||
|
(ptk/reify ::retry-last-export
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [{:keys [exports filename]} (:export state)]
|
||||||
|
(rx/of (request-multiple-export {:exports exports :filename filename}))))))
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
[app.main.data.media :as di]
|
[app.main.data.media :as di]
|
||||||
|
[app.main.data.websocket :as ws]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.util.i18n :as i18n]
|
[app.util.i18n :as i18n]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
|
@ -167,7 +168,8 @@
|
||||||
(when (is-authenticated? profile)
|
(when (is-authenticated? profile)
|
||||||
(->> (rx/of (profile-fetched profile)
|
(->> (rx/of (profile-fetched profile)
|
||||||
(fetch-teams)
|
(fetch-teams)
|
||||||
(get-redirect-event))
|
(get-redirect-event)
|
||||||
|
(ws/initialize))
|
||||||
(rx/observe-on :async)))))))
|
(rx/observe-on :async)))))))
|
||||||
|
|
||||||
(s/def ::invitation-token ::us/not-empty-string)
|
(s/def ::invitation-token ::us/not-empty-string)
|
||||||
|
@ -268,10 +270,12 @@
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
|
(rx/merge
|
||||||
;; NOTE: We need the `effect` of the current event to be
|
;; NOTE: We need the `effect` of the current event to be
|
||||||
;; executed before the redirect.
|
;; executed before the redirect.
|
||||||
(->> (rx/of (rt/nav :auth-login))
|
(->> (rx/of (rt/nav :auth-login))
|
||||||
(rx/observe-on :async)))
|
(rx/observe-on :async))
|
||||||
|
(rx/of (ws/finalize))))
|
||||||
|
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ _ _]
|
(effect [_ _ _]
|
||||||
|
|
70
frontend/src/app/main/data/websocket.cljs
Normal file
70
frontend/src/app/main/data/websocket.cljs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
;; 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.websocket
|
||||||
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.uri :as u]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.util.websocket :as ws]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[potok.core :as ptk]))
|
||||||
|
|
||||||
|
(dm/export ws/send!)
|
||||||
|
|
||||||
|
(defn- prepare-uri
|
||||||
|
[params]
|
||||||
|
(let [base (-> (u/join cf/public-uri "ws/notifications")
|
||||||
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
(cond-> base
|
||||||
|
(= "https" (:scheme base))
|
||||||
|
(assoc :scheme "wss")
|
||||||
|
|
||||||
|
(= "http" (:scheme base))
|
||||||
|
(assoc :scheme "ws"))))
|
||||||
|
|
||||||
|
(defn send
|
||||||
|
[message]
|
||||||
|
(ptk/reify ::send-message
|
||||||
|
ptk/EffectEvent
|
||||||
|
(effect [_ state _]
|
||||||
|
(let [ws-conn (:ws-conn state)]
|
||||||
|
(ws/send! ws-conn message)))))
|
||||||
|
|
||||||
|
(defn initialize
|
||||||
|
[]
|
||||||
|
(ptk/reify ::initialize
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [sid (:session-id state)
|
||||||
|
uri (prepare-uri {:session-id sid})]
|
||||||
|
(assoc state :ws-conn (ws/create uri))))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(let [ws-conn (:ws-conn state)
|
||||||
|
stoper (rx/merge
|
||||||
|
(rx/filter (ptk/type? ::finalize) stream)
|
||||||
|
(rx/filter (ptk/type? ::initialize) stream))]
|
||||||
|
|
||||||
|
(->> (rx/merge
|
||||||
|
(->> (ws/get-rcv-stream ws-conn)
|
||||||
|
(rx/filter ws/message-event?)
|
||||||
|
(rx/map :payload)
|
||||||
|
(rx/map #(ptk/data-event ::message %)))
|
||||||
|
(->> (ws/get-rcv-stream ws-conn)
|
||||||
|
(rx/filter ws/opened-event?)
|
||||||
|
(rx/map (fn [_] (ptk/data-event ::opened {})))))
|
||||||
|
(rx/take-until stoper))))))
|
||||||
|
|
||||||
|
;; --- Finalize Websocket
|
||||||
|
|
||||||
|
(defn finalize
|
||||||
|
[]
|
||||||
|
(ptk/reify ::finalize
|
||||||
|
ptk/EffectEvent
|
||||||
|
(effect [_ state _]
|
||||||
|
(some-> (:ws-conn state) ws/close!))))
|
|
@ -116,15 +116,16 @@
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/map deref)
|
(rx/map deref)
|
||||||
(rx/mapcat (fn [bundle]
|
(rx/mapcat (fn [bundle]
|
||||||
|
(let [team-id (-> bundle :project :team-id)]
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/of (dwn/initialize file-id)
|
(rx/of (dwn/initialize team-id file-id)
|
||||||
(dwp/initialize-file-persistence file-id)
|
(dwp/initialize-file-persistence file-id)
|
||||||
(dwc/initialize-indices bundle))
|
(dwc/initialize-indices bundle))
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter #(= ::dwc/index-initialized %))
|
(rx/filter #(= ::dwc/index-initialized %))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
(rx/map #(file-initialized bundle)))))))))
|
(rx/map #(file-initialized bundle))))))))))
|
||||||
|
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ _ _]
|
(effect [_ _ _]
|
||||||
|
|
|
@ -7,18 +7,15 @@
|
||||||
(ns app.main.data.workspace.notifications
|
(ns app.main.data.workspace.notifications
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.point :as gpt]
|
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.spec.change :as spec.change]
|
[app.common.spec.change :as spec.change]
|
||||||
[app.common.transit :as t]
|
[app.common.uuid :as uuid]
|
||||||
[app.common.uri :as u]
|
[app.main.data.websocket :as dws]
|
||||||
[app.config :as cf]
|
|
||||||
[app.main.data.workspace.changes :as dch]
|
[app.main.data.workspace.changes :as dch]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
[app.main.data.workspace.persistence :as dwp]
|
[app.main.data.workspace.persistence :as dwp]
|
||||||
[app.main.streams :as ms]
|
[app.main.streams :as ms]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.websockets :as ws]
|
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
|
@ -30,103 +27,91 @@
|
||||||
(declare handle-file-change)
|
(declare handle-file-change)
|
||||||
(declare handle-library-change)
|
(declare handle-library-change)
|
||||||
(declare handle-pointer-send)
|
(declare handle-pointer-send)
|
||||||
(declare send-keepalive)
|
(declare handle-export-update)
|
||||||
|
|
||||||
(s/def ::type keyword?)
|
|
||||||
(s/def ::message
|
|
||||||
(s/keys :req-un [::type]))
|
|
||||||
|
|
||||||
(defn prepare-uri
|
|
||||||
[params]
|
|
||||||
(let [base (-> (u/join cf/public-uri "ws/notifications")
|
|
||||||
(assoc :query (u/map->query-string params)))]
|
|
||||||
(cond-> base
|
|
||||||
(= "https" (:scheme base))
|
|
||||||
(assoc :scheme "wss")
|
|
||||||
|
|
||||||
(= "http" (:scheme base))
|
|
||||||
(assoc :scheme "ws"))))
|
|
||||||
|
|
||||||
(defn initialize
|
(defn initialize
|
||||||
[file-id]
|
[team-id file-id]
|
||||||
(ptk/reify ::initialize
|
(ptk/reify ::initialize
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [_ state]
|
|
||||||
(let [sid (:session-id state)
|
|
||||||
uri (prepare-uri {:file-id file-id :session-id sid})]
|
|
||||||
(assoc-in state [:ws file-id] (ws/open uri))))
|
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [wsession (get-in state [:ws file-id])
|
(let [subs-id (uuid/next)
|
||||||
stoper (->> stream
|
stoper (rx/filter (ptk/type? ::finalize) stream)
|
||||||
(rx/filter (ptk/type? ::finalize)))
|
|
||||||
interval (* 1000 60)]
|
|
||||||
(->> (rx/merge
|
|
||||||
;; Each 60 seconds send a keepalive message for maintain
|
|
||||||
;; this socket open.
|
|
||||||
(->> (rx/timer interval interval)
|
|
||||||
(rx/map #(send-keepalive file-id)))
|
|
||||||
|
|
||||||
;; Process all incoming messages.
|
initmsg [{:type :subscribe-file
|
||||||
(->> (ws/-stream wsession)
|
:subs-id subs-id
|
||||||
(rx/filter ws/message?)
|
:file-id file-id}
|
||||||
(rx/map (comp t/decode-str :payload))
|
{:type :subscribe-team
|
||||||
(rx/filter #(s/valid? ::message %))
|
:team-id team-id}]
|
||||||
(rx/map process-message))
|
|
||||||
|
|
||||||
|
endmsg {:type :unsubscribe-file
|
||||||
|
:subs-id subs-id}
|
||||||
|
|
||||||
|
stream (->> (rx/merge
|
||||||
|
;; Send the subscription message
|
||||||
|
(->> (rx/from initmsg)
|
||||||
|
(rx/map dws/send))
|
||||||
|
|
||||||
|
;; Subscribe to notifications of the subscription
|
||||||
|
(->> stream
|
||||||
|
(rx/filter (ptk/type? ::dws/message))
|
||||||
|
(rx/map deref)
|
||||||
|
(rx/map process-message)
|
||||||
|
(rx/filter #(= subs-id (:subs-id %))))
|
||||||
|
|
||||||
|
;; On reconnect, send again the subscription messages
|
||||||
|
(->> stream
|
||||||
|
(rx/filter (ptk/type? ::dws/opened))
|
||||||
|
(rx/mapcat #(->> (rx/from initmsg)
|
||||||
|
(rx/map dws/send))))
|
||||||
|
|
||||||
|
;; Emit presence event for current user;
|
||||||
|
;; this is because websocket server don't
|
||||||
|
;; emits this for the same user.
|
||||||
(rx/of (handle-presence {:type :connect
|
(rx/of (handle-presence {:type :connect
|
||||||
:session-id (:session-id state)
|
:session-id (:session-id state)
|
||||||
:profile-id (:profile-id state)}))
|
:profile-id (:profile-id state)}))
|
||||||
|
|
||||||
;; Send back to backend all pointer messages.
|
;; Emit to all other connected users the current pointer
|
||||||
|
;; position changes.
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter ms/pointer-event?)
|
(rx/filter ms/pointer-event?)
|
||||||
(rx/sample 50)
|
(rx/sample 50)
|
||||||
(rx/map #(handle-pointer-send file-id (:pt %)))))
|
(rx/map #(handle-pointer-send subs-id file-id (:pt %)))))
|
||||||
(rx/take-until stoper))))))
|
|
||||||
|
(rx/take-until stoper))]
|
||||||
|
|
||||||
|
(rx/concat stream (rx/of (dws/send endmsg)))))))
|
||||||
|
|
||||||
(defn- process-message
|
(defn- process-message
|
||||||
[{:keys [type] :as msg}]
|
[{:keys [type] :as msg}]
|
||||||
(case type
|
(case type
|
||||||
:connect (handle-presence msg)
|
:join-file (handle-presence msg)
|
||||||
|
:leave-file (handle-presence msg)
|
||||||
:presence (handle-presence msg)
|
:presence (handle-presence msg)
|
||||||
:disconnect (handle-presence msg)
|
:disconnect (handle-presence msg)
|
||||||
:pointer-update (handle-pointer-update msg)
|
:pointer-update (handle-pointer-update msg)
|
||||||
:file-change (handle-file-change msg)
|
:file-change (handle-file-change msg)
|
||||||
:library-change (handle-library-change msg)
|
:library-change (handle-library-change msg)
|
||||||
::unknown))
|
nil))
|
||||||
|
|
||||||
(defn- send-keepalive
|
|
||||||
[file-id]
|
|
||||||
(ptk/reify ::send-keepalive
|
|
||||||
ptk/EffectEvent
|
|
||||||
(effect [_ state _]
|
|
||||||
(when-let [ws (get-in state [:ws file-id])]
|
|
||||||
(ws/send! ws {:type :keepalive})))))
|
|
||||||
|
|
||||||
(defn- handle-pointer-send
|
(defn- handle-pointer-send
|
||||||
[file-id point]
|
[subs-id file-id point]
|
||||||
(ptk/reify ::handle-pointer-send
|
(ptk/reify ::handle-pointer-send
|
||||||
ptk/EffectEvent
|
ptk/WatchEvent
|
||||||
(effect [_ state _]
|
(watch [_ state _]
|
||||||
(let [ws (get-in state [:ws file-id])
|
(let [page-id (:current-page-id state)
|
||||||
pid (:current-page-id state)
|
message {:type :pointer-update
|
||||||
msg {:type :pointer-update
|
:subs-id subs-id
|
||||||
:page-id pid
|
:file-id file-id
|
||||||
:x (:x point)
|
:page-id page-id
|
||||||
:y (:y point)}]
|
:position point}]
|
||||||
(ws/send! ws msg)))))
|
(rx/of (dws/send message))))))
|
||||||
|
|
||||||
;; --- Finalize Websocket
|
;; --- Finalize Websocket
|
||||||
|
|
||||||
(defn finalize
|
(defn finalize
|
||||||
[file-id]
|
[_]
|
||||||
(ptk/reify ::finalize
|
(ptk/reify ::finalize))
|
||||||
ptk/EffectEvent
|
|
||||||
(effect [_ state _]
|
|
||||||
(when-let [ws (get-in state [:ws file-id])]
|
|
||||||
(ws/-close ws)))))
|
|
||||||
|
|
||||||
;; --- Handle: Presence
|
;; --- Handle: Presence
|
||||||
|
|
||||||
|
@ -165,7 +150,8 @@
|
||||||
(assoc :profile-id profile-id)
|
(assoc :profile-id profile-id)
|
||||||
(assoc :updated-at (dt/now))
|
(assoc :updated-at (dt/now))
|
||||||
(update :color update-color presence)
|
(update :color update-color presence)
|
||||||
(assoc :text-color (if (contains? ["#00fa9a" "#ffd700" "#dda0dd" "#ffafda"] (update-color (:color presence) presence))
|
(assoc :text-color (if (contains? ["#00fa9a" "#ffd700" "#dda0dd" "#ffafda"]
|
||||||
|
(update-color (:color presence) presence))
|
||||||
"#000"
|
"#000"
|
||||||
"#fff"))))
|
"#fff"))))
|
||||||
|
|
||||||
|
@ -179,20 +165,19 @@
|
||||||
(ptk/reify ::handle-presence
|
(ptk/reify ::handle-presence
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
;; (let [profiles (:users state)]
|
(if (or (= :disconnect type) (= :leave-file type))
|
||||||
(if (= :disconnect type)
|
|
||||||
(update state :workspace-presence dissoc session-id)
|
(update state :workspace-presence dissoc session-id)
|
||||||
(update state :workspace-presence update-presence))))))
|
(update state :workspace-presence update-presence))))))
|
||||||
|
|
||||||
(defn handle-pointer-update
|
(defn handle-pointer-update
|
||||||
[{:keys [page-id session-id x y] :as msg}]
|
[{:keys [page-id session-id position] :as msg}]
|
||||||
(ptk/reify ::handle-pointer-update
|
(ptk/reify ::handle-pointer-update
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update-in state [:workspace-presence session-id]
|
(update-in state [:workspace-presence session-id]
|
||||||
(fn [session]
|
(fn [session]
|
||||||
(assoc session
|
(assoc session
|
||||||
:point (gpt/point x y)
|
:point position
|
||||||
:updated-at (dt/now)
|
:updated-at (dt/now)
|
||||||
:page-id page-id))))))
|
:page-id page-id))))))
|
||||||
|
|
||||||
|
@ -241,4 +226,3 @@
|
||||||
(when (contains? (:workspace-libraries state) file-id)
|
(when (contains? (:workspace-libraries state) file-id)
|
||||||
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
|
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
|
||||||
(dwl/notify-sync-file file-id))))))
|
(dwl/notify-sync-file file-id))))))
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.object :as obj]
|
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.uri :as uu]
|
[app.util.uri :as uu]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
|
@ -71,7 +70,7 @@
|
||||||
on-dirty
|
on-dirty
|
||||||
(fn []
|
(fn []
|
||||||
;; Enable reload stoper
|
;; Enable reload stoper
|
||||||
(obj/set! js/window "onbeforeunload" (constantly false))
|
(swap! st/ongoing-tasks conj :workspace-change)
|
||||||
(st/emit! (update-persistence-status {:status :pending})))
|
(st/emit! (update-persistence-status {:status :pending})))
|
||||||
|
|
||||||
on-saving
|
on-saving
|
||||||
|
@ -81,7 +80,7 @@
|
||||||
on-saved
|
on-saved
|
||||||
(fn []
|
(fn []
|
||||||
;; Disable reload stoper
|
;; Disable reload stoper
|
||||||
(obj/set! js/window "onbeforeunload" nil)
|
(swap! st/ongoing-tasks disj :workspace-change)
|
||||||
(st/emit! (update-persistence-status {:status :saved})))]
|
(st/emit! (update-persistence-status {:status :saved})))]
|
||||||
(->> (rx/merge
|
(->> (rx/merge
|
||||||
(->> stream
|
(->> stream
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.data.workspace.shortcuts
|
(ns app.main.data.workspace.shortcuts
|
||||||
(:require
|
(:require
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
|
[app.main.data.exports :as de]
|
||||||
[app.main.data.shortcuts :as ds]
|
[app.main.data.shortcuts :as ds]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.colors :as mdc]
|
[app.main.data.workspace.colors :as mdc]
|
||||||
|
@ -61,6 +62,11 @@
|
||||||
:command (ds/c-mod "shift+r")
|
:command (ds/c-mod "shift+r")
|
||||||
:fn #(st/emit! (toggle-layout-flag :rules))}
|
:fn #(st/emit! (toggle-layout-flag :rules))}
|
||||||
|
|
||||||
|
:export-shapes {:tooltip (ds/meta-shift "E")
|
||||||
|
:command (ds/c-mod "shift+e")
|
||||||
|
:fn #(st/emit!
|
||||||
|
(de/show-workspace-export-dialog))}
|
||||||
|
|
||||||
:select-all {:tooltip (ds/meta "A")
|
:select-all {:tooltip (ds/meta "A")
|
||||||
:command (ds/c-mod "a")
|
:command (ds/c-mod "a")
|
||||||
:fn #(st/emit! (dw/select-all))}
|
:fn #(st/emit! (dw/select-all))}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.data.workspace.state-helpers
|
(ns app.main.data.workspace.state-helpers
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.pages.helpers :as cph]))
|
[app.common.pages.helpers :as cph]))
|
||||||
|
|
||||||
(defn lookup-page
|
(defn lookup-page
|
||||||
|
@ -35,14 +36,16 @@
|
||||||
([state]
|
([state]
|
||||||
(get-in state [:workspace-data :components])))
|
(get-in state [:workspace-data :components])))
|
||||||
|
|
||||||
|
;; TODO: improve performance of this
|
||||||
|
|
||||||
(defn lookup-selected
|
(defn lookup-selected
|
||||||
([state]
|
([state]
|
||||||
(lookup-selected state nil))
|
(lookup-selected state nil))
|
||||||
|
([state options]
|
||||||
([state {:keys [omit-blocked?]
|
(lookup-selected state (:current-page-id state) options))
|
||||||
:or {omit-blocked? false}}]
|
([state page-id {:keys [omit-blocked?] :or {omit-blocked? false}}]
|
||||||
(let [objects (lookup-page-objects state)
|
(let [objects (lookup-page-objects state page-id)
|
||||||
selected (->> (get-in state [:workspace-local :selected])
|
selected (->> (dm/get-in state [:workspace-local :selected])
|
||||||
(cph/clean-loops objects))
|
(cph/clean-loops objects))
|
||||||
selectable? (fn [id]
|
selectable? (fn [id]
|
||||||
(and (contains? objects id)
|
(and (contains? objects id)
|
||||||
|
@ -51,3 +54,17 @@
|
||||||
(into (d/ordered-set)
|
(into (d/ordered-set)
|
||||||
(filter selectable?)
|
(filter selectable?)
|
||||||
selected))))
|
selected))))
|
||||||
|
|
||||||
|
(defn lookup-shapes
|
||||||
|
([state ids]
|
||||||
|
(lookup-shapes state (:current-page-id state) ids))
|
||||||
|
([state page-id ids]
|
||||||
|
(let [objects (lookup-page-objects state page-id)]
|
||||||
|
(into [] (keep (d/getf objects)) ids))))
|
||||||
|
|
||||||
|
(defn filter-shapes
|
||||||
|
([state filter-fn]
|
||||||
|
(filter-shapes state (:current-page-id state) filter-fn))
|
||||||
|
([state page-id filter-fn]
|
||||||
|
(let [objects (lookup-page-objects state page-id)]
|
||||||
|
(into [] (filter filter-fn) (vals objects)))))
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.messages :as msg]
|
[app.main.data.messages :as msg]
|
||||||
[app.main.data.users :as du]
|
[app.main.data.users :as du]
|
||||||
[app.main.sentry :as sentry]
|
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
|
@ -26,7 +25,7 @@
|
||||||
[error]
|
[error]
|
||||||
(cond
|
(cond
|
||||||
(instance? ExceptionInfo error)
|
(instance? ExceptionInfo error)
|
||||||
(-> error sentry/capture-exception ex-data ptk/handle-error)
|
(-> error ex-data ptk/handle-error)
|
||||||
|
|
||||||
(map? error)
|
(map? error)
|
||||||
(ptk/handle-error error)
|
(ptk/handle-error error)
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
:else
|
:else
|
||||||
(let [hint (ex-message error)
|
(let [hint (ex-message error)
|
||||||
msg (dm/str "Internal Error: " hint)]
|
msg (dm/str "Internal Error: " hint)]
|
||||||
(sentry/capture-exception error)
|
|
||||||
(ts/schedule (st/emitf (rt/assign-exception error)))
|
(ts/schedule (st/emitf (rt/assign-exception error)))
|
||||||
|
|
||||||
(js/console.group msg)
|
(js/console.group msg)
|
||||||
|
@ -68,7 +66,8 @@
|
||||||
;; Error that happens on an active business model validation does not
|
;; Error that happens on an active business model validation does not
|
||||||
;; passes an validation (example: profile can't leave a team). From
|
;; passes an validation (example: profile can't leave a team). From
|
||||||
;; the user perspective a error flash message should be visualized but
|
;; the user perspective a error flash message should be visualized but
|
||||||
;; user can continue operate on the application.
|
;; user can continue operate on the application. Can happen in backend
|
||||||
|
;; and frontend.
|
||||||
(defmethod ptk/handle-error :validation
|
(defmethod ptk/handle-error :validation
|
||||||
[error]
|
[error]
|
||||||
(ts/schedule
|
(ts/schedule
|
||||||
|
@ -92,6 +91,7 @@
|
||||||
|
|
||||||
|
|
||||||
;; Error on parsing an SVG
|
;; Error on parsing an SVG
|
||||||
|
;; TODO: looks unused and deprecated
|
||||||
(defmethod ptk/handle-error :svg-parser
|
(defmethod ptk/handle-error :svg-parser
|
||||||
[_]
|
[_]
|
||||||
(ts/schedule
|
(ts/schedule
|
||||||
|
@ -100,6 +100,7 @@
|
||||||
:type :error
|
:type :error
|
||||||
:timeout 3000}))))
|
:timeout 3000}))))
|
||||||
|
|
||||||
|
;; TODO: should be handled in the event and not as general error handler
|
||||||
(defmethod ptk/handle-error :comment-error
|
(defmethod ptk/handle-error :comment-error
|
||||||
[_]
|
[_]
|
||||||
(ts/schedule
|
(ts/schedule
|
||||||
|
@ -160,10 +161,9 @@
|
||||||
(defn on-unhandled-error
|
(defn on-unhandled-error
|
||||||
[error]
|
[error]
|
||||||
(if (instance? ExceptionInfo error)
|
(if (instance? ExceptionInfo error)
|
||||||
(-> error sentry/capture-exception ex-data ptk/handle-error)
|
(-> error ex-data ptk/handle-error)
|
||||||
(let [hint (ex-message error)
|
(let [hint (ex-message error)
|
||||||
msg (dm/str "Unhandled Internal Error: " hint)]
|
msg (dm/str "Unhandled Internal Error: " hint)]
|
||||||
(sentry/capture-exception error)
|
|
||||||
(ts/schedule (st/emitf (rt/assign-exception error)))
|
(ts/schedule (st/emitf (rt/assign-exception error)))
|
||||||
(js/console.group msg)
|
(js/console.group msg)
|
||||||
(ex/ignoring (js/console.error error))
|
(ex/ignoring (js/console.error error))
|
||||||
|
|
|
@ -42,6 +42,9 @@
|
||||||
(def share-links
|
(def share-links
|
||||||
(l/derived :share-links st/state))
|
(l/derived :share-links st/state))
|
||||||
|
|
||||||
|
(def export
|
||||||
|
(l/derived :export st/state))
|
||||||
|
|
||||||
;; ---- Dashboard refs
|
;; ---- Dashboard refs
|
||||||
|
|
||||||
(def dashboard-local
|
(def dashboard-local
|
||||||
|
@ -98,6 +101,7 @@
|
||||||
(def workspace-drawing
|
(def workspace-drawing
|
||||||
(l/derived :workspace-drawing st/state))
|
(l/derived :workspace-drawing st/state))
|
||||||
|
|
||||||
|
;; TODO: rename to workspace-selected (?)
|
||||||
(def selected-shapes
|
(def selected-shapes
|
||||||
(l/derived wsh/lookup-selected st/state =))
|
(l/derived wsh/lookup-selected st/state =))
|
||||||
|
|
||||||
|
@ -105,6 +109,27 @@
|
||||||
[id]
|
[id]
|
||||||
(l/derived #(contains? % id) selected-shapes))
|
(l/derived #(contains? % id) selected-shapes))
|
||||||
|
|
||||||
|
(def export-in-progress?
|
||||||
|
(l/derived :export-in-progress? export))
|
||||||
|
|
||||||
|
(def export-error?
|
||||||
|
(l/derived :export-error? export))
|
||||||
|
|
||||||
|
(def export-progress
|
||||||
|
(l/derived :export-progress export))
|
||||||
|
|
||||||
|
(def exports
|
||||||
|
(l/derived :exports export))
|
||||||
|
|
||||||
|
(def export-detail-visibililty
|
||||||
|
(l/derived :export-detail-visibililty export))
|
||||||
|
|
||||||
|
(def export-widget-visibililty
|
||||||
|
(l/derived :export-widget-visibililty export))
|
||||||
|
|
||||||
|
(def export-health
|
||||||
|
(l/derived :export-health export))
|
||||||
|
|
||||||
(def selected-zoom
|
(def selected-zoom
|
||||||
(l/derived :zoom workspace-local))
|
(l/derived :zoom workspace-local))
|
||||||
|
|
||||||
|
@ -233,11 +258,7 @@
|
||||||
|
|
||||||
(defn objects-by-id
|
(defn objects-by-id
|
||||||
[ids]
|
[ids]
|
||||||
(let [selector
|
(l/derived #(wsh/lookup-shapes % ids) st/state =))
|
||||||
(fn [state]
|
|
||||||
(let [objects (wsh/lookup-page-objects state)]
|
|
||||||
(into [] (keep (d/getf objects)) ids)))]
|
|
||||||
(l/derived selector st/state =)))
|
|
||||||
|
|
||||||
(defn- set-content-modifiers [state]
|
(defn- set-content-modifiers [state]
|
||||||
(fn [id shape]
|
(fn [id shape]
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.config :as cfg]
|
[app.config :as cf]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[beicon.core :as rx]))
|
[beicon.core :as rx]))
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
:status status
|
:status status
|
||||||
:data body})))
|
:data body})))
|
||||||
|
|
||||||
(def ^:private base-uri cfg/public-uri)
|
(def ^:private base-uri cf/public-uri)
|
||||||
|
|
||||||
(defn- send-query!
|
(defn- send-query!
|
||||||
"A simple helper for send and receive transit data on the penpot
|
"A simple helper for send and receive transit data on the penpot
|
||||||
|
@ -105,23 +105,44 @@
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
(rx/mapcat handle-response)))
|
(rx/mapcat handle-response)))
|
||||||
|
|
||||||
(defmethod query :export
|
(defn- send-export-command
|
||||||
|
[& {:keys [cmd params blob?]}]
|
||||||
|
(->> (http/send! {:method :post
|
||||||
|
:uri (u/join base-uri "export")
|
||||||
|
:body (http/transit-data (assoc params :cmd cmd))
|
||||||
|
:credentials "include"
|
||||||
|
:response-type (if blob? :blob :text)})
|
||||||
|
(rx/map http/conditional-decode-transit)
|
||||||
|
(rx/mapcat handle-response)))
|
||||||
|
|
||||||
|
(defmethod query :export-shapes-simple
|
||||||
[_ params]
|
[_ params]
|
||||||
|
(let [params (merge {:wait true} params)]
|
||||||
|
(->> (rx/of params)
|
||||||
|
(rx/mapcat #(send-export-command :cmd :export-shapes :params % :blob? false))
|
||||||
|
(rx/mapcat #(send-export-command :cmd :get-resource :params % :blob? true)))))
|
||||||
|
|
||||||
|
(defmethod query :export-shapes-multiple
|
||||||
|
[_ params]
|
||||||
|
(send-export-command :cmd :export-shapes :params params :blob? false))
|
||||||
|
|
||||||
|
(defmethod query :download-export-resource
|
||||||
|
[_ id]
|
||||||
|
(send-export-command :cmd :get-resource :params {:id id} :blob? true))
|
||||||
|
|
||||||
|
(defmethod query :export-frames
|
||||||
|
[_ exports]
|
||||||
|
(let [params {:uri (str base-uri)
|
||||||
|
:cmd :export-frames
|
||||||
|
:wait false
|
||||||
|
:exports exports}]
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri (u/join base-uri "export")
|
:uri (u/join base-uri "export")
|
||||||
:body (http/transit-data params)
|
:body (http/transit-data params)
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
:response-type :blob})
|
:response-type :blob})
|
||||||
(rx/mapcat handle-response)))
|
(rx/mapcat handle-response)
|
||||||
|
(rx/ignore))))
|
||||||
(defmethod query :export-frames
|
|
||||||
[_ params]
|
|
||||||
(->> (http/send! {:method :post
|
|
||||||
:uri (u/join base-uri "export-frames")
|
|
||||||
:body (http/transit-data params)
|
|
||||||
:credentials "include"
|
|
||||||
:response-type :blob})
|
|
||||||
(rx/mapcat handle-response)))
|
|
||||||
|
|
||||||
(derive :upload-file-media-object ::multipart-upload)
|
(derive :upload-file-media-object ::multipart-upload)
|
||||||
(derive :update-profile-photo ::multipart-upload)
|
(derive :update-profile-photo ::multipart-upload)
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.store
|
(ns app.main.store
|
||||||
(:require-macros [app.main.store])
|
(:require-macros [app.main.store])
|
||||||
(:require
|
(:require
|
||||||
|
[app.util.object :as obj]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[potok.core :as ptk]))
|
[potok.core :as ptk]))
|
||||||
|
@ -59,4 +60,10 @@
|
||||||
[& events]
|
[& events]
|
||||||
#(apply ptk/emit! state events))
|
#(apply ptk/emit! state events))
|
||||||
|
|
||||||
|
(defonce ongoing-tasks (l/atom #{}))
|
||||||
|
|
||||||
|
(add-watch ongoing-tasks ::ongoing-tasks
|
||||||
|
(fn [_ _ _ events]
|
||||||
|
(if (empty? events)
|
||||||
|
(obj/set! js/window "onbeforeunload" nil)
|
||||||
|
(obj/set! js/window "onbeforeunload" (constantly false)))))
|
||||||
|
|
211
frontend/src/app/main/ui/export.cljs
Normal file
211
frontend/src/app/main/ui/export.cljs
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
;; 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.export
|
||||||
|
"Assets exportation common components."
|
||||||
|
(:require
|
||||||
|
[app.common.colors :as clr]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.data.exports :as de]
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.icons :as i]
|
||||||
|
[app.main.ui.workspace.shapes :refer [shape-wrapper]]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.i18n :as i18n :refer [tr c]]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(mf/defc export-shapes-dialog
|
||||||
|
{::mf/register modal/components
|
||||||
|
::mf/register-as :export-shapes}
|
||||||
|
[{:keys [exports filename]}]
|
||||||
|
(let [lstate (mf/deref refs/export)
|
||||||
|
in-progress? (:in-progress lstate)
|
||||||
|
|
||||||
|
exports (mf/use-state exports)
|
||||||
|
|
||||||
|
all-exports (deref exports)
|
||||||
|
all-checked? (every? :enabled all-exports)
|
||||||
|
all-unchecked? (every? (complement :enabled) all-exports)
|
||||||
|
|
||||||
|
enabled-exports (into [] (filter :enabled) all-exports)
|
||||||
|
|
||||||
|
cancel-fn
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(st/emit! (modal/hide)))
|
||||||
|
|
||||||
|
accept-fn
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(st/emit! (modal/hide)
|
||||||
|
(de/request-multiple-export {:filename filename :exports enabled-exports})))
|
||||||
|
on-toggle-enabled
|
||||||
|
(fn [index]
|
||||||
|
(swap! exports update-in [index :enabled] not))
|
||||||
|
|
||||||
|
change-all
|
||||||
|
(fn [_]
|
||||||
|
(swap! exports (fn [exports]
|
||||||
|
(mapv #(assoc % :enabled (not all-checked?)) exports))))
|
||||||
|
]
|
||||||
|
[:div.modal-overlay
|
||||||
|
[:div.modal-container.export-shapes-dialog
|
||||||
|
{:class (when (empty? all-exports) "no-shapes")}
|
||||||
|
|
||||||
|
[:div.modal-header
|
||||||
|
[:div.modal-header-title
|
||||||
|
[:h2 (tr "dashboard.export-shapes.title")]]
|
||||||
|
|
||||||
|
[:div.modal-close-button
|
||||||
|
{:on-click cancel-fn} i/close]]
|
||||||
|
|
||||||
|
[:*
|
||||||
|
[:div.modal-content
|
||||||
|
(if (> (count all-exports) 0)
|
||||||
|
[:*
|
||||||
|
[:div.header
|
||||||
|
[:div.field.check {:on-click change-all}
|
||||||
|
(cond
|
||||||
|
all-checked? [:span i/checkbox-checked]
|
||||||
|
all-unchecked? [:span i/checkbox-unchecked]
|
||||||
|
:else [:span i/checkbox-intermediate])]
|
||||||
|
[:div.field.title (tr "dashboard.export-shapes.selected"
|
||||||
|
(c (count enabled-exports))
|
||||||
|
(c (count all-exports)))]]
|
||||||
|
|
||||||
|
[:div.body
|
||||||
|
(for [[index {:keys [shape suffix] :as export}] (d/enumerate @exports)]
|
||||||
|
(let [{:keys [x y width height]} (:selrect shape)]
|
||||||
|
[:div.row
|
||||||
|
[:div.field.check {:on-click #(on-toggle-enabled index)}
|
||||||
|
(if (:enabled export)
|
||||||
|
[:span.checked i/checkbox-checked]
|
||||||
|
[:span.unchecked i/checkbox-unchecked])]
|
||||||
|
|
||||||
|
[:div.field.image
|
||||||
|
[:svg {:view-box (dm/str x " " y " " width " " height)
|
||||||
|
:width 24
|
||||||
|
:height 20
|
||||||
|
:version "1.1"
|
||||||
|
:xmlns "http://www.w3.org/2000/svg"
|
||||||
|
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||||
|
;; Fix Chromium bug about color of html texts
|
||||||
|
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
|
||||||
|
:style {:-webkit-print-color-adjust :exact}}
|
||||||
|
|
||||||
|
[:& shape-wrapper {:shape shape}]]]
|
||||||
|
|
||||||
|
[:div.field.name (cond-> (:name shape) suffix (str suffix))]
|
||||||
|
[:div.field.scale (dm/str (* width (:scale export)) "x"
|
||||||
|
(* height (:scale export)) "px ")]
|
||||||
|
[:div.field.extension (-> export :type d/name str/upper)]]))]
|
||||||
|
|
||||||
|
[:div.modal-footer
|
||||||
|
[:div.action-buttons
|
||||||
|
[:input.cancel-button
|
||||||
|
{:type "button"
|
||||||
|
:value (tr "labels.cancel")
|
||||||
|
:on-click cancel-fn}]
|
||||||
|
|
||||||
|
[:input.accept-button.primary
|
||||||
|
{:class (dom/classnames
|
||||||
|
:btn-disabled (or in-progress? all-unchecked?))
|
||||||
|
:disabled (or in-progress? all-unchecked?)
|
||||||
|
:type "button"
|
||||||
|
:value (if in-progress?
|
||||||
|
(tr "workspace.options.exporting-object")
|
||||||
|
(tr "labels.export"))
|
||||||
|
:on-click (when-not in-progress? accept-fn)}]]]]
|
||||||
|
|
||||||
|
[:div.no-selection
|
||||||
|
[:img {:src "images/export-no-shapes.png" :border "0"}]
|
||||||
|
[:p (tr "dashboard.export-shapes.no-elements")]
|
||||||
|
[:p (tr "dashboard.export-shapes.how-to")]
|
||||||
|
[:p [:a {:target "_blank"
|
||||||
|
:href "https://help.penpot.app/user-guide/exporting/ "}
|
||||||
|
(tr "dashboard.export-shapes.how-to-link")]]])]]]]))
|
||||||
|
|
||||||
|
(mf/defc export-progress-widget
|
||||||
|
{::mf/wrap [mf/memo]}
|
||||||
|
[]
|
||||||
|
(let [state (mf/deref refs/export)
|
||||||
|
error? (:error state)
|
||||||
|
healthy? (:healthy? state)
|
||||||
|
detail-visible? (:detail-visible state)
|
||||||
|
widget-visible? (:widget-visible state)
|
||||||
|
progress (:progress state)
|
||||||
|
exports (:exports state)
|
||||||
|
total (count exports)
|
||||||
|
circ (* 2 Math/PI 12)
|
||||||
|
pct (- circ (* circ (/ progress total)))
|
||||||
|
|
||||||
|
pwidth (if error?
|
||||||
|
280
|
||||||
|
(/ (* progress 280) total))
|
||||||
|
color (cond
|
||||||
|
error? clr/danger
|
||||||
|
healthy? clr/primary
|
||||||
|
(not healthy?) clr/warning)
|
||||||
|
title (cond
|
||||||
|
error? (tr "workspace.options.exporting-object-error")
|
||||||
|
healthy? (tr "workspace.options.exporting-object")
|
||||||
|
(not healthy?) (tr "workspace.options.exporting-object-slow"))
|
||||||
|
|
||||||
|
retry-last-export
|
||||||
|
(mf/use-fn #(st/emit! (de/retry-last-export)))
|
||||||
|
|
||||||
|
toggle-detail-visibility
|
||||||
|
(mf/use-fn #(st/emit! (de/toggle-detail-visibililty)))]
|
||||||
|
|
||||||
|
[:*
|
||||||
|
(when widget-visible?
|
||||||
|
[:div.export-progress-widget {:on-click toggle-detail-visibility}
|
||||||
|
[:svg {:width "32" :height "32"}
|
||||||
|
[:circle {:r "12"
|
||||||
|
:cx "16"
|
||||||
|
:cy "16"
|
||||||
|
:fill "transparent"
|
||||||
|
:stroke clr/gray-40
|
||||||
|
:stroke-width "4"}]
|
||||||
|
[:circle {:r "12"
|
||||||
|
:cx "16"
|
||||||
|
:cy "16"
|
||||||
|
:fill "transparent"
|
||||||
|
:stroke color
|
||||||
|
:stroke-width "4"
|
||||||
|
:stroke-dasharray (dm/str circ " " circ)
|
||||||
|
:stroke-dashoffset pct
|
||||||
|
:transform "rotate(-90 16,16)"
|
||||||
|
:style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])
|
||||||
|
|
||||||
|
(when detail-visible?
|
||||||
|
[:div.export-progress-modal-overlay
|
||||||
|
[:div.export-progress-modal-container
|
||||||
|
[:div.export-progress-modal-header
|
||||||
|
[:p.export-progress-modal-title title]
|
||||||
|
(if error?
|
||||||
|
[:button.btn-secondary.retry {:on-click retry-last-export} (tr "workspace.options.retry")]
|
||||||
|
[:p.progress (dm/str progress " / " total)])
|
||||||
|
|
||||||
|
[:button.modal-close-button {:on-click toggle-detail-visibility} i/close]]
|
||||||
|
|
||||||
|
[:svg.progress-bar {:height 8 :width 280}
|
||||||
|
[:g
|
||||||
|
[:path {:d "M0 0 L280 0"
|
||||||
|
:stroke clr/gray-10
|
||||||
|
:stroke-width 30}]
|
||||||
|
[:path {:d (dm/str "M0 0 L280 0")
|
||||||
|
:stroke color
|
||||||
|
:stroke-width 30
|
||||||
|
:fill "transparent"
|
||||||
|
:stroke-dasharray 280
|
||||||
|
:stroke-dashoffset (- 280 pwidth)
|
||||||
|
:style {:transition "stroke-dashoffset 1s ease-in-out"}}]]]]])]))
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
(def chat (icon-xref :chat))
|
(def chat (icon-xref :chat))
|
||||||
(def checkbox-checked (icon-xref :checkbox-checked))
|
(def checkbox-checked (icon-xref :checkbox-checked))
|
||||||
(def checkbox-unchecked (icon-xref :checkbox-unchecked))
|
(def checkbox-unchecked (icon-xref :checkbox-unchecked))
|
||||||
|
(def checkbox-intermediate (icon-xref :checkbox-intermediate))
|
||||||
(def circle (icon-xref :circle))
|
(def circle (icon-xref :circle))
|
||||||
(def close (icon-xref :close))
|
(def close (icon-xref :close))
|
||||||
(def code (icon-xref :code))
|
(def code (icon-xref :code))
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.viewer.handoff.attributes
|
(ns app.main.ui.viewer.handoff.attributes
|
||||||
(:require
|
(:require
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]]
|
[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.fill :refer [fill-panel]]
|
||||||
[app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
|
[app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
|
||||||
|
@ -16,7 +17,6 @@
|
||||||
[app.main.ui.viewer.handoff.attributes.svg :refer [svg-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.attributes.text :refer [text-panel]]
|
||||||
[app.main.ui.viewer.handoff.exports :refer [exports]]
|
[app.main.ui.viewer.handoff.exports :refer [exports]]
|
||||||
[app.util.i18n :as i18n]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(def type->options
|
(def type->options
|
||||||
|
@ -31,8 +31,9 @@
|
||||||
|
|
||||||
(mf/defc attributes
|
(mf/defc attributes
|
||||||
[{:keys [page-id file-id shapes frame]}]
|
[{:keys [page-id file-id shapes frame]}]
|
||||||
(let [locale (mf/deref i18n/locale)
|
(let [shapes (hooks/use-equal-memo shapes)
|
||||||
shapes (->> shapes (map #(gsh/translate-to-frame % frame)))
|
shapes (mf/with-memo [shapes]
|
||||||
|
(mapv #(gsh/translate-to-frame % frame) shapes))
|
||||||
type (if (= (count shapes) 1) (-> shapes first :type) :multiple)
|
type (if (= (count shapes) 1) (-> shapes first :type) :multiple)
|
||||||
options (type->options type)]
|
options (type->options type)]
|
||||||
[:div.element-options
|
[:div.element-options
|
||||||
|
@ -47,10 +48,9 @@
|
||||||
:text text-panel
|
:text text-panel
|
||||||
:svg svg-panel)
|
:svg svg-panel)
|
||||||
{:shapes shapes
|
{:shapes shapes
|
||||||
:frame frame
|
:frame frame}])
|
||||||
:locale locale}])
|
|
||||||
(when-not (= :multiple type)
|
|
||||||
[:& exports
|
[:& exports
|
||||||
{:shape (first shapes)
|
{:shapes shapes
|
||||||
|
:type type
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:file-id file-id}])]))
|
:file-id file-id}]]))
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||||
[app.util.code-gen :as cg]
|
[app.util.code-gen :as cg]
|
||||||
[app.util.i18n :refer [t]]
|
[app.util.i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
@ -22,17 +22,17 @@
|
||||||
{:to-prop "filter"
|
{:to-prop "filter"
|
||||||
:format #(str/fmt "blur(%spx)" (:value %))}))
|
:format #(str/fmt "blur(%spx)" (:value %))}))
|
||||||
|
|
||||||
(mf/defc blur-panel [{:keys [shapes locale]}]
|
(mf/defc blur-panel [{:keys [shapes]}]
|
||||||
(let [shapes (->> shapes (filter has-blur?))]
|
(let [shapes (->> shapes (filter has-blur?))]
|
||||||
(when (seq shapes)
|
(when (seq shapes)
|
||||||
[:div.attributes-block
|
[:div.attributes-block
|
||||||
[:div.attributes-block-title
|
[:div.attributes-block-title
|
||||||
[:div.attributes-block-title-text (t locale "handoff.attributes.blur")]
|
[:div.attributes-block-title-text (tr "handoff.attributes.blur")]
|
||||||
(when (= (count shapes) 1)
|
(when (= (count shapes) 1)
|
||||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||||
|
|
||||||
(for [shape shapes]
|
(for [shape shapes]
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.blur.value")]
|
[:div.attributes-label (tr "handoff.attributes.blur.value")]
|
||||||
[:div.attributes-value (-> shape :blur :value) "px"]
|
[:div.attributes-value (-> shape :blur :value) "px"]
|
||||||
[:& copy-button {:data (copy-data shape)}]])])))
|
[:& copy-button {:data (copy-data shape)}]])])))
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||||
[app.util.color :as uc]
|
[app.util.color :as uc]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :refer [t] :as i18n]
|
[app.util.i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
@ -27,9 +27,7 @@
|
||||||
#(l/derived get-library st/state)))
|
#(l/derived get-library st/state)))
|
||||||
|
|
||||||
(mf/defc color-row [{:keys [color format copy-data on-change-format]}]
|
(mf/defc color-row [{:keys [color format copy-data on-change-format]}]
|
||||||
(let [locale (mf/deref i18n/locale)
|
(let [colors-library-ref (mf/use-memo
|
||||||
|
|
||||||
colors-library-ref (mf/use-memo
|
|
||||||
(mf/deps (:file-id color))
|
(mf/deps (:file-id color))
|
||||||
(make-colors-library-ref (:file-id color)))
|
(make-colors-library-ref (:file-id color)))
|
||||||
colors-library (mf/deref colors-library-ref)
|
colors-library (mf/deref colors-library-ref)
|
||||||
|
@ -60,13 +58,13 @@
|
||||||
(when-not (and on-change-format (:gradient color))
|
(when-not (and on-change-format (:gradient color))
|
||||||
[:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
|
[:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
|
||||||
[:option {:value "hex"}
|
[:option {:value "hex"}
|
||||||
(t locale "handoff.attributes.color.hex")]
|
(tr "handoff.attributes.color.hex")]
|
||||||
|
|
||||||
[:option {:value "rgba"}
|
[:option {:value "rgba"}
|
||||||
(t locale "handoff.attributes.color.rgba")]
|
(tr "handoff.attributes.color.rgba")]
|
||||||
|
|
||||||
[:option {:value "hsla"}
|
[:option {:value "hsla"}
|
||||||
(t locale "handoff.attributes.color.hsla")]])]
|
(tr "handoff.attributes.color.hsla")]])]
|
||||||
(when copy-data
|
(when copy-data
|
||||||
[:& copy-button {:data copy-data}])]))
|
[:& copy-button {:data copy-data}])]))
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
[app.common.spec.radius :as ctr]
|
[app.common.spec.radius :as ctr]
|
||||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||||
[app.util.code-gen :as cg]
|
[app.util.code-gen :as cg]
|
||||||
[app.util.i18n :refer [t]]
|
[app.util.i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
@ -32,41 +32,41 @@
|
||||||
(cg/generate-css-props shape properties params)))
|
(cg/generate-css-props shape properties params)))
|
||||||
|
|
||||||
(mf/defc layout-block
|
(mf/defc layout-block
|
||||||
[{:keys [shape locale]}]
|
[{:keys [shape]}]
|
||||||
(let [selrect (:selrect shape)
|
(let [selrect (:selrect shape)
|
||||||
{:keys [width height x y]} selrect]
|
{:keys [width height x y]} selrect]
|
||||||
[:*
|
[:*
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
|
[:div.attributes-label (tr "handoff.attributes.layout.width")]
|
||||||
[:div.attributes-value width "px"]
|
[:div.attributes-value width "px"]
|
||||||
[:& copy-button {:data (copy-data selrect :width)}]]
|
[:& copy-button {:data (copy-data selrect :width)}]]
|
||||||
|
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
|
[:div.attributes-label (tr "handoff.attributes.layout.height")]
|
||||||
[:div.attributes-value height "px"]
|
[:div.attributes-value height "px"]
|
||||||
[:& copy-button {:data (copy-data selrect :height)}]]
|
[:& copy-button {:data (copy-data selrect :height)}]]
|
||||||
|
|
||||||
(when (not= (:x shape) 0)
|
(when (not= (:x shape) 0)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
|
[:div.attributes-label (tr "handoff.attributes.layout.left")]
|
||||||
[:div.attributes-value x "px"]
|
[:div.attributes-value x "px"]
|
||||||
[:& copy-button {:data (copy-data selrect :x)}]])
|
[:& copy-button {:data (copy-data selrect :x)}]])
|
||||||
|
|
||||||
(when (not= (:y shape) 0)
|
(when (not= (:y shape) 0)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
|
[:div.attributes-label (tr "handoff.attributes.layout.top")]
|
||||||
[:div.attributes-value y "px"]
|
[:div.attributes-value y "px"]
|
||||||
[:& copy-button {:data (copy-data selrect :y)}]])
|
[:& copy-button {:data (copy-data selrect :y)}]])
|
||||||
|
|
||||||
(when (ctr/radius-1? shape)
|
(when (ctr/radius-1? shape)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
[:div.attributes-label (tr "handoff.attributes.layout.radius")]
|
||||||
[:div.attributes-value (:rx shape 0) "px"]
|
[:div.attributes-value (:rx shape 0) "px"]
|
||||||
[:& copy-button {:data (copy-data shape :rx)}]])
|
[:& copy-button {:data (copy-data shape :rx)}]])
|
||||||
|
|
||||||
(when (ctr/radius-4? shape)
|
(when (ctr/radius-4? shape)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
[:div.attributes-label (tr "handoff.attributes.layout.radius")]
|
||||||
[:div.attributes-value
|
[:div.attributes-value
|
||||||
(:r1 shape) ", "
|
(:r1 shape) ", "
|
||||||
(:r2 shape) ", "
|
(:r2 shape) ", "
|
||||||
|
@ -76,19 +76,18 @@
|
||||||
|
|
||||||
(when (not= (:rotation shape 0) 0)
|
(when (not= (:rotation shape 0) 0)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
[:div.attributes-label (tr "handoff.attributes.layout.rotation")]
|
||||||
[:div.attributes-value (:rotation shape) "deg"]
|
[:div.attributes-value (:rotation shape) "deg"]
|
||||||
[:& copy-button {:data (copy-data shape :rotation)}]])]))
|
[:& copy-button {:data (copy-data shape :rotation)}]])]))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc layout-panel
|
(mf/defc layout-panel
|
||||||
[{:keys [shapes locale]}]
|
[{:keys [shapes]}]
|
||||||
[:div.attributes-block
|
[:div.attributes-block
|
||||||
[:div.attributes-block-title
|
[:div.attributes-block-title
|
||||||
[:div.attributes-block-title-text (t locale "handoff.attributes.layout")]
|
[:div.attributes-block-title-text (tr "handoff.attributes.layout")]
|
||||||
(when (= (count shapes) 1)
|
(when (= (count shapes) 1)
|
||||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||||
|
|
||||||
(for [shape shapes]
|
(for [shape shapes]
|
||||||
[:& layout-block {:shape shape
|
[:& layout-block {:shape shape}])])
|
||||||
:locale locale}])])
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||||
[app.util.code-gen :as cg]
|
[app.util.code-gen :as cg]
|
||||||
[app.util.color :as uc]
|
[app.util.color :as uc]
|
||||||
[app.util.i18n :refer [t]]
|
[app.util.i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
:format #(uc/color->background (shape->color shape))}))
|
:format #(uc/color->background (shape->color shape))}))
|
||||||
|
|
||||||
(mf/defc stroke-block
|
(mf/defc stroke-block
|
||||||
[{:keys [shape locale]}]
|
[{:keys [shape]}]
|
||||||
(let [color-format (mf/use-state :hex)
|
(let [color-format (mf/use-state :hex)
|
||||||
color (shape->color shape)]
|
color (shape->color shape)]
|
||||||
[:*
|
[:*
|
||||||
|
@ -65,19 +65,19 @@
|
||||||
stroke-style (if (= stroke-style :svg) :solid stroke-style)
|
stroke-style (if (= stroke-style :svg) :solid stroke-style)
|
||||||
stroke-alignment (or stroke-alignment :center)]
|
stroke-alignment (or stroke-alignment :center)]
|
||||||
[:div.attributes-stroke-row
|
[:div.attributes-stroke-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
|
[:div.attributes-label (tr "handoff.attributes.stroke.width")]
|
||||||
[:div.attributes-value (:stroke-width shape) "px"]
|
[:div.attributes-value (:stroke-width shape) "px"]
|
||||||
[:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
|
[:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (tr))]
|
||||||
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (t locale))]
|
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (tr))]
|
||||||
[:& copy-button {:data (copy-stroke-data shape)}]])]))
|
[:& copy-button {:data (copy-stroke-data shape)}]])]))
|
||||||
|
|
||||||
(mf/defc stroke-panel
|
(mf/defc stroke-panel
|
||||||
[{:keys [shapes locale]}]
|
[{:keys [shapes]}]
|
||||||
(let [shapes (->> shapes (filter has-stroke?))]
|
(let [shapes (->> shapes (filter has-stroke?))]
|
||||||
(when (seq shapes)
|
(when (seq shapes)
|
||||||
[:div.attributes-block
|
[:div.attributes-block
|
||||||
[:div.attributes-block-title
|
[:div.attributes-block-title
|
||||||
[:div.attributes-block-title-text (t locale "handoff.attributes.stroke")]
|
[:div.attributes-block-title-text (tr "handoff.attributes.stroke")]
|
||||||
(when (= (count shapes) 1)
|
(when (= (count shapes) 1)
|
||||||
[:& copy-button {:data (copy-stroke-data (first shapes))}])]
|
[:& copy-button {:data (copy-stroke-data (first shapes))}])]
|
||||||
|
|
||||||
|
@ -85,8 +85,6 @@
|
||||||
(if (seq (:strokes shape))
|
(if (seq (:strokes shape))
|
||||||
(for [value (:strokes shape [])]
|
(for [value (:strokes shape [])]
|
||||||
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
||||||
:shape value
|
:shape value}])
|
||||||
:locale locale}])
|
|
||||||
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
||||||
:shape shape
|
:shape shape}]))])))
|
||||||
:locale locale}]))])))
|
|
||||||
|
|
|
@ -7,21 +7,56 @@
|
||||||
(ns app.main.ui.viewer.handoff.exports
|
(ns app.main.ui.viewer.handoff.exports
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.main.data.exports :as de]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.exports :as we]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr c]]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(mf/defc exports
|
(mf/defc exports
|
||||||
[{:keys [shape page-id file-id] :as props}]
|
{::mf/wrap [#(mf/memo % =)]}
|
||||||
(let [exports (mf/use-state (:exports shape []))
|
[{:keys [shapes page-id file-id type] :as props}]
|
||||||
|
(let [exports (mf/use-state [])
|
||||||
|
xstate (mf/deref refs/export)
|
||||||
|
vstate (mf/deref refs/viewer-data)
|
||||||
|
page (get-in vstate [:pages page-id])
|
||||||
|
filename (if (= (count shapes) 1)
|
||||||
|
(let [sname (-> shapes first :name)
|
||||||
|
suffix (-> @exports first :suffix)]
|
||||||
|
(cond-> sname
|
||||||
|
(and (= 1 (count @exports)) (some? suffix))
|
||||||
|
(str suffix)))
|
||||||
|
(:name page))
|
||||||
|
|
||||||
[on-download loading?] (we/use-download-export shape page-id file-id @exports)
|
in-progress? (:in-progress xstate)
|
||||||
|
|
||||||
|
on-download
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(if (= :multiple type)
|
||||||
|
(st/emit! (de/show-viewer-export-dialog {:shapes shapes
|
||||||
|
:exports @exports
|
||||||
|
:filename filename
|
||||||
|
:page-id page-id
|
||||||
|
:file-id file-id}))
|
||||||
|
|
||||||
|
;; In other all cases we only allowed to have a single
|
||||||
|
;; shape-id because multiple shape-ids are handled
|
||||||
|
;; separatelly by the export-modal.
|
||||||
|
(let [defaults {:page-id page-id
|
||||||
|
:file-id file-id
|
||||||
|
:name filename
|
||||||
|
:object-id (-> shapes first :id)}
|
||||||
|
exports (mapv #(merge % defaults) @exports)]
|
||||||
|
(if (= 1 (count exports))
|
||||||
|
(st/emit! (de/request-simple-export {:export (first exports)}))
|
||||||
|
(st/emit! (de/request-multiple-export {:exports exports :filename filename}))))))
|
||||||
|
|
||||||
add-export
|
add-export
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps shapes)
|
||||||
(fn []
|
(fn []
|
||||||
(let [xspec {:type :png
|
(let [xspec {:type :png
|
||||||
:suffix ""
|
:suffix ""
|
||||||
|
@ -30,7 +65,7 @@
|
||||||
|
|
||||||
delete-export
|
delete-export
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps shapes)
|
||||||
(fn [index]
|
(fn [index]
|
||||||
(swap! exports (fn [exports]
|
(swap! exports (fn [exports]
|
||||||
(let [[before after] (split-at index exports)]
|
(let [[before after] (split-at index exports)]
|
||||||
|
@ -38,7 +73,7 @@
|
||||||
|
|
||||||
on-scale-change
|
on-scale-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps shapes)
|
||||||
(fn [index event]
|
(fn [index event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
value (dom/get-value target)
|
value (dom/get-value target)
|
||||||
|
@ -47,7 +82,7 @@
|
||||||
|
|
||||||
on-suffix-change
|
on-suffix-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps shapes)
|
||||||
(fn [index event]
|
(fn [index event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
value (dom/get-value target)]
|
value (dom/get-value target)]
|
||||||
|
@ -55,7 +90,7 @@
|
||||||
|
|
||||||
on-type-change
|
on-type-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps shapes)
|
||||||
(fn [index event]
|
(fn [index event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
value (dom/get-value target)
|
value (dom/get-value target)
|
||||||
|
@ -63,9 +98,12 @@
|
||||||
(swap! exports assoc-in [index :type] value))))]
|
(swap! exports assoc-in [index :type] value))))]
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(mf/deps shape)
|
(mf/deps shapes)
|
||||||
(fn []
|
(fn []
|
||||||
(reset! exports (:exports shape []))))
|
(reset! exports (-> (mapv #(:exports % []) shapes)
|
||||||
|
flatten
|
||||||
|
distinct
|
||||||
|
vec))))
|
||||||
|
|
||||||
[:div.element-set.exports-options
|
[:div.element-set.exports-options
|
||||||
[:div.element-set-title
|
[:div.element-set-title
|
||||||
|
@ -99,10 +137,10 @@
|
||||||
i/minus]])
|
i/minus]])
|
||||||
|
|
||||||
[:div.btn-icon-dark.download-button
|
[:div.btn-icon-dark.download-button
|
||||||
{:on-click (when-not loading? on-download)
|
{:on-click (when-not in-progress? on-download)
|
||||||
:class (dom/classnames :btn-disabled loading?)
|
:class (dom/classnames :btn-disabled in-progress?)
|
||||||
:disabled loading?}
|
:disabled in-progress?}
|
||||||
(if loading?
|
(if in-progress?
|
||||||
(tr "workspace.options.exporting-object")
|
(tr "workspace.options.exporting-object")
|
||||||
(tr "workspace.options.export-object"))]])]))
|
(tr "workspace.options.export-object" (c (count shapes))))]])]))
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
[{:keys [frame page file selected]}]
|
[{:keys [frame page file selected]}]
|
||||||
(let [expanded (mf/use-state false)
|
(let [expanded (mf/use-state false)
|
||||||
section (mf/use-state :info #_:code)
|
section (mf/use-state :info #_:code)
|
||||||
|
|
||||||
shapes (resolve-shapes (:objects page) selected)
|
shapes (resolve-shapes (:objects page) selected)
|
||||||
|
|
||||||
first-shape (first shapes)
|
first-shape (first shapes)
|
||||||
|
|
||||||
selected-type (or (:type first-shape) :not-found)
|
selected-type (or (:type first-shape) :not-found)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main.ui.viewer.handoff.selection-feedback
|
(ns app.main.ui.viewer.handoff.selection-feedback
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.main.ui.measurements :refer [selection-guides size-display measurement]]
|
[app.main.ui.measurements :refer [selection-guides size-display measurement]]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
@ -21,10 +22,8 @@
|
||||||
|
|
||||||
(defn resolve-shapes
|
(defn resolve-shapes
|
||||||
[objects ids]
|
[objects ids]
|
||||||
(let [resolve-shape #(get objects %)]
|
(let [resolve-shape (d/getf objects)]
|
||||||
(into [] (comp (map resolve-shape)
|
(into [] (keep resolve-shape) ids)))
|
||||||
(filter some?))
|
|
||||||
ids)))
|
|
||||||
|
|
||||||
;; ------------------------------------------------
|
;; ------------------------------------------------
|
||||||
;; HELPERS
|
;; HELPERS
|
||||||
|
|
|
@ -12,12 +12,13 @@
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||||
|
[app.main.ui.export :refer [export-progress-widget]]
|
||||||
[app.main.ui.formats :as fmt]
|
[app.main.ui.formats :as fmt]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.main.ui.viewer.comments :refer [comments-menu]]
|
[app.main.ui.viewer.comments :refer [comments-menu]]
|
||||||
[app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
|
[app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(mf/defc zoom-widget
|
(mf/defc zoom-widget
|
||||||
|
@ -88,6 +89,7 @@
|
||||||
|
|
||||||
[:div.view-options])
|
[:div.view-options])
|
||||||
|
|
||||||
|
[:& export-progress-widget]
|
||||||
[:& zoom-widget
|
[:& zoom-widget
|
||||||
{:zoom zoom
|
{:zoom zoom
|
||||||
:on-increase (st/emitf dv/increase-zoom)
|
:on-increase (st/emitf dv/increase-zoom)
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
(ns app.main.ui.workspace.header
|
(ns app.main.ui.workspace.header
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
[app.main.data.messages :as dm]
|
[app.main.data.exports :as de]
|
||||||
|
[app.main.data.messages :as msg]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.shortcuts :as sc]
|
[app.main.data.workspace.shortcuts :as sc]
|
||||||
|
@ -17,6 +19,7 @@
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||||
|
[app.main.ui.export :refer [export-progress-widget]]
|
||||||
[app.main.ui.formats :as fmt]
|
[app.main.ui.formats :as fmt]
|
||||||
[app.main.ui.hooks.resize :as r]
|
[app.main.ui.hooks.resize :as r]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
|
@ -30,11 +33,12 @@
|
||||||
[potok.core :as ptk]
|
[potok.core :as ptk]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
;; --- Zoom Widget
|
|
||||||
|
|
||||||
(def workspace-persistence-ref
|
(def workspace-persistence-ref
|
||||||
(l/derived :workspace-persistence st/state))
|
(l/derived :workspace-persistence st/state))
|
||||||
|
|
||||||
|
;; --- Persistence state Widget
|
||||||
|
|
||||||
(mf/defc persistence-state-widget
|
(mf/defc persistence-state-widget
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[]
|
[]
|
||||||
|
@ -60,6 +64,8 @@
|
||||||
[:span.icon i/msg-warning]
|
[:span.icon i/msg-warning]
|
||||||
[:span.label (tr "workspace.header.save-error")]])]))
|
[:span.label (tr "workspace.header.save-error")]])]))
|
||||||
|
|
||||||
|
;; --- Zoom Widget
|
||||||
|
|
||||||
(mf/defc zoom-widget-workspace
|
(mf/defc zoom-widget-workspace
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [zoom
|
[{:keys [zoom
|
||||||
|
@ -150,6 +156,11 @@
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(reset! editing? true))
|
(reset! editing? true))
|
||||||
|
|
||||||
|
on-export-shapes
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [_]
|
||||||
|
(st/emit! (de/show-workspace-export-dialog))))
|
||||||
|
|
||||||
on-export-file
|
on-export-file
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps file team-id)
|
(mf/deps file team-id)
|
||||||
|
@ -178,21 +189,20 @@
|
||||||
(mf/deps file frames)
|
(mf/deps file frames)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(when (seq frames)
|
(when (seq frames)
|
||||||
(let [filename (str (:name file) ".pdf")
|
(let [filename (dm/str (:name file) ".pdf")
|
||||||
frame-ids (mapv :id frames)]
|
xform (comp (map :id)
|
||||||
(st/emit! (dm/info (tr "workspace.options.exporting-object")
|
(map (fn [id]
|
||||||
{:timeout nil}))
|
{:file-id (:id file)
|
||||||
(->> (rp/query! :export-frames
|
|
||||||
{:name (:name file)
|
|
||||||
:file-id (:id file)
|
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:frame-ids frame-ids})
|
:frame-id id})))]
|
||||||
|
(st/emit! (msg/info (tr "workspace.options.exporting-object") {:timeout nil}))
|
||||||
|
(->> (rp/query! :export-frames (into [] xform frames))
|
||||||
(rx/subs
|
(rx/subs
|
||||||
(fn [body]
|
(fn [body]
|
||||||
(dom/trigger-download filename body))
|
(dom/trigger-download filename body))
|
||||||
(fn [_error]
|
(fn [_error]
|
||||||
(st/emit! (dm/error (tr "errors.unexpected-error"))))
|
(st/emit! (msg/error (tr "errors.unexpected-error"))))
|
||||||
(st/emitf dm/hide)))))))
|
(st/emitf msg/hide)))))))
|
||||||
|
|
||||||
on-item-hover
|
on-item-hover
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
@ -269,6 +279,9 @@
|
||||||
[:span (tr "dashboard.remove-shared")]]
|
[:span (tr "dashboard.remove-shared")]]
|
||||||
[:li {:on-click on-add-shared}
|
[:li {:on-click on-add-shared}
|
||||||
[:span (tr "dashboard.add-shared")]])
|
[:span (tr "dashboard.add-shared")]])
|
||||||
|
[:li.export-file {:on-click on-export-shapes}
|
||||||
|
[:span (tr "dashboard.export-shapes")]
|
||||||
|
[:span.shortcut (sc/get-tooltip :export-shapes)]]
|
||||||
[:li.export-file {:on-click on-export-file}
|
[:li.export-file {:on-click on-export-file}
|
||||||
[:span (tr "dashboard.export-single")]]
|
[:span (tr "dashboard.export-single")]]
|
||||||
(when (seq frames)
|
(when (seq frames)
|
||||||
|
@ -429,6 +442,7 @@
|
||||||
[:div.right-area
|
[:div.right-area
|
||||||
[:div.options-section
|
[:div.options-section
|
||||||
[:& persistence-state-widget]
|
[:& persistence-state-widget]
|
||||||
|
[:& export-progress-widget]
|
||||||
[:button.document-history
|
[:button.document-history
|
||||||
{:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
|
{:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
|
||||||
:class (when (contains? layout :document-history) "selected")
|
:class (when (contains? layout :document-history) "selected")
|
||||||
|
|
|
@ -50,7 +50,9 @@
|
||||||
:bool [:& bool/options {:shape shape}]
|
:bool [:& bool/options {:shape shape}]
|
||||||
nil)
|
nil)
|
||||||
[:& exports-menu
|
[:& exports-menu
|
||||||
{:shape shape
|
{:ids [(:id shape)]
|
||||||
|
:values (select-keys shape [:exports])
|
||||||
|
:shape shape
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:file-id file-id}]])
|
:file-id file-id}]])
|
||||||
|
|
||||||
|
@ -82,7 +84,9 @@
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:shapes-with-children shapes-with-children}]
|
:shapes-with-children shapes-with-children}]
|
||||||
:else [:& multiple/options {:shapes-with-children shapes-with-children
|
:else [:& multiple/options {:shapes-with-children shapes-with-children
|
||||||
:shapes selected-shapes}])]]
|
:shapes selected-shapes
|
||||||
|
:page-id page-id
|
||||||
|
:file-id file-id}])]]
|
||||||
|
|
||||||
[:& tab-element {:id :prototype
|
[:& tab-element {:id :prototype
|
||||||
:title (tr "workspace.options.prototype")}
|
:title (tr "workspace.options.prototype")}
|
||||||
|
|
|
@ -7,121 +7,138 @@
|
||||||
(ns app.main.ui.workspace.sidebar.options.menus.exports
|
(ns app.main.ui.workspace.sidebar.options.menus.exports
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.main.data.messages :as dm]
|
[app.main.data.exports :as de]
|
||||||
[app.main.data.workspace :as udw]
|
[app.main.data.workspace.changes :as dch]
|
||||||
[app.main.data.workspace.persistence :as dwp]
|
[app.main.data.workspace.state-helpers :as wsh]
|
||||||
[app.main.repo :as rp]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.export]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :refer [tr c]]
|
||||||
[beicon.core :as rx]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(defn request-export
|
(def exports-attrs
|
||||||
[shape exports]
|
"Shape attrs that corresponds to exports. Used in other namespaces."
|
||||||
;; Force a persist before exporting otherwise the exported shape could be outdated
|
[:exports])
|
||||||
(st/emit! ::dwp/force-persist)
|
|
||||||
(rp/query!
|
|
||||||
:export
|
|
||||||
{:page-id (:page-id shape)
|
|
||||||
:file-id (:file-id shape)
|
|
||||||
:object-id (:id shape)
|
|
||||||
:name (:name shape)
|
|
||||||
:exports exports}))
|
|
||||||
|
|
||||||
(defn use-download-export
|
|
||||||
[shape page-id file-id exports]
|
|
||||||
(let [loading? (mf/use-state false)
|
|
||||||
|
|
||||||
filename (cond-> (:name shape)
|
|
||||||
(and (= (count exports) 1)
|
|
||||||
(not (empty (:suffix (first exports)))))
|
|
||||||
(str (:suffix (first exports))))
|
|
||||||
|
|
||||||
on-download-callback
|
|
||||||
(mf/use-callback
|
|
||||||
(mf/deps filename shape exports)
|
|
||||||
(fn [event]
|
|
||||||
(dom/prevent-default event)
|
|
||||||
(swap! loading? not)
|
|
||||||
(->> (request-export (assoc shape :page-id page-id :file-id file-id) exports)
|
|
||||||
(rx/subs
|
|
||||||
(fn [body]
|
|
||||||
(dom/trigger-download filename body))
|
|
||||||
(fn [_error]
|
|
||||||
(swap! loading? not)
|
|
||||||
(st/emit! (dm/error (tr "errors.unexpected-error"))))
|
|
||||||
(fn []
|
|
||||||
(swap! loading? not))))))]
|
|
||||||
[on-download-callback @loading?]))
|
|
||||||
|
|
||||||
(mf/defc exports-menu
|
(mf/defc exports-menu
|
||||||
[{:keys [shape page-id file-id] :as props}]
|
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "page-id" "file-id"]))]}
|
||||||
(let [exports (:exports shape [])
|
[{:keys [ids type values page-id file-id] :as props}]
|
||||||
|
(let [exports (:exports values [])
|
||||||
|
|
||||||
|
state (mf/deref refs/export)
|
||||||
|
in-progress? (:in-progress state)
|
||||||
|
|
||||||
|
filename (when (seqable? exports)
|
||||||
|
(let [shapes (wsh/lookup-shapes @st/state ids)
|
||||||
|
sname (-> shapes first :name)
|
||||||
|
suffix (-> exports first :suffix)]
|
||||||
|
(cond-> sname
|
||||||
|
(and (= 1 (count exports)) (some? suffix))
|
||||||
|
(str suffix))))
|
||||||
|
|
||||||
scale-enabled?
|
scale-enabled?
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [export]
|
(fn [export]
|
||||||
(#{:png :jpeg} (:type export))))
|
(#{:png :jpeg} (:type export))))
|
||||||
|
|
||||||
[on-download loading?] (use-download-export shape page-id file-id exports)
|
on-download
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps ids page-id file-id exports)
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(if (= :multiple type)
|
||||||
|
(st/emit! (de/show-workspace-export-dialog {:selected ids}))
|
||||||
|
|
||||||
|
;; In other all cases we only allowed to have a single
|
||||||
|
;; shape-id because multiple shape-ids are handled
|
||||||
|
;; separatelly by the export-modal.
|
||||||
|
(let [defaults {:page-id page-id
|
||||||
|
:file-id file-id
|
||||||
|
:name filename
|
||||||
|
:object-id (first ids)}
|
||||||
|
exports (mapv #(merge % defaults) exports)]
|
||||||
|
(if (= 1 (count exports))
|
||||||
|
(st/emit! (de/request-simple-export {:export (first exports)}))
|
||||||
|
(st/emit! (de/request-multiple-export {:exports exports :filename filename})))))))
|
||||||
|
|
||||||
|
;; TODO: maybe move to specific events for avoid to have this logic here?
|
||||||
add-export
|
add-export
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps ids)
|
||||||
(fn []
|
(fn []
|
||||||
(let [xspec {:type :png
|
(let [xspec {:type :png :suffix "" :scale 1}]
|
||||||
:suffix ""
|
(st/emit! (dch/update-shapes ids
|
||||||
:scale 1}]
|
(fn [shape]
|
||||||
(st/emit! (udw/update-shape (:id shape)
|
(assoc shape :exports (into [xspec] (:exports shape)))))))))
|
||||||
{:exports (conj exports xspec)})))))
|
|
||||||
delete-export
|
delete-export
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps ids)
|
||||||
(fn [index]
|
(fn [position]
|
||||||
(let [[before after] (split-at index exports)
|
(let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
|
||||||
exports (d/concat-vec before (rest after))]
|
(filterv (fn [[idx _]] (not= idx index)))
|
||||||
(st/emit! (udw/update-shape (:id shape)
|
(mapv second)))
|
||||||
{:exports exports})))))
|
|
||||||
|
remove (fn [shape] (update shape :exports remove-fill-by-index position))]
|
||||||
|
(st/emit! (dch/update-shapes ids remove)))))
|
||||||
|
|
||||||
on-scale-change
|
on-scale-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps ids)
|
||||||
(fn [index event]
|
(fn [index event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
value (dom/get-value target)
|
value (dom/get-value target)
|
||||||
value (d/parse-double value)
|
value (d/parse-double value)]
|
||||||
exports (assoc-in exports [index :scale] value)]
|
(st/emit! (dch/update-shapes ids
|
||||||
(st/emit! (udw/update-shape (:id shape)
|
(fn [shape]
|
||||||
{:exports exports})))))
|
(assoc-in shape [:exports index :scale] value)))))))
|
||||||
|
|
||||||
on-suffix-change
|
on-suffix-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps ids)
|
||||||
(fn [index event]
|
(fn [index event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
value (dom/get-value target)
|
value (dom/get-value target)]
|
||||||
exports (assoc-in exports [index :suffix] value)]
|
(st/emit! (dch/update-shapes ids
|
||||||
(st/emit! (udw/update-shape (:id shape)
|
(fn [shape]
|
||||||
{:exports exports})))))
|
(assoc-in shape [:exports index :suffix] value)))))))
|
||||||
|
|
||||||
on-type-change
|
on-type-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps ids)
|
||||||
(fn [index event]
|
(fn [index event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
value (dom/get-value target)
|
value (dom/get-value target)
|
||||||
value (keyword value)
|
value (keyword value)]
|
||||||
exports (assoc-in exports [index :type] value)]
|
(st/emit! (dch/update-shapes ids
|
||||||
(st/emit! (udw/update-shape (:id shape)
|
(fn [shape]
|
||||||
{:exports exports})))))]
|
(assoc-in shape [:exports index :type] value)))))))
|
||||||
|
|
||||||
|
on-remove-all
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps ids)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dch/update-shapes ids
|
||||||
|
(fn [shape]
|
||||||
|
(assoc shape :exports []))))))]
|
||||||
|
|
||||||
[:div.element-set.exports-options
|
[:div.element-set.exports-options
|
||||||
[:div.element-set-title
|
[:div.element-set-title
|
||||||
[:span (tr "workspace.options.export")]
|
[:span (tr (if (> (count ids) 1) "workspace.options.export-multiple" "workspace.options.export"))]
|
||||||
[:div.add-page {:on-click add-export} i/close]]
|
(when (not (= :multiple exports))
|
||||||
(when (seq exports)
|
[:div.add-page {:on-click add-export} i/close])]
|
||||||
|
|
||||||
|
(cond
|
||||||
|
(= :multiple exports)
|
||||||
|
[:div.element-set-options-group
|
||||||
|
[:div.element-set-label (tr "settings.multiple")]
|
||||||
|
[:div.element-set-actions
|
||||||
|
[:div.element-set-actions-button {:on-click on-remove-all}
|
||||||
|
i/minus]]]
|
||||||
|
|
||||||
|
(seq exports)
|
||||||
[:div.element-set-content
|
[:div.element-set-content
|
||||||
(for [[index export] (d/enumerate exports)]
|
(for [[index export] (d/enumerate exports)]
|
||||||
[:div.element-set-options-group
|
[:div.element-set-options-group
|
||||||
|
@ -146,14 +163,14 @@
|
||||||
[:option {:value "svg"} "SVG"]
|
[:option {:value "svg"} "SVG"]
|
||||||
[:option {:value "pdf"} "PDF"]]
|
[:option {:value "pdf"} "PDF"]]
|
||||||
[:div.delete-icon {:on-click (partial delete-export index)}
|
[:div.delete-icon {:on-click (partial delete-export index)}
|
||||||
i/minus]])
|
i/minus]])])
|
||||||
|
|
||||||
|
(when (or (= :multiple exports) (seq exports))
|
||||||
[:div.btn-icon-dark.download-button
|
[:div.btn-icon-dark.download-button
|
||||||
{:on-click (when-not loading? on-download)
|
{:on-click (when-not in-progress? on-download)
|
||||||
:class (dom/classnames
|
:class (dom/classnames
|
||||||
:btn-disabled loading?)
|
:btn-disabled in-progress?)
|
||||||
:disabled loading?}
|
:disabled in-progress?}
|
||||||
(if loading?
|
(if in-progress?
|
||||||
(tr "workspace.options.exporting-object")
|
(tr "workspace.options.exporting-object")
|
||||||
(tr "workspace.options.export-object"))]])]))
|
(tr "workspace.options.export-object" (c (count ids))))])]))
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
|
||||||
|
[app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-attrs exports-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
||||||
|
@ -36,7 +37,8 @@
|
||||||
:shadow :children
|
:shadow :children
|
||||||
:blur :children
|
:blur :children
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :children}
|
:text :children
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:group
|
:group
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -46,7 +48,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :children
|
:stroke :children
|
||||||
:text :children}
|
:text :children
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:path
|
:path
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -56,7 +59,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :ignore}
|
:text :ignore
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:text
|
:text
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -66,7 +70,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :text}
|
:text :text
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:image
|
:image
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -76,7 +81,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :ignore
|
:stroke :ignore
|
||||||
:text :ignore}
|
:text :ignore
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:rect
|
:rect
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -86,7 +92,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :ignore}
|
:text :ignore
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:circle
|
:circle
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -96,7 +103,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :ignore}
|
:text :ignore
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:svg-raw
|
:svg-raw
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -106,7 +114,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :ignore}
|
:text :ignore
|
||||||
|
:exports :shape}
|
||||||
|
|
||||||
:bool
|
:bool
|
||||||
{:measure :shape
|
{:measure :shape
|
||||||
|
@ -116,7 +125,8 @@
|
||||||
:shadow :shape
|
:shadow :shape
|
||||||
:blur :shape
|
:blur :shape
|
||||||
:stroke :shape
|
:stroke :shape
|
||||||
:text :ignore}})
|
:text :ignore
|
||||||
|
:exports :shape}})
|
||||||
|
|
||||||
(def group->attrs
|
(def group->attrs
|
||||||
{:measure measure-attrs
|
{:measure measure-attrs
|
||||||
|
@ -126,7 +136,8 @@
|
||||||
:shadow shadow-attrs
|
:shadow shadow-attrs
|
||||||
:blur blur-attrs
|
:blur blur-attrs
|
||||||
:stroke stroke-attrs
|
:stroke stroke-attrs
|
||||||
:text ot/attrs})
|
:text ot/attrs
|
||||||
|
:exports exports-attrs})
|
||||||
|
|
||||||
(def shadow-keys [:style :color :offset-x :offset-y :blur :spread])
|
(def shadow-keys [:style :color :offset-x :offset-y :blur :spread])
|
||||||
|
|
||||||
|
@ -211,11 +222,13 @@
|
||||||
(dissoc :content)))
|
(dissoc :content)))
|
||||||
|
|
||||||
(mf/defc options
|
(mf/defc options
|
||||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes" "shapes-with-children"]))]
|
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes" "shapes-with-children" "page-id" "file-id"]))]
|
||||||
::mf/wrap-props false}
|
::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [shapes (unchecked-get props "shapes")
|
(let [shapes (unchecked-get props "shapes")
|
||||||
shapes-with-children (unchecked-get props "shapes-with-children")
|
shapes-with-children (unchecked-get props "shapes-with-children")
|
||||||
|
page-id (unchecked-get props "page-id")
|
||||||
|
file-id (unchecked-get props "file-id")
|
||||||
objects (->> shapes-with-children (group-by :id) (d/mapm (fn [_ v] (first v))))
|
objects (->> shapes-with-children (group-by :id) (d/mapm (fn [_ v] (first v))))
|
||||||
show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes)
|
show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes)
|
||||||
|
|
||||||
|
@ -235,7 +248,8 @@
|
||||||
shadow-ids shadow-values
|
shadow-ids shadow-values
|
||||||
blur-ids blur-values
|
blur-ids blur-values
|
||||||
stroke-ids stroke-values
|
stroke-ids stroke-values
|
||||||
text-ids text-values]
|
text-ids text-values
|
||||||
|
exports-ids exports-values]
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
(mf/deps objects-no-measures)
|
(mf/deps objects-no-measures)
|
||||||
(fn []
|
(fn []
|
||||||
|
@ -248,7 +262,8 @@
|
||||||
(get-attrs shapes objects-no-measures :shadow)
|
(get-attrs shapes objects-no-measures :shadow)
|
||||||
(get-attrs shapes objects-no-measures :blur)
|
(get-attrs shapes objects-no-measures :blur)
|
||||||
(get-attrs shapes objects-no-measures :stroke)
|
(get-attrs shapes objects-no-measures :stroke)
|
||||||
(get-attrs shapes objects-no-measures :text)])))]
|
(get-attrs shapes objects-no-measures :text)
|
||||||
|
(get-attrs shapes objects-no-measures :exports)])))]
|
||||||
|
|
||||||
[:div.options
|
[:div.options
|
||||||
(when-not (empty? measure-ids)
|
(when-not (empty? measure-ids)
|
||||||
|
@ -273,4 +288,7 @@
|
||||||
[:& blur-menu {:type type :ids blur-ids :values blur-values}])
|
[:& blur-menu {:type type :ids blur-ids :values blur-values}])
|
||||||
|
|
||||||
(when-not (empty? text-ids)
|
(when-not (empty? text-ids)
|
||||||
[:& ot/text-menu {:type type :ids text-ids :values text-values}])]))
|
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
|
||||||
|
|
||||||
|
(when-not (empty? exports-ids)
|
||||||
|
[:& exports-menu {:type type :ids exports-ids :values exports-values :page-id page-id :file-id file-id}])]))
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
(defn transit-data
|
(defn transit-data
|
||||||
[data]
|
[data]
|
||||||
(reify IBodyData
|
(reify IBodyData
|
||||||
(-get-body-data [_] (t/encode-str data))
|
(-get-body-data [_] (t/encode-str data {:type :json-verbose}))
|
||||||
(-update-headers [_ headers]
|
(-update-headers [_ headers]
|
||||||
(assoc headers "content-type" "application/transit+json"))))
|
(assoc headers "content-type" "application/transit+json"))))
|
||||||
|
|
||||||
|
|
119
frontend/src/app/util/websocket.cljs
Normal file
119
frontend/src/app/util/websocket.cljs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
;; 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.util.websocket
|
||||||
|
"A interface to webworkers exposed functionality."
|
||||||
|
(:require
|
||||||
|
[app.common.transit :as t]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[goog.events :as ev])
|
||||||
|
(:import
|
||||||
|
goog.net.WebSocket
|
||||||
|
goog.net.WebSocket.EventType))
|
||||||
|
|
||||||
|
(defprotocol IWebSocket
|
||||||
|
(-stream [_] "Retrieve the message stream")
|
||||||
|
(-send [_ message] "send a message")
|
||||||
|
(-close [_] "close websocket")
|
||||||
|
(-open? [_] "check if the channel is open"))
|
||||||
|
|
||||||
|
(defn create
|
||||||
|
[uri]
|
||||||
|
(let [sb (rx/subject)
|
||||||
|
ws (WebSocket. #js {:autoReconnect true})
|
||||||
|
data (atom {})
|
||||||
|
lk1 (ev/listen ws EventType.MESSAGE
|
||||||
|
#(rx/push! sb {:type :message :payload (.-message %)}))
|
||||||
|
lk2 (ev/listen ws EventType.ERROR
|
||||||
|
#(rx/push! sb {:type :error :payload %}))
|
||||||
|
lk3 (ev/listen ws EventType.OPENED
|
||||||
|
#(rx/push! sb {:type :opened :payload %}))]
|
||||||
|
|
||||||
|
(.open ws (str uri))
|
||||||
|
(reify
|
||||||
|
IDeref
|
||||||
|
(-deref [_] (-deref data))
|
||||||
|
|
||||||
|
IReset
|
||||||
|
(-reset! [_ newval]
|
||||||
|
(-reset! data newval))
|
||||||
|
|
||||||
|
ISwap
|
||||||
|
(-swap! [_ f]
|
||||||
|
(-swap! data f))
|
||||||
|
(-swap! [_ f x]
|
||||||
|
(-swap! data f x))
|
||||||
|
(-swap! [_ f x y]
|
||||||
|
(-swap! data f x y))
|
||||||
|
(-swap! [_ f x y more]
|
||||||
|
(-swap! data f x y more))
|
||||||
|
|
||||||
|
IWatchable
|
||||||
|
(-notify-watches [_ oldval newval]
|
||||||
|
(-notify-watches data oldval newval))
|
||||||
|
|
||||||
|
(-add-watch [_ key f]
|
||||||
|
(-add-watch data key f))
|
||||||
|
|
||||||
|
(-remove-watch [_ key]
|
||||||
|
(-remove-watch data key))
|
||||||
|
|
||||||
|
IHash
|
||||||
|
(-hash [_] (goog/getUid ws))
|
||||||
|
|
||||||
|
IWebSocket
|
||||||
|
(-stream [_]
|
||||||
|
(->> sb
|
||||||
|
(rx/map (fn [{:keys [type payload] :as message}]
|
||||||
|
(cond-> message
|
||||||
|
(= :message type)
|
||||||
|
(assoc :payload (t/decode-str payload)))))))
|
||||||
|
|
||||||
|
(-send [_ msg]
|
||||||
|
(when (.isOpen ^js ws)
|
||||||
|
(.send ^js ws msg)))
|
||||||
|
|
||||||
|
(-open? [_]
|
||||||
|
(.isOpen ^js ws))
|
||||||
|
|
||||||
|
(-close [_]
|
||||||
|
(rx/end! sb)
|
||||||
|
(ev/unlistenByKey lk1)
|
||||||
|
(ev/unlistenByKey lk2)
|
||||||
|
(ev/unlistenByKey lk3)
|
||||||
|
(.close ^js ws)
|
||||||
|
(.dispose ^js ws)))))
|
||||||
|
|
||||||
|
(defn message-event?
|
||||||
|
^boolean
|
||||||
|
[msg]
|
||||||
|
(= (:type msg) :message))
|
||||||
|
|
||||||
|
(defn error-event?
|
||||||
|
^boolean
|
||||||
|
[msg]
|
||||||
|
(= (:type msg) :error))
|
||||||
|
|
||||||
|
(defn opened-event?
|
||||||
|
^boolean
|
||||||
|
[msg]
|
||||||
|
(= (:type msg) :opened))
|
||||||
|
|
||||||
|
(defn send!
|
||||||
|
[ws msg]
|
||||||
|
(-send ws (t/encode-str msg)))
|
||||||
|
|
||||||
|
(defn close!
|
||||||
|
[ws]
|
||||||
|
(-close ws))
|
||||||
|
|
||||||
|
(defn open?
|
||||||
|
[ws]
|
||||||
|
(-open? ws))
|
||||||
|
|
||||||
|
(defn get-rcv-stream
|
||||||
|
[ws]
|
||||||
|
(-stream ws))
|
|
@ -1,57 +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.util.websockets
|
|
||||||
"A interface to webworkers exposed functionality."
|
|
||||||
(:require
|
|
||||||
[app.common.transit :as t]
|
|
||||||
[beicon.core :as rx]
|
|
||||||
[goog.events :as ev])
|
|
||||||
(:import
|
|
||||||
goog.net.WebSocket
|
|
||||||
goog.net.WebSocket.EventType))
|
|
||||||
|
|
||||||
(defprotocol IWebSocket
|
|
||||||
(-stream [_] "Retrieve the message stream")
|
|
||||||
(-send [_ message] "send a message")
|
|
||||||
(-close [_] "close websocket"))
|
|
||||||
|
|
||||||
(defn open
|
|
||||||
[uri]
|
|
||||||
(let [sb (rx/subject)
|
|
||||||
ws (WebSocket. #js {:autoReconnect true})
|
|
||||||
lk1 (ev/listen ws EventType.MESSAGE
|
|
||||||
#(rx/push! sb {:type :message :payload (.-message %)}))
|
|
||||||
lk2 (ev/listen ws EventType.ERROR
|
|
||||||
#(rx/push! sb {:type :error :payload %}))
|
|
||||||
lk3 (ev/listen ws EventType.OPENED
|
|
||||||
#(rx/push! sb {:type :opened :payload %}))]
|
|
||||||
(.open ws (str uri))
|
|
||||||
(reify
|
|
||||||
cljs.core/IDeref
|
|
||||||
(-deref [_] ws)
|
|
||||||
|
|
||||||
IWebSocket
|
|
||||||
(-stream [_] sb)
|
|
||||||
(-send [_ msg]
|
|
||||||
(when (.isOpen ^js ws)
|
|
||||||
(.send ^js ws msg)))
|
|
||||||
(-close [_]
|
|
||||||
(rx/end! sb)
|
|
||||||
(ev/unlistenByKey lk1)
|
|
||||||
(ev/unlistenByKey lk2)
|
|
||||||
(ev/unlistenByKey lk3)
|
|
||||||
(.close ^js ws)
|
|
||||||
(.dispose ^js ws)))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn message?
|
|
||||||
[msg]
|
|
||||||
(= (:type msg) :message))
|
|
||||||
|
|
||||||
(defn send!
|
|
||||||
[ws msg]
|
|
||||||
(-send ws (t/encode-str msg)))
|
|
|
@ -256,6 +256,10 @@ msgstr ""
|
||||||
"Oh no! You have no files yet! If you want to try with some templates go "
|
"Oh no! You have no files yet! If you want to try with some templates go "
|
||||||
"to [Libraries & templates](https://penpot.app/libraries-templates.html)"
|
"to [Libraries & templates](https://penpot.app/libraries-templates.html)"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "dashboard.export-shapes"
|
||||||
|
msgstr "Exportar"
|
||||||
|
|
||||||
msgid "dashboard.export-frames"
|
msgid "dashboard.export-frames"
|
||||||
msgstr "Export artboards to PDF..."
|
msgstr "Export artboards to PDF..."
|
||||||
|
|
||||||
|
@ -300,6 +304,26 @@ msgstr "Include shared library assets in file libraries"
|
||||||
msgid "dashboard.export.title"
|
msgid "dashboard.export.title"
|
||||||
msgstr "Export files"
|
msgstr "Export files"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.title"
|
||||||
|
msgstr "Export selection"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.selected"
|
||||||
|
msgstr "%s de %s elements selected"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.no-elements"
|
||||||
|
msgstr "There are no elements with export settings."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.how-to"
|
||||||
|
msgstr "You can add export settings to elements from the design properties (at the bottom of the right sidebar)."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.how-to-link"
|
||||||
|
msgstr "Info how to set exports at Penpot."
|
||||||
|
|
||||||
msgid "dashboard.fonts.deleted-placeholder"
|
msgid "dashboard.fonts.deleted-placeholder"
|
||||||
msgstr "Font deleted"
|
msgstr "Font deleted"
|
||||||
|
|
||||||
|
@ -2480,18 +2504,40 @@ msgstr "Design"
|
||||||
msgid "workspace.options.export"
|
msgid "workspace.options.export"
|
||||||
msgstr "Export"
|
msgstr "Export"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
||||||
|
msgid "workspace.options.export-multiple"
|
||||||
|
msgstr "Export selection"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
||||||
msgid "workspace.options.export-object"
|
msgid "workspace.options.export-object"
|
||||||
msgstr "Export"
|
msgid_plural "workspace.options.export-object"
|
||||||
|
msgstr[0] "Export 1 element"
|
||||||
|
msgstr[1] "Export %s elements"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
msgid "workspace.options.export.suffix"
|
msgid "workspace.options.export.suffix"
|
||||||
msgstr "Suffix"
|
msgstr "Suffix"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
msgid "workspace.options.exporting-object"
|
msgid "workspace.options.exporting-object"
|
||||||
msgstr "Exporting…"
|
msgstr "Exporting…"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.exporting-object-slow"
|
||||||
|
msgstr "Export unexpectedly slow"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.exporting-object-error"
|
||||||
|
msgstr "Export failed"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.exporting-object-slow"
|
||||||
|
msgstr "Export unexpectedly slow"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.retry"
|
||||||
|
msgstr "Retry"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
|
||||||
msgid "workspace.options.fill"
|
msgid "workspace.options.fill"
|
||||||
msgstr "Fill"
|
msgstr "Fill"
|
||||||
|
|
|
@ -260,6 +260,10 @@ msgstr ""
|
||||||
"¡Oh, no! ¡Aún no tienes archivos! Si quieres probar con alguna plantilla ve a "
|
"¡Oh, no! ¡Aún no tienes archivos! Si quieres probar con alguna plantilla ve a "
|
||||||
"[Bibliotecas y plantillas](https://penpot.app/libraries-templates.html)"
|
"[Bibliotecas y plantillas](https://penpot.app/libraries-templates.html)"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "dashboard.export-shapes"
|
||||||
|
msgstr "Exportar"
|
||||||
|
|
||||||
msgid "dashboard.export-frames"
|
msgid "dashboard.export-frames"
|
||||||
msgstr "Exportar tableros a PDF..."
|
msgstr "Exportar tableros a PDF..."
|
||||||
|
|
||||||
|
@ -304,6 +308,26 @@ msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
|
||||||
msgid "dashboard.export.title"
|
msgid "dashboard.export.title"
|
||||||
msgstr "Exportar ficheros"
|
msgstr "Exportar ficheros"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.title"
|
||||||
|
msgstr "Exportar selección"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.selected"
|
||||||
|
msgstr "%s de %s elementos seleccionados"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.no-elements"
|
||||||
|
msgstr "No hay elementos con configuraciones de exportación."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.how-to"
|
||||||
|
msgstr " Puedes añadir configuraciones de exportación a elementos desde las propiedades de diseño (al final del lateral derecho)."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
|
msgid "dashboard.export-shapes.how-to-link"
|
||||||
|
msgstr "Información sobre cómo configurar exportaciones en Penpot."
|
||||||
|
|
||||||
msgid "dashboard.fonts.deleted-placeholder"
|
msgid "dashboard.fonts.deleted-placeholder"
|
||||||
msgstr "Fuente eliminada."
|
msgstr "Fuente eliminada."
|
||||||
|
|
||||||
|
@ -2496,17 +2520,35 @@ msgstr "Diseño"
|
||||||
msgid "workspace.options.export"
|
msgid "workspace.options.export"
|
||||||
msgstr "Exportar"
|
msgstr "Exportar"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
||||||
|
msgid "workspace.options.export-multiple"
|
||||||
|
msgstr "Exportar selección"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
||||||
msgid "workspace.options.export-object"
|
msgid "workspace.options.export-object"
|
||||||
msgstr "Exportar"
|
msgid_plural "workspace.options.export-object"
|
||||||
|
msgstr[0] "Exportar 1 elemento"
|
||||||
|
msgstr[1] "Exportar %s elementos"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
|
||||||
msgid "workspace.options.export.suffix"
|
msgid "workspace.options.export.suffix"
|
||||||
msgstr "Sufijo"
|
msgstr "Sufijo"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
msgid "workspace.options.exporting-object"
|
msgid "workspace.options.exporting-object"
|
||||||
msgstr "Exportando"
|
msgstr "Exportando..."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.exporting-object-slow"
|
||||||
|
msgstr "Exportación lenta"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.retry"
|
||||||
|
msgstr "Reintentar"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs
|
||||||
|
msgid "workspace.options.exporting-object-error"
|
||||||
|
msgstr "Exportación fallida"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
|
||||||
msgid "workspace.options.fill"
|
msgid "workspace.options.fill"
|
||||||
|
|
Loading…
Add table
Reference in a new issue