mirror of
https://github.com/penpot/penpot.git
synced 2025-03-18 10:41:29 -05: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 canvas "#E8E9EA")
|
||||
(def default-layout "#DE4762")
|
||||
(def gray-10 "#E3E3E3")
|
||||
(def gray-20 "#B1B2B5")
|
||||
(def gray-30 "#7B7D85")
|
||||
(def gray-40 "#64666A")
|
||||
(def info "#59B9E2")
|
||||
(def test "#fabada")
|
||||
(def white "#FFFFFF")
|
||||
(def primary "#31EFB8")
|
||||
|
||||
(def danger "#E65244")
|
||||
(def warning "#FC8802")
|
||||
|
|
|
@ -65,7 +65,8 @@
|
|||
:masked-group? :mask-group
|
||||
:constraints-h :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
|
||||
(def editable-attrs
|
||||
|
@ -99,7 +100,9 @@
|
|||
:stroke-opacity
|
||||
:stroke-color-gradient
|
||||
:stroke-cap-start
|
||||
:stroke-cap-end}
|
||||
:stroke-cap-end
|
||||
|
||||
:exports}
|
||||
|
||||
:group #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -120,7 +123,9 @@
|
|||
|
||||
:shadow
|
||||
|
||||
:blur}
|
||||
:blur
|
||||
|
||||
:exports}
|
||||
|
||||
:rect #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -147,7 +152,7 @@
|
|||
:fill-color-ref-id
|
||||
:fill-color-ref-file
|
||||
:fill-color-gradient
|
||||
|
||||
|
||||
:strokes
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
|
@ -162,7 +167,9 @@
|
|||
|
||||
:shadow
|
||||
|
||||
:blur}
|
||||
:blur
|
||||
|
||||
:exports}
|
||||
|
||||
:circle #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -202,7 +209,9 @@
|
|||
|
||||
:shadow
|
||||
|
||||
:blur}
|
||||
:blur
|
||||
|
||||
:exports}
|
||||
|
||||
:path #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -242,7 +251,9 @@
|
|||
|
||||
:shadow
|
||||
|
||||
:blur}
|
||||
:blur
|
||||
|
||||
:exports}
|
||||
|
||||
:text #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -305,7 +316,9 @@
|
|||
|
||||
:text-transform
|
||||
|
||||
:grow-type}
|
||||
:grow-type
|
||||
|
||||
:exports}
|
||||
|
||||
:image #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -328,7 +341,9 @@
|
|||
|
||||
:shadow
|
||||
|
||||
:blur}
|
||||
:blur
|
||||
|
||||
:exports}
|
||||
|
||||
:svg-raw #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -370,7 +385,9 @@
|
|||
|
||||
:shadow
|
||||
|
||||
:blur}
|
||||
:blur
|
||||
|
||||
:exports}
|
||||
|
||||
:bool #{:proportion-lock
|
||||
:width :height
|
||||
|
@ -410,5 +427,7 @@
|
|||
|
||||
: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;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
width: 30px;
|
||||
|
||||
svg {
|
||||
transform: rotate(45deg);
|
||||
|
@ -140,7 +139,7 @@
|
|||
.modal-footer {
|
||||
display: flex;
|
||||
height: 63px;
|
||||
padding: 0px 16px;
|
||||
padding: 0px 18px;
|
||||
border-top: 1px solid $color-gray-10;
|
||||
|
||||
.action-buttons {
|
||||
|
@ -235,12 +234,17 @@
|
|||
}
|
||||
|
||||
.import-dialog,
|
||||
.export-dialog {
|
||||
.export-dialog,
|
||||
.export-shapes-dialog {
|
||||
background-color: $color-white;
|
||||
border: 1px solid $color-gray-20;
|
||||
width: 30rem;
|
||||
min-height: 14rem;
|
||||
|
||||
&.no-shapes {
|
||||
width: 39rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $fs14;
|
||||
color: $color-black;
|
||||
|
@ -259,9 +263,9 @@
|
|||
border: 1px solid $color-gray-20;
|
||||
background: $color-white;
|
||||
border-radius: 3px;
|
||||
padding: 0.3rem 1.25rem;
|
||||
padding: 0.5rem 2.25rem;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
margin-right: 18px;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-20;
|
||||
|
@ -274,7 +278,7 @@
|
|||
border: 1px solid $color-primary;
|
||||
color: $color-black;
|
||||
cursor: pointer;
|
||||
padding: 0.3rem 1.25rem;
|
||||
padding: 0.5rem 2.25rem;
|
||||
|
||||
&[disabled] {
|
||||
border: 1px solid #e3e3e3;
|
||||
|
@ -295,7 +299,7 @@
|
|||
padding-left: 2rem;
|
||||
|
||||
h2 {
|
||||
font-size: $fs14;
|
||||
font-size: $fs18;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,11 +307,6 @@
|
|||
padding: 1rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
margin: 0.75rem 1rem;
|
||||
user-select: none;
|
||||
|
@ -515,6 +514,38 @@
|
|||
&.selected {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
display: flex;
|
||||
margin-left: 0px;
|
||||
margin-right: 10px;
|
||||
/* border: 1px solid red; */
|
||||
width: 150px;
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.errors]
|
||||
[app.main.sentry :as sentry]
|
||||
[app.main.store :as st]
|
||||
|
@ -58,7 +59,15 @@
|
|||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(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?
|
||||
(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.main.data.events :as ev]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.router :as rt]
|
||||
|
@ -167,7 +168,8 @@
|
|||
(when (is-authenticated? profile)
|
||||
(->> (rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(get-redirect-event))
|
||||
(get-redirect-event)
|
||||
(ws/initialize))
|
||||
(rx/observe-on :async)))))))
|
||||
|
||||
(s/def ::invitation-token ::us/not-empty-string)
|
||||
|
@ -268,10 +270,12 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
;; NOTE: We need the `effect` of the current event to be
|
||||
;; executed before the redirect.
|
||||
(->> (rx/of (rt/nav :auth-login))
|
||||
(rx/observe-on :async)))
|
||||
(rx/merge
|
||||
;; NOTE: We need the `effect` of the current event to be
|
||||
;; executed before the redirect.
|
||||
(->> (rx/of (rt/nav :auth-login))
|
||||
(rx/observe-on :async))
|
||||
(rx/of (ws/finalize))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(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/map deref)
|
||||
(rx/mapcat (fn [bundle]
|
||||
(rx/merge
|
||||
(rx/of (dwn/initialize file-id)
|
||||
(dwp/initialize-file-persistence file-id)
|
||||
(dwc/initialize-indices bundle))
|
||||
(let [team-id (-> bundle :project :team-id)]
|
||||
(rx/merge
|
||||
(rx/of (dwn/initialize team-id file-id)
|
||||
(dwp/initialize-file-persistence file-id)
|
||||
(dwc/initialize-indices bundle))
|
||||
|
||||
(->> stream
|
||||
(rx/filter #(= ::dwc/index-initialized %))
|
||||
(rx/take 1)
|
||||
(rx/map #(file-initialized bundle)))))))))
|
||||
(->> stream
|
||||
(rx/filter #(= ::dwc/index-initialized %))
|
||||
(rx/take 1)
|
||||
(rx/map #(file-initialized bundle))))))))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
|
@ -982,7 +983,7 @@
|
|||
pages (get-in state [:workspace-data
|
||||
:pages-index])
|
||||
file-thumbnails (->> pages
|
||||
(mapcat #(extract-file-thumbnails-from-page state selected %)))]
|
||||
(mapcat #(extract-file-thumbnails-from-page state selected %)))]
|
||||
(rx/concat
|
||||
(rx/from
|
||||
(for [ft file-thumbnails]
|
||||
|
|
|
@ -7,18 +7,15 @@
|
|||
(ns app.main.data.workspace.notifications
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.spec :as us]
|
||||
[app.common.spec.change :as spec.change]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.time :as dt]
|
||||
[app.util.websockets :as ws]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
|
@ -30,103 +27,91 @@
|
|||
(declare handle-file-change)
|
||||
(declare handle-library-change)
|
||||
(declare handle-pointer-send)
|
||||
(declare send-keepalive)
|
||||
|
||||
(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"))))
|
||||
(declare handle-export-update)
|
||||
|
||||
(defn initialize
|
||||
[file-id]
|
||||
[team-id file-id]
|
||||
(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
|
||||
(watch [_ state stream]
|
||||
(let [wsession (get-in state [:ws file-id])
|
||||
stoper (->> 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)))
|
||||
(let [subs-id (uuid/next)
|
||||
stoper (rx/filter (ptk/type? ::finalize) stream)
|
||||
|
||||
;; Process all incoming messages.
|
||||
(->> (ws/-stream wsession)
|
||||
(rx/filter ws/message?)
|
||||
(rx/map (comp t/decode-str :payload))
|
||||
(rx/filter #(s/valid? ::message %))
|
||||
(rx/map process-message))
|
||||
initmsg [{:type :subscribe-file
|
||||
:subs-id subs-id
|
||||
:file-id file-id}
|
||||
{:type :subscribe-team
|
||||
:team-id team-id}]
|
||||
|
||||
(rx/of (handle-presence {:type :connect
|
||||
:session-id (:session-id state)
|
||||
:profile-id (:profile-id state)}))
|
||||
endmsg {:type :unsubscribe-file
|
||||
:subs-id subs-id}
|
||||
|
||||
;; Send back to backend all pointer messages.
|
||||
(->> stream
|
||||
(rx/filter ms/pointer-event?)
|
||||
(rx/sample 50)
|
||||
(rx/map #(handle-pointer-send file-id (:pt %)))))
|
||||
(rx/take-until stoper))))))
|
||||
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
|
||||
:session-id (:session-id state)
|
||||
:profile-id (:profile-id state)}))
|
||||
|
||||
;; Emit to all other connected users the current pointer
|
||||
;; position changes.
|
||||
(->> stream
|
||||
(rx/filter ms/pointer-event?)
|
||||
(rx/sample 50)
|
||||
(rx/map #(handle-pointer-send subs-id file-id (:pt %)))))
|
||||
|
||||
(rx/take-until stoper))]
|
||||
|
||||
(rx/concat stream (rx/of (dws/send endmsg)))))))
|
||||
|
||||
(defn- process-message
|
||||
[{:keys [type] :as msg}]
|
||||
(case type
|
||||
:connect (handle-presence msg)
|
||||
:join-file (handle-presence msg)
|
||||
:leave-file (handle-presence msg)
|
||||
:presence (handle-presence msg)
|
||||
:disconnect (handle-presence msg)
|
||||
:pointer-update (handle-pointer-update msg)
|
||||
:file-change (handle-file-change msg)
|
||||
:library-change (handle-library-change msg)
|
||||
::unknown))
|
||||
|
||||
(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})))))
|
||||
nil))
|
||||
|
||||
(defn- handle-pointer-send
|
||||
[file-id point]
|
||||
[subs-id file-id point]
|
||||
(ptk/reify ::handle-pointer-send
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [ws (get-in state [:ws file-id])
|
||||
pid (:current-page-id state)
|
||||
msg {:type :pointer-update
|
||||
:page-id pid
|
||||
:x (:x point)
|
||||
:y (:y point)}]
|
||||
(ws/send! ws msg)))))
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
message {:type :pointer-update
|
||||
:subs-id subs-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:position point}]
|
||||
(rx/of (dws/send message))))))
|
||||
|
||||
;; --- Finalize Websocket
|
||||
|
||||
(defn finalize
|
||||
[file-id]
|
||||
(ptk/reify ::finalize
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when-let [ws (get-in state [:ws file-id])]
|
||||
(ws/-close ws)))))
|
||||
[_]
|
||||
(ptk/reify ::finalize))
|
||||
|
||||
;; --- Handle: Presence
|
||||
|
||||
|
@ -165,7 +150,8 @@
|
|||
(assoc :profile-id profile-id)
|
||||
(assoc :updated-at (dt/now))
|
||||
(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"
|
||||
"#fff"))))
|
||||
|
||||
|
@ -179,20 +165,19 @@
|
|||
(ptk/reify ::handle-presence
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; (let [profiles (:users state)]
|
||||
(if (= :disconnect type)
|
||||
(if (or (= :disconnect type) (= :leave-file type))
|
||||
(update state :workspace-presence dissoc session-id)
|
||||
(update state :workspace-presence update-presence))))))
|
||||
|
||||
(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/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-presence session-id]
|
||||
(fn [session]
|
||||
(assoc session
|
||||
:point (gpt/point x y)
|
||||
:point position
|
||||
:updated-at (dt/now)
|
||||
:page-id page-id))))))
|
||||
|
||||
|
@ -241,4 +226,3 @@
|
|||
(when (contains? (:workspace-libraries state) file-id)
|
||||
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
|
||||
(dwl/notify-sync-file file-id))))))
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
[app.main.store :as st]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.time :as dt]
|
||||
[app.util.uri :as uu]
|
||||
[beicon.core :as rx]
|
||||
|
@ -71,7 +70,7 @@
|
|||
on-dirty
|
||||
(fn []
|
||||
;; Enable reload stoper
|
||||
(obj/set! js/window "onbeforeunload" (constantly false))
|
||||
(swap! st/ongoing-tasks conj :workspace-change)
|
||||
(st/emit! (update-persistence-status {:status :pending})))
|
||||
|
||||
on-saving
|
||||
|
@ -81,7 +80,7 @@
|
|||
on-saved
|
||||
(fn []
|
||||
;; Disable reload stoper
|
||||
(obj/set! js/window "onbeforeunload" nil)
|
||||
(swap! st/ongoing-tasks disj :workspace-change)
|
||||
(st/emit! (update-persistence-status {:status :saved})))]
|
||||
(->> (rx/merge
|
||||
(->> stream
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.data.workspace.shortcuts
|
||||
(:require
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.colors :as mdc]
|
||||
|
@ -61,9 +62,14 @@
|
|||
:command (ds/c-mod "shift+r")
|
||||
:fn #(st/emit! (toggle-layout-flag :rules))}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:fn #(st/emit! (dw/select-all))}
|
||||
: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")
|
||||
:command (ds/c-mod "a")
|
||||
:fn #(st/emit! (dw/select-all))}
|
||||
|
||||
:toggle-grid {:tooltip (ds/meta "'")
|
||||
:command (ds/c-mod "'")
|
||||
|
@ -329,7 +335,7 @@
|
|||
:align-vcenter {:tooltip (ds/alt "V")
|
||||
:command "alt+v"
|
||||
:fn #(st/emit! (dw/align-objects :vcenter))}
|
||||
|
||||
|
||||
:align-bottom {:tooltip (ds/alt "S")
|
||||
:command "alt+s"
|
||||
:fn #(st/emit! (dw/align-objects :vbottom))}
|
||||
|
@ -365,7 +371,7 @@
|
|||
:toggle-focus-mode {:command "f"
|
||||
:tooltip "F"
|
||||
:fn #(st/emit! (dw/toggle-focus-mode))}
|
||||
|
||||
|
||||
:thumbnail-set {:tooltip (ds/shift "T")
|
||||
:command "shift+t"
|
||||
:fn #(st/emit! (dw/toggle-file-thumbnail-selected))}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.data.workspace.state-helpers
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.pages.helpers :as cph]))
|
||||
|
||||
(defn lookup-page
|
||||
|
@ -35,14 +36,16 @@
|
|||
([state]
|
||||
(get-in state [:workspace-data :components])))
|
||||
|
||||
;; TODO: improve performance of this
|
||||
|
||||
(defn lookup-selected
|
||||
([state]
|
||||
(lookup-selected state nil))
|
||||
|
||||
([state {:keys [omit-blocked?]
|
||||
:or {omit-blocked? false}}]
|
||||
(let [objects (lookup-page-objects state)
|
||||
selected (->> (get-in state [:workspace-local :selected])
|
||||
([state options]
|
||||
(lookup-selected state (:current-page-id state) options))
|
||||
([state page-id {:keys [omit-blocked?] :or {omit-blocked? false}}]
|
||||
(let [objects (lookup-page-objects state page-id)
|
||||
selected (->> (dm/get-in state [:workspace-local :selected])
|
||||
(cph/clean-loops objects))
|
||||
selectable? (fn [id]
|
||||
(and (contains? objects id)
|
||||
|
@ -51,3 +54,17 @@
|
|||
(into (d/ordered-set)
|
||||
(filter selectable?)
|
||||
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.main.data.messages :as msg]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.sentry :as sentry]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
|
@ -26,7 +25,7 @@
|
|||
[error]
|
||||
(cond
|
||||
(instance? ExceptionInfo error)
|
||||
(-> error sentry/capture-exception ex-data ptk/handle-error)
|
||||
(-> error ex-data ptk/handle-error)
|
||||
|
||||
(map? error)
|
||||
(ptk/handle-error error)
|
||||
|
@ -34,7 +33,6 @@
|
|||
:else
|
||||
(let [hint (ex-message error)
|
||||
msg (dm/str "Internal Error: " hint)]
|
||||
(sentry/capture-exception error)
|
||||
(ts/schedule (st/emitf (rt/assign-exception error)))
|
||||
|
||||
(js/console.group msg)
|
||||
|
@ -68,7 +66,8 @@
|
|||
;; Error that happens on an active business model validation does not
|
||||
;; passes an validation (example: profile can't leave a team). From
|
||||
;; 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
|
||||
[error]
|
||||
(ts/schedule
|
||||
|
@ -92,6 +91,7 @@
|
|||
|
||||
|
||||
;; Error on parsing an SVG
|
||||
;; TODO: looks unused and deprecated
|
||||
(defmethod ptk/handle-error :svg-parser
|
||||
[_]
|
||||
(ts/schedule
|
||||
|
@ -100,6 +100,7 @@
|
|||
:type :error
|
||||
:timeout 3000}))))
|
||||
|
||||
;; TODO: should be handled in the event and not as general error handler
|
||||
(defmethod ptk/handle-error :comment-error
|
||||
[_]
|
||||
(ts/schedule
|
||||
|
@ -160,10 +161,9 @@
|
|||
(defn on-unhandled-error
|
||||
[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)
|
||||
msg (dm/str "Unhandled Internal Error: " hint)]
|
||||
(sentry/capture-exception error)
|
||||
(ts/schedule (st/emitf (rt/assign-exception error)))
|
||||
(js/console.group msg)
|
||||
(ex/ignoring (js/console.error error))
|
||||
|
|
|
@ -42,6 +42,9 @@
|
|||
(def share-links
|
||||
(l/derived :share-links st/state))
|
||||
|
||||
(def export
|
||||
(l/derived :export st/state))
|
||||
|
||||
;; ---- Dashboard refs
|
||||
|
||||
(def dashboard-local
|
||||
|
@ -98,6 +101,7 @@
|
|||
(def workspace-drawing
|
||||
(l/derived :workspace-drawing st/state))
|
||||
|
||||
;; TODO: rename to workspace-selected (?)
|
||||
(def selected-shapes
|
||||
(l/derived wsh/lookup-selected st/state =))
|
||||
|
||||
|
@ -105,6 +109,27 @@
|
|||
[id]
|
||||
(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
|
||||
(l/derived :zoom workspace-local))
|
||||
|
||||
|
@ -233,11 +258,7 @@
|
|||
|
||||
(defn objects-by-id
|
||||
[ids]
|
||||
(let [selector
|
||||
(fn [state]
|
||||
(let [objects (wsh/lookup-page-objects state)]
|
||||
(into [] (keep (d/getf objects)) ids)))]
|
||||
(l/derived selector st/state =)))
|
||||
(l/derived #(wsh/lookup-shapes % ids) st/state =))
|
||||
|
||||
(defn- set-content-modifiers [state]
|
||||
(fn [id shape]
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.util.http :as http]
|
||||
[beicon.core :as rx]))
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
|||
:status status
|
||||
:data body})))
|
||||
|
||||
(def ^:private base-uri cfg/public-uri)
|
||||
(def ^:private base-uri cf/public-uri)
|
||||
|
||||
(defn- send-query!
|
||||
"A simple helper for send and receive transit data on the penpot
|
||||
|
@ -105,23 +105,44 @@
|
|||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
(defmethod query :export
|
||||
[_ params]
|
||||
(defn- send-export-command
|
||||
[& {:keys [cmd params blob?]}]
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join base-uri "export")
|
||||
:body (http/transit-data params)
|
||||
:body (http/transit-data (assoc params :cmd cmd))
|
||||
:credentials "include"
|
||||
:response-type :blob})
|
||||
:response-type (if blob? :blob :text)})
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)))
|
||||
|
||||
(defmethod query :export-frames
|
||||
(defmethod query :export-shapes-simple
|
||||
[_ 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)))
|
||||
(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
|
||||
:uri (u/join base-uri "export")
|
||||
:body (http/transit-data params)
|
||||
:credentials "include"
|
||||
:response-type :blob})
|
||||
(rx/mapcat handle-response)
|
||||
(rx/ignore))))
|
||||
|
||||
(derive :upload-file-media-object ::multipart-upload)
|
||||
(derive :update-profile-photo ::multipart-upload)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.store
|
||||
(:require-macros [app.main.store])
|
||||
(:require
|
||||
[app.util.object :as obj]
|
||||
[beicon.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[potok.core :as ptk]))
|
||||
|
@ -59,4 +60,10 @@
|
|||
[& 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 checkbox-checked (icon-xref :checkbox-checked))
|
||||
(def checkbox-unchecked (icon-xref :checkbox-unchecked))
|
||||
(def checkbox-intermediate (icon-xref :checkbox-intermediate))
|
||||
(def circle (icon-xref :circle))
|
||||
(def close (icon-xref :close))
|
||||
(def code (icon-xref :code))
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.viewer.handoff.attributes
|
||||
(:require
|
||||
[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.fill :refer [fill-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.text :refer [text-panel]]
|
||||
[app.main.ui.viewer.handoff.exports :refer [exports]]
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def type->options
|
||||
|
@ -31,8 +31,9 @@
|
|||
|
||||
(mf/defc attributes
|
||||
[{:keys [page-id file-id shapes frame]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
shapes (->> shapes (map #(gsh/translate-to-frame % frame)))
|
||||
(let [shapes (hooks/use-equal-memo shapes)
|
||||
shapes (mf/with-memo [shapes]
|
||||
(mapv #(gsh/translate-to-frame % frame) shapes))
|
||||
type (if (= (count shapes) 1) (-> shapes first :type) :multiple)
|
||||
options (type->options type)]
|
||||
[:div.element-options
|
||||
|
@ -47,10 +48,9 @@
|
|||
:text text-panel
|
||||
:svg svg-panel)
|
||||
{:shapes shapes
|
||||
:frame frame
|
||||
:locale locale}])
|
||||
(when-not (= :multiple type)
|
||||
[:& exports
|
||||
{:shape (first shapes)
|
||||
:page-id page-id
|
||||
:file-id file-id}])]))
|
||||
:frame frame}])
|
||||
[:& exports
|
||||
{:shapes shapes
|
||||
:type type
|
||||
:page-id page-id
|
||||
:file-id file-id}]]))
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [t]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -22,17 +22,17 @@
|
|||
{:to-prop "filter"
|
||||
: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?))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[: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)
|
||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||
|
||||
(for [shape shapes]
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data shape)}]])])))
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [t] :as i18n]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
@ -27,9 +27,7 @@
|
|||
#(l/derived get-library st/state)))
|
||||
|
||||
(mf/defc color-row [{:keys [color format copy-data on-change-format]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
|
||||
colors-library-ref (mf/use-memo
|
||||
(let [colors-library-ref (mf/use-memo
|
||||
(mf/deps (:file-id color))
|
||||
(make-colors-library-ref (:file-id color)))
|
||||
colors-library (mf/deref colors-library-ref)
|
||||
|
@ -60,13 +58,13 @@
|
|||
(when-not (and on-change-format (:gradient color))
|
||||
[:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
|
||||
[:option {:value "hex"}
|
||||
(t locale "handoff.attributes.color.hex")]
|
||||
(tr "handoff.attributes.color.hex")]
|
||||
|
||||
[:option {:value "rgba"}
|
||||
(t locale "handoff.attributes.color.rgba")]
|
||||
(tr "handoff.attributes.color.rgba")]
|
||||
|
||||
[:option {:value "hsla"}
|
||||
(t locale "handoff.attributes.color.hsla")]])]
|
||||
(tr "handoff.attributes.color.hsla")]])]
|
||||
(when copy-data
|
||||
[:& copy-button {:data copy-data}])]))
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
[app.common.spec.radius :as ctr]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.i18n :refer [t]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -32,41 +32,41 @@
|
|||
(cg/generate-css-props shape properties params)))
|
||||
|
||||
(mf/defc layout-block
|
||||
[{:keys [shape locale]}]
|
||||
[{:keys [shape]}]
|
||||
(let [selrect (:selrect shape)
|
||||
{:keys [width height x y]} selrect]
|
||||
[:*
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data selrect :width)}]]
|
||||
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data selrect :height)}]]
|
||||
|
||||
(when (not= (:x shape) 0)
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data selrect :x)}]])
|
||||
|
||||
(when (not= (:y shape) 0)
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data selrect :y)}]])
|
||||
|
||||
(when (ctr/radius-1? shape)
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data shape :rx)}]])
|
||||
|
||||
(when (ctr/radius-4? shape)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-label (tr "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value
|
||||
(:r1 shape) ", "
|
||||
(:r2 shape) ", "
|
||||
|
@ -76,19 +76,18 @@
|
|||
|
||||
(when (not= (:rotation shape 0) 0)
|
||||
[: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"]
|
||||
[:& copy-button {:data (copy-data shape :rotation)}]])]))
|
||||
|
||||
|
||||
(mf/defc layout-panel
|
||||
[{:keys [shapes locale]}]
|
||||
[{:keys [shapes]}]
|
||||
[:div.attributes-block
|
||||
[: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)
|
||||
[:& copy-button {:data (copy-data (first shapes))}])]
|
||||
|
||||
(for [shape shapes]
|
||||
[:& layout-block {:shape shape
|
||||
:locale locale}])])
|
||||
[:& layout-block {:shape shape}])])
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [t]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
:format #(uc/color->background (shape->color shape))}))
|
||||
|
||||
(mf/defc stroke-block
|
||||
[{:keys [shape locale]}]
|
||||
[{:keys [shape]}]
|
||||
(let [color-format (mf/use-state :hex)
|
||||
color (shape->color shape)]
|
||||
[:*
|
||||
|
@ -65,19 +65,19 @@
|
|||
stroke-style (if (= stroke-style :svg) :solid stroke-style)
|
||||
stroke-alignment (or stroke-alignment :center)]
|
||||
[: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-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
|
||||
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (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.") (tr))]
|
||||
[:& copy-button {:data (copy-stroke-data shape)}]])]))
|
||||
|
||||
(mf/defc stroke-panel
|
||||
[{:keys [shapes locale]}]
|
||||
[{:keys [shapes]}]
|
||||
(let [shapes (->> shapes (filter has-stroke?))]
|
||||
(when (seq shapes)
|
||||
[:div.attributes-block
|
||||
[: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)
|
||||
[:& copy-button {:data (copy-stroke-data (first shapes))}])]
|
||||
|
||||
|
@ -85,8 +85,6 @@
|
|||
(if (seq (:strokes shape))
|
||||
(for [value (:strokes shape [])]
|
||||
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
||||
:shape value
|
||||
:locale locale}])
|
||||
:shape value}])
|
||||
[:& stroke-block {:key (str "stroke-color-" (:id shape))
|
||||
:shape shape
|
||||
:locale locale}]))])))
|
||||
:shape shape}]))])))
|
||||
|
|
|
@ -7,21 +7,56 @@
|
|||
(ns app.main.ui.viewer.handoff.exports
|
||||
(:require
|
||||
[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.workspace.sidebar.options.menus.exports :as we]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.i18n :refer [tr c]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc exports
|
||||
[{:keys [shape page-id file-id] :as props}]
|
||||
(let [exports (mf/use-state (:exports shape []))
|
||||
{::mf/wrap [#(mf/memo % =)]}
|
||||
[{: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
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps shapes)
|
||||
(fn []
|
||||
(let [xspec {:type :png
|
||||
:suffix ""
|
||||
|
@ -30,7 +65,7 @@
|
|||
|
||||
delete-export
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps shapes)
|
||||
(fn [index]
|
||||
(swap! exports (fn [exports]
|
||||
(let [[before after] (split-at index exports)]
|
||||
|
@ -38,7 +73,7 @@
|
|||
|
||||
on-scale-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps shapes)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
|
@ -47,7 +82,7 @@
|
|||
|
||||
on-suffix-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps shapes)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
|
@ -55,7 +90,7 @@
|
|||
|
||||
on-type-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps shapes)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
|
@ -63,9 +98,12 @@
|
|||
(swap! exports assoc-in [index :type] value))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps shape)
|
||||
(mf/deps shapes)
|
||||
(fn []
|
||||
(reset! exports (:exports shape []))))
|
||||
(reset! exports (-> (mapv #(:exports % []) shapes)
|
||||
flatten
|
||||
distinct
|
||||
vec))))
|
||||
|
||||
[:div.element-set.exports-options
|
||||
[:div.element-set-title
|
||||
|
@ -99,10 +137,10 @@
|
|||
i/minus]])
|
||||
|
||||
[:div.btn-icon-dark.download-button
|
||||
{:on-click (when-not loading? on-download)
|
||||
:class (dom/classnames :btn-disabled loading?)
|
||||
:disabled loading?}
|
||||
(if loading?
|
||||
{:on-click (when-not in-progress? on-download)
|
||||
:class (dom/classnames :btn-disabled in-progress?)
|
||||
:disabled in-progress?}
|
||||
(if in-progress?
|
||||
(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]}]
|
||||
(let [expanded (mf/use-state false)
|
||||
section (mf/use-state :info #_:code)
|
||||
|
||||
shapes (resolve-shapes (:objects page) selected)
|
||||
|
||||
first-shape (first shapes)
|
||||
|
||||
selected-type (or (:type first-shape) :not-found)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.viewer.handoff.selection-feedback
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.measurements :refer [selection-guides size-display measurement]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
@ -21,10 +22,8 @@
|
|||
|
||||
(defn resolve-shapes
|
||||
[objects ids]
|
||||
(let [resolve-shape #(get objects %)]
|
||||
(into [] (comp (map resolve-shape)
|
||||
(filter some?))
|
||||
ids)))
|
||||
(let [resolve-shape (d/getf objects)]
|
||||
(into [] (keep resolve-shape) ids)))
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; HELPERS
|
||||
|
|
|
@ -12,12 +12,13 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.export :refer [export-progress-widget]]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.comments :refer [comments-menu]]
|
||||
[app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc zoom-widget
|
||||
|
@ -88,6 +89,7 @@
|
|||
|
||||
[:div.view-options])
|
||||
|
||||
[:& export-progress-widget]
|
||||
[:& zoom-widget
|
||||
{:zoom zoom
|
||||
:on-increase (st/emitf dv/increase-zoom)
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
(ns app.main.ui.workspace.header
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[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.workspace :as dw]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
|
@ -17,6 +19,7 @@
|
|||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.export :refer [export-progress-widget]]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.hooks.resize :as r]
|
||||
[app.main.ui.icons :as i]
|
||||
|
@ -30,11 +33,12 @@
|
|||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; --- Zoom Widget
|
||||
|
||||
(def workspace-persistence-ref
|
||||
(l/derived :workspace-persistence st/state))
|
||||
|
||||
;; --- Persistence state Widget
|
||||
|
||||
(mf/defc persistence-state-widget
|
||||
{::mf/wrap [mf/memo]}
|
||||
[]
|
||||
|
@ -60,6 +64,8 @@
|
|||
[:span.icon i/msg-warning]
|
||||
[:span.label (tr "workspace.header.save-error")]])]))
|
||||
|
||||
;; --- Zoom Widget
|
||||
|
||||
(mf/defc zoom-widget-workspace
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [zoom
|
||||
|
@ -150,6 +156,11 @@
|
|||
(dom/prevent-default event)
|
||||
(reset! editing? true))
|
||||
|
||||
on-export-shapes
|
||||
(mf/use-callback
|
||||
(fn [_]
|
||||
(st/emit! (de/show-workspace-export-dialog))))
|
||||
|
||||
on-export-file
|
||||
(mf/use-callback
|
||||
(mf/deps file team-id)
|
||||
|
@ -178,21 +189,20 @@
|
|||
(mf/deps file frames)
|
||||
(fn [_]
|
||||
(when (seq frames)
|
||||
(let [filename (str (:name file) ".pdf")
|
||||
frame-ids (mapv :id frames)]
|
||||
(st/emit! (dm/info (tr "workspace.options.exporting-object")
|
||||
{:timeout nil}))
|
||||
(->> (rp/query! :export-frames
|
||||
{:name (:name file)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:frame-ids frame-ids})
|
||||
(let [filename (dm/str (:name file) ".pdf")
|
||||
xform (comp (map :id)
|
||||
(map (fn [id]
|
||||
{:file-id (:id file)
|
||||
:page-id page-id
|
||||
:frame-id id})))]
|
||||
(st/emit! (msg/info (tr "workspace.options.exporting-object") {:timeout nil}))
|
||||
(->> (rp/query! :export-frames (into [] xform frames))
|
||||
(rx/subs
|
||||
(fn [body]
|
||||
(dom/trigger-download filename body))
|
||||
(fn [_error]
|
||||
(st/emit! (dm/error (tr "errors.unexpected-error"))))
|
||||
(st/emitf dm/hide)))))))
|
||||
(st/emit! (msg/error (tr "errors.unexpected-error"))))
|
||||
(st/emitf msg/hide)))))))
|
||||
|
||||
on-item-hover
|
||||
(mf/use-callback
|
||||
|
@ -269,6 +279,9 @@
|
|||
[:span (tr "dashboard.remove-shared")]]
|
||||
[:li {:on-click on-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}
|
||||
[:span (tr "dashboard.export-single")]]
|
||||
(when (seq frames)
|
||||
|
@ -397,9 +410,9 @@
|
|||
|
||||
(mf/defc header
|
||||
[{:keys [file layout project page-id] :as props}]
|
||||
(let [team-id (:team-id project)
|
||||
zoom (mf/deref refs/selected-zoom)
|
||||
params {:page-id page-id :file-id (:id file) :section "interactions"}
|
||||
(let [team-id (:team-id project)
|
||||
zoom (mf/deref refs/selected-zoom)
|
||||
params {:page-id page-id :file-id (:id file) :section "interactions"}
|
||||
|
||||
go-back
|
||||
(mf/use-callback
|
||||
|
@ -429,6 +442,7 @@
|
|||
[:div.right-area
|
||||
[:div.options-section
|
||||
[:& persistence-state-widget]
|
||||
[:& export-progress-widget]
|
||||
[:button.document-history
|
||||
{:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
|
||||
:class (when (contains? layout :document-history) "selected")
|
||||
|
|
|
@ -50,7 +50,9 @@
|
|||
:bool [:& bool/options {:shape shape}]
|
||||
nil)
|
||||
[:& exports-menu
|
||||
{:shape shape
|
||||
{:ids [(:id shape)]
|
||||
:values (select-keys shape [:exports])
|
||||
:shape shape
|
||||
:page-id page-id
|
||||
:file-id file-id}]])
|
||||
|
||||
|
@ -82,7 +84,9 @@
|
|||
:file-id file-id
|
||||
: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
|
||||
:title (tr "workspace.options.prototype")}
|
||||
|
|
|
@ -7,121 +7,138 @@
|
|||
(ns app.main.ui.workspace.sidebar.options.menus.exports
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.export]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[app.util.i18n :refer [tr c]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn request-export
|
||||
[shape exports]
|
||||
;; Force a persist before exporting otherwise the exported shape could be outdated
|
||||
(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?]))
|
||||
(def exports-attrs
|
||||
"Shape attrs that corresponds to exports. Used in other namespaces."
|
||||
[:exports])
|
||||
|
||||
(mf/defc exports-menu
|
||||
[{:keys [shape page-id file-id] :as props}]
|
||||
(let [exports (:exports shape [])
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "page-id" "file-id"]))]}
|
||||
[{: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?
|
||||
(mf/use-callback
|
||||
(fn [export]
|
||||
(#{:png :jpeg} (:type export))))
|
||||
(fn [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
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps ids)
|
||||
(fn []
|
||||
(let [xspec {:type :png
|
||||
:suffix ""
|
||||
:scale 1}]
|
||||
(st/emit! (udw/update-shape (:id shape)
|
||||
{:exports (conj exports xspec)})))))
|
||||
(let [xspec {:type :png :suffix "" :scale 1}]
|
||||
(st/emit! (dch/update-shapes ids
|
||||
(fn [shape]
|
||||
(assoc shape :exports (into [xspec] (:exports shape)))))))))
|
||||
|
||||
delete-export
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(fn [index]
|
||||
(let [[before after] (split-at index exports)
|
||||
exports (d/concat-vec before (rest after))]
|
||||
(st/emit! (udw/update-shape (:id shape)
|
||||
{:exports exports})))))
|
||||
(mf/deps ids)
|
||||
(fn [position]
|
||||
(let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
|
||||
(filterv (fn [[idx _]] (not= idx index)))
|
||||
(mapv second)))
|
||||
|
||||
remove (fn [shape] (update shape :exports remove-fill-by-index position))]
|
||||
(st/emit! (dch/update-shapes ids remove)))))
|
||||
|
||||
on-scale-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps ids)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
value (d/parse-double value)
|
||||
exports (assoc-in exports [index :scale] value)]
|
||||
(st/emit! (udw/update-shape (:id shape)
|
||||
{:exports exports})))))
|
||||
value (d/parse-double value)]
|
||||
(st/emit! (dch/update-shapes ids
|
||||
(fn [shape]
|
||||
(assoc-in shape [:exports index :scale] value)))))))
|
||||
|
||||
on-suffix-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps ids)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
exports (assoc-in exports [index :suffix] value)]
|
||||
(st/emit! (udw/update-shape (:id shape)
|
||||
{:exports exports})))))
|
||||
value (dom/get-value target)]
|
||||
(st/emit! (dch/update-shapes ids
|
||||
(fn [shape]
|
||||
(assoc-in shape [:exports index :suffix] value)))))))
|
||||
|
||||
on-type-change
|
||||
(mf/use-callback
|
||||
(mf/deps shape)
|
||||
(mf/deps ids)
|
||||
(fn [index event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)
|
||||
value (keyword value)
|
||||
exports (assoc-in exports [index :type] value)]
|
||||
(st/emit! (udw/update-shape (:id shape)
|
||||
{:exports exports})))))]
|
||||
value (keyword value)]
|
||||
(st/emit! (dch/update-shapes ids
|
||||
(fn [shape]
|
||||
(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-title
|
||||
[:span (tr "workspace.options.export")]
|
||||
[:div.add-page {:on-click add-export} i/close]]
|
||||
(when (seq exports)
|
||||
[:span (tr (if (> (count ids) 1) "workspace.options.export-multiple" "workspace.options.export"))]
|
||||
(when (not (= :multiple 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
|
||||
(for [[index export] (d/enumerate exports)]
|
||||
[:div.element-set-options-group
|
||||
|
@ -146,14 +163,14 @@
|
|||
[:option {:value "svg"} "SVG"]
|
||||
[:option {:value "pdf"} "PDF"]]
|
||||
[:div.delete-icon {:on-click (partial delete-export index)}
|
||||
i/minus]])
|
||||
|
||||
[:div.btn-icon-dark.download-button
|
||||
{:on-click (when-not loading? on-download)
|
||||
:class (dom/classnames
|
||||
:btn-disabled loading?)
|
||||
:disabled loading?}
|
||||
(if loading?
|
||||
(tr "workspace.options.exporting-object")
|
||||
(tr "workspace.options.export-object"))]])]))
|
||||
i/minus]])])
|
||||
|
||||
(when (or (= :multiple exports) (seq exports))
|
||||
[:div.btn-icon-dark.download-button
|
||||
{:on-click (when-not in-progress? on-download)
|
||||
:class (dom/classnames
|
||||
:btn-disabled in-progress?)
|
||||
:disabled in-progress?}
|
||||
(if in-progress?
|
||||
(tr "workspace.options.exporting-object")
|
||||
(tr "workspace.options.export-object" (c (count ids))))])]))
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
[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.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.layer :refer [layer-attrs layer-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
||||
|
@ -36,7 +37,8 @@
|
|||
:shadow :children
|
||||
:blur :children
|
||||
:stroke :shape
|
||||
:text :children}
|
||||
:text :children
|
||||
:exports :shape}
|
||||
|
||||
:group
|
||||
{:measure :shape
|
||||
|
@ -46,7 +48,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :children
|
||||
:text :children}
|
||||
:text :children
|
||||
:exports :shape}
|
||||
|
||||
:path
|
||||
{:measure :shape
|
||||
|
@ -56,7 +59,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :ignore}
|
||||
:text :ignore
|
||||
:exports :shape}
|
||||
|
||||
:text
|
||||
{:measure :shape
|
||||
|
@ -66,7 +70,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :text}
|
||||
:text :text
|
||||
:exports :shape}
|
||||
|
||||
:image
|
||||
{:measure :shape
|
||||
|
@ -76,7 +81,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :ignore
|
||||
:text :ignore}
|
||||
:text :ignore
|
||||
:exports :shape}
|
||||
|
||||
:rect
|
||||
{:measure :shape
|
||||
|
@ -86,7 +92,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :ignore}
|
||||
:text :ignore
|
||||
:exports :shape}
|
||||
|
||||
:circle
|
||||
{:measure :shape
|
||||
|
@ -96,7 +103,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :ignore}
|
||||
:text :ignore
|
||||
:exports :shape}
|
||||
|
||||
:svg-raw
|
||||
{:measure :shape
|
||||
|
@ -106,7 +114,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :ignore}
|
||||
:text :ignore
|
||||
:exports :shape}
|
||||
|
||||
:bool
|
||||
{:measure :shape
|
||||
|
@ -116,7 +125,8 @@
|
|||
:shadow :shape
|
||||
:blur :shape
|
||||
:stroke :shape
|
||||
:text :ignore}})
|
||||
:text :ignore
|
||||
:exports :shape}})
|
||||
|
||||
(def group->attrs
|
||||
{:measure measure-attrs
|
||||
|
@ -126,7 +136,8 @@
|
|||
:shadow shadow-attrs
|
||||
:blur blur-attrs
|
||||
:stroke stroke-attrs
|
||||
:text ot/attrs})
|
||||
:text ot/attrs
|
||||
:exports exports-attrs})
|
||||
|
||||
(def shadow-keys [:style :color :offset-x :offset-y :blur :spread])
|
||||
|
||||
|
@ -211,11 +222,13 @@
|
|||
(dissoc :content)))
|
||||
|
||||
(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}
|
||||
[props]
|
||||
(let [shapes (unchecked-get props "shapes")
|
||||
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))))
|
||||
show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes)
|
||||
|
||||
|
@ -235,7 +248,8 @@
|
|||
shadow-ids shadow-values
|
||||
blur-ids blur-values
|
||||
stroke-ids stroke-values
|
||||
text-ids text-values]
|
||||
text-ids text-values
|
||||
exports-ids exports-values]
|
||||
(mf/use-memo
|
||||
(mf/deps objects-no-measures)
|
||||
(fn []
|
||||
|
@ -248,7 +262,8 @@
|
|||
(get-attrs shapes objects-no-measures :shadow)
|
||||
(get-attrs shapes objects-no-measures :blur)
|
||||
(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
|
||||
(when-not (empty? measure-ids)
|
||||
|
@ -273,4 +288,7 @@
|
|||
[:& blur-menu {:type type :ids blur-ids :values blur-values}])
|
||||
|
||||
(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
|
||||
[data]
|
||||
(reify IBodyData
|
||||
(-get-body-data [_] (t/encode-str data))
|
||||
(-get-body-data [_] (t/encode-str data {:type :json-verbose}))
|
||||
(-update-headers [_ headers]
|
||||
(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 "
|
||||
"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"
|
||||
msgstr "Export artboards to PDF..."
|
||||
|
||||
|
@ -300,6 +304,26 @@ msgstr "Include shared library assets in file libraries"
|
|||
msgid "dashboard.export.title"
|
||||
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"
|
||||
msgstr "Font deleted"
|
||||
|
||||
|
@ -2480,18 +2504,40 @@ msgstr "Design"
|
|||
msgid "workspace.options.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
|
||||
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
|
||||
msgid "workspace.options.export.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"
|
||||
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
|
||||
msgid "workspace.options.fill"
|
||||
msgstr "Fill"
|
||||
|
|
|
@ -260,6 +260,10 @@ msgstr ""
|
|||
"¡Oh, no! ¡Aún no tienes archivos! Si quieres probar con alguna plantilla ve a "
|
||||
"[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"
|
||||
msgstr "Exportar tableros a PDF..."
|
||||
|
||||
|
@ -304,6 +308,26 @@ msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
|
|||
msgid "dashboard.export.title"
|
||||
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"
|
||||
msgstr "Fuente eliminada."
|
||||
|
||||
|
@ -2496,17 +2520,35 @@ msgstr "Diseño"
|
|||
msgid "workspace.options.export"
|
||||
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
|
||||
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
|
||||
msgid "workspace.options.export.suffix"
|
||||
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"
|
||||
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
|
||||
msgid "workspace.options.fill"
|
||||
|
|
Loading…
Add table
Reference in a new issue