🎉 Add dashboard custom fonts management.

This commit is contained in:
Andrey Antukh 2021-04-29 13:04:19 +02:00 committed by Andrés Moya
parent 2582e87ffa
commit e15a212b14
42 changed files with 1329 additions and 208 deletions

1
.gitignore vendored
View file

@ -26,6 +26,7 @@ node_modules
/frontend/out/ /frontend/out/
/frontend/.shadow-cljs /frontend/.shadow-cljs
/frontend/resources/public/* /frontend/resources/public/*
/frontend/resources/fonts/experiments
/exporter/target /exporter/target
/exporter/.shadow-cljs /exporter/.shadow-cljs
/docker/images/bundle* /docker/images/bundle*

View file

@ -47,7 +47,7 @@
org.postgresql/postgresql {:mvn/version "42.2.19"} org.postgresql/postgresql {:mvn/version "42.2.19"}
com.zaxxer/HikariCP {:mvn/version "4.0.3"} com.zaxxer/HikariCP {:mvn/version "4.0.3"}
funcool/datoteka {:mvn/version "1.2.0"} funcool/datoteka {:mvn/version "2.0.0"}
funcool/promesa {:mvn/version "6.0.0"} funcool/promesa {:mvn/version "6.0.0"}
funcool/cuerdas {:mvn/version "2020.03.26-3"} funcool/cuerdas {:mvn/version "2020.03.26-3"}

View file

@ -122,10 +122,15 @@
:app.rlimits/image :app.rlimits/image
(cf/get :rlimits-image) (cf/get :rlimits-image)
;; RLimit definition for font processing
:app.rlimits/font
(cf/get :rlimits-font 2)
;; A collection of rlimits as hash-map. ;; A collection of rlimits as hash-map.
:app.rlimits/all :app.rlimits/all
{:password (ig/ref :app.rlimits/password) {:password (ig/ref :app.rlimits/password)
:image (ig/ref :app.rlimits/image)} :image (ig/ref :app.rlimits/image)
:font (ig/ref :app.rlimits/font)}
:app.rpc/rpc :app.rpc/rpc
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)

View file

@ -5,7 +5,7 @@
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.media (ns app.media
"Media postprocessing." "Media & Font postprocessing."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@ -13,20 +13,29 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.rlimits :as rlm] [app.rlimits :as rlm]
[app.rpc.queries.svg :as svg] [app.rpc.queries.svg :as svg]
[clojure.java.io :as io]
[clojure.java.shell :as sh]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.core :as fs]) [datoteka.core :as fs])
(:import (:import
java.io.ByteArrayInputStream java.io.ByteArrayInputStream
java.io.OutputStream
org.apache.commons.io.IOUtils
org.im4java.core.ConvertCmd org.im4java.core.ConvertCmd
org.im4java.core.IMOperation org.im4java.core.IMOperation
org.im4java.core.Info)) org.im4java.core.Info))
;; --- Generic specs ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Utility functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::image-content-type cm/valid-image-types)
(s/def ::font-content-type cm/valid-font-types)
(s/def :internal.http.upload/filename ::us/string) (s/def :internal.http.upload/filename ::us/string)
(s/def :internal.http.upload/size ::us/integer) (s/def :internal.http.upload/size ::us/integer)
(s/def :internal.http.upload/content-type cm/valid-media-types) (s/def :internal.http.upload/content-type ::us/string)
(s/def :internal.http.upload/tempfile any?) (s/def :internal.http.upload/tempfile any?)
(s/def ::upload (s/def ::upload
@ -35,8 +44,44 @@
:internal.http.upload/tempfile :internal.http.upload/tempfile
:internal.http.upload/content-type])) :internal.http.upload/content-type]))
(defn validate-media-type
([mtype] (validate-media-type mtype cm/valid-image-types))
([mtype allowed]
(when-not (contains? allowed mtype)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "Seems like you are uploading an invalid media object"))))
(defmulti process :cmd)
(defmulti process-error class)
(defmethod process :default
[{:keys [cmd] :as params}]
(ex/raise :type :internal
:code :not-implemented
:hint (str/fmt "No impl found for process cmd: %s" cmd)))
(defmethod process-error :default
[error]
(ex/raise :type :internal :cause error))
(defn run
[{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
(us/assert map? rlimits)
(let [rlimit (get rlimits rlimit)]
(when-not rlimit
(ex/raise :type :internal
:code :rlimit-not-configured
:hint ":image rlimit not configured"))
(try
(rlm/execute rlimit (process params))
(catch Throwable e
(process-error e)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Thumbnails Generation ;; --- Thumbnails Generation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::cmd keyword?) (s/def ::cmd keyword?)
@ -77,8 +122,6 @@
:size (alength ^bytes thumbnail-data) :size (alength ^bytes thumbnail-data)
:data (ByteArrayInputStream. thumbnail-data))))) :data (ByteArrayInputStream. thumbnail-data)))))
(defmulti process :cmd)
(defmethod process :generic-thumbnail (defmethod process :generic-thumbnail
[{:keys [quality width height] :as params}] [{:keys [quality width height] :as params}]
(us/assert ::thumbnail-params params) (us/assert ::thumbnail-params params)
@ -161,33 +204,63 @@
:height (.getPageHeight instance) :height (.getPageHeight instance)
:mtype mtype})))) :mtype mtype}))))
(defmethod process :default (defmethod process-error org.im4java.core.InfoException
[{:keys [cmd] :as params}] [error]
(ex/raise :type :internal (ex/raise :type :validation
:code :not-implemented :code :invalid-image
:hint (str "No impl found for process cmd:" cmd))) :cause error))
(defn run ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[{:keys [rlimits]} params] ;; --- Fonts Generation
(us/assert map? rlimits) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(let [rlimit (get rlimits :image)]
(when-not rlimit
(ex/raise :type :internal
:code :rlimit-not-configured
:hint ":image rlimit not configured"))
(try
(rlm/execute rlimit (process params))
(catch org.im4java.core.InfoException e
(ex/raise :type :validation
:code :invalid-image
:cause e)))))
;; --- Utility functions (def all-fotmats #{"font/woff2", "font/woff", "font/otf", "font/ttf"})
(defn validate-media-type (defmethod process :generate-fonts
([mtype] (validate-media-type mtype cm/valid-media-types)) [{:keys [input] :as params}]
([mtype allowed] (letfn [(ttf->otf [data]
(when-not (contains? allowed mtype) (let [input-file (fs/create-tempfile :prefix "penpot")
(ex/raise :type :validation output-file (fs/path (str input-file ".otf"))
:code :media-type-not-allowed _ (with-open [out (io/output-stream input-file)]
:hint "Seems like you are uploading an invalid media object")))) (IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "fontforge" "-lang=ff" "-c"
(str/fmt "Open('%s'); Generate('%s')"
(str input-file)
(str output-file)))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))
(ttf-or-otf->woff [data]
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
output-file (fs/path (str input-file ".woff"))
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "sfnt2woff" (str input-file))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))
(ttf-or-otf->woff2 [data]
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
output-file (fs/path (str input-file ".woff2"))
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "woff2_compress" (str input-file))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))]
(let [current (into #{} (keys input))]
(if (contains? current "font/ttf")
(-> input
(assoc "font/otf" (ttf->otf (get input "font/ttf")))
(assoc "font/woff" (ttf-or-otf->woff (get input "font/ttf")))
(assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/ttf"))))
(-> input
;; TODO: pending to implement
;; (assoc "font/ttf" (otf->ttf (get input "font/ttf")))
(assoc "font/woff" (ttf-or-otf->woff (get input "font/otf")))
(assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/otf"))))))))

View file

@ -166,6 +166,9 @@
{:name "0052-del-legacy-user-and-team" {:name "0052-del-legacy-user-and-team"
:fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")} :fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")}
{:name "0053-add-team-font-variant-table"
:fn (mg/resource "app/migrations/sql/0053-add-team-font-variant-table.sql")}
]) ])

View file

@ -0,0 +1,20 @@
CREATE TABLE team_font_variant (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
profile_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
modified_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL DEFAULT NULL,
font_id text NOT NULL,
font_family text NOT NULL,
font_weight smallint NOT NULL,
font_style text NOT NULL,
otf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
ttf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
woff1_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
woff2_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL
);

View file

@ -18,6 +18,7 @@
(derive ::password ::instance) (derive ::password ::instance)
(derive ::image ::instance) (derive ::image ::instance)
(derive ::font ::instance)
(defmethod ig/pre-init-spec ::instance [_] (defmethod ig/pre-init-spec ::instance [_]
(s/spec int?)) (s/spec int?))

View file

@ -120,6 +120,7 @@
'app.rpc.queries.profile 'app.rpc.queries.profile
'app.rpc.queries.recent-files 'app.rpc.queries.recent-files
'app.rpc.queries.viewer 'app.rpc.queries.viewer
'app.rpc.queries.fonts
'app.rpc.queries.svg) 'app.rpc.queries.svg)
(map (partial process-method cfg)) (map (partial process-method cfg))
(into {})))) (into {}))))
@ -143,6 +144,7 @@
'app.rpc.mutations.teams 'app.rpc.mutations.teams
'app.rpc.mutations.management 'app.rpc.mutations.management
'app.rpc.mutations.ldap 'app.rpc.mutations.ldap
'app.rpc.mutations.fonts
'app.rpc.mutations.verify-token) 'app.rpc.mutations.verify-token)
(map (partial process-method cfg)) (map (partial process-method cfg))
(into {})))) (into {}))))

View file

@ -0,0 +1,116 @@
;; 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.rpc.mutations.fonts
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(declare create-font-variant)
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::name ::us/not-empty-string)
(s/def ::weight valid-weight)
(s/def ::style valid-style)
(s/def ::font-id (s/and ::us/string #(str/starts-with? % "custom-")))
(s/def ::content-type ::media/font-content-type)
(s/def ::data (s/map-of ::us/string any?))
(s/def ::create-font-variant
(s/keys :req-un [::profile-id ::team-id ::data
::font-id ::font-family ::font-weight ::font-style]))
(sv/defmethod ::create-font-variant
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(teams/check-edition-permissions! conn profile-id team-id)
(create-font-variant cfg params))))
(defn create-font-variant
[{:keys [conn storage] :as cfg} {:keys [data] :as params}]
(let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
storage (assoc storage :conn conn)
otf (when-let [fdata (get data "font/otf")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/otf"}))
ttf (when-let [fdata (get data "font/ttf")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/ttf"}))
woff1 (when-let [fdata (get data "font/woff")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/woff"}))
woff2 (when-let [fdata (get data "font/woff2")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/woff2"}))]
(db/insert! conn :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)
:font-id (:font-id params)
:font-family (:font-family params)
:font-weight (:font-weight params)
:font-style (:font-style params)
:woff1-file-id (:id woff1)
:woff2-file-id (:id woff2)
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)})))
;; --- UPDATE FONT VARIANT
(s/def ::update-font-variant
(s/keys :req-un [::profile-id ::team-id ::id ::font-family ::font-id]))
(sv/defmethod ::update-font-variant
[{:keys [pool] :as cfg} {:keys [id team-id profile-id font-family font-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(db/update! conn :team-font-variant
{:font-family font-family
:font-id font-id}
{:id id
:team-id team-id})
nil))
;; --- DELETE FONT VARIANT
(s/def ::delete-font-variant
(s/keys :req-un [::profile-id ::team-id ::id]))
(sv/defmethod ::delete-font-variant
[{:keys [pool] :as cfg} {:keys [id team-id profile-id font-family font-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
;; Schedule object deletion
(wrk/submit! {::wrk/task :delete-object
::wrk/delay cf/deletion-delay
::wrk/conn conn
:id id
:type :team-font-variant})
(db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id id
:team-id team-id})
nil))

View file

@ -32,12 +32,15 @@
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
;; --- Create File Media object (upload) ;; --- Create File Media object (upload)
(declare create-file-media-object) (declare create-file-media-object)
(declare select-file) (declare select-file)
(s/def ::content ::media/upload) (s/def ::content-type ::media/image-content-type)
(s/def ::content (s/and ::media/upload (s/keys :req-un [::content-type])))
(s/def ::is-local ::us/boolean) (s/def ::is-local ::us/boolean)
(s/def ::upload-file-media-object (s/def ::upload-file-media-object

View file

@ -401,7 +401,9 @@
(declare update-profile-photo) (declare update-profile-photo)
(s/def ::file ::media/upload) (s/def ::content-type ::media/image-content-type)
(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
(s/def ::update-profile-photo (s/def ::update-profile-photo
(s/keys :req-un [::profile-id ::file])) (s/keys :req-un [::profile-id ::file]))

View file

@ -249,7 +249,9 @@
(declare upload-photo) (declare upload-photo)
(s/def ::file ::media/upload) (s/def ::content-type ::media/image-content-type)
(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
(s/def ::update-team-photo (s/def ::update-team-photo
(s/keys :req-un [::profile-id ::team-id ::file])) (s/keys :req-un [::profile-id ::team-id ::file]))

View file

@ -0,0 +1,29 @@
;; 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.rpc.queries.fonts
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Query: Team Font Variants
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::team-font-variants
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::team-font-variants
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(db/query conn :team-font-variant
{:team-id team-id
:deleted-at nil})))

View file

@ -145,8 +145,8 @@
(make-output-stream [_ opts] (make-output-stream [_ opts]
(throw (UnsupportedOperationException. "not implemented"))) (throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted clojure.lang.Counted
(count [_] size))) (count [_] size)))
(defn content (defn content
([data] (content data nil)) ([data] (content data nil))

View file

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.storage :as sto]
[app.util.logging :as l] [app.util.logging :as l]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig])) [integrant.core :as ig]))
@ -24,7 +25,8 @@
(fn [{:keys [props] :as task}] (fn [{:keys [props] :as task}]
(us/verify ::props props) (us/verify ::props props)
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(handle-deletion conn props)))) (let [cfg (assoc cfg :conn conn)]
(handle-deletion cfg props)))))
(s/def ::type ::us/keyword) (s/def ::type ::us/keyword)
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
@ -34,21 +36,32 @@
(fn [_ props] (:type props))) (fn [_ props] (:type props)))
(defmethod handle-deletion :default (defmethod handle-deletion :default
[_conn {:keys [type]}] [_cfg {:keys [type]}]
(l/warn :hint "no handler found" (l/warn :hint "no handler found"
:type (d/name type))) :type (d/name type)))
(defmethod handle-deletion :file (defmethod handle-deletion :file
[conn {:keys [id] :as props}] [{:keys [conn]} {:keys [id] :as props}]
(let [sql "delete from file where id=? and deleted_at is not null"] (let [sql "delete from file where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :project (defmethod handle-deletion :project
[conn {:keys [id] :as props}] [{:keys [conn]} {:keys [id] :as props}]
(let [sql "delete from project where id=? and deleted_at is not null"] (let [sql "delete from project where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :team (defmethod handle-deletion :team
[conn {:keys [id] :as props}] [{:keys [conn]} {:keys [id] :as props}]
(let [sql "delete from team where id=? and deleted_at is not null"] (let [sql "delete from team where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id]))) (db/exec-one! conn [sql id])))
(defmethod handle-deletion :team-font-variant
[{:keys [conn storage]} {:keys [id] :as props}]
(let [font (db/get-by-id conn :team-font-variant id {:uncheked true})
storage (assoc storage :conn conn)]
(when (:deleted-at font)
(db/delete! conn :team-font-variant {:id id})
(some->> (:woff1-file-id font) (sto/del-object storage))
(some->> (:woff2-file-id font) (sto/del-object storage))
(some->> (:otf-file-id font) (sto/del-object storage))
(some->> (:ttf-file-id font) (sto/del-object storage)))))

View file

@ -101,7 +101,10 @@
:media-id (:media-id mobj) :media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj)) :thumbnail-id (:thumbnail-id mobj))
;; NOTE: deleting the file-media-object in the database ;; NOTE: deleting the file-media-object in the database
;; automatically marks as toched the referenced storage objects. ;; automatically marks as toched the referenced storage
;; objects. The touch mechanism is needed because many files can
;; point to the same storage objects and we can't just delete
;; them.
(db/delete! conn :file-media-object {:id (:id mobj)})) (db/delete! conn :file-media-object {:id (:id mobj)}))
nil)) nil))

View file

@ -261,6 +261,19 @@
(recur (reduce-kv assoc! res (first maps)) (recur (reduce-kv assoc! res (first maps))
(next maps))))) (next maps)))))
(defn distinct-xf
[f]
(fn [rf]
(let [seen (volatile! #{})]
(fn
([] (rf))
([result] (rf result))
([result input]
(let [input* (f input)]
(if (contains? @seen input*)
result
(do (vswap! seen conj input*)
(rf result input)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion ;; Data Parsing / Conversion

View file

@ -9,10 +9,10 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(def valid-media-types (def valid-font-types #{"font/ttf" "font/woff", "font/otf"})
#{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"}) (def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
(def str-image-types (str/join "," valid-image-types))
(def str-media-types (str/join "," valid-media-types)) (def str-font-types (str/join "," valid-font-types))
(defn format->extension (defn format->extension
[format] [format]
@ -65,3 +65,38 @@
::modified-at ::modified-at
::uri])) ::uri]))
(defn parse-font-weight
[variant]
(cond
(re-seq #"(?i)(?:hairline|thin)" variant) 100
(re-seq #"(?i)(?:extra light|ultra light)" variant) 200
(re-seq #"(?i)(?:light)" variant) 300
(re-seq #"(?i)(?:normal|regular)" variant) 400
(re-seq #"(?i)(?:medium)" variant) 500
(re-seq #"(?i)(?:semi bold|demi bold)" variant) 600
(re-seq #"(?i)(?:bold)" variant) 700
(re-seq #"(?i)(?:extra bold|ultra bold)" variant) 800
(re-seq #"(?i)(?:black|heavy)" variant) 900
(re-seq #"(?i)(?:extra black|ultra black)" variant) 950
:else 400))
(defn parse-font-style
[variant]
(if (re-seq #"(?i)(?:italic)" variant)
"italic"
"normal"))
(defn font-weight->name
[weight]
(case weight
100 "Hairline"
200 "Extra Light"
300 "Light"
400 "Regular"
500 "Medium"
600 "Semi Bold"
700 "Bold"
800 "Extra Bold"
900 "Black"
950 "Extra Black"))

View file

@ -6,7 +6,7 @@
(ns app.common.spec (ns app.common.spec
"Data manipulation and query helper functions." "Data manipulation and query helper functions."
(:refer-clojure :exclude [assert]) (:refer-clojure :exclude [assert bytes?])
#?(:cljs (:require-macros [app.common.spec :refer [assert]])) #?(:cljs (:require-macros [app.common.spec :refer [assert]]))
(:require (:require
#?(:clj [clojure.spec.alpha :as s] #?(:clj [clojure.spec.alpha :as s]
@ -108,6 +108,20 @@
(s/def ::point gpt/point?) (s/def ::point gpt/point?)
(s/def ::id ::uuid) (s/def ::id ::uuid)
(defn bytes?
"Test if a first parameter is a byte
array or not."
[x]
(if (nil? x)
false
#?(:clj (= (Class/forName "[B")
(.getClass ^Object x))
:cljs (or (instance? js/Uint8Array x)
(instance? js/ArrayBuffer x)))))
(s/def ::bytes bytes?)
(s/def ::safe-integer (s/def ::safe-integer
#(and #(and
(int? %) (int? %)

View file

@ -17,6 +17,7 @@ const mkdirp = require("mkdirp");
const rimraf = require("rimraf"); const rimraf = require("rimraf");
const sass = require("sass"); const sass = require("sass");
const gettext = require("gettext-parser"); const gettext = require("gettext-parser");
const marked = require("marked");
const mapStream = require("map-stream"); const mapStream = require("map-stream");
const paths = {}; const paths = {};
@ -45,17 +46,35 @@ function readLocales() {
for (let key of Object.keys(trdata)) { for (let key of Object.keys(trdata)) {
if (key === "") continue; if (key === "") continue;
const comments = trdata[key].comments || {};
if (l.isNil(result[key])) { if (l.isNil(result[key])) {
result[key] = {}; result[key] = {};
} }
const msgstr = trdata[key].msgstr; const isMarkdown = l.includes(comments.flag, "markdown");
if (msgstr.length === 1) {
result[key][lang] = msgstr[0]; const msgs = trdata[key].msgstr;
if (msgs.length === 1) {
let message = msgs[0];
if (isMarkdown) {
message = marked.parseInline(message);
}
result[key][lang] = message;
} else { } else {
result[key][lang] = msgstr; result[key][lang] = msgs.map((item) => {
if (isMarkdown) {
return marked.parseInline(item);
} else {
return item;
}
});
} }
// if (key === "modals.delete-font.title") {
// console.dir(trdata[key], {depth:10});
// console.dir(result[key], {depth:10});
// }
} }
} }

View file

@ -27,12 +27,13 @@
"gulp-sourcemaps": "^3.0.0", "gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^1.5.0", "gulp-svg-sprite": "^1.5.0",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"marked": "^2.0.3",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"postcss": "^8.2.7", "postcss": "^8.2.7",
"postcss-clean": "^1.2.2", "postcss-clean": "^1.2.2",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"sass": "^1.32.8", "sass": "^1.32.8",
"shadow-cljs": "^2.11.20" "shadow-cljs": "2.12.5"
}, },
"dependencies": { "dependencies": {
"date-fns": "^2.21.1", "date-fns": "^2.21.1",
@ -41,6 +42,7 @@
"js-beautify": "^1.13.5", "js-beautify": "^1.13.5",
"luxon": "^1.26.0", "luxon": "^1.26.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"opentype.js": "^1.3.3",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react": "~17.0.1", "react": "~17.0.1",
"react-dom": "~17.0.1", "react-dom": "~17.0.1",

View file

@ -63,6 +63,7 @@
@import "main/partials/dashboard-sidebar"; @import "main/partials/dashboard-sidebar";
@import "main/partials/dashboard-team"; @import "main/partials/dashboard-team";
@import "main/partials/dashboard-settings"; @import "main/partials/dashboard-settings";
@import "main/partials/dashboard-fonts";
@import "main/partials/debug-icons-preview"; @import "main/partials/debug-icons-preview";
@import "main/partials/editable-label"; @import "main/partials/editable-label";
@import "main/partials/left-toolbar"; @import "main/partials/left-toolbar";

View file

@ -0,0 +1,167 @@
.dashboard-fonts {
display: flex;
flex-direction: column;
align-items: center;
.dashboard-installed-fonts {
max-width: 1000px;
width: 100%;
display: flex;
margin-top: $big;
flex-direction: column;
h3 {
font-size: $fs14;
color: $color-gray-30;
margin: $x-small;
}
.font-item {
color: $color-black;
}
}
.installed-fonts-header {
color: $color-gray-40;
display: flex;
height: 40px;
font-size: $fs12;
background-color: $color-white;
align-items: center;
padding: 0px $big;
> div {
width: 30%;
}
.search-input {
display: flex;
flex-grow: 1;
justify-content: flex-end;
input {
font-size: $fs12;
border: 1px solid $color-gray-30;
border-radius: $br-small;
width: 130px;
padding: $x-small;
margin: 0px;
}
}
}
.fonts-group {
margin-top: $big;
}
.font-item {
color: $color-gray-40;
font-size: $fs14;
background-color: $color-white;
display: flex;
min-width: 1000px;
width: 100%;
height: 97px;
align-items: center;
padding: $big;
&:not(:first-child) {
border-top: 1px solid $color-gray-10;
}
input {
border: 1px solid $color-gray-30;
border-radius: $br-small;
margin: 0px;
padding: $small;
font-size: $fs12;
}
> div {
width: 30%;
}
.variant {
font-size: $fs14;
}
.filenames {
display: flex;
flex-direction: column;
font-size: $fs12;
}
.options {
display: flex;
justify-content: flex-end;
.icon {
width: $big;
cursor: pointer;
display: flex;
margin-left: 10px;
justify-content: center;
align-items: center;
svg {
width: 16px;
height: 16px;
}
&.close {
svg {
transform: rotate(45deg);
}
}
}
}
}
.dashboard-fonts-upload {
max-width: 1000px;
width: 100%;
display: flex;
flex-direction: column;
.upload-button {
width: 100px;
}
}
.dashboard-fonts-hero {
font-size: $fs14;
padding: $x-big;
background-color: $color-white;
margin-top: $x-big;
display: flex;
justify-content: space-between;
.banner {
background-color: unset;
display: flex;
.icon {
display: flex;
align-items: center;
padding-left: 0px;
padding-right: 10px;
svg {
fill: $color-info;
}
}
}
.desc {
h2 {
margin-bottom: $medium;
color: $color-black;
}
width: 80%;
color: $color-gray-40;
}
}
}

View file

@ -99,8 +99,6 @@
(->> (rp/query :team-stats {:team-id id}) (->> (rp/query :team-stats {:team-id id})
(rx/map #(partial fetched %))))))) (rx/map #(partial fetched %)))))))
;; --- Fetch Projects
(defn fetch-projects (defn fetch-projects
[{:keys [team-id] :as params}] [{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id) (us/assert ::us/uuid team-id)
@ -123,8 +121,6 @@
(ptk/watch (fetch-projects {:team-id id}) state stream) (ptk/watch (fetch-projects {:team-id id}) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)))))) (ptk/watch (du/fetch-users {:team-id id}) state stream))))))
;; --- Search Files
(s/def :internal.event.search-files/team-id ::us/uuid) (s/def :internal.event.search-files/team-id ::us/uuid)
(s/def :internal.event.search-files/search-term (s/nilable ::us/string)) (s/def :internal.event.search-files/search-term (s/nilable ::us/string))
@ -149,8 +145,6 @@
(->> (rp/query :search-files params) (->> (rp/query :search-files params)
(rx/map #(partial fetched %))))))) (rx/map #(partial fetched %)))))))
;; --- Fetch Files
(defn fetch-files (defn fetch-files
[{:keys [project-id] :as params}] [{:keys [project-id] :as params}]
(us/assert ::us/uuid project-id) (us/assert ::us/uuid project-id)
@ -162,8 +156,6 @@
(->> (rp/query :files params) (->> (rp/query :files params)
(rx/map #(partial fetched %))))))) (rx/map #(partial fetched %)))))))
;; --- Fetch Shared Files
(defn fetch-shared-files (defn fetch-shared-files
[{:keys [team-id] :as params}] [{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id) (us/assert ::us/uuid team-id)
@ -175,8 +167,6 @@
(->> (rp/query :shared-files {:team-id team-id}) (->> (rp/query :shared-files {:team-id team-id})
(rx/map #(partial fetched %))))))) (rx/map #(partial fetched %)))))))
;; --- Fetch recent files
(declare recent-files-fetched) (declare recent-files-fetched)
(defn fetch-recent-files (defn fetch-recent-files

View file

@ -0,0 +1,94 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.data.dashboard.fonts
(:require
[app.common.exceptions :as ex]
[app.common.data :as d]
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
[app.util.time :as dt]
[app.util.timers :as ts]
[app.main.data.messages :as dm]
[app.util.webapi :as wa]
[app.util.object :as obj]
[app.util.transit :as t]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]))
(defn fetch-fonts
[{:keys [id] :as team}]
(ptk/reify ::fetch-fonts
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query! :team-font-variants {:team-id id})
(rx/map (fn [items]
#(assoc % :dashboard-fonts (d/index-by :id items))))))))
(defn add-font
[font]
(ptk/reify ::add-font
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-fonts assoc (:id font) font))))
(defn update-font
[{:keys [id font-family] :as font}]
(ptk/reify ::update-font
ptk/UpdateEvent
(update [_ state]
(let [font (assoc font :font-id (str "custom-" (str/slug font-family)))]
(update state :dashboard-fonts assoc id font)))
ptk/WatchEvent
(watch [_ state stream]
(let [font (get-in state [:dashboard-fonts id])]
(->> (rp/mutation! :update-font-variant font)
(rx/ignore))))))
(defn delete-font
[{:keys [id] :as font}]
(ptk/reify ::delete-font
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-fonts dissoc id))
ptk/WatchEvent
(watch [_ state stream]
(let [params (select-keys font [:id :team-id])]
(->> (rp/mutation! :delete-font-variant params)
(rx/ignore))))))
;; (defn upload-font
;; [{:keys [id] :as font}]
;; (ptk/reify ::upload-font
;; ptk/WatchEvent
;; (watch [_ state stream]
;; (let [{:keys [on-success on-error]
;; :or {on-success identity
;; on-error rx/throw}} (meta params)]
;; (->> (rp/mutation! :create-font-variant font)
;; (rx/tap on-success)
;; (rx/catch on-error))))))
;; (defn add-font
;; "Add fonts to the state in a pending to upload state."
;; [font]
;; (ptk/reify ::add-font
;; ptk/UpdateEvent
;; (update [_ state]
;; (let [id (uuid/next)
;; font (-> font
;; (assoc :created-at (dt/now))
;; (assoc :id id)
;; (assoc :status :draft))]
;; (js/console.log (clj->js font))
;; (assoc-in state [:dashboard-fonts id] font)))))

View file

@ -51,7 +51,7 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :media-too-large :code :media-too-large
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file)))) :hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
(when-not (contains? cm/valid-media-types (.-type file)) (when-not (contains? cm/valid-image-types (.-type file))
(ex/raise :type :validation (ex/raise :type :validation
:code :media-type-not-allowed :code :media-type-not-allowed
:hint (str/fmt "media type %s is not supported" (.-type file)))) :hint (str/fmt "media type %s is not supported" (.-type file))))

View file

@ -43,6 +43,9 @@
(def dashboard-local (def dashboard-local
(l/derived :dashboard-local st/state)) (l/derived :dashboard-local st/state))
(def dashboard-fonts
(l/derived :dashboard-fonts st/state))
(def dashboard-selected-project (def dashboard-selected-project
(l/derived (fn [state] (l/derived (fn [state]
(get-in state [:dashboard-local :selected-project])) (get-in state [:dashboard-local :selected-project]))

View file

@ -284,7 +284,7 @@
([matches other] ([matches other]
(let [merge-coord (let [merge-coord
(fn [matches other] (fn [matches other]
(let [matches (into {} matches) (let [matches (into {} matches)
other (into {} other) other (into {} other)
keys (set/union (keys matches) (keys other))] keys (set/union (keys matches) (keys other))]
@ -305,7 +305,7 @@
(if (< (mth/abs cur-val) (mth/abs other-val)) (if (< (mth/abs cur-val) (mth/abs other-val))
current current
other)) other))
min-match-coord min-match-coord
(fn [matches] (fn [matches]
(if (and (seq matches) (not (empty? matches))) (if (and (seq matches) (not (empty? matches)))

View file

@ -90,6 +90,8 @@
["/settings" :dashboard-team-settings] ["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects] ["/projects" :dashboard-projects]
["/search" :dashboard-search] ["/search" :dashboard-search]
["/fonts" :dashboard-fonts]
["/fonts/providers" :dashboard-font-providers]
["/libraries" :dashboard-libraries] ["/libraries" :dashboard-libraries]
["/projects/:project-id" :dashboard-files]] ["/projects/:project-id" :dashboard-files]]
@ -135,12 +137,11 @@
:dashboard-projects :dashboard-projects
:dashboard-files :dashboard-files
:dashboard-libraries :dashboard-libraries
:dashboard-fonts
:dashboard-font-providers
:dashboard-team-members :dashboard-team-members
:dashboard-team-settings) :dashboard-team-settings)
[:* [:& dashboard {:route route}]
#_[:div.modal-wrapper
[:& app.main.ui.onboarding/release-notes-modal {:version "1.4"}]]
[:& dashboard {:route route}]]
:viewer :viewer
(let [index (get-in route [:query-params :index]) (let [index (get-in route [:query-params :index])

View file

@ -11,7 +11,6 @@
[app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.components.dropdown :refer [dropdown']]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.data :refer [classnames]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.object :as obj])) [app.util.object :as obj]))
@ -22,18 +21,18 @@
(assert (boolean? (gobj/get props "show")) "missing `show` prop") (assert (boolean? (gobj/get props "show")) "missing `show` prop")
(assert (vector? (gobj/get props "options")) "missing `options` prop") (assert (vector? (gobj/get props "options")) "missing `options` prop")
(let [open? (gobj/get props "show") (let [open? (gobj/get props "show")
on-close (gobj/get props "on-close") on-close (gobj/get props "on-close")
options (gobj/get props "options") options (gobj/get props "options")
is-selectable (gobj/get props "selectable") is-selectable (gobj/get props "selectable")
selected (gobj/get props "selected") selected (gobj/get props "selected")
top (gobj/get props "top" 0) top (gobj/get props "top" 0)
left (gobj/get props "left" 0) left (gobj/get props "left" 0)
fixed? (gobj/get props "fixed?" false) fixed? (gobj/get props "fixed?" false)
min-width? (gobj/get props "min-width?" false) min-width? (gobj/get props "min-width?" false)
local (mf/use-state {:offset 0 local (mf/use-state {:offset 0
:levels nil}) :levels nil})
on-local-close on-local-close
(mf/use-callback (mf/use-callback
@ -81,13 +80,13 @@
(when (and open? (some? (:levels @local))) (when (and open? (some? (:levels @local)))
[:> dropdown' props [:> dropdown' props
[:div.context-menu {:class (classnames :is-open open? [:div.context-menu {:class (dom/classnames :is-open open?
:fixed fixed? :fixed fixed?
:is-selectable is-selectable) :is-selectable is-selectable)
:style {:top (+ top (:offset @local)) :style {:top (+ top (:offset @local))
:left left}} :left left}}
(let [level (-> @local :levels peek)] (let [level (-> @local :levels peek)]
[:ul.context-menu-items {:class (classnames :min-width min-width?) [:ul.context-menu-items {:class (dom/classnames :min-width min-width?)
:ref check-menu-offscreen} :ref check-menu-offscreen}
(when-let [parent-option (:parent-option level)] (when-let [parent-option (:parent-option level)]
[:* [:*
@ -103,8 +102,7 @@
(if (= option-name :separator) (if (= option-name :separator)
[:li.separator] [:li.separator]
[:li.context-menu-item [:li.context-menu-item
{:class (classnames :is-selected (and selected {:class (dom/classnames :is-selected (and selected (= option-name selected)))
(= option-name selected)))
:key option-name} :key option-name}
(if-not sub-options (if-not sub-options
[:a.context-menu-action {:on-click option-handler} [:a.context-menu-action {:on-click option-handler}

View file

@ -18,6 +18,7 @@
[app.main.ui.dashboard.files :refer [files-section]] [app.main.ui.dashboard.files :refer [files-section]]
[app.main.ui.dashboard.libraries :refer [libraries-page]] [app.main.ui.dashboard.libraries :refer [libraries-page]]
[app.main.ui.dashboard.projects :refer [projects-section]] [app.main.ui.dashboard.projects :refer [projects-section]]
[app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
[app.main.ui.dashboard.search :refer [search-page]] [app.main.ui.dashboard.search :refer [search-page]]
[app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
@ -65,6 +66,12 @@
:dashboard-projects :dashboard-projects
[:& projects-section {:team team :projects projects}] [:& projects-section {:team team :projects projects}]
:dashboard-fonts
[:& fonts-page {:team team}]
:dashboard-font-providers
[:& font-providers-page {:team team}]
:dashboard-files :dashboard-files
(when project (when project
[:& files-section {:team team :project project}]) [:& files-section {:team team :project project}])
@ -121,17 +128,19 @@
[:& (mf/provider ctx/current-page-id) {:value nil} [:& (mf/provider ctx/current-page-id) {:value nil}
[:section.dashboard-layout [:section.dashboard-layout
[:& sidebar {:team team [:& sidebar
:projects projects {:team team
:project project :projects projects
:profile profile :project project
:section section :profile profile
:search-term search-term}] :section section
:search-term search-term}]
(when (and team (seq projects)) (when (and team (seq projects))
[:& dashboard-content {:projects projects [:& dashboard-content
:profile profile {:projects projects
:project project :profile profile
:section section :project project
:search-term search-term :section section
:team team}])]]]]])) :search-term search-term
:team team}])]]]]]))

View file

@ -0,0 +1,353 @@
;; 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.fonts
(:require
["opentype.js" :as ot]
[app.common.media :as cm]
[app.common.uuid :as uuid]
[app.main.data.dashboard :as dd]
[app.main.data.dashboard.fonts :as df]
[app.main.data.modal :as modal]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.store :as st]
[app.main.repo :as rp]
[app.main.refs :as refs]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.logging :as log]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.webapi :as wa]
[cuerdas.core :as str]
[beicon.core :as rx]
[okulary.core :as l]
[rumext.alpha :as mf]))
(log/set-level! :trace)
(defn- use-set-page-title
[team section]
(mf/use-effect
(mf/deps team)
(fn []
(when team
(let [tname (if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))]
(case section
:fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
:providers (dom/set-html-title (tr "title.dashboard.font-providers" tname))))))))
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [section team] :as props}]
(let [go-fonts
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
go-providers
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-font-providers {:team-id (:id team)})))]
(use-set-page-title team section)
[:header.dashboard-header
[:div.dashboard-title
[:h1 (tr "labels.fonts")]]
[:nav
[:ul
[:li {:class (when (= section :fonts) "active")}
[:a {:on-click go-fonts} (tr "labels.custom-fonts")]]
[:li {:class (when (= section :providers) "active")}
[:a {:on-click go-providers} (tr "labels.font-providers")]]]]
[:div]]))
(defn- prepare-fonts
[blobs]
(letfn [(prepare [{:keys [font type name data] :as params}]
(let [family (or (.getEnglishName ^js font "preferredFamily")
(.getEnglishName ^js font "fontFamily"))
variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontSubfamily"))]
{:content {:data (js/Uint8Array. data)
:name name
:type type}
:font-id (str "custom-" (str/slug family))
:font-family family
:font-weight (cm/parse-font-weight variant)
:font-style (cm/parse-font-style variant)}))
(parse-mtype [mtype]
(case mtype
"application/vnd.oasis.opendocument.formula-template" "font/otf"
mtype))
(parse-font [{:keys [data] :as params}]
(try
(assoc params :font (ot/parse data))
(catch :default e
(log/warn :msg (str/fmt "skiping file %s, unsupported format" (:name params)))
nil)))
(read-blob [blob]
(->> (wa/read-file-as-array-buffer blob)
(rx/map (fn [data]
{:data data
:name (.-name blob)
:type (parse-mtype (.-type blob))}))))]
(->> (rx/from blobs)
(rx/mapcat read-blob)
(rx/map parse-font)
(rx/filter some?)
(rx/map prepare))))
(mf/defc fonts-upload
[{:keys [team] :as props}]
(let [fonts (mf/use-state {})
input-ref (mf/use-ref)
uploading (mf/use-state #{})
on-click
(mf/use-callback #(dom/click (mf/ref-val input-ref)))
font-key-fn
(mf/use-callback (juxt :font-family :font-weight :font-style))
on-selected
(mf/use-callback
(mf/deps team)
(fn [blobs]
(->> (prepare-fonts blobs)
(rx/subs (fn [{:keys [content] :as font}]
(let [key (font-key-fn font)]
(swap! fonts update key
(fn [val]
(-> (or val font)
(assoc :team-id (:id team))
(update :id #(or % (uuid/next)))
(update :data assoc (:type content) (:data content))
(update :names (fnil conj #{}) (:name content))
(dissoc :content))))))
(fn [error]
(js/console.error "error" error))))))
on-upload
(mf/use-callback
(mf/deps team)
(fn [item]
(let [key (font-key-fn item)]
(swap! uploading conj (:id item))
(->> (rp/mutation! :create-font-variant item)
(rx/delay-at-least 2000)
(rx/subs (fn [font]
(swap! fonts dissoc key)
(swap! uploading disj (:id item))
(st/emit! (df/add-font font)))
(fn [error]
(js/console.log "error" error)))))))
on-delete
(mf/use-callback
(mf/deps team)
(fn [item]
(swap! fonts dissoc (font-key-fn item))))]
[:div.dashboard-fonts-upload
[:div.dashboard-fonts-hero
[:div.desc
[:h2 (tr "labels.upload-custom-fonts")]
[:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}]
[:div.banner
[:div.icon i/msg-info]
[:div.content
[:& i18n/tr-html {:tag-name "span"
:label "dashboard.fonts.hero-text2"}]]]]
[:div.btn-primary
{:on-click on-click}
[:span "Add custom font"]
[:& file-uploader {:input-id "font-upload"
:accept cm/str-font-types
:multi true
:input-ref input-ref
:on-selected on-selected}]]]
[:*
(for [item (sort-by :font-family (vals @fonts))]
(let [uploading? (contains? @uploading (:id item))]
[:div.font-item.table-row {:key (:id item)}
[:div.table-field.family
[:input {:type "text"
:default-value (:font-family item)}]]
[:div.table-field.variant
[:span (cm/font-weight->name (:font-weight item))]
(when (not= "normal" (:font-style item))
[:span " " (str/capital (:font-style item))])]
[:div.table-field.filenames
(for [item (:names item)]
[:span item])]
[:div.table-field.options
[:button.btn-primary.upload-button
{:on-click #(on-upload item)
:class (dom/classnames :disabled uploading?)
:disabled uploading?}
(if uploading?
(tr "labels.uploading")
(tr "labels.upload"))]
[:span.icon.close {:on-click #(on-delete item)} i/close]]]))]]))
(mf/defc installed-font
[{:keys [font] :as props}]
(let [open-menu? (mf/use-state false)
edit? (mf/use-state false)
state (mf/use-var (:font-family font))
on-change
(mf/use-callback
(mf/deps font)
(fn [event]
(reset! state (dom/get-target-val event))))
on-save
(mf/use-callback
(mf/deps font)
(fn [event]
(let [font (assoc font :font-family @state)]
(st/emit! (df/update-font font))
(reset! edit? false))))
on-key-down
(mf/use-callback
(mf/deps font)
(fn [event]
(when (kbd/enter? event)
(on-save event))))
on-cancel
(mf/use-callback
(mf/deps font)
(fn [event]
(reset! edit? false)
(reset! state (:font-family font))))
delete-fn
(mf/use-callback
(mf/deps font)
(st/emitf (df/delete-font font)))
on-delete
(mf/use-callback
(mf/deps font)
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.delete-font.title")
:message (tr "modals.delete-font.message")
:accept-label (tr "labels.delete")
:on-accept delete-fn})))]
[:div.font-item.table-row {:key (:id font)}
[:div.table-field.family
(if @edit?
[:input {:type "text"
:default-value @state
:on-key-down on-key-down
:on-change on-change}]
[:span (:font-family font)])]
[:div.table-field.variant
[:span (cm/font-weight->name (:font-weight font))]
(when (not= "normal" (:font-style font))
[:span " " (str/capital (:font-style font))])]
[:div]
(if @edit?
[:div.table-field.options
[:button.btn-primary
{:disabled (str/blank? @state)
:on-click on-save
:class (dom/classnames :btn-disabled (str/blank? @state))}
"Save"]
[:span.icon.close {:on-click on-cancel} i/close]]
[:div.table-field.options
[:span.icon {:on-click #(reset! open-menu? true)} i/actions]
[:& context-menu
{:on-close #(reset! open-menu? false)
:show @open-menu?
:fixed? false
:top -15
:left -115
:options [[(tr "labels.edit") #(reset! edit? true)]
[(tr "labels.delete") on-delete]]}]])]))
(mf/defc installed-fonts
[{:keys [team fonts] :as props}]
(let [sterm (mf/use-state "")
matches?
#(str/includes? (str/lower (:font-family %)) @sterm)
on-change
(mf/use-callback
(fn [event]
(let [val (dom/get-target-val event)]
(reset! sterm val))))]
[:div.dashboard-installed-fonts
[:h3 (tr "labels.installed-fonts")]
[:div.installed-fonts-header
[:div.table-field.family (tr "labels.font-family")]
[:div.table-field.variant (tr "labels.font-variant")]
[:div]
[:div.table-field.search-input
[:input {:placeholder (tr "labels.search-font")
:default-value ""
:on-change on-change
}]]]
(for [[font-id fonts] (->> fonts
(filter matches?)
(group-by :font-id))]
[:div.fonts-group
(for [font (sort-by (juxt :font-weight :font-style) fonts)]
[:& installed-font {:key (:id font) :font font}])])]))
(mf/defc fonts-page
[{:keys [team] :as props}]
(let [fonts-map (mf/deref refs/dashboard-fonts)
fonts (vals fonts-map)]
(mf/use-effect
(mf/deps team)
(st/emitf (df/fetch-fonts team)))
[:*
[:& header {:team team :section :fonts}]
[:section.dashboard-container.dashboard-fonts
[:& fonts-upload {:team team}]
(when fonts
[:& installed-fonts {:team team
:fonts fonts}])]]))
(mf/defc font-providers-page
[{:keys [team] :as props}]
[:*
[:& header {:team team :section :providers}]
[:section.dashboard-container
[:span "hello world font providers"]]])

View file

@ -28,7 +28,7 @@
[app.util.avatars :as avatars] [app.util.avatars :as avatars]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.router :as rt] [app.util.router :as rt]
@ -47,10 +47,11 @@
selected-project (:selected-project dstate) selected-project (:selected-project dstate)
edit-id (:project-for-edit dstate) edit-id (:project-for-edit dstate)
local (mf/use-state {:menu-open false local (mf/use-state
:menu-pos nil {:menu-open false
:edition? (= (:id item) edit-id) :menu-pos nil
:dragging? false}) :edition? (= (:id item) edit-id)
:dragging? false})
on-click on-click
(mf/use-callback (mf/use-callback
@ -60,11 +61,13 @@
:project-id (:id item)})))) :project-id (:id item)}))))
on-menu-click on-menu-click
(mf/use-callback (fn [event] (mf/use-callback
(let [position (dom/get-client-position event)] (fn [event]
(dom/prevent-default event) (let [position (dom/get-client-position event)]
(swap! local assoc :menu-open true (dom/prevent-default event)
:menu-pos position)))) (swap! local assoc
:menu-open true
:menu-pos position))))
on-menu-close on-menu-close
(mf/use-callback #(swap! local assoc :menu-open false)) (mf/use-callback #(swap! local assoc :menu-open false))
@ -139,7 +142,7 @@
:on-menu-close on-menu-close}]])) :on-menu-close on-menu-close}]]))
(mf/defc sidebar-search (mf/defc sidebar-search
[{:keys [search-term team-id locale] :as props}] [{:keys [search-term team-id] :as props}]
(let [search-term (or search-term "") (let [search-term (or search-term "")
focused? (mf/use-state false) focused? (mf/use-state false)
emit! (mf/use-memo #(f/debounce st/emit! 500)) emit! (mf/use-memo #(f/debounce st/emit! 500))
@ -183,7 +186,7 @@
{:key :images-search-box {:key :images-search-box
:id "search-input" :id "search-input"
:type "text" :type "text"
:placeholder (t locale "dashboard.search-placeholder") :placeholder (tr "dashboard.search-placeholder")
:default-value search-term :default-value search-term
:auto-complete "off" :auto-complete "off"
:on-focus on-search-focus :on-focus on-search-focus
@ -201,7 +204,7 @@
i/search])])) i/search])]))
(mf/defc teams-selector-dropdown (mf/defc teams-selector-dropdown
[{:keys [team profile locale] :as props}] [{:keys [team profile] :as props}]
(let [show-dropdown? (mf/use-state false) (let [show-dropdown? (mf/use-state false)
teams (mf/deref refs/teams) teams (mf/deref refs/teams)
@ -216,11 +219,11 @@
(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))] (st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
[:ul.dropdown.teams-dropdown [:ul.dropdown.teams-dropdown
[:li.title (t locale "dashboard.switch-team")] [:li.title (tr "dashboard.switch-team")]
[:hr] [:hr]
[:li.team-name {:on-click (partial team-selected (:default-team-id profile))} [:li.team-name {:on-click (partial team-selected (:default-team-id profile))}
[:span.team-icon i/logo-icon] [:span.team-icon i/logo-icon]
[:span.team-text (t locale "dashboard.your-penpot")]] [:span.team-text (tr "dashboard.your-penpot")]]
(for [team (remove :is-default (vals teams))] (for [team (remove :is-default (vals teams))]
[:* {:key (:id team)} [:* {:key (:id team)}
@ -231,7 +234,7 @@
[:hr] [:hr]
[:li.action {:on-click on-create-clicked} [:li.action {:on-click on-create-clicked}
(t locale "dashboard.create-new-team")]])) (tr "dashboard.create-new-team")]]))
(s/def ::member-id ::us/uuid) (s/def ::member-id ::us/uuid)
(s/def ::leave-modal-form (s/def ::leave-modal-form
@ -292,7 +295,7 @@
(mf/defc team-options-dropdown (mf/defc team-options-dropdown
[{:keys [team locale profile] :as props}] [{:keys [team profile] :as props}]
(let [members (mf/use-state []) (let [members (mf/use-state [])
go-members go-members
@ -341,9 +344,9 @@
(mf/deps team) (mf/deps team)
(st/emitf (modal/show (st/emitf (modal/show
{:type :confirm {:type :confirm
:title (t locale "modals.leave-confirm.title") :title (tr "modals.leave-confirm.title")
:message (t locale "modals.leave-confirm.message") :message (tr "modals.leave-confirm.message")
:accept-label (t locale "modals.leave-confirm.accept") :accept-label (tr "modals.leave-confirm.accept")
:on-accept leave-fn}))) :on-accept leave-fn})))
on-leave-as-owner-clicked on-leave-as-owner-clicked
@ -366,9 +369,9 @@
(mf/deps team) (mf/deps team)
(st/emitf (modal/show (st/emitf (modal/show
{:type :confirm {:type :confirm
:title (t locale "modals.delete-team-confirm.title") :title (tr "modals.delete-team-confirm.title")
:message (t locale "modals.delete-team-confirm.message") :message (tr "modals.delete-team-confirm.message")
:accept-label (t locale "modals.delete-team-confirm.accept") :accept-label (tr "modals.delete-team-confirm.accept")
:on-accept delete-fn})))] :on-accept delete-fn})))]
(mf/use-layout-effect (mf/use-layout-effect
@ -378,25 +381,25 @@
(rx/subs #(reset! members %))))) (rx/subs #(reset! members %)))))
[:ul.dropdown.options-dropdown [:ul.dropdown.options-dropdown
[:li {:on-click go-members} (t locale "labels.members")] [:li {:on-click go-members} (tr "labels.members")]
[:li {:on-click go-settings} (t locale "labels.settings")] [:li {:on-click go-settings} (tr "labels.settings")]
[:hr] [:hr]
[:li {:on-click on-rename-clicked} (t locale "labels.rename")] [:li {:on-click on-rename-clicked} (tr "labels.rename")]
(cond (cond
(:is-owner team) (:is-owner team)
[:li {:on-click on-leave-as-owner-clicked} (t locale "dashboard.leave-team")] [:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
(> (count @members) 1) (> (count @members) 1)
[:li {:on-click on-leave-clicked} (t locale "dashboard.leave-team")]) [:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")])
(when (:is-owner team) (when (:is-owner team)
[:li {:on-click on-delete-clicked} (t locale "dashboard.delete-team")])])) [:li {:on-click on-delete-clicked} (tr "dashboard.delete-team")])]))
(mf/defc sidebar-team-switch (mf/defc sidebar-team-switch
[{:keys [team profile locale] :as props}] [{:keys [team profile] :as props}]
(let [show-dropdown? (mf/use-state false) (let [show-dropdown? (mf/use-state false)
show-team-opts-ddwn? (mf/use-state false) show-team-opts-ddwn? (mf/use-state false)
@ -408,7 +411,7 @@
(if (:is-default team) (if (:is-default team)
[:div.team-name [:div.team-name
[:span.team-icon i/logo-icon] [:span.team-icon i/logo-icon]
[:span.team-text (t locale "dashboard.default-team-name")]] [:span.team-text (tr "dashboard.default-team-name")]]
[:div.team-name [:div.team-name
[:span.team-icon [:span.team-icon
[:img {:src (cfg/resolve-team-photo-url team)}]] [:img {:src (cfg/resolve-team-photo-url team)}]]
@ -425,23 +428,22 @@
[:& dropdown {:show @show-teams-ddwn? [:& dropdown {:show @show-teams-ddwn?
:on-close #(reset! show-teams-ddwn? false)} :on-close #(reset! show-teams-ddwn? false)}
[:& teams-selector-dropdown {:team team [:& teams-selector-dropdown {:team team
:profile profile :profile profile}]]
:locale locale}]]
[:& dropdown {:show @show-team-opts-ddwn? [:& dropdown {:show @show-team-opts-ddwn?
:on-close #(reset! show-team-opts-ddwn? false)} :on-close #(reset! show-team-opts-ddwn? false)}
[:& team-options-dropdown {:team team [:& team-options-dropdown {:team team
:profile profile :profile profile}]]]))
:locale locale}]]]))
(mf/defc sidebar-content (mf/defc sidebar-content
[{:keys [locale projects profile section team project search-term] :as props}] [{:keys [projects profile section team project search-term] :as props}]
(let [default-project-id (let [default-project-id
(->> (vals projects) (->> (vals projects)
(d/seek :is-default) (d/seek :is-default)
(:id)) (:id))
projects? (= section :dashboard-projects) projects? (= section :dashboard-projects)
fonts? (= section :dashboard-fonts)
libs? (= section :dashboard-libraries) libs? (= section :dashboard-libraries)
drafts? (and (= section :dashboard-files) drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id)) (= (:id project) default-project-id))
@ -451,6 +453,11 @@
(mf/deps team) (mf/deps team)
(st/emitf (rt/nav :dashboard-projects {:team-id (:id team)}))) (st/emitf (rt/nav :dashboard-projects {:team-id (:id team)})))
go-fonts
(mf/use-callback
(mf/deps team)
(st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
go-drafts go-drafts
(mf/use-callback (mf/use-callback
(mf/deps team default-project-id) (mf/deps team default-project-id)
@ -469,29 +476,36 @@
(filter :is-pinned))] (filter :is-pinned))]
[:div.sidebar-content [:div.sidebar-content
[:& sidebar-team-switch {:team team :profile profile :locale locale}] [:& sidebar-team-switch {:team team :profile profile}]
[:hr] [:hr]
[:& sidebar-search {:search-term search-term [:& sidebar-search {:search-term search-term
:team-id (:id team) :team-id (:id team)}]
:locale locale}]
[:div.sidebar-content-section [:div.sidebar-content-section
[:ul.sidebar-nav.no-overflow [:ul.sidebar-nav.no-overflow
[:li.recent-projects [:li.recent-projects
{:on-click go-projects {:on-click go-projects
:class-name (when projects? "current")} :class-name (when projects? "current")}
[:span.element-title (t locale "labels.projects")]] [:span.element-title (tr "labels.projects")]]
[:li {:on-click go-drafts [:li {:on-click go-drafts
:class-name (when drafts? "current")} :class-name (when drafts? "current")}
[:span.element-title (t locale "labels.drafts")]] [:span.element-title (tr "labels.drafts")]]
[:li {:on-click go-libs [:li {:on-click go-libs
:class-name (when libs? "current")} :class-name (when libs? "current")}
[:span.element-title (t locale "labels.shared-libraries")]]]] [:span.element-title (tr "labels.shared-libraries")]]]]
[:hr] [:hr]
[:div.sidebar-content-section
[:ul.sidebar-nav.no-overflow
[:li.recent-projects
{:on-click go-fonts
:class-name (when fonts? "current")}
[:span.element-title (tr "labels.fonts")]]]]
[:hr]
[:div.sidebar-content-section [:div.sidebar-content-section
(if (seq pinned-projects) (if (seq pinned-projects)
[:ul.sidebar-nav [:ul.sidebar-nav
@ -504,11 +518,11 @@
:selected? (= (:id item) (:id project))}])] :selected? (= (:id item) (:id project))}])]
[:div.sidebar-empty-placeholder [:div.sidebar-empty-placeholder
[:span.icon i/pin] [:span.icon i/pin]
[:span.text (t locale "dashboard.no-projects-placeholder")]])]])) [:span.text (tr "dashboard.no-projects-placeholder")]])]]))
(mf/defc profile-section (mf/defc profile-section
[{:keys [profile locale team] :as props}] [{:keys [profile team] :as props}]
(let [show (mf/use-state false) (let [show (mf/use-state false)
photo (cfg/resolve-profile-photo-url profile) photo (cfg/resolve-profile-photo-url profile)
@ -530,18 +544,18 @@
[:ul.dropdown [:ul.dropdown
[:li {:on-click (partial on-click :settings-profile)} [:li {:on-click (partial on-click :settings-profile)}
[:span.icon i/user] [:span.icon i/user]
[:span.text (t locale "labels.profile")]] [:span.text (tr "labels.profile")]]
[:li {:on-click (partial on-click :settings-password)} [:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock] [:span.icon i/lock]
[:span.text (t locale "labels.password")]] [:span.text (tr "labels.password")]]
[:li {:on-click (partial on-click (da/logout))} [:li {:on-click (partial on-click (da/logout))}
[:span.icon i/exit] [:span.icon i/exit]
[:span.text (t locale "labels.logout")]] [:span.text (tr "labels.logout")]]
(when cfg/feedback-enabled (when cfg/feedback-enabled
[:li.feedback {:on-click (partial on-click :settings-feedback)} [:li.feedback {:on-click (partial on-click :settings-feedback)}
[:span.icon i/msg-info] [:span.icon i/msg-info]
[:span.text (t locale "labels.give-feedback")] [:span.text (tr "labels.give-feedback")]
[:span.primary-badge "ALPHA"]])]]] [:span.primary-badge "ALPHA"]])]]]
(when (and team profile) (when (and team profile)
@ -552,15 +566,11 @@
{::mf/wrap-props false {::mf/wrap-props false
::mf/wrap [mf/memo]} ::mf/wrap [mf/memo]}
[props] [props]
(let [locale (mf/deref i18n/locale) (let [team (obj/get props "team")
team (obj/get props "team") profile (obj/get props "profile")]
profile (obj/get props "profile")
props (-> (obj/clone props)
(obj/set! "locale" locale))]
[:div.dashboard-sidebar [:div.dashboard-sidebar
[:div.sidebar-inside [:div.sidebar-inside
[:> sidebar-content props] [:> sidebar-content props]
[:& profile-section [:& profile-section
{:profile profile {:profile profile
:team team :team team}]]]))
:locale locale}]]]))

View file

@ -18,33 +18,33 @@
(mf/defc banner (mf/defc banner
[{:keys [type position status controls content actions on-close] :as props}] [{:keys [type position status controls content actions on-close] :as props}]
[:div.banner {:class (dom/classnames [:div.banner {:class (dom/classnames
:warning (= type :warning) :warning (= type :warning)
:error (= type :error) :error (= type :error)
:success (= type :success) :success (= type :success)
:info (= type :info) :info (= type :info)
:fixed (= position :fixed) :fixed (= position :fixed)
:floating (= position :floating) :floating (= position :floating)
:inline (= position :inline) :inline (= position :inline)
:hide (= status :hide))} :hide (= status :hide))}
[:div.wrapper [:div.wrapper
[:div.icon (case type [:div.icon (case type
:warning i/msg-warning :warning i/msg-warning
:error i/msg-error :error i/msg-error
:success i/msg-success :success i/msg-success
:info i/msg-info :info i/msg-info
i/msg-error)] i/msg-error)]
[:div.content {:class (dom/classnames [:div.content {:class (dom/classnames
:inline-actions (= controls :inline-actions) :inline-actions (= controls :inline-actions)
:bottom-actions (= controls :bottom-actions))} :bottom-actions (= controls :bottom-actions))}
content content
(when (or (= controls :bottom-actions) (= controls :inline-actions)) (when (or (= controls :bottom-actions) (= controls :inline-actions))
[:div.actions [:div.actions
(for [action actions] (for [action actions]
[:div.btn-secondary.btn-small {:key (uuid/next) [:div.btn-secondary.btn-small {:key (uuid/next)
:on-click (:callback action)} :on-click (:callback action)}
(:label action)])])] (:label action)])])]
(when (= controls :close) (when (= controls :close)
[:div.btn-close {:on-click on-close} i/close])]]) [:div.btn-close {:on-click on-close} i/close])]])
(mf/defc notifications (mf/defc notifications
[] []

View file

@ -50,7 +50,7 @@
[:* [:*
i/image i/image
[:& file-uploader {:input-id "image-upload" [:& file-uploader {:input-id "image-upload"
:accept cm/str-media-types :accept cm/str-image-types
:multi true :multi true
:input-ref ref :input-ref ref
:on-selected on-files-selected}]]])) :on-selected on-files-selected}]]]))

View file

@ -212,7 +212,7 @@
(fn [path] (fn [path]
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! state update :folded-groups (swap! state update :folded-groups
toggle-folded-group path)))) toggle-folded-group path))))
on-group on-group
@ -400,7 +400,7 @@
(fn [path] (fn [path]
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! state update :folded-groups (swap! state update :folded-groups
toggle-folded-group path)))) toggle-folded-group path))))
on-group on-group
@ -426,7 +426,7 @@
(when local? (when local?
[:div.assets-button {:on-click add-graphic} [:div.assets-button {:on-click add-graphic}
i/plus i/plus
[:& file-uploader {:accept cm/str-media-types [:& file-uploader {:accept cm/str-image-types
:multi true :multi true
:input-ref input-ref :input-ref input-ref
:on-selected on-file-selected}]])] :on-selected on-file-selected}]])]

View file

@ -116,7 +116,6 @@
snap-lines (->> (into (process-snap-lines @state :x) snap-lines (->> (into (process-snap-lines @state :x)
(process-snap-lines @state :y)) (process-snap-lines @state :y))
(into #{}))] (into #{}))]
(mf/use-effect (mf/use-effect
(fn [] (fn []
(let [sub (->> subject (let [sub (->> subject

View file

@ -10,6 +10,7 @@
[app.config :as cfg] [app.config :as cfg]
[app.util.globals :as globals] [app.util.globals :as globals]
[app.util.storage :refer [storage]] [app.util.storage :refer [storage]]
[app.util.object :as obj]
[app.util.transit :as t] [app.util.transit :as t]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -136,6 +137,13 @@
([code] (t @locale code)) ([code] (t @locale code))
([code & args] (apply t @locale code args))) ([code & args] (apply t @locale code args)))
(mf/defc tr-html
{::mf/wrap-props false}
[props]
(let [label (obj/get props "label")
tag-name (obj/get props "tag-name" "p")]
[:> tag-name {:dangerouslySetInnerHTML #js {:__html (tr label)}}]))
;; DEPRECATED ;; DEPRECATED
(defn use-locale (defn use-locale
[] []

View file

@ -29,6 +29,10 @@
[file] [file]
(file-reader #(.readAsText %1 file))) (file-reader #(.readAsText %1 file)))
(defn read-file-as-array-buffer
[file]
(file-reader #(.readAsArrayBuffer %1 file)))
(defn read-file-as-data-url (defn read-file-as-data-url
[file] [file]
(file-reader #(.readAsDataURL ^js %1 file))) (file-reader #(.readAsDataURL ^js %1 file)))

View file

@ -1,25 +1,68 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Free Software Foundation, Inc.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "PO-Revision-Date: 2021-04-14 13:44+0000\n"
"PO-Revision-Date: 2021-04-22 13:43+0200\n" "Last-Translator: Andrey Antukh <niwi@niwi.nz>\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: Spanish "
"Language-Team: LANGUAGE <LL@li.org>\n" "<https://hosted.weblate.org/projects/penpot/frontend/en/>\n"
"Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=iso-8859-1\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.6-dev\n"
#: src/app/main/ui/dashboard/fonts.cljs
msgid "title.dashboard.fonts"
msgstr "Fonts - %s - Penpot"
#: src/app/main/ui/dashboard/fonts.cljs
msgid "title.dashboard.font-providers"
msgstr "Font Providers - %s - Penpot"
msgid "labels.upload"
msgstr "Upload"
msgid "labels.uploading"
msgstr "Uploading..."
msgid "modals.delete-font.title"
msgstr "Deleting font"
msgid "modals.delete-font.message"
msgstr "Are you sure you want to delete this font? It will not load if is used in a file."
msgid "labels.fonts"
msgstr "Fonts"
msgid "labels.installed-fonts"
msgstr "Installed fonts"
msgid "labels.font-family"
msgstr "Font Family"
msgid "labels.font-variant"
msgstr "Style"
msgid "labels.custom-fonts"
msgstr "Custom fonts"
msgid "labels.search-font"
msgstr "Search font"
msgid "labels.font-providers"
msgstr "Font providers"
msgid "labels.upload-custom-fonts"
msgstr "Upload custom fonts"
#, markdown
msgid "dashboard.fonts.hero-text1"
msgstr "Any web font you upload here will be added to the font family list available at the text properties of the files of this team. Fonts with the same font family name will be grouped as a **single font family**. You can upload fonts with the following formats: **TTF, OTF and WOFF** (only one will be needed)."
#, markdown
msgid "dashboard.fonts.hero-text2"
msgstr "You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of [Penpot's Terms of Service](https://penpot.app/terms.html). You also might want to read about [font licensing](2)."
# ~ msgid ""
# ~ msgstr ""
# ~ "Language: en\n"
# ~ "MIME-Version: 1.0\n"
# ~ "Content-Type: text/plain; charset=utf-8\n"
# ~ "Content-Transfer-Encoding: 8bit\n"
# ~ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/app/main/ui/auth/register.cljs #: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account" msgid "auth.already-have-account"
msgstr "Already have an account?" msgstr "Already have an account?"

View file

@ -11,6 +11,68 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.6-dev\n" "X-Generator: Weblate 4.6-dev\n"
#: src/app/main/ui/dashboard/fonts.cljs
msgid "title.dashboard.fonts"
msgstr "Fuentes - %s - Penpot"
#: src/app/main/ui/dashboard/fonts.cljs
msgid "title.dashboard.font-providers"
msgstr "Proveedores de fuentes - %s - Penpot"
msgid "labels.upload"
msgstr "Subir"
msgid "labels.uploading"
msgstr "Subiendo..."
msgid "modals.delete-font.title"
msgstr "Eliminando fuente"
msgid "modals.delete-font.message"
msgstr "Are you sure you want to delete this font? It will not load if is used in a file."
msgstr "¿Estas seguro que quieres eliminar esta fuente? La fuente dejara de cargar si es usada en algun fichero."
msgid "labels.fonts"
msgstr "Fuentes"
msgid "labels.installed-fonts"
msgstr "Fuentes instaladas"
msgid "labels.font-family"
msgstr "Familia de fuente"
msgid "labels.font-variant"
msgstr "Estilo"
msgid "labels.custom-fonts"
msgstr "Fuentes personalizadas"
msgid "labels.search-font"
msgstr "Buscar fuente"
msgid "labels.font-providers"
msgstr "Proveedores de fuentes"
msgid "labels.upload-custom-fonts"
msgstr "Subir fuente"
#, markdown
msgid "dashboard.fonts.hero-text1"
msgstr "Any web font you upload here will be added to the font family list available at the text properties of the files of this team. Fonts with the same font family name will be grouped as a **single font family**. You can upload fonts with the following formats: **TTF, OTF and WOFF** (only one will be needed)."
#, markdown
msgid "dashboard.fonts.hero-text2"
msgstr "You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of [Penpot's Terms of Service](https://penpot.app/terms.html). You also might want to read about [font licensing](2)."
#: src/app/main/ui/auth/register.cljs #: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account" msgid "auth.already-have-account"
msgstr "¿Tienes ya una cuenta?" msgstr "¿Tienes ya una cuenta?"

View file

@ -3120,6 +3120,11 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" object-visit "^1.0.0"
marked@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0"
integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==
matchdep@^2.0.0: matchdep@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
@ -3644,6 +3649,14 @@ one-time@^1.0.0:
dependencies: dependencies:
fn.name "1.x.x" fn.name "1.x.x"
opentype.js@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.3.tgz#65b8645b090a1ad444065b784d442fa19d1061f6"
integrity sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==
dependencies:
string.prototype.codepointat "^0.2.1"
tiny-inflate "^1.0.3"
ordered-read-streams@^1.0.0: ordered-read-streams@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
@ -4364,9 +4377,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
inherits "^2.0.1" inherits "^2.0.1"
rxjs@~7.0.0-beta.12: rxjs@~7.0.0-beta.12:
version "7.0.0-rc.1" version "7.0.0-rc.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.1.tgz#11f368e740e2b3cfe805891be127d07391673654" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.2.tgz#bd5b18ff9b60ca28ea4b3a824419035007064fdf"
integrity sha512-FVFOeT+eGdbcPe+uH+cWnEElrU4LiDMrlstNSUpI3MPErICLtVoUCbKrF+n+8DYemHDe7wPqYtuNEYTM3ur3xw== integrity sha512-81+TFxK8hUK3tmJ9TPon07bgun2ASgZ8OXumUuWSAnktSAzTvubw4NCJTr0Tc0lO9IfTThi5z3GDVlmjY3n5ug==
dependencies: dependencies:
tslib "~2.1.0" tslib "~2.1.0"
@ -4480,7 +4493,7 @@ shadow-cljs-jar@1.3.2:
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@^2.11.20: shadow-cljs@2.12.5:
version "2.12.5" version "2.12.5"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4" resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ== integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ==
@ -4796,6 +4809,11 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0" strip-ansi "^5.1.0"
string.prototype.codepointat@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc"
integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
string.prototype.trimend@^1.0.4: string.prototype.trimend@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@ -5021,6 +5039,11 @@ timers-ext@^0.1.7:
es5-ext "~0.10.46" es5-ext "~0.10.46"
next-tick "1" next-tick "1"
tiny-inflate@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
to-absolute-glob@^2.0.0: to-absolute-glob@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"