0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-25 06:01:46 -05:00

🎉 Add stacked exports.

This commit is contained in:
Andrey Antukh 2020-07-02 14:48:17 +02:00 committed by Hirunatan
parent a8d5cdc29f
commit 2fb4e72240
14 changed files with 549 additions and 208 deletions

View file

@ -80,7 +80,7 @@
(s/def ::stroke-width number?)
(s/def ::stroke-alignment #{:center :inner :outer})
(s/def ::text-align #{"left" "right" "center" "justify"})
(s/def ::type #{:rect :path :circle :image :text :canvas :curve :icon :frame :group})
(s/def ::type keyword?)
(s/def ::x number?)
(s/def ::y number?)
(s/def ::cx number?)
@ -93,6 +93,14 @@
(s/def ::x2 number?)
(s/def ::y2 number?)
(s/def ::suffix string?)
(s/def ::scale number?)
(s/def ::export
(s/keys :req-un [::type ::suffix ::scale]))
(s/def ::exports (s/coll-of ::export :kind vector?))
(s/def ::selrect (s/keys :req-un [::x
::y
::x1
@ -124,6 +132,7 @@
::rx ::ry
::cx ::cy
::x ::y
::exports
::stroke-color
::stroke-opacity
::stroke-style

View file

@ -86,6 +86,7 @@
;; --- Default Specs
(s/def ::keyword keyword?)
(s/def ::inst inst?)
(s/def ::string string?)
(s/def ::email (s/conformer email-conformer str))

View file

@ -261,6 +261,11 @@
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
"dev": true
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cache-content-type": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
@ -344,8 +349,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"create-ecdh": {
"version": "4.0.3",
@ -697,11 +701,29 @@
}
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"inflation": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz",
"integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -724,8 +746,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
@ -733,6 +754,41 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"jszip": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
"integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"keygrip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
@ -795,6 +851,14 @@
}
}
},
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"requires": {
"immediate": "~3.0.5"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -993,8 +1057,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parse-asn1": {
"version": "5.1.5",
@ -1053,8 +1116,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"progress": {
"version": "2.0.3",
@ -1190,6 +1252,17 @@
"safe-buffer": "^5.1.0"
}
},
"raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
"integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.3",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@ -1234,6 +1307,16 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@ -1497,6 +1580,11 @@
"through": "^2.3.8"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"url": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",

View file

@ -9,9 +9,12 @@
"author": "UXBOX LABS SL",
"license": "SEE LICENSE IN <LICENSE>",
"dependencies": {
"inflation": "^2.0.0",
"jszip": "^3.5.0",
"koa": "^2.13.0",
"puppeteer": "^4.0.1",
"puppeteer-cluster": "^0.21.0",
"raw-body": "^2.4.1",
"xregexp": "^4.3.0"
},
"devDependencies": {

View file

@ -4,7 +4,8 @@
[funcool/cuerdas "2020.03.26-3"]
[lambdaisland/glogi "1.0.63"]
[metosin/reitit-core "0.5.2"]
[com.cognitect/transit-cljs "0.8.264"]]
[com.cognitect/transit-cljs "0.8.264"]
[frankiesardo/linked "1.3.0"]]
:source-paths ["src" "../common"]
:nrepl {:port 3497}

View file

@ -15,11 +15,13 @@
(f page)))))
(defn emulate!
[page {:keys [viewport user-agent]
:or {user-agent USER-AGENT}}]
[page {:keys [viewport user-agent scale]
:or {user-agent USER-AGENT
scale 1}}]
(let [[width height] viewport]
(.emulate page #js {:viewport #js {:width width
:height height}
:height height
:deviceScaleFactor scale}
:userAgent user-agent})))
(defn navigate!
@ -33,10 +35,20 @@
(.waitFor ^js page ms))
(defn screenshot
([page] (screenshot page nil))
([page {:keys [full-page?]
:or {full-page? true}}]
(.screenshot ^js page #js {:fullPage full-page? :omitBackground true})))
([frame] (screenshot frame nil))
([frame {:keys [full-page? omit-background?]
:or {full-page? false
omit-background? false}}]
(.screenshot ^js frame #js {:fullPage full-page?
:omitBackground omit-background?})))
(defn eval!
[frame f]
(.evaluate ^js frame f))
(defn select
[frame selector]
(.$ ^js frame selector))
(defn set-cookie!
[page {:keys [key value domain]}]
@ -47,16 +59,15 @@
(defn start!
([] (start! nil))
([{:keys [concurrency concurrency-strategy]
:or {concurrency 2
concurrency-strategy :browser}}]
:or {concurrency 10
concurrency-strategy :incognito}}]
(let [ccst (case concurrency-strategy
:browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
:incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
:page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
opts #js {:concurrency ccst
:maxConcurrency concurrency
:puppeteerOptions #js {:args #js ["--no-sandbox"
"--explicitly-allowed-ports=6000"]}}]
:puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
(.launch ^js ppc/Cluster opts))))
(defn stop!

View file

@ -3,13 +3,14 @@
[promesa.core :as p]
[lambdaisland.glogi :as log]
[app.browser :as bwr]
[app.http.screenshot :refer [bitmap-handler
page-handler]]
[app.http.bitmap-export :refer [bitmap-export-handler]]
[app.util.transit :as t]
[reitit.core :as r]
[cuerdas.core :as str]
["koa" :as koa]
["http" :as http])
["http" :as http]
["inflation" :as inflate]
["raw-body" :as raw-body])
(:import
goog.Uri))
@ -26,11 +27,7 @@
[router ctx]
(let [uri (.parse Uri (unchecked-get ctx "originalUrl"))]
(when-let [match (r/match-by-path router (.getPath uri))]
(let [qparams (query-params uri)
params {:path (:path-params match) :query qparams}]
(assoc match
:params params
:query-params qparams)))))
(assoc match :query-params (query-params uri)))))
(defn- handle-error
[error request]
@ -72,15 +69,33 @@
(transient {})
(js/Object.keys orig)))))
(def parse-body?
#{"POST" "PUT" "DELETE"})
(defn- parse-body
[ctx]
(let [headers (unchecked-get ctx "headers")
ctype (unchecked-get headers "content-type")]
(when (parse-body? (.-method ^js ctx))
(-> (inflate (.-req ^js ctx))
(raw-body #js {:limit "5mb" :encoding "utf8"})
(p/then (fn [data]
(cond-> data
(= ctype "application/transit+json")
(t/decode))))))))
(defn- wrap-handler
[f extra]
(fn [ctx]
(let [cookies (unchecked-get ctx "cookies")
headers (parse-headers ctx)
request (assoc extra
:ctx ctx
:headers headers
:cookies cookies)]
(p/let [cookies (unchecked-get ctx "cookies")
headers (parse-headers ctx)
body (parse-body ctx)
request (assoc extra
:method (str/lower (unchecked-get ctx "method"))
:body body
:ctx ctx
:headers headers
:cookies cookies)]
(-> (p/do! (f request))
(p/then (fn [rsp]
(when (map? rsp)
@ -91,16 +106,20 @@
(def routes
[["/export"
["/bitmap" {:handler bitmap-handler}]
["/page" {:handler page-handler}]]])
["/bitmap" {:handler bitmap-export-handler}]]])
(defn- router-handler
[router]
(fn [{:keys [ctx] :as req}]
(fn [{:keys [ctx body] :as request}]
(let [route (match router ctx)
request (assoc req
params (merge {}
(:query-params route)
(:path-params route)
(when (map? body) body))
request (assoc request
:route route
:params (:params route))
:params params)
handler (get-in route [:data :handler])]
(if (and route handler)
(handler request)

View file

@ -0,0 +1,120 @@
(ns app.http.bitmap-export
(:require
[cuerdas.core :as str]
[app.browser :as bwr]
[app.config :as cfg]
[app.zipfile :as zip]
[lambdaisland.glogi :as log]
[cljs.spec.alpha :as s]
[promesa.core :as p]
[uxbox.common.exceptions :as exc :include-macros true]
[uxbox.common.data :as d]
[uxbox.common.pages :as cp]
[uxbox.common.spec :as us])
(:import
goog.Uri))
(defn- screenshot-object
[browser {:keys [page-id object-id token scale suffix]}]
(letfn [(handle [page]
(let [path (str "/render-object/" page-id "/" object-id)
uri (doto (Uri. (:public-uri cfg/config))
(.setPath "/")
(.setFragment path))
cookie {:domain (str (.getDomain uri)
":"
(.getPort uri))
:key "auth-token"
:value token}]
(log/info :uri (.toString uri))
(screenshot page (.toString uri) cookie)))
(screenshot [page uri cookie]
(p/do!
(bwr/emulate! page {:viewport [1920 1080]
:scale scale})
(bwr/set-cookie! page cookie)
(bwr/navigate! page uri)
(bwr/eval! page (js* "() => document.body.style.background = 'transparent'"))
(p/let [dom (bwr/select page "#screenshot")]
(bwr/screenshot dom {:omit-background? true
:type type}))))]
(bwr/exec! browser handle)))
(s/def ::name ::us/string)
(s/def ::page-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::suffix ::us/string)
(s/def ::type ::us/keyword)
(s/def ::suffix string?)
(s/def ::scale number?)
(s/def ::export
(s/keys :req-un [::type ::suffix ::scale]))
(s/def ::exports (s/coll-of ::export :kind vector?))
(s/def ::bitmap-handler-params
(s/keys :req-un [::page-id ::object-id ::name ::exports]))
(declare handle-single-export)
(declare handle-multiple-export)
(defn bitmap-export-handler
[{:keys [params browser cookies] :as request}]
(let [{:keys [exports page-id object-id name]} (us/conform ::bitmap-handler-params params)
token (.get ^js cookies "auth-token")]
(case (count exports)
0 (exc/raise :type :validation :code :missing-exports)
1 (handle-single-export
request
(assoc (first exports)
:name name
:token token
:page-id page-id
:object-id object-id))
(handle-multiple-export
request
(->> (d/enumerate exports)
(map (fn [[index item]]
(assoc item
:name name
:index index
:token token
:page-id page-id
:object-id object-id))))))))
(defn perform-bitmap-export
[browser params]
(p/let [content (screenshot-object browser params)]
{:content content
:filename (str (str/slug (:name params))
(if (not (str/blank? (:suffix params "")))
(:suffix params "")
(let [index (:index params 0)]
(when (pos? index)
(str "-" (inc index)))))
".png")
:length (alength content)
:mime-type "image/png"}))
(defn handle-single-export
[{:keys [browser]} params]
(p/let [result (perform-bitmap-export browser params)]
{:status 200
:body (:content result)
:headers {"content-type" (:mime-type result)
"content-length" (:length result)}}))
(defn handle-multiple-export
[{:keys [browser]} exports]
(let [proms (map (partial perform-bitmap-export browser) exports)]
(-> (p/all proms)
(p/then (fn [results]
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
(p/then (fn [fzip]
{:status 200
:headers {"content-type" "application/zip"}
:body (.generateNodeStream ^js fzip)})))))

View file

@ -1,73 +0,0 @@
(ns app.http.screenshot
(:require
[app.browser :as bwr]
[app.config :as cfg]
[lambdaisland.glogi :as log]
[cljs.spec.alpha :as s]
[promesa.core :as p]
[uxbox.common.exceptions :as exc :include-macros true]
[uxbox.common.spec :as us])
(:import
goog.Uri))
(defn- load-and-screenshot
[page url cookie]
(p/do!
(bwr/emulate! page {:viewport [1920 1080]})
(bwr/set-cookie! page cookie)
(bwr/navigate! page url)
(bwr/sleep page 500)
(.evaluate page (js* "() => document.body.style.background = 'transparent'"))
;; (.screenshot ^js page #js {:omitBackground true :fullPage true})
(p/let [dom (.$ page "#screenshot")]
(.screenshot ^js dom #js {:omitBackground true}))))
(defn- take-screenshot
[browser {:keys [page-id object-id token]}]
(letfn [(on-browser [page]
(let [path (str "/render-object/" page-id "/" object-id)
uri (doto (Uri. (:public-uri cfg/config))
(.setPath "/")
(.setFragment path))
cookie {:domain (str (.getDomain uri)
":"
(.getPort uri))
:key "auth-token"
:value token}]
(log/info :uri (.toString uri))
(load-and-screenshot page (.toString uri) cookie)))]
(bwr/exec! browser on-browser)))
(s/def ::page-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::bitmap-handler-params
(s/keys :req-un [::page-id ::object-id]))
(defn bitmap-handler
[{:keys [params browser cookies] :as request}]
(let [params (us/conform ::bitmap-handler-params (:query params))
token (.get ^js cookies "auth-token")]
(-> (take-screenshot browser {:page-id (:page-id params)
:object-id (:object-id params)
:token token})
(p/then (fn [result]
{:status 200
:body result
:headers {"content-type" "image/png"
"content-length" (alength result)}})))))
(defn page-handler
[{:keys [params browser] :as request}]
(letfn [(screenshot [page uri]
(p/do!
(bwr/emulate! page {:viewport [1920 1080]})
(bwr/navigate! page uri)
(bwr/sleep page 500)
;; (.evaluate page (js* "() => document.body.style.background = 'transparent'"))
(.screenshot ^js page #js {:omitBackground false})))]
(p/let [uri (get-in params [:query :uri])
sht (bwr/exec! browser #(screenshot % uri))]
{:status 200
:body sht
:headers {"content-type" "image/png"
"content-length" (alength sht)}})))

View file

@ -0,0 +1,13 @@
(ns app.zipfile
(:require
["jszip" :as jszip]))
(defn create
[]
(new jszip))
(defn add!
[zfile name data]
(.file ^js zfile name data)
zfile)

View file

@ -416,7 +416,7 @@
}
},
"dashboard.sidebar.drafts" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:129" ],
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:128" ],
"translations" : {
"en" : "Drafts",
"fr" : "Brouillons",
@ -424,7 +424,7 @@
}
},
"dashboard.sidebar.libraries" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:135" ],
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:134" ],
"translations" : {
"en" : "Libraries",
"fr" : "Librairies",
@ -432,7 +432,7 @@
}
},
"dashboard.sidebar.recent" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:122" ],
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:121" ],
"translations" : {
"en" : "Recent",
"fr" : "Récent",
@ -528,7 +528,7 @@
}
},
"ds.search.placeholder" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:188" ],
"used-in" : [ "src/uxbox/main/ui/dashboard/sidebar.cljs:187" ],
"translations" : {
"en" : "Search...",
"fr" : "Rechercher...",
@ -584,7 +584,7 @@
}
},
"errors.image-format-unsupported" : {
"used-in" : [ "src/uxbox/main/data/images.cljs:376", "src/uxbox/main/data/workspace/persistence.cljs:365", "src/uxbox/main/data/users.cljs:177" ],
"used-in" : [ "src/uxbox/main/data/users.cljs:177", "src/uxbox/main/data/workspace/persistence.cljs:365", "src/uxbox/main/data/images.cljs:376" ],
"translations" : {
"en" : "The image format is not supported (must be svg, jpg or png).",
"fr" : "Le format d'image n'est pas supporté (doit être svg, jpg ou png).",
@ -592,7 +592,7 @@
}
},
"errors.image-too-large" : {
"used-in" : [ "src/uxbox/main/data/images.cljs:374", "src/uxbox/main/data/workspace/persistence.cljs:363", "src/uxbox/main/data/users.cljs:175" ],
"used-in" : [ "src/uxbox/main/data/users.cljs:175", "src/uxbox/main/data/workspace/persistence.cljs:363", "src/uxbox/main/data/images.cljs:374" ],
"translations" : {
"en" : "The image is too large to be inserted (must be under 5mb).",
"fr" : "L'image est trop grande (doit être inférieure à 5 Mo).",
@ -632,7 +632,7 @@
}
},
"errors.unexpected-error" : {
"used-in" : [ "src/uxbox/main/data/images.cljs:385", "src/uxbox/main/data/workspace/persistence.cljs:334", "src/uxbox/main/data/workspace/persistence.cljs:374", "src/uxbox/main/data/users.cljs:185", "src/uxbox/main/ui/auth/register.cljs:54", "src/uxbox/main/ui/settings/change_email.cljs:51" ],
"used-in" : [ "src/uxbox/main/data/users.cljs:185", "src/uxbox/main/data/workspace/persistence.cljs:334", "src/uxbox/main/data/workspace/persistence.cljs:374", "src/uxbox/main/data/images.cljs:385", "src/uxbox/main/ui/settings/change_email.cljs:51", "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:65", "src/uxbox/main/ui/auth/register.cljs:54" ],
"translations" : {
"en" : "An unexpected error occurred.",
"fr" : "Une erreur inattendue c'est produite",
@ -672,7 +672,7 @@
}
},
"image.loading" : {
"used-in" : [ "src/uxbox/main/data/images.cljs:393", "src/uxbox/main/data/workspace/persistence.cljs:341", "src/uxbox/main/data/workspace/persistence.cljs:382", "src/uxbox/main/data/users.cljs:191" ],
"used-in" : [ "src/uxbox/main/data/users.cljs:191", "src/uxbox/main/data/workspace/persistence.cljs:341", "src/uxbox/main/data/workspace/persistence.cljs:382", "src/uxbox/main/data/images.cljs:393" ],
"translations" : {
"en" : "Loading image...",
"fr" : "Chargement de l'image...",
@ -840,11 +840,12 @@
}
},
"settings.multiple" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:132", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:117", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:126" ],
"translations" : {
"en" : "Multiple",
"es" : "Múltiple"
}
"en" : null,
"fr" : null,
"es" : null
},
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:117", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:126", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:132" ]
},
"settings.new-email-label" : {
"used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:75" ],
@ -1367,27 +1368,33 @@
}
},
"workspace.options.design" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:70" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:65" ],
"translations" : {
"en" : "Design",
"fr" : "Conception",
"es" : "Diseño"
}
},
"workspace.options.export-object" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:78" ],
"workspace.options.export" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:115", "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:148" ],
"translations" : {
"en" : "Export"
}
},
"workspace.options.export-object" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:144" ],
"translations" : {
"en" : "Export shape"
}
},
"workspace.options.exporting-object" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:77" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:143" ],
"translations" : {
"en" : "Exporting..."
}
},
"workspace.options.fill" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:40", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:382" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:382", "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:40" ],
"translations" : {
"en" : "Fill",
"fr" : "Remplissage",
@ -1395,7 +1402,7 @@
}
},
"workspace.options.font-options" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:388" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:387" ],
"translations" : {
"en" : "Text",
"fr" : "Texte",
@ -1715,18 +1722,20 @@
}
},
"workspace.options.group-fill" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:39" ],
"translations" : {
"en" : "Group fill",
"es" : "Relleno de grupo"
}
"en" : null,
"fr" : null,
"es" : null
},
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:39" ]
},
"workspace.options.group-stroke" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:65" ],
"translations" : {
"en" : "Group stroke",
"es" : "Borde de grupo"
}
"en" : null,
"fr" : null,
"es" : null
},
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:65" ]
},
"workspace.options.navigate-to" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs:59" ],
@ -1745,7 +1754,7 @@
}
},
"workspace.options.position" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:112", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:125" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:144", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:125" ],
"translations" : {
"en" : "Position",
"fr" : "Position",
@ -1753,7 +1762,7 @@
}
},
"workspace.options.prototype" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:80" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options.cljs:74" ],
"translations" : {
"en" : "Prototype",
"fr" : "Prototype",
@ -1761,7 +1770,7 @@
}
},
"workspace.options.radius" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:158" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:186" ],
"translations" : {
"en" : "Radius",
"fr" : "Rayon",
@ -1769,7 +1778,7 @@
}
},
"workspace.options.rotation" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:131" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:163" ],
"translations" : {
"en" : "Rotation",
"fr" : "Rotation",
@ -1793,21 +1802,23 @@
}
},
"workspace.options.selection-fill" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:38" ],
"translations" : {
"en" : "Selection fill",
"es" : "Relleno de selección"
}
"en" : null,
"fr" : null,
"es" : null
},
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:38" ]
},
"workspace.options.selection-stroke" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:64" ],
"translations" : {
"en" : "Selection stroke",
"es" : "Borde de selección"
}
"en" : null,
"fr" : null,
"es" : null
},
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:64" ]
},
"workspace.options.size" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:82", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:98" ],
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:114", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:98" ],
"translations" : {
"en" : "Size",
"fr" : "Taille",

View file

@ -760,3 +760,42 @@
cursor: auto;
}
}
.element-set-options-group {
display: flex;
padding: 3px;
border: 1px solid $color-black;
border-radius: 4px;
&:hover {
background: #1F1F1F;
}
}
.exports-options {
.element-set-options-group {
justify-content: space-between;
.delete-icon {
display: flex;
min-width: 40px;
min-height: 40px;
justify-content: center;
align-items: center;
cursor: pointer;
svg {
width: 20px;
height: 20px;
fill: $color-gray-20;
}
}
&:not(:first-child) {
margin-top: 7px;
}
}
.download-button {
margin-top: 10px;
}
}

View file

@ -10,11 +10,8 @@
(ns uxbox.main.ui.workspace.sidebar.options
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.data.messages :as dm]
[uxbox.main.data.workspace :as udw]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
@ -22,6 +19,7 @@
[uxbox.main.ui.icons :as i]
[uxbox.main.ui.workspace.sidebar.align :refer [align-options]]
[uxbox.main.ui.workspace.sidebar.options.circle :as circle]
[uxbox.main.ui.workspace.sidebar.options.exports :refer [exports-menu]]
[uxbox.main.ui.workspace.sidebar.options.frame :as frame]
[uxbox.main.ui.workspace.sidebar.options.group :as group]
[uxbox.main.ui.workspace.sidebar.options.icon :as icon]
@ -32,61 +30,11 @@
[uxbox.main.ui.workspace.sidebar.options.interactions :refer [interactions-menu]]
[uxbox.main.ui.workspace.sidebar.options.path :as path]
[uxbox.main.ui.workspace.sidebar.options.rect :as rect]
[uxbox.util.dom :as dom]
[uxbox.util.http :as http]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.object :as obj]))
[uxbox.main.ui.workspace.sidebar.options.text :as text]
[uxbox.util.i18n :as i18n :refer [tr t]]))
;; --- Options
(defn- request-screenshot
[page-id shape-id]
(http/send! {:method :get
:uri "/export/bitmap"
:query {:page-id page-id
:object-id shape-id}}
{:credentials? true
:response-type :blob}))
(defn- trigger-download
[name blob]
(let [link (dom/create-element "a")
uri (dom/create-uri blob)]
(obj/set! link "href" uri)
(obj/set! link "download" (str/slug name))
(obj/set! (.-style ^js link) "display" "none")
(.appendChild (.-body ^js js/document) link)
(.click link)
(.remove link)))
(mf/defc shape-export
{::mf/wrap [mf/memo]}
[{:keys [shape page] :as props}]
(let [loading? (mf/use-state false)
locale (mf/deref i18n/locale)
on-click (fn [event]
(dom/prevent-default event)
(swap! loading? not)
(->> (request-screenshot (:id page) (:id shape))
(rx/subs
(fn [{:keys [status body] :as response}]
(if (= status 200)
(trigger-download (:name shape) body)
(st/emit! (dm/error (tr "errors.unexpected-error")))))
(constantly nil)
(fn []
(swap! loading? not)))))]
[:div.element-set
[:div.btn-large.btn-icon-dark
{:on-click (when-not @loading? on-click)
:class (dom/classnames
:btn-disabled @loading?)
:disabled @loading?}
(if @loading?
(t locale "workspace.options.exporting-object")
(t locale "workspace.options.export-object"))]]))
(mf/defc shape-options
{::mf/wrap [#(mf/throttle % 60)]}
[{:keys [shape page] :as props}]
@ -102,7 +50,7 @@
:curve [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}]
nil)
[:& shape-export {:shape shape :page page}]])
[:& exports-menu {:shape shape :page page}]])
(mf/defc options-content

View file

@ -0,0 +1,151 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.workspace.sidebar.options.exports
(:require
[cuerdas.core :as str]
[beicon.core :as rx]
[rumext.alpha :as mf]
[uxbox.common.data :as d]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.messages :as dm]
[uxbox.main.data.workspace :as udw]
[uxbox.main.store :as st]
[uxbox.util.object :as obj]
[uxbox.util.dom :as dom]
[uxbox.util.http-api :as http]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(defn- request-export
[shape exports]
(http/send! {:method :post
:uri "/export/bitmap"
:response-type :blob
:auth true
:body {:page-id (:page-id shape)
:object-id (:id shape)
:name (:name shape)
:exports exports}}))
(defn- trigger-download
[name blob]
(let [link (dom/create-element "a")
uri (dom/create-uri blob)]
(obj/set! link "href" uri)
(obj/set! link "download" (str/slug name))
(obj/set! (.-style ^js link) "display" "none")
(.appendChild (.-body ^js js/document) link)
(.click link)
(.remove link)))
(mf/defc exports-menu
[{:keys [shape page] :as props}]
(let [locale (mf/deref i18n/locale)
exports (:exports shape [])
loading? (mf/use-state false)
on-download
(mf/use-callback
(mf/deps shape)
(fn [event]
(dom/prevent-default event)
(swap! loading? not)
(->> (request-export (assoc shape :page-id (:id page)) exports)
(rx/subs
(fn [{:keys [status body] :as response}]
(js/console.log status body)
(if (= status 200)
(trigger-download (:name shape) body)
(st/emit! (dm/error (tr "errors.unexpected-error")))))
(constantly nil)
(fn []
(swap! loading? not))))))
_ (prn "exports-menu" exports)
add-export
(mf/use-callback
(mf/deps shape)
(fn []
(let [xspec {:type :png
:suffix ""
:scale 1}]
(st/emit! (udw/update-shape (:id shape)
{:exports (conj exports xspec)})))))
delete-export
(mf/use-callback
(mf/deps shape)
(fn [index]
(let [[before after] (split-at index exports)
exports (d/concat [] before (rest after))]
(st/emit! (udw/update-shape (:id shape)
{:exports exports})))))
on-scale-change
(mf/use-callback
(mf/deps shape)
(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})))))
on-suffix-change
(mf/use-callback
(mf/deps shape)
(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})))))]
(if (seq exports)
[:div.element-set.exports-options
[:div.element-set-title
[:span (t locale "workspace.options.export")]
[:div.add-page {:on-click add-export} i/close]]
[:div.element-set-content
(for [[index export] (d/enumerate exports)]
[:div.element-set-options-group
{:key index}
[:select.input-select {:on-change (partial on-scale-change index)
:value (:scale export)}
[:option {:value "0.5"} "0.5x"]
[:option {:value "0.75"} "0.75x"]
[:option {:value "1"} "1x"]
[:option {:value "1.5"} "1.5x"]
[:option {:value "2"} "2x"]
[:option {:value "4"} "4x"]
[:option {:value "6"} "6x"]]
[:input.input-text {:value (:suffix export)
:on-change (partial on-suffix-change index)}]
[:select.input-select
[:option {:value "png"} "PNG"]]
[:div.delete-icon {:on-click (partial delete-export index)}
i/trash]])
[:div.btn-large.btn-icon-dark.download-button
{:on-click (when-not @loading? on-download)
:class (dom/classnames
:btn-disabled @loading?)
:disabled @loading?}
(if @loading?
(t locale "workspace.options.exporting-object")
(t locale "workspace.options.export-object"))]]]
[:div.element-set
[:div.element-set-title
[:span (t locale "workspace.options.export")]
[:div.add-page {:on-click add-export} i/close]]])))