mirror of
https://github.com/penpot/penpot.git
synced 2025-02-15 11:38:24 -05:00
Merge pull request #1037 from penpot/feat/export-import
Import/export more features and toggleable UI
This commit is contained in:
commit
4683d959a5
19 changed files with 355 additions and 122 deletions
|
@ -87,15 +87,15 @@
|
||||||
:changes []})))
|
:changes []})))
|
||||||
|
|
||||||
(defn add-page
|
(defn add-page
|
||||||
[file name]
|
[file data]
|
||||||
(let [page-id (uuid/next)]
|
(let [page-id (uuid/next)
|
||||||
|
page (-> init/empty-page-data
|
||||||
|
(assoc :id page-id)
|
||||||
|
(d/deep-merge data))]
|
||||||
(-> file
|
(-> file
|
||||||
(commit-change
|
(commit-change
|
||||||
{:type :add-page
|
{:type :add-page
|
||||||
:id page-id
|
:page page})
|
||||||
:name name
|
|
||||||
:page (-> init/empty-page-data
|
|
||||||
(assoc :name name))})
|
|
||||||
|
|
||||||
;; Current page being edited
|
;; Current page being edited
|
||||||
(assoc :current-page-id page-id)
|
(assoc :current-page-id page-id)
|
||||||
|
|
|
@ -7,14 +7,72 @@
|
||||||
(ns app.libs.file-builder
|
(ns app.libs.file-builder
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.file-builder :as fb]))
|
[app.common.file-builder :as fb]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
(defn parse-data [data]
|
||||||
|
(as-> data $
|
||||||
|
(js->clj $ :keywordize-keys true)
|
||||||
|
;; Transforms camelCase to kebab-case
|
||||||
|
(d/deep-mapm
|
||||||
|
(fn [[key value]]
|
||||||
|
(let [value (if (= (type value) js/Symbol)
|
||||||
|
(keyword (js/Symbol.keyFor value))
|
||||||
|
value)
|
||||||
|
key (-> key d/name str/kebab keyword)]
|
||||||
|
[key value])) $)))
|
||||||
|
|
||||||
(deftype File [^:mutable file]
|
(deftype File [^:mutable file]
|
||||||
Object
|
Object
|
||||||
(addPage [self name]
|
|
||||||
(set! file (fb/add-page file name))
|
|
||||||
(str (:current-page-id file))))
|
|
||||||
|
|
||||||
|
(addPage [self name]
|
||||||
|
(set! file (fb/add-page file {:name name}))
|
||||||
|
(str (:current-page-id file)))
|
||||||
|
|
||||||
|
(addPage [self name options]
|
||||||
|
(set! file (fb/add-page file {:name name :options options}))
|
||||||
|
(str (:current-page-id file)))
|
||||||
|
|
||||||
|
(closePage [self]
|
||||||
|
(set! file (fb/close-page file)))
|
||||||
|
|
||||||
|
(addArtboard [self data]
|
||||||
|
(set! file (fb/add-artboard file (parse-data data)))
|
||||||
|
(str (:last-id file)))
|
||||||
|
|
||||||
|
(closeArtboard [self data]
|
||||||
|
(set! file (fb/close-artboard file)))
|
||||||
|
|
||||||
|
(addGroup [self data]
|
||||||
|
(set! file (fb/add-group file (parse-data data)))
|
||||||
|
(str (:last-id file)))
|
||||||
|
|
||||||
|
(closeGroup [self]
|
||||||
|
(set! file (fb/close-group file)))
|
||||||
|
|
||||||
|
(createRect [self data]
|
||||||
|
(set! file (fb/create-rect file (parse-data data))))
|
||||||
|
|
||||||
|
(createCircle [self data]
|
||||||
|
(set! file (fb/create-circle file (parse-data data))))
|
||||||
|
|
||||||
|
(createPath [self data]
|
||||||
|
(set! file (fb/create-path file (parse-data data))))
|
||||||
|
|
||||||
|
(createText [self data]
|
||||||
|
(set! file (fb/create-text file (parse-data data))))
|
||||||
|
|
||||||
|
(createImage [self data]
|
||||||
|
(set! file (fb/create-image file (parse-data data))))
|
||||||
|
|
||||||
|
(createSVG [self data]
|
||||||
|
(set! file (fb/create-svg-raw file (parse-data data))))
|
||||||
|
|
||||||
|
(closeSVG [self]
|
||||||
|
(set! file (fb/close-svg-raw file)))
|
||||||
|
|
||||||
|
(asMap [self]
|
||||||
|
(clj->js file)))
|
||||||
|
|
||||||
(defn create-file-export [^string name]
|
(defn create-file-export [^string name]
|
||||||
(File. (fb/create-file name)))
|
(File. (fb/create-file name)))
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.ui.shapes.circle :as circle]
|
[app.main.ui.shapes.circle :as circle]
|
||||||
[app.main.ui.shapes.embed :as embed]
|
[app.main.ui.shapes.embed :as embed]
|
||||||
|
[app.main.ui.shapes.export :as use]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
[app.main.ui.shapes.frame :as frame]
|
[app.main.ui.shapes.frame :as frame]
|
||||||
[app.main.ui.shapes.group :as group]
|
[app.main.ui.shapes.group :as group]
|
||||||
|
@ -158,6 +159,9 @@
|
||||||
:style {:width "100%"
|
:style {:width "100%"
|
||||||
:height "100%"
|
:height "100%"
|
||||||
:background background-color}}
|
:background background-color}}
|
||||||
|
|
||||||
|
[:& use/export-page {:options (:options data)}]
|
||||||
|
|
||||||
(for [item shapes]
|
(for [item shapes]
|
||||||
(let [frame? (= (:type item) :frame)]
|
(let [frame? (= (:type item) :frame)]
|
||||||
(cond
|
(cond
|
||||||
|
|
|
@ -12,17 +12,21 @@
|
||||||
[app.util.dom :as dom]))
|
[app.util.dom :as dom]))
|
||||||
|
|
||||||
(mf/defc file-uploader
|
(mf/defc file-uploader
|
||||||
[{:keys [accept multi label-text label-class input-id input-ref on-selected] :as props}]
|
{::mf/forward-ref true}
|
||||||
|
[{:keys [accept multi label-text label-class input-id on-selected] :as props} input-ref]
|
||||||
(let [opt-pick-one #(if multi % (first %))
|
(let [opt-pick-one #(if multi % (first %))
|
||||||
|
|
||||||
on-files-selected (fn [event]
|
on-files-selected
|
||||||
(let [target (dom/get-target event)]
|
(mf/use-callback
|
||||||
(st/emit!
|
(mf/deps opt-pick-one)
|
||||||
(some-> target
|
(fn [event]
|
||||||
(dom/get-files)
|
(let [target (dom/get-target event)]
|
||||||
(opt-pick-one)
|
(st/emit!
|
||||||
(on-selected)))
|
(some-> target
|
||||||
(dom/clean-value! target)))]
|
(dom/get-files)
|
||||||
|
(opt-pick-one)
|
||||||
|
(on-selected)))
|
||||||
|
(dom/clean-value! target))))]
|
||||||
[:*
|
[:*
|
||||||
(when label-text
|
(when label-text
|
||||||
[:label {:for input-id :class-name label-class} label-text])
|
[:label {:for input-id :class-name label-class} label-text])
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
[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.ui.context :as ctx]
|
||||||
[app.main.worker :as uw]
|
[app.main.worker :as uw]
|
||||||
|
[app.util.debug :as d]
|
||||||
[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]
|
||||||
|
@ -192,7 +193,8 @@
|
||||||
[[(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]
|
(when (d/debug? :export)
|
||||||
|
[(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]]
|
||||||
|
|
||||||
|
@ -204,7 +206,8 @@
|
||||||
(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]
|
(when (d/debug? :export)
|
||||||
|
[(tr "dashboard.export-single") on-export-files])
|
||||||
[:separator]
|
[:separator]
|
||||||
[(tr "labels.delete") on-delete]])]
|
[(tr "labels.delete") on-delete]])]
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@
|
||||||
[:& file-uploader {:input-id "font-upload"
|
[:& file-uploader {:input-id "font-upload"
|
||||||
:accept cm/str-font-types
|
:accept cm/str-font-types
|
||||||
:multi true
|
:multi true
|
||||||
:input-ref input-ref
|
:ref input-ref
|
||||||
:on-selected on-selected}]]]
|
:on-selected on-selected}]]]
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
|
|
|
@ -39,17 +39,15 @@
|
||||||
(log/debug :action "import-end")
|
(log/debug :action "import-end")
|
||||||
(when on-finish-import (on-finish-import))))))))))
|
(when on-finish-import (on-finish-import))))))))))
|
||||||
|
|
||||||
(mf/defc import-button
|
(mf/defc import-form
|
||||||
[{:keys [project-id on-finish-import]}]
|
{::mf/forward-ref true}
|
||||||
|
[{:keys [project-id on-finish-import]} external-ref]
|
||||||
|
|
||||||
(let [file-input (mf/use-ref nil)
|
(let [on-file-selected (use-import-file project-id on-finish-import)]
|
||||||
on-file-selected (use-import-file project-id on-finish-import)]
|
|
||||||
[:form.import-file
|
[: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"
|
[:& file-uploader {:accept "application/zip"
|
||||||
:multi true
|
:multi true
|
||||||
:input-ref file-input
|
:ref external-ref
|
||||||
:on-selected on-file-selected}]]))
|
:on-selected on-file-selected}]]))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[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.ui.context :as ctx]
|
||||||
|
[app.main.ui.dashboard.import :as udi]
|
||||||
|
[app.util.debug :as d]
|
||||||
|
[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]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
|
@ -71,22 +74,43 @@
|
||||||
:title (tr "modals.delete-project-confirm.title")
|
:title (tr "modals.delete-project-confirm.title")
|
||||||
:message (tr "modals.delete-project-confirm.message")
|
:message (tr "modals.delete-project-confirm.message")
|
||||||
:accept-label (tr "modals.delete-project-confirm.accept")
|
:accept-label (tr "modals.delete-project-confirm.accept")
|
||||||
:on-accept delete-fn}))]
|
:on-accept delete-fn}))
|
||||||
|
|
||||||
[:& context-menu {:on-close on-menu-close
|
|
||||||
:show show?
|
file-input (mf/use-ref nil)
|
||||||
:fixed? (or (not= top 0) (not= left 0))
|
|
||||||
:min-width? true
|
on-import-files
|
||||||
:top top
|
(mf/use-callback
|
||||||
:left left
|
(fn []
|
||||||
:options [(when-not (:is-default project)
|
(dom/click (mf/ref-val file-input))))
|
||||||
[(tr "labels.rename") on-edit])
|
|
||||||
[(tr "dashboard.duplicate") on-duplicate]
|
on-finish-import
|
||||||
[(tr "dashboard.pin-unpin") toggle-pin]
|
(mf/use-callback
|
||||||
(when (seq teams)
|
(fn []
|
||||||
[(tr "dashboard.move-to") nil
|
(st/emit! (dd/fetch-recent-files)
|
||||||
(for [team teams]
|
(dd/clear-selected-files))))]
|
||||||
[(:name team) (on-move (:id team))])])
|
|
||||||
[:separator]
|
[:*
|
||||||
[(tr "labels.delete") on-delete]]}]))
|
[:& udi/import-form {:ref file-input
|
||||||
|
:project-id (:id project)
|
||||||
|
:on-finish-import on-finish-import}]
|
||||||
|
[:& context-menu
|
||||||
|
{:on-close on-menu-close
|
||||||
|
:show show?
|
||||||
|
:fixed? (or (not= top 0) (not= left 0))
|
||||||
|
:min-width? true
|
||||||
|
:top top
|
||||||
|
:left left
|
||||||
|
:options [(when-not (:is-default project)
|
||||||
|
[(tr "labels.rename") on-edit])
|
||||||
|
[(tr "dashboard.duplicate") on-duplicate]
|
||||||
|
[(tr "dashboard.pin-unpin") toggle-pin]
|
||||||
|
(when (seq teams)
|
||||||
|
[(tr "dashboard.move-to") nil
|
||||||
|
(for [team teams]
|
||||||
|
[(:name team) (on-move (:id team))])])
|
||||||
|
(when (d/debug? :import)
|
||||||
|
[(tr "dashboard.import") on-import-files])
|
||||||
|
[:separator]
|
||||||
|
[(tr "labels.delete") on-delete]]}]]))
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,7 @@
|
||||||
[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]}
|
||||||
|
@ -98,13 +97,7 @@
|
||||||
(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
|
||||||
|
@ -117,13 +110,12 @@
|
||||||
(tr "labels.drafts")
|
(tr "labels.drafts")
|
||||||
(:name project))])
|
(:name project))])
|
||||||
|
|
||||||
(when (:menu-open @local)
|
[:& project-menu {:project project
|
||||||
[:& project-menu {:project project
|
:show? (:menu-open @local)
|
||||||
:show? (:menu-open @local)
|
:left (:x (:menu-pos @local))
|
||||||
:left (:x (:menu-pos @local))
|
:top (:y (:menu-pos @local))
|
||||||
:top (:y (:menu-pos @local))
|
:on-edit on-edit-open
|
||||||
:on-edit on-edit-open
|
:on-menu-close on-menu-close}]
|
||||||
:on-menu-close on-menu-close}])
|
|
||||||
|
|
||||||
[:span.info (str file-count " files")]
|
[:span.info (str file-count " files")]
|
||||||
(when (> file-count 0)
|
(when (> file-count 0)
|
||||||
|
@ -131,9 +123,6 @@
|
||||||
(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}]
|
|
||||||
|
|
||||||
(when-not (:is-default project)
|
(when-not (:is-default project)
|
||||||
[:span.pin-icon.tooltip.tooltip-bottom
|
[:span.pin-icon.tooltip.tooltip-bottom
|
||||||
{:class (when (:is-pinned project) "active")
|
{:class (when (:is-pinned project) "active")
|
||||||
|
|
|
@ -285,7 +285,7 @@
|
||||||
[:img {:src (cfg/resolve-team-photo-url team)}]
|
[:img {:src (cfg/resolve-team-photo-url team)}]
|
||||||
[:& file-uploader {:accept "image/jpeg,image/png"
|
[:& file-uploader {:accept "image/jpeg,image/png"
|
||||||
:multi false
|
:multi false
|
||||||
:input-ref finput
|
:ref finput
|
||||||
:on-selected on-file-selected}]]]
|
:on-selected on-file-selected}]]]
|
||||||
|
|
||||||
[:div.block.owner-block
|
[:div.block.owner-block
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
[:img {:src photo}]
|
[:img {:src photo}]
|
||||||
[:& file-uploader {:accept "image/jpeg,image/png"
|
[:& file-uploader {:accept "image/jpeg,image/png"
|
||||||
:multi false
|
:multi false
|
||||||
:input-ref file-input
|
:ref file-input
|
||||||
:on-selected on-file-selected}]]]))
|
:on-selected on-file-selected}]]]))
|
||||||
|
|
||||||
;; --- Profile Page
|
;; --- Profile Page
|
||||||
|
|
|
@ -70,6 +70,11 @@
|
||||||
(obj/set! "penpot:center-x" (-> center :x str))
|
(obj/set! "penpot:center-x" (-> center :x str))
|
||||||
(obj/set! "penpot:center-y" (-> center :y str))
|
(obj/set! "penpot:center-y" (-> center :y str))
|
||||||
|
|
||||||
|
;; Constraints
|
||||||
|
(add! :constraints-h)
|
||||||
|
(add! :constraints-v)
|
||||||
|
(add! :fixed-scroll)
|
||||||
|
|
||||||
(cond-> (and rect? (some? (:r1 shape)))
|
(cond-> (and rect? (some? (:r1 shape)))
|
||||||
(-> (add! :r1)
|
(-> (add! :r1)
|
||||||
(add! :r2)
|
(add! :r2)
|
||||||
|
@ -83,6 +88,37 @@
|
||||||
(cond-> mask?
|
(cond-> mask?
|
||||||
(obj/set! "penpot:masked-group" "true"))))))
|
(obj/set! "penpot:masked-group" "true"))))))
|
||||||
|
|
||||||
|
(defn prefix-keys [m]
|
||||||
|
(letfn [(prefix-entry [[k v]]
|
||||||
|
[(str "penpot:" (d/name k)) v])]
|
||||||
|
(into {} (map prefix-entry) m)))
|
||||||
|
|
||||||
|
(mf/defc export-grid-data
|
||||||
|
[{:keys [grids]}]
|
||||||
|
[:> "penpot:grids" #js {}
|
||||||
|
(for [{:keys [type display params]} grids]
|
||||||
|
(let [props (->> (d/without-keys params [:color])
|
||||||
|
(prefix-keys)
|
||||||
|
(clj->js))]
|
||||||
|
[:> "penpot:grid"
|
||||||
|
(-> props
|
||||||
|
(obj/set! "penpot:color" (get-in params [:color :color]))
|
||||||
|
(obj/set! "penpot:opacity" (get-in params [:color :opacity]))
|
||||||
|
(obj/set! "penpot:type" (d/name type))
|
||||||
|
(cond-> (some? display)
|
||||||
|
(obj/set! "penpot:display" (str display))))]))])
|
||||||
|
|
||||||
|
(mf/defc export-page
|
||||||
|
[{:keys [options]}]
|
||||||
|
(let [saved-grids (get options :saved-grids)]
|
||||||
|
(when-not (empty? saved-grids)
|
||||||
|
(let [parse-grid
|
||||||
|
(fn [[type params]]
|
||||||
|
{:type type :params params})
|
||||||
|
grids (->> saved-grids (mapv parse-grid))]
|
||||||
|
[:> "penpot:page" #js {}
|
||||||
|
[:& export-grid-data {:grids grids}]]))))
|
||||||
|
|
||||||
(mf/defc export-data
|
(mf/defc export-data
|
||||||
[{:keys [shape]}]
|
[{:keys [shape]}]
|
||||||
(let [props (-> (obj/new)
|
(let [props (-> (obj/new)
|
||||||
|
@ -135,5 +171,10 @@
|
||||||
(clj->js))))]
|
(clj->js))))]
|
||||||
[:> "penpot:svg-content" props
|
[:> "penpot:svg-content" props
|
||||||
(for [leaf (->> shape :content :content (filter string?))]
|
(for [leaf (->> shape :content :content (filter string?))]
|
||||||
[:> "penpot:svg-child" {} leaf])]))]))
|
[:> "penpot:svg-child" {} leaf])]))
|
||||||
|
|
||||||
|
|
||||||
|
(when (and (= (:type shape) :frame)
|
||||||
|
(not (empty? (:grids shape))))
|
||||||
|
[:& export-grid-data {:grids (:grids shape)}])]))
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
[:& file-uploader {:input-id "image-upload"
|
[:& file-uploader {:input-id "image-upload"
|
||||||
:accept cm/str-image-types
|
:accept cm/str-image-types
|
||||||
:multi true
|
:multi true
|
||||||
:input-ref ref
|
:ref ref
|
||||||
:on-selected on-files-selected}]]]))
|
:on-selected on-files-selected}]]]))
|
||||||
|
|
||||||
(mf/defc left-toolbar
|
(mf/defc left-toolbar
|
||||||
|
|
|
@ -743,7 +743,7 @@
|
||||||
i/plus
|
i/plus
|
||||||
[:& file-uploader {:accept cm/str-image-types
|
[:& file-uploader {:accept cm/str-image-types
|
||||||
:multi true
|
:multi true
|
||||||
:input-ref input-ref
|
:ref input-ref
|
||||||
:on-selected on-file-selected}]]])
|
:on-selected on-file-selected}]]])
|
||||||
|
|
||||||
[:& asset-section-block {:role :content}
|
[:& asset-section-block {:role :content}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.measurements :as msr]
|
[app.main.ui.measurements :as msr]
|
||||||
[app.main.ui.shapes.embed :as embed]
|
[app.main.ui.shapes.embed :as embed]
|
||||||
|
[app.main.ui.shapes.export :as use]
|
||||||
[app.main.ui.workspace.shapes :as shapes]
|
[app.main.ui.workspace.shapes :as shapes]
|
||||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
[app.main.ui.workspace.shapes.text.editor :as editor]
|
||||||
[app.main.ui.workspace.viewport.actions :as actions]
|
[app.main.ui.workspace.viewport.actions :as actions]
|
||||||
|
@ -188,6 +189,8 @@
|
||||||
:style {:background-color (get options :background "#E8E9EA")
|
:style {:background-color (get options :background "#E8E9EA")
|
||||||
:pointer-events "none"}}
|
:pointer-events "none"}}
|
||||||
|
|
||||||
|
[:& use/export-page {:options options}]
|
||||||
|
|
||||||
[:& (mf/provider embed/context) {:value true}
|
[:& (mf/provider embed/context) {:value true}
|
||||||
;; Render root shape
|
;; Render root shape
|
||||||
[:& shapes/root-shape {:key page-id
|
[:& shapes/root-shape {:key page-id
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[cljs.pprint :refer [pprint]]))
|
[cljs.pprint :refer [pprint]]))
|
||||||
|
|
||||||
(def debug-options #{:bounding-boxes :group :events :rotation-handler :resize-handler :selection-center #_:simple-selection})
|
(def debug-options #{:bounding-boxes :group :events :rotation-handler :resize-handler :selection-center :export :import #_:simple-selection})
|
||||||
|
|
||||||
;; These events are excluded when we activate the :events flag
|
;; These events are excluded when we activate the :events flag
|
||||||
(def debug-exclude-events
|
(def debug-exclude-events
|
||||||
|
|
|
@ -30,13 +30,28 @@
|
||||||
(and (vector? node)
|
(and (vector? node)
|
||||||
(= ::close (first node))))
|
(= ::close (first node))))
|
||||||
|
|
||||||
|
(defn find-node
|
||||||
|
[node tag]
|
||||||
|
(when (some? node)
|
||||||
|
(->> node :content (d/seek #(= (:tag %) tag)))))
|
||||||
|
|
||||||
|
(defn find-node-by-id
|
||||||
|
[id coll]
|
||||||
|
(->> coll (d/seek #(= id (-> % :attrs :id)))))
|
||||||
|
|
||||||
|
(defn find-all-nodes
|
||||||
|
[node tag]
|
||||||
|
(when (some? node)
|
||||||
|
(->> node :content (filterv #(= (:tag %) :defs)))))
|
||||||
|
|
||||||
(defn get-data
|
(defn get-data
|
||||||
([node]
|
([node]
|
||||||
(->> node :content (d/seek #(= :penpot:shape (:tag %)))))
|
(or (find-node node :penpot:shape)
|
||||||
([node tag]
|
(find-node node :penpot:page)))
|
||||||
(->> (get-data node)
|
|
||||||
:content
|
([node tag]
|
||||||
(d/seek #(= tag (:tag %))))))
|
(-> (get-data node)
|
||||||
|
(find-node tag))))
|
||||||
|
|
||||||
(defn get-type
|
(defn get-type
|
||||||
[node]
|
[node]
|
||||||
|
@ -98,6 +113,41 @@
|
||||||
m
|
m
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
|
(defn without-penpot-prefix
|
||||||
|
[m]
|
||||||
|
(let [no-penpot-prefix?
|
||||||
|
(fn [[k v]]
|
||||||
|
(not (str/starts-with? (d/name k) "penpot:")))]
|
||||||
|
(into {} (filter no-penpot-prefix?) m)))
|
||||||
|
|
||||||
|
(defn remove-penpot-prefix
|
||||||
|
[m]
|
||||||
|
(into {}
|
||||||
|
(map (fn [[k v]]
|
||||||
|
(if (str/starts-with? (d/name k) "penpot:")
|
||||||
|
[(-> k d/name (str/replace "penpot:" "") keyword) v]
|
||||||
|
[k v])))
|
||||||
|
m))
|
||||||
|
|
||||||
|
(defn camelize [[k v]]
|
||||||
|
[(-> k d/name str/camel keyword) v])
|
||||||
|
|
||||||
|
(defn camelize-keys
|
||||||
|
[m]
|
||||||
|
(assert (map? m) (str m))
|
||||||
|
|
||||||
|
(into {} (map camelize) m))
|
||||||
|
|
||||||
|
(defn fix-style-attr
|
||||||
|
[m]
|
||||||
|
(let [fix-style
|
||||||
|
(fn [[k v]]
|
||||||
|
(if (= k :style)
|
||||||
|
[k (-> v parse-style camelize-keys)]
|
||||||
|
[k v]))]
|
||||||
|
|
||||||
|
(d/deep-mapm (comp camelize fix-style) m)))
|
||||||
|
|
||||||
(def search-data-node? #{:rect :image :path :text :circle})
|
(def search-data-node? #{:rect :image :path :text :circle})
|
||||||
|
|
||||||
(defn get-svg-data
|
(defn get-svg-data
|
||||||
|
@ -148,6 +198,7 @@
|
||||||
selrect (gsh/content->selrect content-tr)
|
selrect (gsh/content->selrect content-tr)
|
||||||
points (-> (gsh/rect->points selrect)
|
points (-> (gsh/rect->points selrect)
|
||||||
(gsh/transform-points center transform))]
|
(gsh/transform-points center transform))]
|
||||||
|
|
||||||
(-> props
|
(-> props
|
||||||
(assoc :content content)
|
(assoc :content content)
|
||||||
(assoc :selrect selrect)
|
(assoc :selrect selrect)
|
||||||
|
@ -166,9 +217,6 @@
|
||||||
|
|
||||||
(def url-regex #"url\(#([^\)]*)\)")
|
(def url-regex #"url\(#([^\)]*)\)")
|
||||||
|
|
||||||
(defn seek-node
|
|
||||||
[id coll]
|
|
||||||
(->> coll (d/seek #(= id (-> % :attrs :id)))))
|
|
||||||
|
|
||||||
(defn parse-stops
|
(defn parse-stops
|
||||||
[gradient-node]
|
[gradient-node]
|
||||||
|
@ -183,7 +231,7 @@
|
||||||
(defn parse-gradient
|
(defn parse-gradient
|
||||||
[node ref-url]
|
[node ref-url]
|
||||||
(let [[_ url] (re-find url-regex ref-url)
|
(let [[_ url] (re-find url-regex ref-url)
|
||||||
gradient-node (->> node (node-seq) (seek-node url))
|
gradient-node (->> node (node-seq) (find-node-by-id url))
|
||||||
stops (parse-stops gradient-node)]
|
stops (parse-stops gradient-node)]
|
||||||
|
|
||||||
(when (contains? (:attrs gradient-node) :penpot:gradient)
|
(when (contains? (:attrs gradient-node) :penpot:gradient)
|
||||||
|
@ -231,19 +279,35 @@
|
||||||
flip-y (get-meta node :flip-y str->bool)
|
flip-y (get-meta node :flip-y str->bool)
|
||||||
proportion (get-meta node :proportion d/parse-double)
|
proportion (get-meta node :proportion d/parse-double)
|
||||||
proportion-lock (get-meta node :proportion-lock str->bool)
|
proportion-lock (get-meta node :proportion-lock str->bool)
|
||||||
rotation (get-meta node :rotation d/parse-double)]
|
rotation (get-meta node :rotation d/parse-double)
|
||||||
|
constraints-h (get-meta node :constraints-h keyword)
|
||||||
|
constraints-v (get-meta node :constraints-v keyword)
|
||||||
|
fixed-scroll (get-meta node :fixed-scroll str->bool)]
|
||||||
|
|
||||||
(-> props
|
(-> props
|
||||||
(assoc :name name)
|
(assoc :name name)
|
||||||
(assoc :blocked blocked)
|
(assoc :blocked blocked)
|
||||||
(assoc :hidden hidden)
|
(assoc :hidden hidden)
|
||||||
(assoc :transform transform)
|
|
||||||
(assoc :transform-inverse transform-inverse)
|
|
||||||
(assoc :flip-x flip-x)
|
(assoc :flip-x flip-x)
|
||||||
(assoc :flip-y flip-y)
|
(assoc :flip-y flip-y)
|
||||||
(assoc :proportion proportion)
|
(assoc :proportion proportion)
|
||||||
(assoc :proportion-lock proportion-lock)
|
(assoc :proportion-lock proportion-lock)
|
||||||
(assoc :rotation rotation))))
|
(assoc :rotation rotation)
|
||||||
|
|
||||||
|
(cond-> (some? transform)
|
||||||
|
(assoc :transform transform))
|
||||||
|
|
||||||
|
(cond-> (some? transform-inverse)
|
||||||
|
(assoc :transform-inverse transform-inverse))
|
||||||
|
|
||||||
|
(cond-> (some? constraints-h)
|
||||||
|
(assoc :constraints-h constraints-h))
|
||||||
|
|
||||||
|
(cond-> (some? constraints-v)
|
||||||
|
(assoc :constraints-v constraints-v))
|
||||||
|
|
||||||
|
(cond-> (some? fixed-scroll)
|
||||||
|
(assoc :fixed-scroll fixed-scroll)))))
|
||||||
|
|
||||||
(defn add-position
|
(defn add-position
|
||||||
[props type node svg-data]
|
[props type node svg-data]
|
||||||
|
@ -330,12 +394,17 @@
|
||||||
(assoc :rx rx :ry ry))))
|
(assoc :rx rx :ry ry))))
|
||||||
|
|
||||||
(defn add-image-data
|
(defn add-image-data
|
||||||
[props node]
|
[props type node]
|
||||||
(-> props
|
(let [metadata {:id (get-meta node :media-id)
|
||||||
(assoc-in [:metadata :id] (get-meta node :media-id))
|
:width (get-meta node :media-width)
|
||||||
(assoc-in [:metadata :width] (get-meta node :media-width))
|
:height (get-meta node :media-height)
|
||||||
(assoc-in [:metadata :height] (get-meta node :media-height))
|
:mtype (get-meta node :media-mtype)}]
|
||||||
(assoc-in [:metadata :mtype] (get-meta node :media-mtype))))
|
(cond-> props
|
||||||
|
(= type :image)
|
||||||
|
(assoc :metadata metadata)
|
||||||
|
|
||||||
|
(not= type :image)
|
||||||
|
(assoc :fill-image metadata))))
|
||||||
|
|
||||||
(defn add-text-data
|
(defn add-text-data
|
||||||
[props node]
|
[props node]
|
||||||
|
@ -372,6 +441,26 @@
|
||||||
:suffix (get-meta node :suffix)
|
:suffix (get-meta node :suffix)
|
||||||
:scale (get-meta node :scale d/parse-double)})
|
:scale (get-meta node :scale d/parse-double)})
|
||||||
|
|
||||||
|
|
||||||
|
(defn parse-grid-node [node]
|
||||||
|
(let [attrs (-> node :attrs remove-penpot-prefix)
|
||||||
|
color {:color (:color attrs)
|
||||||
|
:opacity (-> attrs :opacity d/parse-double)}
|
||||||
|
|
||||||
|
params (-> (d/without-keys attrs [:color :opacity :display :type])
|
||||||
|
(d/update-when :size d/parse-double)
|
||||||
|
(d/update-when :item-length d/parse-double)
|
||||||
|
(d/update-when :gutter d/parse-double)
|
||||||
|
(d/update-when :margin d/parse-double)
|
||||||
|
(assoc :color color))]
|
||||||
|
{:type (-> attrs :type keyword)
|
||||||
|
:display (-> attrs :display str->bool)
|
||||||
|
:params params}))
|
||||||
|
|
||||||
|
(defn parse-grids [node]
|
||||||
|
(let [grid-node (get-data node :penpot:grids)]
|
||||||
|
(->> grid-node :content (mapv parse-grid-node))))
|
||||||
|
|
||||||
(defn extract-from-data
|
(defn extract-from-data
|
||||||
([node tag]
|
([node tag]
|
||||||
(extract-from-data node tag identity))
|
(extract-from-data node tag identity))
|
||||||
|
@ -477,32 +566,6 @@
|
||||||
|
|
||||||
props)))
|
props)))
|
||||||
|
|
||||||
(defn without-penpot-prefix
|
|
||||||
[m]
|
|
||||||
(let [no-penpot-prefix?
|
|
||||||
(fn [[k v]]
|
|
||||||
(not (str/starts-with? (d/name k) "penpot:")))]
|
|
||||||
(into {} (filter no-penpot-prefix?) m)))
|
|
||||||
|
|
||||||
(defn camelize [[k v]]
|
|
||||||
[(-> k d/name str/camel keyword) v])
|
|
||||||
|
|
||||||
(defn camelize-keys
|
|
||||||
[m]
|
|
||||||
(assert (map? m) (str m))
|
|
||||||
|
|
||||||
(into {} (map camelize) m))
|
|
||||||
|
|
||||||
(defn fix-style-attr
|
|
||||||
[m]
|
|
||||||
(let [fix-style
|
|
||||||
(fn [[k v]]
|
|
||||||
(if (= k :style)
|
|
||||||
[k (-> v parse-style camelize-keys)]
|
|
||||||
[k v]))]
|
|
||||||
|
|
||||||
(d/deep-mapm (comp camelize fix-style) m)))
|
|
||||||
|
|
||||||
(defn add-svg-content
|
(defn add-svg-content
|
||||||
[props node]
|
[props node]
|
||||||
(let [svg-content (get-data node :penpot:svg-content)
|
(let [svg-content (get-data node :penpot:svg-content)
|
||||||
|
@ -522,13 +585,36 @@
|
||||||
:tag tag
|
:tag tag
|
||||||
:content node-content})))
|
:content node-content})))
|
||||||
|
|
||||||
|
(defn add-frame-data [props node]
|
||||||
|
(let [grids (parse-grids node)]
|
||||||
|
(cond-> props
|
||||||
|
(not (empty? grids))
|
||||||
|
(assoc :grids grids))))
|
||||||
|
|
||||||
|
(defn has-image?
|
||||||
|
[type node]
|
||||||
|
(let [pattern-image
|
||||||
|
(-> node
|
||||||
|
(find-node :defs)
|
||||||
|
(find-node :pattern)
|
||||||
|
(find-node :image))]
|
||||||
|
(or (= type :image)
|
||||||
|
(some? pattern-image))))
|
||||||
|
|
||||||
(defn get-image-name
|
(defn get-image-name
|
||||||
[node]
|
[node]
|
||||||
(get-in node [:attrs :penpot:name]))
|
(get-in node [:attrs :penpot:name]))
|
||||||
|
|
||||||
(defn get-image-data
|
(defn get-image-data
|
||||||
[node]
|
[node]
|
||||||
(let [svg-data (get-svg-data :image node)]
|
(let [pattern-data
|
||||||
|
(-> node
|
||||||
|
(find-node :defs)
|
||||||
|
(find-node :pattern)
|
||||||
|
(find-node :image)
|
||||||
|
:attrs)
|
||||||
|
image-data (get-svg-data :image node)
|
||||||
|
svg-data (or image-data pattern-data)]
|
||||||
(:xlink:href svg-data)))
|
(:xlink:href svg-data)))
|
||||||
|
|
||||||
(defn parse-data
|
(defn parse-data
|
||||||
|
@ -550,14 +636,31 @@
|
||||||
(cond-> (= :svg-raw type)
|
(cond-> (= :svg-raw type)
|
||||||
(add-svg-content node))
|
(add-svg-content node))
|
||||||
|
|
||||||
|
(cond-> (= :frame type)
|
||||||
|
(add-frame-data node))
|
||||||
|
|
||||||
(cond-> (= :group type)
|
(cond-> (= :group type)
|
||||||
(add-group-data node))
|
(add-group-data node))
|
||||||
|
|
||||||
(cond-> (= :rect type)
|
(cond-> (= :rect type)
|
||||||
(add-rect-data node svg-data))
|
(add-rect-data node svg-data))
|
||||||
|
|
||||||
(cond-> (= :image type)
|
(cond-> (some? (get-in node [:attrs :penpot:media-id]))
|
||||||
(add-image-data node))
|
(add-image-data type node))
|
||||||
|
|
||||||
(cond-> (= :text type)
|
(cond-> (= :text type)
|
||||||
(add-text-data node))))))
|
(add-text-data node))))))
|
||||||
|
|
||||||
|
(defn parse-page-data
|
||||||
|
[node]
|
||||||
|
(let [style (parse-style (get-in node [:attrs :style]))
|
||||||
|
background (:background style)
|
||||||
|
grids (->> (parse-grids node)
|
||||||
|
(group-by :type)
|
||||||
|
(d/mapm (fn [k v] (-> v first :params))))]
|
||||||
|
(cond-> {}
|
||||||
|
(some? background)
|
||||||
|
(assoc-in [:options :background] background)
|
||||||
|
|
||||||
|
(not (empty? grids))
|
||||||
|
(assoc-in [:options :saved-grids] grids))))
|
||||||
|
|
|
@ -117,7 +117,8 @@
|
||||||
|
|
||||||
(defn resolve-images
|
(defn resolve-images
|
||||||
[file-id node]
|
[file-id node]
|
||||||
(if (and (cip/shape? node) (= (cip/get-type node) :image) (not (cip/close? node)))
|
(if (and (not (cip/close? node))
|
||||||
|
(cip/has-image? type node))
|
||||||
(let [name (cip/get-image-name node)
|
(let [name (cip/get-image-name node)
|
||||||
data-uri (cip/get-image-data node)]
|
data-uri (cip/get-image-data node)]
|
||||||
(->> (upload-media-files file-id name data-uri)
|
(->> (upload-media-files file-id name data-uri)
|
||||||
|
@ -137,11 +138,13 @@
|
||||||
[file [page-name content]]
|
[file [page-name content]]
|
||||||
(if (cip/valid? content)
|
(if (cip/valid? content)
|
||||||
(let [nodes (->> content cip/node-seq)
|
(let [nodes (->> content cip/node-seq)
|
||||||
file-id (:id file)]
|
file-id (:id file)
|
||||||
|
page-data (-> (cip/parse-page-data content)
|
||||||
|
(assoc :name page-name))]
|
||||||
(->> (rx/from nodes)
|
(->> (rx/from nodes)
|
||||||
(rx/filter cip/shape?)
|
(rx/filter cip/shape?)
|
||||||
(rx/mapcat (partial resolve-images file-id))
|
(rx/mapcat (partial resolve-images file-id))
|
||||||
(rx/reduce add-shape-file (fb/add-page file page-name))
|
(rx/reduce add-shape-file (fb/add-page file page-data))
|
||||||
(rx/map fb/close-page)))
|
(rx/map fb/close-page)))
|
||||||
(rx/empty)))
|
(rx/empty)))
|
||||||
|
|
||||||
|
|
|
@ -2694,5 +2694,8 @@ msgstr "Export file"
|
||||||
msgid "dashboard.export-multi"
|
msgid "dashboard.export-multi"
|
||||||
msgstr "Export %s files"
|
msgstr "Export %s files"
|
||||||
|
|
||||||
|
msgid "dashboard.import"
|
||||||
|
msgstr "Import files"
|
||||||
|
|
||||||
msgid "dashboard.options"
|
msgid "dashboard.options"
|
||||||
msgstr "Options"
|
msgstr "Options"
|
||||||
|
|
Loading…
Add table
Reference in a new issue