mirror of
https://github.com/penpot/penpot.git
synced 2026-09-11 06:28:47 +00:00
Merge pull request #1006 from penpot/feat/export-import
Import/export (partial)
This commit is contained in:
commit
31689cd947
@ -54,11 +54,11 @@
|
|||||||
(db/insert! conn :file-profile-rel))))
|
(db/insert! conn :file-profile-rel))))
|
||||||
|
|
||||||
(defn create-file
|
(defn create-file
|
||||||
[conn {:keys [id name project-id is-shared]
|
[conn {:keys [id name project-id is-shared data]
|
||||||
:or {is-shared false}
|
:or {is-shared false}
|
||||||
:as params}]
|
:as params}]
|
||||||
(let [id (or id (uuid/next))
|
(let [id (or id (:id data) (uuid/next))
|
||||||
data (cp/make-file-data id)
|
data (or data (cp/make-file-data id))
|
||||||
file (db/insert! conn :file
|
file (db/insert! conn :file
|
||||||
{:id id
|
{:id id
|
||||||
:project-id project-id
|
:project-id project-id
|
||||||
|
|||||||
159
common/src/app/common/file_builder.cljc
Normal file
159
common/src/app/common/file_builder.cljc
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
;; 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.common.file-builder
|
||||||
|
"A version parsing helper."
|
||||||
|
(:require
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.pages.changes :as ch]
|
||||||
|
[app.common.pages.init :as init]
|
||||||
|
[app.common.pages.spec :as spec]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]))
|
||||||
|
|
||||||
|
(def root-frame uuid/zero)
|
||||||
|
|
||||||
|
;; This flag controls if we should execute spec validation after every commit
|
||||||
|
(def verify-on-commit? true)
|
||||||
|
|
||||||
|
(defn- commit-change [file change]
|
||||||
|
(when verify-on-commit?
|
||||||
|
(us/assert ::spec/change change))
|
||||||
|
(-> file
|
||||||
|
(update :changes conj change)
|
||||||
|
(update :data ch/process-changes [change] verify-on-commit?)))
|
||||||
|
|
||||||
|
(defn- lookup-objects
|
||||||
|
([file]
|
||||||
|
(lookup-objects file (:current-page-id file)))
|
||||||
|
|
||||||
|
([file page-id]
|
||||||
|
(get-in file [:data :pages-index page-id :objects])))
|
||||||
|
|
||||||
|
(defn- lookup-shape [file shape-id]
|
||||||
|
(-> (lookup-objects file)
|
||||||
|
(get shape-id)))
|
||||||
|
|
||||||
|
(defn- commit-shape [file obj]
|
||||||
|
(let [page-id (:current-page-id file)
|
||||||
|
frame-id (:current-frame-id file)
|
||||||
|
parent-id (-> file :parent-stack peek)]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-obj
|
||||||
|
:id (:id obj)
|
||||||
|
:page-id page-id
|
||||||
|
:frame-id frame-id
|
||||||
|
:parent-id parent-id
|
||||||
|
:obj obj}))))
|
||||||
|
|
||||||
|
;; PUBLIC API
|
||||||
|
|
||||||
|
(defn create-file
|
||||||
|
([name]
|
||||||
|
(let [id (uuid/next)]
|
||||||
|
{:id id
|
||||||
|
:name name
|
||||||
|
:data (-> init/empty-file-data
|
||||||
|
(assoc :id id))
|
||||||
|
|
||||||
|
;; We keep the changes so we can send them to the backend
|
||||||
|
:changes []})))
|
||||||
|
|
||||||
|
(defn add-page
|
||||||
|
[file name]
|
||||||
|
(let [page-id (uuid/next)]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-page
|
||||||
|
:id page-id
|
||||||
|
:name name
|
||||||
|
:page (-> init/empty-page-data
|
||||||
|
(assoc :name name))})
|
||||||
|
|
||||||
|
;; Current page being edited
|
||||||
|
(assoc :current-page-id page-id)
|
||||||
|
|
||||||
|
;; Current frame-id
|
||||||
|
(assoc :current-frame-id root-frame)
|
||||||
|
|
||||||
|
;; Current parent stack we'll be nesting
|
||||||
|
(assoc :parent-stack [root-frame]))))
|
||||||
|
|
||||||
|
(defn add-artboard [file data]
|
||||||
|
(let [obj (-> (init/make-minimal-shape :frame)
|
||||||
|
(merge data))]
|
||||||
|
(-> file
|
||||||
|
(commit-shape obj)
|
||||||
|
(assoc :current-frame-id (:id obj))
|
||||||
|
(update :parent-stack conj (:id obj)))))
|
||||||
|
|
||||||
|
(defn close-artboard [file]
|
||||||
|
(-> file
|
||||||
|
(assoc :current-frame-id root-frame)
|
||||||
|
(update :parent-stack pop)))
|
||||||
|
|
||||||
|
(defn add-group [file data]
|
||||||
|
(let [frame-id (:current-frame-id file)
|
||||||
|
selrect init/empty-selrect
|
||||||
|
name (:name data)
|
||||||
|
obj (-> (init/make-minimal-group frame-id selrect name)
|
||||||
|
(merge data))]
|
||||||
|
(-> file
|
||||||
|
(commit-shape obj)
|
||||||
|
(update :parent-stack conj (:id obj)))))
|
||||||
|
|
||||||
|
(defn close-group [file]
|
||||||
|
(let [group-id (-> file :parent-stack peek)
|
||||||
|
group (lookup-shape file group-id)
|
||||||
|
shapes (->> group :shapes (mapv #(lookup-shape file %)))
|
||||||
|
selrect (gsh/selection-rect shapes)
|
||||||
|
points (gsh/rect->points selrect)]
|
||||||
|
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :mod-obj
|
||||||
|
:page-id (:current-page-id file)
|
||||||
|
:id group-id
|
||||||
|
:operations
|
||||||
|
[{:type :set :attr :selrect :val selrect}
|
||||||
|
{:type :set :attr :points :val points}]})
|
||||||
|
(update :parent-stack pop))))
|
||||||
|
|
||||||
|
(defn create-shape [file type data]
|
||||||
|
(let [frame-id (:current-frame-id file)
|
||||||
|
frame (when-not (= frame-id root-frame)
|
||||||
|
(lookup-shape file frame-id))
|
||||||
|
obj (-> (init/make-minimal-shape type)
|
||||||
|
(merge data)
|
||||||
|
(cond-> frame
|
||||||
|
(gsh/translate-from-frame frame)))]
|
||||||
|
(commit-shape file obj)))
|
||||||
|
|
||||||
|
(defn create-rect [file data]
|
||||||
|
(create-shape file :rect data))
|
||||||
|
|
||||||
|
(defn create-circle [file data]
|
||||||
|
(create-shape file :circle data))
|
||||||
|
|
||||||
|
(defn create-path [file data]
|
||||||
|
(create-shape file :path data))
|
||||||
|
|
||||||
|
(defn create-text [file data]
|
||||||
|
(create-shape file :text data))
|
||||||
|
|
||||||
|
(defn create-image [file data]
|
||||||
|
(create-shape file :image data))
|
||||||
|
|
||||||
|
(defn close-page [file]
|
||||||
|
(-> file
|
||||||
|
(dissoc :current-page-id)
|
||||||
|
(dissoc :parent-stack)))
|
||||||
|
|
||||||
|
(defn generate-changes
|
||||||
|
[file]
|
||||||
|
(:changes file))
|
||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
#?(:cljs [cljs.pprint :as pp]
|
#?(:cljs [cljs.pprint :as pp]
|
||||||
:clj [clojure.pprint :as pp])
|
:clj [clojure.pprint :as pp])
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.math :as mth]))
|
[app.common.math :as mth]))
|
||||||
|
|
||||||
@ -25,6 +26,15 @@
|
|||||||
([a b c d e f]
|
([a b c d e f]
|
||||||
(Matrix. a b c d e f)))
|
(Matrix. a b c d e f)))
|
||||||
|
|
||||||
|
(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
|
||||||
|
|
||||||
|
(defn str->matrix
|
||||||
|
[matrix-str]
|
||||||
|
(let [params (->> (re-seq number-regex matrix-str)
|
||||||
|
(filter #(-> % first empty? not))
|
||||||
|
(map (comp d/parse-double first)))]
|
||||||
|
(apply matrix params)))
|
||||||
|
|
||||||
(defn multiply
|
(defn multiply
|
||||||
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
||||||
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
||||||
|
|||||||
@ -100,6 +100,10 @@
|
|||||||
[shape {:keys [x y]}]
|
[shape {:keys [x y]}]
|
||||||
(gtr/move shape (gpt/negate (gpt/point x y))) )
|
(gtr/move shape (gpt/negate (gpt/point x y))) )
|
||||||
|
|
||||||
|
(defn translate-from-frame
|
||||||
|
[shape {:keys [x y]}]
|
||||||
|
(gtr/move shape (gpt/point x y)) )
|
||||||
|
|
||||||
;; --- Helpers
|
;; --- Helpers
|
||||||
|
|
||||||
(defn fully-contained?
|
(defn fully-contained?
|
||||||
|
|||||||
@ -84,6 +84,7 @@
|
|||||||
(d/export init/make-file-data)
|
(d/export init/make-file-data)
|
||||||
(d/export init/make-minimal-shape)
|
(d/export init/make-minimal-shape)
|
||||||
(d/export init/make-minimal-group)
|
(d/export init/make-minimal-group)
|
||||||
|
(d/export init/empty-file-data)
|
||||||
|
|
||||||
;; Specs
|
;; Specs
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,12 @@
|
|||||||
|
|
||||||
{:type :svg-raw}])
|
{:type :svg-raw}])
|
||||||
|
|
||||||
|
(def empty-selrect
|
||||||
|
{:x 0 :y 0
|
||||||
|
:x1 0 :y1 0
|
||||||
|
:x2 1 :y2 1
|
||||||
|
:width 1 :height 1})
|
||||||
|
|
||||||
(defn make-minimal-shape
|
(defn make-minimal-shape
|
||||||
[type]
|
[type]
|
||||||
(let [type (cond (= type :curve) :path
|
(let [type (cond (= type :curve) :path
|
||||||
@ -126,10 +132,11 @@
|
|||||||
:height (:height selection-rect)})
|
:height (:height selection-rect)})
|
||||||
|
|
||||||
(defn make-file-data
|
(defn make-file-data
|
||||||
([file-id] (make-file-data file-id(uuid/next)))
|
([file-id]
|
||||||
|
(make-file-data file-id (uuid/next)))
|
||||||
|
|
||||||
([file-id page-id]
|
([file-id page-id]
|
||||||
(let [
|
(let [pd (assoc empty-page-data
|
||||||
pd (assoc empty-page-data
|
|
||||||
:id page-id
|
:id page-id
|
||||||
:name "Page-1")]
|
:name "Page-1")]
|
||||||
(-> empty-file-data
|
(-> empty-file-data
|
||||||
|
|||||||
@ -16,7 +16,8 @@ goog.scope(function() {
|
|||||||
const self = app.common.uuid_impl;
|
const self = app.common.uuid_impl;
|
||||||
|
|
||||||
const fill = (() => {
|
const fill = (() => {
|
||||||
if (typeof global.crypto !== "undefined") {
|
if (typeof global.crypto !== "undefined" &&
|
||||||
|
typeof global.crypto.getRandomValues !== "undefined") {
|
||||||
return (buf) => {
|
return (buf) => {
|
||||||
global.crypto.getRandomValues(buf);
|
global.crypto.getRandomValues(buf);
|
||||||
return buf;
|
return buf;
|
||||||
|
|||||||
@ -165,3 +165,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-file-btn {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4px;
|
||||||
|
width: 2rem;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: 1px solid $color-gray-20;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.4s;
|
||||||
|
margin-left: 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
|
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
|
||||||
:dev-http {8888 "classpath:public"}
|
:dev-http {8888 "classpath:public"}
|
||||||
|
|
||||||
|
|
||||||
:builds
|
:builds
|
||||||
{:main
|
{:main
|
||||||
{:target :browser
|
{:target :browser
|
||||||
@ -35,6 +36,27 @@
|
|||||||
:anon-fn-naming-policy :off
|
:anon-fn-naming-policy :off
|
||||||
:source-map-detail-level :all}}}
|
:source-map-detail-level :all}}}
|
||||||
|
|
||||||
|
:lib-penpot
|
||||||
|
{:target :esm
|
||||||
|
:output-dir "resources/public/libs"
|
||||||
|
|
||||||
|
:modules
|
||||||
|
{:penpot {:exports {:renderPage app.libs.render/render-page-export
|
||||||
|
:createFile app.libs.file-builder/create-file-export}}}
|
||||||
|
|
||||||
|
:compiler-options
|
||||||
|
{:output-feature-set :es8
|
||||||
|
:output-wrapper false
|
||||||
|
:warnings {:fn-deprecated false}}
|
||||||
|
|
||||||
|
:release
|
||||||
|
{:compiler-options
|
||||||
|
{:fn-invoke-direct true
|
||||||
|
:source-map true
|
||||||
|
:elide-asserts true
|
||||||
|
:anon-fn-naming-policy :off
|
||||||
|
:source-map-detail-level :all}}}
|
||||||
|
|
||||||
:test
|
:test
|
||||||
{:target :node-test
|
{:target :node-test
|
||||||
:output-to "target/tests.js"
|
:output-to "target/tests.js"
|
||||||
@ -45,4 +67,3 @@
|
|||||||
{:output-feature-set :es8
|
{:output-feature-set :es8
|
||||||
:output-wrapper false
|
:output-wrapper false
|
||||||
:warnings {:fn-deprecated false}}}}}
|
:warnings {:fn-deprecated false}}}}}
|
||||||
|
|
||||||
|
|||||||
23
frontend/src/app/libs/file_builder.cljs
Normal file
23
frontend/src/app/libs/file_builder.cljs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
;; 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.libs.file-builder
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.file-builder :as fb]))
|
||||||
|
|
||||||
|
(deftype File [^:mutable file]
|
||||||
|
Object
|
||||||
|
(addPage [self name]
|
||||||
|
(set! file (fb/add-page file name))
|
||||||
|
(str (:current-page-id file))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn create-file-export [^string name]
|
||||||
|
(File. (fb/create-file name)))
|
||||||
|
|
||||||
|
(defn exports []
|
||||||
|
#js { :createFile create-file-export })
|
||||||
28
frontend/src/app/libs/render.cljs
Normal file
28
frontend/src/app/libs/render.cljs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
;; 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.libs.render
|
||||||
|
(:require
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.render :as r]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
(defn render-page-export
|
||||||
|
[file ^string page-id]
|
||||||
|
|
||||||
|
;; Better to expose the api as a promise to be consumed from JS
|
||||||
|
(let [page-id (uuid/uuid page-id)
|
||||||
|
file-data (.-file file)
|
||||||
|
data (get-in file-data [:data :pages-index page-id])]
|
||||||
|
(p/create
|
||||||
|
(fn [resolve reject]
|
||||||
|
(->> (r/render-page data)
|
||||||
|
(rx/take 1)
|
||||||
|
(rx/subs resolve reject))) )))
|
||||||
|
|
||||||
|
(defn exports []
|
||||||
|
#js {:renderPage render-page-export})
|
||||||
74
frontend/src/app/main/render.cljs
Normal file
74
frontend/src/app/main/render.cljs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
;; 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.render
|
||||||
|
(:require
|
||||||
|
["react-dom/server" :as rds]
|
||||||
|
[app.config :as cfg]
|
||||||
|
[app.main.exports :as exports]
|
||||||
|
[app.main.exports :as svg]
|
||||||
|
[app.main.fonts :as fonts]
|
||||||
|
[app.util.http :as http]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[clojure.set :as set]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(defn- text? [{type :type}]
|
||||||
|
(= type :text))
|
||||||
|
|
||||||
|
(defn- get-image-data [shape]
|
||||||
|
(cond
|
||||||
|
(= :image (:type shape))
|
||||||
|
[(:metadata shape)]
|
||||||
|
|
||||||
|
(some? (:fill-image shape))
|
||||||
|
[(:fill-image shape)]
|
||||||
|
|
||||||
|
:else
|
||||||
|
[]))
|
||||||
|
|
||||||
|
(defn populate-images-cache
|
||||||
|
([data]
|
||||||
|
(populate-images-cache data nil))
|
||||||
|
|
||||||
|
([data {:keys [resolve-media?] :or {resolve-media? false}}]
|
||||||
|
(let [images (->> (:objects data)
|
||||||
|
(vals)
|
||||||
|
(mapcat get-image-data))]
|
||||||
|
(->> (rx/from images)
|
||||||
|
(rx/map #(cfg/resolve-file-media %))
|
||||||
|
(rx/flat-map http/fetch-data-uri)))))
|
||||||
|
|
||||||
|
(defn populate-fonts-cache [data]
|
||||||
|
(let [texts (->> (:objects data)
|
||||||
|
(vals)
|
||||||
|
(filterv text?)
|
||||||
|
(mapv :content)) ]
|
||||||
|
|
||||||
|
(->> (rx/from texts)
|
||||||
|
(rx/map fonts/get-content-fonts)
|
||||||
|
(rx/reduce set/union #{})
|
||||||
|
(rx/flat-map identity)
|
||||||
|
(rx/flat-map fonts/fetch-font-css)
|
||||||
|
(rx/flat-map fonts/extract-fontface-urls)
|
||||||
|
(rx/flat-map http/fetch-data-uri))))
|
||||||
|
|
||||||
|
(defn render-page
|
||||||
|
[data]
|
||||||
|
(rx/concat
|
||||||
|
(->> (rx/merge
|
||||||
|
(populate-images-cache data)
|
||||||
|
(populate-fonts-cache data))
|
||||||
|
(rx/ignore))
|
||||||
|
|
||||||
|
(->> (rx/of data)
|
||||||
|
(rx/map
|
||||||
|
(fn [data]
|
||||||
|
(let [elem (mf/element exports/page-svg #js {:data data :embed? true})]
|
||||||
|
(rds/renderToStaticMarkup elem)))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -11,8 +11,9 @@
|
|||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.context :as ctx]
|
|
||||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||||
|
[app.main.ui.context :as ctx]
|
||||||
|
[app.main.worker :as uw]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
@ -150,7 +151,22 @@
|
|||||||
:hint (tr "modals.remove-shared-confirm.hint")
|
:hint (tr "modals.remove-shared-confirm.hint")
|
||||||
:cancel-label :omit
|
:cancel-label :omit
|
||||||
:accept-label (tr "modals.remove-shared-confirm.accept")
|
:accept-label (tr "modals.remove-shared-confirm.accept")
|
||||||
:on-accept del-shared})))]
|
:on-accept del-shared})))
|
||||||
|
|
||||||
|
on-export-files
|
||||||
|
(fn [event]
|
||||||
|
(->> (uw/ask-many!
|
||||||
|
{:cmd :export-file
|
||||||
|
:team-id current-team-id
|
||||||
|
:files files})
|
||||||
|
(rx/subs
|
||||||
|
(fn [{:keys [type data] :as msg}]
|
||||||
|
(case type
|
||||||
|
:progress
|
||||||
|
(prn "[Progress]" data)
|
||||||
|
|
||||||
|
:finish
|
||||||
|
(dom/save-as data "export" "application/zip" "Export package (*.zip)"))))))]
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(fn []
|
(fn []
|
||||||
@ -176,6 +192,7 @@
|
|||||||
[[(tr "dashboard.duplicate-multi" file-count) on-duplicate]
|
[[(tr "dashboard.duplicate-multi" file-count) on-duplicate]
|
||||||
(when (or (seq current-projects) (seq other-teams))
|
(when (or (seq current-projects) (seq other-teams))
|
||||||
[(tr "dashboard.move-to-multi" file-count) nil sub-options])
|
[(tr "dashboard.move-to-multi" file-count) nil sub-options])
|
||||||
|
#_[(tr "dashboard.export-multi" file-count) on-export-files]
|
||||||
[:separator]
|
[:separator]
|
||||||
[(tr "labels.delete-multi-files" file-count) on-delete]]
|
[(tr "labels.delete-multi-files" file-count) on-delete]]
|
||||||
|
|
||||||
@ -187,6 +204,7 @@
|
|||||||
(if (:is-shared file)
|
(if (:is-shared file)
|
||||||
[(tr "dashboard.remove-shared") on-del-shared]
|
[(tr "dashboard.remove-shared") on-del-shared]
|
||||||
[(tr "dashboard.add-shared") on-add-shared])
|
[(tr "dashboard.add-shared") on-add-shared])
|
||||||
|
#_[(tr "dashboard.export-single") on-export-files]
|
||||||
[:separator]
|
[:separator]
|
||||||
[(tr "labels.delete") on-delete]])]
|
[(tr "labels.delete") on-delete]])]
|
||||||
|
|
||||||
|
|||||||
57
frontend/src/app/main/ui/dashboard/import.cljs
Normal file
57
frontend/src/app/main/ui/dashboard/import.cljs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
;; 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.dashboard.import
|
||||||
|
(:require
|
||||||
|
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||||
|
[app.main.ui.icons :as i]
|
||||||
|
[app.main.worker :as uw]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.logging :as log]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(log/set-level! :debug)
|
||||||
|
|
||||||
|
(defn use-import-file
|
||||||
|
[project-id on-finish-import]
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps project-id on-finish-import)
|
||||||
|
(fn [files]
|
||||||
|
(when files
|
||||||
|
(let [files (->> files (mapv dom/create-uri))]
|
||||||
|
(->> (uw/ask-many!
|
||||||
|
{:cmd :import-file
|
||||||
|
:project-id project-id
|
||||||
|
:files files})
|
||||||
|
|
||||||
|
(rx/subs
|
||||||
|
(fn [result]
|
||||||
|
(log/debug :action "import-result" :result result))
|
||||||
|
|
||||||
|
(fn [err]
|
||||||
|
(log/debug :action "import-error" :result err))
|
||||||
|
|
||||||
|
(fn []
|
||||||
|
(log/debug :action "import-end")
|
||||||
|
(when on-finish-import (on-finish-import))))))))))
|
||||||
|
|
||||||
|
(mf/defc import-button
|
||||||
|
[{:keys [project-id on-finish-import]}]
|
||||||
|
|
||||||
|
(let [file-input (mf/use-ref nil)
|
||||||
|
on-file-selected (use-import-file project-id on-finish-import)]
|
||||||
|
[:form.import-file
|
||||||
|
[:button.import-file-btn {:type "button"
|
||||||
|
:on-click #(dom/click (mf/ref-val file-input))} i/import]
|
||||||
|
[:& file-uploader {:accept "application/zip"
|
||||||
|
:multi true
|
||||||
|
:input-ref file-input
|
||||||
|
:on-selected on-file-selected}]]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +21,8 @@
|
|||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]
|
||||||
|
[app.main.ui.dashboard.import :refer [import-button]]))
|
||||||
|
|
||||||
(mf/defc header
|
(mf/defc header
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
@ -30,6 +31,7 @@
|
|||||||
[:header.dashboard-header
|
[:header.dashboard-header
|
||||||
[:div.dashboard-title
|
[:div.dashboard-title
|
||||||
[:h1 (tr "dashboard.projects-title")]]
|
[:h1 (tr "dashboard.projects-title")]]
|
||||||
|
|
||||||
[:a.btn-secondary.btn-small {:on-click create}
|
[:a.btn-secondary.btn-small {:on-click create}
|
||||||
(tr "dashboard.new-project")]]))
|
(tr "dashboard.new-project")]]))
|
||||||
|
|
||||||
@ -96,7 +98,13 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(let [mdata {:on-success on-file-created}
|
(let [mdata {:on-success on-file-created}
|
||||||
params {:project-id (:id project)}]
|
params {:project-id (:id project)}]
|
||||||
(st/emit! (dd/create-file (with-meta params mdata))))))]
|
(st/emit! (dd/create-file (with-meta params mdata))))))
|
||||||
|
|
||||||
|
on-finish-import
|
||||||
|
(mf/use-callback
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dd/fetch-recent-files)
|
||||||
|
(dd/clear-selected-files))))]
|
||||||
|
|
||||||
[:div.dashboard-project-row {:class (when first? "first")}
|
[:div.dashboard-project-row {:class (when first? "first")}
|
||||||
[:div.project
|
[:div.project
|
||||||
@ -130,6 +138,9 @@
|
|||||||
(dt/timeago {:locale locale}))]
|
(dt/timeago {:locale locale}))]
|
||||||
[:span.recent-files-row-title-info (str ", " time)]))
|
[:span.recent-files-row-title-info (str ", " time)]))
|
||||||
|
|
||||||
|
#_[:& import-button {:project-id (:id project)
|
||||||
|
:on-finish-import on-finish-import}]
|
||||||
|
|
||||||
[:a.btn-secondary.btn-small
|
[:a.btn-secondary.btn-small
|
||||||
{:on-click create-file}
|
{:on-click create-file}
|
||||||
(tr "dashboard.new-file")]]
|
(tr "dashboard.new-file")]]
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.main.ui.icons
|
(ns app.main.ui.icons
|
||||||
|
(:refer-clojure :exclude [import])
|
||||||
(:require-macros [app.main.ui.icons :refer [icon-xref]])
|
(:require-macros [app.main.ui.icons :refer [icon-xref]])
|
||||||
(:require [rumext.alpha :as mf]))
|
(:require [rumext.alpha :as mf]))
|
||||||
|
|
||||||
@ -53,6 +54,7 @@
|
|||||||
(def icon-set (icon-xref :icon-set))
|
(def icon-set (icon-xref :icon-set))
|
||||||
(def icon-verify (icon-xref :icon-verify))
|
(def icon-verify (icon-xref :icon-verify))
|
||||||
(def image (icon-xref :image))
|
(def image (icon-xref :image))
|
||||||
|
(def import (icon-xref :import))
|
||||||
(def infocard (icon-xref :infocard))
|
(def infocard (icon-xref :infocard))
|
||||||
(def interaction (icon-xref :interaction))
|
(def interaction (icon-xref :interaction))
|
||||||
(def layers (icon-xref :layers))
|
(def layers (icon-xref :layers))
|
||||||
@ -60,9 +62,9 @@
|
|||||||
(def libraries (icon-xref :libraries))
|
(def libraries (icon-xref :libraries))
|
||||||
(def library (icon-xref :library))
|
(def library (icon-xref :library))
|
||||||
(def line (icon-xref :line))
|
(def line (icon-xref :line))
|
||||||
|
(def line-height (icon-xref :line-height))
|
||||||
(def listing-enum (icon-xref :listing-enum))
|
(def listing-enum (icon-xref :listing-enum))
|
||||||
(def listing-thumbs (icon-xref :listing-thumbs))
|
(def listing-thumbs (icon-xref :listing-thumbs))
|
||||||
(def line-height (icon-xref :line-height))
|
|
||||||
(def loader (icon-xref :loader))
|
(def loader (icon-xref :loader))
|
||||||
(def lock (icon-xref :lock))
|
(def lock (icon-xref :lock))
|
||||||
(def logo (icon-xref :uxbox-logo))
|
(def logo (icon-xref :uxbox-logo))
|
||||||
|
|||||||
@ -78,7 +78,8 @@
|
|||||||
:height height
|
:height height
|
||||||
:version "1.1"
|
:version "1.1"
|
||||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||||
:xmlns "http://www.w3.org/2000/svg"}
|
:xmlns "http://www.w3.org/2000/svg"
|
||||||
|
:xmlns:penpot "https://penpot.app/xmlns"}
|
||||||
(case (:type object)
|
(case (:type object)
|
||||||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||||
:group [:> shape-container {:shape object}
|
:group [:> shape-container {:shape object}
|
||||||
|
|||||||
@ -32,6 +32,5 @@
|
|||||||
:ry ry
|
:ry ry
|
||||||
:transform transform}))]
|
:transform transform}))]
|
||||||
|
|
||||||
[:& shape-custom-stroke {:shape shape
|
[:& shape-custom-stroke {:shape shape}
|
||||||
:base-props props
|
[:> :ellipse props]]))
|
||||||
:elem-name "ellipse"}]))
|
|
||||||
|
|||||||
@ -6,10 +6,130 @@
|
|||||||
|
|
||||||
(ns app.main.ui.shapes.custom-stroke
|
(ns app.main.ui.shapes.custom-stroke
|
||||||
(:require
|
(:require
|
||||||
[rumext.alpha :as mf]
|
[app.common.data :as d]
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.common.geom.shapes :as geom]
|
[app.common.geom.shapes :as geom]
|
||||||
[app.util.object :as obj]))
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.ui.context :as muc]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(defn add-props
|
||||||
|
[props new-props]
|
||||||
|
(-> props
|
||||||
|
(obj/merge (clj->js new-props))))
|
||||||
|
|
||||||
|
(defn add-style
|
||||||
|
[props new-style]
|
||||||
|
(let [old-style (obj/get props "style")
|
||||||
|
style (obj/merge old-style (clj->js new-style))]
|
||||||
|
(-> props (obj/merge #js {:style style}))))
|
||||||
|
|
||||||
|
(mf/defc inner-stroke-clip-path
|
||||||
|
[{:keys [shape render-id]}]
|
||||||
|
(let [clip-id (str "inner-stroke-" render-id)
|
||||||
|
shape-id (str "stroke-shape-" render-id)]
|
||||||
|
[:> "clipPath" #js {:id clip-id}
|
||||||
|
[:use {:href (str "#" shape-id)}]]))
|
||||||
|
|
||||||
|
(mf/defc outer-stroke-mask
|
||||||
|
[{:keys [shape render-id]}]
|
||||||
|
(let [stroke-mask-id (str "outer-stroke-" render-id)
|
||||||
|
shape-id (str "stroke-shape-" render-id)
|
||||||
|
stroke-width (:stroke-width shape 0)]
|
||||||
|
[:mask {:id stroke-mask-id}
|
||||||
|
[:use {:href (str "#" shape-id)
|
||||||
|
:style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
|
||||||
|
|
||||||
|
[:use {:href (str "#" shape-id)
|
||||||
|
:style #js {:fill "black"}}]]))
|
||||||
|
|
||||||
|
(mf/defc stroke-defs
|
||||||
|
[{:keys [shape render-id]}]
|
||||||
|
(cond
|
||||||
|
(and (= :inner (:stroke-alignment shape :center))
|
||||||
|
(> (:stroke-width shape 0) 0))
|
||||||
|
[:& inner-stroke-clip-path {:shape shape
|
||||||
|
:render-id render-id}]
|
||||||
|
|
||||||
|
(and (= :outer (:stroke-alignment shape :center))
|
||||||
|
(> (:stroke-width shape 0) 0))
|
||||||
|
[:& outer-stroke-mask {:shape shape
|
||||||
|
:render-id render-id}]))
|
||||||
|
|
||||||
|
;; Outer alingmnent: display the shape in two layers. One
|
||||||
|
;; without stroke (only fill), and another one only with stroke
|
||||||
|
;; at double width (transparent fill) and passed through a mask
|
||||||
|
;; that shows the whole shape, but hides the original shape
|
||||||
|
;; without stroke
|
||||||
|
(mf/defc outer-stroke
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
|
||||||
|
(let [render-id (mf/use-ctx muc/render-ctx)
|
||||||
|
child (obj/get props "children")
|
||||||
|
base-props (obj/get child "props")
|
||||||
|
elem-name (obj/get child "type")
|
||||||
|
shape (obj/get props "shape")
|
||||||
|
stroke-width (:stroke-width shape 0)
|
||||||
|
stroke-mask-id (str "outer-stroke-" render-id)
|
||||||
|
shape-id (str "stroke-shape-" render-id)
|
||||||
|
|
||||||
|
style-str (->> (obj/get base-props "style")
|
||||||
|
(js->clj)
|
||||||
|
(mapv (fn [[k v]]
|
||||||
|
(-> (d/name k)
|
||||||
|
(str/kebab)
|
||||||
|
(str ":" v))))
|
||||||
|
(str/join ";"))]
|
||||||
|
|
||||||
|
[:g.outer-stroke-shape
|
||||||
|
[:symbol
|
||||||
|
[:> elem-name (-> (obj/clone base-props)
|
||||||
|
(obj/set! "id" shape-id)
|
||||||
|
(obj/set! "data-style" style-str)
|
||||||
|
(obj/without ["style"]))]]
|
||||||
|
|
||||||
|
[:use {:href (str "#" shape-id)
|
||||||
|
:mask (str "url(#" stroke-mask-id ")")
|
||||||
|
:style (-> (obj/get base-props "style")
|
||||||
|
(obj/clone)
|
||||||
|
(obj/update! "strokeWidth" * 2)
|
||||||
|
(obj/without ["fill" "fillOpacity"])
|
||||||
|
(obj/set! "fill" "none"))}]
|
||||||
|
|
||||||
|
[:use {:href (str "#" shape-id)
|
||||||
|
:style (-> (obj/get base-props "style")
|
||||||
|
(obj/clone)
|
||||||
|
(obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]]))
|
||||||
|
|
||||||
|
|
||||||
|
;; Inner alignment: display the shape with double width stroke,
|
||||||
|
;; and clip the result with the original shape without stroke.
|
||||||
|
(mf/defc inner-stroke
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [render-id (mf/use-ctx muc/render-ctx)
|
||||||
|
child (obj/get props "children")
|
||||||
|
base-props (obj/get child "props")
|
||||||
|
elem-name (obj/get child "type")
|
||||||
|
shape (obj/get props "shape")
|
||||||
|
transform (obj/get base-props "transform")
|
||||||
|
|
||||||
|
stroke-width (:stroke-width shape 0)
|
||||||
|
|
||||||
|
clip-id (str "inner-stroke-" render-id)
|
||||||
|
shape-id (str "stroke-shape-" render-id)
|
||||||
|
|
||||||
|
shape-props (-> base-props
|
||||||
|
(add-props {:id shape-id
|
||||||
|
:transform nil
|
||||||
|
:clipPath (str "url('#" clip-id "')")})
|
||||||
|
(add-style {:strokeWidth (* stroke-width 2)}))]
|
||||||
|
|
||||||
|
[:g.inner-stroke-shape {:transform transform}
|
||||||
|
[:> elem-name shape-props]]))
|
||||||
|
|
||||||
|
|
||||||
; The SVG standard does not implement yet the 'stroke-alignment'
|
; The SVG standard does not implement yet the 'stroke-alignment'
|
||||||
; attribute, to define the position of the stroke relative to the
|
; attribute, to define the position of the stroke relative to the
|
||||||
@ -19,100 +139,25 @@
|
|||||||
(mf/defc shape-custom-stroke
|
(mf/defc shape-custom-stroke
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [shape (unchecked-get props "shape")
|
(let [child (obj/get props "children")
|
||||||
base-props (unchecked-get props "base-props")
|
shape (obj/get props "shape")
|
||||||
elem-name (unchecked-get props "elem-name")
|
stroke-width (:stroke-width shape 0)
|
||||||
base-style (obj/get base-props "style")
|
|
||||||
{:keys [x y width height]} (:selrect shape)
|
|
||||||
stroke-id (mf/use-var (uuid/next))
|
|
||||||
stroke-style (:stroke-style shape :none)
|
stroke-style (:stroke-style shape :none)
|
||||||
stroke-position (:stroke-alignment shape :center)]
|
stroke-position (:stroke-alignment shape :center)
|
||||||
|
has-stroke? (and (and (> stroke-width 0)
|
||||||
|
(not= stroke-style :none)))
|
||||||
|
inner? (= :inner stroke-position)
|
||||||
|
outer? (= :outer stroke-position)]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
;; Center alignment (or no stroke): the default in SVG
|
(and has-stroke? inner?)
|
||||||
(or (= stroke-style :none) (= stroke-position :center))
|
[:& inner-stroke {:shape shape}
|
||||||
[:> elem-name (obj/merge! #js {} base-props)]
|
child]
|
||||||
|
|
||||||
;; Inner alignment: display the shape with double width stroke,
|
(and has-stroke? outer?)
|
||||||
;; and clip the result with the original shape without stroke.
|
[:& outer-stroke {:shape shape}
|
||||||
(= stroke-position :inner)
|
child]
|
||||||
(let [clip-id (str "clip-" @stroke-id)
|
|
||||||
|
|
||||||
clip-props (obj/merge
|
:else
|
||||||
base-props
|
child)))
|
||||||
#js {:transform nil
|
|
||||||
:style (obj/merge
|
|
||||||
base-style
|
|
||||||
#js {:stroke nil
|
|
||||||
:strokeWidth nil
|
|
||||||
:strokeOpacity nil
|
|
||||||
:strokeDasharray nil
|
|
||||||
:fill "white"
|
|
||||||
:fillOpacity 1})})
|
|
||||||
|
|
||||||
stroke-width (obj/get base-style "strokeWidth" 0)
|
|
||||||
shape-props (obj/merge
|
|
||||||
base-props
|
|
||||||
#js {:clipPath (str "url('#" clip-id "')")
|
|
||||||
:style (obj/merge
|
|
||||||
base-style
|
|
||||||
#js {:strokeWidth (* stroke-width 2)})})]
|
|
||||||
[:*
|
|
||||||
[:> "clipPath" #js {:id clip-id}
|
|
||||||
[:> elem-name clip-props]]
|
|
||||||
[:> elem-name shape-props]])
|
|
||||||
|
|
||||||
;; Outer alingmnent: display the shape in two layers. One
|
|
||||||
;; without stroke (only fill), and another one only with stroke
|
|
||||||
;; at double width (transparent fill) and passed through a mask
|
|
||||||
;; that shows the whole shape, but hides the original shape
|
|
||||||
;; without stroke
|
|
||||||
|
|
||||||
(= stroke-position :outer)
|
|
||||||
(let [stroke-mask-id (str "mask-" @stroke-id)
|
|
||||||
stroke-width (obj/get base-style "strokeWidth" 0)
|
|
||||||
mask-props1 (obj/merge
|
|
||||||
base-props
|
|
||||||
#js {:transform nil
|
|
||||||
:style (obj/merge
|
|
||||||
base-style
|
|
||||||
#js {:stroke "white"
|
|
||||||
:strokeWidth (* stroke-width 2)
|
|
||||||
:strokeOpacity 1
|
|
||||||
:strokeDasharray nil
|
|
||||||
:fill "white"
|
|
||||||
:fillOpacity 1})})
|
|
||||||
mask-props2 (obj/merge
|
|
||||||
base-props
|
|
||||||
#js {:transform nil
|
|
||||||
:style (obj/merge
|
|
||||||
base-style
|
|
||||||
#js {:stroke nil
|
|
||||||
:strokeWidth nil
|
|
||||||
:strokeOpacity nil
|
|
||||||
:strokeDasharray nil
|
|
||||||
:fill "black"
|
|
||||||
:fillOpacity 1})})
|
|
||||||
|
|
||||||
shape-props1 (obj/merge
|
|
||||||
base-props
|
|
||||||
#js {:style (obj/merge
|
|
||||||
base-style
|
|
||||||
#js {:stroke nil
|
|
||||||
:strokeWidth nil
|
|
||||||
:strokeOpacity nil
|
|
||||||
:strokeDasharray nil})})
|
|
||||||
shape-props2 (obj/merge
|
|
||||||
base-props
|
|
||||||
#js {:mask (str "url('#" stroke-mask-id "')")
|
|
||||||
:style (obj/merge
|
|
||||||
base-style
|
|
||||||
#js {:strokeWidth (* stroke-width 2)
|
|
||||||
:fill "none"
|
|
||||||
:fillOpacity 0})})]
|
|
||||||
[:*
|
|
||||||
[:mask {:id stroke-mask-id}
|
|
||||||
[:> elem-name mask-props1]
|
|
||||||
[:> elem-name mask-props2]]
|
|
||||||
[:> elem-name shape-props1]
|
|
||||||
[:> elem-name shape-props2]]))))
|
|
||||||
|
|
||||||
|
|||||||
@ -26,16 +26,6 @@
|
|||||||
props (-> (attrs/extract-style-attrs shape)
|
props (-> (attrs/extract-style-attrs shape)
|
||||||
(obj/merge!
|
(obj/merge!
|
||||||
#js {:d pdata}))]
|
#js {:d pdata}))]
|
||||||
(if background?
|
[:& shape-custom-stroke {:shape shape}
|
||||||
[:g
|
[:> :path props]]))
|
||||||
[:path {:stroke "none"
|
|
||||||
:fill "none"
|
|
||||||
:stroke-width "20px"
|
|
||||||
:d pdata}]
|
|
||||||
[:& shape-custom-stroke {:shape shape
|
|
||||||
:base-props props
|
|
||||||
:elem-name "path"}]]
|
|
||||||
[:& shape-custom-stroke {:shape shape
|
|
||||||
:base-props props
|
|
||||||
:elem-name "path"}])))
|
|
||||||
|
|
||||||
|
|||||||
@ -6,23 +6,19 @@
|
|||||||
|
|
||||||
(ns app.main.ui.shapes.rect
|
(ns app.main.ui.shapes.rect
|
||||||
(:require
|
(:require
|
||||||
[rumext.alpha :as mf]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.main.ui.shapes.attrs :as attrs]
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
|
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
|
||||||
[app.common.geom.shapes :as geom]
|
|
||||||
[app.util.object :as obj]
|
|
||||||
[app.main.ui.shapes.gradients :refer [gradient]]
|
[app.main.ui.shapes.gradients :refer [gradient]]
|
||||||
|
[app.util.object :as obj]
|
||||||
[cuerdas.core :as str]
|
[rumext.alpha :as mf]))
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.common.geom.point :as gpt]))
|
|
||||||
|
|
||||||
(mf/defc rect-shape
|
(mf/defc rect-shape
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [shape (unchecked-get props "shape")
|
(let [shape (unchecked-get props "shape")
|
||||||
{:keys [id x y width height]} shape
|
{:keys [id x y width height]} shape
|
||||||
transform (geom/transform-matrix shape)
|
transform (gsh/transform-matrix shape)
|
||||||
|
|
||||||
props (-> (attrs/extract-style-attrs shape)
|
props (-> (attrs/extract-style-attrs shape)
|
||||||
(obj/merge!
|
(obj/merge!
|
||||||
@ -30,11 +26,11 @@
|
|||||||
:y y
|
:y y
|
||||||
:transform transform
|
:transform transform
|
||||||
:width width
|
:width width
|
||||||
:height height}))]
|
:height height}))
|
||||||
|
|
||||||
[:& shape-custom-stroke {:shape shape
|
path? (some? (.-d props))]
|
||||||
:base-props props
|
|
||||||
:elem-name
|
[:& shape-custom-stroke {:shape shape}
|
||||||
(if (.-d props)
|
(if path?
|
||||||
"path"
|
[:> :path props]
|
||||||
"rect")}]))
|
[:> :rect props])]))
|
||||||
|
|||||||
@ -8,7 +8,9 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
|
[app.main.ui.shapes.custom-stroke :as cs]
|
||||||
[app.main.ui.shapes.fill-image :as fim]
|
[app.main.ui.shapes.fill-image :as fim]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
[app.main.ui.shapes.gradients :as grad]
|
[app.main.ui.shapes.gradients :as grad]
|
||||||
@ -16,6 +18,36 @@
|
|||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(defn add-metadata
|
||||||
|
"Adds as metadata properties that we cannot deduce from the exported SVG"
|
||||||
|
[props shape]
|
||||||
|
(let [add!
|
||||||
|
(fn [props attr val]
|
||||||
|
(let [ns-attr (str "penpot:" (-> attr d/name))]
|
||||||
|
(-> props
|
||||||
|
(obj/set! ns-attr val))))
|
||||||
|
frame? (= :frame type)]
|
||||||
|
(-> props
|
||||||
|
(add! :name (-> shape :name))
|
||||||
|
(add! :blocked (-> shape (:blocked false) str))
|
||||||
|
(add! :hidden (-> shape (:hidden false) str))
|
||||||
|
(add! :type (-> shape :type d/name))
|
||||||
|
|
||||||
|
(add! :stroke-style (-> shape (:stroke-style :none) d/name))
|
||||||
|
(add! :stroke-alignment (-> shape (:stroke-alignment :center) d/name))
|
||||||
|
|
||||||
|
(add! :transform (-> shape (:transform (gmt/matrix)) str))
|
||||||
|
(add! :transform-inverse (-> shape (:transform-inverse (gmt/matrix)) str))
|
||||||
|
|
||||||
|
(cond-> (some? (:r1 shape))
|
||||||
|
(-> (add! :r1 (-> shape (:r1 0) str))
|
||||||
|
(add! :r2 (-> shape (:r2 0) str))
|
||||||
|
(add! :r3 (-> shape (:r3 0) str))
|
||||||
|
(add! :r4 (-> shape (:r4 0) str))))
|
||||||
|
|
||||||
|
(cond-> frame?
|
||||||
|
(obj/set! "xmlns:penpot" "https://penpot.app/xmlns")))))
|
||||||
|
|
||||||
(mf/defc shape-container
|
(mf/defc shape-container
|
||||||
{::mf/forward-ref true
|
{::mf/forward-ref true
|
||||||
::mf/wrap-props false}
|
::mf/wrap-props false}
|
||||||
@ -33,28 +65,34 @@
|
|||||||
|
|
||||||
{:keys [x y width height type]} shape
|
{:keys [x y width height type]} shape
|
||||||
frame? (= :frame type)
|
frame? (= :frame type)
|
||||||
group-props (-> (obj/clone props)
|
|
||||||
(obj/without ["shape" "children"])
|
|
||||||
(obj/set! "ref" ref)
|
|
||||||
(obj/set! "id" (str "shape-" (:id shape)))
|
|
||||||
(obj/set! "filter" (filters/filter-str filter-id shape))
|
|
||||||
(obj/set! "style" styles)
|
|
||||||
|
|
||||||
(cond-> frame?
|
wrapper-props
|
||||||
(-> (obj/set! "x" x)
|
(-> (obj/clone props)
|
||||||
(obj/set! "y" y)
|
(obj/without ["shape" "children"])
|
||||||
(obj/set! "width" width)
|
(obj/set! "ref" ref)
|
||||||
(obj/set! "height" height)
|
(obj/set! "id" (str "shape-" (:id shape)))
|
||||||
(obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink")
|
(obj/set! "filter" (filters/filter-str filter-id shape))
|
||||||
(obj/set! "xmlns" "http://www.w3.org/2000/svg"))))
|
(obj/set! "style" styles)
|
||||||
|
|
||||||
|
(cond-> frame?
|
||||||
|
(-> (obj/set! "x" x)
|
||||||
|
(obj/set! "y" y)
|
||||||
|
(obj/set! "width" width)
|
||||||
|
(obj/set! "height" height)
|
||||||
|
(obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink")
|
||||||
|
(obj/set! "xmlns" "http://www.w3.org/2000/svg")))
|
||||||
|
|
||||||
|
(add-metadata shape))
|
||||||
|
|
||||||
wrapper-tag (if frame? "svg" "g")]
|
wrapper-tag (if frame? "svg" "g")]
|
||||||
|
|
||||||
[:& (mf/provider muc/render-ctx) {:value render-id}
|
[:& (mf/provider muc/render-ctx) {:value render-id}
|
||||||
[:> wrapper-tag group-props
|
[:> wrapper-tag wrapper-props
|
||||||
[:defs
|
[:defs
|
||||||
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
||||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||||
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
|
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
|
||||||
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
|
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
|
||||||
[:& fim/fill-image-pattern {:shape shape :render-id render-id}]]
|
[:& fim/fill-image-pattern {:shape shape :render-id render-id}]
|
||||||
|
[:& cs/stroke-defs {:shape shape :render-id render-id}]]
|
||||||
children]]))
|
children]]))
|
||||||
|
|||||||
@ -28,5 +28,4 @@
|
|||||||
|
|
||||||
[:> shape-container {:shape shape
|
[:> shape-container {:shape shape
|
||||||
:pointer-events (when editing? "none")}
|
:pointer-events (when editing? "none")}
|
||||||
[:& path/path-shape {:shape shape
|
[:& path/path-shape {:shape shape}]]))
|
||||||
:background? true}]]))
|
|
||||||
|
|||||||
@ -278,7 +278,6 @@
|
|||||||
(let [point (gpt/transform point transform-inverse)
|
(let [point (gpt/transform point transform-inverse)
|
||||||
end-x (/ (- (:x point) x) width)
|
end-x (/ (- (:x point) x) width)
|
||||||
end-y (/ (- (:y point) y) height)
|
end-y (/ (- (:y point) y) height)
|
||||||
|
|
||||||
end-x (mth/precision end-x 2)
|
end-x (mth/precision end-x 2)
|
||||||
end-y (mth/precision end-y 2)]
|
end-y (mth/precision end-y 2)]
|
||||||
(change! {:end-x end-x :end-y end-y})))
|
(change! {:end-x end-x :end-y end-y})))
|
||||||
@ -287,8 +286,8 @@
|
|||||||
(let [scale-factor-y (/ gradient-length (/ height 2))
|
(let [scale-factor-y (/ gradient-length (/ height 2))
|
||||||
norm-dist (/ (gpt/distance point from-p)
|
norm-dist (/ (gpt/distance point from-p)
|
||||||
(* (/ width 2) scale-factor-y))]
|
(* (/ width 2) scale-factor-y))]
|
||||||
|
(when (and norm-dist (mth/finite? norm-dist))
|
||||||
(change! {:width norm-dist})))]
|
(change! {:width norm-dist}))))]
|
||||||
|
|
||||||
(when (and gradient
|
(when (and gradient
|
||||||
(= id (:shape-id gradient))
|
(= id (:shape-id gradient))
|
||||||
|
|||||||
@ -8,10 +8,11 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.util.object :as obj]
|
|
||||||
[app.util.globals :as globals]
|
[app.util.globals :as globals]
|
||||||
|
[app.util.object :as obj]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[goog.dom :as dom]))
|
[goog.dom :as dom]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
;; --- Deprecated methods
|
;; --- Deprecated methods
|
||||||
|
|
||||||
@ -333,4 +334,23 @@
|
|||||||
|
|
||||||
(defn trigger-download
|
(defn trigger-download
|
||||||
[filename blob]
|
[filename blob]
|
||||||
(trigger-download-uri filename (.-type ^js blob) (dom/create-uri blob)))
|
(trigger-download-uri filename (.-type ^js blob) (create-uri blob)))
|
||||||
|
|
||||||
|
(defn save-as
|
||||||
|
[uri filename mtype description]
|
||||||
|
|
||||||
|
;; Only chrome supports the save dialog
|
||||||
|
(if (obj/contains? globals/window "showSaveFilePicker")
|
||||||
|
(let [extension (mtype->extension mtype)
|
||||||
|
opts {:suggestedName (str filename "." extension)
|
||||||
|
:types [{:description description
|
||||||
|
:accept { mtype [(str "." extension)]}}]}]
|
||||||
|
|
||||||
|
(p/let [file-system (.showSaveFilePicker globals/window (clj->js opts))
|
||||||
|
writable (.createWritable file-system)
|
||||||
|
response (js/fetch uri)
|
||||||
|
blob (.blob response)
|
||||||
|
_ (.write writable blob)]
|
||||||
|
(.close writable)))
|
||||||
|
|
||||||
|
(trigger-download-uri filename mtype uri)))
|
||||||
|
|||||||
164
frontend/src/app/util/import/parser.cljc
Normal file
164
frontend/src/app/util/import/parser.cljc
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
;; 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.import.parser
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[app.util.path.parser :as upp]))
|
||||||
|
|
||||||
|
(defn valid?
|
||||||
|
[root]
|
||||||
|
(contains? (:attrs root) :xmlns:penpot))
|
||||||
|
|
||||||
|
(defn branch?
|
||||||
|
[node]
|
||||||
|
(and (contains? node :content)
|
||||||
|
(some? (:content node))))
|
||||||
|
|
||||||
|
(defn close?
|
||||||
|
[node]
|
||||||
|
(and (vector? node)
|
||||||
|
(= ::close (first node))))
|
||||||
|
|
||||||
|
(defn get-type
|
||||||
|
[node]
|
||||||
|
(if (close? node)
|
||||||
|
(second node)
|
||||||
|
(-> (get-in node [:attrs :penpot:type])
|
||||||
|
(keyword))))
|
||||||
|
|
||||||
|
(defn shape?
|
||||||
|
[node]
|
||||||
|
(or (close? node)
|
||||||
|
(contains? (:attrs node) :penpot:type)))
|
||||||
|
|
||||||
|
(defn get-attr
|
||||||
|
([m att]
|
||||||
|
(get-attr m att identity))
|
||||||
|
([m att val-fn]
|
||||||
|
(let [ns-att (->> att d/name (str "penpot:") keyword)
|
||||||
|
val (get-in m [:attrs ns-att])]
|
||||||
|
(when val (val-fn val)))))
|
||||||
|
|
||||||
|
(defn get-children
|
||||||
|
[node]
|
||||||
|
(cond-> (:content node)
|
||||||
|
;; We add a "fake" node to know when we are leaving the shape children
|
||||||
|
(shape? node)
|
||||||
|
(conj [::close (get-type node)])))
|
||||||
|
|
||||||
|
(defn node-seq
|
||||||
|
[content]
|
||||||
|
(->> content (tree-seq branch? get-children)))
|
||||||
|
|
||||||
|
(defn get-transform
|
||||||
|
[type node])
|
||||||
|
|
||||||
|
(defn parse-style
|
||||||
|
"Transform style list into a map"
|
||||||
|
[style-str]
|
||||||
|
(if (string? style-str)
|
||||||
|
(->> (str/split style-str ";")
|
||||||
|
(map str/trim)
|
||||||
|
(map #(str/split % ":"))
|
||||||
|
(group-by first)
|
||||||
|
(map (fn [[key val]]
|
||||||
|
(vector (keyword key) (second (first val)))))
|
||||||
|
(into {}))
|
||||||
|
style-str))
|
||||||
|
|
||||||
|
(defn add-attrs
|
||||||
|
[m attrs]
|
||||||
|
(reduce-kv
|
||||||
|
(fn [m k v]
|
||||||
|
(if (#{:style :data-style} k)
|
||||||
|
(assoc m :style (parse-style v))
|
||||||
|
(assoc m k v)))
|
||||||
|
m
|
||||||
|
attrs))
|
||||||
|
|
||||||
|
(defn get-data-node
|
||||||
|
[node]
|
||||||
|
|
||||||
|
(let [data-tags #{:ellipse :rect :path}]
|
||||||
|
(->> node
|
||||||
|
(node-seq)
|
||||||
|
(filter #(contains? data-tags (:tag %)))
|
||||||
|
(map #(:attrs %))
|
||||||
|
(reduce add-attrs {}))))
|
||||||
|
|
||||||
|
(def search-data-node? #{:rect :image :path :text :circle})
|
||||||
|
(def has-position? #{:frame :rect :image :text})
|
||||||
|
|
||||||
|
(defn parse-position
|
||||||
|
[props data]
|
||||||
|
(let [values (->> (select-keys data [:x :y :width :height])
|
||||||
|
(d/mapm (fn [_ val] (d/parse-double val))))]
|
||||||
|
(d/merge props values)))
|
||||||
|
|
||||||
|
(defn parse-circle
|
||||||
|
[props data]
|
||||||
|
(let [values (->> (select-keys data [:cx :cy :rx :ry])
|
||||||
|
(d/mapm (fn [_ val] (d/parse-double val))))]
|
||||||
|
|
||||||
|
{:x (- (:cx values) (:rx values))
|
||||||
|
:y (- (:cy values) (:ry values))
|
||||||
|
:width (* (:rx values) 2)
|
||||||
|
:height (* (:ry values) 2)}))
|
||||||
|
|
||||||
|
(defn parse-path
|
||||||
|
[props data]
|
||||||
|
(let [content (upp/parse-path (:d data))
|
||||||
|
selrect (gsh/content->selrect content)
|
||||||
|
points (gsh/rect->points selrect)]
|
||||||
|
|
||||||
|
(-> props
|
||||||
|
(assoc :content content)
|
||||||
|
(assoc :selrect selrect)
|
||||||
|
(assoc :points points))))
|
||||||
|
|
||||||
|
(defn extract-data
|
||||||
|
[type node]
|
||||||
|
(let [data (if (search-data-node? type)
|
||||||
|
(get-data-node node)
|
||||||
|
(:attrs node))]
|
||||||
|
(cond-> {}
|
||||||
|
(has-position? type)
|
||||||
|
(-> (parse-position data)
|
||||||
|
(gsh/setup-selrect))
|
||||||
|
|
||||||
|
(= type :circle)
|
||||||
|
(-> (parse-circle data)
|
||||||
|
(gsh/setup-selrect))
|
||||||
|
|
||||||
|
(= type :path)
|
||||||
|
(parse-path data))))
|
||||||
|
|
||||||
|
(defn str->bool
|
||||||
|
[val]
|
||||||
|
(= val "true"))
|
||||||
|
|
||||||
|
(defn parse-data
|
||||||
|
[type node]
|
||||||
|
|
||||||
|
(when-not (close? node)
|
||||||
|
(let [name (get-attr node :name)
|
||||||
|
blocked (get-attr node :blocked str->bool)
|
||||||
|
hidden (get-attr node :hidden str->bool)
|
||||||
|
transform (get-attr node :transform gmt/str->matrix)
|
||||||
|
transform-inverse (get-attr node :transform-inverse gmt/str->matrix)]
|
||||||
|
|
||||||
|
(-> (extract-data type node)
|
||||||
|
(assoc :name name)
|
||||||
|
(assoc :blocked blocked)
|
||||||
|
(assoc :hidden hidden)
|
||||||
|
(cond-> (some? transform)
|
||||||
|
(assoc :transform transform))
|
||||||
|
(cond-> (some? transform-inverse)
|
||||||
|
(assoc :transform-inverse transform-inverse))))))
|
||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
(ns app.util.object
|
(ns app.util.object
|
||||||
"A collection of helpers for work with javascript objects."
|
"A collection of helpers for work with javascript objects."
|
||||||
(:refer-clojure :exclude [set! get get-in merge clone])
|
(:refer-clojure :exclude [set! get get-in merge clone contains?])
|
||||||
(:require
|
(:require
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[goog.object :as gobj]
|
[goog.object :as gobj]
|
||||||
@ -22,22 +22,27 @@
|
|||||||
(let [result (get obj k)]
|
(let [result (get obj k)]
|
||||||
(if (undefined? result) default result))))
|
(if (undefined? result) default result))))
|
||||||
|
|
||||||
|
(defn contains?
|
||||||
|
[obj k]
|
||||||
|
(some? (unchecked-get obj k)))
|
||||||
|
|
||||||
(defn get-keys
|
(defn get-keys
|
||||||
[obj]
|
[obj]
|
||||||
(js/Object.keys ^js obj))
|
(js/Object.keys ^js obj))
|
||||||
|
|
||||||
(defn get-in
|
(defn get-in
|
||||||
[obj keys]
|
([obj keys]
|
||||||
(loop [key (first keys)
|
(get-in obj keys nil))
|
||||||
keys (rest keys)
|
|
||||||
res obj]
|
([obj keys default]
|
||||||
(if (nil? key)
|
(loop [key (first keys)
|
||||||
res
|
keys (rest keys)
|
||||||
(if (nil? res)
|
res obj]
|
||||||
res
|
(if (or (nil? key) (nil? res))
|
||||||
(recur (first keys)
|
(or res default)
|
||||||
(rest keys)
|
(recur (first keys)
|
||||||
(unchecked-get res key))))))
|
(rest keys)
|
||||||
|
(unchecked-get res key))))))
|
||||||
|
|
||||||
(defn without
|
(defn without
|
||||||
[obj keys]
|
[obj keys]
|
||||||
@ -68,6 +73,14 @@
|
|||||||
(unchecked-set obj key value)
|
(unchecked-set obj key value)
|
||||||
obj)
|
obj)
|
||||||
|
|
||||||
|
(defn update!
|
||||||
|
[obj key f & args]
|
||||||
|
(let [found (get obj key ::not-found)]
|
||||||
|
(if-not (identical? ::not-found found)
|
||||||
|
(do (unchecked-set obj key (apply f found args))
|
||||||
|
obj)
|
||||||
|
obj)))
|
||||||
|
|
||||||
(defn- props-key-fn
|
(defn- props-key-fn
|
||||||
[key]
|
[key]
|
||||||
(if (or (= key :class) (= key :class-name))
|
(if (or (= key :class) (= key :class-name))
|
||||||
|
|||||||
@ -9,14 +9,13 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes.path :as gshp]
|
[app.common.geom.shapes.path :as gshp]
|
||||||
|
[app.common.math :as mth]
|
||||||
[app.util.path.arc-to-curve :refer [a2c]]
|
[app.util.path.arc-to-curve :refer [a2c]]
|
||||||
[app.util.path.commands :as upc]
|
[app.util.path.commands :as upc]
|
||||||
[app.util.svg :as usvg]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[clojure.set :as set]
|
|
||||||
[app.common.math :as mth]
|
|
||||||
[app.util.path.geom :as upg]
|
[app.util.path.geom :as upg]
|
||||||
))
|
[app.util.svg :as usvg]
|
||||||
|
[clojure.set :as set]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
;;
|
;;
|
||||||
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
|
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
|
||||||
|
|||||||
@ -745,8 +745,6 @@
|
|||||||
(reduce gmt/multiply (gmt/matrix) matrices))
|
(reduce gmt/multiply (gmt/matrix) matrices))
|
||||||
(gmt/matrix)))
|
(gmt/matrix)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn format-move [[x y]] (str "M" x " " y))
|
(defn format-move [[x y]] (str "M" x " " y))
|
||||||
(defn format-line [[x y]] (str "L" x " " y))
|
(defn format-line [[x y]] (str "L" x " " y))
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.worker
|
(ns app.worker
|
||||||
(:require
|
(:require
|
||||||
|
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
@ -14,6 +15,9 @@
|
|||||||
[app.util.worker :as w]
|
[app.util.worker :as w]
|
||||||
[app.worker.impl :as impl]
|
[app.worker.impl :as impl]
|
||||||
[app.worker.selection]
|
[app.worker.selection]
|
||||||
|
|
||||||
|
[app.worker.import]
|
||||||
|
[app.worker.export]
|
||||||
[app.worker.snaps]
|
[app.worker.snaps]
|
||||||
[app.worker.thumbnails]
|
[app.worker.thumbnails]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
@ -159,4 +163,3 @@
|
|||||||
(set! process-message-sub (subscribe-buffer-messages))
|
(set! process-message-sub (subscribe-buffer-messages))
|
||||||
(.addEventListener js/self "message" on-message))
|
(.addEventListener js/self "message" on-message))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
56
frontend/src/app/worker/export.cljs
Normal file
56
frontend/src/app/worker/export.cljs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
;; 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.worker.export
|
||||||
|
(:require
|
||||||
|
[app.main.render :as r]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.zip :as uz]
|
||||||
|
[app.worker.impl :as impl]
|
||||||
|
[beicon.core :as rx]))
|
||||||
|
|
||||||
|
(defn get-page-data
|
||||||
|
[{file-name :file-name {:keys [id name] :as data} :data}]
|
||||||
|
(->> (r/render-page data)
|
||||||
|
(rx/map (fn [markup]
|
||||||
|
{:id id
|
||||||
|
:name name
|
||||||
|
:file-name file-name
|
||||||
|
:markup markup}))))
|
||||||
|
|
||||||
|
(defn process-pages [file]
|
||||||
|
(let [pages (get-in file [:data :pages])
|
||||||
|
pages-index (get-in file [:data :pages-index])]
|
||||||
|
(->> pages
|
||||||
|
(map #(hash-map
|
||||||
|
:file-name (:name file)
|
||||||
|
:data (get pages-index %))))))
|
||||||
|
|
||||||
|
(defn collect-page
|
||||||
|
[coll {:keys [id file-name name markup] :as page}]
|
||||||
|
(conj coll [(str file-name "/" name ".svg") markup]))
|
||||||
|
|
||||||
|
(defmethod impl/handler :export-file
|
||||||
|
[{:keys [team-id files] :as message}]
|
||||||
|
|
||||||
|
(let [render-stream
|
||||||
|
(->> (rx/from (->> files (mapv :id)))
|
||||||
|
(rx/merge-map #(rp/query :file {:id %}))
|
||||||
|
(rx/flat-map process-pages)
|
||||||
|
(rx/observe-on :async)
|
||||||
|
(rx/flat-map get-page-data)
|
||||||
|
(rx/share))]
|
||||||
|
|
||||||
|
(rx/merge
|
||||||
|
(->> render-stream
|
||||||
|
(rx/map #(hash-map :type :progress
|
||||||
|
:data (str "Render " (:file-name %) " - " (:name %)))))
|
||||||
|
(->> render-stream
|
||||||
|
(rx/reduce collect-page [])
|
||||||
|
(rx/flat-map uz/compress-files)
|
||||||
|
(rx/map #(hash-map :type :finish
|
||||||
|
:data (dom/create-uri %)))))))
|
||||||
143
frontend/src/app/worker/import.cljs
Normal file
143
frontend/src/app/worker/import.cljs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
;; 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.worker.import
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.file-builder :as fb]
|
||||||
|
[app.common.pages :as cp]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[app.util.import.parser :as cip]
|
||||||
|
[app.util.zip :as uz]
|
||||||
|
[app.worker.impl :as impl]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[tubax.core :as tubax]))
|
||||||
|
|
||||||
|
;; Upload changes batches size
|
||||||
|
(def change-batch-size 100)
|
||||||
|
|
||||||
|
(defn create-empty-file
|
||||||
|
"Create a new file on the back-end"
|
||||||
|
[project-id file]
|
||||||
|
(rp/mutation
|
||||||
|
:create-file
|
||||||
|
{:id (:id file)
|
||||||
|
:name (:name file)
|
||||||
|
:project-id project-id
|
||||||
|
:data (-> cp/empty-file-data
|
||||||
|
(assoc :id (:id file)))}))
|
||||||
|
|
||||||
|
(defn send-changes
|
||||||
|
"Creates batches of changes to be sent to the backend"
|
||||||
|
[file init-revn]
|
||||||
|
(let [revn (atom init-revn)
|
||||||
|
file-id (:id file)
|
||||||
|
session-id (uuid/next)
|
||||||
|
changes-batches
|
||||||
|
(->> (fb/generate-changes file)
|
||||||
|
(partition change-batch-size change-batch-size nil)
|
||||||
|
(mapv vec))]
|
||||||
|
|
||||||
|
(->> (rx/from changes-batches)
|
||||||
|
(rx/merge-map
|
||||||
|
(fn [cur-changes-batch]
|
||||||
|
(rp/mutation
|
||||||
|
:update-file
|
||||||
|
{:id file-id
|
||||||
|
:session-id session-id
|
||||||
|
:revn @revn
|
||||||
|
:changes cur-changes-batch})))
|
||||||
|
|
||||||
|
(rx/tap #(reset! revn (:revn %))))))
|
||||||
|
|
||||||
|
(defn persist-file
|
||||||
|
"Sends to the back-end the imported data"
|
||||||
|
[project-id file]
|
||||||
|
(->> (create-empty-file project-id file)
|
||||||
|
(rx/flat-map #(send-changes file (:revn %)))))
|
||||||
|
|
||||||
|
(defn parse-file-name
|
||||||
|
[dir]
|
||||||
|
(if (str/ends-with? dir "/")
|
||||||
|
(subs dir 0 (dec (count dir)))
|
||||||
|
dir))
|
||||||
|
|
||||||
|
(defn parse-page-name
|
||||||
|
[path]
|
||||||
|
(let [[file page] (str/split path "/")]
|
||||||
|
(str/replace page ".svg" "")))
|
||||||
|
|
||||||
|
(defn add-shape-file
|
||||||
|
[file node]
|
||||||
|
|
||||||
|
(let [type (cip/get-type node)
|
||||||
|
close? (cip/close? node)
|
||||||
|
data (cip/parse-data type node)]
|
||||||
|
|
||||||
|
(if close?
|
||||||
|
(case type
|
||||||
|
:frame
|
||||||
|
(fb/close-artboard file)
|
||||||
|
|
||||||
|
:group
|
||||||
|
(fb/close-group file)
|
||||||
|
|
||||||
|
;; default
|
||||||
|
file)
|
||||||
|
|
||||||
|
(case type
|
||||||
|
:frame (fb/add-artboard file data)
|
||||||
|
:group (fb/add-group file data)
|
||||||
|
:rect (fb/create-rect file data)
|
||||||
|
:circle (fb/create-circle file data)
|
||||||
|
:path (fb/create-path file data)
|
||||||
|
:text (fb/create-text file data)
|
||||||
|
:image (fb/create-image file data)
|
||||||
|
|
||||||
|
;; default
|
||||||
|
file))))
|
||||||
|
|
||||||
|
(defn import-page
|
||||||
|
[file {:keys [path content]}]
|
||||||
|
(let [page-name (parse-page-name path)]
|
||||||
|
(when (cip/valid? content)
|
||||||
|
(let [nodes (->> content cip/node-seq)]
|
||||||
|
(->> nodes
|
||||||
|
(filter cip/shape?)
|
||||||
|
(reduce add-shape-file (fb/add-page file page-name))
|
||||||
|
(fb/close-page))))))
|
||||||
|
|
||||||
|
(defmethod impl/handler :import-file
|
||||||
|
[{:keys [project-id files]}]
|
||||||
|
|
||||||
|
(let [extract-stream
|
||||||
|
(->> (rx/from files)
|
||||||
|
(rx/merge-map uz/extract-files))
|
||||||
|
|
||||||
|
dir-str
|
||||||
|
(->> extract-stream
|
||||||
|
(rx/filter #(contains? % :dir))
|
||||||
|
(rx/map :dir))
|
||||||
|
|
||||||
|
file-str
|
||||||
|
(->> extract-stream
|
||||||
|
(rx/filter #(not (contains? % :dir)))
|
||||||
|
(rx/map #(d/update-when % :content tubax/xml->clj)))]
|
||||||
|
|
||||||
|
(->> dir-str
|
||||||
|
(rx/merge-map
|
||||||
|
(fn [dir]
|
||||||
|
(let [file (fb/create-file (parse-file-name dir))]
|
||||||
|
(rx/concat
|
||||||
|
(->> file-str
|
||||||
|
(rx/filter #(str/starts-with? (:path %) dir))
|
||||||
|
(rx/reduce import-page file)
|
||||||
|
(rx/flat-map #(persist-file project-id %))
|
||||||
|
(rx/ignore))
|
||||||
|
|
||||||
|
(rx/of (select-keys file [:id :name])))))))))
|
||||||
@ -2633,4 +2633,10 @@ msgid "workspace.updates.update"
|
|||||||
msgstr "Update"
|
msgstr "Update"
|
||||||
|
|
||||||
msgid "workspace.viewport.click-to-close-path"
|
msgid "workspace.viewport.click-to-close-path"
|
||||||
msgstr "Click to close the path"
|
msgstr "Click to close the path"
|
||||||
|
|
||||||
|
msgid "dashboard.export-single"
|
||||||
|
msgstr "Export file"
|
||||||
|
|
||||||
|
msgid "dashboard.export-multi"
|
||||||
|
msgstr "Export %s files"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user