diff --git a/.gitignore b/.gitignore index 0bf57fa62..4695ea70a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ node_modules /frontend/out/ /frontend/.shadow-cljs /frontend/resources/public/* +/frontend/resources/fonts/experiments /exporter/target /exporter/.shadow-cljs /docker/images/bundle* diff --git a/CHANGES.md b/CHANGES.md index 4d35b7109..3a0659912 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,14 +1,59 @@ # CHANGELOG # + ## :rocket: Next ### :sparkles: New features + ### :bug: Bugs fixed ### :arrow_up: Deps updates ### :boom: Breaking changes ### :heart: Community contributions by (Thank you!) +## 1.6.0-alpha + +### :sparkles: New features + +- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292). +- Add option to interactively scale text [Taiga #1527](https://tree.taiga.io/project/penpot/us/1527) +- Add performance improvements on dashboard data loading. +- Add performance improvements to indexes handling on workspace. +- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292). +- Transform shapes to path on double click +- Translate automatic names of new files and projects. +- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697). +- New translations: Portuguese (Brazil) and Romanias. + + +### :bug: Bugs fixed + +- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656). +- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940). +- Fix problem with imported SVG on editing paths [#971](https://github.com/penpot/penpot/issues/971) +- Fix problem with color picker positioning +- Fix order on color palette [#961](https://github.com/penpot/penpot/issues/961) +- Fix issue when group creation leaves an empty group [#1724](https://tree.taiga.io/project/penpot/issue/1724) +- Fix problem with :multiple for colors and typographies [#1668](https://tree.taiga.io/project/penpot/issue/1668) +- Fix problem with locked shapes when change parents [#974](https://github.com/penpot/penpot/issues/974) + +### :arrow_up: Deps updates + +- Update exporter dependencies (puppeteer), that fixes some unexpected exceptions. +- Update string manipulation library. + + +### :boom: Breaking changes + +- The OIDC setting `PENPOT_OIDC_SCOPES` has changed the default semantics. Before this + configuration added scopes to the default set. Now it replaces it, so use with care, because + penpot requires at least `name` and `email` props found on the user info object. + +### :heart: Community contributions by (Thank you!) + +- Translations: Portuguese (Brazil) and Romanias. + + ## 1.5.4-alpha ### :bug: Bugs fixed diff --git a/backend/deps.edn b/backend/deps.edn index 5659d1a62..8032825c0 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -4,8 +4,8 @@ "jcenter" {:url "https://jcenter.bintray.com/"}} :deps {org.clojure/clojure {:mvn/version "1.10.3"} - org.clojure/data.json {:mvn/version "2.2.1"} - org.clojure/core.async {:mvn/version "1.3.610"} + org.clojure/data.json {:mvn/version "2.2.3"} + org.clojure/core.async {:mvn/version "1.3.618"} org.clojure/tools.cli {:mvn/version "1.0.206"} org.clojure/clojurescript {:mvn/version "1.10.844"} @@ -32,28 +32,28 @@ org.eclipse.jetty/jetty-servlet]} io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"} - selmer/selmer {:mvn/version "1.12.33"} + selmer/selmer {:mvn/version "1.12.40"} expound/expound {:mvn/version "0.8.9"} com.cognitect/transit-clj {:mvn/version "1.0.324"} - io.lettuce/lettuce-core {:mvn/version "6.1.1.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.2"} info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"} - com.github.seancorfield/next.jdbc {:mvn/version "1.1.646"} - metosin/reitit-ring {:mvn/version "0.5.12"} - metosin/jsonista {:mvn/version "0.3.1"} + com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"} + metosin/reitit-ring {:mvn/version "0.5.13"} + metosin/jsonista {:mvn/version "0.3.3"} - org.postgresql/postgresql {:mvn/version "42.2.19"} + org.postgresql/postgresql {:mvn/version "42.2.20"} com.zaxxer/HikariCP {:mvn/version "4.0.3"} - funcool/datoteka {:mvn/version "1.2.0"} - funcool/promesa {:mvn/version "6.0.0"} - funcool/cuerdas {:mvn/version "2020.03.26-3"} + funcool/datoteka {:mvn/version "2.0.0"} + funcool/promesa {:mvn/version "6.0.1"} + funcool/cuerdas {:mvn/version "2021.05.09-0"} - buddy/buddy-core {:mvn/version "1.9.0"} - buddy/buddy-hashers {:mvn/version "1.7.0"} - buddy/buddy-sign {:mvn/version "3.3.0"} + buddy/buddy-core {:mvn/version "1.10.1"} + buddy/buddy-hashers {:mvn/version "1.8.1"} + buddy/buddy-sign {:mvn/version "3.4.1"} lambdaisland/uri {:mvn/version "1.4.54" :exclusions [org.clojure/data.json]} @@ -69,7 +69,7 @@ org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} integrant/integrant {:mvn/version "0.8.0"} - software.amazon.awssdk/s3 {:mvn/version "2.16.44"} + software.amazon.awssdk/s3 {:mvn/version "2.16.62"} ;; exception printing io.aviso/pretty {:mvn/version "0.1.37"} @@ -78,9 +78,9 @@ :aliases {:dev {:extra-deps - {com.bhauman/rebel-readline {:mvn/version "0.1.4"} - org.clojure/tools.namespace {:mvn/version "1.1.0"} - org.clojure/test.check {:mvn/version "1.1.0"} + {com.bhauman/rebel-readline {:mvn/version "RELEASE"} + org.clojure/tools.namespace {:mvn/version "RELEASE"} + org.clojure/test.check {:mvn/version "RELEASE"} fipp/fipp {:mvn/version "0.6.23"} criterium/criterium {:mvn/version "0.4.6"} diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index eb38cb183..1b9dba567 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -12,6 +12,11 @@ + + + tcp://localhost:45556 + + @@ -30,10 +35,12 @@ + - - + + + diff --git a/backend/scripts/repl b/backend/scripts/repl index 32fd12d42..1f434abcf 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -2,7 +2,9 @@ export PENPOT_ASSERTS_ENABLED=true -export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Xms512m -J-Xmx512m -J-Dlog4j2.configurationFile=log4j2-devenv.xml" +export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m"; +# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions"; +# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000"; export OPTIONS_EVAL="nil" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)" diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 0e9d4ac5d..30ca83d38 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -8,6 +8,8 @@ "A configuration management." (:refer-clojure :exclude [get]) (:require + [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.version :as v] [app.util.time :as dt] @@ -16,7 +18,8 @@ [clojure.pprint :as pprint] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [environ.core :refer [env]])) + [environ.core :refer [env]] + [integrant.core :as ig])) (prefer-method print-method clojure.lang.IRecord @@ -26,6 +29,16 @@ clojure.lang.IPersistentMap clojure.lang.IDeref) +(defmethod ig/init-key :default + [_ data] + (d/without-nils data)) + +(defmethod ig/prep-key :default + [_ data] + (if (map? data) + (d/without-nils data) + data)) + (def defaults {:http-server-port 6060 :host "devenv" @@ -34,8 +47,7 @@ :database-username "penpot" :database-password "penpot" - :default-blob-version 1 - + :default-blob-version 3 :loggers-zmq-uri "tcp://localhost:45556" :asserts-enabled false @@ -72,7 +84,6 @@ :allow-demo-users true :registration-enabled true - :registration-domain-whitelist "" :telemetry-enabled false :telemetry-uri "https://telemetry.penpot.app/" @@ -87,6 +98,13 @@ :initial-project-skey "initial-project" }) +(s/def ::audit-enabled ::us/boolean) +(s/def ::audit-archive-enabled ::us/boolean) +(s/def ::audit-archive-uri ::us/string) +(s/def ::audit-archive-gc-enabled ::us/boolean) +(s/def ::audit-archive-gc-max-age ::dt/duration) + +(s/def ::secret-key ::us/string) (s/def ::allow-demo-users ::us/boolean) (s/def ::asserts-enabled ::us/boolean) (s/def ::assets-path ::us/string) @@ -142,7 +160,7 @@ (s/def ::profile-complaint-threshold ::us/integer) (s/def ::public-uri ::us/string) (s/def ::redis-uri ::us/string) -(s/def ::registration-domain-whitelist ::us/string) +(s/def ::registration-domain-whitelist ::us/set-of-str) (s/def ::registration-enabled ::us/boolean) (s/def ::rlimits-image ::us/integer) (s/def ::rlimits-password ::us/integer) @@ -162,14 +180,18 @@ (s/def ::storage-s3-bucket ::us/string) (s/def ::storage-s3-region ::us/keyword) (s/def ::telemetry-enabled ::us/boolean) -(s/def ::telemetry-server-enabled ::us/boolean) -(s/def ::telemetry-server-port ::us/integer) (s/def ::telemetry-uri ::us/string) (s/def ::telemetry-with-taiga ::us/boolean) (s/def ::tenant ::us/string) (s/def ::config - (s/keys :opt-un [::allow-demo-users + (s/keys :opt-un [::secret-key + ::allow-demo-users + ::audit-enabled + ::audit-archive-enabled + ::audit-archive-uri + ::audit-archive-gc-enabled + ::audit-archive-gc-max-age ::asserts-enabled ::database-password ::database-uri @@ -242,8 +264,6 @@ ::storage-s3-bucket ::storage-s3-region ::telemetry-enabled - ::telemetry-server-enabled - ::telemetry-server-port ::telemetry-uri ::telemetry-referer ::telemetry-with-taiga @@ -263,9 +283,17 @@ (defn- read-config [] - (->> (read-env "penpot") - (merge defaults) - (us/conform ::config))) + (try + (->> (read-env "penpot") + (merge defaults) + (us/conform ::config)) + (catch Throwable e + (when (ex/ex-info? e) + (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;") + (println "Error on validating configuration:") + (println (:explain (ex-data e)) + (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))) + (throw e)))) (def version (v/parse (or (some-> (io/resource "version.txt") (slurp) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 32a91e8d6..b9886ec7e 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -200,6 +200,13 @@ (sql/insert table params opts) (assoc opts :return-keys true)))) +(defn insert-multi! + ([ds table cols rows] (insert-multi! ds table cols rows nil)) + ([ds table cols rows opts] + (exec! ds + (sql/insert-multi table cols rows opts) + (assoc opts :return-keys true)))) + (defn update! ([ds table params where] (update! ds table params where nil)) ([ds table params where opts] @@ -326,6 +333,12 @@ (t/decode-str val) val))) +(defn inet + [ip-addr] + (doto (org.postgresql.util.PGobject.) + (.setType "inet") + (.setValue (str ip-addr)))) + (defn tjson "Encode as transit json." [data] diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj index d2c92db38..6ee5d3073 100644 --- a/backend/src/app/db/sql.clj +++ b/backend/src/app/db/sql.clj @@ -32,6 +32,11 @@ (assoc :suffix "ON CONFLICT DO NOTHING"))] (sql/for-insert table key-map opts)))) +(defn insert-multi + [table cols rows opts] + (let [opts (merge default-opts opts)] + (sql/for-insert-multi table cols rows opts))) + (defn select ([table where-params] (select table where-params nil)) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index d2e45ed01..6f0d85cc0 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -76,6 +76,7 @@ {:status 500 :body {:type :server-error + :code :assertion :data (-> edata (assoc :explain (explain-error edata)) (dissoc :data))}})) @@ -103,6 +104,7 @@ :cause error) {:status 500 :body {:type :server-error + :code :unexpected :hint (ex-message error) :data edata}})))) @@ -132,7 +134,8 @@ :else {:status 500 - :body {:type :server-timeout + :body {:type :server-error + :code :psql-exception :hint (ex-message error) :state state}}))) diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 60e0a90c8..41a40ab18 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -98,10 +98,11 @@ res (http/send! req)] (when (= 200 (:status res)) - (let [{:keys [name] :as data} (json/read-str (:body res) :key-fn keyword)] - (-> data - (assoc :backend (:name provider)) - (assoc :fullname name))))) + (let [info (json/read-str (:body res) :key-fn keyword)] + {:backend (:name provider) + :email (:email info) + :fullname (:name info) + :props (dissoc info :name :email)}))) (catch Exception e (l/error :hint "unexpected exception on retrieve-user-info" @@ -117,7 +118,8 @@ (retrieve-user-info cfg))] (when-not info (ex/raise :type :internal - :code :unable-to-auth)) + :code :unable-to-auth + :hint "no user info")) ;; If the provider is OIDC, we can proceed to check ;; roles if they are defined. @@ -138,16 +140,35 @@ (cond-> info (some? (:invitation-token state)) - (assoc :invitation-token (:invitation-token state))))) + (assoc :invitation-token (:invitation-token state)) + + ;; If state token comes with props, merge them. The state token + ;; props can contain pm_ and utm_ prefixed query params. + (map? (:props state)) + (update :props merge (:props state))))) ;; --- HTTP HANDLERS +(defn extract-props + [params] + (reduce-kv (fn [params k v] + (let [sk (name k)] + (cond-> params + (or (str/starts-with? sk "pm_") + (str/starts-with? sk "pm-") + (str/starts-with? sk "utm_")) + (assoc (-> sk str/kebab keyword) v)))) + {} + params)) + (defn- auth-handler - [{:keys [tokens] :as cfg} request] - (let [invitation (get-in request [:params :invitation-token]) + [{:keys [tokens] :as cfg} {:keys [params] :as request}] + (let [invitation (:invitation-token params) + props (extract-props params) state (tokens :generate {:iss :oauth :invitation-token invitation + :props props :exp (dt/in-future "15m")}) uri (build-auth-uri cfg state)] {:status 200 @@ -215,8 +236,7 @@ :token-uri (cf/get :oidc-token-uri) :auth-uri (cf/get :oidc-auth-uri) :user-uri (cf/get :oidc-user-uri) - :scopes (into #{"openid" "profile" "email" "name"} - (cf/get :oidc-scopes #{})) + :scopes (cf/get :oidc-scopes #{"openid" "profile"}) :roles-attr (cf/get :oidc-roles-attr) :roles (cf/get :oidc-roles) :name "oidc"}] @@ -238,9 +258,7 @@ [cfg] (let [opts {:client-id (cf/get :google-client-id) :client-secret (cf/get :google-client-secret) - :scopes #{"email" "profile" "openid" - "https://www.googleapis.com/auth/userinfo.email" - "https://www.googleapis.com/auth/userinfo.profile"} + :scopes #{"openid" "email" "profile"} :auth-uri "https://accounts.google.com/o/oauth2/v2/auth" :token-uri "https://oauth2.googleapis.com/token" :user-uri "https://openidconnect.googleapis.com/v1/userinfo" @@ -256,8 +274,7 @@ [cfg] (let [opts {:client-id (cf/get :github-client-id) :client-secret (cf/get :github-client-secret) - :scopes #{"read:user" - "user:email"} + :scopes #{"read:user" "user:email"} :auth-uri "https://github.com/login/oauth/authorize" :token-uri "https://github.com/login/oauth/access_token" :user-uri "https://api.github.com/user" diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index af0fd6d8f..d1b0170bb 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -106,7 +106,6 @@ ;; --- STATE INIT: SESSION UPDATER -(declare batch-events) (declare update-sessions) (s/def ::session map?) @@ -129,7 +128,9 @@ (l/info :action "initialize session updater" :max-batch-age (str (:max-batch-age cfg)) :max-batch-size (str (:max-batch-size cfg))) - (let [input (batch-events cfg (::events-ch session)) + (let [input (aa/batch (::events-ch session) + {:max-batch-size (:max-batch-size cfg) + :max-batch-age (inst-ms (:max-batch-age cfg))}) mcnt (mtx/create {:name "http_session_update_total" :help "A counter of session update batch events." @@ -149,36 +150,6 @@ :count result)) (recur)))))) -(defn- timeout-chan - [cfg] - (a/timeout (inst-ms (:max-batch-age cfg)))) - -(defn- batch-events - [cfg in] - (let [out (a/chan)] - (a/go-loop [tch (timeout-chan cfg) - buf #{}] - (let [[val port] (a/alts! [tch in])] - (cond - (identical? port tch) - (if (empty? buf) - (recur (timeout-chan cfg) buf) - (do - (a/>! out [:timeout buf]) - (recur (timeout-chan cfg) #{}))) - - (nil? val) - (a/close! out) - - (identical? port in) - (let [buf (conj buf val)] - (if (>= (count buf) (:max-batch-size cfg)) - (do - (a/>! out [:size buf]) - (recur (timeout-chan cfg) #{})) - (recur tch buf)))))) - out)) - (defn- update-sessions [{:keys [pool executor]} ids] (aa/with-thread executor diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj new file mode 100644 index 000000000..bbffcf781 --- /dev/null +++ b/backend/src/app/loggers/audit.clj @@ -0,0 +1,232 @@ +;; 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.loggers.audit + "Services related to the user activity (audit log)." + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.util.async :as aa] + [app.util.http :as http] + [app.util.logging :as l] + [app.util.time :as dt] + [app.util.transit :as t] + [app.worker :as wrk] + [clojure.core.async :as a] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [lambdaisland.uri :as u])) + +(defn clean-props + [{:keys [profile-id] :as event}] + (letfn [(clean-common [props] + (-> props + (dissoc :session-id) + (dissoc :password) + (dissoc :old-password) + (dissoc :token))) + + (clean-profile-id [props] + (cond-> props + (= profile-id (:profile-id props)) + (dissoc :profile-id))) + + (clean-complex-data [props] + (reduce-kv (fn [props k v] + (cond-> props + (or (string? v) + (uuid? v) + (boolean? v) + (number? v)) + (assoc k v) + + (keyword? v) + (assoc k (name v)))) + {} + props))] + (update event :props #(-> % clean-common clean-profile-id clean-complex-data)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Collector +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Defines a service that collects the audit/activity log using +;; internal database. Later this audit log can be transferred to +;; an external storage and data cleared. + +(declare persist-events) +(s/def ::enabled ::us/boolean) + +(defmethod ig/pre-init-spec ::collector [_] + (s/keys :req-un [::db/pool ::wrk/executor ::enabled])) + +(def event-xform + (comp + (filter :profile-id) + (map clean-props))) + +(defmethod ig/init-key ::collector + [_ {:keys [enabled] :as cfg}] + (when enabled + (l/info :msg "intializing audit collector") + (let [input (a/chan 1 event-xform) + buffer (aa/batch input {:max-batch-size 100 + :max-batch-age (* 5 1000) + :init []})] + (a/go-loop [] + (when-let [[type events] (a/row [event] + [(uuid/next) + (:name event) + (:type event) + (:profile-id event) + (db/tjson (:props event))])] + + (aa/with-thread executor + (db/with-atomic [conn pool] + (db/insert-multi! conn :audit-log + [:id :name :type :profile-id :props] + (sequence (map event->row) events)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Archive Task +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; This is a task responsible to send the accomulated events to an +;; external service for archival. + +(declare archive-events) + +(s/def ::uri ::us/string) +(s/def ::tokens fn?) + +(defmethod ig/pre-init-spec ::archive-task [_] + (s/keys :req-un [::db/pool ::tokens ::enabled] + :opt-un [::uri])) + +(defmethod ig/init-key ::archive-task + [_ {:keys [uri enabled] :as cfg}] + (fn [_] + (when (and enabled (not uri)) + (ex/raise :type :internal + :code :task-not-configured + :hint "archive task not configured, missing uri")) + (loop [] + (let [res (archive-events cfg)] + (when (= res :continue) + (aa/thread-sleep 200) + (recur)))))) + +(def sql:retrieve-batch-of-audit-log + "select * from audit_log + where archived_at is null + order by created_at asc + limit 100 + for update skip locked;") + +(defn archive-events + [{:keys [pool uri tokens] :as cfg}] + (letfn [(decode-row [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props) + (assoc :props (db/decode-transit-pgobject props)))) + + (row->event [{:keys [name type created-at profile-id props]}] + {:type type + :name name + :timestamp created-at + :profile-id profile-id + :props props}) + + (send [events] + (let [token (tokens :generate {:iss "authentication" + :iat (dt/now) + :uid uuid/zero}) + body (t/encode {:events events}) + headers {"content-type" "application/transit+json" + "origin" (cf/get :public-uri) + "cookie" (u/map->query-string {:auth-token token})} + params {:uri uri + :timeout 5000 + :method :post + :headers headers + :body body} + resp (http/send! params)] + (when (not= (:status resp) 204) + (ex/raise :type :internal + :code :unable-to-send-events + :hint "unable to send events" + :context resp)))) + + (mark-as-archived [conn rows] + (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" + (->> (map :id rows) + (into-array java.util.UUID) + (db/create-array conn "uuid"))]))] + + (db/with-atomic [conn pool] + (let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log]) + + xform (comp (map decode-row) + (map row->event)) + events (into [] xform rows)] + (l/debug :action "archive-events" :uri uri :events (count events)) + (if (empty? events) + :empty + (do + (send events) + (mark-as-archived conn rows) + :continue)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GC Task +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare clean-archived) + +(s/def ::max-age ::cf/audit-archive-gc-max-age) + +(defmethod ig/pre-init-spec ::archive-gc-task [_] + (s/keys :req-un [::db/pool ::enabled ::max-age])) + +(defmethod ig/init-key ::archive-gc-task + [_ cfg] + (fn [_] + (clean-archived cfg))) + +(def sql:clean-archived + "delete from audit_log + where archived_at is not null + and archived_at < now() - ?::interval") + +(defn- clean-archived + [{:keys [pool max-age]}] + (let [interval (db/interval max-age) + result (db/exec-one! pool [sql:clean-archived interval]) + result (:next.jdbc/update-count result)] + (l/debug :action "clean archived audit log" :removed result) + result)) diff --git a/backend/src/app/loggers/loki.clj b/backend/src/app/loggers/loki.clj index 607f06e3b..1bafb7c4f 100644 --- a/backend/src/app/loggers/loki.clj +++ b/backend/src/app/loggers/loki.clj @@ -31,16 +31,16 @@ [_ {:keys [receiver uri] :as cfg}] (when uri (l/info :msg "intializing loki reporter" :uri uri) - (let [output (a/chan (a/sliding-buffer 1024))] - (receiver :sub output) + (let [input (a/chan (a/sliding-buffer 1024))] + (receiver :sub input) (a/go-loop [] - (let [msg (a/otf [data] + (let [input-file (fs/create-tempfile :prefix "penpot") + output-file (fs/path (str input-file ".otf")) + _ (with-open [out (io/output-stream input-file)] + (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)))) + + + (otf->ttf [data] + (let [input-file (fs/create-tempfile :prefix "penpot") + output-file (fs/path (str input-file ".ttf")) + _ (with-open [out (io/output-stream input-file)] + (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)))) + + (woff->sfnt [data] + (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") + _ (with-open [out (io/output-stream input-file)] + (IOUtils/writeChunked ^bytes data ^OutputStream out) + (.flush ^OutputStream out)) + res (sh/sh "woff2sfnt" (str input-file) + :out-enc :bytes)] + (when (zero? (:exit res)) + (:out res)))) + + ;; Documented here: + ;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory + (get-sfnt-type [data] + (let [buff (bb/slice data 0 4) + type (bc/bytes->hex buff)] + (case type + "4f54544f" :otf + "00010000" :ttf + (ex/raise :type :internal + :code :unexpected-data + :hint "unexpected font data")))) + + (gen-if-nil [val factory] + (if (nil? val) + (factory) + val))] + + (let [current (into #{} (keys input))] + (cond + (contains? current "font/ttf") + (let [data (get input "font/ttf")] + (-> input + (update "font/otf" gen-if-nil #(ttf->otf data)) + (update "font/woff" gen-if-nil #(ttf-or-otf->woff data)) + (assoc "font/woff2" (ttf-or-otf->woff2 data)))) + + (contains? current "font/otf") + (let [data (get input "font/otf")] + (-> input + (update "font/woff" gen-if-nil #(ttf-or-otf->woff data)) + (assoc "font/ttf" (otf->ttf data)) + (assoc "font/woff2" (ttf-or-otf->woff2 data)))) + + (contains? current "font/woff") + (let [data (get input "font/woff") + sfnt (woff->sfnt data)] + (when-not sfnt + (ex/raise :type :validation + :code :invalid-woff-file + :hint "invalid woff file")) + (let [stype (get-sfnt-type sfnt)] + (cond-> input + true + (-> (assoc "font/woff" data) + (assoc "font/woff2" (ttf-or-otf->woff2 sfnt))) + + (= stype :otf) + (-> (assoc "font/otf" sfnt) + (assoc "font/ttf" (otf->ttf sfnt))) + + (= stype :ttf) + (-> (assoc "font/otf" (ttf->otf sfnt)) + (assoc "font/ttf" sfnt))))))))) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 18a8917c4..8e6350995 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -166,6 +166,15 @@ {:name "0052-del-legacy-user-and-team" :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")} + + {:name "0054-add-audit-log-table" + :fn (mg/resource "app/migrations/sql/0054-add-audit-log-table.sql")} + + {:name "0055-mod-file-media-object-table" + :fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")} ]) diff --git a/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql b/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql index 5427757cf..1787c5a70 100644 --- a/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql +++ b/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql @@ -1,4 +1,4 @@ -DROP TABLE task; +DROP TABLE IF EXISTS task; CREATE TABLE task ( id uuid DEFAULT uuid_generate_v4(), @@ -27,3 +27,11 @@ CREATE TABLE task_default partition OF task default; CREATE INDEX task__scheduled_at__queue__idx ON task (scheduled_at, queue) WHERE status = 'new' or status = 'retry'; + +ALTER TABLE task + ALTER COLUMN queue SET STORAGE external, + ALTER COLUMN name SET STORAGE external, + ALTER COLUMN props SET STORAGE external, + ALTER COLUMN status SET STORAGE external, + ALTER COLUMN error SET STORAGE external; + diff --git a/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql b/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql index b36c2a205..caad9b33b 100644 --- a/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql +++ b/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql @@ -1,4 +1,4 @@ -DROP TABLE scheduled_task; +DROP TABLE IF EXISTS scheduled_task; CREATE TABLE scheduled_task ( id text PRIMARY KEY, @@ -22,3 +22,7 @@ CREATE TABLE scheduled_task_history ( CREATE INDEX scheduled_task_history__task_id__idx ON scheduled_task_history(task_id); + +ALTER TABLE scheduled_task + ALTER COLUMN id SET STORAGE external, + ALTER COLUMN cron_expr SET STORAGE external; diff --git a/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql b/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql index 5cd38a5f4..6a9e7c8c4 100644 --- a/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql +++ b/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql @@ -27,17 +27,6 @@ ALTER TABLE comment_thread ALTER COLUMN participants SET STORAGE external, ALTER COLUMN page_name SET STORAGE external; -ALTER TABLE task - ALTER COLUMN queue SET STORAGE external, - ALTER COLUMN name SET STORAGE external, - ALTER COLUMN props SET STORAGE external, - ALTER COLUMN status SET STORAGE external, - ALTER COLUMN error SET STORAGE external; - -ALTER TABLE scheduled_task - ALTER COLUMN id SET STORAGE external, - ALTER COLUMN cron_expr SET STORAGE external; - ALTER TABLE http_session ALTER COLUMN id SET STORAGE external, ALTER COLUMN user_agent SET STORAGE external; diff --git a/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql b/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql new file mode 100644 index 000000000..e03d1da99 --- /dev/null +++ b/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql @@ -0,0 +1,43 @@ +CREATE TABLE team_font_variant ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + + team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE, + profile_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE, + + created_at timestamptz NOT NULL DEFAULT now(), + modified_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz NULL DEFAULT NULL, + + font_id uuid 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 DEFERRABLE, + ttf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE, + woff1_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE, + woff2_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE +); + +CREATE INDEX team_font_variant_team_id_font_id_idx + ON team_font_variant (team_id, font_id); + +CREATE INDEX team_font_variant_profile_id_idx + ON team_font_variant (profile_id); + +CREATE INDEX team_font_variant_otf_file_id_idx + ON team_font_variant (otf_file_id); + +CREATE INDEX team_font_variant_ttf_file_id_idx + ON team_font_variant (ttf_file_id); + +CREATE INDEX team_font_variant_woff1_file_id_idx + ON team_font_variant (woff1_file_id); + +CREATE INDEX team_font_variant_woff2_file_id_idx + ON team_font_variant (woff2_file_id); + +ALTER TABLE team_font_variant + ALTER COLUMN font_family SET STORAGE external, + ALTER COLUMN font_style SET STORAGE external; + diff --git a/backend/src/app/migrations/sql/0054-add-audit-log-table.sql b/backend/src/app/migrations/sql/0054-add-audit-log-table.sql new file mode 100644 index 000000000..b7097fde2 --- /dev/null +++ b/backend/src/app/migrations/sql/0054-add-audit-log-table.sql @@ -0,0 +1,25 @@ +CREATE TABLE audit_log ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + + name text NOT NULL, + type text NOT NULL, + + created_at timestamptz DEFAULT clock_timestamp() NOT NULL, + archived_at timestamptz NULL, + + profile_id uuid NOT NULL, + props jsonb, + + PRIMARY KEY (created_at, profile_id) +) PARTITION BY RANGE (created_at); + +ALTER TABLE audit_log + ALTER COLUMN name SET STORAGE external, + ALTER COLUMN type SET STORAGE external, + ALTER COLUMN props SET STORAGE external; + +CREATE INDEX audit_log_id_archived_at_idx ON audit_log (id, archived_at); + +CREATE TABLE audit_log_default (LIKE audit_log INCLUDING ALL); + +ALTER TABLE audit_log ATTACH PARTITION audit_log_default DEFAULT; diff --git a/backend/src/app/migrations/sql/0055-mod-file-media-object-table.sql b/backend/src/app/migrations/sql/0055-mod-file-media-object-table.sql new file mode 100644 index 000000000..7017cb8d9 --- /dev/null +++ b/backend/src/app/migrations/sql/0055-mod-file-media-object-table.sql @@ -0,0 +1,4 @@ +ALTER TABLE file_media_object + DROP CONSTRAINT file_media_object_thumbnail_id_fkey, + ADD CONSTRAINT file_media_object_thumbnail_id_fkey + FOREIGN KEY (thumbnail_id) REFERENCES storage_object (id) ON DELETE SET NULL; diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj index 8058c6577..f7a013f72 100644 --- a/backend/src/app/msgbus.clj +++ b/backend/src/app/msgbus.clj @@ -21,6 +21,7 @@ java.time.Duration io.lettuce.core.RedisClient io.lettuce.core.RedisURI + io.lettuce.core.api.StatefulConnection io.lettuce.core.api.StatefulRedisConnection io.lettuce.core.api.async.RedisAsyncCommands io.lettuce.core.codec.ByteArrayCodec @@ -130,6 +131,7 @@ ;; --- REDIS BACKEND IMPL +(declare impl-redis-open?) (declare impl-redis-pub) (declare impl-redis-sub) (declare impl-redis-unsub) @@ -162,7 +164,8 @@ (a/go-loop [] (when-let [val (a/> (sv/scan-ns 'app.rpc.queries.projects 'app.rpc.queries.files 'app.rpc.queries.teams @@ -120,6 +136,7 @@ 'app.rpc.queries.profile 'app.rpc.queries.recent-files 'app.rpc.queries.viewer + 'app.rpc.queries.fonts 'app.rpc.queries.svg) (map (partial process-method cfg)) (into {})))) @@ -132,7 +149,7 @@ :registry (get-in cfg [:metrics :registry]) :type :histogram :help "Timing of mutation services."}) - cfg (assoc cfg ::mobj mobj)] + cfg (assoc cfg ::mobj mobj ::type "mutation")] (->> (sv/scan-ns 'app.rpc.mutations.demo 'app.rpc.mutations.media 'app.rpc.mutations.profile @@ -143,6 +160,7 @@ 'app.rpc.mutations.teams 'app.rpc.mutations.management 'app.rpc.mutations.ldap + 'app.rpc.mutations.fonts 'app.rpc.mutations.verify-token) (map (partial process-method cfg)) (into {})))) @@ -150,9 +168,11 @@ (s/def ::storage some?) (s/def ::session map?) (s/def ::tokens fn?) +(s/def ::audit (s/nilable fn?)) (defmethod ig/pre-init-spec ::rpc [_] - (s/keys :req-un [::db/pool ::storage ::session ::tokens ::mtx/metrics ::rlm/rlimits])) + (s/keys :req-un [::storage ::session ::tokens ::audit + ::mtx/metrics ::rlm/rlimits ::db/pool])) (defmethod ig/init-key ::rpc [_ cfg] diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj index 2f0c583c2..a74f8b4f8 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/mutations/demo.clj @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.config :as cfg] [app.db :as db] + [app.loggers.audit :as audit] [app.rpc.mutations.profile :as profile] [app.setup.initial-data :as sid] [app.util.services :as sv] @@ -53,5 +54,6 @@ ::wrk/conn conn :profile-id id}) - {:email email - :password password}))) + (with-meta {:email email + :password password} + {::audit/profile-id id})))) diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index 493be817b..d6fb24c59 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -228,16 +228,10 @@ {:id file-id})) +;; --- MUTATION: update-file + ;; A generic, Changes based (granular) file update method. -(s/def ::changes - (s/coll-of map? :kind vector?)) - -(s/def ::session-id ::us/uuid) -(s/def ::revn ::us/integer) -(s/def ::update-file - (s/keys :req-un [::id ::session-id ::profile-id ::revn ::changes])) - ;; File changes that affect to the library, and must be notified ;; to all clients using it. (defn library-change? @@ -256,6 +250,31 @@ (declare send-notifications) (declare update-file) +(s/def ::changes + (s/coll-of map? :kind vector?)) + +(s/def ::hint-origin ::us/keyword) +(s/def ::hint-events + (s/every ::us/keyword :kind vector?)) + +(s/def ::change-with-metadata + (s/keys :req-un [::changes] + :opt-un [::hint-origin + ::hint-events])) + +(s/def ::changes-with-metadata + (s/every ::change-with-metadata :kind vector?)) + +(s/def ::session-id ::us/uuid) +(s/def ::revn ::us/integer) +(s/def ::update-file + (s/and + (s/keys :req-un [::id ::session-id ::profile-id ::revn] + :opt-un [::changes ::changes-with-metadata]) + (fn [o] + (or (contains? o :changes) + (contains? o :changes-with-metadata))))) + (sv/defmethod ::update-file [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] @@ -265,7 +284,7 @@ (assoc params :file file))))) (defn- update-file - [{:keys [conn] :as cfg} {:keys [file changes session-id profile-id] :as params}] + [{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}] (when (> (:revn params) (:revn file)) (ex/raise :type :validation @@ -274,15 +293,19 @@ :context {:incoming-revn (:revn params) :stored-revn (:revn file)})) - (let [file (-> file - (update :revn inc) - (update :data (fn [data] - (-> data - (blob/decode) - (assoc :id (:id file)) - (pmg/migrate-data) - (cp/process-changes changes) - (blob/encode)))))] + (let [changes (if changes-with-metadata + (mapcat :changes changes-with-metadata) + changes) + + file (-> file + (update :revn inc) + (update :data (fn [data] + (-> data + (blob/decode) + (assoc :id (:id file)) + (pmg/migrate-data) + (cp/process-changes changes) + (blob/encode)))))] ;; Insert change to the xlog (db/insert! conn :file-change {:id (uuid/next) @@ -300,7 +323,8 @@ :has-media-trimmed false} {:id (:id file)}) - (let [params (assoc params :file file)] + (let [params (-> params (assoc :file file + :changes changes))] ;; Send asynchronous notifications (send-notifications cfg params) diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj new file mode 100644 index 000000000..ca1d2263e --- /dev/null +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -0,0 +1,143 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.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])) + +(declare create-font-variant) + +(def valid-weight #{100 200 300 400 500 600 700 800 900 950}) +(def valid-style #{"normal" "italic"}) + +(s/def ::id ::us/uuid) +(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 ::us/uuid) +(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 FAMILY + +(s/def ::update-font + (s/keys :req-un [::profile-id ::team-id ::id ::name])) + +(def sql:update-font + "update team_font_variant + set font_family = ? + where team_id = ? + and font_id = ?") + +(sv/defmethod ::update-font + [{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (db/exec-one! conn [sql:update-font name team-id id]) + nil)) + +;; --- DELETE FONT + +(s/def ::delete-font + (s/keys :req-un [::profile-id ::team-id ::id])) + +(sv/defmethod ::delete-font + [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + + (let [items (db/query conn :team-font-variant + {:font-id id :team-id team-id} + {:for-update true})] + (doseq [item items] + ;; Schedule object deletion + (wrk/submit! {::wrk/task :delete-object + ::wrk/delay cf/deletion-delay + ::wrk/conn conn + :id (:id item) + :type :team-font-variant})) + + (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:font-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] :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)) diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj index 177d02964..cc38374b2 100644 --- a/backend/src/app/rpc/mutations/management.clj +++ b/backend/src/app/rpc/mutations/management.clj @@ -91,21 +91,21 @@ (def sql:retrieve-used-media-objects "select fmo.* from file_media_object as fmo - inner join storage_object as o on (fmo.media_id = o.id) + inner join storage_object as so on (fmo.media_id = so.id) where fmo.file_id = ? - and o.deleted_at is null") + and so.deleted_at is null") (defn duplicate-file - [conn {:keys [profile-id file index project-id name]} {:keys [reset-shared-flag] :as opts}] - (let [flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]) - fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]) + [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}] + (let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)])) + fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)])) ;; memo uniform creation/modification date now (dt/now) ignore (dt/plus now (dt/duration {:seconds 5})) ;; add to the index all file media objects. - index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds) + index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds) flibs-xf (comp (map #(remap-id % index :file-id)) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index 26dbac324..62241a48b 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -32,12 +32,15 @@ (s/def ::file-id ::us/uuid) (s/def ::team-id ::us/uuid) + ;; --- Create File Media object (upload) (declare create-file-media-object) (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 ::upload-file-media-object diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 7e719b6c2..71c2a0d90 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -6,13 +6,14 @@ (ns app.rpc.mutations.profile (:require - [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cfg] [app.db :as db] [app.emails :as eml] + [app.http.oauth :refer [extract-props]] + [app.loggers.audit :as audit] [app.media :as media] [app.rpc.mutations.projects :as projects] [app.rpc.mutations.teams :as teams] @@ -59,9 +60,10 @@ (ex/raise :type :restriction :code :registration-disabled)) - (when-not (email-domain-in-whitelist? (cfg/get :registration-domain-whitelist) (:email params)) - (ex/raise :type :validation - :code :email-domain-is-not-allowed)) + (when-let [domains (cfg/get :registration-domain-whitelist)] + (when-not (email-domain-in-whitelist? domains (:email params)) + (ex/raise :type :validation + :code :email-domain-is-not-allowed))) (when-not (:terms-privacy params) (ex/raise :type :validation @@ -101,7 +103,9 @@ resp {:invitation-token token}] (with-meta resp {:transform-response ((:create session) (:id profile)) - :before-complete (annotate-profile-register metrics profile)})) + :before-complete (annotate-profile-register metrics profile) + ::audit/props (:props profile) + ::audit/profile-id (:id profile)})) ;; If no token is provided, send a verification email (let [vtoken (tokens :generate @@ -129,17 +133,20 @@ :extra-data ptoken}) (with-meta profile - {:before-complete (annotate-profile-register metrics profile)}))))) + {:before-complete (annotate-profile-register metrics profile) + ::audit/props (:props profile) + ::audit/profile-id (:id profile)}))))) (defn email-domain-in-whitelist? - "Returns true if email's domain is in the given whitelist or if given - whitelist is an empty string." - [whitelist email] - (if (str/empty-or-nil? whitelist) + "Returns true if email's domain is in the given whitelist or if + given whitelist is an empty string." + [domains email] + (if (or (empty? domains) + (nil? domains)) true - (let [domains (str/split whitelist #",\s*") - domain (second (str/split email #"@" 2))] - (contains? (set domains) domain)))) + (let [[_ candidate] (-> (str/lower email) + (str/split #"@" 2))] + (contains? domains candidate)))) (def ^:private sql:profile-existence "select exists (select * from profile @@ -174,11 +181,12 @@ (defn create-profile "Create the profile entry on the database with limited input filling all the other fields with defaults." - [conn {:keys [id fullname email password props is-active is-muted is-demo opts] - :or {is-active false is-muted false is-demo false}}] + [conn {:keys [id fullname email password is-active is-muted is-demo opts] + :or {is-active false is-muted false is-demo false} + :as params}] (let [id (or id (uuid/next)) is-active (if is-demo true is-active) - props (db/tjson (or props {})) + props (-> params extract-props db/tjson) password (derive-password password) params {:id id :fullname fullname @@ -270,10 +278,12 @@ :member-email (:email profile)) token (tokens :generate claims)] (with-meta {:invitation-token token} - {:transform-response ((:create session) (:id profile))})) + {:transform-response ((:create session) (:id profile)) + ::audit/profile-id (:id profile)})) (with-meta profile - {:transform-response ((:create session) (:id profile))})))))) + {:transform-response ((:create session) (:id profile)) + ::audit/profile-id (:id profile)})))))) ;; --- Mutation: Logout @@ -298,35 +308,39 @@ [{:keys [pool metrics] :as cfg} params] (db/with-atomic [conn pool] (let [profile (-> (assoc cfg :conn conn) - (login-or-register params))] + (login-or-register params)) + props (merge + (select-keys profile [:backend :fullname :email]) + (:props profile))] (with-meta profile - {:before-complete (annotate-profile-register metrics profile)})))) + {:before-complete (annotate-profile-register metrics profile) + ::audit/name (if (::created profile) "register" "login") + ::audit/props props + ::audit/profile-id (:id profile)})))) (defn login-or-register - [{:keys [conn] :as cfg} {:keys [email backend] :as params}] - (letfn [(info->props [info] - (dissoc info :name :fullname :email :backend)) - - (info->lang [{:keys [locale] :as info}] + [{:keys [conn] :as cfg} {:keys [email] :as params}] + (letfn [(info->lang [{:keys [locale] :as info}] (when (and (string? locale) (not (str/blank? locale))) locale)) - (create-profile [conn {:keys [email] :as info}] - (db/insert! conn :profile - {:id (uuid/next) - :fullname (:fullname info) - :email (str/lower email) - :lang (info->lang info) - :auth-backend backend - :is-active true - :password "!" - :props (db/tjson (info->props info)) - :is-demo false})) + (create-profile [conn {:keys [fullname backend email props] :as info}] + (let [params {:id (uuid/next) + :fullname fullname + :email (str/lower email) + :lang (info->lang props) + :auth-backend backend + :is-active true + :password "!" + :props (db/tjson props) + :is-demo false}] + (-> (db/insert! conn :profile params) + (update :props db/decode-transit-pgobject)))) (update-profile [conn info profile] - (let [props (d/merge (:props profile) - (info->props info))] + (let [props (merge (:props profile) + (:props info))] (db/update! conn :profile {:props (db/tjson props) :modified-at (dt/now)} @@ -401,7 +415,9 @@ (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/keys :req-un [::profile-id ::file])) @@ -605,7 +621,7 @@ ;; Schedule a complete deletion of profile (wrk/submit! {::wrk/task :delete-profile - ::wrk/dalay cfg/deletion-delay + ::wrk/delay cfg/deletion-delay ::wrk/conn conn :profile-id profile-id}) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index c545d52b8..662d2dc35 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -249,7 +249,9 @@ (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/keys :req-un [::profile-id ::team-id ::file])) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 9f0a0b3ff..3fa857892 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -6,12 +6,13 @@ (ns app.rpc.queries.files (:require - [app.common.exceptions :as ex] [app.common.pages.migrations :as pmg] [app.common.spec :as us] + [app.common.uuid :as uuid] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as projects] + [app.rpc.queries.teams :as teams] [app.util.blob :as blob] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -97,7 +98,13 @@ ppr.is_owner = true or ppr.can_edit = true) ) - select distinct f.* + select distinct + f.id, + f.project_id, + f.created_at, + f.modified_at, + f.name, + f.is_shared from file as f inner join projects as pr on (f.project_id = pr.id) where f.name ilike ('%' || ? || '%') @@ -109,14 +116,15 @@ (sv/defmethod ::search-files [{:keys [pool] :as cfg} {:keys [profile-id team-id search-term] :as params}] - (let [rows (db/exec! pool [sql:search-files - profile-id team-id - profile-id team-id - search-term])] - (into [] decode-row-xf rows))) + (db/exec! pool [sql:search-files + profile-id team-id + profile-id team-id + search-term])) -;; --- Query: Project Files +;; --- Query: Files + +;; DEPRECATED: should be removed probably on 1.6.x (def ^:private sql:files "select f.* @@ -136,6 +144,29 @@ (into [] decode-row-xf (db/exec! conn [sql:files project-id])))) +;; --- Query: Project Files + +(def ^:private sql:project-files + "select f.id, + f.project_id, + f.created_at, + f.modified_at, + f.name, + f.is_shared + from file as f + where f.project_id = ? + and f.deleted_at is null + order by f.modified_at desc") + +(s/def ::project-files + (s/keys :req-un [::profile-id ::project-id])) + +(sv/defmethod ::project-files + [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] + (with-open [conn (db/open pool)] + (projects/check-read-permissions! conn profile-id project-id) + (db/exec! conn [sql:project-files project-id]))) + ;; --- Query: File (By ID) (defn retrieve-file @@ -154,17 +185,50 @@ (retrieve-file conn id))) (s/def ::page - (s/keys :req-un [::profile-id ::id ::file-id])) + (s/keys :req-un [::profile-id ::file-id])) + +(defn remove-thumbnails-frames + "Removes from data the children for frames that have a thumbnail set up" + [data] + (let [filter-shape? + (fn [objects [id shape]] + (let [frame-id (:frame-id shape)] + (or (= id uuid/zero) + (= frame-id uuid/zero) + (not (some? (get-in objects [frame-id :thumbnail])))))) + + ;; We need to remove from the attribute :shapes its childrens because + ;; they will not be sent in the data + remove-frame-children + (fn [[id shape]] + [id (cond-> shape + (some? (:thumbnail shape)) + (assoc :shapes []))]) + + update-objects + (fn [objects] + (into {} + (comp (map remove-frame-children) + (filter (partial filter-shape? objects))) + objects))] + + (update data :objects update-objects))) (sv/defmethod ::page - [{:keys [pool] :as cfg} {:keys [profile-id file-id id]}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id id strip-thumbnails]}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id]}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) - (let [file (retrieve-file conn file-id)] - (get-in file [:data :pages-index id])))) + (let [file (retrieve-file conn file-id) + page-id (get-in file [:data :pages 0])] + (cond-> (get-in file [:data :pages-index page-id]) + strip-thumbnails + (remove-thumbnails-frames))))) ;; --- Query: Shared Library Files +;; DEPRECATED: and will be removed on 1.6.x + (def ^:private sql:shared-files "select f.* from file as f @@ -183,11 +247,36 @@ (into [] decode-row-xf (db/exec! pool [sql:shared-files team-id]))) +;; --- Query: Shared Library Files + +(def ^:private sql:team-shared-files + "select f.id, + f.project_id, + f.created_at, + f.modified_at, + f.name, + f.is_shared + from file as f + inner join project as p on (p.id = f.project_id) + where f.is_shared = true + and f.deleted_at is null + and p.deleted_at is null + and p.team_id = ? + order by f.modified_at desc") + +(s/def ::team-shared-files + (s/keys :req-un [::profile-id ::team-id])) + +(sv/defmethod ::team-shared-files + [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] + (db/exec! pool [sql:team-shared-files team-id])) + + ;; --- Query: File Libraries used by a File (def ^:private sql:file-libraries "select fl.*, - ? as is_indirect, + flr.synced_at as synced_at from file as fl inner join file_library_rel as flr on (flr.library_file_id = fl.id) @@ -196,22 +285,13 @@ (defn retrieve-file-libraries [conn is-indirect file-id] - (let [direct-libraries - (into [] decode-row-xf (db/exec! conn [sql:file-libraries is-indirect file-id])) + (let [libraries (->> (db/exec! conn [sql:file-libraries file-id]) + (map #(assoc % :is-indirect is-indirect)) + (into #{} decode-row-xf))] + (reduce #(into %1 (retrieve-file-libraries conn true %2)) + libraries + (map :id libraries)))) - select-distinct - (fn [used-libraries new-libraries] - (remove (fn [new-library] - (some #(= (:id %) (:id new-library)) used-libraries)) - new-libraries))] - - (reduce (fn [used-libraries library] - (concat used-libraries - (select-distinct - used-libraries - (retrieve-file-libraries conn true (:id library))))) - direct-libraries - direct-libraries))) (s/def ::file-libraries (s/keys :req-un [::profile-id ::file-id])) @@ -222,31 +302,35 @@ (check-edition-permissions! conn profile-id file-id) (retrieve-file-libraries conn false file-id))) +;; --- QUERY: team-recent-files -;; --- Query: Single File Library +(def sql:team-recent-files + "with recent_files as ( + select f.id, + f.project_id, + f.created_at, + f.modified_at, + f.name, + f.is_shared, + row_number() over w as row_num + from file as f + join project as p on (p.id = f.project_id) + where p.team_id = ? + and p.deleted_at is null + and f.deleted_at is null + window w as (partition by f.project_id order by f.modified_at desc) + order by f.modified_at desc + ) + select * from recent_files where row_num <= 10;") -;; TODO: this looks like is duplicate of `::file` +(s/def ::team-recent-files + (s/keys :req-un [::profile-id ::team-id])) -(def ^:private sql:file-library - "select fl.* - from file as fl - where fl.id = ?") - -(defn retrieve-file-library - [conn file-id] - (let [rows (db/exec! conn [sql:file-library file-id])] - (when-not (seq rows) - (ex/raise :type :not-found)) - (first (sequence decode-row-xf rows)))) - -(s/def ::file-library - (s/keys :req-un [::profile-id ::file-id])) - -(sv/defmethod ::file-library - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id file-id) ;; TODO: this should check read permissions - (retrieve-file-library conn file-id))) +(sv/defmethod ::team-recent-files + [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] + (with-open [conn (db/open pool)] + (teams/check-read-permissions! conn profile-id team-id) + (db/exec! conn [sql:team-recent-files team-id]))) ;; --- Helpers diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj new file mode 100644 index 000000000..b25c780f5 --- /dev/null +++ b/backend/src/app/rpc/queries/fonts.clj @@ -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}))) + diff --git a/backend/src/app/rpc/queries/recent_files.clj b/backend/src/app/rpc/queries/recent_files.clj index 51c1bbe3f..e878d34e6 100644 --- a/backend/src/app/rpc/queries/recent_files.clj +++ b/backend/src/app/rpc/queries/recent_files.clj @@ -13,6 +13,8 @@ [app.util.services :as sv] [clojure.spec.alpha :as s])) +;; DEPRECATED: should be removed on 1.6.x + (def sql:recent-files "with recent_files as ( select f.*, row_number() over w as row_num diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index ec2c10a96..610e9e9ff 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -29,16 +29,26 @@ (initialize-instance-id! cfg) (retrieve-all cfg)))) +(def sql:upsert-secret-key + "insert into server_prop (id, preload, content) + values ('secret-key', true, ?::jsonb) + on conflict (id) do update set content = ?::jsonb") + +(def sql:insert-secret-key + "insert into server_prop (id, preload, content) + values ('secret-key', true, ?::jsonb) + on conflict (id) do nothing") + (defn- initialize-secret-key! - [{:keys [conn] :as cfg}] - (let [key (-> (bn/random-bytes 64) - (bc/bytes->b64u) - (bc/bytes->str))] - (db/insert! conn :server-prop - {:id "secret-key" - :preload true - :content (db/tjson key)} - {:on-conflict-do-nothing true}))) + [{:keys [conn key] :as cfg}] + (if key + (let [key (db/tjson key)] + (db/exec-one! conn [sql:upsert-secret-key key key])) + (let [key (-> (bn/random-bytes 64) + (bc/bytes->b64u) + (bc/bytes->str)) + key (db/tjson key)] + (db/exec-one! conn [sql:insert-secret-key key])))) (defn- initialize-instance-id! [{:keys [conn] :as cfg}] diff --git a/backend/src/app/setup/initial_data.clj b/backend/src/app/setup/initial_data.clj index 5514e7103..6532a54e5 100644 --- a/backend/src/app/setup/initial_data.clj +++ b/backend/src/app/setup/initial_data.clj @@ -8,7 +8,7 @@ (:refer-clojure :exclude [load]) (:require [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.rpc.mutations.management :refer [duplicate-file]] [app.rpc.mutations.projects :refer [create-project create-project-role]] @@ -36,7 +36,7 @@ ([system project-id {:keys [skey project-name] :or {project-name "Penpot Onboarding"}}] (db/with-atomic [conn (:app.db/pool system)] - (let [skey (or skey (cfg/get :initial-project-skey)) + (let [skey (or skey (cf/get :initial-project-skey)) files (db/exec! conn [sql:file project-id]) flibs (db/exec! conn [sql:file-library-rel project-id]) fmeds (db/exec! conn [sql:file-media-object project-id]) @@ -65,7 +65,7 @@ (defn load-initial-project! ([conn profile] (load-initial-project! conn profile nil)) ([conn profile opts] - (let [skey (or (:skey opts) (cfg/get :initial-project-skey)) + (let [skey (or (:skey opts) (cf/get :initial-project-skey)) data (retrieve-data conn skey)] (when data (let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} (:files data)) @@ -82,10 +82,16 @@ :role :owner}) (doseq [file (:files data)] - (let [params {:profile-id (:id profile) + (let [flibs (filterv #(= (:id file) (:file-id %)) (:flibs data)) + fmeds (filterv #(= (:id file) (:file-id %)) (:fmeds data)) + + params {:profile-id (:id profile) :project-id (:id project) :file file - :index index} + :index index + :flibs flibs + :fmeds fmeds} + opts {:reset-shared-flag false}] (duplicate-file conn params opts)))))))) diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index 27a44cbed..a28184df5 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -145,8 +145,8 @@ (make-output-stream [_ opts] (throw (UnsupportedOperationException. "not implemented"))) - clojure.lang.Counted - (count [_] size))) + clojure.lang.Counted + (count [_] size))) (defn content ([data] (content data nil)) diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index bcd6e4a48..8a8335bcd 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.spec :as us] [app.db :as db] + [app.storage :as sto] [app.util.logging :as l] [clojure.spec.alpha :as s] [integrant.core :as ig])) @@ -24,7 +25,8 @@ (fn [{:keys [props] :as task}] (us/verify ::props props) (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 ::id ::us/uuid) @@ -34,21 +36,32 @@ (fn [_ props] (:type props))) (defmethod handle-deletion :default - [_conn {:keys [type]}] + [_cfg {:keys [type]}] (l/warn :hint "no handler found" :type (d/name type))) (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"] (db/exec-one! conn [sql id]))) (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"] (db/exec-one! conn [sql id]))) (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"] (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))))) diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj index d1bdc6751..8b8bc3d28 100644 --- a/backend/src/app/tasks/file_media_gc.clj +++ b/backend/src/app/tasks/file_media_gc.clj @@ -101,7 +101,10 @@ :media-id (:media-id mobj) :thumbnail-id (:thumbnail-id mobj)) ;; 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)})) nil)) diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 564021c3e..44a232aeb 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -60,10 +60,9 @@ :uri (:uri cfg) :headers {"content-type" "application/json"} :body (json/encode-str data)})] - - (when (not= 200 (:status response)) + (when (> (:status response) 206) (ex/raise :type :internal - :code :invalid-response-from-google + :code :invalid-response :context {:status (:status response) :body (:body response)})))) diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index ecf7c84a4..bfa682ea6 100644 --- a/backend/src/app/tokens.clj +++ b/backend/src/app/tokens.clj @@ -51,11 +51,11 @@ claims)) (s/def ::secret-key ::us/string) -(s/def ::sprops +(s/def ::props (s/keys :req-un [::secret-key])) (defmethod ig/pre-init-spec ::tokens [_] - (s/keys :req-un [::sprops])) + (s/keys :req-un [::props])) (defn- generate-predefined [cfg {:keys [iss profile-id] :as params}] @@ -71,8 +71,8 @@ :hint "no predefined token"))) (defmethod ig/init-key ::tokens - [_ {:keys [sprops] :as cfg}] - (let [secret (derive-tokens-secret (:secret-key sprops)) + [_ {:keys [props] :as cfg}] + (let [secret (derive-tokens-secret (:secret-key props)) cfg (assoc cfg ::secret secret)] (fn [action params] (case action diff --git a/backend/src/app/util/async.clj b/backend/src/app/util/async.clj index fb17e6a7e..984cbb167 100644 --- a/backend/src/app/util/async.clj +++ b/backend/src/app/util/async.clj @@ -60,3 +60,42 @@ (if (= executor ::default) `(a/thread-call (^:once fn* [] (try ~@body (catch Exception e# e#)))) `(thread-call ~executor (^:once fn* [] ~@body)))) + +(defn batch + [in {:keys [max-batch-size + max-batch-age + init] + :or {max-batch-size 200 + max-batch-age (* 30 1000) + init #{}} + :as opts}] + (let [out (a/chan)] + (a/go-loop [tch (a/timeout max-batch-age) buf init] + (let [[val port] (a/alts! [tch in])] + (cond + (identical? port tch) + (if (empty? buf) + (recur (a/timeout max-batch-age) buf) + (do + (a/>! out [:timeout buf]) + (recur (a/timeout max-batch-age) init))) + + (nil? val) + (if (empty? buf) + (a/close! out) + (do + (a/offer! out [:timeout buf]) + (a/close! out))) + + (identical? port in) + (let [buf (conj buf val)] + (if (>= (count buf) max-batch-size) + (do + (a/>! out [:size buf]) + (recur (a/timeout max-batch-age) init)) + (recur tch buf)))))) + out)) + +(defn thread-sleep + [ms] + (Thread/sleep ms)) diff --git a/backend/src/app/util/logging.clj b/backend/src/app/util/logging.clj index 5aaa409a8..9a08e66b3 100644 --- a/backend/src/app/util/logging.clj +++ b/backend/src/app/util/logging.clj @@ -60,8 +60,8 @@ ^Object msg))) (defmacro log - [& {:keys [level cause ::logger ::async] :as props}] - (let [props (dissoc props :level :cause ::logger ::async) + [& {:keys [level cause ::logger ::async ::raw] :as props}] + (let [props (dissoc props :level :cause ::logger ::async ::raw) logger (or logger (str *ns*)) logger-sym (gensym "log") level-sym (gensym "log")] @@ -69,8 +69,12 @@ ~level-sym (get-level ~level)] (if (enabled? ~logger-sym ~level-sym) ~(if async - `(send-off logging-agent (fn [_#] (write-log! ~logger-sym ~level-sym ~cause (build-map-message ~props)))) - `(write-log! ~logger-sym ~level-sym ~cause (build-map-message ~props))))))) + `(send-off logging-agent + (fn [_#] + (let [message# (or ~raw (build-map-message ~props))] + (write-log! ~logger-sym ~level-sym ~cause message#)))) + `(let [message# (or ~raw (build-map-message ~props))] + (write-log! ~logger-sym ~level-sym ~cause message#))))))) (defmacro info [& params] diff --git a/backend/tests/app/tests/_files/font-1.otf b/backend/tests/app/tests/_files/font-1.otf new file mode 100644 index 000000000..9326ec784 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.otf differ diff --git a/backend/tests/app/tests/_files/font-1.ttf b/backend/tests/app/tests/_files/font-1.ttf new file mode 100644 index 000000000..cb2f33597 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.ttf differ diff --git a/backend/tests/app/tests/_files/font-1.woff b/backend/tests/app/tests/_files/font-1.woff new file mode 100644 index 000000000..9607e1e19 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.woff differ diff --git a/backend/tests/app/tests/_files/font-2.otf b/backend/tests/app/tests/_files/font-2.otf new file mode 100644 index 000000000..2fbddd00e Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.otf differ diff --git a/backend/tests/app/tests/_files/font-2.woff b/backend/tests/app/tests/_files/font-2.woff new file mode 100644 index 000000000..4edf9e60e Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.woff differ diff --git a/backend/tests/app/tests/test_common_geom_shapes.clj b/backend/tests/app/tests/test_common_geom_shapes.clj index b53d3ebc5..8019da994 100644 --- a/backend/tests/app/tests/test_common_geom_shapes.clj +++ b/backend/tests/app/tests/test_common_geom_shapes.clj @@ -52,7 +52,7 @@ (t/testing "Shape without modifiers should stay the same" (t/are [type] (let [shape-before (create-test-shape type) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (= shape-before shape-after)) :rect :path)) @@ -61,7 +61,7 @@ (t/are [type] (let [modifiers {:displacement (gmt/translate-matrix (gpt/point 10 -10))}] (let [shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/is (not= shape-before shape-after)) (t/is (close? (get-in shape-before [:selrect :x]) @@ -82,7 +82,7 @@ (t/are [type] (let [modifiers {:displacement (gmt/matrix)} shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/are [prop] (t/is (close? (get-in shape-before [:selrect prop]) (get-in shape-after [:selrect prop]))) @@ -95,7 +95,7 @@ :resize-vector (gpt/point 2 2) :resize-transform (gmt/matrix)} shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/is (not= shape-before shape-after)) (t/is (close? (get-in shape-before [:selrect :x]) @@ -117,7 +117,7 @@ :resize-vector (gpt/point 1 1) :resize-transform (gmt/matrix)} shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/are [prop] (t/is (close? (get-in shape-before [:selrect prop]) (get-in shape-after [:selrect prop]))) @@ -130,7 +130,7 @@ :resize-vector (gpt/point 0 0) :resize-transform (gmt/matrix)} shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/is (> (get-in shape-before [:selrect :width]) (get-in shape-after [:selrect :width]))) (t/is (> (get-in shape-after [:selrect :width]) 0)) @@ -144,7 +144,7 @@ (t/are [type] (let [modifiers {:rotation 30} shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/is (not= shape-before shape-after)) @@ -168,7 +168,7 @@ (t/are [type] (let [modifiers {:rotation 0} shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (t/are [prop] (t/is (close? (get-in shape-before [:selrect prop]) (get-in shape-after [:selrect prop]))) @@ -180,7 +180,7 @@ (let [modifiers {:displacement (gmt/matrix)} shape-before (-> (create-test-shape type {:modifiers modifiers}) (assoc :selrect selrect)) - shape-after (gsh/transform-shape shape-before)] + shape-after (gsh/transform-shape shape-before {:round-coords? false})] (= (:selrect shape-before) (:selrect shape-after))) :rect {:x 0 :y 0 :width ##Inf :height ##Inf} diff --git a/backend/tests/app/tests/test_common_pages_migrations.clj b/backend/tests/app/tests/test_common_pages_migrations.clj new file mode 100644 index 000000000..4e8adebd6 --- /dev/null +++ b/backend/tests/app/tests/test_common_pages_migrations.clj @@ -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.tests.test-common-pages-migrations + (:require + [clojure.test :as t] + [clojure.pprint :refer [pprint]] + [promesa.core :as p] + [mockery.core :refer [with-mock]] + [app.common.data :as d] + [app.common.pages :as cp] + [app.common.pages.migrations :as cpm] + [app.common.uuid :as uuid] + [app.tests.helpers :as th])) + +(t/deftest test-migration-8-1 + (let [page-id (uuid/custom 0 0) + objects [{:type :rect :id (uuid/custom 1 0)} + {:type :group + :id (uuid/custom 1 1) + :selrect {} + :shapes [(uuid/custom 1 2) (uuid/custom 1 0)]} + {:type :group + :id (uuid/custom 1 2) + :selrect {} + :shapes [(uuid/custom 1 3)]} + {:type :group + :id (uuid/custom 1 3) + :selrect {} + :shapes [(uuid/custom 1 4)]} + {:type :group + :id (uuid/custom 1 4) + :selrect {} + :shapes [(uuid/custom 1 5)]} + {:type :path :id (uuid/custom 1 5)}] + + data {:pages-index {page-id {:objects (d/index-by :id objects)}} + :components {} + :version 7} + + res (cpm/migrate-data data)] + + (pprint data) + (pprint res) + + (t/is (= (dissoc data :version) + (dissoc res :version))))) + +(t/deftest test-migration-8-2 + (let [page-id (uuid/custom 0 0) + objects [{:type :rect :id (uuid/custom 1 0)} + {:type :group + :id (uuid/custom 1 1) + :selrect {} + :shapes [(uuid/custom 1 2) (uuid/custom 1 0)]} + {:type :group + :id (uuid/custom 1 2) + :selrect {} + :shapes [(uuid/custom 1 3)]} + {:type :group + :id (uuid/custom 1 3) + :selrect {} + :shapes [(uuid/custom 1 4)]} + {:type :group + :id (uuid/custom 1 4) + :selrect {} + :shapes []} + {:type :path :id (uuid/custom 1 5)}] + + data {:pages-index {page-id {:objects (d/index-by :id objects)}} + :components {} + :version 7} + + expct (-> data + (update-in [:pages-index page-id :objects] dissoc + (uuid/custom 1 2) + (uuid/custom 1 3) + (uuid/custom 1 4)) + (update-in [:pages-index page-id :objects (uuid/custom 1 1) :shapes] + (fn [shapes] + (let [id (uuid/custom 1 2)] + (into [] (remove #(= id %)) shapes))))) + + res (cpm/migrate-data data)] + + (pprint res) + (pprint expct) + + (t/is (= (dissoc expct :version) + (dissoc res :version))) + )) diff --git a/backend/tests/app/tests/test_services_files.clj b/backend/tests/app/tests/test_services_files.clj index 68f34eacb..248dc86f2 100644 --- a/backend/tests/app/tests/test_services_files.clj +++ b/backend/tests/app/tests/test_services_files.clj @@ -52,7 +52,7 @@ (t/is (= (:id data) (:id result))) (t/is (= (:name data) (:name result)))))) - (t/testing "query files" + (t/testing "query files (deprecated)" (let [data {::th/type :files :project-id proj-id :profile-id (:id prof)} @@ -67,6 +67,20 @@ (t/is (= "new name" (get-in result [0 :name]))) (t/is (= 1 (count (get-in result [0 :data :pages]))))))) + (t/testing "query files" + (let [data {::th/type :project-files + :project-id proj-id + :profile-id (:id prof)} + out (th/query! data)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= 1 (count result))) + (t/is (= file-id (get-in result [0 :id]))) + (t/is (= "new name" (get-in result [0 :name])))))) + (t/testing "query single file without users" (let [data {::th/type :file :profile-id (:id prof) diff --git a/backend/tests/app/tests/test_services_fonts.clj b/backend/tests/app/tests/test_services_fonts.clj new file mode 100644 index 000000000..86836b71e --- /dev/null +++ b/backend/tests/app/tests/test_services_fonts.clj @@ -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.tests.test-services-fonts + (:require + [app.common.uuid :as uuid] + [app.db :as db] + [app.http :as http] + [app.storage :as sto] + [app.tests.helpers :as th] + [clojure.java.io :as io] + [clojure.test :as t] + [datoteka.core :as fs])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest ttf-font-upload-1 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + font-id (uuid/custom 10 1) + + ttfdata (-> (io/resource "app/tests/_files/font-1.ttf") + (fs/slurp-bytes)) + + params {::th/type :create-font-variant + :profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/ttf" ttfdata}} + out (th/mutation! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (uuid? (:ttf-file-id result))) + (t/is (uuid? (:otf-file-id result))) + (t/is (uuid? (:woff1-file-id result))) + (t/is (uuid? (:woff2-file-id result))) + (t/are [k] (= (get params k) + (get result k)) + :team-id + :font-id + :font-family + :font-weight + :font-style)))) + +(t/deftest ttf-font-upload-2 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + font-id (uuid/custom 10 1) + + data (-> (io/resource "app/tests/_files/font-1.woff") + (fs/slurp-bytes)) + + params {::th/type :create-font-variant + :profile-id (:id prof) + :team-id team-id + :font-id font-id + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data}} + out (th/mutation! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (uuid? (:ttf-file-id result))) + (t/is (uuid? (:otf-file-id result))) + (t/is (uuid? (:woff1-file-id result))) + (t/is (uuid? (:woff2-file-id result))) + (t/are [k] (= (get params k) + (get result k)) + :team-id + :font-id + :font-family + :font-weight + :font-style)))) + + + + + diff --git a/backend/tests/app/tests/test_services_profile.clj b/backend/tests/app/tests/test_services_profile.clj index 7ac20c849..c3daf5c14 100644 --- a/backend/tests/app/tests/test_services_profile.clj +++ b/backend/tests/app/tests/test_services_profile.clj @@ -179,10 +179,10 @@ )) (t/deftest registration-domain-whitelist - (let [whitelist "gmail.com, hey.com, ya.ru"] + (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}] (t/testing "allowed email domain" (t/is (true? (profile/email-domain-in-whitelist? whitelist "username@ya.ru"))) - (t/is (true? (profile/email-domain-in-whitelist? "" "username@somedomain.com")))) + (t/is (true? (profile/email-domain-in-whitelist? #{} "username@somedomain.com")))) (t/testing "not allowed email domain" (t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc index a46b306f2..649e29097 100644 --- a/common/app/common/data.cljc +++ b/common/app/common/data.cljc @@ -12,6 +12,7 @@ (:require [linked.set :as lks] [app.common.math :as mth] + [clojure.set :as set] #?(:clj [cljs.analyzer.api :as aapi]) #?(:cljs [cljs.reader :as r] :clj [clojure.edn :as r]) @@ -252,15 +253,22 @@ (map (fn [x] (f x) x) coll))) (defn merge - "A faster merge." [& maps] - (loop [res (transient (or (first maps) {})) - maps (next maps)] - (if (nil? maps) - (persistent! res) - (recur (reduce-kv assoc! res (first maps)) - (next maps))))) + (reduce conj (or (first maps) {}) (rest 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 @@ -448,3 +456,50 @@ kw (if (keyword? kw) (name kw) kw)] (keyword (str prefix kw)))) + +(defn tap + "Simpilar to the tap in rxjs but for plain collections" + [f coll] + (f coll) + coll) + +(defn map-diff + "Given two maps returns the diff of its attributes in a map where + the keys will be the attributes that change and the values the previous + and current value. For attributes which value is a map this will be recursive. + + For example: + (map-diff {:a 1 :b 2 :c { :foo 1 :var 2} + {:a 2 :c { :foo 10 } :d 10) + + => { :a [1 2] + :b [2 nil] + :c { :foo [1 10] + :var [2 nil]} + :d [nil 10] } + + If both maps are identical the result will be an empty map + " + [m1 m2] + + (let [m1ks (keys m1) + m2ks (keys m2) + keys (set/union m1ks m2ks) + + diff-attr + (fn [diff key] + + (let [v1 (get m1 key) + v2 (get m2 key)] + (cond + (= v1 v2) + diff + + (and (map? v1) (map? v2)) + (assoc diff key (map-diff v1 v2)) + + :else + (assoc diff key [(get m1 key) (get m2 key)]))))] + + (->> keys + (reduce diff-attr {})))) diff --git a/common/app/common/exceptions.cljc b/common/app/common/exceptions.cljc index 4fe202efd..5818578a9 100644 --- a/common/app/common/exceptions.cljc +++ b/common/app/common/exceptions.cljc @@ -36,7 +36,7 @@ (defn try* [f on-error] - (try (f) (catch #?(:clj Exception :cljs :default) e (on-error e)))) + (try (f) (catch #?(:clj Throwable :cljs :default) e (on-error e)))) ;; http://clj-me.cgrand.net/2013/09/11/macros-closures-and-unexpected-object-retention/ ;; Explains the use of ^:once metadata diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index a93c20ad3..db0d51e29 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -162,7 +162,7 @@ :points points)))) (defn rotation-modifiers - [center shape angle] + [shape center angle] (let [displacement (let [shape-center (gco/center-shape shape)] (-> (gmt/matrix) (gmt/rotate angle center) diff --git a/common/app/common/geom/shapes/intersect.cljc b/common/app/common/geom/shapes/intersect.cljc index 0b6fbcd6f..2b55cb339 100644 --- a/common/app/common/geom/shapes/intersect.cljc +++ b/common/app/common/geom/shapes/intersect.cljc @@ -174,9 +174,17 @@ "Checks if the given rect overlaps with the path in any point" [shape rect] - (let [rect-points (gpr/rect->points rect) + (let [;; If paths are too complex the intersection is too expensive + ;; we fallback to check its bounding box otherwise the performance penalty + ;; is too big + ;; TODO: Look for ways to optimize this operation + simple? (> (count (:content shape)) 100) + + rect-points (gpr/rect->points rect) rect-lines (points->lines rect-points) - path-lines (gpp/path->lines shape) + path-lines (if simple? + (points->lines (:points shape)) + (gpp/path->lines shape)) start-point (-> shape :content (first) :params (gpt/point))] (or (is-point-inside-nonzero? (first rect-points) path-lines) diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index cf9845741..0c7b8183d 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -6,13 +6,15 @@ (ns app.common.geom.shapes.transforms (:require + [app.common.attrs :as attrs] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.path :as gpa] [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] - [app.common.data :as d])) + [app.common.data :as d] + [app.common.text :as txt])) ;; --- Relative Movement @@ -264,7 +266,7 @@ (defn apply-transform "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" - [shape transform] + [shape transform round-coords?] ;; (let [points (-> shape :points (transform-points transform)) center (gco/center-points points) @@ -288,6 +290,13 @@ [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape)) + rect-shape (cond-> rect-shape + round-coords? + (-> (update :x mth/round) + (update :y mth/round) + (update :width mth/round) + (update :height mth/round))) + shape (cond (= :path (:type shape)) (-> shape @@ -295,11 +304,7 @@ :else (-> shape - (merge rect-shape) - (update :x #(mth/precision % 0)) - (update :y #(mth/precision % 0)) - (update :width #(mth/precision % 0)) - (update :height #(mth/precision % 0))))] + (merge rect-shape)))] (as-> shape $ (update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix)) (update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix)))) @@ -328,17 +333,40 @@ (dissoc :modifiers)))) shape))) -(defn transform-shape [shape] - (let [shape (apply-displacement shape) - center (gco/center-shape shape) - modifiers (:modifiers shape)] - (if (and modifiers center) - (let [transform (modifiers->transform center modifiers)] - (-> shape - (set-flip modifiers) - (apply-transform transform) - (dissoc :modifiers))) - shape))) +(defn apply-text-resize + [shape orig-shape modifiers] + (if (and (= (:type shape) :text) + (:resize-scale-text modifiers)) + (let [merge-attrs (fn [attrs] + (let [font-size (-> (get attrs :font-size 14) + (d/parse-double) + (* (-> modifiers :resize-vector :x)) + (str) + )] + (attrs/merge attrs {:font-size font-size})))] + (update shape :content #(txt/transform-nodes + txt/is-text-node? + merge-attrs + %))) + shape)) + +(defn transform-shape + ([shape] + (transform-shape shape nil)) + + ([shape {:keys [round-coords?] + :or {round-coords? true}}] + (let [shape (apply-displacement shape) + center (gco/center-shape shape) + modifiers (:modifiers shape)] + (if (and modifiers center) + (let [transform (modifiers->transform center modifiers)] + (-> shape + (set-flip modifiers) + (apply-transform transform round-coords?) + (apply-text-resize shape modifiers) + (dissoc :modifiers))) + shape)))) (defn update-group-viewbox "Updates the viewbox for groups imported from SVG's" @@ -387,5 +415,5 @@ ;; need to remove the flip flags (assoc :flip-x false) (assoc :flip-y false) - (apply-transform (gmt/matrix))))) + (apply-transform (gmt/matrix) true)))) diff --git a/common/app/common/media.cljc b/common/app/common/media.cljc index df3a556ca..cbf1fb826 100644 --- a/common/app/common/media.cljc +++ b/common/app/common/media.cljc @@ -9,10 +9,10 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str])) -(def valid-media-types - #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"}) - -(def str-media-types (str/join "," valid-media-types)) +(def valid-font-types #{"font/ttf" "font/woff", "font/otf"}) +(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-font-types (str/join "," valid-font-types)) (defn format->extension [format] @@ -65,3 +65,38 @@ ::modified-at ::uri])) + +(defn parse-font-weight + [variant] + (cond + (re-seq #"(?i)(?:hairline|thin)" variant) 100 + (re-seq #"(?i)(?:extra\s*light|ultra\s*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\s*bold|demi\s*bold)" variant) 600 + (re-seq #"(?i)(?:extra\s*bold|ultra\s*bold)" variant) 800 + (re-seq #"(?i)(?:bold)" variant) 700 + (re-seq #"(?i)(?:extra\s*black|ultra\s*black)" variant) 950 + (re-seq #"(?i)(?:black|heavy)" variant) 900 + :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")) diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index 2776e8168..4ef70baba 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -11,6 +11,7 @@ [app.common.pages.changes :as changes] [app.common.pages.common :as common] [app.common.pages.helpers :as helpers] + [app.common.pages.indices :as indices] [app.common.pages.init :as init] [app.common.pages.spec :as spec] [clojure.spec.alpha :as s])) @@ -42,7 +43,6 @@ (d/export helpers/is-shape-grouped) (d/export helpers/get-parent) (d/export helpers/get-parents) -(d/export helpers/generate-child-parent-index) (d/export helpers/clean-loops) (d/export helpers/calculate-invalid-targets) (d/export helpers/valid-frame-target) @@ -60,12 +60,18 @@ (d/export helpers/get-base-shape) (d/export helpers/is-parent?) (d/export helpers/get-index-in-parent) -(d/export helpers/calculate-z-index) -(d/export helpers/generate-child-all-parents-index) (d/export helpers/parse-path-name) (d/export helpers/merge-path-item) (d/export helpers/compact-path) (d/export helpers/compact-name) +(d/export helpers/merge-modifiers) + +;; Indices +(d/export indices/calculate-z-index) +(d/export indices/update-z-index) +(d/export indices/generate-child-all-parents-index) +(d/export indices/generate-child-parent-index) +(d/export indices/create-mask-index) ;; Process changes (d/export changes/process-changes) diff --git a/common/app/common/pages/common.cljc b/common/app/common/pages/common.cljc index db1e9341c..9a39105bf 100644 --- a/common/app/common/pages/common.cljc +++ b/common/app/common/pages/common.cljc @@ -8,7 +8,7 @@ (:require [app.common.uuid :as uuid])) -(def file-version 6) +(def file-version 8) (def default-color "#b1b2b5") ;; $color-gray-20 (def root uuid/zero) diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc index 39597788d..87299b2a3 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/app/common/pages/helpers.cljc @@ -10,6 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.spec :as us] [app.common.uuid :as uuid] + [clojure.set :as set] [cuerdas.core :as str])) (defn walk-pages @@ -160,27 +161,6 @@ (when parent-id (lazy-seq (cons parent-id (get-parents parent-id objects)))))) -(defn generate-child-parent-index - [objects] - (reduce-kv - (fn [index id obj] - (assoc index id (:parent-id obj))) - {} objects)) - -(defn generate-child-all-parents-index - "Creates an index where the key is the shape id and the value is a set - with all the parents" - ([objects] - (generate-child-all-parents-index objects (vals objects))) - - ([objects shapes] - (let [shape->parents - (fn [shape] - (->> (get-parents (:id shape) objects) - (into [])))] - (->> shapes - (map #(vector (:id %) (shape->parents %))) - (into {}))))) (defn clean-loops "Clean a list of ids from circular references." @@ -347,40 +327,7 @@ (reduce red-fn cur-idx (reverse (:shapes object)))))] (into {} (rec-index '() uuid/zero)))) -(defn calculate-z-index - "Given a collection of shapes calculates their z-index. Greater index - means is displayed over other shapes with less index." - [objects] - (let [is-frame? (fn [id] (= :frame (get-in objects [id :type]))) - root-children (get-in objects [uuid/zero :shapes]) - num-frames (->> root-children (filter is-frame?) count)] - (when (seq root-children) - (loop [current (peek root-children) - pending (pop root-children) - current-idx (+ (count objects) num-frames -1) - z-index {}] - - (let [children (->> (get-in objects [current :shapes])) - children (cond - (and (is-frame? current) (contains? z-index current)) - [] - - (and (is-frame? current) - (not (contains? z-index current))) - (into [current] children) - - :else - children) - pending (into (vec pending) children)] - (if (empty? pending) - (assoc z-index current current-idx) - - (let [] - (recur (peek pending) - (pop pending) - (dec current-idx) - (assoc z-index current current-idx))))))))) (defn expand-region-selection "Given a selection selects all the shapes between the first and last in @@ -511,3 +458,12 @@ (let [path-split (split-path path)] (merge-path-item (first path-split) name))) +(defn merge-modifiers + [objects modifiers] + + (let [set-modifier + (fn [objects [id modifiers]] + (-> objects + (d/update-when id merge modifiers)))] + (->> modifiers + (reduce set-modifier objects)))) diff --git a/common/app/common/pages/indices.cljc b/common/app/common/pages/indices.cljc new file mode 100644 index 000000000..fa3993799 --- /dev/null +++ b/common/app/common/pages/indices.cljc @@ -0,0 +1,110 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.pages.indices + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as helpers] + [app.common.uuid :as uuid] + [clojure.set :as set])) + +(defn calculate-frame-z-index [z-index frame-id objects] + (let [is-frame? (fn [id] (= :frame (get-in objects [id :type]))) + frame-shapes (->> objects (vals) (filterv #(= (:frame-id %) frame-id))) + children (or (get-in objects [frame-id :shapes]) [])] + + (if (empty? children) + z-index + + (loop [current (peek children) + pending (pop children) + current-idx (count frame-shapes) + z-index z-index] + + (let [children (get-in objects [current :shapes]) + is-frame? (is-frame? current) + pending (if (not is-frame?) + (d/concat pending children) + pending)] + + (if (empty? pending) + (-> z-index + (assoc current current-idx)) + + (recur (peek pending) + (pop pending) + (dec current-idx) + (assoc z-index current current-idx)))))))) + +;; The z-index is really calculated per-frame. Every frame will have its own +;; internal z-index. To calculate the "final" z-index we add the shape z-index with +;; the z-index of its frame. This way we can update the z-index per frame without +;; the need of recalculate all the frames +(defn calculate-z-index + "Given a collection of shapes calculates their z-index. Greater index + means is displayed over other shapes with less index." + [objects] + + (let [frames (helpers/select-frames objects) + z-index (calculate-frame-z-index {} uuid/zero objects)] + (->> frames + (map :id) + (reduce #(calculate-frame-z-index %1 %2 objects) z-index)))) + +(defn update-z-index + "Updates the z-index given a set of ids to change and the old and new objects + representations" + [z-index changed-ids old-objects new-objects] + + (let [old-frames (into #{} (map #(get-in old-objects [% :frame-id])) changed-ids) + new-frames (into #{} (map #(get-in new-objects [% :frame-id])) changed-ids) + + changed-frames (set/union old-frames new-frames) + + frames (->> (helpers/select-frames new-objects) + (map :id) + (filter #(contains? changed-frames %))) + + z-index (calculate-frame-z-index z-index uuid/zero new-objects)] + + (->> frames + (reduce #(calculate-frame-z-index %1 %2 new-objects) z-index)))) + +(defn generate-child-parent-index + [objects] + (reduce-kv + (fn [index id obj] + (assoc index id (:parent-id obj))) + {} objects)) + +(defn generate-child-all-parents-index + "Creates an index where the key is the shape id and the value is a set + with all the parents" + ([objects] + (generate-child-all-parents-index objects (vals objects))) + + ([objects shapes] + (let [shape->parents + (fn [shape] + (->> (helpers/get-parents (:id shape) objects) + (into [])))] + (->> shapes + (map #(vector (:id %) (shape->parents %))) + (into {}))))) + +(defn create-mask-index + "Retrieves the mask information for an object" + [objects parents-index] + (let [retrieve-masks + (fn [id parents] + (->> parents + (map #(get objects %)) + (filter #(:masked-group? %)) + ;; Retrieve the masking element + (mapv #(get objects (->> % :shapes first)))))] + (->> parents-index + (d/mapm retrieve-masks)))) diff --git a/common/app/common/pages/init.cljc b/common/app/common/pages/init.cljc index 79e0b50de..5ca2b85f1 100644 --- a/common/app/common/pages/init.cljc +++ b/common/app/common/pages/init.cljc @@ -63,8 +63,6 @@ {:type :path :name "Path" - :fill-color "#000000" - :fill-opacity 0 :stroke-style :solid :stroke-alignment :center :stroke-width 2 diff --git a/common/app/common/pages/migrations.cljc b/common/app/common/pages/migrations.cljc index 588da5d95..8e93da804 100644 --- a/common/app/common/pages/migrations.cljc +++ b/common/app/common/pages/migrations.cljc @@ -163,3 +163,62 @@ (-> data (update :components #(d/mapm update-container %)) (update :pages-index #(d/mapm update-container %))))) + + +;; Remove interactions pointing to deleted frames +(defmethod migrate 7 + [data] + (letfn [(update-object [page _ object] + (d/update-when object :interactions + (fn [interactions] + (filterv #(get-in page [:objects (:destination %)]) + interactions)))) + + (update-page [_ page] + (update page :objects #(d/mapm (partial update-object page) %)))] + + (update data :pages-index #(d/mapm update-page %)))) + + +;; Remove groups without any shape, both in pages and components + +(defmethod migrate 8 + [data] + (letfn [(clean-parents [obj deleted?] + (d/update-when obj :shapes + (fn [shapes] + (into [] (remove deleted?) shapes)))) + + (obj-is-empty? [obj] + (and (= (:type obj) :group) + (or (empty? (:shapes obj)) + (nil? (:selrect obj))))) + + (clean-objects [objects] + (loop [entries (seq objects) + deleted #{} + result objects] + (let [[id obj :as entry] (first entries)] + (if entry + (if (obj-is-empty? obj) + (recur (rest entries) + (conj deleted id) + (dissoc result id)) + (recur (rest entries) + deleted + result)) + [(count deleted) + (d/mapm #(clean-parents %2 deleted) result)])))) + + (clean-container [_ container] + (loop [n 0 + objects (:objects container)] + (let [[deleted objects] (clean-objects objects)] + (if (and (pos? deleted) (< n 1000)) + (recur (inc n) objects) + (assoc container :objects objects)))))] + + (-> data + (update :pages-index #(d/mapm clean-container %)) + (d/update-when :components #(d/mapm clean-container %))))) + diff --git a/common/app/common/pages/spec.cljc b/common/app/common/pages/spec.cljc index ffea43331..24c523fd0 100644 --- a/common/app/common/pages/spec.cljc +++ b/common/app/common/pages/spec.cljc @@ -90,6 +90,7 @@ ;;; COLORS (s/def :internal.color/name ::string) +(s/def :internal.color/path (s/nilable ::string)) (s/def :internal.color/value (s/nilable ::string)) (s/def :internal.color/color (s/nilable ::string)) (s/def :internal.color/opacity (s/nilable ::safe-number)) @@ -98,13 +99,13 @@ (s/def ::color (s/keys :opt-un [::id :internal.color/name + :internal.color/path :internal.color/value :internal.color/color :internal.color/opacity :internal.color/gradient])) - ;;; SHADOW EFFECT (s/def :internal.shadow/id uuid?) @@ -380,6 +381,7 @@ (s/def :internal.typography/id ::id) (s/def :internal.typography/name ::string) +(s/def :internal.typography/path (s/nilable ::string)) (s/def :internal.typography/font-id ::string) (s/def :internal.typography/font-family ::string) (s/def :internal.typography/font-variant-id ::string) @@ -401,7 +403,8 @@ :internal.typography/font-style :internal.typography/line-height :internal.typography/letter-spacing - :internal.typography/text-transform])) + :internal.typography/text-transform] + :opt-un [:internal.typography/path])) (s/def :internal.file/pages (s/coll-of ::uuid :kind vector?)) diff --git a/common/app/common/spec.cljc b/common/app/common/spec.cljc index eabce91ff..61c651136 100644 --- a/common/app/common/spec.cljc +++ b/common/app/common/spec.cljc @@ -6,7 +6,7 @@ (ns app.common.spec "Data manipulation and query helper functions." - (:refer-clojure :exclude [assert]) + (:refer-clojure :exclude [assert bytes?]) #?(:cljs (:require-macros [app.common.spec :refer [assert]])) (:require #?(:clj [clojure.spec.alpha :as s] @@ -108,6 +108,20 @@ (s/def ::point gpt/point?) (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 #(and (int? %) @@ -123,29 +137,34 @@ ;; --- SPEC: email +(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") -(let [re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+" - cfn (fn [v] - (if (string? v) - (if-let [matches (re-seq re v)] - (first matches) - (do ::s/invalid)) - ::s/invalid))] - (s/def ::email (s/conformer cfn str))) - +(s/def ::email + (s/conformer + (fn [v] + (if (string? v) + (if-let [matches (re-seq email-re v)] + (first matches) + (do ::s/invalid)) + ::s/invalid)) + str)) ;; --- SPEC: set-of-str -(letfn [(conformer [s] - (cond - (string? s) (into #{} (str/split s #"\s*,\s*")) - (set? s) (if (every? string? s) - s - ::s/invalid) - :else ::s/invalid)) - (unformer [s] - (str/join "," s))] - (s/def ::set-of-str (s/conformer conformer unformer))) +(s/def ::set-of-str + (s/conformer + (fn [s] + (let [xform (comp + (filter string?) + (remove str/empty?) + (remove str/blank?))] + (cond + (string? s) (->> (str/split s #"\s*,\s*") + (into #{} xform)) + (set? s) (into #{} xform s) + :else ::s/invalid))) + (fn [s] + (str/join "," s)))) ;; --- Macros diff --git a/common/app/common/uuid_impl.js b/common/app/common/uuid_impl.js index d276ce516..791dd58ea 100644 --- a/common/app/common/uuid_impl.js +++ b/common/app/common/uuid_impl.js @@ -12,17 +12,13 @@ goog.provide("app.common.uuid_impl"); goog.scope(function() { const core = cljs.core; + const global = goog.global; const self = app.common.uuid_impl; const fill = (() => { - if (typeof window === "object" && typeof window.crypto !== "undefined") { + if (typeof global.crypto !== "undefined") { return (buf) => { - window.crypto.getRandomValues(buf); - return buf; - }; - } else if (typeof self === "object" && typeof self.crypto !== "undefined") { - return (buf) => { - self.crypto.getRandomValues(buf); + global.crypto.getRandomValues(buf); return buf; }; } else if (typeof require === "function") { @@ -34,7 +30,7 @@ goog.scope(function() { }; } else { // FALLBACK - console.warn("No high quality RNG available, switching back to Math.random."); + console.warn("No SRNG available, switching back to Math.random."); return (buf) => { for (let i = 0, r; i < buf.length; i++) { diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 9e90ed04d..f7e2eebdc 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -6,7 +6,7 @@ ARG DEBIAN_FRONTEND=noninteractive ENV NODE_VERSION=v14.16.1 \ CLOJURE_VERSION=1.10.3.822 \ CLJKONDO_VERSION=2021.04.23 \ - BABASHKA_VERSION=0.3.5 \ + BABASHKA_VERSION=0.4.0 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index 9046c6dd8..b34f56b1c 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -38,6 +38,7 @@ services: environment: - EXTERNAL_UID=${CURRENT_USER_ID} + - PENPOT_SECRET_KEY=super-secret-devenv-key # STMP setup - PENPOT_SMTP_ENABLED=true - PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index eefd05cd0..ee7e37bb0 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -46,7 +46,7 @@ http { listen 3449 default_server; server_name _; - client_max_body_size 5M; + client_max_body_size 20M; charset utf-8; proxy_http_version 1.1; diff --git a/docker/images/files/config.js b/docker/images/files/config.js index aac4fb709..8af727193 100644 --- a/docker/images/files/config.js +++ b/docker/images/files/config.js @@ -9,3 +9,4 @@ //var penpotOIDCClientID = ""; //var penpotLoginWithLDAP = ; //var penpotRegistrationEnabled = ; +//var penpotAnalyticsEnabled = ; diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index b341649a7..51a118c5a 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -97,6 +97,14 @@ update_registration_enabled() { fi } +update_registration_enabled() { + if [ -n "$PENPOT_ANALYTICS_ENABLED" ]; then + sed -i \ + -e "s|^//var penpotAnalyticsEnabled = .*;|var penpotAnalyticsEnabled = $PENPOT_ANALYTICS_ENABLED;|g" \ + "$1" + fi +} + update_public_uri /var/www/app/js/config.js update_demo_warning /var/www/app/js/config.js update_allow_demo_users /var/www/app/js/config.js @@ -106,5 +114,6 @@ update_github_client_id /var/www/app/js/config.js update_oidc_client_id /var/www/app/js/config.js update_login_with_ldap /var/www/app/js/config.js update_registration_enabled /var/www/app/js/config.js +update_analytics_enabled /var/www/app/js/config.js exec "$@"; diff --git a/exporter/shadow-cljs.edn b/exporter/shadow-cljs.edn index 3a81cff0f..7a7ca5859 100644 --- a/exporter/shadow-cljs.edn +++ b/exporter/shadow-cljs.edn @@ -1,11 +1,13 @@ {:dependencies - [[funcool/promesa "6.0.0"] + [[com.cognitect/transit-cljs "0.8.269"] [danlentz/clj-uuid "0.1.9"] + [frankiesardo/linked "1.3.0"] [funcool/cuerdas "2021.05.02-0"] + [funcool/promesa "6.0.0"] + [integrant/integrant "0.8.0"] [lambdaisland/glogi "1.0.106"] - [metosin/reitit-core "0.5.13"] - [com.cognitect/transit-cljs "0.8.269"] - [frankiesardo/linked "1.3.0"]] + [lambdaisland/uri "1.4.54"] + [metosin/reitit-core "0.5.13"]] :source-paths ["src" "vendor" "../common"] :jvm-opts ["-Xmx512m" "-Xms50m" "-XX:+UseSerialGC"] diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 374464420..90f7cd00b 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -6,11 +6,17 @@ (ns app.browser (:require + ["puppeteer-cluster" :as ppc] + [app.common.data :as d] + [app.config :as cf] [lambdaisland.glogi :as log] - [promesa.core :as p] - ["puppeteer-cluster" :as ppc])) + [promesa.core :as p])) -(def USER-AGENT +;; --- BROWSER API + +(def default-timeout 30000) +(def default-viewport {:width 1920 :height 1080 :scale 1}) +(def default-user-agent (str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36")) @@ -20,15 +26,25 @@ (let [page (unchecked-get props "page")] (f page))))) -(defn emulate! - [page {:keys [viewport user-agent scale] - :or {user-agent USER-AGENT - scale 1}}] - (let [[width height] viewport] - (.emulate ^js page #js {:viewport #js {:width width - :height height - :deviceScaleFactor scale} - :userAgent user-agent}))) +(defn set-cookie! + [page {:keys [key value domain]}] + (.setCookie ^js page #js {:name key + :value value + :domain domain})) + +(defn configure-page! + [page {:keys [timeout cookie user-agent viewport]}] + (let [timeout (or timeout default-timeout) + user-agent (or user-agent default-user-agent) + viewport (d/merge default-viewport viewport)] + (p/do! + (.setViewport ^js page #js {:width (:width viewport) + :height (:height viewport) + :deviceScaleFactor (:scale viewport)}) + (.setUserAgent ^js page user-agent) + (.setDefaultTimeout ^js page timeout) + (when cookie + (set-cookie! page cookie))))) (defn navigate! ([page url] (navigate! page url nil)) @@ -40,10 +56,9 @@ [page ms] (.waitForTimeout ^js page ms)) - (defn wait-for ([page selector] (wait-for page selector nil)) - ([page selector {:keys [visible] :or {visible false}}] + ([page selector {:keys [visible timeout] :or {visible false timeout 10000}}] (.waitForSelector ^js page selector #js {:visible visible}))) (defn screenshot @@ -68,30 +83,39 @@ [frame selector] (.$$ ^js frame selector)) -(defn set-cookie! - [page {:keys [key value domain]}] - (.setCookie ^js page #js {:name key - :value value - :domain domain})) -(defn start! - ([] (start! nil)) - ([{:keys [concurrency concurrency-strategy] - :or {concurrency 10 - concurrency-strategy :incognito}}] - (let [ccst (case concurrency-strategy - :browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster) - :incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster) - :page (.-CONCURRENCY_PAGE ^js ppc/Cluster)) - opts #js {:concurrency ccst - :maxConcurrency concurrency - :puppeteerOptions #js {:args #js ["--no-sandbox"]}}] - (.launch ^js ppc/Cluster opts)))) +;; --- BROWSER STATE -(defn stop! - [instance] - (p/do! - (.idle ^js instance) - (.close ^js instance) - (log/info :msg "shutdown headless browser") - nil)) +(def instance (atom nil)) + +(defn- create-browser + [concurrency strategy] + (let [strategy (case strategy + :browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster) + :incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster) + :page (.-CONCURRENCY_PAGE ^js ppc/Cluster)) + opts #js {:concurrency strategy + :maxConcurrency concurrency + :puppeteerOptions #js {:args #js ["--no-sandbox"]}}] + (.launch ^js ppc/Cluster opts))) + + +(defn init + [] + (let [concurrency (cf/get :browser-concurrency) + strategy (cf/get :browser-strategy)] + (-> (create-browser concurrency strategy) + (p/then #(reset! instance %)) + (p/catch (fn [error] + (log/error :msg "failed to initialize browser") + (js/console.error error)))))) + + +(defn stop + [] + (if-let [instance @instance] + (p/do! + (.idle ^js instance) + (.close ^js instance) + (log/info :msg "shutdown headless browser")) + (p/resolved nil))) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index e9b83c23b..eb65382a4 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -5,22 +5,62 @@ ;; Copyright (c) UXBOX Labs SL (ns app.config + (:refer-clojure :exclude [get]) (:require + [app.common.data :as d] ["process" :as process] [cljs.pprint] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [app.common.spec :as us] + [cljs.spec.alpha :as s] + [cljs.core :as c] + [lambdaisland.uri :as u])) -(defn- keywordize - [s] - (-> (str/kebab s) - (str/keyword))) +(def defaults + {:public-uri "http://localhost:3449" + :http-server-port 6061 + :browser-concurrency 5 + :browser-strategy :incognito}) -(defonce env - (let [env (unchecked-get process "env")] - (persistent! - (reduce #(assoc! %1 (keywordize %2) (unchecked-get env %2)) - (transient {}) - (js/Object.keys env))))) +(s/def ::public-uri ::us/string) +(s/def ::http-server-port ::us/integer) +(s/def ::browser-concurrency ::us/integer) +(s/def ::browser-strategy ::us/keyword) -(defonce config - {:public-uri (:penpot-public-uri env "http://localhost:3449")}) +(s/def ::config + (s/keys :opt-un [::public-uri + ::http-server-port + ::browser-concurrency + ::browser-strategy])) +(defn- read-env + [prefix] + (let [env (unchecked-get process "env") + kwd (fn [s] (-> (str/kebab s) (str/keyword))) + prefix (str prefix "_") + len (count prefix)] + (reduce (fn [res key] + (let [val (unchecked-get env key) + key (str/lower key)] + (cond-> res + (str/starts-with? key prefix) + (assoc (kwd (subs key len)) val)))) + {} + (js/Object.keys env)))) + +(defn- prepare-config + [] + (let [env (read-env "penpot") + env (d/without-nils env) + data (merge defaults env)] + (us/conform ::config data))) + +(def config + (atom (prepare-config))) + + +(defn get + "A configuration getter." + ([key] + (c/get @config key)) + ([key default] + (c/get @config key default))) diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 6bc699476..7fafd8393 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -21,10 +21,9 @@ (defn start [& args] (log/info :msg "initializing") - (p/let [browser (bwr/start!) - server (http/start! {:browser browser})] - (reset! state {:http server - :browser browser}))) + (p/do! + (bwr/init) + (http/init))) (def main start) @@ -35,8 +34,6 @@ (log/info :msg "stoping") (p/do! - (when-let [instance (:browser @state)] - (bwr/stop! instance)) - (when-let [instance (:http @state)] - (http/stop! instance)) + (bwr/stop) + (http/stop) (done))) diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs index d0d32ebf8..4dca3cac4 100644 --- a/exporter/src/app/http.cljs +++ b/exporter/src/app/http.cljs @@ -6,29 +6,33 @@ (ns app.http (:require + [app.config :as cf] [app.http.export :refer [export-handler]] - [app.http.thumbnail :refer [thumbnail-handler]] [app.http.impl :as impl] [lambdaisland.glogi :as log] [promesa.core :as p] [reitit.core :as r])) (def routes - [["/export/thumbnail" {:handler thumbnail-handler}] - ["/export" {:handler export-handler}]]) + [["/export" {:handler export-handler}]]) -(defn start! - [extra] - (log/info :msg "starting http server" :port 6061) +(def instance (atom nil)) + +(defn init + [] (let [router (r/router routes) - handler (impl/handler router extra) - server (impl/server handler)] - (.listen server 6061) - (p/resolved server))) + handler (impl/handler router) + server (impl/server handler) + port (cf/get :http-server-port 6061)] + (.listen server port) + (log/info :msg "starting http server" :port port) + (reset! instance server))) -(defn stop! - [server] - (p/create (fn [resolve] - (.close server (fn [] - (log/info :msg "shutdown http server") - (resolve)))))) +(defn stop + [] + (if-let [server @instance] + (p/create (fn [resolve] + (.close server (fn [] + (log/info :msg "shutdown http server") + (resolve))))) + (p/resolved nil))) diff --git a/exporter/src/app/http/export.cljs b/exporter/src/app/http/export.cljs index 15ac3e8ba..8be2f5470 100644 --- a/exporter/src/app/http/export.cljs +++ b/exporter/src/app/http/export.cljs @@ -6,15 +6,15 @@ (ns app.http.export (:require - [app.http.export-bitmap :as bitmap] - [app.http.export-svg :as svg] + [app.common.exceptions :as exc :include-macros true] + [app.common.spec :as us] + [app.renderer.bitmap :as rb] + [app.renderer.svg :as rs] [app.zipfile :as zip] [cljs.spec.alpha :as s] [cuerdas.core :as str] [lambdaisland.glogi :as log] - [promesa.core :as p] - [app.common.exceptions :as exc :include-macros true] - [app.common.spec :as us])) + [promesa.core :as p])) (s/def ::name ::us/string) (s/def ::page-id ::us/uuid) @@ -38,42 +38,44 @@ (declare attach-filename) (defn export-handler - [{:keys [params browser cookies] :as request}] + [{:keys [params cookies] :as request}] (let [{:keys [exports page-id file-id object-id name]} (us/conform ::handler-params params) token (.get ^js cookies "auth-token")] (case (count exports) - 0 (exc/raise :type :validation :code :missing-exports) - 1 (handle-single-export - request - (assoc (first exports) - :name name - :token token - :file-id file-id - :page-id page-id - :object-id object-id)) - (handle-multiple-export - request - (map (fn [item] - (assoc item - :name name - :token token - :file-id file-id - :page-id page-id - :object-id object-id)) exports))))) + 0 (exc/raise :type :validation + :code :missing-exports) + + 1 (-> (first exports) + (assoc :name name) + (assoc :token token) + (assoc :file-id file-id) + (assoc :page-id page-id) + (assoc :object-id object-id) + (handle-single-export)) + + (->> exports + (map (fn [item] + (-> item + (assoc :name name) + (assoc :token token) + (assoc :file-id file-id) + (assoc :page-id page-id) + (assoc :object-id object-id)))) + (handle-multiple-export))))) (defn- handle-single-export - [{:keys [browser]} params] - (p/let [result (perform-export browser params)] + [params] + (p/let [result (perform-export params)] {:status 200 :body (:content result) :headers {"content-type" (:mime-type result) "content-length" (:length result)}})) (defn- handle-multiple-export - [{:keys [browser]} exports] + [exports] (let [proms (->> exports (attach-filename) - (map (partial perform-export browser)))] + (map perform-export))] (-> (p/all proms) (p/then (fn [results] (reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results))) @@ -83,11 +85,11 @@ :body (.generateNodeStream ^js fzip)}))))) (defn- perform-export - [browser params] + [params] (case (:type params) - :png (bitmap/export browser params) - :jpeg (bitmap/export browser params) - :svg (svg/export browser params))) + :png (rb/render params) + :jpeg (rb/render params) + :svg (rs/render params))) (defn- find-filename-candidate [params used] diff --git a/exporter/src/app/http/export_bitmap.cljs b/exporter/src/app/http/export_bitmap.cljs deleted file mode 100644 index cf17a5e6d..000000000 --- a/exporter/src/app/http/export_bitmap.cljs +++ /dev/null @@ -1,80 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.http.export-bitmap - (:require - [cuerdas.core :as str] - [app.browser :as bwr] - [app.config :as cfg] - [lambdaisland.glogi :as log] - [cljs.spec.alpha :as s] - [promesa.core :as p] - [app.common.exceptions :as exc :include-macros true] - [app.common.data :as d] - [app.common.pages :as cp] - [app.common.spec :as us]) - (:import - goog.Uri)) - -(defn screenshot-object - [browser {:keys [file-id page-id object-id token scale type]}] - (letfn [(handle [page] - (let [path (str "/render-object/" file-id "/" page-id "/" object-id) - uri (doto (Uri. (:public-uri cfg/config)) - (.setPath "/") - (.setFragment path)) - cookie {:domain (str (.getDomain uri) - ":" - (.getPort uri)) - :key "auth-token" - :value token}] - (log/info :uri (.toString uri)) - (screenshot page (.toString uri) cookie))) - - (screenshot [page uri cookie] - (p/do! - (bwr/emulate! page {:viewport [1920 1080] - :scale scale}) - (bwr/set-cookie! page cookie) - (bwr/navigate! page uri) - (bwr/eval! page (js* "() => document.body.style.background = 'transparent'")) - (p/let [dom (bwr/select page "#screenshot")] - (case type - :png (bwr/screenshot dom {:omit-background? true :type type}) - :jpeg (bwr/screenshot dom {:omit-background? false :type type})))))] - - (bwr/exec! browser handle))) - -(s/def ::name ::us/string) -(s/def ::suffix ::us/string) -(s/def ::type #{:jpeg :png}) -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::object-id ::us/uuid) -(s/def ::scale ::us/number) -(s/def ::token ::us/string) -(s/def ::filename ::us/string) - -(s/def ::export-params - (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id] - :opt-un [::filename])) - -(defn export - [browser params] - (us/assert ::export-params params) - (p/let [content (screenshot-object browser params)] - {:content content - :filename (or (:filename params) - (str (:name params) - (:suffix params "") - (case (:type params) - :png ".png" - :jpeg ".jpg"))) - :length (alength content) - :mime-type (case (:type params) - :png "image/png" - :jpeg "image/jpeg")})) - diff --git a/exporter/src/app/http/impl.cljs b/exporter/src/app/http/impl.cljs index 15f00ba5b..3401194d4 100644 --- a/exporter/src/app/http/impl.cljs +++ b/exporter/src/app/http/impl.cljs @@ -13,25 +13,15 @@ [app.util.transit :as t] [cuerdas.core :as str] [lambdaisland.glogi :as log] + [lambdaisland.uri :as u] [promesa.core :as p] - [reitit.core :as r]) - (:import - goog.Uri)) - -(defn- query-params - "Given goog.Uri, read query parameters into Clojure map." - [^Uri uri] - (let [^js q (.getQueryData uri)] - (->> q - (.getKeys) - (map (juxt keyword #(.get q %))) - (into {})))) + [reitit.core :as r])) (defn- match [router ctx] - (let [uri (.parse Uri (unchecked-get ctx "originalUrl"))] - (when-let [match (r/match-by-path router (.getPath ^js uri))] - (assoc match :query-params (query-params uri))))) + (let [uri (u/uri (unchecked-get ctx "originalUrl"))] + (when-let [match (r/match-by-path router (:path uri))] + (assoc match :query-params (u/query-string->map (:query uri)))))) (defn- handle-error [error request] @@ -48,17 +38,21 @@ :headers {"content-type" "text/html"} :body (str "
" (:explain data) "
\n")})) + (and (= :internal type) + (= :browser-not-ready code)) + {:status 503 + :headers {"x-error" (t/encode data)} + :body ""} + :else (do (log/error :msg "Unexpected error" :error error) (js/console.error error) {:status 500 - :headers {"x-metadata" (t/encode {:type :unexpected - :message (ex-message error)})} + :headers {"x-error" (t/encode data)} :body ""})))) - (defn- handle-response [ctx {:keys [body headers status] :or {headers {} status 200}}] (run! (fn [[k v]] (.set ^js ctx k v)) headers) @@ -89,17 +83,16 @@ (t/decode)))))))) (defn- wrap-handler - [f extra] + [f] (fn [ctx] (p/let [cookies (unchecked-get ctx "cookies") headers (parse-headers ctx) body (parse-body ctx) - request (assoc extra - :method (str/lower (unchecked-get ctx "method")) - :body body - :ctx ctx - :headers headers - :cookies cookies)] + request {:method (str/lower (unchecked-get ctx "method")) + :body body + :ctx ctx + :headers headers + :cookies cookies}] (-> (p/do! (f request)) (p/then (fn [rsp] (when (map? rsp) @@ -131,10 +124,10 @@ (.createServer http @handler)) (defn handler - [router extra] + [router] (let [instance (doto (new koa) (.use (-> (router-handler router) - (wrap-handler extra))))] + (wrap-handler))))] (specify! instance cljs.core/IDeref (-deref [_] diff --git a/exporter/src/app/http/thumbnail.cljs b/exporter/src/app/http/thumbnail.cljs deleted file mode 100644 index f351d67a4..000000000 --- a/exporter/src/app/http/thumbnail.cljs +++ /dev/null @@ -1,43 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.http.thumbnail - (:require - [app.common.exceptions :as exc :include-macros true] - [app.common.spec :as us] - [app.http.export-bitmap :as bitmap] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [lambdaisland.glogi :as log] - [promesa.core :as p])) - -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::object-id ::us/uuid) -(s/def ::scale ::us/number) - -(s/def ::handler-params - (s/keys :req-un [::page-id ::file-id ::object-id])) - -(declare handle-single-export) -(declare handle-multiple-export) -(declare perform-export) -(declare attach-filename) - -(defn thumbnail-handler - [{:keys [params browser cookies] :as request}] - (let [{:keys [page-id file-id object-id]} (us/conform ::handler-params params) - params {:token (.get ^js cookies "auth-token") - :file-id file-id - :page-id page-id - :object-id object-id - :scale 0.3 - :type :jpeg}] - (p/let [content (bitmap/screenshot-object browser params)] - {:status 200 - :body content - :headers {"content-type" "image/jpeg" - "content-length" (alength content)}}))) diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs new file mode 100644 index 000000000..d80d53ab6 --- /dev/null +++ b/exporter/src/app/renderer/bitmap.cljs @@ -0,0 +1,95 @@ +;; 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.renderer.bitmap + "A bitmap renderer." + (:require + [app.browser :as bw] + [app.common.data :as d] + [app.common.exceptions :as ex :include-macros true] + [app.common.pages :as cp] + [app.common.spec :as us] + [app.config :as cf] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [lambdaisland.uri :as u] + [lambdaisland.glogi :as log] + [promesa.core :as p])) + +(defn create-cookie + [uri token] + (let [domain (str (:host uri) + (when (:port uri) + (str ":" (:port uri))))] + {:domain domain + :key "auth-token" + :value token})) + +(defn screenshot-object + [browser {:keys [file-id page-id object-id token scale type]}] + (letfn [(handle [page] + (let [path (str "/render-object/" file-id "/" page-id "/" object-id) + uri (-> (u/uri (cf/get :public-uri)) + (assoc :path "/") + (assoc :fragment path)) + cookie (create-cookie uri token)] + (screenshot page (str uri) cookie))) + + (screenshot [page uri cookie] + (log/info :uri uri) + (let [viewport {:width 1920 + :height 1080 + :scale scale} + options {:viewport viewport + :cookie cookie}] + (p/do! + (bw/configure-page! page options) + (bw/navigate! page uri) + (bw/eval! page (js* "() => document.body.style.background = 'transparent'")) + (bw/wait-for page "#screenshot") + (p/let [dom (bw/select page "#screenshot")] + (case type + :png (bw/screenshot dom {:omit-background? true :type type}) + :jpeg (bw/screenshot dom {:omit-background? false :type type}))))))] + + (bw/exec! browser handle))) + +(s/def ::name ::us/string) +(s/def ::suffix ::us/string) +(s/def ::type #{:jpeg :png}) +(s/def ::page-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::object-id ::us/uuid) +(s/def ::scale ::us/number) +(s/def ::token ::us/string) +(s/def ::filename ::us/string) + +(s/def ::render-params + (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id] + :opt-un [::filename])) + +(defn render + [params] + (us/assert ::render-params params) + (let [browser @bw/instance] + (when-not browser + (ex/raise :type :internal + :code :browser-not-ready + :hint "browser cluster is not initialized yet")) + + (p/let [content (screenshot-object browser params)] + {:content content + :filename (or (:filename params) + (str (:name params) + (:suffix params "") + (case (:type params) + :png ".png" + :jpeg ".jpg"))) + :length (alength content) + :mime-type (case (:type params) + :png "image/png" + :jpeg "image/jpeg")}))) + diff --git a/exporter/src/app/http/export_svg.cljs b/exporter/src/app/renderer/svg.cljs similarity index 81% rename from exporter/src/app/http/export_svg.cljs rename to exporter/src/app/renderer/svg.cljs index 13b6bf946..64f298ca0 100644 --- a/exporter/src/app/http/export_svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -4,24 +4,24 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.http.export-svg +(ns app.renderer.svg (:require ["path" :as path] ["xml-js" :as xml] - [app.browser :as bwr] + [app.browser :as bw] [app.common.data :as d] - [app.common.exceptions :as exc :include-macros true] + [app.common.exceptions :as ex :include-macros true] [app.common.pages :as cp] [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.util.shell :as sh] [cljs.spec.alpha :as s] [clojure.walk :as walk] [cuerdas.core :as str] [lambdaisland.glogi :as log] - [promesa.core :as p]) - (:import - goog.Uri)) + [lambdaisland.uri :as u] + [app.renderer.bitmap :refer [create-cookie]] + [promesa.core :as p])) (log/set-level "app.http.export-svg" :trace) @@ -67,7 +67,6 @@ (nil? d) (str/empty? d))))) - (defn flatten-toplevel-svg-elements "Flattens XML data structure if two nested top-side SVG elements found." [item] @@ -165,7 +164,9 @@ ;; objects. (let [vbox (-> (get-in result ["attributes" "viewBox"]) (parse-viewbox)) - transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y (/ width (:width vbox)) (/ height (:height vbox)))] + transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y + (/ width (:width vbox)) + (/ height (:height vbox)))] (-> result (assoc "name" "g") (assoc "attributes" {}) @@ -212,8 +213,8 @@ (extract-single-node [node] (log/trace :fn :extract-single-node) - (p/let [attrs (bwr/eval! node extract-element-attrs) - shot (bwr/screenshot node {:omit-background? true :type "png"})] + (p/let [attrs (bw/eval! node extract-element-attrs) + shot (bw/screenshot node {:omit-background? true :type "png"})] {:id (unchecked-get attrs "id") :x (unchecked-get attrs "x") :y (unchecked-get attrs "y") @@ -235,12 +236,12 @@ (process-text-nodes [page] (log/trace :fn :process-text-nodes) - (-> (bwr/select-all page "#screenshot foreignObject") + (-> (bw/select-all page "#screenshot foreignObject") (p/then (fn [nodes] (p/all (map process-text-node nodes)))))) (extract-svg [page] - (p/let [dom (bwr/select page "#screenshot") - xmldata (bwr/eval! dom (fn [elem] (.-outerHTML ^js elem))) + (p/let [dom (bw/select page "#screenshot") + xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem))) nodes (process-text-nodes page) nodes (d/index-by :id nodes) result (replace-text-nodes xmldata nodes)] @@ -252,31 +253,33 @@ result)) (render-in-page [page {:keys [uri cookie] :as rctx}] - (p/do! - (bwr/emulate! page {:viewport [1920 1080] - :scale 4}) - (bwr/set-cookie! page cookie) - (bwr/navigate! page uri) - ;; (bwr/wait-for page "#screenshot foreignObject" {:visible true}) - (bwr/sleep page 2000) - ;; (bwr/eval! page (js* "() => document.body.style.background = 'transparent'")) - page)) + (let [viewport {:width 1920 + :height 1080 + :scale 4} + options {:viewport viewport + :timeout 15000 + :cookie cookie}] + (p/do! + (bw/configure-page! page options) + (bw/navigate! page uri) + (bw/wait-for page "#screenshot") + (bw/sleep page 2000) + ;; (bw/eval! page (js* "() => document.body.style.background = 'transparent'")) + page))) (handle [rctx page] (p/let [page (render-in-page page rctx)] (extract-svg page)))] - (let [path (str "/render-object/" file-id "/" page-id "/" object-id) - uri (doto (Uri. (:public-uri cfg/config)) - (.setPath "/") - (.setFragment path)) - rctx {:cookie {:domain (str (.getDomain uri) ":" (.getPort uri)) - :key "auth-token" - :value token} - :uri (.toString uri)}] - - (log/info :uri (.toString uri)) - (bwr/exec! browser (partial handle rctx))))) + (let [path (str "/render-object/" file-id "/" page-id "/" object-id) + uri (-> (u/uri (cf/get :public-uri)) + (assoc :path "/") + (assoc :fragment path)) + cookie (create-cookie uri token) + rctx {:cookie cookie + :uri (str uri)}] + (log/info :uri (:uri rctx)) + (bw/exec! browser (partial handle rctx))))) (s/def ::name ::us/string) (s/def ::suffix ::us/string) @@ -288,18 +291,25 @@ (s/def ::token ::us/string) (s/def ::filename ::us/string) -(s/def ::export-params +(s/def ::render-params (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token] :opt-un [::filename])) -(defn export - [browser params] - (us/assert ::export-params params) - (p/let [content (render-object browser params)] - {:content content - :filename (or (:filename params) - (str (:name params) - (:suffix params "") - ".svg")) - :length (alength content) - :mime-type "image/svg+xml"})) +(defn render + [params] + (us/assert ::render-params params) + (let [browser @bw/instance] + (when-not browser + (ex/raise :type :internal + :code :browser-not-ready + :hint "browser cluster is not initialized yet")) + + + (p/let [content (render-object browser params)] + {:content content + :filename (or (:filename params) + (str (:name params) + (:suffix params "") + ".svg")) + :length (alength content) + :mime-type "image/svg+xml"}))) diff --git a/exporter/src/app/util/transit.cljs b/exporter/src/app/util/transit.cljs index 38fbe3d4b..6afaa015e 100644 --- a/exporter/src/app/util/transit.cljs +++ b/exporter/src/app/util/transit.cljs @@ -25,5 +25,5 @@ (defn encode [data] - (let [w (t/writer :json {:handlers +write-handlers+})] + (let [w (t/writer :json-verbose {:handlers +write-handlers+})] (t/write w data))) diff --git a/exporter/yarn.lock b/exporter/yarn.lock index a122fe7ac..43907db23 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -11,9 +11,9 @@ regenerator-runtime "^0.13.4" "@types/node@*": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a" - integrity sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA== + version "15.0.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67" + integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA== "@types/yauzl@^2.9.1": version "2.9.1" @@ -272,9 +272,9 @@ cookies@~0.8.0: keygrip "~1.1.0" core-js-pure@^3.0.0: - version "3.11.2" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.11.2.tgz#10e3b35788c00f431bc0d601d7551475ec3e792c" - integrity sha512-DQxdEKm+zFsnON7ZGOgUAQXBt1UJJ01tOzN/HgQ7cNf0oEHW1tcBLfCQQd1q6otdLu5gAdvKYxKHAoXGwE/kiQ== + version "3.12.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.12.1.tgz#934da8b9b7221e2a2443dc71dfa5bd77a7ea00b8" + integrity sha512-1cch+qads4JnDSWsvc7d6nzlKAippwjUlf6vykkTLW53VSV+NkE6muGBToAjEA8pG90cSfcud3JgVmW2ds5TaQ== core-util-is@~1.0.0: version "1.0.2" @@ -492,9 +492,9 @@ get-stream@^5.1.0: pump "^3.0.0" glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -618,9 +618,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= is-generator-function@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b" - integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ== + version "1.0.9" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c" + integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A== isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" @@ -982,9 +982,9 @@ puppeteer-cluster@^0.22.0: debug "^4.1.1" puppeteer@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.0.tgz#0530ed1f595088eefd078c8f1f7618d00f216a56" - integrity sha512-+BWwEKYQ9oBTUcDYwfgnVPlHSEYqD4sXsMqQf70vSlTE6YIuXujc7zKgO3FyZNJYVrdrUppy/LLwGF1IRacQMQ== + version "9.1.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.1.tgz#f74b7facf86887efd6c6b9fabb7baae6fdce012c" + integrity sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw== dependencies: debug "^4.1.0" devtools-protocol "0.0.869402" diff --git a/frontend/dev/cljs/user.cljs b/frontend/dev/cljs/user.cljs deleted file mode 100644 index 2d251b6c7..000000000 --- a/frontend/dev/cljs/user.cljs +++ /dev/null @@ -1 +0,0 @@ -(ns cljs.user) diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index ba8cedd86..428bbf9a7 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -17,6 +17,7 @@ const mkdirp = require("mkdirp"); const rimraf = require("rimraf"); const sass = require("sass"); const gettext = require("gettext-parser"); +const marked = require("marked"); const mapStream = require("map-stream"); const paths = {}; @@ -32,7 +33,7 @@ paths.dist = "./target/dist/"; // Templates function readLocales() { - const langs = ["ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_cn"]; + const langs = ["ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_CN", "pt_BR", "ro"]; const result = {}; for (let lang of langs) { @@ -45,17 +46,35 @@ function readLocales() { for (let key of Object.keys(trdata)) { if (key === "") continue; + const comments = trdata[key].comments || {}; if (l.isNil(result[key])) { result[key] = {}; } - const msgstr = trdata[key].msgstr; - if (msgstr.length === 1) { - result[key][lang] = msgstr[0]; + const isMarkdown = l.includes(comments.flag, "markdown"); + + const msgs = trdata[key].msgstr; + if (msgs.length === 1) { + let message = msgs[0]; + if (isMarkdown) { + message = marked.parseInline(message); + } + + result[key][lang] = message; } 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}); + // } } } diff --git a/frontend/package.json b/frontend/package.json index dbc09258c..9e583d58a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,26 +27,31 @@ "gulp-sourcemaps": "^3.0.0", "gulp-svg-sprite": "^1.5.0", "map-stream": "0.0.7", + "marked": "^2.0.3", "mkdirp": "^1.0.4", - "postcss": "^8.2.7", + "postcss": "^8.2.15", "postcss-clean": "^1.2.2", "rimraf": "^3.0.0", "sass": "^1.32.8", - "shadow-cljs": "^2.11.20" + "shadow-cljs": "2.12.6" }, "dependencies": { - "date-fns": "^2.21.1", + "date-fns": "^2.21.3", "draft-js": "^0.11.7", "highlight.js": "^10.6.0", "js-beautify": "^1.13.5", "luxon": "^1.26.0", "mousetrap": "^1.6.5", + "opentype.js": "^1.3.3", "randomcolor": "^0.6.2", "react": "~17.0.1", "react-dom": "~17.0.1", - "rxjs": "~7.0.0-beta.12", + "react-virtualized": "^9.22.3", + "rxjs": "~7.0.1", + "sax": "^1.2.4", "source-map-support": "^0.5.16", "tdigest": "^0.1.1", + "ua-parser-js": "^0.7.28", "xregexp": "^5.0.1" } } diff --git a/frontend/resources/images/features/custom-fonts.gif b/frontend/resources/images/features/custom-fonts.gif new file mode 100644 index 000000000..191dcf36c Binary files /dev/null and b/frontend/resources/images/features/custom-fonts.gif differ diff --git a/frontend/resources/images/features/performance.gif b/frontend/resources/images/features/performance.gif new file mode 100644 index 000000000..afd4c582a Binary files /dev/null and b/frontend/resources/images/features/performance.gif differ diff --git a/frontend/resources/images/features/scale-text.gif b/frontend/resources/images/features/scale-text.gif new file mode 100644 index 000000000..bc56494a6 Binary files /dev/null and b/frontend/resources/images/features/scale-text.gif differ diff --git a/frontend/resources/images/features/shapes-to-path.gif b/frontend/resources/images/features/shapes-to-path.gif new file mode 100644 index 000000000..cea102568 Binary files /dev/null and b/frontend/resources/images/features/shapes-to-path.gif differ diff --git a/frontend/resources/styles/common/dependencies/colors.scss b/frontend/resources/styles/common/dependencies/colors.scss index bd995bc8c..5a9c19c4d 100644 --- a/frontend/resources/styles/common/dependencies/colors.scss +++ b/frontend/resources/styles/common/dependencies/colors.scss @@ -23,6 +23,7 @@ $color-info: #59b9e2; $color-ocean: #4285f4; $color-component: #76B0B8; $color-component-highlight: #00E0FF; +$color-pink: #feecfc; // Gray scale $color-gray-10: #E3E3E3; diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 0c9ee57b1..27c7c3610 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -63,6 +63,7 @@ @import "main/partials/dashboard-sidebar"; @import "main/partials/dashboard-team"; @import "main/partials/dashboard-settings"; +@import "main/partials/dashboard-fonts"; @import "main/partials/debug-icons-preview"; @import "main/partials/editable-label"; @import "main/partials/left-toolbar"; diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss new file mode 100644 index 000000000..0f61a12a3 --- /dev/null +++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss @@ -0,0 +1,234 @@ +.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; + + > .family { + min-width: 200px; + width: 200px; + } + + > .variants { + padding-left: 12px; + } + + .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; + } + } + } + + .font-item { + margin-top: $big; + color: $color-gray-40; + font-size: $fs14; + background-color: $color-white; + display: flex; + min-width: 1000px; + width: 100%; + min-height: 97px; + align-items: center; + padding: $big; + justify-content: space-between; + + &: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; + } + + > .family { + min-width: 200px; + width: 200px; + } + + > .filenames { + min-width: 200px; + } + + > .variants { + font-size: $fs14; + display: flex; + flex-wrap: wrap; + flex-grow: 1; + + .variant { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + cursor: pointer; + + .icon { + display: flex; + height: 16px; + width: 16px; + margin-left: 6px; + align-items: center; + svg { + fill: transparent; + width: 12px; + height: 12px; + transform: rotate(45deg); + } + } + + &:hover { + .icon svg { + fill: $color-gray-30; + } + } + + } + } + + + .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; + } + } + + .fonts-placeholder { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + max-width: 1000px; + width: 100%; + height: 161px; + + border: 1px dashed $color-gray-20; + margin-top: 16px; + + + .icon { + svg { + fill: $color-gray-40; + width: 32px; + height: 32px; + } + } + + .label { + color: $color-gray-40; + font-size: $fs14; + } + } +} diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index d67985d15..bb0b12751 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -382,7 +382,7 @@ .modal-left { align-items: center; - background-color: $color-primary; + background-color: $color-pink; border-top-left-radius: 5px; border-bottom-left-radius: 5px; display: flex; @@ -391,6 +391,10 @@ overflow: hidden; padding: $x-big; width: 230px; + + &.welcome { + padding: 0; + } } .modal-right { @@ -498,6 +502,7 @@ color: $color-black; flex: 1; flex-direction: column; + overflow: visible; padding: $x-big 40px; text-align: center; diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index bcae65e66..4389cf8b6 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -164,6 +164,8 @@ .asset-title { display: flex; cursor: pointer; + font-size: $fs11; + text-transform: uppercase; & .num-assets { color: $color-gray-30; @@ -371,14 +373,11 @@ // overflow-y: scroll; // } - .asset-list { - margin-top: $medium; - } - .asset-list-item { display: flex; align-items: center; border: 1px solid transparent; + border-radius: $br-small; margin-top: $x-small; padding: 2px; font-size: $fs12; diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 36da777ab..af2a9d22a 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -2,8 +2,7 @@ // 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) 2015-2016 Andrey Antukh -// Copyright (c) 2015-2016 Juan de la Cruz +// Copyright (c) UXBOX Labs SL .element-options { display: flex; @@ -809,9 +808,10 @@ left: 0; position: absolute; top: 0; - width: calc(100% - 8px); + width: calc(100%); opacity: 0.4; z-index: 10; + display: flex; } .advanced-options-wrapper { @@ -1061,37 +1061,200 @@ } .multiple-typography { - margin: 0.5rem; - padding: 0.5rem; - border: 1px dashed $color-gray-30; - border-radius: 4px; - display: flex; - justify-content: space-between; + margin: 0.5rem; + padding: 0.5rem; + border: 1px dashed $color-gray-30; + border-radius: 4px; + display: flex; + justify-content: space-between; - .multiple-typography-text, - .multiple-typography-button { - font-size: $fs13; - display: flex; - align-items: center; + .multiple-typography-text, + .multiple-typography-button { + font-size: $fs13; + display: flex; + align-items: center; + } + + .multiple-typography-button { + cursor: pointer; + svg { + transition: fill 0.3s; + width: 16px; + height: 16px; + fill: $color-gray-10; } - .multiple-typography-button { - cursor: pointer; - svg { - transition: fill 0.3s; - width: 16px; - height: 16px; - fill: $color-gray-10; - } + &:hover svg { + fill: $color-primary; + } + } - &:hover svg { - fill: $color-primary; + svg { + } + + .multiple-typography-button:hover svg { + } +} + +.font-selector { + background: $color-black; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: calc(100%); + z-index: 10; + display: flex; + justify-content: center; + align-items: center; + + .font-selector-dropdown { + background: #303236; + display: flex; + flex-direction: column; + flex-grow: 1; + height: 100%; + } + + header { + padding: 15px 17px; + display: flex; + align-items: center; + position: relative; + + .backend-filters { + padding: $small $medium; + // width: 220px; + top: 40px; + right: 20px; + } + .backend-filter { + display: flex; + align-items: center; + padding: $small 0; + cursor: pointer; + + .checkbox-icon { + display: flex; + justify-content: center; + align-items: center; + width: $medium; + height: $medium; + border: 1px solid $color-gray-30; + border-radius: $br-small; + + svg { + width: 8px; + display: none; + height: 8px; + fill: $color-black; } + } + + .backend-name { + margin-left: $small; + color: $color-gray-50; + } + + &.selected { + .checkbox-icon { + svg { + display: inherit; + } + } + } + + } + + input { + display: flex; + flex-grow: 1; + padding: 4px; + font-size: $fs12; + background: $color-gray-50; + border-radius: $br-small; + color: $color-gray-20; + border: 1px solid $color-gray-30; + margin: 0px; + } + + .options { + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + margin-left: $small; + + svg { + width: 16px; + height: 16px; + fill: $color-gray-20 + } + + &.active { + svg { + fill: $color-primary; + } + } + } + } + + .fonts-list { + display: flex; + flex-direction: column; + height: 100%; + + position: relative; + -webkit-box-flex: 1; + flex: 1 1 auto; + } + + hr { + margin-bottom: 0px; + margin-top: 0px; + } + + .font-item { + padding-left: $big; + height: $x-big; + max-height: $x-big; + width: 100%; + display: flex; + align-items: center; + cursor: pointer; + color: $color-gray-10; + + &.selected { + background-color: $color-black; + color: $color-primary; + + .icon svg {fill: $color-primary;} + } + + &:hover { + background-color: $color-primary; + color: $color-black; + } + + .icon { + display: flex; + // justify-content: center; + align-items: center; + // border: 1px solid red; + width: $big + } + + .label { + font-size: 12px; } svg { + fill: $color-gray-10; + width: 10px; + height: 10px; } - - .multiple-typography-button:hover svg { - } + } } + + diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 57baeec1b..d2f630bb5 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -69,6 +69,7 @@ $width-settings-bar: 16rem; height: 100%; .tool-window { + position: relative; border-bottom: 1px solid $color-gray-60; display: flex; flex-direction: column; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index d8e3b69ef..f167e9814 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -134,8 +134,8 @@ width: 100%; display: grid; - grid-template-rows: 20px 1fr; - grid-template-columns: 20px 1fr; + grid-template-rows: 20px 100%; + grid-template-columns: 20px 100%; } .viewport { @@ -145,6 +145,11 @@ overflow: hidden; position: relative; + svg { + widht: 100%; + height: 100%; + } + .viewport-overlays { position: absolute; width: 100%; @@ -164,12 +169,6 @@ } } - .selection-rect { - fill: rgba(235, 215, 92, 0.1); - stroke: #000000; - stroke-width: 0.1px; - } - .render-shapes { position: absolute; } diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index da046cd80..d4b2b0a2e 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -1,25 +1,26 @@ {:http {:port 3448} :nrepl {:port 3447} - :jvm-opts ["-Xmx500m" "-Xms50m" "-XX:+UseSerialGC"] + :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] :dev-http {8888 "classpath:public"} - :source-paths ["src", "vendor", "resources", "../common", "tests"] + :source-paths ["src", "vendor", "resources", "../common", "tests", "dev"] :dependencies [[binaryage/devtools "RELEASE"] [environ/environ "1.2.0"] - [metosin/reitit-core "0.5.12"] + [metosin/reitit-core "0.5.13"] [expound/expound "0.8.9"] [danlentz/clj-uuid "0.1.9"] [frankiesardo/linked "1.3.0"] - [funcool/beicon "2021.04.27-2"] - [funcool/cuerdas "2020.03.26-3"] + [funcool/beicon "2021.04.29-0"] + [funcool/cuerdas "2021.05.09-0"] [funcool/okulary "2020.04.14-0"] - [funcool/potok "3.2.0"] + [funcool/potok "4.0.0"] [funcool/promesa "6.0.0"] - [funcool/rumext "2021.01.26-0"] + [funcool/rumext "2021.05.12-1"] + [funcool/tubax "2021.05.20-0"] [lambdaisland/uri "1.4.54" :exclusions [org.clojure/data.json]] @@ -43,9 +44,11 @@ :worker {:entries [app.worker] :web-worker true :depends-on #{:shared}}} + :compiler-options {:output-feature-set :es8 - :output-wrapper false} + :output-wrapper false + :warnings {:fn-deprecated false}} :release {:compiler-options @@ -59,6 +62,10 @@ {:target :node-test :output-to "target/tests.js" :ns-regexp "^app.test-" - :autorun true}}} + :autorun true + :compiler-options + {:output-feature-set :es8 + :output-wrapper false + :warnings {:fn-deprecated false}}}}} diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 5f801d155..87dcf4431 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -76,6 +76,7 @@ (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def translations (obj/get global "penpotTranslations")) (def themes (obj/get global "penpotThemes")) +(def analytics (obj/get global "penpotAnalyticsEnabled" false)) (def version (delay (parse-version global))) (def target (delay (parse-target global))) @@ -120,9 +121,10 @@ (defn resolve-file-media ([media] (resolve-file-media media false)) - ([{:keys [id] :as media} thumnail?] + + ([{:keys [id]} thumbnail?] (str (cond-> (u/join public-uri "assets/by-file-media-id/") - (true? thumnail?) (u/join (str id "/thumbnail")) - (false? thumnail?) (u/join (str id)))))) + (true? thumbnail?) (u/join (str id "/thumbnail")) + (false? thumbnail?) (u/join (str id)))))) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index e80e545f8..dc2a58f96 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -9,7 +9,7 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cfg] - [app.main.data.auth :as da] + [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.users :as du] [app.main.repo :as rp] @@ -28,6 +28,7 @@ [app.util.timers :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] + [potok.core :as ptk] [rumext.alpha :as mf])) (log/initialize!) @@ -52,18 +53,16 @@ (defn on-navigate [router path] (let [match (match-path router path) - profile (:profile storage) + profile (:profile @storage) nopath? (or (= path "") (= path "/")) authed? (and (not (nil? profile)) (not= (:id profile) uuid/zero))] (cond (and nopath? authed? (nil? match)) - (->> (rp/query! :profile) - (rx/subs (fn [profile] - (if (not= uuid/zero profile) - (st/emit! (rt/nav :dashboard-projects {:team-id (da/current-team-id profile)})) - (st/emit! (rt/nav :auth-login)))))) + (if (not= uuid/zero profile) + (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) + (st/emit! (rt/nav :auth-login))) (and (not authed?) (nil? match)) (st/emit! (rt/nav :auth-login)) @@ -72,23 +71,42 @@ (st/emit! (dm/assign-exception {:type :not-found})) :else - (st/emit! #(assoc % :route match))))) + (st/emit! (rt/navigated match))))) (defn init-ui [] (mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (mf/element modal) (dom/get-element "modal"))) + +(defn initialize + [] + (letfn [(on-profile [profile] + (rx/of (rt/initialize-router ui/routes) + (rt/initialize-history on-navigate)))] + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (assoc state :session-id (uuid/next))) + + ptk/WatchEvent + (watch [_ state stream] + (rx/merge + (rx/of + (ptk/event ::ev/initialize) + (du/initialize-profile)) + (->> stream + (rx/filter (ptk/type? ::du/profile-fetched)) + (rx/take 1) + (rx/map deref) + (rx/mapcat on-profile))))))) + (defn ^:export init [] (i18n/init! cfg/translations) (theme/init! cfg/themes) - (st/init) (init-ui) - - (st/emit! (rt/initialize-router ui/routes) - (rt/initialize-history on-navigate) - (du/fetch-profile-and-teams))) + (st/emit! (initialize))) (defn reinit [] @@ -103,3 +121,4 @@ (defn ^:dev/after-load after-load [] (reinit)) + diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs deleted file mode 100644 index 694ff93b9..000000000 --- a/frontend/src/app/main/data/auth.cljs +++ /dev/null @@ -1,205 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.main.data.auth - (:require - [app.common.spec :as us] - [app.config :as cf] - [app.main.data.messages :as dm] - [app.main.data.modal :as modal] - [app.main.data.users :as du] - [app.main.repo :as rp] - [app.main.store :refer [initial-state]] - [app.util.i18n :as i18n :refer [tr]] - [app.util.router :as rt] - [app.util.storage :refer [storage]] - [beicon.core :as rx] - [cljs.spec.alpha :as s] - [potok.core :as ptk])) - -(s/def ::email ::us/email) -(s/def ::password string?) -(s/def ::fullname string?) - -;; --- Current team for a profile - -(defn current-team-id - [profile] - (let [team-id (:current-team-id storage)] - (or team-id (:default-team-id profile)))) - -(defn set-current-team! - [team-id] - (swap! storage assoc :current-team-id team-id)) - -;; --- Logged In - -(defn logged-in - [profile] - (ptk/reify ::logged-in - ptk/WatchEvent - (watch [this state stream] - (let [team-id (current-team-id profile) - props (:props profile)] - (rx/concat - (rx/of (du/profile-fetched profile)) - (rx/of (du/fetch-teams)) - (rx/of (rt/nav' :dashboard-projects {:team-id team-id})) - (when-not (:onboarding-viewed props) - (->> (rx/of (modal/show {:type :onboarding})) - (rx/delay 1000)))))))) - -;; --- Login - -(s/def ::login-params - (s/keys :req-un [::email ::password])) - -(defn login - [{:keys [email password] :as data}] - (us/verify ::login-params data) - (ptk/reify ::login - ptk/UpdateEvent - (update [_ state] - (merge state (dissoc initial-state :route :router))) - - ptk/WatchEvent - (watch [this state s] - (let [{:keys [on-error on-success] - :or {on-error identity - on-success identity}} (meta data) - params {:email email - :password password - :scope "webapp"}] - (->> (rx/timer 100) - (rx/mapcat #(rp/mutation :login params)) - (rx/tap on-success) - (rx/catch on-error) - (rx/map logged-in)))))) - -(defn login-from-token - [{:keys [profile] :as tdata}] - (ptk/reify ::login-from-token - ptk/UpdateEvent - (update [_ state] - (merge state (dissoc initial-state :route :router))) - - ptk/WatchEvent - (watch [this state s] - (rx/of (logged-in profile))))) - -;; --- Logout - -(def clear-user-data - (ptk/reify ::clear-user-data - ptk/UpdateEvent - (update [_ state] - (select-keys state [:route :router :session-id :history])) - - ptk/WatchEvent - (watch [_ state s] - (->> (rp/mutation :logout) - (rx/catch (constantly (rx/empty))) - (rx/ignore))) - - ptk/EffectEvent - (effect [_ state s] - (reset! storage {}) - (i18n/reset-locale)))) - -(defn logout - [] - (ptk/reify ::logout - ptk/WatchEvent - (watch [_ state stream] - (rx/of clear-user-data - (rt/nav :auth-login))))) - -;; --- Register - -(s/def ::invitation-token ::us/not-empty-string) - -(s/def ::register - (s/keys :req-un [::fullname ::password ::email] - :opt-un [::invitation-token])) - -(defn register - "Create a register event instance." - [data] - (s/assert ::register data) - (ptk/reify ::register - ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [on-error on-success] - :or {on-error identity - on-success identity}} (meta data)] - (->> (rp/mutation :register-profile data) - (rx/tap on-success) - (rx/catch on-error)))))) - - -;; --- Request Account Deletion - -(defn request-account-deletion - [params] - (ptk/reify ::request-account-deletion - ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [on-error on-success] - :or {on-error identity - on-success identity}} (meta params)] - (->> (rp/mutation :delete-profile {}) - (rx/tap on-success) - (rx/catch on-error)))))) - -;; --- Recovery Request - -(s/def ::recovery-request - (s/keys :req-un [::email])) - -(defn request-profile-recovery - [data] - (us/verify ::recovery-request data) - (ptk/reify ::request-profile-recovery - ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [on-error on-success] - :or {on-error identity - on-success identity}} (meta data)] - - (->> (rp/mutation :request-profile-recovery data) - (rx/tap on-success) - (rx/catch on-error)))))) - -;; --- Recovery (Password) - -(s/def ::token string?) -(s/def ::recover-profile - (s/keys :req-un [::password ::token])) - -(defn recover-profile - [{:keys [token password] :as data}] - (us/verify ::recover-profile data) - (ptk/reify ::recover-profile - ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [on-error on-success] - :or {on-error identity - on-success identity}} (meta data)] - (->> (rp/mutation :recover-profile data) - (rx/tap on-success) - (rx/catch (fn [err] - (on-error) - (rx/empty)))))))) - - -;; --- Create Demo Profile - -(def create-demo-profile - (ptk/reify ::create-demo-profile - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/mutation :create-demo-profile {}) - (rx/map login))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index bb56ba551..eacc98c3a 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -11,7 +11,9 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.repo :as rp] + [app.main.data.events :as ev] [app.main.data.users :as du] + [app.main.data.fonts :as df] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.time :as dt] @@ -53,156 +55,195 @@ (s/def ::file (s/keys :req-un [::id ::name - ::created-at - ::modified-at - ::project-id])) + ::project-id] + :opt-un [::created-at + ::modified-at])) (s/def ::set-of-uuid (s/every ::us/uuid :kind set?)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Data Fetching +;; Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn fetch-team +(declare fetch-projects) + +(defn initialize [{:keys [id] :as params}] - (letfn [(fetched [team state] - (update state :teams assoc id team))] - (ptk/reify ::fetch-team - ptk/WatchEvent - (watch [_ state stream] - (let [profile (:profile state)] - (->> (rp/query :team params) - (rx/map #(partial fetched %)))))))) + (us/assert ::us/uuid id) + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (let [prev-team-id (:current-team-id state)] + (cond-> state + (not= prev-team-id id) + (-> (assoc :current-team-id id) + (dissoc :dashboard-files) + (dissoc :dashboard-projects) + (dissoc :dashboard-recent-files) + (dissoc :dashboard-team-members) + (dissoc :dashboard-team-stats))))) + + ptk/WatchEvent + (watch [_ state stream] + (rx/merge + (ptk/watch (df/load-team-fonts id) state stream) + (ptk/watch (fetch-projects) state stream) + (ptk/watch (du/fetch-teams) state stream) + (ptk/watch (du/fetch-users {:team-id id}) state stream))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Data Fetching (context aware: current team) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- EVENT: fetch-team-members + +(defn team-members-fetched + [members] + (ptk/reify ::team-members-fetched + ptk/UpdateEvent + (update [_ state] + (assoc state :dashboard-team-members (d/index-by :id members))))) (defn fetch-team-members - [{:keys [id] :as params}] - (us/assert ::us/uuid id) - (letfn [(fetched [members state] - (->> members - (d/index-by :id) - (assoc-in state [:team-members id])))] - (ptk/reify ::fetch-team-members - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :team-members {:team-id id}) - (rx/map #(partial fetched %))))))) - -(defn fetch-team-stats - [{:keys [id] :as team}] - (us/assert ::us/uuid id) + [] (ptk/reify ::fetch-team-members ptk/WatchEvent (watch [_ state stream] - (let [fetched #(assoc-in %2 [:team-stats id] %1)] - (->> (rp/query :team-stats {:team-id id}) - (rx/map #(partial fetched %))))))) + (let [team-id (:current-team-id state)] + (->> (rp/query :team-members {:team-id team-id}) + (rx/map team-members-fetched)))))) -;; --- Fetch Projects +;; --- EVENT: fetch-team-stats -(defn fetch-projects - [{:keys [team-id] :as params}] - (us/assert ::us/uuid team-id) - (letfn [(fetched [projects state] - (assoc-in state [:projects team-id] (d/index-by :id projects)))] - (ptk/reify ::fetch-projects - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :projects {:team-id team-id}) - (rx/map #(partial fetched %))))))) +(defn team-stats-fetched + [stats] + (ptk/reify ::team-stats-fetched + ptk/UpdateEvent + (update [_ state] + (assoc state :dashboard-team-stats stats)))) -(defn fetch-bundle - [{:keys [id] :as params}] - (us/assert ::us/uuid id) - (ptk/reify ::fetch-bundle +(defn fetch-team-stats + [] + (ptk/reify ::fetch-team-stats ptk/WatchEvent (watch [_ state stream] - (let [profile (:profile state)] - (rx/merge (ptk/watch (fetch-team params) state stream) - (ptk/watch (fetch-projects {:team-id id}) state stream) - (ptk/watch (du/fetch-users {:team-id id}) state stream)))))) + (let [team-id (:current-team-id state)] + (->> (rp/query :team-stats {:team-id team-id}) + (rx/map team-stats-fetched)))))) -;; --- Search Files +;; --- EVENT: fetch-projects -(s/def :internal.event.search-files/team-id ::us/uuid) -(s/def :internal.event.search-files/search-term (s/nilable ::us/string)) +(defn projects-fetched + [projects] + (ptk/reify ::projects-fetched + ptk/UpdateEvent + (update [_ state] + (let [projects (d/index-by :id projects)] + (assoc state :dashboard-projects projects))))) -(s/def :internal.event/search-files - (s/keys :req-un [:internal.event.search-files/search-term - :internal.event.search-files/team-id])) +(defn fetch-projects + [] + (ptk/reify ::fetch-projects + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (->> (rp/query :projects {:team-id team-id}) + (rx/map projects-fetched)))))) -(defn search-files +;; --- EVENT: search + +(defn search-result-fetched + [result] + (ptk/reify ::search-result-fetched + ptk/UpdateEvent + (update [_ state] + (assoc state :dashboard-search-result result)))) + +(s/def ::search-term (s/nilable ::us/string)) +(s/def ::search + (s/keys :req-un [::search-term ])) + +(defn search [params] - (us/assert :internal.event/search-files params) - (letfn [(fetched [result state] - (update state :dashboard-local - assoc :search-result result))] - (ptk/reify ::search-files + (us/assert ::search params) + (ptk/reify ::search + ptk/UpdateEvent + (update [_ state] + (dissoc state :dashboard-search-result)) + + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state) + params (assoc params :team-id team-id)] + (->> (rp/query :search-files params) + (rx/map search-result-fetched)))))) + +;; --- EVENT: files + +(defn files-fetched + [project-id files] + (letfn [(remove-project-files [files] + (reduce-kv (fn [result id file] + (cond-> result + (= (:project-id file) project-id) (dissoc id))) + files + files))] + (ptk/reify ::files-fetched ptk/UpdateEvent (update [_ state] - (update state :dashboard-local - assoc :search-result nil)) - - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :search-files params) - (rx/map #(partial fetched %))))))) - -;; --- Fetch Files + (update state :dashboard-files + (fn [state] + (let [state (remove-project-files state)] + (reduce #(assoc %1 (:id %2) %2) state files)))))))) (defn fetch-files [{:keys [project-id] :as params}] (us/assert ::us/uuid project-id) - (letfn [(fetched [files state] - (update state :files assoc project-id (d/index-by :id files)))] - (ptk/reify ::fetch-files - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :files params) - (rx/map #(partial fetched %))))))) - -;; --- Fetch Shared Files - -(defn fetch-shared-files - [{:keys [team-id] :as params}] - (us/assert ::us/uuid team-id) - (letfn [(fetched [files state] - (update state :shared-files assoc team-id (d/index-by :id files)))] - (ptk/reify ::fetch-shared-files - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :shared-files {:team-id team-id}) - (rx/map #(partial fetched %))))))) - -;; --- Fetch recent files - -(declare recent-files-fetched) - -(defn fetch-recent-files - [{:keys [team-id] :as params}] - (us/assert ::us/uuid team-id) - (ptk/reify ::fetch-recent-files + (ptk/reify ::fetch-files ptk/WatchEvent (watch [_ state stream] - (let [params {:team-id team-id}] - (->> (rp/query :recent-files params) - (rx/map #(recent-files-fetched team-id %))))))) + (->> (rp/query :project-files {:project-id project-id}) + (rx/map #(files-fetched project-id %)))))) + +;; --- EVENT: shared-files + +(defn shared-files-fetched + [files] + (ptk/reify ::shared-files-fetched + ptk/UpdateEvent + (update [_ state] + (assoc state :dashboard-shared-files (d/index-by :id files))))) + +(defn fetch-shared-files + [] + (ptk/reify ::fetch-shared-files + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (->> (rp/query :team-shared-files {:team-id team-id}) + (rx/map shared-files-fetched)))))) + +;; --- EVENT: recent-files (defn recent-files-fetched - [team-id files] + [files] (ptk/reify ::recent-files-fetched ptk/UpdateEvent (update [_ state] - (let [projects (keys (get-in state [:projects team-id]))] - (reduce (fn [state project-id] - (let [files (filter #(= project-id (:project-id %)) files)] - (-> state - (update-in [:files project-id] merge (d/index-by :id files)) - (assoc-in [:recent-files project-id] (into #{} (map :id) files))))) - state - projects))))) + (let [files (d/index-by :id files)] + (-> state + (assoc :dashboard-recent-files files) + (update :dashboard-files d/merge files)))))) +(defn fetch-recent-files + [] + (ptk/reify ::fetch-recent-files + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (->> (rp/query :team-recent-files {:team-id team-id}) + (rx/map recent-files-fetched)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Selection @@ -218,31 +259,34 @@ :selected-project nil)))) (defn toggle-file-select - [{:keys [file] :as params}] + [{:keys [id project-id] :as file}] + (us/assert ::file file) (ptk/reify ::toggle-file-select ptk/UpdateEvent (update [_ state] - (let [file-id (:id file) - selected-project (get-in state [:dashboard-local - :selected-project])] - (if (or (nil? selected-project) - (= selected-project (:project-id file))) + (let [selected-project-id (get-in state [:dashboard-local :selected-project])] + (if (or (nil? selected-project-id) + (= selected-project-id project-id)) (update state :dashboard-local (fn [local] (-> local - (update :selected-files - #(if (contains? % file-id) - (disj % file-id) - (conj % file-id))) - (assoc :selected-project - (:project-id file))))) + (update :selected-files #(if (contains? % id) + (disj % id) + (conj % id))) + (assoc :selected-project project-id)))) state))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Modification ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; --- Create Project +;; --- EVENT: create-team + +(defn team-created + [team] + (ptk/reify ::team-created + IDeref + (-deref [_] team))) (defn create-team [{:keys [name] :as params}] @@ -252,11 +296,14 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params)] + on-error rx/throw}} (meta params)] (->> (rp/mutation! :create-team {:name name}) (rx/tap on-success) + (rx/map team-created) (rx/catch on-error)))))) +;; --- EVENT: update-team + (defn update-team [{:keys [id name] :as params}] (us/assert ::team params) @@ -271,75 +318,75 @@ (rx/ignore))))) (defn update-team-photo - [{:keys [file team-id] :as params}] + [{:keys [file] :as params}] (us/assert ::di/file file) - (us/assert ::us/uuid team-id) (ptk/reify ::update-team-photo ptk/WatchEvent (watch [_ state stream] (let [on-success di/notify-finished-loading - - on-error #(do (di/notify-finished-loading) - (di/process-error %)) - - prepare #(hash-map :file % :team-id team-id)] + on-error #(do (di/notify-finished-loading) + (di/process-error %)) + team-id (:current-team-id state) + prepare #(hash-map :file % :team-id team-id)] (di/notify-start-loading) - (->> (rx/of file) (rx/map di/validate-file) (rx/map prepare) (rx/mapcat #(rp/mutation :update-team-photo %)) (rx/do on-success) - (rx/map #(fetch-team %)) + (rx/map du/fetch-teams) (rx/catch on-error)))))) (defn update-team-member-role - [{:keys [team-id role member-id] :as params}] - (us/assert ::us/uuid team-id) + [{:keys [role member-id] :as params}] (us/assert ::us/uuid member-id) (us/assert ::us/keyword role) (ptk/reify ::update-team-member-role ptk/WatchEvent (watch [_ state stream] - (->> (rp/mutation! :update-team-member-role params) - (rx/mapcat #(rx/of (fetch-team-members {:id team-id}) - (fetch-team {:id team-id}))))))) + (let [team-id (:current-team-id state) + params (assoc params :team-id team-id)] + (->> (rp/mutation! :update-team-member-role params) + (rx/mapcat (fn [_] + (rx/of (fetch-team-members) + (du/fetch-teams))))))))) (defn delete-team-member - [{:keys [team-id member-id] :as params}] - (us/assert ::us/uuid team-id) + [{:keys [member-id] :as params}] (us/assert ::us/uuid member-id) (ptk/reify ::delete-team-member ptk/WatchEvent (watch [_ state stream] - (->> (rp/mutation! :delete-team-member params) - (rx/mapcat #(rx/of (fetch-team-members {:id team-id}) - (fetch-team {:id team-id}))))))) + (let [team-id (:current-team-id state) + params (assoc params :team-id team-id)] + (->> (rp/mutation! :delete-team-member params) + (rx/mapcat (fn [_] + (rx/of (fetch-team-members) + (du/fetch-teams))))))))) (defn leave-team - [{:keys [id reassign-to] :as params}] - (us/assert ::team params) + [{:keys [reassign-to] :as params}] (us/assert (s/nilable ::us/uuid) reassign-to) (ptk/reify ::leave-team ptk/WatchEvent (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params)] + on-error rx/throw}} (meta params) + team-id (:current-team-id state)] (rx/concat (when (uuid? reassign-to) - (->> (rp/mutation! :update-team-member-role {:team-id id + (->> (rp/mutation! :update-team-member-role {:team-id team-id :role :owner :member-id reassign-to}) (rx/ignore))) - (->> (rp/mutation! :leave-team {:id id}) + (->> (rp/mutation! :leave-team {:id team-id}) (rx/tap on-success) (rx/catch on-error))))))) (defn invite-team-member - [{:keys [team-id email role] :as params}] - (us/assert ::us/uuid team-id) + [{:keys [email role] :as params}] (us/assert ::us/email email) (us/assert ::us/keyword role) (ptk/reify ::invite-team-member @@ -347,11 +394,15 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params)] + on-error rx/throw}} (meta params) + team-id (:current-team-id state) + params (assoc params :team-id team-id)] (->> (rp/mutation! :invite-team-member params) (rx/tap on-success) (rx/catch on-error)))))) +;; --- EVENT: delete-team + (defn delete-team [{:keys [id] :as params}] (us/assert ::team params) @@ -360,50 +411,68 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params)] + on-error rx/throw}} (meta params)] (->> (rp/mutation! :delete-team {:id id}) (rx/tap on-success) (rx/catch on-error)))))) +;; --- EVENT: create-project + +(defn- project-created + [{:keys [id] :as project}] + (ptk/reify ::project-created + IDeref + (-deref [_] project) + + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:dashboard-projects id] project) + (assoc-in [:dashboard-local :project-for-edit] id))))) + (defn create-project - [{:keys [team-id] :as params}] - (us/assert ::us/uuid team-id) - (letfn [(created [project state] - (-> state - (assoc-in [:projects team-id (:id project)] project) - (assoc-in [:dashboard-local :project-for-edit] (:id project))))] - (ptk/reify ::create-project - ptk/WatchEvent - (watch [_ state stream] - (let [name (name (gensym "New Project ")) - {:keys [on-success on-error] - :or {on-success identity - on-error identity}} (meta params)] - (->> (rp/mutation! :create-project {:name name :team-id team-id}) - (rx/tap on-success) - (rx/map #(partial created %)) - (rx/catch on-error))))))) + [] + (ptk/reify ::create-project + ptk/WatchEvent + (watch [_ state stream] + (let [name (name (gensym (str (tr "dashboard.new-project-prefix") " "))) + team-id (:current-team-id state) + params {:name name + :team-id team-id} + {:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params)] + (->> (rp/mutation! :create-project params) + (rx/tap on-success) + (rx/map project-created) + (rx/catch on-error)))))) + +;; --- EVENT: duplicate-project + +(defn project-duplicated + [{:keys [id] :as project}] + (ptk/reify ::project-duplicated + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:dashboard-projects id] project)))) (defn duplicate-project [{:keys [id name] :as params}] (us/assert ::us/uuid id) - (letfn [(duplicated [project state] - (-> state - (assoc-in [:projects (:team-id project) (:id project)] project)))] - (ptk/reify ::duplicate-project - ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [on-success on-error] - :or {on-success identity - on-error identity}} (meta params) + (ptk/reify ::duplicate-project + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params) - new-name (str name " " (tr "dashboard.copy-suffix"))] + new-name (str name " " (tr "dashboard.copy-suffix"))] - (->> (rp/mutation! :duplicate-project {:project-id id - :name new-name}) - (rx/tap on-success) - (rx/map #(partial duplicated %)) - (rx/catch on-error))))))) + (->> (rp/mutation! :duplicate-project {:project-id id + :name new-name}) + (rx/tap on-success) + (rx/map project-duplicated) + (rx/catch on-error)))))) (defn move-project [{:keys [id team-id] :as params}] @@ -414,7 +483,7 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params)] + on-error rx/throw}} (meta params)] (->> (rp/mutation! :move-project {:project-id id :team-id team-id}) @@ -422,21 +491,21 @@ (rx/catch on-error)))))) (defn toggle-project-pin - [{:keys [id is-pinned team-id] :as params}] - (us/assert ::project params) + [{:keys [id is-pinned team-id] :as project}] + (us/assert ::project project) (ptk/reify ::toggle-project-pin ptk/UpdateEvent (update [_ state] - (assoc-in state [:projects team-id id :is-pinned] (not is-pinned))) + (assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned))) ptk/WatchEvent (watch [_ state stream] - (let [project (get-in state [:projects team-id id]) + (let [project (get-in state [:dashboard-projects id]) params (select-keys project [:id :is-pinned :team-id])] (->> (rp/mutation :update-project-pin params) (rx/ignore)))))) -;; --- Rename Project +;; --- EVENT: rename-project (defn rename-project [{:keys [id name team-id] :as params}] @@ -445,7 +514,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (assoc-in [:projects team-id id :name] name) + (update-in [:dashboard-projects id :name] (constantly name)) (update :dashboard-local dissoc :project-for-edit))) ptk/WatchEvent @@ -454,7 +523,7 @@ (->> (rp/mutation :rename-project params) (rx/ignore)))))) -;; --- Delete Project (by id) +;; --- EVENT: delete-project (defn delete-project [{:keys [id team-id] :as params}] @@ -462,16 +531,21 @@ (ptk/reify ::delete-project ptk/UpdateEvent (update [_ state] - (update-in state [:projects team-id] dissoc id)) + (update state :dashboard-projects dissoc id)) ptk/WatchEvent (watch [_ state s] (->> (rp/mutation :delete-project {:id id}) (rx/ignore))))) -;; --- Delete File (by id) +;; --- EVENT: delete-file -(declare delete-file-result) +(defn file-deleted + [team-id project-id] + (ptk/reify ::file-deleted + ptk/UpdateEvent + (update [_ state] + (update-in state [:dashboard-projects project-id :count] dec)))) (defn delete-file [{:keys [id project-id] :as params}] @@ -480,33 +554,26 @@ ptk/UpdateEvent (update [_ state] (-> state - (update-in [:files project-id] dissoc id) - (update-in [:recent-files project-id] (fnil disj #{}) id))) + (d/update-when :dashboard-files dissoc id) + (d/update-when :dashboard-recent-files dissoc id))) ptk/WatchEvent (watch [_ state s] (let [team-id (uuid/uuid (get-in state [:route :path-params :team-id]))] (->> (rp/mutation :delete-file {:id id}) - (rx/map #(delete-file-result team-id project-id))))))) - -(defn delete-file-result - [team-id project-id] - - (ptk/reify ::delete-file - ptk/UpdateEvent - (update [_ state] - (-> state - (update-in [:projects team-id project-id :count] dec))))) + (rx/map #(file-deleted team-id project-id))))))) ;; --- Rename File (defn rename-file - [{:keys [id name project-id] :as params}] + [{:keys [id name] :as params}] (us/assert ::file params) (ptk/reify ::rename-file ptk/UpdateEvent (update [_ state] - (assoc-in state [:files project-id id :name] name)) + (-> state + (d/update-in-when [:dashboard-files id :name] (constantly name)) + (d/update-in-when [:dashboard-recent-files id :name] (constantly name)))) ptk/WatchEvent (watch [_ state stream] @@ -517,12 +584,14 @@ ;; --- Set File shared (defn set-file-shared - [{:keys [id project-id is-shared] :as params}] + [{:keys [id is-shared] :as params}] (us/assert ::file params) (ptk/reify ::set-file-shared ptk/UpdateEvent (update [_ state] - (assoc-in state [:files project-id id :is-shared] is-shared)) + (-> state + (d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared)) + (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared)))) ptk/WatchEvent (watch [_ state stream] @@ -530,10 +599,23 @@ (->> (rp/mutation :set-file-shared params) (rx/ignore)))))) -;; --- Create File +;; --- EVENT: create-file (declare file-created) +(defn file-created + [{:keys [id] :as file}] + (us/verify ::file file) + (ptk/reify ::file-created + IDeref + (-deref [_] file) + + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:dashboard-files id] file) + (assoc-in [:dashboard-recent-files id] file))))) + (defn create-file [{:keys [project-id] :as params}] (us/assert ::us/uuid project-id) @@ -542,9 +624,9 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params) + on-error rx/throw}} (meta params) - name (name (gensym "New File ")) + name (name (gensym (str (tr "dashboard.new-file-prefix") " "))) params (assoc params :name name)] (->> (rp/mutation! :create-file params) @@ -552,17 +634,7 @@ (rx/map file-created) (rx/catch on-error)))))) -(defn file-created - [{:keys [project-id id] :as file}] - (us/verify ::file file) - (ptk/reify ::file-created - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:files project-id id] file) - (update-in [:recent-files project-id] (fnil conj #{}) id))))) - -;; --- Duplicate File +;; --- EVENT: duplicate-file (defn duplicate-file [{:keys [id name] :as params}] @@ -573,7 +645,7 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params) + on-error rx/throw}} (meta params) new-name (str name " " (tr "dashboard.copy-suffix"))] @@ -583,7 +655,7 @@ (rx/map file-created) (rx/catch on-error)))))) -;; --- Move File +;; --- EVENT: move-files (defn move-files [{:keys [ids project-id] :as params}] @@ -594,10 +666,76 @@ (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity - on-error identity}} (meta params)] + on-error rx/throw}} (meta params)] - (->> (rp/mutation! :move-files {:ids ids - :project-id project-id}) + (->> (rp/mutation! :move-files {:ids ids :project-id project-id}) (rx/tap on-success) (rx/catch on-error)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Navigation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn go-to-workspace + [{:keys [id project-id] :as file}] + (us/assert ::file file) + (ptk/reify ::go-to-workspace + ptk/WatchEvent + (watch [_ state stream] + (let [pparams {:project-id project-id :file-id id}] + (rx/of (rt/nav :workspace pparams)))))) + + +(defn go-to-files + [project-id] + (ptk/reify ::go-to-files + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (rx/of (rt/nav :dashboard-files {:team-id team-id + :project-id project-id})))))) + +(defn go-to-search + ([] (go-to-search nil)) + ([term] + (ptk/reify ::go-to-search + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (if (empty? term) + (rx/of (rt/nav :dashboard-search + {:team-id team-id})) + (rx/of (rt/nav :dashboard-search + {:team-id team-id} + {:search-term term})))))))) + +(defn go-to-projects + ([] + (ptk/reify ::go-to-projects + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (rx/of (rt/nav :dashboard-projects {:team-id team-id})))))) + ([team-id] + (ptk/reify ::go-to-projects + ptk/WatchEvent + (watch [_ state stream] + (du/set-current-team! team-id) + (rx/of (rt/nav :dashboard-projects {:team-id team-id})))))) + +(defn go-to-team-members + [] + (ptk/reify ::go-to-team-members + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (rx/of (rt/nav :dashboard-team-members {:team-id team-id})))))) + +(defn go-to-team-settings + [] + (ptk/reify ::go-to-team-settings + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (rx/of (rt/nav :dashboard-team-settings {:team-id team-id})))))) diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs new file mode 100644 index 000000000..ca4b99a61 --- /dev/null +++ b/frontend/src/app/main/data/events.cljs @@ -0,0 +1,247 @@ +;; 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.events + (:require + ["ua-parser-js" :as UAParser] + [app.common.data :as d] + [app.main.repo :as rp] + [app.config :as cf] + [app.util.globals :as g] + [app.util.http :as http] + [app.util.object :as obj] + [app.util.storage :refer [storage]] + [app.util.time :as dt] + [app.util.i18n :as i18n] + [beicon.core :as rx] + [lambdaisland.uri :as u] + [potok.core :as ptk])) + +;; Defines the maximum buffer size, after events start discarding. +(def max-buffer-size 1024) + +;; Defines the maximum number of events that can go in a single batch. +(def max-chunk-size 100) + +;; Defines the time window within events belong to the same session. +(def session-timeout + (dt/duration {:minutes 30})) + +;; --- CONTEXT + +(defn- collect-context + [] + (let [uagent (UAParser.)] + (d/merge + {:app-version (:full @cf/version) + :locale @i18n/locale} + (let [browser (.getBrowser uagent)] + {:browser (obj/get browser "name") + :browser-version (obj/get browser "version")}) + (let [engine (.getEngine uagent)] + {:engine (obj/get engine "name") + :engine-version (obj/get engine "version")}) + (let [os (.getOS uagent) + name (obj/get os "name") + version (obj/get os "version")] + {:os (str name " " version) + :os-version version}) + (let [device (.getDevice uagent)] + (if-let [type (obj/get device "type")] + {:device-type type + :device-vendor (obj/get device "vendor") + :device-model (obj/get device "model")} + {:device-type "unknown"})) + (let [screen (obj/get g/window "screen") + orientation (obj/get screen "orientation")] + {:screen-width (obj/get screen "width") + :screen-height (obj/get screen "height") + :screen-color-depth (obj/get screen "colorDepth") + :screen-orientation (obj/get orientation "type")}) + (let [cpu (.getCPU uagent)] + {:device-arch (obj/get cpu "architecture")})))) + +(def context + (atom (d/without-nils (collect-context)))) + +(add-watch i18n/locale ::events #(swap! context assoc :locale %4)) + +;; --- EVENT TRANSLATION + +(defmulti ^:private process-event ptk/type) +(defmethod process-event :default [_] nil) + +(defmethod process-event ::event + [event] + (let [data (deref event)] + (when (::name data) + (d/without-nils + {:type (::type data "action") + :name (::name data) + :context (::context data) + :props (dissoc data ::name ::type ::context)})))) + +(defmethod process-event :app.util.router/navigated + [event] + (let [match (deref event) + route (get-in match [:data :name]) + props {:route (name route) + :team-id (get-in match [:path-params :team-id]) + :file-id (get-in match [:path-params :file-id]) + :project-id (get-in match [:path-params :project-id])}] + {:name "navigate" + :type "action" + :timestamp (dt/now) + :props (d/without-nils props)})) + +(defmethod process-event :app.main.data.users/logged-in + [event] + (let [data (deref event) + mdata (meta data) + props {:signin-source (::source mdata) + :email (:email data) + :auth-backend (:auth-backend data) + :fullname (:fullname data) + :is-muted (:is-muted data) + :default-team-id (str (:default-team-id data)) + :default-project-id (str (:default-project-id data))}] + {:name "signin" + :type "identify" + :profile-id (:id data) + :props (d/without-nils props)})) + +(defmethod process-event :app.main.data.dashboard/project-created + [event] + (let [data (deref event)] + {:type "action" + :name "create-project" + :props {:id (:id data) + :team-id (:team-id data)}})) + +(defmethod process-event :app.main.data.dashboard/file-created + [event] + (let [data (deref event)] + {:type "action" + :name "create-file" + :props {:id (:id data) + :project-id (:project-id data)}})) + +(defmethod process-event :app.main.data.workspace/create-page + [event] + (let [data (deref event)] + {:type "action" + :name "create-page" + :props {:id (:id data) + :file-id (:file-id data) + :project-id (:project-id data)}})) + +(defn- event->generic-action + [event name] + {:type "action" + :name name + :props {}}) + +(defmethod process-event :app.main.data.users/logout + [event] + (event->generic-action event "signout")) + + +;; --- MAIN LOOP + +(defn- append-to-buffer + [buffer item] + (if (>= (count buffer) max-buffer-size) + buffer + (conj buffer item))) + +(defn- remove-from-buffer + [buffer items] + (into #queue [] (drop items) buffer)) + +(defn- persist-events + [events] + (if (seq events) + (let [uri (u/join cf/public-uri "events") + params {:events events}] + (->> (http/send! {:uri uri + :method :post + :body (http/transit-data params)}) + (rx/mapcat rp/handle-response))) + (rx/of nil))) + +(defmethod ptk/resolve ::persistence + [_ {:keys [buffer] :as params}] + (ptk/reify ::persistence + ptk/EffectEvent + (effect [_ state stream] + (let [profile-id (:profile-id state) + events (into [] (take max-buffer-size) @buffer)] + (when (seq events) + (->> events + (filterv #(= profile-id (:profile-id %))) + (persist-events) + (rx/subs (fn [_] + (swap! buffer remove-from-buffer (count events)))))))))) + +(defn initialize + [] + (let [buffer (atom #queue [])] + (ptk/reify ::initialize + ptk/WatchEvent + (watch [_ state stream] + (->> (rx/merge + (->> (rx/from-atom buffer) + (rx/filter #(pos? (count %))) + (rx/debounce 2000)) + (->> stream + (rx/filter (ptk/type? :app.main.data.users/logout)) + (rx/observe-on :async))) + (rx/map #(ptk/event ::persistence {:buffer buffer})))) + + ptk/EffectEvent + (effect [_ state stream] + (let [events (methods process-event) + session (atom nil) + + profile (->> (rx/from-atom storage {:emit-current-value? true}) + (rx/map :profile) + (rx/map :id) + (rx/dedupe)) + + source (->> stream + (rx/with-latest-from profile) + (rx/map (fn [result] + (let [event (aget result 0) + profile-id (aget result 1) + type (ptk/type event) + impl-fn (get events type)] + (when (fn? impl-fn) + (some-> (impl-fn event) + (update :profile-id #(or % profile-id))))))) + (rx/filter :profile-id) + (rx/map (fn [event] + (let [session* (or @session (dt/now)) + context (-> @context + (d/merge (:context event)) + (assoc :session session*))] + (reset! session session*) + (-> event + (assoc :timestamp (dt/now)) + (assoc :context context))))) + (rx/share))] + (->> source + (rx/switch-map #(rx/timer (inst-ms session-timeout))) + (rx/subs #(reset! session nil))) + + (->> source + (rx/subs (fn [event] + (swap! buffer append-to-buffer event))))))))) + +(defmethod ptk/resolve ::initialize + [_ params] + (if cf/analytics + (initialize) + (ptk/data-event ::initialize params))) diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs new file mode 100644 index 000000000..899285654 --- /dev/null +++ b/frontend/src/app/main/data/fonts.cljs @@ -0,0 +1,244 @@ +;; 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.fonts + (:require + ["opentype.js" :as ot] + [app.common.data :as d] + [app.common.spec :as us] + [app.common.media :as cm] + [app.common.uuid :as uuid] + [app.main.fonts :as fonts] + [app.main.repo :as rp] + [app.util.i18n :as i18n :refer [tr]] + [app.util.logging :as log] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [app.util.webapi :as wa] + [cuerdas.core :as str] + [potok.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; General purpose events & IMPL +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn team-fonts-loaded + [fonts] + (letfn [;; Prepare font to the internal font database format. + (prepare-font [[id [item :as items]]] + {:id id + :name (:font-family item) + :family (:font-family item) + :variants (->> items + (map prepare-font-variant) + (sort-by variant-sort-fn) + (vec))}) + + (variant-sort-fn [item] + [(:weight item) + (if (= "normal" (:style item)) 1 2)]) + + (prepare-font-variant [item] + {:id (str (:font-style item) "-" (:font-weight item)) + :name (str (cm/font-weight->name (:font-weight item)) + (when (not= "normal" (:font-style item)) + (str " " (str/capital (:font-style item))))) + :style (:font-style item) + :weight (str (:font-weight item)) + ::fonts/woff1-file-id (:woff1-file-id item) + ::fonts/woff2-file-id (:woff2-file-id item) + ::fonts/ttf-file-id (:ttf-file-id item) + ::fonts/otf-file-id (:otf-file-id item)}) + + (adapt-font-id [variant] + (update variant :font-id #(str "custom-" %)))] + + (ptk/reify ::team-fonts-loaded + ptk/UpdateEvent + (update [_ state] + (assoc state :dashboard-fonts (d/index-by :id fonts))) + + ptk/EffectEvent + (effect [_ state stream] + (let [fonts (->> fonts + (map adapt-font-id) + (group-by :font-id) + (mapv prepare-font))] + (fonts/register! :custom fonts)))))) + +(defn load-team-fonts + [team-id] + (ptk/reify ::load-team-fonts + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :team-font-variants {:team-id team-id}) + (rx/map team-fonts-loaded))))) + +(defn process-upload + "Given a seq of blobs and the team id, creates a ready-to-use fonts + map with temporal ID's associated to each font entry." + [blobs team-id] + (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-family family + :font-weight (cm/parse-font-weight variant) + :font-style (cm/parse-font-style variant)})) + + (join [res {:keys [content] :as font}] + (let [key-fn (juxt :font-family :font-weight :font-style) + existing (d/seek #(= (key-fn font) (key-fn %)) (vals res))] + (if existing + (update res + (:id existing) + (fn [existing] + (-> existing + (update :data assoc (:type content) (:data content)) + (update :names conj (:name content))))) + (let [tmp-id (uuid/next)] + (assoc res tmp-id + (-> font + (assoc :id tmp-id) + (assoc :team-id team-id) + (assoc :names #{(:name content)}) + (assoc :data {(:type content) + (:data content)}) + (dissoc :content))))))) + + (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) + (rx/reduce join {})))) + +(defn- calculate-family-to-id-mapping + [existing] + (reduce #(assoc %1 (:font-family %2) (:font-id %2)) {} (vals existing))) + +(defn merge-and-group-fonts + "Function responsible to merge (and apropriatelly group) incoming + fonts (processed by `process-upload`) into existing fonts + in local state, preserving correct font-id references." + [current-fonts installed-fonts incoming-fonts] + (loop [famdb (-> (merge current-fonts installed-fonts) + (calculate-family-to-id-mapping)) + items (vals incoming-fonts) + result current-fonts] + (if-let [{:keys [id font-family] :as item} (first items)] + (let [font-id (or (get famdb font-family) + (uuid/next)) + font (assoc item :font-id font-id)] + (recur (assoc famdb font-family font-id) + (rest items) + (assoc result id font))) + result))) + +(defn rename-and-regroup + "Function responsible to rename a font in a local state and properly + regroup it to the apropriate `font-id` having in account current + fonts and installed fonts." + [current-fonts id name installed-fonts] + (let [famdb (-> (merge current-fonts installed-fonts) + (calculate-family-to-id-mapping)) + font-id (or (get famdb name) + (uuid/next))] + (update current-fonts id (fn [font] + (-> font + (assoc :name name) + (assoc :font-id font-id)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Dashboard related events +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(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 name] :as params}] + (us/assert ::us/uuid id) + (us/assert ::us/not-empty-string name) + (ptk/reify ::update-font + ptk/UpdateEvent + (update [_ state] + ;; Update all variants that has the same font-id with the new + ;; name in the local state. + (update state :dashboard-fonts + (fn [fonts] + (d/mapm (fn [_ font] + (cond-> font + (= id (:font-id font)) + (assoc :font-family name))) + fonts)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (->> (rp/mutation! :update-font {:id id :name name :team-id team-id}) + (rx/ignore)))))) + +(defn delete-font + "Delete all variants related to the provided `font-id`." + [font-id] + (us/assert ::us/uuid font-id) + (ptk/reify ::delete-font + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-fonts + (fn [variants] + (d/removem (fn [[id variant]] + (= (:font-id variant) font-id)) variants)))) + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (->> (rp/mutation! :delete-font {:id font-id :team-id team-id}) + (rx/ignore)))))) + +(defn delete-font-variant + [id] + (us/assert ::us/uuid id) + (ptk/reify ::delete-font-variants + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-fonts + (fn [variants] + (d/removem (fn [[_ variant]] + (= (:id variant) id)) + variants)))) + ptk/WatchEvent + (watch [_ state stream] + (let [team-id (:current-team-id state)] + (->> (rp/mutation! :delete-font-variant {:id id :team-id team-id}) + (rx/ignore)))))) diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs index 27794fd77..695807cc2 100644 --- a/frontend/src/app/main/data/media.cljs +++ b/frontend/src/app/main/data/media.cljs @@ -51,7 +51,7 @@ (ex/raise :type :validation :code :media-too-large :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 :code :media-type-not-allowed :hint (str/fmt "media type %s is not supported" (.-type file)))) diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 850380517..e3eb7810d 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -5,14 +5,22 @@ ;; Copyright (c) UXBOX Labs SL (ns app.main.data.shortcuts + (:refer-clojure :exclude [meta reset!]) (:require ["mousetrap" :as mousetrap] + [app.common.data :as d] + [app.common.spec :as us] [app.config :as cfg] - [app.util.logging :as log]) - (:refer-clojure :exclude [meta])) + [app.util.logging :as log] + [cljs.spec.alpha :as s] + [potok.core :as ptk])) (log/set-level! :warn) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (def mac-command "\u2318") (def mac-option "\u2325") (def mac-delete "\u232B") @@ -44,30 +52,8 @@ [shortcut] (c-mod (a-mod shortcut))) -(defn bind-shortcuts - ([shortcuts-config] - (bind-shortcuts - shortcuts-config - mousetrap/bind - (fn [key cb] - (fn [event] - (log/debug :msg (str "Shortcut" key)) - (.preventDefault event) - (cb event))))) - - ([shortcuts-config bind-fn cb-fn] - (doseq [[key {:keys [command disabled fn type]}] shortcuts-config] - (when-not disabled - (if (vector? command) - (doseq [cmd (seq command)] - (bind-fn cmd (cb-fn key fn) type)) - (bind-fn command (cb-fn key fn) type)))))) - -(defn remove-shortcuts - [] - (mousetrap/reset)) - -(defn meta [key] +(defn meta + [key] ;; If the key is "+" we need to surround with quotes ;; otherwise will not be very readable (let [key (if (and (not (cfg/check-platform? :macos)) @@ -80,37 +66,120 @@ "Ctrl+") key))) -(defn shift [key] +(defn shift + [key] (str (if (cfg/check-platform? :macos) mac-shift "Shift+") key)) -(defn alt [key] +(defn alt + [key] (str (if (cfg/check-platform? :macos) mac-option "Alt+") key)) -(defn meta-shift [key] +(defn meta-shift + [key] (-> key meta shift)) -(defn meta-alt [key] +(defn meta-alt + [key] (-> key meta alt)) -(defn supr [] +(defn supr + [] (if (cfg/check-platform? :macos) mac-delete "Supr")) -(defn esc [] +(defn esc + [] (if (cfg/check-platform? :macos) mac-esc "Escape")) -(defn enter [] +(defn enter + [] (if (cfg/check-platform? :macos) mac-enter "Enter")) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Events +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- EVENT: push + +(s/def ::tooltip ::us/string) +(s/def ::fn fn?) + +(s/def ::command + (s/or :str ::us/string + :vec vector?)) + +(s/def ::shortcut + (s/keys :req-un [::command] + :opt-un [::fn + ::tooltip])) + +(s/def ::shortcuts + (s/map-of ::us/keyword + ::shortcut)) + +(defn- wrap-cb + [key cb] + (fn [event] + (log/debug :msg (str "Shortcut" key)) + (.preventDefault event) + (cb event))) + +(defn- bind! + [shortcuts] + (->> shortcuts + (remove #(:disabled (second %))) + (run! (fn [[key {:keys [command fn type]}]] + (if (vector? command) + (run! #(mousetrap/bind % (wrap-cb key fn) type) command) + (mousetrap/bind command (wrap-cb key fn) type)))))) + +(defn- reset! + ([] + (mousetrap/reset)) + ([shortcuts] + (mousetrap/reset) + (bind! shortcuts))) + +(defn push-shortcuts + [key shortcuts] + (us/assert ::us/keyword key) + (us/assert ::shortcuts shortcuts) + (ptk/reify ::push-shortcuts + ptk/UpdateEvent + (update [_ state] + (-> state + (update :shortcuts (fnil conj '()) [key shortcuts]))) + + ptk/EffectEvent + (effect [_ state stream] + (let [[key shortcuts] (peek (:shortcuts state))] + (reset! shortcuts))))) + +(defn pop-shortcuts + [key] + (ptk/reify ::pop-shortcuts + ptk/UpdateEvent + (update [_ state] + (update state :shortcuts (fn [shortcuts] + (let [current-key (first (peek shortcuts))] + (if (= key current-key) + (pop shortcuts) + shortcuts))))) + ptk/EffectEvent + (effect [_ state stream] + (let [[key* shortcuts] (peek (:shortcuts state))] + (when (not= key key*) + (reset! shortcuts)))))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 283a9bad9..906df6b8d 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -10,7 +10,9 @@ [app.common.data :as d] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.data.media :as di] + [app.main.data.modal :as modal] [app.main.data.messages :as dm] [app.main.repo :as rp] [app.main.store :as st] @@ -24,7 +26,7 @@ [cuerdas.core :as str] [potok.core :as ptk])) -;; --- Common Specs +;; --- COMMON SPECS (s/def ::id ::us/uuid) (s/def ::fullname ::us/string) @@ -45,39 +47,61 @@ ::lang ::theme])) +;; --- HELPERS + +(defn get-current-team-id + [profile] + (let [team-id (::current-team-id @storage)] + (or team-id (:default-team-id profile)))) + +(defn set-current-team! + [team-id] + (swap! storage assoc ::current-team-id team-id)) + +;; --- EVENT: fetch-teams + +(defn teams-fetched + [teams] + (let [teams (d/index-by :id teams)] + (ptk/reify ::teams-fetched + IDeref + (-deref [_] teams) + + ptk/UpdateEvent + (update [_ state] + (assoc state :teams teams))))) + (defn fetch-teams [] - (letfn [(on-fetched [state data] - (let [teams (d/index-by :id data)] - (assoc state :teams teams)))] - (ptk/reify ::fetch-teams - ptk/WatchEvent - (watch [_ state s] - (->> (rp/query! :teams) - (rx/map (fn [data] #(on-fetched % data)))))))) + (ptk/reify ::fetch-teams + ptk/WatchEvent + (watch [_ state s] + (->> (rp/query! :teams) + (rx/map teams-fetched))))) + +;; --- EVENT: fetch-profile (defn profile-fetched - [{:keys [fullname id] :as data}] - (us/verify ::profile data) + [{:keys [id] :as profile}] + (us/verify ::profile profile) (ptk/reify ::profile-fetched IDeref - (-deref [_] data) + (-deref [_] profile) ptk/UpdateEvent (update [_ state] (-> state (assoc :profile-id id) - (assoc :profile data))) + (assoc :profile profile))) ptk/EffectEvent (effect [_ state stream] (let [profile (:profile state)] - (swap! storage assoc :profile profile) - (i18n/set-locale! (:lang profile)) - (some-> (:theme profile) - (theme/set-current-theme!)))))) - -;; --- Fetch Profile + (when (not= uuid/zero (:id profile)) + (swap! storage assoc :profile profile) + (i18n/set-locale! (:lang profile)) + (some-> (:theme profile) + (theme/set-current-theme!))))))) (defn fetch-profile [] @@ -87,12 +111,14 @@ (->> (rp/query! :profile) (rx/map profile-fetched))))) -(defn fetch-profile-and-teams +;; --- EVENT: INITIALIZE PROFILE + +(defn initialize-profile "Event used mainly on application bootstrap; it fetches the profile and if and only if the fetched profile corresponds to an authenticated user; proceed to fetch teams." [] - (ptk/reify ::fetch-profile-and-teams + (ptk/reify ::initialize-profile ptk/WatchEvent (watch [_ state stream] (rx/merge @@ -106,6 +132,115 @@ (rx/empty) (rx/of (fetch-teams)))))))))) +;; --- EVENT: login + +(defn- logged-in + [profile] + (ptk/reify ::logged-in + IDeref + (-deref [_] profile) + + ptk/WatchEvent + (watch [this state stream] + (let [team-id (get-current-team-id profile) + profile (with-meta profile + {::ev/source "login"})] + (->> (rx/concat + (rx/of (profile-fetched profile) + (fetch-teams)) + + (->> (rx/of (rt/nav' :dashboard-projects {:team-id team-id})) + (rx/delay 1000)) + + (when-not (get-in profile [:props :onboarding-viewed]) + (->> (rx/of (modal/show {:type :onboarding})) + (rx/delay 1000)))) + + (rx/observe-on :async)))))) + +(s/def ::login-params + (s/keys :req-un [::email ::password])) + +(defn login + [{:keys [email password] :as data}] + (us/verify ::login-params data) + (ptk/reify ::login + ptk/WatchEvent + (watch [this state s] + (let [{:keys [on-error on-success] + :or {on-error rx/throw + on-success identity}} (meta data) + params {:email email + :password password + :scope "webapp"}] + (->> (rx/timer 100) + (rx/mapcat #(rp/mutation :login params)) + (rx/tap on-success) + (rx/catch on-error) + (rx/map (fn [profile] + (with-meta profile + {::ev/source "login"}))) + (rx/map logged-in)))))) + +(defn login-from-token + [{:keys [profile] :as tdata}] + (ptk/reify ::login-from-token + ptk/WatchEvent + (watch [this state s] + (rx/of (logged-in + (with-meta profile + {::ev/source "login-with-token"})))))) + +;; --- EVENT: logout + +(defn logged-out + [] + (ptk/reify ::logged-out + ptk/UpdateEvent + (update [_ state] + (select-keys state [:route :router :session-id :history])) + + ptk/WatchEvent + (watch [_ state s] + (rx/of (rt/nav :auth-login))) + + ptk/EffectEvent + (effect [_ state s] + (reset! storage {}) + (i18n/reset-locale)))) + +(defn logout + [] + (ptk/reify ::logout + ptk/WatchEvent + (watch [_ state s] + (->> (rp/mutation :logout) + (rx/delay-at-least 300) + (rx/catch (constantly (rx/of 1))) + (rx/map logged-out))))) + +;; --- EVENT: register + +(s/def ::invitation-token ::us/not-empty-string) + +(s/def ::register + (s/keys :req-un [::fullname ::password ::email] + :opt-un [::invitation-token])) + +(defn register + "Create a register event instance." + [data] + (s/assert ::register data) + (ptk/reify ::register + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [on-error on-success] + :or {on-error identity + on-success identity}} (meta data)] + (->> (rp/mutation :register-profile data) + (rx/tap on-success) + (rx/catch on-error)))))) + ;; --- Update Profile (defn update-profile @@ -231,3 +366,72 @@ (watch [_ state stream] (->> (rp/query :team-users {:team-id team-id}) (rx/map #(partial fetched %))))))) + +;; --- EVENT: request-account-deletion + +(defn request-account-deletion + [params] + (ptk/reify ::request-account-deletion + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [on-error on-success] + :or {on-error rx/throw + on-success identity}} (meta params)] + (->> (rp/mutation :delete-profile {}) + (rx/tap on-success) + (rx/delay-at-least 300) + (rx/catch (constantly (rx/of 1))) + (rx/map logged-out) + (rx/catch on-error)))))) + +;; --- EVENT: request-profile-recovery + +(s/def ::request-profile-recovery + (s/keys :req-un [::email])) + +(defn request-profile-recovery + [data] + (us/verify ::request-profile-recovery data) + (ptk/reify ::request-profile-recovery + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [on-error on-success] + :or {on-error rx/throw + on-success identity}} (meta data)] + + (->> (rp/mutation :request-profile-recovery data) + (rx/tap on-success) + (rx/catch on-error)))))) + +;; --- EVENT: recover-profile (Password) + +(s/def ::token string?) +(s/def ::recover-profile + (s/keys :req-un [::password ::token])) + +(defn recover-profile + [{:keys [token password] :as data}] + (us/verify ::recover-profile data) + (ptk/reify ::recover-profile + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [on-error on-success] + :or {on-error rx/throw + on-success identity}} (meta data)] + (->> (rp/mutation :recover-profile data) + (rx/tap on-success) + (rx/catch (fn [err] + (on-error) + (rx/empty)))))))) + +;; --- EVENT: crete-demo-profile + +(defn create-demo-profile + [] + (ptk/reify ::create-demo-profile + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :create-demo-profile {}) + (rx/map login))))) + + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 0546fe18b..5b70d9ce9 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -34,6 +34,7 @@ [app.main.data.workspace.undo :as dwu] [app.main.repo :as rp] [app.main.streams :as ms] + [app.main.worker :as uw] [app.util.http :as http] [app.util.i18n :as i18n] [app.util.logging :as log] @@ -72,6 +73,7 @@ :rules :display-grid :snap-grid + :scale-text :dynamic-alignment}) (s/def ::layout-flags (s/coll-of ::layout-flag)) @@ -132,7 +134,7 @@ (or layout default-layout)))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (if (and layout-name (contains? layout-names layout-name)) (rx/of (ensure-layout layout-name)) (rx/of (ensure-layout :layers)))))) @@ -151,51 +153,48 @@ :workspace-presence {})) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/merge (rx/of (dwp/fetch-bundle project-id file-id)) ;; Initialize notifications (websocket connection) and the file persistence (->> stream (rx/filter (ptk/type? ::dwp/bundle-fetched)) - (rx/first) - (rx/mapcat #(rx/of (dwn/initialize file-id) - (dwp/initialize-file-persistence file-id)))) - - ;; Initialize Indexes (webworker) - (->> stream - (rx/filter (ptk/type? ::dwp/bundle-fetched)) + (rx/take 1) (rx/map deref) - (rx/map dwc/initialize-indices) - (rx/first)) + (rx/mapcat (fn [{:keys [project] :as bundle}] + (rx/merge + (rx/of (dwn/initialize file-id) + (dwp/initialize-file-persistence file-id) + (dwc/initialize-indices bundle)) - ;; Mark file initialized when indexes are ready - (->> stream - (rx/filter #(= ::dwc/index-initialized %)) - (rx/first) - (rx/map (fn [] - (file-initialized project-id file-id)))) - - )))) + (->> stream + (rx/filter #(= ::dwc/index-initialized %)) + (rx/first) + (rx/map #(file-initialized bundle))))))))))) (defn- file-initialized - [project-id file-id] + [{:keys [file users project libraries] :as bundle}] (ptk/reify ::file-initialized ptk/UpdateEvent (update [_ state] - (update state :workspace-file - (fn [file] - (if (= (:id file) file-id) - (assoc file :initialized true) - file)))) + (assoc state + :current-team-id (:team-id project) + :users (d/index-by :id users) + :workspace-undo {} + :workspace-project project + :workspace-file (assoc file :initialized true) + :workspace-data (:data file) + :workspace-libraries (d/index-by :id libraries))) ptk/WatchEvent - (watch [_ state stream] - (let [ignore-until (get-in state [:workspace-file :ignore-sync-until]) + (watch [it state stream] + (let [file-id (:id file) + ignore-until (:ignore-sync-until file) needs-update? (some #(and (> (:modified-at %) (:synced-at %)) (or (not ignore-until) (> (:modified-at %) ignore-until))) - (vals (get state :workspace-libraries)))] + libraries)] (when needs-update? (rx/of (dwl/notify-sync-file file-id))))))) @@ -211,39 +210,43 @@ :workspace-persistence)) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/of (dwn/finalize file-id) ::dwp/finalize)))) +(declare go-to-page) (defn initialize-page [page-id] + (us/assert ::us/uuid page-id) (ptk/reify ::initialize-page ptk/UpdateEvent (update [_ state] (let [;; we maintain a cache of page state for user convenience ;; with the exception of the selection; when user abandon ;; the current page, the selection is lost - local (-> state - (get-in [:workspace-cache page-id] workspace-local-default) - (assoc :selected (d/ordered-set))) - page (-> (get-in state [:workspace-data :pages-index page-id]) - (select-keys [:id :name]))] - (assoc state - :current-page-id page-id ; mainly used by events - :trimmed-page page - :workspace-local local))))) + + page (get-in state [:workspace-data :pages-index page-id]) + local (-> state + (get-in [:workspace-cache page-id] workspace-local-default) + (assoc :selected (d/ordered-set)))] + (-> state + (assoc :current-page-id page-id) + (assoc :trimmed-page (select-keys page [:id :name])) + (assoc :workspace-local local) + (update-in [:route :params :query] assoc :page-id (str page-id))))))) (defn finalize-page [page-id] - (us/verify ::us/uuid page-id) + (us/assert ::us/uuid page-id) (ptk/reify ::finalize-page ptk/UpdateEvent (update [_ state] - (let [local (-> (:workspace-local state) - (dissoc :edition) - (dissoc :edit-path) - (dissoc :selected))] + (let [page-id (or page-id (get-in state [:workspace-data :pages 0])) + local (-> (:workspace-local state) + (dissoc :edition) + (dissoc :edit-path) + (dissoc :selected))] (-> state (assoc-in [:workspace-cache page-id] local) (dissoc :current-page-id :workspace-local :trimmed-page :workspace-drawing)))))) @@ -252,23 +255,31 @@ ;; Workspace Page CRUD ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def create-empty-page - (ptk/reify ::create-empty-page - ptk/WatchEvent - (watch [this state stream] - (let [id (uuid/next) - pages (get-in state [:workspace-data :pages-index]) - unames (dwc/retrieve-used-names pages) - name (dwc/generate-unique-name unames "Page") +(defn create-page + [{:keys [file-id]}] + (let [id (uuid/next)] + (ptk/reify ::create-page + IDeref + (-deref [_] + {:id id :file-id file-id}) - rchange {:type :add-page - :id id - :name name} - uchange {:type :del-page - :id id}] - (rx/of (dch/commit-changes [rchange] [uchange] {:commit-local? true})))))) + ptk/WatchEvent + (watch [it state stream] + (let [pages (get-in state [:workspace-data :pages-index]) + unames (dwc/retrieve-used-names pages) + name (dwc/generate-unique-name unames "Page") -(defn duplicate-page [page-id] + rchange {:type :add-page + :id id + :name name} + uchange {:type :del-page + :id id}] + (rx/of (dch/commit-changes {:redo-changes [rchange] + :undo-changes [uchange] + :origin it}))))))) + +(defn duplicate-page + [page-id] (ptk/reify ::duplicate-page ptk/WatchEvent (watch [this state stream] @@ -284,7 +295,9 @@ :page page} uchange {:type :del-page :id id}] - (rx/of (dch/commit-changes [rchange] [uchange] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchange] + :undo-changes [uchange] + :origin this})))))) (s/def ::rename-page (s/keys :req-un [::id ::name])) @@ -295,7 +308,7 @@ (us/verify string? name) (ptk/reify ::rename-page ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page (get-in state [:workspace-data :pages-index id]) rchg {:type :mod-page :id id @@ -303,7 +316,9 @@ uchg {:type :mod-page :id id :name (:name page)}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it})))))) (declare purge-page) (declare go-to-file) @@ -314,13 +329,15 @@ [id] (ptk/reify ::delete-page ptk/WatchEvent - (watch [_ state s] + (watch [it state stream] (let [page (get-in state [:workspace-data :pages-index id]) rchg {:type :del-page :id id} uchg {:type :add-page :page page}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it}) (when (= id (:current-page-id state)) go-to-file)))))) @@ -338,7 +355,7 @@ (assoc-in state [:workspace-file :name] name)) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [params {:id id :name name}] (->> (rp/mutation :rename-file params) (rx/ignore)))))) @@ -437,7 +454,7 @@ (defn start-panning [] (ptk/reify ::start-panning ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning))) zoom (-> (get-in state [:workspace-local :zoom]) gpt/point)] (when-not (get-in state [:workspace-local :panning]) @@ -535,7 +552,7 @@ ptk/UpdateEvent (update [_ state] (update state :workspace-local - #(impl-update-zoom % center (fn [z] (min (* z 1.1) 200))))))) + #(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))) (defn decrease-zoom [center] @@ -543,7 +560,7 @@ ptk/UpdateEvent (update [_ state] (update state :workspace-local - #(impl-update-zoom % center (fn [z] (max (* z 0.9) 0.01))))))) + #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))) (def reset-zoom (ptk/reify ::reset-zoom @@ -600,7 +617,7 @@ (us/verify ::shape-attrs attrs) (ptk/reify ::update-shape ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/of (dch/update-shapes [id] #(merge % attrs)))))) (defn start-rename-shape @@ -625,7 +642,7 @@ (us/verify ::shape-attrs attrs) (ptk/reify ::update-selected-shapes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [selected (wsh/lookup-selected state)] (rx/from (map #(update-shape % attrs) selected)))))) @@ -663,7 +680,7 @@ "Deselect all and remove all selected shapes." (ptk/reify ::delete-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [selected (wsh/lookup-selected state)] (rx/of (dwc/delete-shapes selected) (dws/deselect-all)))))) @@ -675,9 +692,9 @@ (defn vertical-order-selected [loc] (us/verify ::loc loc) - (ptk/reify ::vertical-order-selected-shpes + (ptk/reify ::vertical-order-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -709,7 +726,9 @@ :index (cp/position-on-parent id objects)})) selected)] ;; TODO: maybe missing the :reg-objects event? - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) ;; --- Change Shape Order (D&D Ordering) @@ -884,7 +903,7 @@ (ptk/reify ::relocate-shapes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -972,25 +991,30 @@ [[] [] []] ids) - [rchanges uchanges] (relocate-shapes-changes objects - parents - parent-id - page-id - to-index - ids - groups-to-delete - groups-to-unmask - shapes-to-detach - shapes-to-reroot - shapes-to-deroot)] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + [rchanges uchanges] + (relocate-shapes-changes objects + parents + parent-id + page-id + to-index + ids + groups-to-delete + groups-to-unmask + shapes-to-detach + shapes-to-reroot + shapes-to-deroot) + + ] + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/expand-collapse parent-id)))))) (defn relocate-selected-shapes [parent-id to-index] (ptk/reify ::relocate-selected-shapes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [selected (wsh/lookup-selected state)] (rx/of (relocate-shapes selected parent-id to-index)))))) @@ -999,7 +1023,7 @@ [] (ptk/reify ::start-editing-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [selected (wsh/lookup-selected state)] (if-not (= 1 (count selected)) (rx/empty) @@ -1027,7 +1051,7 @@ [id index] (ptk/reify ::relocate-pages ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [cidx (-> (get-in state [:workspace-data :pages]) (d/index-of id)) rchg {:type :mov-page @@ -1036,7 +1060,9 @@ uchg {:type :mov-page :id id :index cidx}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it})))))) ;; --- Shape / Selection Alignment and Distribution @@ -1048,7 +1074,7 @@ (us/verify ::gal/align-axis axis) (ptk/reify :align-objects ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -1079,7 +1105,7 @@ (us/verify ::gal/dist-axis axis) (ptk/reify :align-objects ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -1097,7 +1123,7 @@ [id lock] (ptk/reify ::set-shape-proportion-lock ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (letfn [(assign-proportions [shape] (if-not lock (assoc shape :proportion-lock false) @@ -1118,7 +1144,7 @@ (us/verify ::position position) (ptk/reify ::update-position ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) shape (get objects id) @@ -1140,12 +1166,16 @@ (s/assert ::shape-attrs flags) (ptk/reify ::update-shape-flags ptk/WatchEvent - (watch [_ state stream] - (letfn [(update-fn [obj] - (cond-> obj - (boolean? blocked) (assoc :blocked blocked) - (boolean? hidden) (assoc :hidden hidden)))] - (rx/of (dch/update-shapes-recursive [id] update-fn)))))) + (watch [it state stream] + (let [update-fn + (fn [obj] + (cond-> obj + (boolean? blocked) (assoc :blocked blocked) + (boolean? hidden) (assoc :hidden hidden))) + + objects (wsh/lookup-page-objects state) + ids (d/concat [id] (cp/get-children id objects))] + (rx/of (dch/update-shapes ids update-fn)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1156,29 +1186,41 @@ [project-id] (ptk/reify ::navigate-to-project ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-ids (get-in state [:projects project-id :pages]) params {:project project-id :page (first page-ids)}] (rx/of (rt/nav :workspace/page params)))))) (defn go-to-page - [page-id] - (us/verify ::us/uuid page-id) - (ptk/reify ::go-to-page - ptk/WatchEvent - (watch [_ state stream] - (let [project-id (get-in state [:workspace-project :id]) - file-id (get-in state [:workspace-file :id]) - pparams {:file-id file-id :project-id project-id} - qparams {:page-id page-id}] - (rx/of (rt/nav :workspace pparams qparams)))))) + ([] + + (ptk/reify ::go-to-page + ptk/WatchEvent + (watch [it state stream] + (let [project-id (:current-project-id state) + file-id (:current-file-id state) + page-id (get-in state [:workspace-data :pages 0]) + + pparams {:file-id file-id :project-id project-id} + qparams {:page-id page-id}] + (rx/of (rt/nav :workspace pparams qparams)))))) + ([page-id] + (us/verify ::us/uuid page-id) + (ptk/reify ::go-to-page + ptk/WatchEvent + (watch [it state stream] + (let [project-id (:current-project-id state) + file-id (:current-file-id state) + pparams {:file-id file-id :project-id project-id} + qparams {:page-id page-id}] + (rx/of (rt/nav :workspace pparams qparams))))))) (defn go-to-layout [layout] (us/verify ::layout-flag layout) (ptk/reify ::go-to-layout ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [project-id (get-in state [:workspace-project :id]) file-id (get-in state [:workspace-file :id]) page-id (get-in state [:current-page-id]) @@ -1189,7 +1231,7 @@ (def go-to-file (ptk/reify ::go-to-file ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [{:keys [id project-id data] :as file} (:workspace-file state) page-id (get-in data [:pages 0]) pparams {:project-id project-id :file-id id} @@ -1202,7 +1244,7 @@ ([{:keys [file-id page-id]}] (ptk/reify ::go-to-viewer ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [{:keys [current-file-id current-page-id]} state params {:file-id (or file-id current-file-id) :page-id (or page-id current-page-id)}] @@ -1214,11 +1256,20 @@ ([{:keys [team-id]}] (ptk/reify ::go-to-dashboard ptk/WatchEvent - (watch [_ state stream] - (let [team-id (or team-id (get-in state [:workspace-project :team-id]))] + (watch [it state stream] + (when-let [team-id (or team-id (:current-team-id state))] (rx/of ::dwp/force-persist (rt/nav :dashboard-projects {:team-id team-id}))))))) +(defn go-to-dashboard-fonts + [] + (ptk/reify ::go-to-dashboard + ptk/WatchEvent + (watch [it state stream] + (let [team-id (:current-team-id state)] + (rx/of ::dwp/force-persist + (rt/nav :dashboard-fonts {:team-id team-id})))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Context Menu ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1244,7 +1295,7 @@ (us/verify ::cp/minimal-shape shape) (ptk/reify ::show-shape-context-menu ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [selected (wsh/lookup-selected state)] (rx/concat (when-not (selected (:id shape)) @@ -1264,7 +1315,27 @@ (defn copy-selected [] - (letfn [;; Retrieve all ids of selected shapes with corresponding + (letfn [;; Sort objects so they have the same relative ordering + ;; when pasted later. + (sort-selected [state data] + (let [selected (:selected data) + page-id (:current-page-id state) + objects (get-in state [:workspace-data + :pages-index + page-id + :objects])] + (->> (uw/ask! {:cmd :selection/query-z-index + :page-id page-id + :objects objects + :ids selected}) + (rx/map (fn [z-indexes] + (assoc data :selected + (->> (d/zip selected z-indexes) + (sort-by second) + (map first) + (into (d/ordered-set))))))))) + + ;; Retrieve all ids of selected shapes with corresponding ;; children; this is needed because each shape should be ;; processed one by one because of async events (data url ;; fetching). @@ -1316,7 +1387,7 @@ (ptk/reify ::copy-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [objects (wsh/lookup-page-objects state) selected (->> (wsh/lookup-selected state) (cp/clean-loops objects)) @@ -1329,6 +1400,7 @@ (->> (rx/from (seq (vals pdata))) (rx/merge-map (partial prepare-object objects selected)) (rx/reduce collect-data initial) + (rx/mapcat (partial sort-selected state)) (rx/map t/encode) (rx/map wapi/write-to-clipboard) (rx/catch on-copy-error) @@ -1342,7 +1414,7 @@ (def paste (ptk/reify ::paste ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (try (let [clipboard-str (wapi/read-from-clipboard) @@ -1381,7 +1453,7 @@ [event in-viewport?] (ptk/reify ::paste-from-event ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (try (let [objects (wsh/lookup-page-objects state) paste-data (wapi/read-from-paste-event event) @@ -1499,7 +1571,7 @@ change))) ;; Procceed with the standard shape paste procediment. - (do-paste [state mouse-pos media] + (do-paste [it state mouse-pos media] (let [media-idx (d/index-by :prev-id media) page-id (:current-page-id state) @@ -1545,19 +1617,21 @@ (map #(get-in % [:obj :id])) (into (d/ordered-set)))] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes selected))))] (ptk/reify ::paste-shape ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [file-id (:current-file-id state) mouse-pos (deref ms/mouse-position)] (if (= file-id (:file-id data)) - (do-paste state mouse-pos []) + (do-paste it state mouse-pos []) (->> (rx/from images) (rx/merge-map (partial upload-media file-id)) (rx/reduce conj []) - (rx/mapcat (partial do-paste state mouse-pos))))))))) + (rx/mapcat (partial do-paste it state mouse-pos))))))))) (defn as-content [text] @@ -1573,7 +1647,7 @@ (s/assert string? text) (ptk/reify ::paste-text ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [id (uuid/next) {:keys [x y]} @ms/mouse-position width (max 8 (min (* 7 (count text)) 700)) @@ -1602,7 +1676,7 @@ (s/assert string? text) (ptk/reify ::paste-svg ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [position (deref ms/mouse-position) file-id (:current-file-id state)] (->> (dwp/parse-svg ["svg" text]) @@ -1612,7 +1686,7 @@ [image] (ptk/reify ::paste-bin-impl ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [file-id (get-in state [:workspace-file :id]) params {:file-id file-id :blobs [image] @@ -1637,7 +1711,7 @@ [] (ptk/reify ::start-create-interaction ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [initial-pos @ms/mouse-position selected (wsh/lookup-selected state) stopper (rx/filter ms/mouse-up? stream)] @@ -1674,7 +1748,7 @@ (assoc-in [:workspace-local :draw-interaction-to-frame] nil))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [position @ms/mouse-position page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -1702,20 +1776,20 @@ [color] (ptk/reify ::change-canvas-color ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (get state :current-page-id) options (wsh/lookup-page-options state page-id) previus-color (:background options)] (rx/of (dch/commit-changes - [{:type :set-option - :page-id page-id - :option :background - :value (:color color)}] - [{:type :set-option - :page-id page-id - :option :background - :value previus-color}] - {:commit-local? true})))))) + {:redo-changes [{:type :set-option + :page-id page-id + :option :background + :value (:color color)}] + :undo-changes [{:type :set-option + :page-id page-id + :option :background + :value previus-color}] + :origin it})))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1735,6 +1809,7 @@ (d/export dwt/update-dimensions) (d/export dwt/flip-horizontal-selected) (d/export dwt/flip-vertical-selected) +(d/export dwt/selected-to-path) ;; Persistence @@ -1758,12 +1833,10 @@ (d/export dws/duplicate-selected) (d/export dws/handle-selection) (d/export dws/select-inside-group) -;;(d/export dws/select-last-layer) (d/export dwd/select-for-drawing) (d/export dwc/clear-edition-mode) (d/export dwc/add-shape) (d/export dwc/start-edition-mode) -#_(d/export dwc/start-path-edit) ;; Groups diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 7153223b3..b7be2c2eb 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -11,7 +11,9 @@ [app.common.pages.spec :as spec] [app.common.spec :as us] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.state-helpers :as wsh] [app.main.worker :as uw] + [app.main.store :as st] [app.util.logging :as log] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -30,123 +32,75 @@ (def commit-changes? (ptk/type? ::commit-changes)) -(defn- generate-operations - ([ma mb] (generate-operations ma mb false)) - ([ma mb undo?] - (let [ops (let [ma-keys (set (keys ma)) - mb-keys (set (keys mb)) - added (set/difference mb-keys ma-keys) - removed (set/difference ma-keys mb-keys) - both (set/intersection ma-keys mb-keys)] - (d/concat - (mapv #(array-map :type :set :attr % :val (get mb %)) added) - (mapv #(array-map :type :set :attr % :val nil) removed) - (loop [items (seq both) - result []] - (if items - (let [k (first items) - vma (get ma k) - vmb (get mb k)] - (if (= vma vmb) - (recur (next items) result) - (recur (next items) - (conj result {:type :set - :attr k - :val vmb - :ignore-touched undo?})))) - result))))] - (if undo? - (conj ops {:type :set-touched :touched (:touched mb)}) - ops)))) +(defn- generate-operation + "Given an object old and new versions and an attribute will append into changes + the set and undo operations" + [changes attr old new] + (let [old-val (get old attr) + new-val (get new attr)] + (if (= old-val new-val) + changes + (-> changes + (update :rops conj {:type :set :attr attr :val new-val}) + (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true}))))) + +(defn- update-shape-changes + "Calculate the changes and undos to be done when a function is applied to a + single object" + [changes page-id objects update-fn attrs id] + (let [old-obj (get objects id) + new-obj (update-fn old-obj) + + attrs (or attrs (d/concat #{} (keys old-obj) (keys new-obj))) + + {rops :rops uops :uops} + (reduce #(generate-operation %1 %2 old-obj new-obj) + {:rops [] :uops []} + attrs) + + uops (cond-> uops + (not (empty? uops)) + (conj {:type :set-touched :touched (:touched old-obj)})) + + change {:type :mod-obj :page-id page-id :id id}] + + (cond-> changes + (not (empty? rops)) + (update :rch conj (assoc change :operations rops)) + + (not (empty? uops)) + (update :uch conj (assoc change :operations uops))))) (defn update-shapes ([ids f] (update-shapes ids f nil)) - ([ids f {:keys [reg-objects?] :or {reg-objects? false}}] + ([ids f {:keys [reg-objects? save-undo? keys] + :or {reg-objects? false save-undo? true attrs nil}}] + (us/assert ::coll-of-uuid ids) (us/assert fn? f) + (ptk/reify ::update-shapes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects]) - objects' (get-in state [:workspace-file :data :pages-index page-id :objects]) - reg-objects {:type :reg-objects :page-id page-id :shapes (vec ids)}] - (loop [ids (seq ids) - rch [] - uch []] - (if (nil? ids) - (rx/of (let [has-rch? (not (empty? rch)) - has-uch? (not (empty? uch)) - rch (cond-> rch (and has-rch? reg-objects?) (conj reg-objects)) - uch (cond-> uch (and has-rch? reg-objects?) (conj reg-objects))] - (when (and has-rch? has-uch?) - (commit-changes rch uch {:commit-local? true})))) + objects (wsh/lookup-page-objects state) + reg-objects {:type :reg-objects :page-id page-id :shapes (vec ids)} - (let [id (first ids) - obj1 (get objects id) - obj2 (f obj1) - obj3 (get objects' id) - rch-operations (generate-operations obj1 obj2) - uch-operations (generate-operations obj2 obj3 true) - rchg {:type :mod-obj - :page-id page-id - :operations rch-operations - :id id} - uchg {:type :mod-obj - :page-id page-id - :operations uch-operations - :id id}] - (recur (next ids) - (if (empty? rch-operations) rch (conj rch rchg)) - (if (empty? uch-operations) uch (conj uch uchg))))))))))) + {redo-changes :rch undo-changes :uch} + (reduce #(update-shape-changes %1 page-id objects f keys %2) + {:rch [] :uch []} ids)] -(defn update-shapes-recursive - [ids f] - (us/assert ::coll-of-uuid ids) - (us/assert fn? f) - (letfn [(impl-get-children [objects id] - (cons id (cp/get-children id objects))) + (when-not (empty? redo-changes) + (let [redo-changes (cond-> redo-changes + reg-objects? (conj reg-objects)) - (impl-gen-changes [objects page-id ids] - (loop [sids (seq ids) - cids (seq (impl-get-children objects (first sids))) - rchanges [] - uchanges []] - (cond - (nil? sids) - [rchanges uchanges] + undo-changes (cond-> undo-changes + reg-objects? (conj reg-objects))] - (nil? cids) - (recur (next sids) - (seq (impl-get-children objects (first (next sids)))) - rchanges - uchanges) - - :else - (let [id (first cids) - obj1 (get objects id) - obj2 (f obj1) - rops (generate-operations obj1 obj2) - uops (generate-operations obj2 obj1 true) - rchg {:type :mod-obj - :page-id page-id - :operations rops - :id id} - uchg {:type :mod-obj - :page-id page-id - :operations uops - :id id}] - (recur sids - (next cids) - (conj rchanges rchg) - (conj uchanges uchg))))))] - (ptk/reify ::update-shapes-recursive - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects]) - [rchanges uchanges] (impl-gen-changes objects page-id (seq ids))] - (rx/of (commit-changes rchanges uchanges {:commit-local? true}))))))) + (rx/of (commit-changes {:redo-changes redo-changes + :undo-changes undo-changes + :origin it + :save-undo? save-undo?}))))))))) (defn update-indices [page-id changes] @@ -158,67 +112,63 @@ :changes changes})))) (defn commit-changes - ([changes undo-changes] - (commit-changes changes undo-changes {})) - ([changes undo-changes {:keys [save-undo? - commit-local? - file-id] - :or {save-undo? true - commit-local? false} - :as opts}] - (us/assert ::cp/changes changes) - (us/assert ::cp/changes undo-changes) - (log/debug :msg "commit-changes" - :js/changes changes - :js/undo-changes undo-changes) + [{:keys [redo-changes undo-changes origin save-undo? file-id] + :or {save-undo? true}}] - (let [error (volatile! nil)] - (ptk/reify ::commit-changes - cljs.core/IDeref - (-deref [_] {:file-id file-id :changes changes}) - ptk/UpdateEvent - (update [_ state] - (let [current-file-id (get state :current-file-id) - file-id (or file-id current-file-id) - path1 (if (= file-id current-file-id) - [:workspace-file :data] - [:workspace-libraries file-id :data]) - path2 (if (= file-id current-file-id) - [:workspace-data] - [:workspace-libraries file-id :data])] - (try - (us/assert ::spec/changes changes) - (let [state (update-in state path1 cp/process-changes changes false)] - (cond-> state - commit-local? (update-in path2 cp/process-changes changes false))) - (catch :default e - (vreset! error e) - state)))) + (log/debug :msg "commit-changes" + :js/redo-changes redo-changes + :js/undo-changes undo-changes) - ptk/WatchEvent - (watch [_ state stream] - (when-not @error - (let [;; adds page-id to page changes (that have the `id` field instead) - add-page-id - (fn [{:keys [id type page] :as change}] - (cond-> change - (page-change? type) - (assoc :page-id (or id (:id page))))) + (let [error (volatile! nil)] + (ptk/reify ::commit-changes + cljs.core/IDeref + (-deref [_] + {:file-id file-id + :hint-events @st/last-events + :hint-origin (ptk/type origin) + :changes redo-changes}) - changes-by-pages - (->> changes - (map add-page-id) - (remove #(nil? (:page-id %))) - (group-by :page-id)) + ptk/UpdateEvent + (update [_ state] + (let [current-file-id (get state :current-file-id) + file-id (or file-id current-file-id) + path (if (= file-id current-file-id) - process-page-changes - (fn [[page-id changes]] - (update-indices page-id changes))] - (rx/concat - (rx/from (map process-page-changes changes-by-pages)) + [:workspace-data] + [:workspace-libraries file-id :data])] + (try + (us/assert ::spec/changes redo-changes) + (us/assert ::spec/changes undo-changes) + (update-in state path cp/process-changes redo-changes false) - (when (and save-undo? (seq undo-changes)) - (let [entry {:undo-changes undo-changes - :redo-changes changes}] - (rx/of (dwu/append-undo entry)))))))))))) + (catch :default e + (vreset! error e) + state)))) + + ptk/WatchEvent + (watch [it state stream] + (when-not @error + (let [;; adds page-id to page changes (that have the `id` field instead) + add-page-id + (fn [{:keys [id type page] :as change}] + (cond-> change + (page-change? type) + (assoc :page-id (or id (:id page))))) + + changes-by-pages + (->> redo-changes + (map add-page-id) + (remove #(nil? (:page-id %))) + (group-by :page-id)) + + process-page-changes + (fn [[page-id changes]] + (update-indices page-id redo-changes))] + (rx/concat + (rx/from (map process-page-changes changes-by-pages)) + + (when (and save-undo? (seq undo-changes)) + (let [entry {:undo-changes undo-changes + :redo-changes redo-changes}] + (rx/of (dwu/append-undo entry))))))))))) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index c9f8bfcd8..3bffa6bdb 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -40,7 +40,7 @@ [{:keys [file] :as bundle}] (ptk/reify ::setup-selection-index ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [msg {:cmd :initialize-indices :file-id (:id file) :data (:data file)}] @@ -112,7 +112,7 @@ (def undo (ptk/reify ::undo ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] ;; Editors handle their own undo's @@ -123,12 +123,15 @@ (when-not (or (empty? items) (= index -1)) (let [changes (get-in items [index :undo-changes])] (rx/of (dwu/materialize-undo changes (dec index)) - (dch/commit-changes changes [] {:save-undo? false})))))))))) + (dch/commit-changes {:redo-changes changes + :undo-changes [] + :save-undo? false + :origin it})))))))))) (def redo (ptk/reify ::redo ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] (when-not (or (some? edition) (not-empty drawing)) @@ -138,7 +141,10 @@ (when-not (or (empty? items) (= index (dec (count items)))) (let [changes (get-in items [(inc index) :redo-changes])] (rx/of (dwu/materialize-undo changes (inc index)) - (dch/commit-changes changes [] {:save-undo? false})))))))))) + (dch/commit-changes {:redo-changes changes + :undo-changes [] + :origin it + :save-undo? false})))))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shapes @@ -174,7 +180,7 @@ (assoc-in state [:workspace-local :selected] ids)) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id)] (rx/of (expand-all-parents ids objects)))))) @@ -196,7 +202,7 @@ state))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [objects (wsh/lookup-page-objects state)] (->> stream (rx/filter interrupt?) @@ -276,7 +282,7 @@ (us/verify ::shape-attrs attrs) (ptk/reify ::add-shape ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -296,7 +302,9 @@ (assoc :name name)))] (rx/concat - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (select-shapes (d/ordered-set id))) (when (= :text (:type attrs)) (->> (rx/of (start-edition-mode id)) @@ -305,7 +313,7 @@ (defn move-shapes-into-frame [frame-id shapes] (ptk/reify ::move-shapes-into-frame ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) to-move-shapes (->> (cp/select-toplevel-shapes objects {:include-frames? false}) @@ -329,15 +337,16 @@ :page-id page-id :index index :shapes [shape-id]})))] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) - + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn delete-shapes [ids] (us/assert (s/coll-of ::us/uuid) ids) (ptk/reify ::delete-shapes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -366,6 +375,12 @@ #{} ids) + interacting-shapes + (filter (fn [shape] + (let [interactions (:interactions shape)] + (some ids (map :destination interactions)))) + (vals objects)) + rchanges (d/concat (reduce (fn [res id] @@ -391,7 +406,18 @@ :operations [{:type :set :attr :masked-group? :val false}]) - groups-to-unmask)) + groups-to-unmask) + (map #(array-map + :type :mod-obj + :page-id page-id + :id (:id %) + :operations [{:type :set + :attr :interactions + :val (vec (remove (fn [interaction] + (contains? ids (:destination interaction))) + (:interactions %)))}]) + interacting-shapes)) + uchanges (d/concat @@ -430,14 +456,23 @@ :operations [{:type :set :attr :masked-group? :val true}]) - groups-to-unmask))] + groups-to-unmask) + (map #(array-map + :type :mod-obj + :page-id page-id + :id (:id %) + :operations [{:type :set + :attr :interactions + :val (:interactions %)}]) + interacting-shapes))] ;; (println "================ rchanges") ;; (cljs.pprint/pprint rchanges) ;; (println "================ uchanges") ;; (cljs.pprint/pprint uchanges) - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) - + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) ;; --- Add shape to Workspace @@ -450,7 +485,7 @@ [type frame-x frame-y data] (ptk/reify ::create-and-add-shape ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [{:keys [width height]} data [vbc-x vbc-y] (viewport-center state) @@ -470,7 +505,7 @@ [image {:keys [x y]}] (ptk/reify ::image-uploaded ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [{:keys [name width height id mtype]} image shape {:name name :width width diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index 553b31038..503d6bee9 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -30,7 +30,12 @@ (ptk/reify ::select-for-drawing ptk/UpdateEvent (update [_ state] - (update state :workspace-drawing assoc :tool tool :object data)) + (-> state + (update :workspace-drawing assoc :tool tool :object data) + ;; When changing drawing tool disable "scale text" mode + ;; automatically, to help users that ignore how this + ;; mode works. + (update :workspace-layout disj :scale-text))) ptk/WatchEvent (watch [_ state stream] diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 6e37c1d93..d9e3879a7 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -87,14 +87,14 @@ (->> ms/mouse-position (rx/filter #(> (gpt/distance % initial) 2)) - (rx/with-latest vector ms/mouse-position-ctrl) + (rx/with-latest vector ms/mouse-position-shift) (rx/switch-map (fn [[point :as current]] (->> (snap/closest-snap-point page-id [shape] layout zoom point) (rx/map #(conj current %))))) (rx/map - (fn [[_ ctrl? point]] - #(update-drawing % point ctrl?))) + (fn [[_ shift? point]] + #(update-drawing % point shift?))) (rx/take-until stoper)) (rx/of common/handle-finish-drawing)))))) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index f31d5be14..92db09960 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -28,7 +28,6 @@ (watch [_ state stream] (let [shape (get-in state [:workspace-drawing :object])] (rx/concat - (rx/of clear-drawing) (when (:initialized? shape) (let [page-id (:current-page-id state) shape-click-width (case (:type shape) @@ -65,4 +64,8 @@ :page-id page-id :rect (:selrect shape)}) (rx/map #(dwc/move-shapes-into-frame (:id shape) %))) - (rx/empty)))))))))) + (rx/empty))))) + + ;; Delay so the mouse event can read the drawing state + (->> (rx/of clear-drawing) + (rx/delay 0))))))) diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index 13a327103..e405c2364 100644 --- a/frontend/src/app/main/data/workspace/grid.cljs +++ b/frontend/src/app/main/data/workspace/grid.cljs @@ -40,7 +40,7 @@ (us/assert ::us/uuid frame-id) (ptk/reify ::add-frame-grid ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) data (get-in state [:workspace-data :pages-index page-id]) params (or (get-in data [:options :saved-grids :square]) @@ -56,29 +56,30 @@ [frame-id index] (ptk/reify ::set-frame-grid ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) (defn set-frame-grid [frame-id index data] (ptk/reify ::set-frame-grid ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/of (dch/update-shapes [frame-id] #(assoc-in % [:grids index] data)))))) (defn set-default-grid [type params] (ptk/reify ::set-default-grid ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [pid (:current-page-id state) prev-value (get-in state [:workspace-data :pages-index pid :options :saved-grids type])] - (rx/of (dch/commit-changes [{:type :set-option - :page-id pid - :option [:saved-grids type] - :value params}] - [{:type :set-option - :page-id pid - :option [:saved-grids type] - :value prev-value}] - {:commit-local? true})))))) + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id pid + :option [:saved-grids type] + :value params}] + :undo-changes [{:type :set-option + :page-id pid + :option [:saved-grids type] + :value prev-value}] + :origin it})))))) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index f34e1475c..849fdde94 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -32,31 +32,97 @@ (gsh/setup selrect) (assoc :shapes (mapv :id shapes))))) +(defn get-empty-groups + "Retrieve emtpy groups after group creation" + [objects parent-id shapes] + (let [ids (cp/clean-loops objects (into #{} (map :id) shapes)) + parents (->> ids + (reduce #(conj %1 (cp/get-parent %2 objects)) + #{}))] + (loop [current-id (first parents) + to-check (rest parents) + removed-id? ids + result #{}] + + (if-not current-id + ;; Base case, no next element + result + + (let [group (get objects current-id)] + (if (and (not= :frame (:type group)) + (not= current-id parent-id) + (empty? (remove removed-id? (:shapes group)))) + + ;; Adds group to the remove and check its parent + (let [to-check (d/concat [] to-check [(cp/get-parent current-id objects)]) ] + (recur (first to-check) + (rest to-check) + (conj removed-id? current-id) + (conj result current-id))) + + ;; otherwise recur + (recur (first to-check) + (rest to-check) + removed-id? + result))))))) + (defn prepare-create-group - [page-id shapes prefix keep-name] + [objects page-id shapes prefix keep-name] (let [group (make-group shapes prefix keep-name) + frame-id (:frame-id (first shapes)) + parent-id (:parent-id (first shapes)) rchanges [{:type :add-obj :id (:id group) :page-id page-id - :frame-id (:frame-id (first shapes)) - :parent-id (:parent-id (first shapes)) + :frame-id frame-id + :parent-id parent-id :obj group :index (::index (first shapes))} + {:type :mov-objects :page-id page-id :parent-id (:id group) :shapes (mapv :id shapes)}] - uchanges (conj - (mapv (fn [obj] {:type :mov-objects - :page-id page-id - :parent-id (:parent-id obj) - :index (::index obj) - :shapes [(:id obj)]}) - shapes) - {:type :del-obj - :id (:id group) - :page-id page-id})] + uchanges (-> (mapv + (fn [obj] + {:type :mov-objects + :page-id page-id + :parent-id (:parent-id obj) + :index (::index obj) + :shapes [(:id obj)]}) shapes) + (conj + {:type :del-obj + :id (:id group) + :page-id page-id})) + + ids-to-delete (get-empty-groups objects parent-id shapes) + + delete-group + (fn [changes id] + (-> changes + (conj {:type :del-obj + :id id + :page-id page-id}))) + + add-deleted-group + (fn [changes id] + (let [obj (-> (get objects id) + (d/without-keys [:shapes]))] + + (d/concat [{:type :add-obj + :id id + :page-id page-id + :frame-id (:frame-id obj) + :parent-id (:parent-id obj) + :obj obj + :index (::index obj)}] changes))) + + rchanges (->> ids-to-delete + (reduce delete-group rchanges)) + + uchanges (->> ids-to-delete + (reduce add-deleted-group uchanges))] [group rchanges uchanges])) (defn prepare-remove-group @@ -100,21 +166,24 @@ (def group-selected (ptk/reify ::group-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) selected (cp/clean-loops objects selected) shapes (shapes-for-grouping objects selected)] (when-not (empty? shapes) - (let [[group rchanges uchanges] (prepare-create-group page-id shapes "Group-" false)] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (let [[group rchanges uchanges] + (prepare-create-group objects page-id shapes "Group-" false)] + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) (def ungroup-selected (ptk/reify ::ungroup-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -124,12 +193,14 @@ (= (:type group) :group)) (let [[rchanges uchanges] (prepare-remove-group page-id group objects)] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))))) (def mask-group (ptk/reify ::mask-group ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -142,7 +213,7 @@ (if (and (= (count shapes) 1) (= (:type (first shapes)) :group)) [(first shapes) [] []] - (prepare-create-group page-id shapes "Group-" true)) + (prepare-create-group objects page-id shapes "Group-" true)) rchanges (d/concat rchanges [{:type :mod-obj @@ -178,13 +249,15 @@ :page-id page-id :shapes [(:id group)]})] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) (def unmask-group (ptk/reify ::unmask-group ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state)] @@ -211,7 +284,9 @@ :page-id page-id :shapes [(:id group)]}]] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index c0370fdb6..5e076b766 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -86,23 +86,26 @@ (us/assert ::cp/color color) (ptk/reify ::add-color ptk/WatchEvent - (watch [_ state s] + (watch [it state s] (let [rchg {:type :add-color :color color} uchg {:type :del-color :id id}] (rx/of #(assoc-in % [:workspace-local :color-for-rename] id) - (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))) - + (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it}))))))) (defn add-recent-color [color] (us/assert ::cp/recent-color color) (ptk/reify ::add-recent-color ptk/WatchEvent - (watch [_ state s] + (watch [it state s] (let [rchg {:type :add-recent-color :color color}] - (rx/of (dch/commit-changes [rchg] [] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [] + :origin it})))))) (def clear-color-for-rename (ptk/reify ::clear-color-for-rename @@ -116,13 +119,17 @@ (us/assert ::us/uuid file-id) (ptk/reify ::update-color ptk/WatchEvent - (watch [_ state stream] - (let [prev (get-in state [:workspace-data :colors id]) + (watch [it state stream] + (let [[path name] (cp/parse-path-name (:name color)) + color (assoc color :path path :name name) + prev (get-in state [:workspace-data :colors id]) rchg {:type :mod-color :color color} uchg {:type :mod-color :color prev}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it}) (sync-file (:current-file-id state) file-id)))))) (defn delete-color @@ -130,26 +137,30 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-color ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [prev (get-in state [:workspace-data :colors id]) rchg {:type :del-color :id id} uchg {:type :add-color :color prev}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it})))))) (defn add-media [{:keys [id] :as media}] (us/assert ::cp/media-object media) (ptk/reify ::add-media ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [obj (select-keys media [:id :name :width :height :mtype]) rchg {:type :add-media :object obj} uchg {:type :del-media :id id}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it})))))) (defn rename-media [id new-name] @@ -157,7 +168,7 @@ (us/assert ::us/string new-name) (ptk/reify ::rename-media ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [object (get-in state [:workspace-data :media id]) [path name] (cp/parse-path-name new-name) @@ -171,20 +182,24 @@ :name (:name object) :path (:path object)}}]] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn delete-media [{:keys [id] :as params}] (us/assert ::us/uuid id) (ptk/reify ::delete-media ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [prev (get-in state [:workspace-data :media id]) rchg {:type :del-media :id id} uchg {:type :add-media :object prev}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it})))))) (defn add-typography ([typography] (add-typography typography true)) @@ -193,12 +208,14 @@ (us/assert ::cp/typography typography) (ptk/reify ::add-typography ptk/WatchEvent - (watch [_ state s] + (watch [it state s] (let [rchg {:type :add-typography :typography typography} uchg {:type :del-typography :id (:id typography)}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it}) #(cond-> % edit? (assoc-in [:workspace-local :rename-typography] (:id typography)))))))))) @@ -209,13 +226,17 @@ (us/assert ::us/uuid file-id) (ptk/reify ::update-typography ptk/WatchEvent - (watch [_ state stream] - (let [prev (get-in state [:workspace-data :typographies (:id typography)]) + (watch [it state stream] + (let [[path name] (cp/parse-path-name (:name typography)) + typography (assoc typography :path path :name name) + prev (get-in state [:workspace-data :typographies (:id typography)]) rchg {:type :mod-typography :typography typography} uchg {:type :mod-typography :typography prev}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it}) (sync-file (:current-file-id state) file-id)))))) (defn delete-typography @@ -223,19 +244,21 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-typography ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [prev (get-in state [:workspace-data :typographies id]) rchg {:type :del-typography :id id} uchg {:type :add-typography :typography prev}] - (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes [rchg] + :undo-changes [uchg] + :origin it})))))) (def add-component "Add a new component to current file library, from the currently selected shapes." (ptk/reify ::add-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [file-id (:current-file-id state) page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -244,7 +267,9 @@ (let [[group rchanges uchanges] (dwlh/generate-add-component selected objects page-id file-id)] (when-not (empty? rchanges) - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) (defn rename-component @@ -254,7 +279,7 @@ (us/assert ::us/string new-name) (ptk/reify ::rename-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [[path name] (cp/parse-path-name new-name) component (get-in state [:workspace-data :components id]) objects (get component :objects) @@ -275,14 +300,16 @@ :path (:path component) :objects objects}]] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn duplicate-component "Create a new component copied from the one with the given id." [{:keys [id] :as params}] (ptk/reify ::duplicate-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [component (cp/get-component id (:current-file-id state) (dwlh/get-local-file state) @@ -303,7 +330,9 @@ uchanges [{:type :del-component :id (:id new-shape)}]] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn delete-component "Delete the component with the given id, from the current file library." @@ -311,7 +340,7 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [component (get-in state [:workspace-data :components id]) rchanges [{:type :del-component @@ -323,7 +352,9 @@ :path (:path component) :shapes (vals (:objects component))}]] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn instantiate-component "Create a new shape in the current page, from the component with the given id @@ -334,7 +365,7 @@ (us/assert ::us/point position) (ptk/reify ::instantiate-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [local-library (dwlh/get-local-file state) libraries (get state :workspace-libraries) component (cp/get-component component-id file-id local-library libraries) @@ -400,7 +431,9 @@ :ignore-touched true}) new-shapes)] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes (d/ordered-set (:id new-shape)))))))) (defn detach-component @@ -410,7 +443,7 @@ (us/assert ::us/uuid id) (ptk/reify ::detach-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) shapes (cp/get-object-with-children id objects) @@ -463,14 +496,16 @@ :val (:touched obj)}]}) shapes)] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn nav-to-component-file [file-id] (us/assert ::us/uuid file-id) (ptk/reify ::nav-to-component-file ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [file (get-in state [:workspace-libraries file-id]) pparams {:project-id (:project-id file) :file-id (:id file)} @@ -499,7 +534,7 @@ (us/assert ::us/uuid id) (ptk/reify ::reset-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (log/info :msg "RESET-COMPONENT of shape" :id (str id)) (let [local-library (dwlh/get-local-file state) libraries (dwlh/get-libraries state) @@ -516,7 +551,9 @@ rchanges local-library)) - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it})))))) (defn update-component "Modify the component linked to the shape with the given id, in the @@ -531,7 +568,7 @@ (us/assert ::us/uuid id) (ptk/reify ::update-component ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (log/info :msg "UPDATE-COMPONENT of shape" :id (str id)) (let [page-id (get state :current-page-id) local-library (dwlh/get-local-file state) @@ -571,12 +608,14 @@ file)) (rx/of (when (seq local-rchanges) - (dch/commit-changes local-rchanges local-uchanges - {:commit-local? true + (dch/commit-changes {:redo-changes local-rchanges + :undo-changes local-uchanges + :origin it :file-id (:id local-library)})) (when (seq rchanges) - (dch/commit-changes rchanges uchanges - {:commit-local? true + (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it :file-id file-id}))))))) (declare sync-file-2nd-stage) @@ -597,7 +636,7 @@ state)) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (log/info :msg "SYNC-FILE" :file (dwlh/pretty-file file-id state) :library (dwlh/pretty-file library-id state)) @@ -625,8 +664,10 @@ (rx/concat (rx/of (dm/hide-tag :sync-dialog)) (when rchanges - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true - :file-id file-id}))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it + :file-id file-id}))) (when (not= file-id library-id) ;; When we have just updated the library file, give some time for the ;; update to finish, before marking this file as synced. @@ -655,7 +696,7 @@ (us/assert ::us/uuid library-id) (ptk/reify ::sync-file-2nd-stage ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (log/info :msg "SYNC-FILE (2nd stage)" :file (dwlh/pretty-file file-id state) :library (dwlh/pretty-file library-id state)) @@ -668,8 +709,10 @@ (log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges (log-changes rchanges file)) - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true - :file-id file-id}))))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it + :file-id file-id}))))))) (def ignore-sync (ptk/reify ::ignore-sync @@ -678,7 +721,7 @@ (assoc-in state [:workspace-file :ignore-sync-until] (dt/now))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rp/mutation :ignore-sync {:file-id (get-in state [:workspace-file :id]) :date (dt/now)})))) @@ -688,7 +731,7 @@ (us/assert ::us/uuid file-id) (ptk/reify ::notify-sync-file ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) (vals (get state :workspace-libraries))) do-update #(do (apply st/emit! (map (fn [library] diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 01d8a61a8..c59050916 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -131,7 +131,7 @@ (if (and (= (count shapes) 1) (= (:type (first shapes)) :group)) [(first shapes) [] []] - (dwg/prepare-create-group page-id shapes "Component-" true)) + (dwg/prepare-create-group objects page-id shapes "Component-" true)) [new-shape new-shapes updated-shapes] (make-component-shape group objects file-id) diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index 33a5c2b52..57f2af43b 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -18,6 +18,7 @@ (d/export drawing/start-path-from-point) (d/export drawing/close-path-drag-start) (d/export drawing/change-edit-mode) +(d/export drawing/reset-last-handler) ;; Edition (d/export edition/start-move-handler) diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index 15c5c6c9b..8461b2e68 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -21,9 +21,9 @@ [objects page-id shape old-content new-content] (us/verify ::spec/content old-content) (us/verify ::spec/content new-content) - (let [shape-id (:id shape) - frame-id (:frame-id shape) - parent-id (:parent-id shape) + (let [shape-id (:id shape) + frame-id (:frame-id shape) + parent-id (:parent-id shape) parent-index (cp/position-on-parent shape-id objects) [old-points old-selrect] (helpers/content->points+selrect shape old-content) @@ -72,7 +72,6 @@ (defn save-path-content ([] (save-path-content {})) - ([{:keys [preserve-move-to] :or {preserve-move-to false}}] (ptk/reify ::save-path-content ptk/UpdateEvent @@ -85,15 +84,17 @@ (assoc-in state (st/get-path state :content) content))) ptk/WatchEvent - (watch [_ state stream] - (let [objects (wsh/lookup-page-objects state) - id (get-in state [:workspace-local :edition]) + (watch [it state stream] + (let [objects (wsh/lookup-page-objects state) + page-id (:current-page-id state) + id (get-in state [:workspace-local :edition]) old-content (get-in state [:workspace-local :edit-path id :old-content])] (if (some? old-content) - (let [shape (get-in state (st/get-path state)) - page-id (:current-page-id state) + (let [shape (get-in state (st/get-path state)) [rch uch] (generate-path-changes objects page-id shape old-content (:content shape))] - (rx/of (dch/commit-changes rch uch {:commit-local? true}))) + (rx/of (dch/commit-changes {:redo-changes rch + :undo-changes uch + :origin it}))) (rx/empty))))))) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 1931a8d27..75c2f2202 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -360,3 +360,13 @@ (and id (= :move mode)) (rx/of (common/finish-path "change-edit-mode")) (and id (= :draw mode)) (rx/of (start-draw-mode)) :else (rx/empty)))))) + +(defn reset-last-handler + [] + (ptk/reify ::reset-last-handler + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state)] + (-> state + (assoc-in [:workspace-local :edit-path id :prev-handler] nil)))))) + diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index b2217ea5e..9f5b3b639 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -47,7 +47,7 @@ (defn apply-content-modifiers [] (ptk/reify ::apply-content-modifiers ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [objects (wsh/lookup-page-objects state) id (st/get-path-id state) @@ -65,9 +65,13 @@ [rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)] (if (empty? new-content) - (rx/of (dch/commit-changes rch uch {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rch + :undo-changes uch + :origin it}) dwc/clear-edition-mode) - (rx/of (dch/commit-changes rch uch {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rch + :undo-changes uch + :origin it}) (selection/update-selection point-change) (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler)))))))) @@ -133,7 +137,7 @@ [position shift?] (ptk/reify ::start-move-path-point ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [id (get-in state [:workspace-local :edition]) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) selected? (contains? selected-points position)] @@ -147,7 +151,7 @@ [start-position] (ptk/reify ::drag-selected-points ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [stopper (->> stream (rx/filter ms/mouse-up?)) id (get-in state [:workspace-local :edition]) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) @@ -202,7 +206,7 @@ state))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [id (get-in state [:workspace-local :edition]) current-move (get-in state [:workspace-local :edit-path id :current-move])] (if (= same-event current-move) @@ -236,7 +240,7 @@ [index prefix] (ptk/reify ::start-move-handler ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [id (get-in state [:workspace-local :edition]) cx (d/prefix-keyword prefix :x) cy (d/prefix-keyword prefix :y) @@ -292,7 +296,7 @@ (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [mode (get-in state [:workspace-local :edit-path id :edit-mode])] (rx/concat (rx/of (undo/start-path-undo)) @@ -322,5 +326,5 @@ (update-in (st/get-path state :content) upt/split-segments #{from-p to-p} t)))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/of (changes/save-path-content {:preserve-move-to true}))))) diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 2470f2bb3..b88ee5268 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -40,8 +40,8 @@ :command "p" :fn #(st/emit! (drp/change-edit-mode :draw))} - :add-node {:tooltip "+" - :command "+" + :add-node {:tooltip (ds/shift "+") + :command "shift++" :fn #(st/emit! (drp/add-node))} :delete-node {:tooltip (ds/supr) @@ -88,7 +88,30 @@ :command [(ds/c-mod "shift+z") (ds/c-mod "y")] :fn #(st/emit! (drp/redo-path))} + ;; ZOOM + + :increase-zoom {:tooltip "+" + :command "+" + :fn #(st/emit! (dw/increase-zoom nil))} + + :decrease-zoom {:tooltip "-" + :command "-" + :fn #(st/emit! (dw/decrease-zoom nil))} + + :reset-zoom {:tooltip (ds/shift "0") + :command "shift+0" + :fn #(st/emit! dw/reset-zoom)} + + :fit-all {:tooltip (ds/shift "1") + :command "shift+1" + :fn #(st/emit! dw/zoom-to-fit-all)} + + :zoom-selected {:tooltip (ds/shift "2") + :command "shift+2" + :fn #(st/emit! dw/zoom-to-selected-shape)} + ;; Arrow movement + :move-fast-up {:tooltip (ds/shift ds/up-arrow) :command "shift+up" :fn #(st/emit! (drp/move-selected :up true))} diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index a6771a54f..ddd2ed3f2 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -25,7 +25,7 @@ ([points tool-fn] (ptk/reify ::process-path-tool ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [objects (wsh/lookup-page-objects state) id (st/get-path-id state) page-id (:current-page-id state) @@ -37,7 +37,9 @@ (let [new-content (-> (tool-fn (:content shape) points) (ups/close-subpaths)) [rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)] - (rx/of (dch/commit-changes rch uch {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rch + :undo-changes uch + :origin it}) (when (empty? new-content) dwc/clear-edition-mode))))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 7b5d4433b..3228e0e5e 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -14,13 +14,16 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.data.dashboard :as dd] + [app.main.data.fonts :as df] [app.main.data.media :as di] [app.main.data.messages :as dm] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.svg-upload :as svg] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.avatars :as avatars] @@ -33,9 +36,11 @@ [app.util.uri :as uu] [beicon.core :as rx] [cljs.spec.alpha :as s] + [clojure.set :as set] [cuerdas.core :as str] [potok.core :as ptk] - [promesa.core :as p])) + [promesa.core :as p] + [tubax.core :as tubax])) (declare persist-changes) (declare persist-sychronous-changes) @@ -86,9 +91,11 @@ (rx/tap on-dirty) (rx/buffer-until notifier) (rx/filter (complement empty?)) - (rx/map (fn [buf] {:file-id file-id - :changes (into [] (mapcat :changes) buf)})) - (rx/map persist-changes) + (rx/map (fn [buf] + (->> (into [] (comp (map #(assoc % :id (uuid/next))) + (map #(assoc % :file-id file-id))) + buf) + (persist-changes file-id)))) (rx/tap on-saving) (rx/take-until (rx/delay 100 stoper))) (->> stream @@ -109,27 +116,25 @@ (on-saved)))))))) (defn persist-changes - [{:keys [file-id changes]}] + [file-id changes] (us/verify ::us/uuid file-id) (ptk/reify ::persist-changes ptk/UpdateEvent (update [_ state] - (let [conj (fnil conj []) - chng {:id (uuid/next) - :changes changes}] - (update-in state [:workspace-persistence :queue] conj chng))) + (let [conj (fnil conj []) + into* (fnil into [])] + (update-in state [:workspace-persistence :queue] into* changes))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [sid (:session-id state) file (get state :workspace-file) queue (get-in state [:workspace-persistence :queue] []) - xf-cat (comp (mapcat :changes)) params {:id (:id file) :revn (:revn file) :session-id sid - :changes (into [] xf-cat queue)} + :changes-with-metadata (into [] queue)} ids (into #{} (map :id) queue) @@ -172,7 +177,7 @@ (us/verify ::us/uuid file-id) (ptk/reify ::persist-synchronous-changes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [sid (:session-id state) file (get-in state [:workspace-libraries file-id]) @@ -211,8 +216,7 @@ (if (= file-id (:current-file-id state)) (-> state (update-in [:workspace-file :revn] max revn) - (update :workspace-data cp/process-changes changes) - (update-in [:workspace-file :data] cp/process-changes changes)) + (update :workspace-data cp/process-changes changes)) (-> state (update-in [:workspace-libraries file-id :revn] max revn) (update-in [:workspace-libraries file-id :data] @@ -256,34 +260,20 @@ [project-id file-id] (ptk/reify ::fetch-bundle ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (->> (rx/zip (rp/query :file {:id file-id}) (rp/query :team-users {:file-id file-id}) (rp/query :project {:id project-id}) (rp/query :file-libraries {:file-id file-id})) - (rx/first) - (rx/map (fn [bundle] (apply bundle-fetched bundle))))))) - -(defn- bundle-fetched - [file users project libraries] - (ptk/reify ::bundle-fetched - IDeref - (-deref [_] - {:file file - :users users - :project project - :libraries libraries}) - - ptk/UpdateEvent - (update [_ state] - (assoc state - :users (d/index-by :id users) - :workspace-undo {} - :workspace-project project - :workspace-file file - :workspace-data (:data file) - :workspace-libraries (d/index-by :id libraries))))) - + (rx/take 1) + (rx/map (fn [[file users project libraries]] + {:file file + :users users + :project project + :libraries libraries})) + (rx/mapcat (fn [{:keys [project] :as bundle}] + (rx/of (ptk/data-event ::bundle-fetched bundle) + (df/load-team-fonts (:team-id project))))))))) ;; --- Set File shared @@ -296,7 +286,7 @@ (assoc-in state [:workspace-file :is-shared] is-shared)) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [params {:id id :is-shared is-shared}] (->> (rp/mutation :set-file-shared params) (rx/ignore)))))) @@ -311,8 +301,8 @@ (us/assert ::us/uuid team-id) (ptk/reify ::fetch-shared-files ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :shared-files params) + (watch [it state stream] + (->> (rp/query :team-shared-files {:team-id team-id}) (rx/map shared-files-fetched))))) (defn shared-files-fetched @@ -331,7 +321,7 @@ [file-id library-id] (ptk/reify ::link-file-to-library ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1) params {:file-id file-id :library-id library-id}] @@ -343,44 +333,24 @@ [file-id library-id] (ptk/reify ::unlink-file-from-library ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [unlinked #(d/dissoc-in % [:workspace-libraries library-id]) params {:file-id file-id :library-id library-id}] (->> (rp/mutation :unlink-file-from-library params) (rx/map (constantly unlinked))))))) -;; --- Fetch Pages - -(declare page-fetched) - -(defn fetch-page - [page-id] - (us/verify ::us/uuid page-id) - (ptk/reify ::fetch-pages - ptk/WatchEvent - (watch [_ state s] - (->> (rp/query :page {:id page-id}) - (rx/map page-fetched))))) - -(defn page-fetched - [{:keys [id] :as page}] - (us/verify ::page page) - (ptk/reify ::page-fetched - IDeref - (-deref [_] page) - - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-pages id] page)))) - ;; --- Upload File Media objects (defn parse-svg [[name text]] - (->> (rp/query! :parsed-svg {:data text}) - (rx/map #(assoc % :name name)))) + (try + (->> (rx/of (-> (tubax/xml->clj text) + (assoc :name name)))) + + (catch :default err + (rx/throw {:type :svg-parser})))) (defn fetch-svg [name uri] (->> (http/send! {:method :get :uri uri :mode :no-cors}) @@ -499,7 +469,7 @@ (us/assert ::process-media-objects params) (ptk/reify ::process-media-objects ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (rx/concat (rx/of (dm/show {:content (tr "media.loading") :type :info @@ -546,7 +516,7 @@ (us/assert ::clone-media-objects-params params) (ptk/reify ::clone-media-objects ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [{:keys [on-success on-error] :or {on-success identity on-error identity}} (meta params) @@ -573,3 +543,109 @@ (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) (update :workspace-pages dissoc id))) +(def update-frame-thumbnail? (ptk/type? ::update-frame-thumbnail)) + +(defn remove-thumbnails + [ids] + (ptk/reify ::remove-thumbnails + ptk/WatchEvent + (watch [_ state stream] + ;; Removes the thumbnail while it's regenerated + (rx/of (dch/update-shapes + ids + #(dissoc % :thumbnail) + {:save-undo? false}))))) + +(defn update-frame-thumbnail + [frame-id] + (ptk/event ::update-frame-thumbnail {:frame-id frame-id})) + +(defn- extract-frame-changes + "Process a changes set in a commit to extract the frames that are channging" + [[event [old-objects new-objects]]] + (let [changes (-> event deref :changes) + + extract-ids + (fn [{type :type :as change}] + (case type + :add-obj [(:id change)] + :mod-obj [(:id change)] + :del-obj [(:id change)] + :reg-objects (:shapes change) + :mov-objects (:shapes change) + [])) + + get-frame-id + (fn [id] + (let [shape (or (get new-objects id) + (get old-objects id))] + + (or (and (= :frame (:type shape)) id) + (:frame-id shape)))) + + ;; Extracts the frames and then removes nils and the root frame + xform (comp (mapcat extract-ids) + (map get-frame-id) + (remove nil?) + (filter #(not= uuid/zero %)))] + + (into #{} xform changes))) + +(defn thumbnail-change? + "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" + [event] + (let [changes (-> event deref :changes) + + is-thumbnail-op? + (fn [{type :type attr :attr}] + (and (= type :set) + (= attr :thumbnail))) + + is-thumbnail-change? + (fn [change] + (and (= (:type change) :mod-obj) + (->> change :operations (every? is-thumbnail-op?))))] + + (->> changes (every? is-thumbnail-change?)))) + +(defn watch-state-changes [] + (ptk/reify ::watch-state-changes + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (->> stream + (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (= ::watch-state-changes (ptk/type %))))) + + objects-stream (->> (rx/concat + (rx/of nil) + (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) + ;; We need to keep the old-objects so we can check the frame for the + ;; deleted objects + (rx/buffer 2 1)) + + frame-changes (->> stream + (rx/filter dch/commit-changes?) + (rx/filter (comp not thumbnail-change?)) + (rx/with-latest-from objects-stream) + (rx/map extract-frame-changes)) + + frames (-> state wsh/lookup-page-objects cp/select-frames) + no-thumb-frames (->> frames + (filter (comp nil? :thumbnail)) + (mapv :id))] + + (rx/concat + (->> (rx/from no-thumb-frames) + (rx/map #(update-frame-thumbnail %))) + + ;; We remove the thumbnails inmediately but defer their generation + (rx/merge + (->> frame-changes + (rx/take-until stopper) + (rx/map #(remove-thumbnails %))) + + (->> frame-changes + (rx/take-until stopper) + (rx/buffer-until (->> frame-changes (rx/debounce 1000))) + (rx/flat-map #(reduce set/union %)) + (rx/map #(update-frame-thumbnail %))))))))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index f73e22467..52d36ee35 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -378,11 +378,12 @@ (def duplicate-selected (ptk/reify ::duplicate-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) delta (gpt/point 0 0) + unames (dwc/retrieve-used-names objects) rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta) @@ -396,7 +397,9 @@ (map #(get-in % [:obj :id])) (into (d/ordered-set)))] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (select-shapes selected)))))) (defn change-hover-state diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 2ca3c32cc..7d2fe9bf4 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -33,7 +33,7 @@ :toggle-assets {:tooltip (ds/alt "I") :command (ds/a-mod "i") :fn #(st/emit! (dw/go-to-layout :assets))} - + :toggle-history {:tooltip (ds/alt "H") :command (ds/a-mod "h") :fn #(st/emit! (dw/go-to-layout :document-history))} @@ -45,7 +45,7 @@ :toggle-rules {:tooltip (ds/meta-shift "R") :command (ds/c-mod "shift+r") :fn #(st/emit! (dw/toggle-layout-flags :rules))} - + :select-all {:tooltip (ds/meta "A") :command (ds/c-mod "a") :fn #(st/emit! (dw/select-all))} @@ -61,7 +61,11 @@ :toggle-alignment {:tooltip (ds/meta "\\") :command (ds/c-mod "\\") :fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))} - + + :toggle-scale-text {:tooltip "K" + :command "k" + :fn #(st/emit! (dw/toggle-layout-flags :scale-text))} + :increase-zoom {:tooltip "+" :command "+" :fn #(st/emit! (dw/increase-zoom nil))} @@ -69,7 +73,7 @@ :decrease-zoom {:tooltip "-" :command "-" :fn #(st/emit! (dw/decrease-zoom nil))} - + :group {:tooltip (ds/meta "G") :command (ds/c-mod "g") :fn #(st/emit! dw/group-selected)} @@ -155,8 +159,8 @@ :command "c" :fn #(st/emit! (dwd/select-for-drawing :comments))} - :insert-image {:tooltip "K" - :command "k" + :insert-image {:tooltip (ds/shift "K") + :command "shift+k" :fn #(-> "image-upload" dom/get-element dom/click)} :copy {:tooltip (ds/meta "C") @@ -169,7 +173,8 @@ :paste {:tooltip (ds/meta "V") :disabled true - :command (ds/c-mod "v")} + :command (ds/c-mod "v") + :fn (constantly nil)} :delete {:tooltip (ds/supr) :command ["del" "backspace"] diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index c7efb8627..0710aba55 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -6,7 +6,8 @@ (ns app.main.data.workspace.state-helpers (:require - [app.common.data :as d])) + [app.common.data :as d] + [app.common.pages :as cp])) (defn lookup-page-objects ([state] @@ -25,10 +26,18 @@ (get-in state [:workspace-data :components component-id :objects]))) (defn lookup-selected - [state] - (let [selected (get-in state [:workspace-local :selected]) - objects (lookup-page-objects state) - is-present? (fn [id] (contains? objects id))] - (into (d/ordered-set) - (filter is-present?) - selected))) + ([state] + (lookup-selected state nil)) + + ([state {:keys [omit-blocked?] + :or {omit-blocked? false}}] + (let [objects (lookup-page-objects state) + selected (->> (get-in state [:workspace-local :selected]) + (cp/clean-loops objects)) + selectable? (fn [id] + (and (contains? objects id) + (or (not omit-blocked?) + (not (get-in objects [id :blocked] false)))))] + (into (d/ordered-set) + (filter selectable?) + selected)))) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 6d34597f3..f2828eb3f 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -387,7 +387,7 @@ [svg-data file-id position] (ptk/reify ::svg-uploaded ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] ;; Once the SVG is uploaded, we need to extract all the bitmap ;; images and upload them separatelly, then proceed to create ;; all shapes. @@ -414,7 +414,7 @@ [svg-data {:keys [x y] :as position}] (ptk/reify ::create-svg-shapes ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (try (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -464,7 +464,9 @@ rchanges (conj rchanges reg-objects-action)] - (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) (dwc/select-shapes (d/ordered-set root-id)))) (catch :default e diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 707b06d54..e726917de 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -23,6 +23,7 @@ [app.main.snap :as snap] [app.main.store :as st] [app.main.streams :as ms] + [app.util.path.shapes-to-path :as ups] [beicon.core :as rx] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -72,6 +73,27 @@ :bottom-left [ex sy])] (gpt/point x y))) +(defn- fix-init-point + "Fix the initial point so the resizes are accurate" + [initial handler shape] + (let [{:keys [x y width height]} (:selrect shape) + {:keys [rotation]} shape + rotation (or rotation 0)] + (if (= rotation 0) + (cond-> initial + (contains? #{:left :top-left :bottom-left} handler) + (assoc :x x) + + (contains? #{:right :top-right :bottom-right} handler) + (assoc :x (+ x width)) + + (contains? #{:top :top-right :top-left} handler) + (assoc :y y) + + (contains? #{:bottom :bottom-right :bottom-left} handler) + (assoc :y (+ y height))) + initial))) + (defn finish-transform [] (ptk/reify ::finish-transform ptk/UpdateEvent @@ -81,11 +103,20 @@ ;; -- RESIZE (defn start-resize [handler initial ids shape] - (letfn [(resize [shape initial resizing-shapes [point lock? point-snap]] + (letfn [(resize [shape initial resizing-shapes layout [point lock? point-snap]] (let [{:keys [width height]} (:selrect shape) {:keys [rotation]} shape + rotation (or rotation 0) + + initial (fix-init-point initial handler shape) + shapev (-> (gpt/point width height)) + scale-text (:scale-text layout) + + ;; Force lock if the scale text mode is active + lock? (or lock? scale-text) + ;; Vector modifiers depending on the handler handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y)) @@ -119,6 +150,7 @@ {:resize-vector scalev :resize-origin origin :resize-transform shape-transform + :resize-scale-text scale-text :resize-transform-inverse shape-transform-inverse} false)))) @@ -135,7 +167,7 @@ (assoc-in [:workspace-local :transform] :resize))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [initial-position @ms/mouse-position stoper (rx/filter ms/mouse-up? stream) layout (:workspace-layout state) @@ -154,7 +186,7 @@ (rx/switch-map (fn [[point :as current]] (->> (snap/closest-snap-point page-id resizing-shapes layout zoom point) (rx/map #(conj current %))))) - (rx/mapcat (partial resize shape initial-position resizing-shapes)) + (rx/mapcat (partial resize shape initial-position resizing-shapes layout)) (rx/take-until stoper)) (rx/of (apply-modifiers ids) (finish-transform)))))))) @@ -169,7 +201,7 @@ (assoc-in [:workspace-local :transform] :rotate))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [stoper (rx/filter ms/mouse-up? stream) group (gsh/selection-rect shapes) group-center (gsh/center-selrect group) @@ -208,30 +240,31 @@ [] (ptk/reify ::start-move-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [initial (deref ms/mouse-position) - selected (wsh/lookup-selected state) + selected (wsh/lookup-selected state {:omit-blocked? true}) stopper (rx/filter ms/mouse-up? stream)] - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(gpt/to-vec initial %)) - (rx/map #(gpt/length %)) - (rx/filter #(> % 1)) - (rx/take 1) - (rx/with-latest vector ms/mouse-position-alt) - (rx/mapcat - (fn [[_ alt?]] - (if alt? - ;; When alt is down we start a duplicate+move - (rx/of (start-move-duplicate initial) - dws/duplicate-selected) - ;; Otherwise just plain old move - (rx/of (start-move initial selected)))))))))) + (when-not (empty? selected) + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(gpt/to-vec initial %)) + (rx/map #(gpt/length %)) + (rx/filter #(> % 1)) + (rx/take 1) + (rx/with-latest vector ms/mouse-position-alt) + (rx/mapcat + (fn [[_ alt?]] + (if alt? + ;; When alt is down we start a duplicate+move + (rx/of (start-move-duplicate initial) + dws/duplicate-selected) + ;; Otherwise just plain old move + (rx/of (start-move initial selected))))))))))) (defn start-move-duplicate [from-position] (ptk/reify ::start-move-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (->> stream (rx/filter (ptk/type? ::dws/duplicate-selected)) (rx/first) @@ -240,13 +273,14 @@ (defn calculate-frame-for-move [ids] (ptk/reify ::calculate-frame-for-move ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [position @ms/mouse-position page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) frame-id (cp/frame-id-by-position objects position) moving-shapes (->> ids + (cp/clean-loops objects) (map #(get objects %)) (remove #(= (:frame-id %) frame-id))) @@ -265,9 +299,11 @@ :index (cp/get-index-in-parent objects (:id shape)) :shapes [(:id shape)]})))] - (when-not (empty? rch) + (when-not (empty? uch) (rx/of dwu/pop-undo-into-transaction - (dch/commit-changes rch uch {:commit-local? true}) + (dch/commit-changes {:redo-changes rch + :undo-changes uch + :origin it}) (dwu/commit-undo-transaction) (dwc/expand-collapse frame-id))))))) @@ -281,10 +317,11 @@ (assoc-in [:workspace-local :transform] :move))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - ids (if (nil? ids) (wsh/lookup-selected state) ids) + selected (wsh/lookup-selected state {:omit-blocked? true}) + ids (if (nil? ids) selected ids) shapes (mapv #(get objects %) ids) stopper (rx/filter ms/mouse-up? stream) layout (get state :workspace-layout) @@ -295,12 +332,15 @@ (rx/take-until stopper) (rx/map #(gpt/to-vec from-position %))) - snap-delta (->> position - (rx/throttle 20) - (rx/switch-map - (fn [pos] - (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) - (rx/map #(vector pos %))))))] + snap-delta (rx/concat + ;; We send the nil first so the stream is not waiting for the first value + (rx/of nil) + (->> position + (rx/throttle 20) + (rx/switch-map + (fn [pos] + (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) + (rx/map #(vector pos %)))))))] (if (empty? shapes) (rx/empty) (rx/concat @@ -309,8 +349,7 @@ (rx/map snap/correct-snap-point) (rx/map start-local-displacement)) - (rx/of (set-modifiers ids) - (apply-modifiers ids) + (rx/of (apply-modifiers ids {:set-modifiers? true}) (calculate-frame-for-move ids) (finish-transform))))))))) @@ -359,9 +398,9 @@ state)) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (if (= same-event (get-in state [:workspace-local :current-move-selected])) - (let [selected (wsh/lookup-selected state) + (let [selected (wsh/lookup-selected state {:omit-blocked? true}) move-events (->> stream (rx/filter (ptk/type? ::move-selected)) (rx/filter #(= direction (deref %)))) @@ -379,8 +418,7 @@ (rx/map start-local-displacement)) (rx/of (move-selected direction shift?))) - (rx/of (set-modifiers selected) - (apply-modifiers selected) + (rx/of (apply-modifiers selected {:set-modifiers? true}) (finish-transform)))) (rx/empty)))))) @@ -399,6 +437,8 @@ page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) + ids (->> ids (into #{} (remove #(get-in objects [% :blocked] false)))) + not-frame-id? (fn [shape-id] (let [shape (get objects shape-id)] @@ -413,40 +453,41 @@ ids-with-children (concat ids (mapcat #(cp/get-children % objects) (filter not-frame-id? ids)))] - (d/update-in-when state [:workspace-data :pages-index page-id :objects] - #(reduce update-shape % ids-with-children))))))) + (update state :workspace-modifiers + #(reduce update-shape % ids-with-children))))))) ;; Set-rotation is custom because applies different modifiers to each ;; shape adjusting their position. (defn set-rotation - ([delta-rotation shapes] - (set-rotation delta-rotation shapes (-> shapes gsh/selection-rect gsh/center-selrect))) + ([angle shapes] + (set-rotation angle shapes (-> shapes gsh/selection-rect gsh/center-selrect))) - ([delta-rotation shapes center] - (letfn [(rotate-shape [objects angle shape center] - (update-in objects [(:id shape) :modifiers] merge (gsh/rotation-modifiers center shape angle))) + ([angle shapes center] + (ptk/reify ::set-rotation + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + id->obj #(get objects %) + get-children (fn [shape] (map id->obj (cp/get-children (:id shape) objects))) - (rotate-around-center [objects angle center shapes] - (reduce #(rotate-shape %1 angle %2 center) objects shapes)) + shapes (->> shapes (into [] (remove #(get % :blocked false)))) - (set-rotation [objects] - (let [id->obj #(get objects %) - get-children (fn [shape] (map id->obj (cp/get-children (:id shape) objects))) - shapes (concat shapes (mapcat get-children shapes))] - (rotate-around-center objects delta-rotation center shapes)))] + shapes (->> shapes (mapcat get-children) (concat shapes)) - (ptk/reify ::set-rotation - ptk/UpdateEvent - (update [_ state] - (let [page-id (:current-page-id state)] - (d/update-in-when state [:workspace-data :pages-index page-id :objects] set-rotation))))))) + update-shape + (fn [modifiers shape] + (let [rotate-modifiers (gsh/rotation-modifiers shape center angle)] + (assoc-in modifiers [(:id shape) :modifiers] rotate-modifiers)))] + (-> state + (update :workspace-modifiers + #(reduce update-shape % shapes)))))))) (defn increase-rotation [ids rotation] (ptk/reify ::increase-rotation ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -458,18 +499,46 @@ (rx/of (apply-modifiers ids))))))) (defn apply-modifiers - [ids] - (us/verify (s/coll-of uuid?) ids) - (ptk/reify ::apply-modifiers - ptk/WatchEvent - (watch [_ state stream] - (let [objects (wsh/lookup-page-objects state) - children-ids (->> ids (mapcat #(cp/get-children % objects))) - ids-with-children (d/concat [] children-ids ids)] - (rx/of (dwu/start-undo-transaction) - (dch/update-shapes ids-with-children gsh/transform-shape {:reg-objects? true}) - (clear-local-transform) - (dwu/commit-undo-transaction)))))) + ([ids] + (apply-modifiers ids nil)) + + ([ids {:keys [set-modifiers?] + :or {set-modifiers? false}}] + (us/verify (s/coll-of uuid?) ids) + (ptk/reify ::apply-modifiers + ptk/WatchEvent + (watch [it state stream] + (let [objects (wsh/lookup-page-objects state) + children-ids (->> ids (mapcat #(cp/get-children % objects))) + ids-with-children (d/concat [] children-ids ids) + + state (if set-modifiers? + (ptk/update (set-modifiers ids) state) + state) + object-modifiers (get state :workspace-modifiers)] + + (rx/of (dwu/start-undo-transaction) + (dch/update-shapes + ids-with-children + (fn [shape] + (-> shape + (merge (get object-modifiers (:id shape))) + (gsh/transform-shape))) + {:reg-objects? true + ;; Attributes that can change in the transform. This way we don't have to check + ;; all the attributes + :attrs [:selrect :points + :x :y + :width :height + :content + :transform + :transform-inverse + :rotation + :flip-x + :flip-y] + }) + (clear-local-transform) + (dwu/commit-undo-transaction))))))) ;; --- Update Dimensions @@ -508,7 +577,7 @@ #(reduce update-shape % ids)))) ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))] @@ -517,9 +586,9 @@ (defn flip-horizontal-selected [] (ptk/reify ::flip-horizontal-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state) + selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape))) origin (gpt/point (:x selrect) (+ (:y selrect) (/ (:height selrect) 2)))] @@ -534,9 +603,9 @@ (defn flip-vertical-selected [] (ptk/reify ::flip-vertical-selected ptk/WatchEvent - (watch [_ state stream] + (watch [it state stream] (let [objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state) + selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape))) origin (gpt/point (+ (:x selrect) (/ (:width selrect) 2)) (:y selrect))] @@ -561,4 +630,13 @@ ptk/UpdateEvent (update [_ state] (-> state + (dissoc :workspace-modifiers) (update :workspace-local dissoc :modifiers :current-move-selected))))) + +(defn selected-to-path + [] + (ptk/reify ::selected-to-path + ptk/WatchEvent + (watch [_ state stream] + (let [ids (wsh/lookup-selected state {:omit-blocked? true})] + (rx/of (dch/update-shapes ids ups/convert-to-path)))))) diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index f0e752556..a63225870 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -27,8 +27,8 @@ ;; Undo / Redo ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::undo-changes ::cp/changes) -(s/def ::redo-changes ::cp/changes) +(s/def ::undo-changes ::spec/changes) +(s/def ::redo-changes ::spec/changes) (s/def ::undo-entry (s/keys :req-un [::undo-changes ::redo-changes])) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 1569144f3..aa5e08c21 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -121,7 +121,7 @@ (mf/defc page-svg {::mf/wrap [mf/memo]} - [{:keys [data width height] :as props}] + [{:keys [data width height thumbnails?] :as props}] (let [objects (:objects data) root (get objects uuid/zero) shapes (->> (:shapes root) @@ -146,11 +146,23 @@ :xmlns "http://www.w3.org/2000/svg"} [:& background {:vbox dim :color background-color}] (for [item shapes] - (if (= (:type item) :frame) - [:& frame-wrapper {:shape item - :key (:id item)}] - [:& shape-wrapper {:shape item - :key (:id item)}]))])) + (let [frame? (= (:type item) :frame)] + (cond + (and frame? thumbnails? (some? (:thumbnail item))) + [:image {:xlinkHref (:thumbnail item) + :x (:x item) + :y (:y item) + :width (:width item) + :height (:height item) + ;; DEBUG + ;; :style {:filter "sepia(1)"} + }] + frame? + [:& frame-wrapper {:shape item + :key (:id item)}] + :else + [:& shape-wrapper {:shape item + :key (:id item)}])))])) (mf/defc frame-svg {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 27a208b9b..e9030abb3 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -8,16 +8,22 @@ "Fonts management and loading logic." (:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require + [app.config :as cf] [app.common.data :as d] [app.util.dom :as dom] [app.util.object :as obj] [app.util.timers :as ts] + [app.util.logging :as log] + [lambdaisland.uri :as u] + [goog.events :as gev] [beicon.core :as rx] [clojure.set :as set] [cuerdas.core :as str] [okulary.core :as l] [promesa.core :as p])) +(log/set-level! :trace) + (def google-fonts (preload-gfonts "fonts/gfonts.2020.04.23.json")) @@ -38,22 +44,23 @@ {:id "blackitalic" :name "black (italic)" :weight "900" :style "italic"}]}]) (defonce fontsdb (l/atom {})) -(defonce fontsview (l/atom {})) +(defonce fonts (l/atom [])) -(defn- materialize-fontsview - [db] - (reset! fontsview (reduce-kv (fn [acc k v] - (assoc acc k (sort-by :name v))) - {} - (group-by :backend (vals db))))) (add-watch fontsdb "main" (fn [_ _ _ db] - (ts/schedule #(materialize-fontsview db)))) + (->> (vals db) + (sort-by :name) + (map-indexed #(assoc %2 :index %1)) + (vec) + (reset! fonts)))) (defn register! [backend fonts] - (let [fonts (map #(assoc % :backend backend) fonts)] - (swap! fontsdb #(merge % (d/index-by :id fonts))))) + (swap! fontsdb + (fn [db] + (let [db (reduce-kv #(cond-> %1 (= backend (:backend %3)) (dissoc %2)) db db) + fonts (map #(assoc % :backend backend) fonts)] + (merge db (d/index-by :id fonts)))))) (register! :builtin local-fonts) (register! :google google-fonts) @@ -67,13 +74,15 @@ (defn resolve-fonts [backend] - (get @fontsview backend)) + (get @fonts backend)) -;; --- Fonts Loader +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FONTS LOADING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defonce loaded (l/atom #{})) -(defn- create-link-node +(defn- create-link-element [uri] (let [node (.createElement js/document "link")] (unchecked-set node "href" uri) @@ -81,32 +90,106 @@ (unchecked-set node "type" "text/css") node)) -(defn gfont-url [family variants] +(defn- create-style-element + [css] + (let [node (.createElement js/document "style")] + (unchecked-set node "innerHTML" css) + node)) + +(defn- load-font-css! + "Creates a link element and attaches it to the dom for correctly + load external css resource." + [url on-loaded] + (let [node (create-link-element url) + head (.-head ^js js/document)] + (gev/listenOnce node "load" (fn [event] + (when (fn? on-loaded) + (on-loaded)))) + (dom/append-child! head node))) + +(defn- add-font-css! + "Creates a style element and attaches it to the dom." + [css] + (let [head (.-head ^js js/document)] + (->> (create-style-element css) + (dom/append-child! head)))) + +;; --- LOADER: BUILTIN + +(defmulti ^:private load-font :backend) + +(defmethod load-font :default + [{:keys [backend] :as font}] + (log/warn :msg "no implementation found for" :backend backend)) + +(defmethod load-font :builtin + [{:keys [id ::on-loaded] :as font}] + (log/debug :action "load-font" :font-id id :backend "builtin") + ;; (js/console.log "[debug:fonts]: loading builtin font" id) + (when (fn? on-loaded) + (on-loaded id))) + +;; --- LOADER: GOOGLE + +(defn generate-gfonts-url + [{:keys [family variants]}] (let [base (str "https://fonts.googleapis.com/css?family=" family) variants (str/join "," (map :id variants))] (str base ":" variants "&display=block"))) -(defmulti ^:private load-font :backend) - -(defmethod load-font :builtin - [{:keys [id ::on-loaded] :as font}] - (js/console.log "[debug:fonts]: loading builtin font" id) - (when (fn? on-loaded) - (on-loaded id))) - (defmethod load-font :google [{:keys [id family variants ::on-loaded] :as font}] (when (exists? js/window) - (js/console.log "[debug:fonts]: loading google font" id) - (let [node (create-link-node (gfont-url family variants))] - (.addEventListener node "load" (fn [event] (when (fn? on-loaded) - (on-loaded id)))) - (.append (.-head js/document) node) + (log/debug :action "load-font" :font-id id :backend "google") + (let [url (generate-gfonts-url font)] + (load-font-css! url (partial on-loaded id)) nil))) -(defmethod load-font :default - [{:keys [backend] :as font}] - (js/console.warn "no implementation found for" backend)) +;; --- LOADER: CUSTOM + +(def font-css-template + "@font-face { + font-family: '%(family)s'; + font-style: %(style)s; + font-weight: %(weight)s; + font-display: block; + src: url(%(woff2-uri)s) format('woff2'), + url(%(woff1-uri)s) format('woff'), + url(%(ttf-uri)s) format('ttf'), + url(%(otf-uri)s) format('otf'); + }") + +(defn- asset-id->uri + [asset-id] + (str (u/join cf/public-uri "assets/by-id/" asset-id))) + +(defn generate-custom-font-variant-css + [family variant] + (str/fmt font-css-template + {:family family + :style (:style variant) + :weight (:weight variant) + :woff2-uri (asset-id->uri (::woff2-file-id variant)) + :woff1-uri (asset-id->uri (::woff1-file-id variant)) + :ttf-uri (asset-id->uri (::ttf-file-id variant)) + :otf-uri (asset-id->uri (::otf-file-id variant))})) + +(defn- generate-custom-font-css + [{:keys [family variants] :as font}] + (->> variants + (map #(generate-custom-font-variant-css family %)) + (str/join "\n"))) + +(defmethod load-font :custom + [{:keys [id family variants ::on-loaded] :as font}] + (when (exists? js/window) + (js/console.log "[debug:fonts]: loading custom font" id) + (let [css (generate-custom-font-css font)] + (add-font-css! css)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; LOAD API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn ensure-loaded! ([id] @@ -119,7 +202,8 @@ (load-font (assoc font ::on-loaded on-loaded)) (swap! loaded conj id))))) -(defn ready [cb] +(defn ready + [cb] (-> (obj/get-in js/document ["fonts" "ready"]) (p/then cb))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 5f3b09cc8..db33ad5aa 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -1,3 +1,4 @@ + ;; 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/. @@ -44,6 +45,33 @@ (def dashboard-local (l/derived :dashboard-local st/state)) +(def dashboard-fonts + (l/derived :dashboard-fonts st/state)) + +(def dashboard-projects + (l/derived :dashboard-projects st/state)) + +(def dashboard-files + (l/derived :dashboard-files st/state)) + +(def dashboard-shared-files + (l/derived :dashboard-shared-files st/state)) + +(def dashboard-search-result + (l/derived :dashboard-search-result st/state)) + +(def dashboard-team + (l/derived (fn [state] + (let [team-id (:current-team-id state)] + (get-in state [:teams team-id]))) + st/state)) + +(def dashboard-team-stats + (l/derived :dashboard-team-stats st/state)) + +(def dashboard-team-members + (l/derived :dashboard-team-members st/state)) + (def dashboard-selected-project (l/derived (fn [state] (get-in state [:dashboard-local :selected-project])) @@ -51,17 +79,14 @@ (def dashboard-selected-files (l/derived (fn [state] - (get-in state [:dashboard-local :selected-files] #{})) - st/state)) - -(def dashboard-selected-file-objs - (l/derived (fn [state] - (let [dashboard-local (get state :dashboard-local) - selected-project (get dashboard-local :selected-project) - selected-files (get dashboard-local :selected-files #{})] - (map #(get-in state [:files selected-project %]) - selected-files))) - st/state)) + (let [get-file #(get-in state [:dashboard-files %]) + sim-file #(select-keys % [:id :name :project-id]) + selected (get-in state [:dashboard-local :selected-files]) + xform (comp (map get-file) + (map sim-file))] + (->> (into #{} xform selected) + (d/index-by :id)))) + st/state =)) ;; ---- Workspace refs @@ -127,16 +152,17 @@ (def workspace-file (l/derived (fn [state] - (when-let [file (:workspace-file state)] + (let [file (:workspace-file state) + data (:workspace-data state)] (-> file (dissoc :data) - (assoc :pages (get-in file [:data :pages]))))) + (assoc :pages (:pages data))))) st/state =)) (def workspace-file-colors (l/derived (fn [state] - (when-let [file (:workspace-file state)] - (->> (get-in file [:data :colors]) + (when-let [file (:workspace-data state)] + (->> (:colors file) (d/mapm #(assoc %2 :file-id (:id file)))))) st/state)) @@ -147,8 +173,8 @@ (def workspace-file-typography (l/derived (fn [state] - (when-let [file (:workspace-file state)] - (get-in file [:data :typographies]))) + (when-let [file (:workspace-data state)] + (:typographies file))) st/state)) (def workspace-project @@ -184,7 +210,10 @@ st/state)) (def workspace-page-objects - (l/derived :objects workspace-page)) + (l/derived wsh/lookup-page-objects st/state =)) + +(def workspace-modifiers + (l/derived :workspace-modifiers st/state)) (def workspace-page-options (l/derived :options workspace-page)) @@ -203,12 +232,21 @@ (l/derived #(get % id) workspace-page-objects)) (defn objects-by-id - [ids] - (l/derived (fn [objects] - (into [] (comp (map #(get objects %)) - (remove nil?)) - ids)) - workspace-page-objects =)) + ([ids] + (objects-by-id ids nil)) + + ([ids {:keys [with-modifiers?] + :or { with-modifiers? false }}] + (l/derived (fn [state] + (let [objects (wsh/lookup-page-objects state) + modifiers (:workspace-modifiers state) + objects (cond-> objects + with-modifiers? + (cp/merge-modifiers modifiers)) + xform (comp (map #(get objects %)) + (remove nil?))] + (into [] xform ids))) + st/state =))) (def selected-data (l/derived #(let [selected (wsh/lookup-selected %) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 2a35a53f1..36d935ffb 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -15,7 +15,7 @@ [beicon.core :as rx] [cuerdas.core :as str])) -(defn- handle-response +(defn handle-response [{:keys [status body] :as response}] (cond (= 204 status) @@ -110,14 +110,6 @@ :response-type :blob}) (rx/mapcat handle-response))) -(defmethod query :parsed-svg - [id params] - (->> (http/send! {:method :post - :uri (u/join base-uri "api/rpc/query/" (name id)) - :body (http/transit-data params)}) - (rx/map http/conditional-decode-transit) - (rx/mapcat handle-response))) - (derive :upload-file-media-object ::multipart-upload) (derive :update-profile-photo ::multipart-upload) (derive :update-team-photo ::multipart-upload) diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index e73f4c777..b8e700b6b 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -19,7 +19,7 @@ [beicon.core :as rx] [clojure.set :as set])) -(defonce ^:private snap-accuracy 10) +(defonce ^:private snap-accuracy 5) (defonce ^:private snap-path-accuracy 10) (defonce ^:private snap-distance-accuracy 10) @@ -284,7 +284,7 @@ ([matches other] (let [merge-coord (fn [matches other] - + (let [matches (into {} matches) other (into {} other) keys (set/union (keys matches) (keys other))] @@ -305,7 +305,7 @@ (if (< (mth/abs cur-val) (mth/abs other-val)) current other)) - + min-match-coord (fn [matches] (if (and (seq matches) (not (empty? matches))) @@ -337,13 +337,15 @@ "Snaps a position given an old snap to a different position. We use this to provide a temporal snap while the new is being processed." [[position [snap-pos snap-delta]]] - (let [dx (if (not= 0 (:x snap-delta)) - (- (+ (:x snap-pos) (:x snap-delta)) (:x position)) - 0) - dy (if (not= 0 (:y snap-delta)) - (- (+ (:y snap-pos) (:y snap-delta)) (:y position)) - 0)] + (if (some? snap-delta) + (let [dx (if (not= 0 (:x snap-delta)) + (- (+ (:x snap-pos) (:x snap-delta)) (:x position)) + 0) + dy (if (not= 0 (:y snap-delta)) + (- (+ (:y snap-pos) (:y snap-delta)) (:y position)) + 0)] - (cond-> position - (<= (mth/abs dx) snap-accuracy) (update :x + dx) - (<= (mth/abs dy) snap-accuracy) (update :y + dy)))) + (cond-> position + (<= (mth/abs dx) snap-accuracy) (update :x + dx) + (<= (mth/abs dy) snap-accuracy) (update :y + dy))) + position)) diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index db162221f..f4f15b941 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -7,16 +7,13 @@ (ns app.main.store (:require-macros [app.main.store]) (:require - [beicon.core :as rx] - [okulary.core :as l] - [potok.core :as ptk] - [cuerdas.core :as str] [app.common.data :as d] [app.common.pages :as cp] - [app.common.pages.helpers :as helpers] - [app.common.uuid :as uuid] - [app.util.storage :refer [storage]] - [app.util.debug :refer [debug? debug-exclude-events logjs]])) + [app.util.debug :refer [debug? debug-exclude-events logjs]] + [beicon.core :as rx] + [cuerdas.core :as str] + [okulary.core :as l] + [potok.core :as ptk])) (enable-console-print!) @@ -26,11 +23,20 @@ (defonce state (ptk/store {:resolve ptk/resolve})) (defonce stream (ptk/input-stream state)) -(defn ^boolean is-logged? - [pdata] - (and (some? pdata) - (uuid? (:id pdata)) - (not= uuid/zero (:id pdata)))) +(defonce last-events + (let [buffer (atom #queue []) + remove #{:potok.core/undefined + :app.main.data.workspace.notifications/handle-pointer-update}] + (->> stream + (rx/filter ptk/event?) + (rx/map ptk/type) + (rx/filter (complement remove)) + (rx/map str) + (rx/dedupe) + (rx/buffer 20 1) + (rx/subs #(reset! buffer %))) + + buffer)) (when *assert* (defonce debug-subscription @@ -53,19 +59,12 @@ [& events] #(apply ptk/emit! state events)) -(def initial-state - {:session-id (uuid/next) - :profile (:profile storage)}) - -(defn init - "Initialize the state materialization." - ([] (init {})) - ([props] - (emit! #(merge % initial-state props)))) - (defn ^:export dump-state [] (logjs "state" @state)) +(defn ^:export dump-buffer [] + (logjs "state" @last-events)) + (defn ^:export get-state [str-path] (let [path (->> (str/split str-path " ") (map d/read-string))] diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 9098fea1d..601b89ecc 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -12,8 +12,9 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cfg] - [app.main.data.auth :refer [logout]] + [app.main.data.users :as du] [app.main.data.messages :as dm] + [app.main.data.events :as ev] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.auth :refer [auth]] @@ -90,6 +91,8 @@ ["/settings" :dashboard-team-settings] ["/projects" :dashboard-projects] ["/search" :dashboard-search] + ["/fonts" :dashboard-fonts] + ["/fonts/providers" :dashboard-font-providers] ["/libraries" :dashboard-libraries] ["/projects/:project-id" :dashboard-files]] @@ -104,82 +107,85 @@ (mf/defc main-page {::mf/wrap [#(mf/catch % {:fallback on-main-error})]} [{:keys [route] :as props}] - [:& (mf/provider ctx/current-route) {:value route} - (case (get-in route [:data :name]) - (:auth-login - :auth-register - :auth-register-success - :auth-recovery-request - :auth-recovery) - [:& auth {:route route}] + (let [{:keys [data params]} route] + [:& (mf/provider ctx/current-route) {:value route} + (case (:name data) + (:auth-login + :auth-register + :auth-register-success + :auth-recovery-request + :auth-recovery) + [:& auth {:route route}] - :auth-verify-token - [:& verify-token {:route route}] + :auth-verify-token + [:& verify-token {:route route}] - (:settings-profile - :settings-password - :settings-options - :settings-feedback) - [:& settings/settings {:route route}] + (:settings-profile + :settings-password + :settings-options + :settings-feedback) + [:& settings/settings {:route route}] - :debug-icons-preview - (when *assert* - [:div.debug-preview - [:h1 "Cursors"] - [:& c/debug-preview] - [:h1 "Icons"] - [:& i/debug-icons-preview] - ]) + :debug-icons-preview + (when *assert* + [:div.debug-preview + [:h1 "Cursors"] + [:& c/debug-preview] + [:h1 "Icons"] + [:& i/debug-icons-preview] + ]) - (:dashboard-search - :dashboard-projects - :dashboard-files - :dashboard-libraries - :dashboard-team-members - :dashboard-team-settings) - [:* - #_[:div.modal-wrapper - [:& app.main.ui.onboarding/release-notes-modal {:version "1.5"}]] - [:& dashboard {:route route}]] + (:dashboard-search + :dashboard-projects + :dashboard-files + :dashboard-libraries + :dashboard-fonts + :dashboard-font-providers + :dashboard-team-members + :dashboard-team-settings) + [:* + #_[:div.modal-wrapper + [:& app.main.ui.onboarding/release-notes-modal {:version "1.6"}]] + [:& dashboard {:route route}]] - :viewer - (let [index (get-in route [:query-params :index]) - token (get-in route [:query-params :token]) - section (get-in route [:query-params :section] :interactions) - file-id (get-in route [:path-params :file-id]) - page-id (get-in route [:path-params :page-id])] - [:& fs/fullscreen-wrapper {} - (if (= section :handoff) - [:& handoff {:page-id page-id - :file-id file-id - :index index - :token token}] - [:& viewer-page {:page-id page-id - :file-id file-id - :section section - :index index - :token token}])]) + :viewer + (let [index (get-in route [:query-params :index]) + token (get-in route [:query-params :token]) + section (get-in route [:query-params :section] :interactions) + file-id (get-in route [:path-params :file-id]) + page-id (get-in route [:path-params :page-id])] + [:& fs/fullscreen-wrapper {} + (if (= section :handoff) + [:& handoff {:page-id page-id + :file-id file-id + :index index + :token token}] + [:& viewer-page {:page-id page-id + :file-id file-id + :section section + :index index + :token token}])]) - :render-object - (do - (let [file-id (uuid (get-in route [:path-params :file-id])) - page-id (uuid (get-in route [:path-params :page-id])) - object-id (uuid (get-in route [:path-params :object-id]))] - [:& render/render-object {:file-id file-id - :page-id page-id - :object-id object-id}])) + :render-object + (do + (let [file-id (uuid (get-in route [:path-params :file-id])) + page-id (uuid (get-in route [:path-params :page-id])) + object-id (uuid (get-in route [:path-params :object-id]))] + [:& render/render-object {:file-id file-id + :page-id page-id + :object-id object-id}])) - :workspace - (let [project-id (uuid (get-in route [:params :path :project-id])) - file-id (uuid (get-in route [:params :path :file-id])) - page-id (uuid (get-in route [:params :query :page-id])) - layout-name (get-in route [:params :query :layout])] - [:& workspace/workspace {:project-id project-id - :file-id file-id - :page-id page-id - :layout-name (keyword layout-name) - :key file-id}]) - nil)]) + :workspace + (let [project-id (some-> params :path :project-id uuid) + file-id (some-> params :path :file-id uuid) + page-id (some-> params :query :page-id uuid) + layout (some-> params :query :layout keyword)] + [:& workspace/workspace {:project-id project-id + :file-id file-id + :page-id page-id + :layout-name layout + :key file-id}]) + nil)])) (mf/defc app [] @@ -210,7 +216,7 @@ ;; all profile data and redirect the user to the login page. (defmethod ptk/handle-error :authentication [error] - (ts/schedule (st/emitf (logout)))) + (ts/schedule (st/emitf (du/logout)))) ;; Error that happens on an active bussines model validation does not ;; passes an validation (example: profile can't leave a team). From @@ -220,8 +226,9 @@ [error] (ts/schedule (st/emitf - (dm/show {:content "Unexpected validation error (server side)." - :type :error}))) + (dm/show {:content "Unexpected validation error." + :type :error + :timeout 3000}))) ;; Print to the console some debug info. (js/console.group "Validation Error") @@ -233,62 +240,95 @@ (js/console.error explain))) (js/console.groupEnd "Validation Error")) +;; Error on parsing an SVG +(defmethod ptk/handle-error :svg-parser + [error] + (ts/schedule + (st/emitf + (dm/show {:content "SVG is invalid or malformed" + :type :error + :timeout 3000})))) + ;; This is a pure frontend error that can be caused by an active ;; assertion (assertion that is preserved on production builds). From ;; the user perspective this should be treated as internal error. (defmethod ptk/handle-error :assertion - [{:keys [data stack message context] :as error}] - (ts/schedule - (st/emitf (dm/show {:content "Internal error: assertion." - :type :error}))) + [{:keys [data stack message hint context] :as error}] + (let [message (or message hint) + context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'" + (:ns context) + (:name context) + (str cfg/public-uri "js/cljs-runtime/" (:file context)) + (:line context))] + (ts/schedule + (st/emitf + (dm/show {:content "Internal error: assertion." + :type :error + :timeout 3000}) + (ptk/event ::ev/event + {::ev/type "exception" + ::ev/name "assertion-error" + :message message + :context context + :trace stack}))) - ;; Print to the console some debugging info - (js/console.group message) - (js/console.info (str/format "ns: '%s'\nname: '%s'\nfile: '%s:%s'" - (:ns context) - (:name context) - (str cfg/public-uri "/js/cljs-runtime/" (:file context)) - (:line context))) - (js/console.groupCollapsed "Stack Trace") - (js/console.info stack) - (js/console.groupEnd "Stack Trace") - (js/console.error (with-out-str (expound/printer data))) - (js/console.groupEnd message)) + ;; Print to the console some debugging info + (js/console.group message) + (js/console.info context) + (js/console.groupCollapsed "Stack Trace") + (js/console.info stack) + (js/console.groupEnd "Stack Trace") + (js/console.error (with-out-str (expound/printer data))) + (js/console.groupEnd message))) ;; This happens when the backed server fails to process the ;; request. This can be caused by an internal assertion or any other ;; uncontrolled error. (defmethod ptk/handle-error :server-error - [{:keys [data] :as error}] - (ts/schedule - (st/emitf (dm/show - {:content "Something wrong has happened (on backend)." - :type :error}))) + [{:keys [data hint] :as error}] + (let [hint (or hint (:hint data) (:message data)) + info (with-out-str (pprint (dissoc data :explain))) + expl (:explain data)] + (ts/schedule + (st/emitf + (dm/show {:content "Something wrong has happened (on backend)." + :type :error + :timeout 3000}) + (ptk/event ::ev/event + {::ev/type "exception" + ::ev/name "server-error" + :hint hint + :info info + :explain expl}))) - (js/console.group "Internal Server Error:") - (js/console.error "hint:" (or (:hint data) (:message data))) - (js/console.info - (with-out-str - (pprint (dissoc data :explain)))) - (when-let [explain (:explain data)] - (js/console.error explain)) - (js/console.groupEnd "Internal Server Error:")) + (js/console.group "Internal Server Error:") + (js/console.error "hint:" hint) + (js/console.info info) + (when expl (js/console.error expl)) + (js/console.groupEnd "Internal Server Error:"))) (defmethod ptk/handle-error :default [error] (if (instance? ExceptionInfo error) (ptk/handle-error (ex-data error)) - (do + (let [stack (.-stack error) + hint (or (ex-message error) + (:hint error) + (:message error))] (ts/schedule - (st/emitf (dm/assign-exception error))) + (st/emitf + (dm/assign-exception error) + (ptk/event ::ev/event + {::ev/type "exception" + ::ev/name "unexpected-error" + :message hint + :trace (.-stack error)}))) (js/console.group "Internal error:") - (js/console.log "hint:" (or (ex-message error) - (:hint error) - (:message error))) + (js/console.log "hint:" hint) (ex/ignoring (js/console.error (clj->js error)) - (js/console.error "stack:" (.-stack error))) + (js/console.error "stack:" stack)) (js/console.groupEnd "Internal error:")))) (defonce uncaught-error-handler diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index f5c01f034..06b56dca4 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.auth (:require [app.common.uuid :as uuid] - [app.main.data.auth :as da] [app.main.data.messages :as dm] [app.main.data.users :as du] [app.main.repo :as rp] @@ -19,7 +18,6 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.forms :as fm] - [app.util.storage :refer [cache]] [app.util.i18n :as i18n :refer [t]] [app.util.router :as rt] [app.util.timers :as ts] @@ -56,8 +54,7 @@ [:& recovery-request-page {:locale locale}] :auth-recovery - [:& recovery-page {:locale locale - :params (:query-params route)}]) + [:& recovery-page {:locale locale :params params}]) [:div.terms-login [:a {:href "https://penpot.app/terms.html" :target "_blank"} "Terms of service"] [:span "and"] diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 5dd40cecc..3b809e986 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -6,22 +6,22 @@ (ns app.main.ui.auth.login (:require - [cljs.spec.alpha :as s] - [beicon.core :as rx] - [rumext.alpha :as mf] - [app.config :as cfg] [app.common.spec :as us] - [app.main.ui.icons :as i] - [app.main.data.auth :as da] + [app.config :as cfg] + [app.main.data.messages :as dm] + [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.messages :as msgs] - [app.main.data.messages :as dm] [app.main.ui.components.forms :as fm] - [app.util.object :as obj] + [app.main.ui.icons :as i] + [app.main.ui.messages :as msgs] [app.util.dom :as dom] [app.util.i18n :refer [tr t]] - [app.util.router :as rt])) + [app.util.object :as obj] + [app.util.router :as rt] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [rumext.alpha :as mf])) (s/def ::email ::us/email) (s/def ::password ::us/not-empty-string) @@ -45,7 +45,7 @@ (rx/subs (fn [profile] (if-let [token (:invitation-token profile)] (st/emit! (rt/nav :auth-verify-token {} {:token token})) - (st/emit! (da/logged-in profile)))) + (st/emit! (du/login-from-token {:profile profile})))) (fn [{:keys [type code] :as error}] (cond (and (= type :restriction) @@ -72,7 +72,7 @@ (reset! error nil) (let [params (with-meta (:clean-data @form) {:on-error on-error})] - (st/emit! (da/login params))))) + (st/emit! (du/login params))))) on-submit-ldap (mf/use-callback @@ -149,15 +149,13 @@ [:div.links [:div.link-entry - [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request)) - :tab-index "5"} + [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))} (tr "auth.forgot-password")]] (when cfg/registration-enabled [:div.link-entry [:span (tr "auth.register") " "] - [:a {:on-click #(st/emit! (rt/nav :auth-register {} params)) - :tab-index "6"} + [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))} (tr "auth.register-submit")]])] [:& login-buttons {:params params}] @@ -166,6 +164,5 @@ [:div.links.demo [:div.link-entry [:span (tr "auth.create-demo-profile") " "] - [:a {:on-click (st/emitf da/create-demo-profile) - :tab-index "6"} + [:a {:on-click (st/emitf (du/create-demo-profile))} (tr "auth.create-demo-account")]]])]]) diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index f437b7fe5..704005986 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -6,18 +6,18 @@ (ns app.main.ui.auth.recovery (:require - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.main.ui.icons :as i] [app.common.spec :as us] - [app.main.data.auth :as uda] [app.main.data.messages :as dm] + [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t tr]] - [app.util.router :as rt])) + [app.util.router :as rt] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (s/def ::password-1 ::us/not-empty-string) (s/def ::password-2 ::us/not-empty-string) @@ -54,7 +54,7 @@ :on-success on-success} params {:token (get-in @form [:clean-data :token]) :password (get-in @form [:clean-data :password-2])}] - (st/emit! (uda/recover-profile (with-meta params mdata))))) + (st/emit! (du/recover-profile (with-meta params mdata))))) (mf/defc recovery-form [{:keys [locale params] :as props}] diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 2d8ae58d1..637bbc852 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -7,8 +7,8 @@ (ns app.main.ui.auth.recovery-request (:require [app.common.spec :as us] - [app.main.data.auth :as uda] [app.main.data.messages :as dm] + [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] @@ -59,7 +59,7 @@ params (with-meta cdata {:on-success #(on-success cdata %) :on-error #(on-error cdata %)})] - (st/emit! (uda/request-profile-recovery params)))))] + (st/emit! (du/request-profile-recovery params)))))] [:& fm/form {:on-submit on-submit :form form} diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index a8321a6a0..97c1aec2e 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -8,7 +8,6 @@ (:require [app.common.spec :as us] [app.config :as cfg] - [app.main.data.auth :as da] [app.main.data.users :as du] [app.main.data.messages :as dm] [app.main.store :as st] @@ -93,7 +92,7 @@ (let [data (with-meta (:clean-data @form) {:on-error (partial on-error form) :on-success (partial on-success form)})] - (st/emit! (da/register data)))))] + (st/emit! (du/register data)))))] [:& fm/form {:on-submit on-submit @@ -158,7 +157,7 @@ (when cfg/allow-demo-users [:div.link-entry [:span (tr "auth.create-demo-profile") " "] - [:a {:on-click #(st/emit! da/create-demo-profile) + [:a {:on-click #(st/emit! (du/create-demo-profile)) :tab-index "5"} (tr "auth.create-demo-account")]]) diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index b24521f09..3e0bf5323 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.auth.verify-token (:require [app.common.uuid :as uuid] - [app.main.data.auth :as da] [app.main.data.messages :as dm] [app.main.data.users :as du] [app.main.repo :as rp] @@ -21,7 +20,6 @@ [app.util.forms :as fm] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [app.util.storage :refer [cache]] [app.util.timers :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -33,7 +31,7 @@ [data] (let [msg (tr "dashboard.notifications.email-verified-successfully")] (ts/schedule 100 #(st/emit! (dm/success msg))) - (st/emit! (da/login-from-token data)))) + (st/emit! (du/login-from-token data)))) (defmethod handle-token :change-email [data] @@ -44,7 +42,7 @@ (defmethod handle-token :auth [tdata] - (st/emit! (da/login-from-token tdata))) + (st/emit! (du/login-from-token tdata))) (defmethod handle-token :team-invitation [tdata] diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs index 50cfbfbde..075d99544 100644 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ b/frontend/src/app/main/ui/components/context_menu.cljs @@ -11,7 +11,6 @@ [app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.icons :as i] [app.common.uuid :as uuid] - [app.util.data :refer [classnames]] [app.util.dom :as dom] [app.util.object :as obj])) @@ -22,18 +21,18 @@ (assert (boolean? (gobj/get props "show")) "missing `show` prop") (assert (vector? (gobj/get props "options")) "missing `options` prop") - (let [open? (gobj/get props "show") - on-close (gobj/get props "on-close") - options (gobj/get props "options") + (let [open? (gobj/get props "show") + on-close (gobj/get props "on-close") + options (gobj/get props "options") is-selectable (gobj/get props "selectable") - selected (gobj/get props "selected") - top (gobj/get props "top" 0) - left (gobj/get props "left" 0) - fixed? (gobj/get props "fixed?" false) - min-width? (gobj/get props "min-width?" false) + selected (gobj/get props "selected") + top (gobj/get props "top" 0) + left (gobj/get props "left" 0) + fixed? (gobj/get props "fixed?" false) + min-width? (gobj/get props "min-width?" false) - local (mf/use-state {:offset 0 - :levels nil}) + local (mf/use-state {:offset 0 + :levels nil}) on-local-close (mf/use-callback @@ -81,13 +80,13 @@ (when (and open? (some? (:levels @local))) [:> dropdown' props - [:div.context-menu {:class (classnames :is-open open? - :fixed fixed? - :is-selectable is-selectable) + [:div.context-menu {:class (dom/classnames :is-open open? + :fixed fixed? + :is-selectable is-selectable) :style {:top (+ top (:offset @local)) :left left}} (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} (when-let [parent-option (:parent-option level)] [:* @@ -103,8 +102,7 @@ (if (= option-name :separator) [:li.separator] [:li.context-menu-item - {:class (classnames :is-selected (and selected - (= option-name selected))) + {:class (dom/classnames :is-selected (and selected (= option-name selected))) :key option-name} (if-not sub-options [:a.context-menu-action {:on-click option-handler} diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index 82738e985..54e33925f 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -13,7 +13,7 @@ [props] (let [children (gobj/get props "children") on-close (gobj/get props "on-close") - ref (gobj/get props "container") + ref (gobj/get props "container") on-click (fn [event] diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs index e9de807d7..27fe4d897 100644 --- a/frontend/src/app/main/ui/components/editable_select.cljs +++ b/frontend/src/app/main/ui/components/editable_select.cljs @@ -44,17 +44,18 @@ (fn [node] ;; There is a problem when changing the state in this callback that ;; produces the dropdown to close in the same event - (timers/schedule - #(when-let [bounds (when node (dom/get-bounding-rect node))] - (let [{window-height :height} (dom/get-window-size) - {:keys [left top height]} bounds - bottom (when (< (- window-height top) 300) (- window-height top)) - top (when (>= (- window-height top) 300) (+ top height))] - (swap! state - assoc - :left left - :top top - :bottom bottom)))))] + (when node + (timers/schedule + #(when-let [bounds (when node (dom/get-bounding-rect node))] + (let [{window-height :height} (dom/get-window-size) + {:keys [left top height]} bounds + bottom (when (< (- window-height top) 300) (- window-height top)) + top (when (>= (- window-height top) 300) (+ top height))] + (swap! state + assoc + :left left + :top top + :bottom bottom))))))] (mf/use-effect (mf/deps value) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 105ec5663..dc14df9cd 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -18,6 +18,7 @@ [app.main.ui.dashboard.files :refer [files-section]] [app.main.ui.dashboard.libraries :refer [libraries-page]] [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.sidebar :refer [sidebar]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page]] @@ -50,14 +51,6 @@ (uuid-str? project-id) (assoc :project-id (uuid project-id))))) -(defn- team-ref - [id] - (l/derived (l/in [:teams id]) st/state)) - -(defn- projects-ref - [team-id] - (l/derived (l/in [:projects team-id]) st/state)) - (mf/defc dashboard-content [{:keys [team projects project section search-term profile] :as props}] [:div.dashboard-content {:on-click (st/emitf (dd/clear-selected-files))} @@ -65,6 +58,12 @@ :dashboard-projects [:& projects-section {:team team :projects projects}] + :dashboard-fonts + [:& fonts-page {:team team}] + + :dashboard-font-providers + [:& font-providers-page {:team team}] + :dashboard-files (when project [:& files-section {:team team :project project}]) @@ -94,16 +93,15 @@ team-id (:team-id params) search-term (:search-term params) - projects-ref (mf/use-memo (mf/deps team-id) #(projects-ref team-id)) - team-ref (mf/use-memo (mf/deps team-id) #(team-ref team-id)) + teams (mf/deref refs/teams) + team (get teams team-id) - team (mf/deref team-ref) - projects (mf/deref projects-ref) + projects (mf/deref refs/dashboard-projects) project (get projects project-id)] (mf/use-effect (mf/deps team-id) - (st/emitf (dd/fetch-bundle {:id team-id}))) + (st/emitf (dd/initialize {:id team-id}))) (mf/use-effect (mf/deps) @@ -115,23 +113,31 @@ (not= "0.0" (:main @cf/version))) (tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)}))))))) - [:& (mf/provider ctx/current-file-id) {:value nil} - [:& (mf/provider ctx/current-team-id) {:value team-id} - [:& (mf/provider ctx/current-project-id) {:value project-id} - [:& (mf/provider ctx/current-page-id) {:value nil} + [:& (mf/provider ctx/current-team-id) {:value team-id} + [:& (mf/provider ctx/current-project-id) {:value project-id} - [:section.dashboard-layout - [:& sidebar {:team team - :projects projects - :project project - :profile profile - :section section - :search-term search-term}] + ;; NOTE: dashboard events and other related functions assumes + ;; that the team is a implicit context variable that is + ;; available using react context or accessing + ;; the :current-team-id on the state. We set the key to the + ;; team-id becase we want to completly refresh all the + ;; components on team change. Many components assumess that the + ;; team is already set so don't put the team into mf/deps. + (when team + [:section.dashboard-layout {:key (:id team)} + [:& sidebar + {:team team + :projects projects + :project project + :profile profile + :section section + :search-term search-term}] (when (and team (seq projects)) - [:& dashboard-content {:projects projects - :profile profile - :project project - :section section - :search-term search-term - :team team}])]]]]])) + [:& dashboard-content + {:projects projects + :profile profile + :project project + :section section + :search-term search-term + :team team}])])]])) diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index 6eec7fc75..48703fdf2 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -10,7 +10,6 @@ [app.common.data :as d] [app.common.spec :as us] [app.config :as cfg] - [app.main.data.auth :as da] [app.main.data.dashboard :as dd] [app.main.data.workspace :as dw] [app.main.data.workspace.comments :as dwcm] diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 5b8f80644..f24a8d412 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -19,6 +19,33 @@ [beicon.core :as rx] [rumext.alpha :as mf])) +(defn get-project-name + [project] + (if (:is-default project) + (tr "labels.drafts") + (:name project))) + +(defn get-team-name + [team] + (if (:is-default team) + (tr "dashboard.your-penpot") + (:name team))) + +(defn group-by-team + "Group projects by team." + [projects] + (reduce (fn [teams project] + (update teams + (:team-id project) + #(if (nil? %) + {:id (:team-id project) + :name (:team-name project) + :is-default (:is-default-team project) + :projects [project]} + (update % :projects conj project)))) + {} + projects)) + (mf/defc file-menu [{:keys [files show? on-edit on-menu-close top left navigate?] :as props}] (assert (seq files) "missing `files` prop") @@ -26,8 +53,8 @@ (assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-menu-close) "missing `on-menu-close` prop") (assert (boolean? navigate?) "missing `navigate?` prop") - (let [top (or top 0) - left (or left 0) + (let [top (or top 0) + left (or left 0) file (first files) file-count (count files) @@ -35,157 +62,113 @@ current-team-id (mf/use-ctx ctx/current-team-id) teams (mf/use-state nil) + current-team (get @teams current-team-id) - other-teams (remove #(= (:id %) current-team-id) - (vals @teams)) + other-teams (remove #(= (:id %) current-team-id) (vals @teams)) + current-projects (remove #(= (:id %) (:project-id file)) (:projects current-team)) - project-name (fn [project] - (if (:is-default project) - (tr "labels.drafts") - (:name project))) - - team-name (fn [team] - (if (:is-default team) - (tr "dashboard.your-penpot") - (:name team))) - on-new-tab - (mf/use-callback - (mf/deps file) - (fn [event] - (let [pparams {:project-id (:project-id file) - :file-id (:id file)} - qparams {:page-id (first (get-in file [:data :pages]))}] - (st/emit! (rt/nav-new-window :workspace pparams qparams))))) + (fn [event] + (let [pparams {:project-id (:project-id file) + :file-id (:id file)} + qparams {:page-id (first (get-in file [:data :pages]))}] + (st/emit! (rt/nav-new-window :workspace pparams qparams)))) on-duplicate - (mf/use-callback - (mf/deps files) - (fn [event] - (apply st/emit! (map dd/duplicate-file files)) - (st/emit! (dm/success (tr "dashboard.success-duplicate-file"))))) + (fn [event] + (apply st/emit! (map dd/duplicate-file files)) + (st/emit! (dm/success (tr "dashboard.success-duplicate-file")))) delete-fn - (mf/use-callback - (mf/deps files) - (fn [event] - (apply st/emit! (map dd/delete-file files)) - (st/emit! (dm/success (tr "dashboard.success-delete-file"))))) + (fn [event] + (apply st/emit! (map dd/delete-file files)) + (st/emit! (dm/success (tr "dashboard.success-delete-file")))) on-delete - (mf/use-callback - (mf/deps files) - (fn [event] - (dom/stop-propagation event) - (if multi? - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-file-multi-confirm.title" file-count) - :message (tr "modals.delete-file-multi-confirm.message" file-count) - :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count) + (fn [event] + (dom/stop-propagation event) + (if multi? + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-file-multi-confirm.title" file-count) + :message (tr "modals.delete-file-multi-confirm.message" file-count) + :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count) :on-accept delete-fn})) - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-file-confirm.title") - :message (tr "modals.delete-file-confirm.message") - :accept-label (tr "modals.delete-file-confirm.accept") - :on-accept delete-fn}))))) + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-file-confirm.title") + :message (tr "modals.delete-file-confirm.message") + :accept-label (tr "modals.delete-file-confirm.accept") + :on-accept delete-fn})))) + + on-move-success + (fn [team-id project-id] + (if multi? + (st/emit! (dm/success (tr "dashboard.success-move-files"))) + (st/emit! (dm/success (tr "dashboard.success-move-file")))) + (if (or navigate? (not= team-id current-team-id)) + (st/emit! (dd/go-to-files project-id)) + (st/emit! (dd/fetch-recent-files) + (dd/clear-selected-files)))) on-move - (mf/use-callback - (mf/deps file) - (fn [team-id project-id] - (let [data {:ids (set (map :id files)) - :project-id project-id} - - mdata {:on-success - #(do - (if multi? - (st/emit! (dm/success (tr "dashboard.success-move-files"))) - (st/emit! (dm/success (tr "dashboard.success-move-file")))) - (if (or navigate? (not= team-id current-team-id)) - (st/emit! (rt/nav :dashboard-files - {:team-id team-id - :project-id project-id})) - (st/emit! (dd/fetch-recent-files {:team-id team-id}) - (dd/clear-selected-files))))}] - - (st/emitf (dd/move-files (with-meta data mdata)))))) + (fn [team-id project-id] + (let [data {:ids (set (map :id files)) + :project-id project-id} + mdata {:on-success #(on-move-success team-id project-id)}] + (st/emitf (dd/move-files (with-meta data mdata))))) add-shared - (mf/use-callback - (mf/deps file) - (st/emitf (dd/set-file-shared (assoc file :is-shared true)))) + (st/emitf (dd/set-file-shared (assoc file :is-shared true))) del-shared - (mf/use-callback - (mf/deps file) - (st/emitf (dd/set-file-shared (assoc file :is-shared false)))) + (st/emitf (dd/set-file-shared (assoc file :is-shared false))) on-add-shared - (mf/use-callback - (mf/deps file) - (fn [event] - (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.add-shared-confirm.message" (:name file)) - :hint (tr "modals.add-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.add-shared-confirm.accept") - :accept-style :primary - :on-accept add-shared})))) + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :message "" + :title (tr "modals.add-shared-confirm.message" (:name file)) + :hint (tr "modals.add-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.add-shared-confirm.accept") + :accept-style :primary + :on-accept add-shared}))) on-del-shared - (mf/use-callback - (mf/deps file) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.remove-shared-confirm.message" (:name file)) - :hint (tr "modals.remove-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.remove-shared-confirm.accept") - :on-accept del-shared}))))] + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :message "" + :title (tr "modals.remove-shared-confirm.message" (:name file)) + :hint (tr "modals.remove-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.remove-shared-confirm.accept") + :on-accept del-shared})))] - (mf/use-layout-effect - (mf/deps show?) - (fn [] - (let [group-by-team (fn [projects] - (reduce - (fn [teams project] - (update teams (:team-id project) - #(if (nil? %) - {:id (:team-id project) - :name (:team-name project) - :is-default (:is-default-team project) - :projects [project]} - (update % :projects conj project)))) - {} - projects))] - (if show? - (->> (rp/query! :all-projects) - (rx/map group-by-team) - (rx/subs #(reset! teams %))) - (reset! teams []))))) + (mf/use-effect + (fn [] + (->> (rp/query! :all-projects) + (rx/map group-by-team) + (rx/subs #(reset! teams %))))) (when current-team (let [sub-options (conj (vec (for [project current-projects] - [(project-name project) + [(get-project-name project) (on-move (:id current-team) (:id project))])) (when (seq other-teams) [(tr "dashboard.move-to-other-team") nil (for [team other-teams] - [(team-name team) nil + [(get-team-name team) nil (for [sub-project (:projects team)] - [(project-name sub-project) + [(get-project-name sub-project) (on-move (:id team) (:id sub-project))])])])) @@ -214,4 +197,3 @@ :top top :left left :options options}])))) - diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 4a5aca8d6..a43026964 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -8,6 +8,7 @@ (:require [app.main.data.dashboard :as dd] [app.main.data.modal :as modal] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] @@ -77,15 +78,11 @@ [:a.btn-secondary.btn-small {:on-click on-create-clicked} (tr "dashboard.new-file")]])) -(defn files-ref - [project-id] - (l/derived (l/in [:files project-id]) st/state)) - (mf/defc files-section [{:keys [project team] :as props}] - (let [files-ref (mf/use-memo (mf/deps (:id project)) #(files-ref (:id project))) - files-map (mf/deref files-ref) + (let [files-map (mf/deref refs/dashboard-files) files (->> (vals files-map) + (filter #(= (:id project) (:project-id %))) (sort-by :modified-at) (reverse))] diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs new file mode 100644 index 000000000..241f7c2b2 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -0,0 +1,335 @@ +;; 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 + [app.common.data :as d] + [app.common.media :as cm] + [app.common.uuid :as uuid] + [app.main.data.dashboard :as dd] + [app.main.data.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]])) + +(mf/defc font-variant-display-name + [{:keys [variant]}] + [:* + [:span (cm/font-weight->name (:font-weight variant))] + (when (not= "normal" (:font-style variant)) + [:span " " (str/capital (:font-style variant))])]) + +(mf/defc fonts-upload + [{:keys [team installed-fonts] :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 installed-fonts) + (fn [blobs] + (->> (df/process-upload blobs (:id team)) + (rx/subs (fn [result] + (swap! fonts df/merge-and-group-fonts installed-fonts result)) + (fn [error] + (js/console.error "error" error)))))) + + on-upload + (mf/use-callback + (mf/deps team) + (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 (:id item)) + (swap! uploading disj (:id item)) + (st/emit! (df/add-font font))) + (fn [error] + (js/console.log "error" error)))))) + + on-blur-name + (fn [id event] + (let [name (dom/get-target-val event)] + (swap! fonts df/rename-and-regroup id name installed-fonts))) + + on-delete + (mf/use-callback + (mf/deps team) + (fn [{:keys [id] :as item}] + (swap! fonts dissoc id)))] + + [: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 (tr "labels.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" + :on-blur #(on-blur-name (:id item) %) + :default-value (:font-family item)}]] + [:div.table-field.variants + [:span.label + [:& font-variant-display-name {:variant 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-id variants] :as props}] + (let [font (first variants) + + variants (sort-by (fn [item] + [(:font-weight item) + (if (= "normal" (:font-style item)) 1 2)]) + variants) + + open-menu? (mf/use-state false) + edit? (mf/use-state false) + state (mf/use-var (:font-family font)) + + on-change + (fn [event] + (reset! state (dom/get-target-val event))) + + on-save + (fn [event] + (let [font-family @state] + (st/emit! (df/update-font + {:id font-id + :name font-family})) + (reset! edit? false))) + + on-key-down + (fn [event] + (when (kbd/enter? event) + (on-save event))) + + on-cancel + (fn [event] + (reset! edit? false) + (reset! state (:font-family font))) + + delete-font-fn + (fn [] (st/emit! (df/delete-font font-id))) + + delete-variant-fn + (fn [id] (st/emit! (df/delete-font-variant id))) + + on-delete + (fn [] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-font.title") + :message (tr "modals.delete-font.message") + :accept-label (tr "labels.delete") + :on-accept (fn [props] + (delete-font-fn))}))) + + on-delete-variant + (fn [id] + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-font-variant.title") + :message (tr "modals.delete-font-variant.message") + :accept-label (tr "labels.delete") + :on-accept (fn [props] + (delete-variant-fn id))})))] + + [:div.font-item.table-row + [: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.variants + (for [item variants] + [:div.variant + [:span.label + [:& font-variant-display-name {:variant item}]] + [:span.icon.close + {:on-click #(on-delete-variant (:id item))} + i/plus]])] + + [: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))} + (tr "labels.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.variants (tr "labels.font-variants")] + [:div] + [:div.table-field.search-input + [:input {:placeholder (tr "labels.search-font") + :default-value "" + :on-change on-change + }]]] + + (cond + (seq fonts) + (for [[font-id variants] (->> (vals fonts) + (filter matches?) + (group-by :font-id))] + [:& installed-font {:key (str font-id) + :font-id font-id + :variants variants}]) + + (nil? fonts) + [:div.fonts-placeholder + [:div.icon i/loader] + [:div.label (tr "dashboard.loading-fonts")]] + + :else + [:div.fonts-placeholder + [:div.icon i/text] + [:div.label (tr "dashboard.fonts.empty-placeholder")]])])) + +(mf/defc fonts-page + [{:keys [team] :as props}] + (let [fonts (mf/deref refs/dashboard-fonts)] + [:* + [:& header {:team team :section :fonts}] + [:section.dashboard-container.dashboard-fonts + [:& fonts-upload {:team team :installed-fonts 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 "font providers"]]]) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index d5822551c..b6a8b10d2 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -21,11 +21,12 @@ [app.main.worker :as wrk] [app.util.dom :as dom] [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.router :as rt] [app.util.time :as dt] [app.util.timers :as ts] + [app.util.webapi :as wapi] [beicon.core :as rx] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -55,76 +56,66 @@ [{:keys [modified-at]}] (let [locale (mf/deref i18n/locale) time (dt/timeago modified-at {:locale locale})] - (str (t locale "ds.updated-at" time)))) + (str (tr "ds.updated-at" time)))) + +(defn create-counter-element + [element file-count] + (let [counter-el (dom/create-element "div")] + (dom/set-property! counter-el "class" "drag-counter") + (dom/set-text! counter-el (str file-count)) + counter-el)) (mf/defc grid-item {:wrap [mf/memo]} - [{:keys [id file selected-files navigate?] :as props}] - (let [local (mf/use-state {:menu-open false - :menu-pos nil - :edition false}) - locale (mf/deref i18n/locale) - item-ref (mf/use-ref) - menu-ref (mf/use-ref) - selected? (contains? selected-files id) - - selected-file-objs - (deref refs/dashboard-selected-file-objs) - ;; not needed to subscribe and repaint if changed + [{:keys [file navigate?] :as props}] + (let [file-id (:id file) + local (mf/use-state {:menu-open false + :menu-pos nil + :edition false}) + selected-files (mf/deref refs/dashboard-selected-files) + item-ref (mf/use-ref) + menu-ref (mf/use-ref) + selected? (contains? selected-files file-id) on-menu-close (mf/use-callback #(swap! local assoc :menu-open false)) on-select - (mf/use-callback - (mf/deps id selected? selected-files @local) - (fn [event] - (when (and (or (not selected?) (> (count selected-files) 1)) - (not (:menu-open @local))) - (dom/stop-propagation event) - (let [shift? (kbd/shift? event)] - (when-not shift? - (st/emit! (dd/clear-selected-files))) - (st/emit! (dd/toggle-file-select {:file file})))))) + (fn [event] + (when (and (or (not selected?) (> (count selected-files) 1)) + (not (:menu-open @local))) + (dom/stop-propagation event) + (let [shift? (kbd/shift? event)] + (when-not shift? + (st/emit! (dd/clear-selected-files))) + (st/emit! (dd/toggle-file-select file))))) on-navigate (mf/use-callback - (mf/deps id) + (mf/deps file) (fn [event] (let [menu-icon (mf/ref-val menu-ref) target (dom/get-target event)] (when-not (dom/child? target menu-icon) - (let [pparams {:project-id (:project-id file) - :file-id (:id file)} - qparams {:page-id (first (get-in file [:data :pages]))}] - (st/emit! (rt/nav :workspace pparams qparams))))))) - - create-counter - (mf/use-callback - (fn [element file-count] - (let [counter-el (dom/create-element "div")] - (dom/set-property! counter-el "class" "drag-counter") - (dom/set-text! counter-el (str file-count)) - counter-el))) + (st/emit! (dd/go-to-workspace file)))))) on-drag-start (mf/use-callback (mf/deps selected-files) (fn [event] - (let [offset (dom/get-offset-position (.-nativeEvent event)) + (let [offset (dom/get-offset-position (.-nativeEvent event)) select-current? (not (contains? selected-files (:id file))) - item-el (mf/ref-val item-ref) - counter-el (create-counter item-el - (if select-current? - 1 - (count selected-files)))] - + item-el (mf/ref-val item-ref) + counter-el (create-counter-element item-el + (if select-current? + 1 + (count selected-files)))] (when select-current? (st/emit! (dd/clear-selected-files)) - (st/emit! (dd/toggle-file-select {:file file}))) + (st/emit! (dd/toggle-file-select file))) (dnd/set-data! event "penpot/files" "dummy") (dnd/set-allowed-effect! event "move") @@ -135,7 +126,7 @@ ;; afterwards, in the next render cycle. (dom/append-child! item-el counter-el) (dnd/set-drag-image! event item-el (:x offset) (:y offset)) - (ts/raf #(.removeChild item-el counter-el))))) + (ts/raf #(.removeChild ^js item-el counter-el))))) on-menu-click (mf/use-callback @@ -146,10 +137,11 @@ (let [shift? (kbd/shift? event)] (when-not shift? (st/emit! (dd/clear-selected-files))) - (st/emit! (dd/toggle-file-select {:file file})))) + (st/emit! (dd/toggle-file-select file)))) (let [position (dom/get-client-position event)] - (swap! local assoc :menu-open true - :menu-pos position)))) + (swap! local assoc + :menu-open true + :menu-pos position)))) edit (mf/use-callback @@ -168,19 +160,20 @@ :menu-open false)))] (mf/use-effect - (mf/deps selected? local) - (fn [] - (when (and (not selected?) (:menu-open @local)) - (swap! local assoc :menu-open false)))) + (mf/deps selected? local) + (fn [] + (when (and (not selected?) (:menu-open @local)) + (swap! local assoc :menu-open false)))) + + [:div.grid-item.project-th + {:class (dom/classnames :selected selected?) + :ref item-ref + :draggable true + :on-click on-select + :on-double-click on-navigate + :on-drag-start on-drag-start + :on-context-menu on-menu-click} - [:div.grid-item.project-th {:class (dom/classnames - :selected selected?) - :ref item-ref - :draggable true - :on-click on-select - :on-double-click on-navigate - :on-drag-start on-drag-start - :on-context-menu on-menu-click} [:div.overlay] [:& grid-item-thumbnail {:file file}] (when (:is-shared file) @@ -198,7 +191,7 @@ :on-click on-menu-click} i/actions (when selected? - [:& file-menu {:files selected-file-objs + [:& file-menu {:files (vals selected-files) :show? (:menu-open @local) :left (+ 24 (:x (:menu-pos @local))) :top (:y (:menu-pos @local)) @@ -223,28 +216,24 @@ (mf/defc grid [{:keys [id opts files] :as props}] - (let [locale (mf/deref i18n/locale) - selected-files (mf/deref refs/dashboard-selected-files)] - [:section.dashboard-grid - (cond - (nil? files) - [:& loading-placeholder] + [:section.dashboard-grid + (cond + (nil? files) + [:& loading-placeholder] - (seq files) - [:div.grid-row - (for [item files] - [:& grid-item - {:id (:id item) - :file item - :selected-files selected-files - :key (:id item) - :navigate? true}])] + (seq files) + [:div.grid-row + (for [item files] + [:& grid-item + {:file item + :key (:id item) + :navigate? true}])] - :else - [:& empty-placeholder])])) + :else + [:& empty-placeholder])]) (mf/defc line-grid-row - [{:keys [locale files team-id selected-files on-load-more dragging?] :as props}] + [{:keys [files team-id selected-files on-load-more dragging?] :as props}] (let [rowref (mf/use-ref) width (mf/use-state nil) @@ -267,17 +256,19 @@ (mf/use-effect (fn [] - (let [node (mf/ref-val rowref) - obs (new js/ResizeObserver - (fn [entries x] - (ts/raf #(let [row (first entries) - row-rect (.-contentRect ^js row) - row-width (.-width ^js row-rect)] - (reset! width row-width)))))] - - (.observe ^js obs node) + (let [node (mf/ref-val rowref) + mnt? (volatile! true) + sub (->> (wapi/observe-resize node) + (rx/observe-on :af) + (rx/subs (fn [entries] + (let [row (first entries) + row-rect (.-contentRect ^js row) + row-width (.-width ^js row-rect)] + (when @mnt? + (reset! width row-width))))))] (fn [] - (.disconnect ^js obs))))) + (vreset! mnt? false) + (rx/dispose! sub))))) [:div.grid-row.no-wrap {:ref rowref} (when dragging? @@ -294,12 +285,11 @@ [:div.grid-item.placeholder {:on-click on-load-more} [:div.placeholder-icon i/arrow-down] [:div.placeholder-label - (t locale "dashboard.show-all-files")]])])) + (tr "dashboard.show-all-files")]])])) (mf/defc line-grid [{:keys [project-id team-id opts files on-load-more] :as props}] - (let [locale (mf/deref i18n/locale) - dragging? (mf/use-state false) + (let [dragging? (mf/use-state false) selected-files (mf/deref refs/dashboard-selected-files) selected-project (mf/deref refs/dashboard-selected-project) @@ -327,6 +317,12 @@ (when-not (dnd/from-child? e) (reset! dragging? false)))) + on-drop-success + (fn [] + (st/emit! (dm/success (tr "dashboard.success-move-file")) + (dd/fetch-recent-files) + (dd/clear-selected-files))) + on-drop (mf/use-callback (mf/deps files selected-files) @@ -335,11 +331,7 @@ (when (not= selected-project project-id) (let [data {:ids selected-files :project-id project-id} - - mdata {:on-success - (st/emitf (dm/success (tr "dashboard.success-move-file")) - (dd/fetch-recent-files {:team-id team-id}) - (dd/clear-selected-files))}] + mdata {:on-success on-drop-success}] (st/emit! (dd/move-files (with-meta data mdata)))))))] [:section.dashboard-grid {:on-drag-enter on-drag-enter @@ -355,8 +347,7 @@ :team-id team-id :selected-files selected-files :on-load-more on-load-more - :dragging? @dragging? - :locale locale}] + :dragging? @dragging?}] :else [:& empty-placeholder {:dragging? @dragging?}])])) diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 77db054de..e61d4bcd4 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -10,20 +10,16 @@ [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] [app.main.ui.icons :as i] + [app.main.refs :as refs] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [okulary.core :as l] [rumext.alpha :as mf])) -(defn files-ref - [team-id] - (l/derived (l/in [:shared-files team-id]) st/state)) - (mf/defc libraries-page [{:keys [team] :as props}] - (let [files-ref (mf/use-memo (mf/deps (:id team)) #(files-ref (:id team))) - files-map (mf/deref files-ref) + (let [files-map (mf/deref refs/dashboard-shared-files) files (->> (vals files-map) (sort-by :modified-at) (reverse))] @@ -33,9 +29,11 @@ (dom/set-html-title (tr "title.dashboard.shared-libraries" (if (:is-default team) (tr "dashboard.your-penpot") - (:name team)))) - (st/emit! (dd/fetch-shared-files {:team-id (:id team)}) - (dd/clear-selected-files)))) + (:name team)))))) + + (mf/use-effect + (st/emitf (dd/fetch-shared-files) + (dd/clear-selected-files))) [:* [:header.dashboard-header diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index fc2ace6f0..946478a09 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -9,10 +9,11 @@ [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] [app.main.data.modal :as modal] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.context :as ctx] [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.context :as ctx] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [beicon.core :as rx] @@ -28,81 +29,63 @@ left (or left 0) current-team-id (mf/use-ctx ctx/current-team-id) - teams (mf/use-state nil) + teams (mf/deref refs/teams) + teams (-> teams (dissoc current-team-id) vals vec) + + on-duplicate-success + (fn [new-project] + (st/emit! (dm/success (tr "dashboard.success-duplicate-project")) + (rt/nav :dashboard-files + {:team-id (:team-id new-project) + :project-id (:id new-project)}))) on-duplicate - (mf/use-callback - (mf/deps project) - #(let [on-success - (fn [new-project] - (st/emit! (dm/success (tr "dashboard.success-duplicate-project")) - (rt/nav :dashboard-files - {:team-id (:team-id new-project) - :project-id (:id new-project)})))] - (st/emit! (dd/duplicate-project - (with-meta project {:on-success on-success}))))) + (fn [] + (st/emit! (dd/duplicate-project + (with-meta project {:on-success on-duplicate-success})))) toggle-pin - (mf/use-callback - (mf/deps project) - (st/emitf (dd/toggle-project-pin project))) + (st/emitf (dd/toggle-project-pin project)) + + on-move-success + (fn [team-id] + (st/emit! (dd/go-to-projects team-id))) on-move - (mf/use-callback - (mf/deps project) - (fn [team-id] - (let [data {:id (:id project) - :team-id team-id} - - mdata {:on-success - (st/emitf (rt/nav :dashboard-projects - {:team-id team-id}))}] - + (fn [team-id] + (let [data {:id (:id project) :team-id team-id} + mdata {:on-success #(on-move-success team-id)}] (st/emitf (dm/success (tr "dashboard.success-move-project")) - (dd/move-project (with-meta data mdata)))))) + (dd/move-project (with-meta data mdata))))) delete-fn - (mf/use-callback - (mf/deps project) - (fn [event] - (st/emit! (dm/success (tr "dashboard.success-delete-project")) - (dd/delete-project project) - (rt/nav :dashboard-projects {:team-id (:team-id project)})))) + (fn [event] + (st/emit! (dm/success (tr "dashboard.success-delete-project")) + (dd/delete-project project) + (dd/go-to-projects (:team-id project)))) on-delete - (mf/use-callback - (mf/deps project) - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.delete-project-confirm.title") - :message (tr "modals.delete-project-confirm.message") - :accept-label (tr "modals.delete-project-confirm.accept") - :on-accept delete-fn})))] + (st/emitf + (modal/show + {:type :confirm + :title (tr "modals.delete-project-confirm.title") + :message (tr "modals.delete-project-confirm.message") + :accept-label (tr "modals.delete-project-confirm.accept") + :on-accept delete-fn}))] - (mf/use-layout-effect - (mf/deps show?) - (fn [] - (if show? - (->> (rp/query! :teams) - (rx/map (fn [teams] - (remove #(= (:id %) current-team-id) teams))) - (rx/subs #(reset! teams %))) - (reset! teams [])))) - - (when @teams - [:& 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 [[(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))])]) - [:separator] - [(tr "labels.delete") on-delete]]}]))) + [:& 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 [[(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))])]) + [:separator] + [(tr "labels.delete") on-delete]]}])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index b75d0545c..911f6afc4 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -25,45 +25,29 @@ (mf/defc header {::mf/wrap [mf/memo]} - [{:keys [locale team] :as props}] - (let [create #(st/emit! (dd/create-project {:team-id (:id team)}))] + [] + (let [create (st/emitf (dd/create-project))] [:header.dashboard-header [:div.dashboard-title - [:h1 (t locale "dashboard.projects-title")]] + [:h1 (tr "dashboard.projects-title")]] [:a.btn-secondary.btn-small {:on-click create} - (t locale "dashboard.new-project")]])) - -(defn files-ref - [project-id] - (l/derived (l/in [:files project-id]) st/state)) - -(defn recent-ref - [project-id] - (l/derived (l/in [:recent-files project-id]) st/state)) + (tr "dashboard.new-project")]])) (mf/defc project-item - [{:keys [project first? locale] :as props}] - (let [files-ref (mf/use-memo (mf/deps project) #(files-ref (:id project))) - recent-ref (mf/use-memo (mf/deps project) #(recent-ref (:id project))) - - files-map (mf/deref files-ref) - recent-ids (mf/deref recent-ref) - - files (some->> recent-ids - (map #(get files-map %)) - (sort-by :modified-at) - (filter some?) - (reverse)) + [{:keys [project first? files] :as props}] + (let [locale (mf/deref i18n/locale) project-id (:id project) team-id (:team-id project) file-count (or (:count project) 0) - dstate (mf/deref refs/dashboard-local) - edit-id (:project-for-edit dstate) - local (mf/use-state {:menu-open false - :menu-pos nil - :edition? (= (:id project) edit-id)}) + dstate (mf/deref refs/dashboard-local) + edit-id (:project-for-edit dstate) + + local + (mf/use-state {:menu-open false + :menu-pos nil + :edition? (= (:id project) edit-id)}) on-nav (mf/use-callback @@ -131,12 +115,15 @@ (if (:is-default project) (tr "labels.drafts") (:name project))]) - [:& project-menu {:project project - :show? (:menu-open @local) - :left (:x (:menu-pos @local)) - :top (:y (:menu-pos @local)) - :on-edit on-edit-open - :on-menu-close on-menu-close}] + + (when (:menu-open @local) + [:& project-menu {:project project + :show? (:menu-open @local) + :left (:x (:menu-pos @local)) + :top (:y (:menu-pos @local)) + :on-edit on-edit-open + :on-menu-close on-menu-close}]) + [:span.info (str file-count " files")] (when (> file-count 0) (let [time (-> (:modified-at project) @@ -145,20 +132,25 @@ [:a.btn-secondary.btn-small {:on-click create-file} - (t locale "dashboard.new-file")]] + (tr "dashboard.new-file")]] [:& line-grid {:project-id (:id project) + :project project :team-id team-id :on-load-more on-nav :files files}]])) + +(def recent-files-ref + (l/derived :dashboard-recent-files st/state)) + (mf/defc projects-section [{:keys [team projects] :as props}] - (let [projects (->> (vals projects) - (sort-by :modified-at) - (reverse)) - locale (mf/deref i18n/locale)] + (let [projects (->> (vals projects) + (sort-by :modified-at) + (reverse)) + recent-map (mf/deref recent-files-ref)] (mf/use-effect (mf/deps team) @@ -166,18 +158,22 @@ (dom/set-html-title (tr "title.dashboard.projects" (if (:is-default team) (tr "dashboard.your-penpot") - (:name team)))) - (st/emit! (dd/fetch-recent-files {:team-id (:id team)}) - (dd/clear-selected-files)))) + (:name team)))))) + + (mf/use-effect + (st/emitf (dd/fetch-recent-files) + (dd/clear-selected-files))) (when (seq projects) [:* - [:& header {:locale locale - :team team}] + [:& header] [:section.dashboard-container - (for [project projects] - [:& project-item {:project project - :locale locale - :first? (= project (first projects)) - :key (:id project)}])]]))) + (for [{:keys [id] :as project} projects] + (let [files (when recent-map + (->> (vals recent-map) + (filterv #(= id (:project-id %)))))] + [:& project-item {:project project + :files files + :first? (= project (first projects)) + :key (:id project)}]))]]))) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 2f9568e14..d3c6ed9ed 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -7,56 +7,52 @@ (ns app.main.ui.dashboard.search (:require [app.main.data.dashboard :as dd] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]] + [app.util.i18n :as i18n :refer [tr]] [okulary.core :as l] [rumext.alpha :as mf])) - -(def result-ref - (l/derived (l/in [:dashboard-local :search-result]) st/state)) - (mf/defc search-page [{:keys [team search-term] :as props}] - (let [result (mf/deref result-ref) - locale (mf/deref i18n/locale)] - + (let [result (mf/deref refs/dashboard-search-result)] (mf/use-effect - (mf/deps team search-term) + (mf/deps team) (fn [] - (dom/set-html-title (t locale "title.dashboard.search" + (dom/set-html-title (tr "title.dashboard.search" (if (:is-default team) - (t locale "dashboard.your-penpot") - (:name team)))) - (when search-term - (st/emit! (dd/search-files {:team-id (:id team) - :search-term search-term}) - (dd/clear-selected-files))))) + (tr "dashboard.your-penpot") + (:name team)))))) + (mf/use-effect + (mf/deps search-term) + (fn [] + (st/emit! (dd/search {:search-term search-term}) + (dd/clear-selected-files)))) [:* [:header.dashboard-header [:div.dashboard-title - [:h1 (t locale "dashboard.title-search")]]] + [:h1 (tr "dashboard.title-search")]]] [:section.dashboard-container.search (cond (empty? search-term) [:div.grid-empty-placeholder [:div.icon i/search] - [:div.text (t locale "dashboard.type-something")]] + [:div.text (tr "dashboard.type-something")]] (nil? result) [:div.grid-empty-placeholder [:div.icon i/search] - [:div.text (t locale "dashboard.searching-for" search-term)]] + [:div.text (tr "dashboard.searching-for" search-term)]] (empty? result) [:div.grid-empty-placeholder [:div.icon i/search] - [:div.text (t locale "dashboard.no-matches-for" search-term)]] + [:div.text (tr "dashboard.no-matches-for" search-term)]] :else [:& grid {:files result diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index f2b331a0e..b35edc7e0 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.spec :as us] [app.config :as cfg] - [app.main.data.auth :as da] [app.main.data.comments :as dcm] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] @@ -28,7 +27,7 @@ [app.util.avatars :as avatars] [app.util.dom :as dom] [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.object :as obj] [app.util.router :as rt] @@ -47,24 +46,26 @@ selected-project (:selected-project dstate) edit-id (:project-for-edit dstate) - local (mf/use-state {:menu-open false - :menu-pos nil - :edition? (= (:id item) edit-id) - :dragging? false}) + local (mf/use-state + {:menu-open false + :menu-pos nil + :edition? (= (:id item) edit-id) + :dragging? false}) on-click (mf/use-callback (mf/deps item) (fn [] - (st/emit! (rt/nav :dashboard-files {:team-id (:team-id item) - :project-id (:id item)})))) + (st/emit! (dd/go-to-files (:id item))))) on-menu-click - (mf/use-callback (fn [event] - (let [position (dom/get-client-position event)] - (dom/prevent-default event) - (swap! local assoc :menu-open true - :menu-pos position)))) + (mf/use-callback + (fn [event] + (let [position (dom/get-client-position event)] + (dom/prevent-default event) + (swap! local assoc + :menu-open true + :menu-pos position)))) on-menu-close (mf/use-callback #(swap! local assoc :menu-open false)) @@ -101,21 +102,22 @@ (when-not (dnd/from-child? e) (swap! local assoc :dragging? false)))) + on-drop-success + (mf/use-callback + (mf/deps (:id item)) + (st/emitf (dm/success (tr "dashboard.success-move-file")) + (dd/go-to-files (:id item)))) + on-drop (mf/use-callback - (mf/deps item selected-files) - (fn [e] - (swap! local assoc :dragging? false) - (when (not= selected-project (:id item)) - (let [data {:ids selected-files - :project-id (:id item)} - - mdata {:on-success - (st/emitf (dm/success (tr "dashboard.success-move-file")) - (rt/nav :dashboard-files - {:team-id team-id - :project-id (:id item)}))}] - (st/emit! (dd/move-files (with-meta data mdata)))))))] + (mf/deps item selected-files) + (fn [e] + (swap! local assoc :dragging? false) + (when (not= selected-project (:id item)) + (let [data {:ids selected-files + :project-id (:id item)} + mdata {:on-success on-drop-success}] + (st/emit! (dd/move-files (with-meta data mdata)))))))] [:* [:li {:class (if selected? "current" @@ -139,7 +141,7 @@ :on-menu-close on-menu-close}]])) (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 "") focused? (mf/use-state false) emit! (mf/use-memo #(f/debounce st/emit! 500)) @@ -149,12 +151,9 @@ (mf/deps team-id) (fn [event] (reset! focused? true) - (let [target (dom/get-target event) - value (dom/get-value target)] - (dom/select-text! target) - (if (empty? value) - (emit! (rt/nav :dashboard-search {:team-id team-id} {})) - (emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value})))))) + (let [value (dom/get-target-val event)] + (dom/select-text! (dom/get-target event)) + (emit! (dd/go-to-search value))))) on-search-blur (mf/use-callback @@ -165,9 +164,8 @@ (mf/use-callback (mf/deps team-id) (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value))] - (emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value}))))) + (let [value (dom/get-target-val event)] + (emit! (dd/go-to-search value))))) on-clear-click (mf/use-callback @@ -176,14 +174,14 @@ (let [search-input (dom/get-element "search-input")] (dom/clean-value! search-input) (dom/focus! search-input) - (emit! (rt/nav :dashboard-search {:team-id team-id} {})))))] + (emit! (dd/go-to-search)))))] [:form.sidebar-search [:input.input-text {:key :images-search-box :id "search-input" :type "text" - :placeholder (t locale "dashboard.search-placeholder") + :placeholder (tr "dashboard.search-placeholder") :default-value search-term :auto-complete "off" :on-focus on-search-focus @@ -201,7 +199,7 @@ i/search])])) (mf/defc teams-selector-dropdown - [{:keys [team profile locale] :as props}] + [{:keys [team profile] :as props}] (let [show-dropdown? (mf/use-state false) teams (mf/deref refs/teams) @@ -211,16 +209,15 @@ team-selected (mf/use-callback - (fn [team-id] - (da/set-current-team! team-id) - (st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))] + (fn [team-id] + (st/emit! (dd/go-to-projects team-id))))] [:ul.dropdown.teams-dropdown - [:li.title (t locale "dashboard.switch-team")] + [:li.title (tr "dashboard.switch-team")] [:hr] [:li.team-name {:on-click (partial team-selected (:default-team-id profile))} [: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))] [:* {:key (:id team)} @@ -231,7 +228,7 @@ [:hr] [: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 ::leave-modal-form @@ -241,21 +238,17 @@ {::mf/register modal/components ::mf/register-as ::leave-and-reassign} [{:keys [members profile team accept]}] - (let [form (fm/use-form :spec ::leave-modal-form :initial {}) - not-current-user? (fn [{:keys [id]}] (not= id (:id profile))) - members (->> members (filterv not-current-user?)) - options (into [{:value "" :label (tr "modals.leave-and-reassign.select-memeber-to-promote")}] - (map #(hash-map :label (:name %) :value (str (:id %))) members)) - - on-cancel - (mf/use-callback (st/emitf (modal/hide))) + (let [form (fm/use-form :spec ::leave-modal-form :initial {}) + members (some->> members (filterv #(not= (:id %) (:id profile)))) + options (into [{:value "" + :label (tr "modals.leave-and-reassign.select-memeber-to-promote")}] + (map #(hash-map :label (:name %) :value (str (:id %))) members)) + on-cancel (st/emitf (modal/hide)) on-accept - (mf/use-callback - (mf/deps form) - (fn [event] - (let [member-id (get-in @form [:clean-data :member-id])] - (accept member-id))))] + (fn [event] + (let [member-id (get-in @form [:clean-data :member-id])] + (accept member-id)))] [:div.modal-overlay [:div.modal-container.confirm-dialog @@ -290,113 +283,81 @@ :value (tr "modals.leave-and-reassign.promote-and-leave") :on-click on-accept}]]]]])) - (mf/defc team-options-dropdown - [{:keys [team locale profile] :as props}] - (let [members (mf/use-state []) + [{:keys [team profile] :as props}] + (let [go-members (st/emitf (dd/go-to-team-members)) + go-settings (st/emitf (dd/go-to-team-settings)) - go-members - (mf/use-callback - (mf/deps team) - (st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)}))) - - go-settings - (mf/use-callback - (mf/deps team) - (st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)}))) + members-map (mf/deref refs/dashboard-team-members) + members (vals members-map) on-create-clicked - (mf/use-callback - (st/emitf (modal/show :team-form {}))) + (st/emitf (modal/show :team-form {})) on-rename-clicked - (mf/use-callback - (mf/deps team) - (st/emitf (modal/show :team-form {:team team}))) + (st/emitf (modal/show :team-form {:team team})) on-leaved-success - (mf/use-callback - (mf/deps team profile) - (fn [] - (let [team-id (:default-team-id profile)] - (da/set-current-team! team-id) - (st/emit! (modal/hide) - (du/fetch-teams) - (rt/nav :dashboard-projects {:team-id team-id}))))) + (fn [] + (st/emit! (modal/hide) + (dd/go-to-projects (:default-team-id profile)))) leave-fn - (mf/use-callback - (mf/deps team) - (st/emitf (dd/leave-team (with-meta team {:on-success on-leaved-success})))) + (st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success}))) leave-and-reassign-fn - (mf/use-callback - (mf/deps team) - (fn [member-id] - (let [team (assoc team :reassign-to member-id)] - (st/emit! (dd/leave-team (with-meta team {:on-success on-leaved-success})))))) + (fn [member-id] + (let [params {:reassign-to member-id}] + (st/emit! (dd/leave-team (with-meta params {:on-success on-leaved-success}))))) on-leave-clicked - (mf/use-callback - (mf/deps team) - (st/emitf (modal/show - {:type :confirm - :title (t locale "modals.leave-confirm.title") - :message (t locale "modals.leave-confirm.message") - :accept-label (t locale "modals.leave-confirm.accept") - :on-accept leave-fn}))) + (st/emitf (modal/show + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-confirm.message") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept leave-fn})) on-leave-as-owner-clicked - (mf/use-callback - (mf/deps team @members) - (st/emitf (modal/show - {:type ::leave-and-reassign - :profile profile - :team team - :accept leave-and-reassign-fn - :members @members}))) + (st/emitf (modal/show + {:type ::leave-and-reassign + :profile profile + :team team + :members members + :accept leave-and-reassign-fn})) delete-fn - (mf/use-callback - (mf/deps team) - (st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success})))) + (st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success}))) on-delete-clicked - (mf/use-callback - (mf/deps team) - (st/emitf (modal/show - {:type :confirm - :title (t locale "modals.delete-team-confirm.title") - :message (t locale "modals.delete-team-confirm.message") - :accept-label (t locale "modals.delete-team-confirm.accept") - :on-accept delete-fn})))] - - (mf/use-layout-effect - (mf/deps (:id team)) - (fn [] - (->> (rp/query! :team-members {:team-id (:id team)}) - (rx/subs #(reset! members %))))) + (st/emitf + (modal/show + {:type :confirm + :title (tr "modals.delete-team-confirm.title") + :message (tr "modals.delete-team-confirm.message") + :accept-label (tr "modals.delete-team-confirm.accept") + :on-accept delete-fn}))] [:ul.dropdown.options-dropdown - [:li {:on-click go-members} (t locale "labels.members")] - [:li {:on-click go-settings} (t locale "labels.settings")] + [:li {:on-click go-members} (tr "labels.members")] + [:li {:on-click go-settings} (tr "labels.settings")] [:hr] - [:li {:on-click on-rename-clicked} (t locale "labels.rename")] + [:li {:on-click on-rename-clicked} (tr "labels.rename")] (cond (: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) - [:li {:on-click on-leave-clicked} (t locale "dashboard.leave-team")]) + (> (count members) 1) + [:li {:on-click on-leave-clicked} (tr "dashboard.leave-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 - [{:keys [team profile locale] :as props}] + [{:keys [team profile] :as props}] (let [show-dropdown? (mf/use-state false) show-team-opts-ddwn? (mf/use-state false) @@ -408,7 +369,7 @@ (if (:is-default team) [:div.team-name [: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 [:span.team-icon [:img {:src (cfg/resolve-team-photo-url team)}]] @@ -425,23 +386,22 @@ [:& dropdown {:show @show-teams-ddwn? :on-close #(reset! show-teams-ddwn? false)} [:& teams-selector-dropdown {:team team - :profile profile - :locale locale}]] + :profile profile}]] [:& dropdown {:show @show-team-opts-ddwn? :on-close #(reset! show-team-opts-ddwn? false)} [:& team-options-dropdown {:team team - :profile profile - :locale locale}]]])) + :profile profile}]]])) (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 (->> (vals projects) (d/seek :is-default) (:id)) projects? (= section :dashboard-projects) + fonts? (= section :dashboard-fonts) libs? (= section :dashboard-libraries) drafts? (and (= section :dashboard-files) (= (:id project) default-project-id)) @@ -451,6 +411,11 @@ (mf/deps 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 (mf/use-callback (mf/deps team default-project-id) @@ -469,29 +434,36 @@ (filter :is-pinned))] [:div.sidebar-content - [:& sidebar-team-switch {:team team :profile profile :locale locale}] + [:& sidebar-team-switch {:team team :profile profile}] [:hr] [:& sidebar-search {:search-term search-term - :team-id (:id team) - :locale locale}] + :team-id (:id team)}] [:div.sidebar-content-section [:ul.sidebar-nav.no-overflow [:li.recent-projects {:on-click go-projects :class-name (when projects? "current")} - [:span.element-title (t locale "labels.projects")]] + [:span.element-title (tr "labels.projects")]] [:li {:on-click go-drafts :class-name (when drafts? "current")} - [:span.element-title (t locale "labels.drafts")]] + [:span.element-title (tr "labels.drafts")]] [:li {:on-click go-libs :class-name (when libs? "current")} - [:span.element-title (t locale "labels.shared-libraries")]]]] + [:span.element-title (tr "labels.shared-libraries")]]]] [: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 (if (seq pinned-projects) [:ul.sidebar-nav @@ -504,11 +476,11 @@ :selected? (= (:id item) (:id project))}])] [:div.sidebar-empty-placeholder [:span.icon i/pin] - [:span.text (t locale "dashboard.no-projects-placeholder")]])]])) + [:span.text (tr "dashboard.no-projects-placeholder")]])]])) (mf/defc profile-section - [{:keys [profile locale team] :as props}] + [{:keys [profile team] :as props}] (let [show (mf/use-state false) photo (cfg/resolve-profile-photo-url profile) @@ -530,18 +502,18 @@ [:ul.dropdown [:li {:on-click (partial on-click :settings-profile)} [:span.icon i/user] - [:span.text (t locale "labels.profile")]] + [:span.text (tr "labels.profile")]] [:li {:on-click (partial on-click :settings-password)} [:span.icon i/lock] - [:span.text (t locale "labels.password")]] - [:li {:on-click (partial on-click (da/logout))} + [:span.text (tr "labels.password")]] + [:li {:on-click (partial on-click (du/logout))} [:span.icon i/exit] - [:span.text (t locale "labels.logout")]] + [:span.text (tr "labels.logout")]] (when cfg/feedback-enabled [:li.feedback {:on-click (partial on-click :settings-feedback)} [:span.icon i/msg-info] - [:span.text (t locale "labels.give-feedback")] + [:span.text (tr "labels.give-feedback")] [:span.primary-badge "ALPHA"]])]]] (when (and team profile) @@ -552,15 +524,11 @@ {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] - (let [locale (mf/deref i18n/locale) - team (obj/get props "team") - profile (obj/get props "profile") - props (-> (obj/clone props) - (obj/set! "locale" locale))] + (let [team (obj/get props "team") + profile (obj/get props "profile")] [:div.dashboard-sidebar [:div.sidebar-inside [:> sidebar-content props] [:& profile-section {:profile profile - :team team - :locale locale}]]])) + :team team}]]])) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 64eed4bee..168a944c2 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -32,22 +32,9 @@ (mf/defc header {::mf/wrap [mf/memo]} [{:keys [section team] :as props}] - (let [go-members - (mf/use-callback - (mf/deps team) - (st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)}))) - - go-settings - (mf/use-callback - (mf/deps team) - (st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)}))) - - invite-member - (mf/use-callback - (mf/deps team) - (st/emitf (modal/show {:type ::invite-member - :team team}))) - + (let [go-members (st/emitf (dd/go-to-team-members)) + go-settings (st/emitf (dd/go-to-team-settings)) + invite-member (st/emitf (modal/show {:type ::invite-member})) members-section? (= section :dashboard-team-members) settings-section? (= section :dashboard-team-settings)] @@ -69,6 +56,16 @@ (tr "dashboard.invite-profile")] [:div])])) +(defn get-available-roles + [] + [{:value "" :label (tr "labels.role")} + {:value "admin" :label (tr "labels.admin")} + {:value "editor" :label (tr "labels.editor")} + ;; Temporarily disabled viewer role + ;; https://tree.taiga.io/project/uxboxproject/issue/1083 + ;; {:value "viewer" :label (tr "labels.viewer")} + ]) + (s/def ::email ::us/email) (s/def ::role ::us/keyword) (s/def ::invite-member-form @@ -77,53 +74,40 @@ (mf/defc invite-member-modal {::mf/register modal/components ::mf/register-as ::invite-member} - [{:keys [team] :as props}] - (let [roles [{:value "" :label (tr "labels.role")} - {:value "admin" :label (tr "labels.admin")} - {:value "editor" :label (tr "labels.editor")}] - ;; Temporarily disabled viewer role - ;; https://tree.taiga.io/project/uxboxproject/issue/1083 - ;; {:value "viewer" :label (tr "labels.viewer")}] - - initial (mf/use-memo (mf/deps team) (constantly {:team-id (:id team) - :role "editor"})) + [] + (let [roles (mf/use-memo get-available-roles) + initial (mf/use-memo (constantly {:role "editor"})) form (fm/use-form :spec ::invite-member-form :initial initial) on-success - (mf/use-callback - (mf/deps team) - (st/emitf (dm/success (tr "notifications.invitation-email-sent")) - (modal/hide))) + (st/emitf (dm/success (tr "notifications.invitation-email-sent")) + (modal/hide)) on-error - (mf/use-callback - (mf/deps team) - (fn [form {:keys [type code] :as error}] - (let [email (get @form [:data :email])] - (cond - (and (= :validation type) - (= :profile-is-muted code)) - (dm/error (tr "errors.profile-is-muted")) + (fn [form {:keys [type code] :as error}] + (let [email (get @form [:data :email])] + (cond + (and (= :validation type) + (= :profile-is-muted code)) + (dm/error (tr "errors.profile-is-muted")) - (and (= :validation type) - (= :member-is-muted code)) - (dm/error (tr "errors.member-is-muted")) + (and (= :validation type) + (= :member-is-muted code)) + (dm/error (tr "errors.member-is-muted")) - (and (= :validation type) - (= :email-has-permanent-bounces)) - (dm/error (tr "errors.email-has-permanent-bounces" email)) + (and (= :validation type) + (= :email-has-permanent-bounces)) + (dm/error (tr "errors.email-has-permanent-bounces" email)) - :else - (dm/error (tr "errors.generic")))))) + :else + (dm/error (tr "errors.generic"))))) on-submit - (mf/use-callback - (mf/deps team) - (fn [form] - (let [params (:clean-data @form) - mdata {:on-success (partial on-success form) - :on-error (partial on-error form)}] - (st/emit! (dd/invite-team-member (with-meta params mdata))))))] + (fn [form] + (let [params (:clean-data @form) + mdata {:on-success (partial on-success form) + :on-error (partial on-error form)}] + (st/emit! (dd/invite-team-member (with-meta params mdata)))))] [:div.modal.dashboard-invite-modal.form-container [:& fm/form {:on-submit on-submit :form form} @@ -139,50 +123,39 @@ [:div.action-buttons [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]])) - (mf/defc team-member + {::mf/wrap [mf/memo]} [{:keys [team member profile] :as props}] (let [show? (mf/use-state false) set-role - #(st/emit! (dd/update-team-member-role {:team-id (:id team) - :member-id (:id member) - :role %})) - set-owner-fn - (partial set-role :owner) + (fn [role] + (let [params {:member-id (:id member) :role role}] + (st/emit! (dd/update-team-member-role params)))) - set-admin - (mf/use-callback (mf/deps team member) (partial set-role :admin)) - - set-editor - (mf/use-callback (mf/deps team member) (partial set-role :editor)) - - set-viewer - (mf/use-callback (mf/deps team member) (partial set-role :viewer)) + set-owner-fn (partial set-role :owner) + set-admin (partial set-role :admin) + set-editor (partial set-role :editor) + set-viewer (partial set-role :viewer) set-owner - (mf/use-callback - (mf/deps team member) - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.promote-owner-confirm.title") - :message (tr "modals.promote-owner-confirm.message") - :accept-label (tr "modals.promote-owner-confirm.accept") - :on-accept set-owner-fn}))) + (st/emitf (modal/show + {:type :confirm + :title (tr "modals.promote-owner-confirm.title") + :message (tr "modals.promote-owner-confirm.message") + :accept-label (tr "modals.promote-owner-confirm.accept") + :on-accept set-owner-fn})) delete-fn - (st/emitf (dd/delete-team-member {:team-id (:id team) :member-id (:id member)})) + (st/emitf (dd/delete-team-member {:member-id (:id member)})) delete - (mf/use-callback - (mf/deps team member) - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.delete-team-member-confirm.title") - :message (tr "modals.delete-team-member-confirm.message") - :accept-label (tr "modals.delete-team-member-confirm.accept") - :on-accept delete-fn})))] - + (st/emitf (modal/show + {:type :confirm + :title (tr "modals.delete-team-member-confirm.title") + :message (tr "modals.delete-team-member-confirm.message") + :accept-label (tr "modals.delete-team-member-confirm.accept") + :on-accept delete-fn}))] [:div.table-row [:div.table-field.name (:name member)] @@ -244,23 +217,21 @@ (for [item members] [:& team-member {:member item :team team :profile profile :key (:id item)}])]])) -(defn- members-ref - [{:keys [id] :as team}] - (l/derived (l/in [:team-members id]) st/state)) - (mf/defc team-members-page [{:keys [team profile] :as props}] - (let [members-ref (mf/use-memo (mf/deps team) #(members-ref team)) - members-map (mf/deref members-ref)] + (let [members-map (mf/deref refs/dashboard-team-members)] (mf/use-effect (mf/deps team) (fn [] - (dom/set-html-title (tr "title.team-members" - (if (:is-default team) - (tr "dashboard.your-penpot") - (:name team)))) - (st/emit! (dd/fetch-team-members team)))) + (dom/set-html-title + (tr "title.team-members" + (if (:is-default team) + (tr "dashboard.your-penpot") + (:name team)))))) + + (mf/use-effect + (st/emitf (dd/fetch-team-members))) [:* [:& header {:section :dashboard-team-members @@ -270,42 +241,35 @@ :team team :members-map members-map}]]])) -(defn- stats-ref - [{:keys [id] :as team}] - (l/derived (l/in [:team-stats id]) st/state)) - (mf/defc team-settings-page [{:keys [team profile] :as props}] (let [finput (mf/use-ref) - members-ref (mf/use-memo (mf/deps team) #(members-ref team)) - members-map (mf/deref members-ref) - + members-map (mf/deref refs/dashboard-team-members) owner (->> (vals members-map) (d/seek :is-owner)) - stats-ref (mf/use-memo (mf/deps team) #(stats-ref team)) - stats (mf/deref stats-ref) + stats (mf/deref refs/dashboard-team-stats) on-image-click (mf/use-callback #(dom/click (mf/ref-val finput))) on-file-selected - (mf/use-callback - (mf/deps team) - (fn [file] - (st/emit! (dd/update-team-photo {:file file - :team-id (:id team)}))))] + (fn [file] + (st/emit! (dd/update-team-photo {:file file})))] + (mf/use-effect - (mf/deps team) - (fn [] - (dom/set-html-title (tr "title.team-settings" - (if (:is-default team) - (tr "dashboard.your-penpot") - (:name team)))) - (st/emit! (dd/fetch-team-members team) - (dd/fetch-team-stats team)))) + (mf/deps team) + (fn [] + (dom/set-html-title (tr "title.team-settings" + (if (:is-default team) + (tr "dashboard.your-penpot") + (:name team)))))) + + (mf/use-effect + (st/emitf (dd/fetch-team-members) + (dd/fetch-team-stats))) [:* [:& header {:section :dashboard-team-settings diff --git a/frontend/src/app/main/ui/handoff.cljs b/frontend/src/app/main/ui/handoff.cljs index 0fd481771..ee82788f0 100644 --- a/frontend/src/app/main/ui/handoff.cljs +++ b/frontend/src/app/main/ui/handoff.cljs @@ -91,7 +91,7 @@ (events/unlistenByKey key1))))] (mf/use-effect on-mount) - (hooks/use-shortcuts sc/shortcuts) + (hooks/use-shortcuts ::handoff sc/shortcuts) [:div.handoff-layout {:class (dom/classnames :force-visible (:show-thumbnails state))} diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index bc2a8fd2d..5b0b0645e 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -9,10 +9,11 @@ (:require [app.common.spec :as us] [app.main.data.shortcuts :as dsc] + [app.main.store :as st] [app.util.dom :as dom] - [app.util.object :as obj] [app.util.dom.dnd :as dnd] [app.util.logging :as log] + [app.util.object :as obj] [app.util.timers :as ts] [app.util.transit :as t] [app.util.webapi :as wapi] @@ -35,11 +36,13 @@ state)) (defn use-shortcuts - [shortcuts] + [key shortcuts] (mf/use-effect + #js [(str key) shortcuts] (fn [] - (dsc/bind-shortcuts shortcuts) - (fn [] (dsc/remove-shortcuts))))) + (st/emit! (dsc/push-shortcuts key shortcuts)) + (fn [] + (st/emit! (dsc/pop-shortcuts key)))))) (defn invisible-image [] diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 0d6fc0d8b..75dce1ac1 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -235,7 +235,7 @@ [{:keys [bounds frame selected-shapes hover-shape zoom]}] (let [selected-ids (into #{} (map :id) selected-shapes) selected-selrect (gsh/selection-rect selected-shapes) - hover-selrect (:selrect hover-shape) + hover-selrect (-> hover-shape :points gsh/points->selrect) bounds-selrect (bound->selrect bounds) hover-selected-shape? (not (contains? selected-ids (:id hover-shape)))] diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs index e1848c969..70aecb9b2 100644 --- a/frontend/src/app/main/ui/messages.cljs +++ b/frontend/src/app/main/ui/messages.cljs @@ -18,33 +18,33 @@ (mf/defc banner [{:keys [type position status controls content actions on-close] :as props}] [:div.banner {:class (dom/classnames - :warning (= type :warning) - :error (= type :error) - :success (= type :success) - :info (= type :info) - :fixed (= position :fixed) - :floating (= position :floating) - :inline (= position :inline) - :hide (= status :hide))} + :warning (= type :warning) + :error (= type :error) + :success (= type :success) + :info (= type :info) + :fixed (= position :fixed) + :floating (= position :floating) + :inline (= position :inline) + :hide (= status :hide))} [:div.wrapper - [:div.icon (case type - :warning i/msg-warning - :error i/msg-error - :success i/msg-success - :info i/msg-info - i/msg-error)] - [:div.content {:class (dom/classnames - :inline-actions (= controls :inline-actions) - :bottom-actions (= controls :bottom-actions))} - content - (when (or (= controls :bottom-actions) (= controls :inline-actions)) - [:div.actions - (for [action actions] - [:div.btn-secondary.btn-small {:key (uuid/next) - :on-click (:callback action)} - (:label action)])])] - (when (= controls :close) - [:div.btn-close {:on-click on-close} i/close])]]) + [:div.icon (case type + :warning i/msg-warning + :error i/msg-error + :success i/msg-success + :info i/msg-info + i/msg-error)] + [:div.content {:class (dom/classnames + :inline-actions (= controls :inline-actions) + :bottom-actions (= controls :bottom-actions))} + content + (when (or (= controls :bottom-actions) (= controls :inline-actions)) + [:div.actions + (for [action actions] + [:div.btn-secondary.btn-small {:key (uuid/next) + :on-click (:callback action)} + (:label action)])])] + (when (= controls :close) + [:div.btn-close {:on-click on-close} i/close])]]) (mf/defc notifications [] diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index a14f22a9f..307915145 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -34,7 +34,7 @@ (mf/defc onboarding-start [{:keys [next] :as props}] [:div.modal-container.onboarding - [:div.modal-left + [:div.modal-left.welcome [:img {:src "images/login-on.jpg" :border "0" :alt "Penpot"}]] [:div.modal-right [:div.modal-title @@ -296,7 +296,7 @@ (defmethod render-release-notes "0.0" [params] - (render-release-notes (assoc params :version "1.5"))) + (render-release-notes (assoc params :version "1.6"))) (defmethod render-release-notes "1.4" [{:keys [slide klass next finish navigate version]}] @@ -474,3 +474,101 @@ {:slide @slide :navigate navigate :total 3}]]]]]]))) + +(defmethod render-release-notes "1.6" + [{:keys [slide klass next finish navigate version]}] + (mf/html + (case @slide + :start + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.6.0"}]] + [:div.modal-right + [:div.modal-title + [:h2 "What's new?"]] + [:span.release "Alpha version " version] + [:div.modal-content + [:p "Penpot continues growing with new features that improve performance, user experience and visual design."] + [:p "We are happy to show you a sneak peak of the most important stuff that the Alpha 1.6.0 version brings."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]] + + 0 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/custom-fonts.gif" :border "0" :alt "Upload/use custom fonts"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Upload/use custom fonts"]] + [:div.modal-content + [:p "From now on you can upload fonts to a Penpot team and use them across its files. This is one of the most requested features since our first release (we listen!)"] + [:p "We hope you enjoy having more typography options and our brand new font selector."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 1 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/scale-text.gif" :border "0" :alt "Interactively scale text"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Scale text layers at resizing"]] + [:div.modal-content + [:p "New main menu option “Scale text (K)” to enable scale text mode."] + [:p "Disabled by default, this tool is disabled back after being used."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 2 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/performance.gif" :border "0" :alt "Performance improvements"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Performance improvements"]] + [:div.modal-content + [:p "Penpot brings important improvements handling large files. The performance in managing files in the dashboard has also been improved."] + [:p "You should have the feeling that files and layers show up a bit faster :)"]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 3 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/shapes-to-path.gif" :border "0" :alt "Shapes to path"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Shapes to path"]] + [:div.modal-content + [:p "Now you can edit basic shapes like rectangles, circles and image containers by double clicking."] + [:p "An easy way to increase speed by working with vectors!"]] + [:div.modal-navigation + [:button.btn-secondary {:on-click finish} "Start!"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index 36402814e..46726c87f 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.settings.change-email (:require [app.common.spec :as us] - [app.main.data.auth :as da] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs index abf29ccc5..f26881ddb 100644 --- a/frontend/src/app/main/ui/settings/delete_account.cljs +++ b/frontend/src/app/main/ui/settings/delete_account.cljs @@ -6,14 +6,13 @@ (ns app.main.ui.settings.delete-account (:require - [app.main.data.auth :as da] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -26,42 +25,36 @@ (rx/of (dm/error msg))) (rx/throw error))) -(defn on-success - [x] - (st/emit! (rt/nav :auth-login))) - (mf/defc delete-account-modal {::mf/register modal/components ::mf/register-as :delete-account} [props] - (let [locale (mf/deref i18n/locale) - on-close + (let [on-close (mf/use-callback (st/emitf (modal/hide))) on-accept (mf/use-callback (st/emitf (modal/hide) - (da/request-account-deletion - (with-meta {} {:on-error on-error - :on-success on-success}))))] + (du/request-account-deletion + (with-meta {} {:on-error on-error}))))] [:div.modal-overlay [:div.modal-container.change-email-modal [:div.modal-header [:div.modal-header-title - [:h2 (t locale "modals.delete-account.title")]] + [:h2 (tr "modals.delete-account.title")]] [:div.modal-close-button {:on-click on-close} i/close]] [:div.modal-content [:& msgs/inline-banner {:type :warning - :content (t locale "modals.delete-account.info")}]] + :content (tr "modals.delete-account.info")}]] [:div.modal-footer [:div.action-buttons [:button.btn-warning.btn-large {:on-click on-accept} - (t locale "modals.delete-account.confirm")] + (tr "modals.delete-account.confirm")] [:button.btn-secondary.btn-large {:on-click on-close} - (t locale "modals.delete-account.cancel")]]]]])) + (tr "modals.delete-account.cancel")]]]]])) diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 83d4993d5..ed5347091 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -7,8 +7,8 @@ (ns app.main.ui.settings.sidebar (:require [app.config :as cf] - [app.main.data.auth :as da] [app.main.data.modal :as modal] + [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.dashboard.sidebar :refer [profile-section]] [app.main.ui.icons :as i] @@ -26,7 +26,7 @@ go-dashboard (mf/use-callback (mf/deps profile) - (st/emitf (rt/nav :dashboard-projects {:team-id (da/current-team-id profile)}))) + (st/emitf (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))) go-settings-profile (mf/use-callback diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 8573682fd..bbd09a262 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -81,6 +81,10 @@ (contains? shape :fill-color) {:fill (:fill-color shape)} + (contains? shape :fill-image) + (let [fill-image-id (str "fill-image-" render-id)] + {:fill (str/format "url(#%s)" fill-image-id) }) + ;; If contains svg-attrs the origin is svg. If it's not svg origin ;; we setup the default fill as transparent (instead of black) (and (not (contains? shape :svg-attrs)) diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs new file mode 100644 index 000000000..76acb5b07 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/fill_image.cljs @@ -0,0 +1,39 @@ +;; 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.shapes.fill-image + (:require + [app.common.geom.shapes :as gsh] + [app.config :as cfg] + [app.util.object :as obj] + [rumext.alpha :as mf] + [app.common.geom.point :as gpt] + [app.main.ui.shapes.image :as image])) + +(mf/defc fill-image-pattern + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + render-id (obj/get props "render-id")] + (when (contains? shape :fill-image) + (let [{:keys [x y width height]} (:selrect shape) + fill-image-id (str "fill-image-" render-id) + media (:fill-image shape) + {:keys [uri loading]} (image/use-image-uri media) + transform (gsh/transform-matrix shape)] + + [:pattern {:id fill-image-id + :patternUnits "userSpaceOnUse" + :x x + :y y + :height height + :width width + :patternTransform transform + :data-loading (str loading)} + [:image {:xlinkHref uri + :width width + :height height}]])))) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 22aa27c30..966619c67 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -21,7 +21,7 @@ [props] (let [childs (unchecked-get props "childs") shape (unchecked-get props "shape") - {:keys [id x y width height]} shape + {:keys [id width height]} shape props (-> (merge frame-default-props shape) (attrs/extract-style-attrs) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 4ad49835b..124ce7052 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -17,13 +17,10 @@ [beicon.core :as rx] [rumext.alpha :as mf])) -(mf/defc image-shape - {::mf/wrap-props false} - [props] - - (let [shape (unchecked-get props "shape") - {:keys [id x y width height rotation metadata]} shape - uri (cfg/resolve-file-media metadata) +(defn use-image-uri + [media] + (let [uri (mf/use-memo (mf/deps (:id media)) + #(cfg/resolve-file-media media)) embed-resources? (mf/use-ctx muc/embed-ctx) data-uri (mf/use-state (when (not embed-resources?) uri))] @@ -38,6 +35,17 @@ (rx/mapcat wapi/read-file-as-data-url) (rx/subs #(reset! data-uri %)))))) + {:uri (or @data-uri uri) + :loading (not (some? @data-uri))})) + +(mf/defc image-shape + {::mf/wrap-props false} + [props] + + (let [shape (unchecked-get props "shape") + {:keys [id x y width height rotation metadata]} shape + {:keys [uri loading]} (use-image-uri metadata)] + (let [transform (geom/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! @@ -46,17 +54,15 @@ :transform transform :width width :height height - :preserveAspectRatio "none"})) + :preserveAspectRatio "none"}) + (cond-> loading + (obj/set! "data-loading" "true"))) + on-drag-start (fn [event] ;; Prevent browser dragging of the image (dom/prevent-default event))] - (if (nil? @data-uri) - [:> "rect" (obj/merge! - props - #js {:fill "#E8E9EA" - :stroke "#000000"})] - [:> "image" (obj/merge! - props - #js {:xlinkHref @data-uri - :onDragStart on-drag-start})])))) + [:> "image" (obj/merge! + props + #js {:xlinkHref uri + :onDragStart on-drag-start})]))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 88df3cff8..82dddd238 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.uuid :as uuid] [app.main.ui.context :as muc] + [app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.svg-defs :as defs] @@ -16,8 +17,9 @@ [rumext.alpha :as mf])) (mf/defc shape-container - {::mf/wrap-props false} - [props] + {::mf/forward-ref true + ::mf/wrap-props false} + [props ref] (let [shape (obj/get props "shape") children (obj/get props "children") pointer-events (obj/get props "pointer-events") @@ -33,6 +35,7 @@ frame? (= :frame type) group-props (-> (obj/clone props) (obj/without ["shape" "children"]) + (obj/set! "ref" ref) (obj/set! "id" (str "shape-" (:id shape))) (obj/set! "filter" (filters/filter-str filter-id shape)) (obj/set! "style" styles) @@ -49,8 +52,9 @@ [:& (mf/provider muc/render-ctx) {:value render-id} [:> wrapper-tag group-props [:defs - [:& defs/svg-defs {:shape shape :render-id render-id}] - [:& filters/filters {:shape shape :filter-id filter-id}] - [:& grad/gradient {:shape shape :attr :fill-color-gradient}] - [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]] + [:& defs/svg-defs {:shape shape :render-id render-id}] + [:& filters/filters {:shape shape :filter-id filter-id}] + [:& grad/gradient {:shape shape :attr :fill-color-gradient}] + [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] + [:& fim/fill-image-pattern {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index f26386dae..48a516240 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -22,7 +22,7 @@ (let [node (obj/get props "node") text (:text node) style (sts/generate-text-styles node)] - [:span {:style style} + [:span.text-node {:style style} (if (= text "") "\u00A0" text)])) (mf/defc render-root @@ -102,6 +102,10 @@ :height (if (#{:auto-height :auto-width} grow-type) 100000 height) :style (-> (obj/new) (attrs/add-layer-props shape)) :ref ref} + ;; We use a class here because react has a bug that won't use the appropiate selector for + ;; `background-clip` + [:style ".text-node { background-clip: text; + -webkit-background-clip: text;" ] [:& render-node {:index 0 :shape shape :node content}]])) diff --git a/frontend/src/app/main/ui/shapes/text/embed.cljs b/frontend/src/app/main/ui/shapes/text/embed.cljs index 8c5fc6284..61ef3fd4e 100644 --- a/frontend/src/app/main/ui/shapes/text/embed.cljs +++ b/frontend/src/app/main/ui/shapes/text/embed.cljs @@ -62,7 +62,7 @@ "Given a font and the variant-id, retrieves the style CSS for it." [{:keys [id backend family variants] :as font} font-variant-id] (if (= :google backend) - (let [uri (fonts/gfont-url family [{:id font-variant-id}])] + (let [uri (fonts/generate-gfonts-url {:family family :variants [{:id font-variant-id}]})] (->> (http/send! {:method :get :mode :cors :omit-default-headers true diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 21825299e..8c15ccb43 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -65,9 +65,7 @@ font-id (:font-id data (:font-id txt/default-text-attrs)) font-variant-id (:font-variant-id data) - font-family (:font-family data) font-size (:font-size data) - fill-color (:fill-color data) fill-opacity (:fill-opacity data) @@ -77,7 +75,8 @@ ;; (uc/hex->rgba fill-color fill-opacity)) [r g b a] (uc/hex->rgba fill-color fill-opacity) - text-color (str/format "rgba(%s, %s, %s, %s)" r g b a) + text-color (when (and (some? fill-color) (some? fill-opacity)) + (str/format "rgba(%s, %s, %s, %s)" r g b a)) fontsdb (deref fonts/fontsdb) base #js {:textDecoration text-decoration @@ -89,10 +88,10 @@ (let [text-color (-> (update gradient :type keyword) (uc/gradient->css))] (-> base - (obj/set! "background" "var(--text-color)") + (obj/set! "--text-color" text-color) + (obj/set! "backgroundImage" "var(--text-color)") (obj/set! "WebkitTextFillColor" "transparent") - (obj/set! "WebkitBackgroundClip" "text") - (obj/set! "--text-color" text-color)))) + (obj/set! "WebkitBackgroundClip" "text")))) (when (and (string? letter-spacing) (pos? (alength letter-spacing))) @@ -106,14 +105,15 @@ (pos? (alength font-id))) (fonts/ensure-loaded! font-id) (let [font (get fontsdb font-id)] - (let [font-family (or (:family font) - (obj/get data "fontFamily")) + (let [font-family (str/quote + (or (:family font) + (:font-family data))) font-variant (d/seek #(= font-variant-id (:id %)) (:variants font)) - font-style (or (:style font-variant) - (obj/get data "fontStyle")) - font-weight (or (:weight font-variant) - (obj/get data "fontWeight"))] + font-style (or (:style font-variant) + (:font-style data)) + font-weight (or (:weight font-variant) + (:font-weight data))] (obj/set! base "fontFamily" font-family) (obj/set! base "fontStyle" font-style) (obj/set! base "fontWeight" font-weight)))) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 86f002d21..0cd9c0b5a 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -6,21 +6,21 @@ (ns app.main.ui.static (:require - [cljs.spec.alpha :as s] - [rumext.alpha :as mf] - [app.main.ui.context :as ctx] - [app.main.data.auth :as da] [app.main.data.messages :as dm] - [app.main.store :as st] + [app.main.data.users :as du] [app.main.refs :as refs] - [cuerdas.core :as str] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] [app.util.i18n :refer [tr]] [app.util.router :as rt] - [app.main.ui.icons :as i])) + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn- go-to-dashboard [profile] - (let [team-id (da/current-team-id profile)] + (let [team-id (du/get-current-team-id profile)] (st/emit! (rt/nav :dashboard-projects {:team-id team-id})))) (mf/defc not-found @@ -38,7 +38,7 @@ [:div.sign-info [:span (tr "labels.not-found.auth-info") " " [:b (:email profile)]] [:a.btn-primary.btn-small - {:on-click (st/emitf (da/logout))} + {:on-click (st/emitf (du/logout))} (tr "labels.sign-out")]]]]])) (mf/defc bad-gateway diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index bbb940413..930968526 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -237,7 +237,7 @@ (events/unlistenByKey key3))))] (mf/use-effect on-mount) - (hooks/use-shortcuts sc/shortcuts) + (hooks/use-shortcuts ::viewer sc/shortcuts) [:div.viewer-layout {:class (dom/classnames :force-visible (:show-thumbnails state))} diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 2644e620c..46a29b984 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -10,6 +10,7 @@ [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.comments :as dcm] + [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] @@ -23,6 +24,7 @@ [app.util.router :as rt] [app.util.webapi :as wapi] [cuerdas.core :as str] + [potok.core :as ptk] [rumext.alpha :as mf])) (mf/defc zoom-widget diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 6833c69d0..6fc58ebd2 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -100,8 +100,13 @@ (mf/use-layout-effect (mf/deps page-id) (fn [] - (st/emit! (dw/initialize-page page-id)) - (st/emitf (dw/finalize-page page-id)))) + (if (nil? page-id) + (st/emit! (dw/go-to-page)) + (st/emit! (dw/initialize-page page-id))) + + (fn [] + (when page-id + (st/emit! (dw/finalize-page page-id)))))) (when page [:& workspace-content {:key page-id @@ -116,7 +121,6 @@ (mf/defc workspace {::mf/wrap [mf/memo]} [{:keys [project-id file-id page-id layout-name] :as props}] - (let [file (mf/deref refs/workspace-file) project (mf/deref refs/workspace-project) layout (mf/deref refs/workspace-layout)] @@ -134,7 +138,7 @@ (mf/use-effect (fn [] ;; Close any non-modal dialog that may be still open - (st/emitf dm/hide))) + (st/emit! dm/hide))) (mf/use-effect (mf/deps file) @@ -154,6 +158,9 @@ (if (and (and file project) (:initialized file)) - [:& workspace-page {:page-id page-id :file file :layout layout}] + [:& workspace-page {:key (str "page-" page-id) + :page-id page-id + :file file + :layout layout}] [:& workspace-loader])]]]]])) diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index 176cb0d46..d9d4df230 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -209,7 +209,7 @@ (into [] (cond (= selected :recent) (reverse recent-colors) - (= selected :file) (vals file-colors) + (= selected :file) (->> (vals file-colors) (sort-by :name)) :else (library->colors shared-libs selected)))))) (mf/use-effect @@ -222,7 +222,8 @@ (mf/deps file-colors) (fn [] (when (= selected :file) - (reset! current-library-colors (into [] (vals file-colors)))))) + (reset! current-library-colors (into [] (->> (vals file-colors) + (sort-by :name))))))) [:& palette {:left-sidebar? left-sidebar? :current-colors @current-library-colors diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 77cd0f51a..309803e9e 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -218,6 +218,13 @@ (tr "workspace.header.menu.enable-dynamic-alignment"))] [:span.shortcut (sc/get-tooltip :toggle-alignment)]] + [:li {:on-click #(st/emit! (dw/toggle-layout-flags :scale-text))} + [:span + (if (contains? layout :scale-text) + (tr "workspace.header.menu.disable-scale-text") + (tr "workspace.header.menu.enable-scale-text"))] + [:span.shortcut (sc/get-tooltip :toggle-scale-text)]] + (if (:is-shared file) [:li {:on-click on-remove-shared} [:span (tr "dashboard.remove-shared")]] diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index f948e8237..1692a392f 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -45,12 +45,12 @@ (st/emit! (dw/upload-media-workspace params)))))] [:li.tooltip.tooltip-right - {:alt (tr "workspace.toolbar.image") + {:alt (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) :on-click on-click} [:* i/image [:& file-uploader {:input-id "image-upload" - :accept cm/str-media-types + :accept cm/str-image-types :multi true :input-ref ref :on-selected on-files-selected}]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 9faf50f85..48b6f00e8 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -51,7 +51,8 @@ "Draws the root shape of the viewport and recursively all the shapes" {::mf/wrap-props false} [props] - (let [objects (obj/get props "objects") + (let [objects (obj/get props "objects") + active-frames (obj/get props "active-frames") root-shapes (get-in objects [uuid/zero :shapes]) shapes (->> root-shapes (mapv #(get objects %)))] @@ -59,7 +60,8 @@ (if (= (:type item) :frame) [:& frame-wrapper {:shape item :key (:id item) - :objects objects}] + :objects objects + :thumbnail? (not (get active-frames (:id item) false))}] [:& shape-wrapper {:shape item :key (:id item)}])))) @@ -70,7 +72,7 @@ [props] (let [shape (obj/get props "shape") frame (obj/get props "frame") - shape (-> (geom/transform-shape shape) + shape (-> (geom/transform-shape shape {:round-coords? false}) (geom/translate-to-frame frame)) opts #js {:shape shape :frame frame} diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 91e31c6ed..8875ba6e5 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -9,6 +9,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.data.workspace :as dw] + [app.main.data.workspace.changes :as dch] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as muc] @@ -17,11 +18,18 @@ [app.main.ui.shapes.text.embed :as ste] [app.util.dom :as dom] [app.util.keyboard :as kbd] + [app.util.object :as obj] [app.util.timers :as ts] [beicon.core :as rx] [okulary.core :as l] [rumext.alpha :as mf])) +(def obs-config + #js {:attributes true + :childList true + :subtree true + :characterData true}) + (defn make-is-moving-ref [id] (let [check-moving (fn [local] @@ -43,14 +51,34 @@ (let [new-shape (unchecked-get new-props "shape") old-shape (unchecked-get old-props "shape") + new-thumbnail? (unchecked-get new-props "thumbnail?") + old-thumbnail? (unchecked-get old-props "thumbnail?") + new-objects (unchecked-get new-props "objects") old-objects (unchecked-get old-props "objects") new-children (->> new-shape :shapes (mapv #(get new-objects %))) old-children (->> old-shape :shapes (mapv #(get old-objects %)))] (and (= new-shape old-shape) + (= new-thumbnail? old-thumbnail?) (= new-children old-children)))) +(mf/defc thumbnail + {::mf/wrap-props false} + [props] + (let [shape (obj/get props "shape")] + (when (:thumbnail shape) + [:image.frame-thumbnail + {:id (str "thumbnail-" (:id shape)) + :xlinkHref (:thumbnail shape) + :x (:x shape) + :y (:y shape) + :width (:width shape) + :height (:height shape) + ;; DEBUG + ;; :style {:filter "sepia(1)"} + }]))) + ;; This custom deffered don't deffer rendering when ghost rendering is ;; used. (defn custom-deferred @@ -76,6 +104,8 @@ [props] (let [shape (unchecked-get props "shape") objects (unchecked-get props "objects") + thumbnail? (unchecked-get props "thumbnail?") + edition (mf/deref refs/selected-edition) embed-fonts? (mf/use-ctx muc/embed-ctx) @@ -86,14 +116,30 @@ (filterv #(and (= :text (:type %)) (= (:id shape) (:frame-id %))))) - ds-modifier (get-in shape [:modifiers :displacement])] + ds-modifier (get-in shape [:modifiers :displacement]) + + rendered? (mf/use-state false) + + show-thumbnail? (and thumbnail? (some? (:thumbnail shape))) + + on-dom + (mf/use-callback + (fn [node] + (ts/schedule-on-idle #(reset! rendered? (some? node)))))] (when (and shape (not (:hidden shape))) [:g.frame-wrapper {:display (when (:hidden shape) "none")} - [:> shape-container {:shape shape} - (when embed-fonts? - [:& ste/embed-fontfaces-style {:shapes text-childs}]) - [:& frame-shape - {:shape shape - :childs children}]]]))))) + + (when-not show-thumbnail? + [:> shape-container {:shape shape + :ref on-dom} + + (when embed-fonts? + [:& ste/embed-fontfaces-style {:shapes text-childs}]) + + [:& frame-shape {:shape shape + :childs children}]]) + + (when (or (not @rendered?) show-thumbnail?) + [:& thumbnail {:shape shape}])]))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index d7ce59151..db7ece44a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -38,7 +38,7 @@ {:keys [id x y width height]} shape - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) + childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true})) childs (mf/deref childs-ref)] [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index 16101feca..88cc10114 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -30,12 +30,14 @@ (let [{:keys [x y]} position on-enter - (fn [event] - (st/emit! (drp/path-pointer-enter position))) + (mf/use-callback + (fn [event] + (st/emit! (drp/path-pointer-enter position)))) on-leave - (fn [event] - (st/emit! (drp/path-pointer-leave position))) + (mf/use-callback + (fn [event] + (st/emit! (drp/path-pointer-leave position)))) on-mouse-down (fn [event] @@ -48,6 +50,9 @@ (let [shift? (kbd/shift? event) ctrl? (kbd/ctrl? event)] (cond + last-p? + (st/emit! (drp/reset-last-handler)) + (and (= edit-mode :move) ctrl? (not curve?)) (st/emit! (drp/make-curve position)) @@ -81,8 +86,7 @@ :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave - :style {:pointer-events (when last-p? "none") - :cursor (cond + :style {:cursor (cond (= edit-mode :draw) cur/pen-node (= edit-mode :move) cur/pointer-node) :fill "transparent"}}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 084da1e56..9e4a3efbe 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -21,6 +21,7 @@ [app.util.logging :as log] [app.util.object :as obj] [app.util.timers :as timers] + [app.util.webapi :as wapi] [app.util.text-editor :as ted] [okulary.core :as l] [beicon.core :as rx] @@ -62,6 +63,7 @@ (true? (obj/get props "edition?")) (update-with-current-editor-state)) + mnt (mf/use-ref true) paragraph-ref (mf/use-state nil) handle-resize-text @@ -83,20 +85,24 @@ (mf/deps handle-resize-text) (fn [node] (when node - (let [obs-ref (atom nil)] - (timers/schedule - (fn [] - (when-let [ps-node (dom/query node ".paragraph-set")] - (reset! paragraph-ref ps-node))))))))] + (timers/schedule + #(when (mf/ref-val mnt) + (when-let [ps-node (dom/query node ".paragraph-set")] + (reset! paragraph-ref ps-node)))))))] (mf/use-effect (mf/deps @paragraph-ref handle-resize-text grow-type) (fn [] (when-let [paragraph-node @paragraph-ref] - (let [observer (js/ResizeObserver. handle-resize-text)] + (let [sub (->> (wapi/observe-resize paragraph-node) + (rx/observe-on :af) + (rx/subs handle-resize-text))] (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) - (.observe observer paragraph-node) - #(.disconnect observer))))) + (fn [] + (rx/dispose! sub)))))) + + (mf/use-effect + (fn [] #(mf/set-ref-val! mnt false))) [:& text/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index fb13896df..de210b422 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -213,7 +213,7 @@ (fn [path] (fn [event] (dom/stop-propagation event) - (swap! state update :folded-groups + (swap! state update :folded-groups toggle-folded-group path)))) on-group @@ -401,7 +401,7 @@ (fn [path] (fn [event] (dom/stop-propagation event) - (swap! state update :folded-groups + (swap! state update :folded-groups toggle-folded-group path)))) on-group @@ -427,7 +427,7 @@ (when local? [:div.assets-button {:on-click add-graphic} i/plus - [:& file-uploader {:accept cm/str-media-types + [:& file-uploader {:accept cm/str-image-types :multi true :input-ref input-ref :on-selected on-file-selected}]])] @@ -499,7 +499,8 @@ (mf/defc color-item [{:keys [color local? file-id selected-colors multi-colors? multi-assets? - on-asset-click on-assets-delete on-clear-selection colors locale] :as props}] + on-asset-click on-assets-delete on-clear-selection on-group + colors locale] :as props}] (let [rename? (= (:color-for-rename @refs/workspace-local) (:id color)) id (:id color) input-ref (mf/use-ref) @@ -623,17 +624,24 @@ [(t locale "workspace.assets.rename") rename-color-clicked]) (when-not (or multi-colors? multi-assets?) [(t locale "workspace.assets.edit") edit-color-clicked]) - [(t locale "workspace.assets.delete") delete-color]]}])])) + [(t locale "workspace.assets.delete") delete-color] + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}])])) (mf/defc colors-box [{:keys [file-id local? colors locale open? selected-assets on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [selected-colors (:colors selected-assets) + (let [state (mf/use-state {:folded-groups empty-folded-groups}) + + selected-colors (:colors selected-assets) multi-colors? (> (count selected-colors) 1) multi-assets? (or (not (empty? (:components selected-assets))) (not (empty? (:graphics selected-assets))) (not (empty? (:typographies selected-assets)))) + groups (group-assets colors) + folded-groups (:folded-groups @state) + add-color (mf/use-callback (mf/deps file-id) @@ -651,7 +659,39 @@ :on-accept add-color :data {:color "#406280" :opacity 1} - :position :right})))] + :position :right}))) + + create-group + (mf/use-callback + (mf/deps colors selected-colors on-clear-selection file-id) + (fn [name] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> colors + (filter #(contains? selected-colors (:id %))) + (map #(dwl/update-color + (assoc % :name + (str name " / " + (cp/merge-path-item (:path %) (:name %)))) + file-id)))) + (st/emit! (dwu/commit-undo-transaction)))) + + on-fold-group + (mf/use-callback + (mf/deps groups folded-groups) + (fn [path] + (fn [event] + (dom/stop-propagation event) + (swap! state update :folded-groups + toggle-folded-group path)))) + + on-group + (mf/use-callback + (mf/deps colors selected-colors) + (fn [event] + (dom/stop-propagation event) + (modal/show! :create-group-dialog {:create create-group})))] [:div.asset-section [:div.asset-title {:class (when (not open?) "closed")} @@ -661,24 +701,41 @@ (when local? [:div.assets-button {:on-click add-color-clicked} i/plus])] (when open? - [:div.asset-list - (for [color colors] - (let [color (cond-> color - (:value color) (assoc :color (:value color) :opacity 1) - (:value color) (dissoc :value) - true (assoc :file-id file-id))] - [:& color-item {:key (:id color) - :color color - :file-id file-id - :local? local? - :selected-colors selected-colors - :multi-colors? multi-colors? - :multi-assets? multi-assets? - :on-asset-click on-asset-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection - :colors colors - :locale locale}]))])])) + (for [group groups] + (let [path (first group) + colors (second group) + group-open? (not (contains? folded-groups path))] + [:* + (when-not (empty? path) + (let [[other-path last-path truncated] (cp/compact-path path 35)] + [:div.group-title {:class (when-not group-open? "closed") + :on-click (on-fold-group path)} + [:span i/arrow-slide] + (when-not (empty? other-path) + [:span.dim {:title (when truncated path)} + other-path "\u00A0/\u00A0"]) + [:span {:title (when truncated path)} + last-path]])) + (when group-open? + [:div.asset-list + (for [color colors] + (let [color (cond-> color + (:value color) (assoc :color (:value color) :opacity 1) + (:value color) (dissoc :value) + true (assoc :file-id file-id))] + [:& color-item {:key (:id color) + :color color + :file-id file-id + :local? local? + :selected-colors selected-colors + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :colors colors + :locale locale}]))])])))])) ;; ---- Typography box ---- @@ -690,15 +747,19 @@ :menu-open? false :top nil :left nil - :id nil}) + :id nil + :folded-groups empty-folded-groups}) local (deref refs/workspace-local) + groups (group-assets typographies) + folded-groups (:folded-groups @state) + selected-typographies (:typographies selected-assets) multi-typographies? (> (count selected-typographies) 1) - multi-assets? (or (not (empty? (:graphics selected-assets))) - (not (empty? (:colors selected-assets))) - (not (empty? (:typographies selected-assets)))) + multi-assets? (or (not (empty? (:components selected-assets))) + (not (empty? (:graphics selected-assets))) + (not (empty? (:colors selected-assets)))) add-typography (mf/use-callback @@ -722,6 +783,38 @@ (run! #(st/emit! (dwt/update-text-attrs {:id % :editor (get-in local [:editors %]) :attrs attrs})) ids))) + create-group + (mf/use-callback + (mf/deps typographies selected-typographies on-clear-selection file-id) + (fn [name] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> typographies + (filter #(contains? selected-typographies (:id %))) + (map #(dwl/update-typography + (assoc % :name + (str name " / " + (cp/merge-path-item (:path %) (:name %)))) + file-id)))) + (st/emit! (dwu/commit-undo-transaction)))) + + on-fold-group + (mf/use-callback + (mf/deps groups folded-groups) + (fn [path] + (fn [event] + (dom/stop-propagation event) + (swap! state update :folded-groups + toggle-folded-group path)))) + + on-group + (mf/use-callback + (mf/deps typographies selected-typographies) + (fn [event] + (dom/stop-propagation event) + (modal/show! :create-group-dialog {:create create-group}))) + on-context-menu (mf/use-callback (mf/deps selected-typographies on-clear-selection) @@ -788,22 +881,40 @@ [(t locale "workspace.assets.rename") handle-rename-typography-clicked]) (when-not (or multi-typographies? multi-assets?) [(t locale "workspace.assets.edit") handle-edit-typography-clicked]) - [(t locale "workspace.assets.delete") handle-delete-typography]]}] + [(t locale "workspace.assets.delete") handle-delete-typography] + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}] (when open? - [:div.asset-list - (for [typography typographies] - [:& typography-entry - {:key (:id typography) - :typography typography - :file file - :read-only? (not local?) - :on-context-menu #(on-context-menu (:id typography) %) - :on-change #(handle-change typography %) - :selected? (contains? selected-typographies (:id typography)) - :on-click #(on-asset-click % (:id typography) {"" typographies} - (partial apply-typography typography)) - :editting? (= editting-id (:id typography)) - :focus-name? (= (:rename-typography local) (:id typography))}])])])) + (for [group groups] + (let [path (first group) + typographies (second group) + group-open? (not (contains? folded-groups path))] + [:* + (when-not (empty? path) + (let [[other-path last-path truncated] (cp/compact-path path 35)] + [:div.group-title {:class (when-not group-open? "closed") + :on-click (on-fold-group path)} + [:span i/arrow-slide] + (when-not (empty? other-path) + [:span.dim {:title (when truncated path)} + other-path "\u00A0/\u00A0"]) + [:span {:title (when truncated path)} + last-path]])) + (when group-open? + [:div.asset-list + (for [typography typographies] + [:& typography-entry + {:key (:id typography) + :typography typography + :file file + :read-only? (not local?) + :on-context-menu #(on-context-menu (:id typography) %) + :on-change #(handle-change typography %) + :selected? (contains? selected-typographies (:id typography)) + :on-click #(on-asset-click % (:id typography) {"" typographies} + (partial apply-typography typography)) + :editting? (= editting-id (:id typography)) + :focus-name? (= (:rename-typography local) (:id typography))}])])])))])) ;; --- Assets toolbox ---- @@ -811,37 +922,36 @@ (defn file-colors-ref [id] (l/derived (fn [state] - (let [wfile (:workspace-file state)] + (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:data :colors])) + (vals (get-in wfile [:colors])) (vals (get-in state [:workspace-libraries id :data :colors]))))) st/state =)) - (defn file-media-ref [id] (l/derived (fn [state] - (let [wfile (:workspace-file state)] + (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:data :media])) + (vals (get-in wfile [:media])) (vals (get-in state [:workspace-libraries id :data :media]))))) st/state =)) (defn file-components-ref [id] (l/derived (fn [state] - (let [wfile (:workspace-file state)] + (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:data :components])) + (vals (get-in wfile [:components])) (vals (get-in state [:workspace-libraries id :data :components]))))) st/state =)) (defn file-typography-ref [id] (l/derived (fn [state] - (let [wfile (:workspace-file state)] + (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:data :typographies])) + (vals (get-in wfile [:typographies])) (vals (get-in state [:workspace-libraries id :data :typographies]))))) st/state =)) @@ -860,7 +970,7 @@ (sort-by #(str/lower (:name %)) comp-fn)))) (mf/defc file-library - [{:keys [file local? default-open? filters locale] :as props}] + [{:keys [file local? default-open? filters locale] :as props}] (let [open-file (mf/deref (open-file-ref (:id file))) open? (-> open-file :library diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index fa842aa4d..310ece276 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -150,10 +150,6 @@ (dom/prevent-default event) (let [id (:id item)] (cond - (or (:blocked item) - (:hidden item)) - nil - (kbd/shift? event) (st/emit! (dw/shift-select-shapes id)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 5c83e42cc..0d6ab2546 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -128,7 +128,6 @@ :on-click #(handle-change % "rtl")} i/text-direction-rtl]])) - (mf/defc vertical-align [{:keys [shapes ids values on-change] :as props}] (let [{:keys [vertical-align]} values @@ -225,68 +224,80 @@ (tr "workspace.options.text-options.title")) emit-update! - (fn [id attrs] - (let [attrs (select-keys attrs root-attrs)] - (when-not (empty? attrs) - (st/emit! (dwt/update-root-attrs {:id id :attrs attrs})))) + (mf/use-callback + (fn [id attrs] + (let [attrs (select-keys attrs root-attrs)] + (when-not (empty? attrs) + (st/emit! (dwt/update-root-attrs {:id id :attrs attrs})))) - (let [attrs (select-keys attrs paragraph-attrs)] - (when-not (empty? attrs) - (st/emit! (dwt/update-paragraph-attrs {:id id :attrs attrs})))) + (let [attrs (select-keys attrs paragraph-attrs)] + (when-not (empty? attrs) + (st/emit! (dwt/update-paragraph-attrs {:id id :attrs attrs})))) - (let [attrs (select-keys attrs text-attrs)] - (when-not (empty? attrs) - (st/emit! (dwt/update-text-attrs {:id id :attrs attrs}))))) + (let [attrs (select-keys attrs text-attrs)] + (when-not (empty? attrs) + (st/emit! (dwt/update-text-attrs {:id id :attrs attrs})))))) + + on-change + (mf/use-callback + (mf/deps ids) + (fn [attrs] + (run! #(emit-update! % attrs) ids))) typography - (cond - (and (:typography-ref-id values) - (not= (:typography-ref-id values) :multiple) - (not= (:typography-ref-file values) file-id)) - (-> shared-libs - (get-in [(:typography-ref-file values) :data :typographies (:typography-ref-id values)]) - (assoc :file-id (:typography-ref-file values))) + (mf/use-memo + (mf/deps values file-id shared-libs) + (fn [] + (cond + (and (:typography-ref-id values) + (not= (:typography-ref-id values) :multiple) + (not= (:typography-ref-file values) file-id)) + (-> shared-libs + (get-in [(:typography-ref-file values) :data :typographies (:typography-ref-id values)]) + (assoc :file-id (:typography-ref-file values))) - (and (:typography-ref-id values) - (not= (:typography-ref-id values) :multiple) - (= (:typography-ref-file values) file-id)) - (get typographies (:typography-ref-id values))) + (and (:typography-ref-id values) + (not= (:typography-ref-id values) :multiple) + (= (:typography-ref-file values) file-id)) + (get typographies (:typography-ref-id values))))) on-convert-to-typography - (mf/use-callback - (mf/deps values) - (fn [event] - (let [setted-values (-> (d/without-nils values) - (select-keys - (d/concat text-font-attrs - text-spacing-attrs - text-transform-attrs))) - typography (merge txt/default-typography setted-values) - typography (generate-typography-name typography)] - (let [id (uuid/next)] - (st/emit! (dwl/add-typography (assoc typography :id id) false)) - (run! #(emit-update! % {:typography-ref-id id - :typography-ref-file file-id}) ids))))) + (fn [event] + (let [setted-values (-> (d/without-nils values) + (select-keys + (d/concat text-font-attrs + text-spacing-attrs + text-transform-attrs))) + typography (merge txt/default-typography setted-values) + typography (generate-typography-name typography)] + (let [id (uuid/next)] + (st/emit! (dwl/add-typography (assoc typography :id id) false)) + (run! #(emit-update! % {:typography-ref-id id + :typography-ref-file file-id}) ids)))) handle-detach-typography - (fn [] - (run! #(emit-update! % {:typography-ref-file nil - :typography-ref-id nil}) - ids)) + (mf/use-callback + (mf/deps on-change) + (fn [] + (on-change {:typography-ref-file nil + :typography-ref-id nil}))) handle-change-typography - (fn [changes] - (st/emit! (dwl/update-typography (merge typography changes) file-id))) + (mf/use-callback + (mf/deps typography file-id) + (fn [changes] + (st/emit! (dwl/update-typography (merge typography changes) file-id)))) + + multiple? (->> values vals (d/seek #(= % :multiple))) opts #js {:ids ids :values values - :on-change (fn [attrs] - (run! #(emit-update! % attrs) ids))}] + :on-change on-change}] [:div.element-set [:div.element-set-title [:span label] - (when (not typography) + (when (and (not typography) (not multiple?)) [:div.add-page {:on-click on-convert-to-typography} i/close])] (cond diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 14e715e87..e0649d7b6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -6,19 +6,30 @@ (ns app.main.ui.workspace.sidebar.options.menus.typography (:require + ["react-virtualized" :as rvt] + [app.common.exceptions :as ex] [app.common.data :as d] + [app.common.pages :as cp] [app.common.text :as txt] [app.main.data.workspace.texts :as dwt] + [app.main.data.shortcuts :as dsc] + [app.main.data.fonts :as df] + [app.main.data.workspace :as dw] [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.editable-select :refer [editable-select]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]] + [app.util.object :as obj] + [app.util.timers :as tm] + [app.util.keyboard :as kbd] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as ts] + [goog.events :as events] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -27,74 +38,288 @@ "" (str value))) -(mf/defc font-select-optgroups +(defn- get-next-font + [{:keys [id] :as current} fonts] + (if (seq fonts) + (let [index (d/index-of-pred fonts #(= (:id %) id)) + index (or index -1) + next (ex/ignoring (nth fonts (inc index)))] + (or next (first fonts))) + current)) + +(defn- get-prev-font + [{:keys [id] :as current} fonts] + (if (seq fonts) + (let [index (d/index-of-pred fonts #(= (:id %) id)) + next (ex/ignoring (nth fonts (dec index)))] + (or next (peek fonts))) + current)) + +(mf/defc font-item {::mf/wrap [mf/memo]} - [{:keys [locale] :as props}] - [:* - [:optgroup {:label (t locale "workspace.options.text-options.preset")} - (for [font fonts/local-fonts] - [:option {:value (:id font) - :key (:id font)} - (:name font)])] - [:optgroup {:label (t locale "workspace.options.text-options.google")} - (for [font (fonts/resolve-fonts :google)] - [:option {:value (:id font) - :key (:id font)} - (:name font)])]]) + [{:keys [font current? on-click style]}] + (let [item-ref (mf/use-ref) + on-click (mf/use-callback (mf/deps font) #(on-click font))] + + (mf/use-effect + (mf/deps current?) + (fn [] + (when current? + (let [element (mf/ref-val item-ref)] + (when-not (dom/is-in-viewport? element) + (dom/scroll-into-view! element)))))) + + [:div.font-item {:ref item-ref + :style style + :class (when current? "selected") + :on-click on-click} + [:span.icon (when current? i/tick)] + [:span.label (:name font)]])) + +(declare row-renderer) + +(defn filter-fonts + [{:keys [term backends]} fonts] + (let [xform (cond-> (map identity) + (seq term) + (comp (filter #(str/includes? (str/lower (:name %)) term))) + + (seq backends) + (comp (filter #(contains? backends (:backend %)))))] + (into [] xform fonts))) + +(defn- toggle-backend + [backends id] + (if (contains? backends id) + (disj backends id) + (conj backends id))) + +(mf/defc font-selector + [{:keys [on-select on-close current-font] :as props}] + (let [selected (mf/use-state current-font) + state (mf/use-state {:term "" :backends #{}}) + + flist (mf/use-ref) + input (mf/use-ref) + ddown (mf/use-ref) + + fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts)) + + select-next + (mf/use-callback + (mf/deps fonts) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (swap! selected get-next-font fonts))) + + select-prev + (mf/use-callback + (mf/deps fonts) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (swap! selected get-prev-font fonts))) + + on-key-down + (mf/use-callback + (mf/deps fonts) + (fn [event] + (cond + (kbd/up-arrow? event) (select-prev event) + (kbd/down-arrow? event) (select-next event) + (kbd/esc? event) (on-close) + (kbd/enter? event) (on-close) + :else (dom/focus! (mf/ref-val input))))) + + on-filter-change + (mf/use-callback + (mf/deps) + (fn [event] + (let [value (dom/get-target-val event)] + (swap! state assoc :term value)))) + + on-select-and-close + (mf/use-callback + (mf/deps on-select on-close) + (fn [font] + (on-select font) + (on-close))) + ] + + (mf/use-effect + (mf/deps fonts) + (fn [] + (let [key (events/listen js/document "keydown" on-key-down)] + #(events/unlistenByKey key)))) + + (mf/use-effect + (mf/deps @selected) + (fn [] + (when-let [inst (mf/ref-val flist)] + (when-let [index (:index @selected)] + (.scrollToRow ^js inst index))))) + + (mf/use-effect + (mf/deps @selected) + (fn [] + (on-select @selected))) + + (mf/use-effect + (fn [] + (st/emit! (dsc/push-shortcuts :typography {})) + (fn [] + (st/emit! (dsc/pop-shortcuts :typography))))) + + (mf/use-effect + (fn [] + (let [index (d/index-of-pred fonts #(= (:id %) (:id current-font))) + inst (mf/ref-val flist)] + (tm/schedule + #(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})] + (.scrollToPosition ^js inst offset)))))) + + [:div.font-selector + [:div.font-selector-dropdown + [:header + [:input {:placeholder "Search font" + :value (:term @state) + :ref input + :spell-check false + :on-change on-filter-change}] + + #_[:div.options + {:on-click #(swap! state assoc :show-options true) + :class (when (seq (:backends @state)) "active")} + i/picker-hsv] + + #_[:& dropdown {:show (:show-options @state false) + :on-close #(swap! state dissoc :show-options)} + (let [backends (:backends @state)] + [:div.backend-filters.dropdown {:ref ddown} + [:div.backend-filter + {:class (when (backends :custom) "selected") + :on-click #(swap! state update :backends toggle-backend :custom)} + [:div.checkbox-icon i/tick] + [:div.backend-name (tr "labels.custom-fonts")]] + [:div.backend-filter + {:class (when (backends :google) "selected") + :on-click #(swap! state update :backends toggle-backend :google)} + [:div.checkbox-icon i/tick] + [:div.backend-name "Google Fonts"]]])]] + + [:hr] + + [:div.fonts-list + [:> rvt/AutoSizer {} + (fn [props] + (let [width (obj/get props "width") + height (obj/get props "height") + render #(row-renderer fonts @selected on-select-and-close %)] + (mf/html + [:> rvt/List #js {:height height + :ref flist + :width width + :rowCount (count fonts) + :rowHeight 32 + :rowRenderer render}])))]]]])) +(defn row-renderer + [fonts selected on-select props] + (let [index (obj/get props "index") + key (obj/get props "key") + style (obj/get props "style") + font (nth fonts index)] + (mf/html + [:& font-item {:key key + :font font + :style style + :on-click on-select + :current? (= (:id font) (:id selected))}]))) (mf/defc font-options - [{:keys [editor ids values locale on-change] :as props}] - (let [{:keys [font-id - font-size - font-variant-id]} values + [{:keys [editor ids values on-change] :as props}] + (let [{:keys [font-id font-size font-variant-id]} values - font-id (or font-id (:font-id txt/default-text-attrs)) - font-size (or font-size (:font-size txt/default-text-attrs)) + font-id (or font-id (:font-id txt/default-text-attrs)) + font-size (or font-size (:font-size txt/default-text-attrs)) font-variant-id (or font-variant-id (:font-variant-id txt/default-text-attrs)) - fonts (mf/deref fonts/fontsdb) - font (get fonts font-id) + fonts (mf/deref fonts/fontsdb) + font (get fonts font-id) + + open-selector? (mf/use-state false) change-font - (fn [new-font-id] - (let [{:keys [family] :as font} (get fonts new-font-id) - {:keys [id name weight style]} (fonts/get-default-variant font)] - (on-change {:font-id new-font-id - :font-family family - :font-variant-id (or id name) - :font-weight weight - :font-style style}))) + (mf/use-callback + (mf/deps on-change fonts) + (fn [new-font-id] + (let [{:keys [family] :as font} (get fonts new-font-id) + {:keys [id name weight style]} (fonts/get-default-variant font)] + (on-change {:font-id new-font-id + :font-family family + :font-variant-id (or id name) + :font-weight weight + :font-style style})))) on-font-family-change - (fn [event] - (let [new-font-id (dom/get-target-val event)] - (when-not (str/empty? new-font-id) - (let [font (get fonts new-font-id)] - (fonts/ensure-loaded! new-font-id (partial change-font new-font-id)))))) + (mf/use-callback + (mf/deps fonts change-font) + (fn [event] + (let [new-font-id (dom/get-target-val event)] + (when-not (str/empty? new-font-id) + (let [font (get fonts new-font-id)] + (fonts/ensure-loaded! new-font-id (partial change-font new-font-id))))))) on-font-size-change - (fn [new-font-size] - (when-not (str/empty? new-font-size) - (on-change {:font-size (str new-font-size)}))) + (mf/use-callback + (mf/deps on-change) + (fn [new-font-size] + (when-not (str/empty? new-font-size) + (on-change {:font-size (str new-font-size)})))) on-font-variant-change - (fn [event] - (let [new-variant-id (dom/get-target-val event) - variant (d/seek #(= new-variant-id (:id %)) (:variants font))] - (on-change {:font-id (:id font) - :font-family (:family font) - :font-variant-id new-variant-id - :font-weight (:weight variant) - :font-style (:style variant)})))] + (mf/use-callback + (mf/deps font on-change) + (fn [event] + (let [new-variant-id (dom/get-target-val event) + variant (d/seek #(= new-variant-id (:id %)) (:variants font))] + (on-change {:font-id (:id font) + :font-family (:family font) + :font-variant-id new-variant-id + :font-weight (:weight variant) + :font-style (:style variant)})))) + + on-font-select + (mf/use-callback + (mf/deps change-font) + (fn [font*] + (when (not= font font*) + (change-font (:id font*))))) + + on-font-selector-close + (mf/use-callback + #(reset! open-selector? false))] [:* + (when @open-selector? + [:& font-selector + {:current-font font + :on-close on-font-selector-close + :on-select on-font-select}]) + [:div.row-flex - [:select.input-select.font-option - {:value (attr->string font-id) - :on-change on-font-family-change} - (when (= font-id :multiple) - [:option {:value ""} (t locale "settings.multiple")]) - [:& font-select-optgroups {:locale locale}]]] + [:div.input-select.font-option + {:on-click #(reset! open-selector? true)} + (cond + (= :multiple font-id) + "--" + + (some? font) + (:name font) + + :else + (tr "dashboard.fonts.deleted-placeholder"))]] + [:div.row-flex (let [size-options [8 9 10 11 12 14 18 24 36 48 72] @@ -120,7 +345,7 @@ (mf/defc spacing-options - [{:keys [editor ids values locale on-change] :as props}] + [{:keys [editor ids values on-change] :as props}] (let [{:keys [line-height letter-spacing]} values @@ -135,7 +360,7 @@ [:div.spacing-options [:div.input-icon [:span.icon-before.tooltip.tooltip-bottom - {:alt (t locale "workspace.options.text-options.line-height")} + {:alt (tr "workspace.options.text-options.line-height")} i/line-height] [:input.input-text {:type "number" @@ -143,12 +368,12 @@ :min "0" :max "200" :value (attr->string line-height) - :placeholder (t locale "settings.multiple") + :placeholder (tr "settings.multiple") :on-change #(handle-change % :line-height)}]] [:div.input-icon [:span.icon-before.tooltip.tooltip-bottom - {:alt (t locale "workspace.options.text-options.letter-spacing")} + {:alt (tr "workspace.options.text-options.letter-spacing")} i/letter-spacing] [:input.input-text {:type "number" @@ -156,11 +381,11 @@ :min "0" :max "200" :value (attr->string letter-spacing) - :placeholder (t locale "settings.multiple") + :placeholder (tr "settings.multiple") :on-change #(handle-change % :letter-spacing)}]]])) (mf/defc text-transform-options - [{:keys [editor ids values locale on-change] :as props}] + [{:keys [editor ids values on-change] :as props}] (let [{:keys [text-transform]} values text-transform (or text-transform "none") @@ -170,35 +395,32 @@ (on-change {:text-transform type}))] [:div.align-icons [:span.tooltip.tooltip-bottom - {:alt (t locale "workspace.options.text-options.none") + {:alt (tr "workspace.options.text-options.none") :class (dom/classnames :current (= "none" text-transform)) :on-click #(handle-change % "none")} i/minus] [:span.tooltip.tooltip-bottom - {:alt (t locale "workspace.options.text-options.uppercase") + {:alt (tr "workspace.options.text-options.uppercase") :class (dom/classnames :current (= "uppercase" text-transform)) :on-click #(handle-change % "uppercase")} i/uppercase] [:span.tooltip.tooltip-bottom - {:alt (t locale "workspace.options.text-options.lowercase") + {:alt (tr "workspace.options.text-options.lowercase") :class (dom/classnames :current (= "lowercase" text-transform)) :on-click #(handle-change % "lowercase")} i/lowercase] [:span.tooltip.tooltip-bottom - {:alt (t locale "workspace.options.text-options.titlecase") + {:alt (tr "workspace.options.text-options.titlecase") :class (dom/classnames :current (= "capitalize" text-transform)) :on-click #(handle-change % "capitalize")} i/titlecase]])) (mf/defc typography-options [{:keys [ids editor values on-change]}] - (let [locale (mf/deref i18n/locale) - opts #js {:editor editor + (let [opts #js {:editor editor :ids ids :values values - :locale locale :on-change on-change}] - [:div.element-set-content [:> font-options opts] [:div.row-flex @@ -208,15 +430,20 @@ (mf/defc typography-entry [{:keys [typography read-only? selected? on-click on-change on-detach on-context-menu editting? focus-name? file]}] - (let [locale (mf/deref i18n/locale) - open? (mf/use-state editting?) + (let [open? (mf/use-state editting?) hover-detach (mf/use-state false) name-input-ref (mf/use-ref nil) + value (mf/use-state (cp/merge-path-item (:path typography) (:name typography))) #_(rt/resolve router :workspace {:project-id (:project-id file) :file-id (:id file)} {:page-id (get-in file [:data :pages 0])}) + + handle-change + (fn [event] + (reset! value (dom/get-target-val event))) + handle-go-to-edit (fn [] (st/emit! (rt/nav :workspace {:project-id (:project-id file) :file-id (:id file)} @@ -248,7 +475,7 @@ {:style {:font-family (:font-family typography) :font-weight (:font-weight typography) :font-style (:font-style typography)}} - (t locale "workspace.assets.typography.sample")] + (tr "workspace.assets.typography.sample")] [:div.typography-name (:name typography)]] [:div.element-set-actions (when on-detach @@ -270,32 +497,32 @@ [:span (:name typography)]] [:div.row-flex - [:span.label (t locale "workspace.assets.typography.font-id")] + [:span.label (tr "workspace.assets.typography.font-id")] [:span (:font-id typography)]] [:div.row-flex - [:span.label (t locale "workspace.assets.typography.font-variant-id")] + [:span.label (tr "workspace.assets.typography.font-variant-id")] [:span (:font-variant-id typography)]] [:div.row-flex - [:span.label (t locale "workspace.assets.typography.font-size")] + [:span.label (tr "workspace.assets.typography.font-size")] [:span (:font-size typography)]] [:div.row-flex - [:span.label (t locale "workspace.assets.typography.line-height")] + [:span.label (tr "workspace.assets.typography.line-height")] [:span (:line-height typography)]] [:div.row-flex - [:span.label (t locale "workspace.assets.typography.letter-spacing")] + [:span.label (tr "workspace.assets.typography.letter-spacing")] [:span (:letter-spacing typography)]] [:div.row-flex - [:span.label (t locale "workspace.assets.typography.text-transform")] + [:span.label (tr "workspace.assets.typography.text-transform")] [:span (:text-transform typography)]] [:div.go-to-lib-button {:on-click handle-go-to-edit} - (t locale "workspace.assets.typography.go-to-edit")]] + (tr "workspace.assets.typography.go-to-edit")]] [:* [:div.element-set-content @@ -303,7 +530,8 @@ [:input.element-name.adv-typography-name {:type "text" :ref name-input-ref - :value (:name typography) - :on-change #(on-change {:name (dom/get-target-val %)})}]]] + :value @value + :on-change handle-change + :on-blur #(on-change {:name @value})}]]] [:& typography-options {:values typography :on-change on-change}]])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 96e5f4021..be8a067ef 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -27,7 +27,19 @@ (defn color-picker-callback [color disable-gradient disable-opacity handle-change-color handle-open handle-close] (fn [event] - (let [x (.-clientX event) + (let [color + (cond + (uc/multiple? color) + {:color cp/default-color + :opacity 1} + + (= :multiple (:opacity color)) + (assoc color :opacity 1) + + :else + color) + + x (.-clientX event) y (.-clientY event) props {:x x :y y @@ -98,16 +110,12 @@ handle-click-color (mf/use-callback (mf/deps color) - (let [;; If multiple, we change to default color - color (if (uc/multiple? color) - {:color cp/default-color :opacity 1} - color)] - (color-picker-callback color - disable-gradient - disable-opacity - handle-pick-color - handle-open - handle-close))) + (color-picker-callback color + disable-gradient + disable-opacity + handle-pick-color + handle-open + handle-close)) prev-color (h/use-previous color)] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 259ff36a6..86a049053 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -148,22 +148,23 @@ extract-attrs (fn [[ids values] {:keys [id type shapes content] :as shape}] - (let [props (get-in type->props [type attr-type]) - result (case props - :ignore [ids values] - :shape [(conj ids id) - (merge-attrs values (merge - (empty-map attrs) - (select-keys shape attrs)))] - :text [(conj ids id) - (-> values - (merge-attrs (select-keys shape attrs)) - (merge-attrs (attrs/get-attrs-multi (txt/node-seq content) attrs)))] - :children (let [children (->> (:shapes shape []) (map #(get objects %))) - [new-ids new-values] (get-attrs children objects attr-type)] - [(d/concat ids new-ids) (merge-attrs values new-values)]) - [])] - result))] + (let [props (get-in type->props [type attr-type])] + (case props + :ignore [ids values] + :shape [(conj ids id) + (merge-attrs values (merge + (empty-map attrs) + (select-keys shape attrs)))] + :text [(conj ids id) + (-> values + (merge-attrs (select-keys shape attrs)) + (merge-attrs (merge + (select-keys txt/default-text-attrs attrs) + (attrs/get-attrs-multi (txt/node-seq content) attrs))))] + :children (let [children (->> (:shapes shape []) (map #(get objects %))) + [new-ids new-values] (get-attrs children objects attr-type)] + [(d/concat ids new-ids) (merge-attrs values new-values)]) + [])))] (reduce extract-attrs [[] []] shapes))) (mf/defc options diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index eb430cbf2..ae5ac0ed8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -164,7 +164,7 @@ (defn- make-page-ref [page-id] (l/derived (fn [state] - (let [page (get-in state [:workspace-file :data :pages-index page-id])] + (let [page (get-in state [:workspace-data :pages-index page-id])] (select-keys page [:id :name]))) st/state =)) @@ -198,11 +198,13 @@ (mf/defc sitemap [{:keys [layout] :as props}] - (let [create (mf/use-callback #(st/emit! dw/create-empty-page)) + (let [file (mf/deref refs/workspace-file) + create (mf/use-callback + (mf/deps file) + (st/emitf (dw/create-page {:file-id (:id file) + :project-id (:project-id file)}))) show-pages? (mf/use-state true) - file (mf/deref refs/workspace-file) - toggle-pages (mf/use-callback #(reset! show-pages? not))] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 6e2363cb6..a210dad73 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.viewport (:require [app.common.data :as d] + [app.common.pages :as cp] [app.common.geom.shapes :as gsh] [app.main.refs :as refs] [app.main.ui.context :as ctx] @@ -27,6 +28,7 @@ [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] + [app.main.ui.workspace.viewport.thumbnail-renderer :as wtr] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.widgets :as widgets] [beicon.core :as rx] @@ -59,6 +61,10 @@ drawing (mf/deref refs/workspace-drawing) options (mf/deref refs/workspace-page-options) objects (mf/deref refs/workspace-page-objects) + object-modifiers (mf/deref refs/workspace-modifiers) + objects (mf/use-memo + (mf/deps objects object-modifiers) + #(cp/merge-modifiers objects object-modifiers)) ;; STATE alt? (mf/use-state false) @@ -67,6 +73,7 @@ hover-ids (mf/use-state nil) hover (mf/use-state nil) frame-hover (mf/use-state nil) + active-frames (mf/use-state {}) ;; REFS viewport-ref (mf/use-ref nil) @@ -131,22 +138,26 @@ show-prototypes? (= options-mode :prototype) show-selection-handlers? (seq selected) show-snap-distance? (and (contains? layout :dynamic-alignment) (= transform :move) (not (empty? selected))) - show-snap-points? (and (contains? layout :dynamic-alignment) (or drawing-obj transform)) + show-snap-points? (and (or (contains? layout :dynamic-alignment) + (contains? layout :snap-grid)) + (or drawing-obj transform)) show-selrect? (and selrect (empty? drawing)) - show-measures? (and (not transform) (not path-editing?) show-distances?) - ] + show-measures? (and (not transform) (not path-editing?) show-distances?)] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) (hooks/setup-viewport-size viewport-ref) (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? path-editing?) (hooks/setup-resize layout viewport-ref) (hooks/setup-keyboard alt? ctrl?) - (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids) + (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids zoom) (hooks/setup-viewport-modifiers modifiers selected objects render-ref) (hooks/setup-shortcuts path-editing? drawing-path?) + (hooks/setup-active-frames objects vbox hover active-frames) [:div.viewport [:div.viewport-overlays + [:& wtr/frame-renderer {:objects objects}] + (when show-comments? [:& comments/comments-layer {:vbox vbox :vport vport @@ -179,7 +190,8 @@ [:& (mf/provider muc/embed-ctx) {:value true} ;; Render root shape [:& shapes/root-shape {:key page-id - :objects objects}]]] + :objects objects + :active-frames @active-frames}]]] [:svg.viewport-controls {:xmlns "http://www.w3.org/2000/svg" @@ -205,8 +217,7 @@ :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave :on-pointer-move on-pointer-move - :on-pointer-up on-pointer-up - } + :on-pointer-up on-pointer-up} [:g {:style {:pointer-events (if disable-events? "none" "auto")}} @@ -222,6 +233,7 @@ (when show-selection-handlers? [:& selection/selection-handlers {:selected selected + :shapes selected-shapes :zoom zoom :edition edition :disable-handlers (or drawing-tool edition) @@ -276,6 +288,7 @@ :zoom zoom :page-id page-id :selected selected + :objects objects :modifiers modifiers}]) (when show-snap-distance? @@ -302,5 +315,6 @@ {:selected selected}]) (when show-selrect? - [:& widgets/selection-rect {:data selrect}])]]])) + [:& widgets/selection-rect {:data selrect + :zoom zoom}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index f7550428f..a32ae106e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -142,7 +142,7 @@ (not selected?) (not edition) (not drawing-path?) - (not (#{:comments :path} drawing-tool))) + (not drawing-tool)) (st/emit! (dw/select-shape (:id @hover))))))))) (defn on-double-click @@ -179,8 +179,8 @@ (dw/start-editing-selected)) :else - ;; Do nothing - nil)))))) + (st/emit! (dw/selected-to-path) + (dw/start-editing-selected)))))))) (defn on-context-menu [hover] diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 1fe1c1685..bbda478b0 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -90,12 +90,12 @@ (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) (hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %))) -(defn setup-hover-shapes [page-id move-stream selected objects transform selected ctrl? hover hover-ids] +(defn setup-hover-shapes [page-id move-stream selected objects transform selected ctrl? hover hover-ids zoom] (let [query-point (mf/use-callback (mf/deps page-id) (fn [point] - (let [rect (gsh/center->rect point 8 8)] + (let [rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))] (uw/ask-buffered! {:cmd :selection/query :page-id page-id @@ -153,14 +153,46 @@ (utils/update-transform render-node roots modifiers) (utils/remove-transform render-node roots)))))) -(defn setup-shortcuts [path-editing? drawing-path?] +(defn inside-vbox [vbox objects frame-id] + (let [frame (get objects frame-id)] + + (and (some? frame) + (gsh/overlaps? frame vbox)))) + +(defn setup-active-frames + [objects vbox hover active-frames] + + (mf/use-effect + (mf/deps vbox) + + (fn [] + (swap! active-frames + (fn [active-frames] + (let [set-active-frames + (fn [active-frames id active?] + (cond-> active-frames + (and active? (inside-vbox vbox objects id)) + (assoc id true)))] + (reduce-kv set-active-frames {} active-frames)))))) + + (mf/use-effect + (mf/deps @hover @active-frames) + (fn [] + (let [frame-id (if (= :frame (:type @hover)) + (:id @hover) + (:frame-id @hover))] + (when (not (contains? @active-frames frame-id)) + (swap! active-frames assoc frame-id true)))))) + +;; NOTE: this is executed on each page change, maybe we need to move +;; this shortcuts outside the viewport? + +(defn setup-shortcuts + [path-editing? drawing-path?] + (hooks/use-shortcuts ::workspace wsc/shortcuts) (mf/use-effect (mf/deps path-editing? drawing-path?) (fn [] - (cond - (or drawing-path? path-editing?) - (dsc/bind-shortcuts psc/shortcuts) - - :else - (dsc/bind-shortcuts wsc/shortcuts)) - dsc/remove-shortcuts))) + (when (or drawing-path? path-editing?) + (st/emit! (dsc/push-shortcuts ::path psc/shortcuts)) + (st/emitf (dsc/pop-shortcuts ::path)))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index e292b16dc..6c8733776 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -29,7 +29,7 @@ (mf/deps shape) #(when path? (upf/format-path (:content shape)))) - {:keys [id x y width height]} shape + {:keys [id x y width height selrect]} shape outline-type (case (:type shape) :circle "ellipse" @@ -53,10 +53,10 @@ {:d path-data :transform nil} - {:x x - :y y - :width width - :height height})] + {:x (:x selrect) + :y (:y selrect) + :width (:width selrect) + :height (:height selrect)})] [:> outline-type (map->obj (merge common props))])) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 8cad2d8fc..85b91a8ff 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -241,8 +241,7 @@ disable-handlers (obj/get props "disable-handlers") current-transform (mf/deref refs/current-transform) - selrect (-> (:selrect shape) - minimum-selrect) + selrect (:selrect shape) transform (geom/transform-matrix shape {:no-flip true})] (when (not (#{:move :rotate} current-transform)) @@ -327,7 +326,7 @@ (mf/defc single-selection-handlers [{:keys [shape zoom color disable-handlers on-move-selected] :as props}] (let [shape-id (:id shape) - shape (geom/transform-shape shape) + shape (geom/transform-shape shape {:round-coords? false}) frame (mf/deref (refs/object-by-id (:frame-id shape))) frame (when-not (= (:id frame) uuid/zero) frame) @@ -355,13 +354,8 @@ (mf/defc selection-handlers {::mf/wrap [mf/memo]} - [{:keys [selected edition zoom disable-handlers on-move-selected] :as props}] - (let [;; We need remove posible nil values because on shape - ;; deletion many shape will reamin selected and deleted - ;; in the same time for small instant of time - shapes (->> (mf/deref (refs/objects-by-id selected)) - (remove nil?)) - num (count shapes) + [{:keys [shapes selected edition zoom disable-handlers on-move-selected] :as props}] + (let [num (count shapes) {:keys [id type] :as shape} (first shapes) color (if (or (> num 1) (nil? (:shape-ref shape))) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index 9f7e24b27..f9cc2707c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.snap-points (:require + [app.common.pages :as cp] [app.common.math :as mth] [app.common.data :as d] [app.common.geom.point :as gpt] @@ -116,7 +117,6 @@ snap-lines (->> (into (process-snap-lines @state :x) (process-snap-lines @state :y)) (into #{}))] - (mf/use-effect (fn [] (let [sub (->> subject @@ -131,7 +131,7 @@ #(rx/dispose! sub)))) (mf/use-effect - (mf/deps shapes modifiers) + (mf/deps shapes filter-shapes modifiers) (fn [] (rx/push! subject props))) @@ -152,15 +152,26 @@ (mf/defc snap-points {::mf/wrap [mf/memo]} - [{:keys [layout zoom selected page-id drawing transform modifiers] :as props}] - (let [shapes (mf/deref (refs/objects-by-id selected)) - filter-shapes (mf/deref refs/selected-shapes-with-children) + [{:keys [layout zoom objects selected page-id drawing transform modifiers] :as props}] + + (let [;; shapes (mf/deref (refs/objects-by-id selected)) + ;; filter-shapes (mf/deref refs/selected-shapes-with-children) + + shapes (->> selected + (map #(get objects %)) + (filterv (comp not nil?))) + filter-shapes (into #{} + (comp (mapcat #(cp/get-object-with-children % objects)) + (map :id)) + selected) + filter-shapes (fn [id] (if (= id :layout) (or (not (contains? layout :display-grid)) (not (contains? layout :snap-grid))) (or (filter-shapes id) (not (contains? layout :dynamic-alignment))))) + shapes (if drawing [drawing] shapes)] (when (or drawing transform) [:& snap-feedback {:shapes shapes diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs new file mode 100644 index 000000000..dc6d5eb69 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs @@ -0,0 +1,135 @@ +;; 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.workspace.viewport.thumbnail-renderer + (:require + [app.main.data.workspace.changes :as dwc] + [app.main.data.workspace.persistence :as dwp] + [app.main.store :as st] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.timers :as timers] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(mf/defc frame-thumbnail + "Renders the canvas and image for a frame thumbnail and stores its value into the shape" + [{:keys [shape on-thumbnail-data on-frame-not-found]}] + + (let [thumbnail-img (mf/use-ref nil) + thumbnail-canvas (mf/use-ref nil) + + on-dom-rendered + (mf/use-callback + (mf/deps (:id shape)) + (fn [node] + (when node + (let [img-node (mf/ref-val thumbnail-img)] + (timers/schedule-on-idle + #(let [frame-node (dom/get-element (str "shape-" (:id shape))) + loading-node (when frame-node + (dom/query frame-node "[data-loading=\"true\"]"))] + (if (and (some? frame-node) (not (some? loading-node))) + (let [xml (-> (js/XMLSerializer.) + (.serializeToString frame-node) + js/encodeURIComponent + js/unescape + js/btoa) + img-src (str "data:image/svg+xml;base64," xml)] + (obj/set! img-node "src" img-src)) + + (on-frame-not-found (:id shape))))))))) + + on-image-load + (mf/use-callback + (mf/deps on-thumbnail-data) + (fn [] + (let [canvas-node (mf/ref-val thumbnail-canvas) + img-node (mf/ref-val thumbnail-img) + canvas-context (.getContext canvas-node "2d") + _ (.drawImage canvas-context img-node 0 0) + data (.toDataURL canvas-node "image/jpeg" 0.8)] + (on-thumbnail-data data))))] + + [:div.frame-renderer {:ref on-dom-rendered + :style {:display "none"}} + [:img.thumbnail-img + {:ref thumbnail-img + :width (:width shape) + :height (:height shape) + :on-load on-image-load}] + + [:canvas.thumbnail-canvas + {:ref thumbnail-canvas + :width (:width shape) + :height (:height shape)}]])) + +(mf/defc frame-renderer + "Component in charge of creating thumbnails and storing them" + {::mf/wrap-props false} + [props] + (let [objects (obj/get props "objects") + + ;; Id of the current frame being rendered + shape-id (mf/use-state nil) + + ;; This subject will emit a value every time there is a free "slot" to render + ;; a thumbnail + next (mf/use-memo #(rx/behavior-subject :next)) + + render-frame + (mf/use-callback + (fn [frame-id] + (reset! shape-id frame-id))) + + updates-stream + (mf/use-memo + (fn [] + (let [update-events + (->> st/stream + (rx/filter dwp/update-frame-thumbnail?))] + (->> (rx/zip update-events next) + (rx/map first))))) + + on-thumbnail-data + (mf/use-callback + (mf/deps @shape-id) + (fn [data] + (reset! shape-id nil) + (timers/schedule + (fn [] + (st/emit! (dwc/update-shapes [@shape-id] + #(assoc % :thumbnail data) + {:save-undo? false})) + (rx/push! next :next))))) + + on-frame-not-found + (mf/use-callback + (fn [frame-id] + ;; If we couldn't find the frame maybe is still rendering. We push the event again + ;; after a time + (reset! shape-id nil) + (rx/push! next :next) + (timers/schedule-on-idle (st/emitf (dwp/update-frame-thumbnail frame-id)))))] + + (mf/use-effect + (mf/deps render-frame) + (fn [] + (let [sub (->> updates-stream + (rx/subs #(render-frame (-> (deref %) :frame-id))))] + + #(rx/dispose! sub)))) + + (mf/use-layout-effect + (fn [] + (timers/schedule-on-idle + #(st/emit! (dwp/watch-state-changes))))) + + (when (and (some? @shape-id) (contains? objects @shape-id)) + [:& frame-thumbnail {:key (str "thumbnail-" @shape-id) + :shape (get objects @shape-id) + :on-thumbnail-data on-thumbnail-data + :on-frame-not-found on-frame-not-found}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index e3df76657..092da6bbc 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -15,8 +15,19 @@ (defn update-transform [node shapes modifiers] (doseq [{:keys [id type]} shapes] - (when-let [node (dom/get-element (str "shape-" id))] - (let [node (if (= :frame type) (.-parentNode node) node)] + (let [shape-node (dom/get-element (str "shape-" id)) + + ;; When the shape is a frame we maybe need to move its thumbnail + thumb-node (dom/get-element (str "thumbnail-" id))] + (when-let [node (cond + (and (some? shape-node) (= :frame type)) + (.-parentNode shape-node) + + (and (some? thumb-node) (= :frame type)) + (.-parentNode thumb-node) + + :else + shape-node)] (dom/set-attribute node "transform" (str (:displacement modifiers))))))) (defn remove-transform [node shapes] diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 6bd73feb8..3faa46555 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -64,13 +64,19 @@ (mf/defc selection-rect {:wrap [mf/memo]} - [{:keys [data] :as props}] + [{:keys [data zoom] :as props}] (when data [:rect.selection-rect {:x (:x data) :y (:y data) :width (:width data) - :height (:height data)}])) + :height (:height data) + :style {;; Primary with 0.1 opacity + :fill "rgb(49, 239, 184, 0.1)" + + ;; Primary color + :stroke "rgb(49, 239, 184)" + :stroke-width (/ 1 zoom)}}])) ;; Ensure that the label has always the same font ;; size, regardless of zoom diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 05183e2eb..8899e6f23 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -135,7 +135,7 @@ :else "transparent"))) -(defn multiple? [{:keys [id file-id value color gradient]}] +(defn multiple? [{:keys [id file-id value color gradient opacity]}] (or (= value :multiple) (= color :multiple) (= gradient :multiple) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 95679b7c8..7601435ca 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -293,3 +293,21 @@ (defn remove-attribute [^js node ^string attr] (.removeAttribute node attr)) + +(defn scroll-into-view! + ([element] + (.scrollIntoView ^js element false)) + ([element scroll-top] + (.scrollIntoView ^js element scroll-top))) + +(defn is-in-viewport? + [element] + (let [rect (.getBoundingClientRect element) + height (or (.-innerHeight js/window) + (.. js/document -documentElement -clientHeight)) + width (or (.-innerWidth js/window) + (.. js/document -documentElement -clientWidth))] + (and (>= (.-top rect) 0) + (>= (.-left rect) 0) + (<= (.-bottom rect) height) + (<= (.-right rect) width)))) diff --git a/frontend/src/app/util/geom/grid.cljs b/frontend/src/app/util/geom/grid.cljs index c1f882204..bd8f58bd1 100644 --- a/frontend/src/app/util/geom/grid.cljs +++ b/frontend/src/app/util/geom/grid.cljs @@ -88,7 +88,9 @@ (defn grid-snap-points "Returns the snap points for a given grid" - ([shape coord] (mapcat #(grid-snap-points shape % coord) (:grids shape))) + ([shape coord] + (mapcat #(grid-snap-points shape % coord) (:grids shape))) + ([shape {:keys [type display params] :as grid} coord] (when (:display grid) (case type diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index ac66402b0..6bc479f80 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -10,6 +10,7 @@ [app.config :as cfg] [app.util.globals :as globals] [app.util.storage :refer [storage]] + [app.util.object :as obj] [app.util.transit :as t] [beicon.core :as rx] [cuerdas.core :as str] @@ -24,6 +25,8 @@ {:label "Deutsch (community)" :value "de"} {:label "Русский (community)" :value "ru"} {:label "Türkçe (community)" :value "tr"} + {:label "Rumanian (communit)" :value "ro"} + {:label "Portuguese (Brazil, community)" :value "pt_br"} {:label "Ελληνική γλώσσα (community)" :value "el"} {:label "简体中文 (community)" :value "zh_cn"}]) @@ -51,7 +54,7 @@ cfg/default-language)))) (defonce translations #js {}) -(defonce locale (l/atom (or (get storage ::locale) +(defonce locale (l/atom (or (get @storage ::locale) (autodetect)))) ;; The traslations `data` is a javascript object and should be treated @@ -136,6 +139,13 @@ ([code] (t @locale code)) ([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 (defn use-locale [] diff --git a/frontend/src/app/util/path/shapes_to_path.cljs b/frontend/src/app/util/path/shapes_to_path.cljs new file mode 100644 index 000000000..a1d42ba47 --- /dev/null +++ b/frontend/src/app/util/path/shapes_to_path.cljs @@ -0,0 +1,147 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.util.path.shapes-to-path + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gsp] + [app.util.path.commands :as pc])) + +(def bezier-circle-c 0.551915024494) +(def dissoc-attrs [:x :y :width :height + :rx :ry :r1 :r2 :r3 :r4 + :medata]) +(def allowed-transform-types #{:rect + :circle + :image}) + +(defn make-corner-arc + "Creates a curvle corner for border radius" + [from to corner radius] + (let [x (case corner + :top-left (:x from) + :top-right (- (:x from) radius) + :bottom-right (- (:x to) radius) + :bottom-left (:x to)) + + y (case corner + :top-left (- (:y from) radius) + :top-right (:y from) + :bottom-right (- (:y to) (* 2 radius)) + :bottom-left (- (:y to) radius)) + + width (* radius 2) + height (* radius 2) + + c bezier-circle-c + c1x (+ x (* (/ width 2) (- 1 c))) + c2x (+ x (* (/ width 2) (+ 1 c))) + c1y (+ y (* (/ height 2) (- 1 c))) + c2y (+ y (* (/ height 2) (+ 1 c))) + + h1 (case corner + :top-left (assoc from :y c1y) + :top-right (assoc from :x c2x) + :bottom-right (assoc from :y c2y) + :bottom-left (assoc from :x c1x)) + + h2 (case corner + :top-left (assoc to :x c1x) + :top-right (assoc to :y c1y) + :bottom-right (assoc to :x c2x) + :bottom-left (assoc to :y c2y))] + + (pc/make-curve-to to h1 h2))) + +(defn circle->path + "Creates the bezier curves to approximate a circle shape" + [x y width height] + (let [mx (+ x (/ width 2)) + my (+ y (/ height 2)) + ex (+ x width) + ey (+ y height) + + pc (gpt/point mx my) + p1 (gpt/point mx y) + p2 (gpt/point ex my) + p3 (gpt/point mx ey) + p4 (gpt/point x my) + + c bezier-circle-c + c1x (+ x (* (/ width 2) (- 1 c))) + c2x (+ x (* (/ width 2) (+ 1 c))) + c1y (+ y (* (/ height 2) (- 1 c))) + c2y (+ y (* (/ height 2) (+ 1 c)))] + + [(pc/make-move-to p1) + (pc/make-curve-to p2 (assoc p1 :x c2x) (assoc p2 :y c1y)) + (pc/make-curve-to p3 (assoc p2 :y c2y) (assoc p3 :x c2x)) + (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y)) + (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))])) + +(defn rect->path + "Creates a bezier curve that approximates a rounded corner rectangle" + [x y width height r1 r2 r3 r4] + (let [p1 (gpt/point x (+ y r1)) + p2 (gpt/point (+ x r1) y) + + p3 (gpt/point (+ width x (- r2)) y) + p4 (gpt/point (+ width x) (+ y r2)) + + p5 (gpt/point (+ width x) (+ height y (- r3))) + p6 (gpt/point (+ width x (- r3)) (+ height y)) + + p7 (gpt/point (+ x r4) (+ height y)) + p8 (gpt/point x (+ height y (- r4)))] + (-> [] + (conj (pc/make-move-to p1)) + (cond-> (not= p1 p2) + (conj (make-corner-arc p1 p2 :top-left r1))) + (conj (pc/make-line-to p3)) + (cond-> (not= p3 p4) + (conj (make-corner-arc p3 p4 :top-right r2))) + (conj (pc/make-line-to p5)) + (cond-> (not= p5 p6) + (conj (make-corner-arc p5 p6 :bottom-right r3))) + (conj (pc/make-line-to p7)) + (cond-> (not= p7 p8) + (conj (make-corner-arc p7 p8 :bottom-left r4))) + (conj (pc/make-line-to p1))))) + +(defn convert-to-path + "Transforms the given shape to a path" + [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape}] + + (if (contains? allowed-transform-types type) + (let [r1 (or r1 rx 0) + r2 (or r2 rx 0) + r3 (or r3 rx 0) + r4 (or r4 rx 0) + + new-content + (case type + :circle + (circle->path x y width height) + (rect->path x y width height r1 r2 r3 r4)) + + ;; Apply the transforms that had the shape + transform (:transform shape) + new-content (cond-> new-content + (some? transform) + (gsp/transform-content (gmt/transform-in (gsh/center-shape shape) transform)))] + + (-> shape + (d/without-keys dissoc-attrs) + (assoc :type :path) + (assoc :content new-content) + (cond-> (= :image type) + (assoc :fill-image metadata)))) + ;; Do nothing if the shape is not of a correct type + shape)) + diff --git a/frontend/src/app/util/path/subpaths.cljs b/frontend/src/app/util/path/subpaths.cljs index 3337b6c88..010f1343a 100644 --- a/frontend/src/app/util/path/subpaths.cljs +++ b/frontend/src/app/util/path/subpaths.cljs @@ -22,7 +22,10 @@ (defn add-subpath-command "Adds a command to the subpath" [subpath command] - (let [p (upc/command->point command)] + (let [command (if (= :close-path (:command command)) + (upc/make-line-to (:from subpath)) + command) + p (upc/command->point command)] (-> subpath (assoc :to p) (update :data conj command)))) diff --git a/frontend/src/app/util/quadtree.js b/frontend/src/app/util/quadtree.js index a6f904c10..000ae7c42 100644 --- a/frontend/src/app/util/quadtree.js +++ b/frontend/src/app/util/quadtree.js @@ -32,12 +32,16 @@ "use strict"; goog.provide("app.util.quadtree"); +goog.require("cljs.core"); goog.scope(function() { const self = app.util.quadtree; + const eq = cljs.core._EQ_; + const contains = cljs.core.contains_QMARK_; class Node { - constructor(bounds, data) { + constructor(id, bounds, data) { + this.id = id; this.bounds = bounds; this.data = data; } @@ -51,8 +55,8 @@ goog.scope(function() { this.level = level || 0; this.bounds = bounds; - this.objects = []; - this.indexes = []; + this.objects = []; + this.indexes = []; } split() { @@ -183,14 +187,18 @@ goog.scope(function() { this.objects = []; this.indexes = []; } + + getObjects() { + return this.objects; + } } self.create = function(rect) { return new Quadtree(rect, 10, 4, 0); }; - self.insert = function(index, bounds, data) { - const node = new Node(bounds, data); + self.insert = function(index, id, bounds, data) { + const node = new Node(id, bounds, data); index.insert(node); return index; }; @@ -210,4 +218,29 @@ goog.scope(function() { } }; + self.remove = function(index, id) { + const result = self.create(index.bounds); + + for (let node of index.objects) { + if (!eq(id, node.id)) { + self.insert(result, node.id, node.bounds, node.data); + } + } + + return result; + } + + // FIXME: Inefficient to recreate the index. Needs to be improved + self.remove_all = function(index, ids) { + const result = self.create(index.bounds); + + for (let node of self.search(index, index.bounds)) { + if (!contains(ids, node.id)) { + self.insert(result, node.id, node.bounds, node.data); + } + } + + return result; + } + }); diff --git a/frontend/src/app/util/range_tree.js b/frontend/src/app/util/range_tree.js index ed62a0200..0b7532b8a 100644 --- a/frontend/src/app/util/range_tree.js +++ b/frontend/src/app/util/range_tree.js @@ -13,7 +13,7 @@ "use strict"; goog.provide("app.util.range_tree"); -goog.require("cljs.core") +goog.require("cljs.core"); goog.scope(function() { const eq = cljs.core._EQ_; @@ -92,7 +92,7 @@ goog.scope(function() { } isEmpty() { - return this.root === null; + return !this.root; } toString() { @@ -111,12 +111,12 @@ goog.scope(function() { // Tree implementation functions function isRed(branch) { - return branch !== null && branch.color === Color.RED; + return branch && branch.color === Color.RED; } // Insert recursively in the tree function recInsert (branch, value, data) { - if (branch === null) { + if (!branch) { const ret = new Node(value, data); ret.color = Color.RED; return ret; @@ -144,7 +144,7 @@ goog.scope(function() { // Search for the min node function searchMin(branch) { - if (branch.left === null) { + if (!branch.left) { return branch; } else { return searchMin(branch.left); @@ -153,7 +153,7 @@ goog.scope(function() { // Remove the lefmost node of the current branch function recRemoveMin(branch) { - if (branch.left === null) { + if (!branch.left) { return null; } @@ -167,7 +167,7 @@ goog.scope(function() { // Remove the data element for the value given // this will not remove the node, we have to remove the empty node afterwards function recRemoveData(branch, value, data) { - if (branch === null) { + if (!branch) { // Not found return branch; } else if (branch.value === value) { @@ -193,7 +193,7 @@ goog.scope(function() { if (isRed(branch.left)) { branch = rotateRight(branch); } - if (value === branch.value && branch.right === null) { + if (value === branch.value && !branch.right) { return null; } if (!isRed(branch.right) && !isRed(branch.right.left)) { @@ -214,7 +214,7 @@ goog.scope(function() { // Retrieve all the data related to value function recGet(branch, value) { - if (branch === null) { + if (!branch) { return null; } else if (branch.value === value) { return branch.data; @@ -226,7 +226,7 @@ goog.scope(function() { } function recUpdate(branch, value, oldData, newData) { - if (branch === null) { + if (!branch) { return branch; } else if (branch.value === value) { branch.data = branch.data.map((it) => (eq(it, oldData)) ? newData : it); @@ -239,7 +239,7 @@ goog.scope(function() { } function recRangeQuery(branch, fromValue, toValue, result) { - if (branch === null) { + if (!branch) { return result; } if (fromValue < branch.value) { @@ -287,7 +287,7 @@ goog.scope(function() { function moveRedLeft(branch) { flipColors(branch); - if (isRed(branch.right.left)) { + if (branch.right && isRed(branch.right.left)) { branch.right = rotateRight(branch.right); branch = rotateLeft(branch); flipColors(branch); @@ -329,7 +329,7 @@ goog.scope(function() { // This will return the string representation. We don't care about internal structure // only the data function recToString(branch, result) { - if (branch === null) { + if (!branch) { return; } diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index ab20b3d67..0fc55dc8f 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -58,29 +58,48 @@ ;; --- Navigate (Event) -(deftype Navigate [id params qparams replace] - ptk/UpdateEvent - (update [_ state] - (dissoc state :exception)) +(defn navigated + [match] + (ptk/reify ::navigated + IDeref + (-deref [_] match) - ptk/EffectEvent - (effect [_ state stream] - (let [router (:router state) - history (:history state) - path (resolve router id params qparams)] - (if ^boolean replace - (bhistory/replace-token! history path) - (bhistory/set-token! history path))))) + ptk/UpdateEvent + (update [_ state] + (assoc state :route match)))) + +(defn navigate* + [id params qparams replace] + (ptk/reify ::navigate + IDeref + (-deref [_] + {:id id + :path-params params + :query-params qparams + :replace replace}) + + ptk/UpdateEvent + (update [_ state] + (dissoc state :exception)) + + ptk/EffectEvent + (effect [_ state stream] + (let [router (:router state) + history (:history state) + path (resolve router id params qparams)] + (if ^boolean replace + (bhistory/replace-token! history path) + (bhistory/set-token! history path)))))) (defn nav ([id] (nav id nil nil)) ([id params] (nav id params nil)) - ([id params qparams] (Navigate. id params qparams false))) + ([id params qparams] (navigate* id params qparams false))) (defn nav' ([id] (nav id nil nil)) ([id params] (nav id params nil)) - ([id params qparams] (Navigate. id params qparams true))) + ([id params qparams] (navigate* id params qparams true))) (def navigate nav) diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs index e88dc656f..aecd1f03a 100644 --- a/frontend/src/app/util/storage.cljs +++ b/frontend/src/app/util/storage.cljs @@ -8,6 +8,7 @@ (:require [app.util.transit :as t] [app.util.timers :as tm] + [app.util.globals :as g] [app.common.exceptions :as ex])) (defn- ^boolean is-worker? @@ -19,65 +20,35 @@ [v] (ex/ignoring (t/decode v))) -(def local - {:get #(decode (.getItem ^js js/localStorage (name %))) - :set #(.setItem ^js js/localStorage (name %1) (t/encode %2))}) - -(def session - {:get #(decode (.getItem ^js js/sessionStorage (name %))) - :set #(.setItem ^js js/sessionStorage (name %1) (t/encode %2))}) (defn- persist - [alias storage value] - (when-not (is-worker?) - (tm/schedule-on-idle - (fn [] ((:set storage) alias value))))) + [storage prev curr] + (run! (fn [key] + (let [prev* (get prev key) + curr* (get curr key)] + (when (not= curr* prev*) + (tm/schedule-on-idle + #(if (some? curr*) + (.setItem ^js storage (t/encode key) (t/encode curr*)) + (.removeItem ^js storage (t/encode key))))))) + + (into #{} (concat (keys curr) + (keys prev))))) (defn- load - [alias storage] - (when-not (is-worker?) - ((:get storage) alias))) - -(defn- make-storage - [alias storage] - (let [data (atom (load alias storage))] - (add-watch data :sub #(persist alias storage %4)) - (reify - Object - (toString [_] - (str "Storage" (pr-str @data))) - - ICounted - (-count [_] - (count @data)) - - ISeqable - (-seq [_] - (seq @data)) - - IReset - (-reset! [self newval] - (reset! data newval)) - - ISwap - (-swap! [self f] - (swap! data f)) - (-swap! [self f x] - (swap! data f x)) - (-swap! [self f x y] - (swap! data f x y)) - (-swap! [self f x y more] - (apply swap! data f x y more)) - - ILookup - (-lookup [_ key] - (get @data key nil)) - (-lookup [_ key not-found] - (get @data key not-found))))) + [storage] + (when storage + (let [len (.-length ^js storage)] + (reduce (fn [res index] + (let [key (.key ^js storage index) + val (.getItem ^js storage key)] + (try + (assoc res (t/decode key) (t/decode val)) + (catch :default e + res)))) + {} + (range len))))) -(defonce storage - (make-storage "app" local)) - -(defonce cache - (make-storage "cache" session)) +(defonce storage (atom (load (unchecked-get g/global "localStorage")))) +(add-watch storage :persistence #(persist js/localStorage %3 %4)) diff --git a/frontend/src/app/util/theme.cljs b/frontend/src/app/util/theme.cljs index 93e76f8d5..deed7644a 100644 --- a/frontend/src/app/util/theme.cljs +++ b/frontend/src/app/util/theme.cljs @@ -17,7 +17,7 @@ [app.util.transit :as t] [app.util.storage :refer [storage]])) -(defonce theme (get storage ::theme cfg/default-theme)) +(defonce theme (get @storage ::theme cfg/default-theme)) (defonce theme-sub (rx/subject)) (defonce themes #js {}) diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index 67b75291c..317ff866e 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -12,6 +12,9 @@ ["date-fns/locale/el" :default dateFnsLocalesEl] ["date-fns/locale/fr" :default dateFnsLocalesFr] ["date-fns/locale/ca" :default dateFnsLocalesCa] + ["date-fns/locale/de" :default dateFnsLocalesDe] + ["date-fns/locale/ro" :default dateFnsLocalesRo] + ["date-fns/locale/pt-BR" :default dateFnsLocalesPtBr] ["date-fns/locale/en-US" :default dateFnsLocalesEnUs] ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] ["date-fns/locale/es" :default dateFnsLocalesEs] @@ -205,6 +208,9 @@ :ca dateFnsLocalesCa :el dateFnsLocalesEl :ru dateFnsLocalesRu + :ro dateFnsLocalesRo + :de dateFnsLocalesDe + :pt_br dateFnsLocalesPtBr :zh_cn dateFnsLocalesZhCn}) (defn timeago diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs index 4675f0262..be8cfdbc8 100644 --- a/frontend/src/app/util/timers.cljs +++ b/frontend/src/app/util/timers.cljs @@ -34,12 +34,13 @@ (-dispose [_] (js/clearInterval sem))))) -(if (and (exists? js/window) (.-requestIdleCallback js/window)) +(if (and (exists? js/window) + (.-requestIdleCallback js/window)) (do (def ^:private request-idle-callback #(js/requestIdleCallback %)) (def ^:private cancel-idle-callback #(js/cancelIdleCallback %))) (do - (def ^:private request-idle-callback #(js/setTimeout % 100)) + (def ^:private request-idle-callback #(js/setTimeout % 250)) (def ^:private cancel-idle-callback #(js/clearTimeout %)))) (defn schedule-on-idle diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 9e289c83e..0da4d753a 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -29,6 +29,10 @@ [file] (file-reader #(.readAsText %1 file))) +(defn read-file-as-array-buffer + [file] + (file-reader #(.readAsArrayBuffer %1 file))) + (defn read-file-as-data-url [file] (file-reader #(.readAsDataURL ^js %1 file))) @@ -127,3 +131,14 @@ :else (ex/raise :type :not-supported :hint "seems like the current browset does not support fullscreen api."))) + +(defn observe-resize + [node] + (rx/create + (fn [subs] + (let [obs (js/ResizeObserver. + (fn [entries x] + (rx/push! subs entries)))] + (.observe ^js obs node) + (fn [] + (.disconnect ^js obs)))))) diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index 107270387..77032133a 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -39,11 +39,15 @@ (defmethod handler :update-page-indices [{:keys [page-id changes] :as message}] - (swap! state ch/process-changes changes false) + (let [old-objects (get-in @state [:pages-index page-id :objects])] + (swap! state ch/process-changes changes false) - (let [objects (get-in @state [:pages-index page-id :objects]) - message (assoc message :objects objects)] - (handler (-> message - (assoc :cmd :selection/update-index))) - (handler (-> message - (assoc :cmd :snaps/update-index))))) + (let [new-objects (get-in @state [:pages-index page-id :objects]) + message (assoc message + :objects new-objects + :new-objects new-objects + :old-objects old-objects)] + (handler (-> message + (assoc :cmd :selection/update-index))) + (handler (-> message + (assoc :cmd :snaps/update-index)))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 2d0ac3a5d..09cc4041a 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -15,12 +15,126 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.util.quadtree :as qdt] - [app.worker.impl :as impl])) + [app.worker.impl :as impl] + [clojure.set :as set])) (defonce state (l/atom {})) -(declare index-object) -(declare create-index) +(defn index-shape + [objects parents-index masks-index] + (fn [index shape] + (let [{:keys [x y width height]} (gsh/points->selrect (:points shape)) + shape-bound #js {:x x :y y :width width :height height} + + parents (get parents-index (:id shape)) + masks (get masks-index (:id shape)) + + frame (when (and (not= :frame (:type shape)) + (not= (:frame-id shape) uuid/zero)) + (get objects (:frame-id shape)))] + (qdt/insert index + (:id shape) + shape-bound + (assoc shape :frame frame :masks masks :parents parents))))) + +(defn- create-index + [objects] + (let [shapes (-> objects (dissoc uuid/zero) (vals)) + parents-index (cp/generate-child-all-parents-index objects) + masks-index (cp/create-mask-index objects parents-index) + bounds #js {:x (int -0.5e7) + :y (int -0.5e7) + :width (int 1e7) + :height (int 1e7)} + + index (reduce (index-shape objects parents-index masks-index) + (qdt/create bounds) + shapes) + + z-index (cp/calculate-z-index objects)] + + {:index index :z-index z-index})) + +(defn- update-index + [{index :index z-index :z-index :as data} old-objects new-objects] + + (if (some? data) + (let [changes? (fn [id] + (not= (get old-objects id) + (get new-objects id))) + + changed-ids (into #{} + (comp (filter changes?) + (filter #(not= % uuid/zero))) + (set/union (keys old-objects) + (keys new-objects))) + + shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) + parents-index (cp/generate-child-all-parents-index new-objects shapes) + masks-index (cp/create-mask-index new-objects parents-index) + + new-index (qdt/remove-all index changed-ids) + + index (reduce (index-shape new-objects parents-index masks-index) + new-index + shapes) + + z-index (cp/update-z-index z-index changed-ids old-objects new-objects)] + + {:index index :z-index z-index}) + + ;; If not previous data. We need to create from scratch + (create-index new-objects))) + +(defn- query-index + [{index :index z-index :z-index} rect frame-id include-frames? include-groups? disabled-masks reverse?] + (let [result (-> (qdt/search index (clj->js rect)) + (es6-iterator-seq)) + + ;; Check if the shape matches the filter criteria + match-criteria? + (fn [shape] + (and (not (:hidden shape)) + (not (:blocked shape)) + (or (not frame-id) (= frame-id (:frame-id shape))) + (case (:type shape) + :frame include-frames? + :group include-groups? + true))) + + overlaps? + (fn [shape] + (gsh/overlaps? shape rect)) + + overlaps-masks? + (fn [masks] + (->> masks + (some (comp not overlaps?)) + not)) + + add-z-index + (fn [{:keys [id frame-id] :as shape}] + (assoc shape :z (+ (get z-index id) + (get z-index frame-id 0)))) + + ;; Shapes after filters of overlapping and criteria + matching-shapes + (into [] + (comp (map #(unchecked-get % "data")) + (filter match-criteria?) + (filter overlaps?) + (filter (comp overlaps? :frame)) + (filter (comp overlaps-masks? :masks)) + (map add-z-index)) + result) + + keyfn (if reverse? (comp - :z) :z)] + + (into (d/ordered-set) + (->> matching-shapes + (sort-by keyfn) + (map :id))))) + (defmethod impl/handler :selection/initialize-index [{:keys [file-id data] :as message}] @@ -35,96 +149,18 @@ nil)) (defmethod impl/handler :selection/update-index - [{:keys [page-id objects] :as message}] - (let [index (create-index objects)] - (swap! state update page-id (constantly index)) - nil)) + [{:keys [page-id old-objects new-objects] :as message}] + (swap! state update page-id update-index old-objects new-objects) + nil) (defmethod impl/handler :selection/query [{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks reverse?] :or {include-groups? true disabled-masks #{} reverse? false} :as message}] (when-let [index (get @state page-id)] - (let [result (-> (qdt/search index (clj->js rect)) - (es6-iterator-seq)) - - ;; Check if the shape matches the filter criteria - match-criteria? - (fn [shape] - (and (not (:hidden shape)) - (not (:blocked shape)) - (or (not frame-id) (= frame-id (:frame-id shape))) - (case (:type shape) - :frame include-frames? - :group include-groups? - true))) - - overlaps? - (fn [shape] - (gsh/overlaps? shape rect)) - - overlaps-masks? - (fn [masks] - (->> masks - (some (comp not overlaps?)) - not)) - - ;; Shapes after filters of overlapping and criteria - matching-shapes - (into [] - (comp (map #(unchecked-get % "data")) - (filter match-criteria?) - (filter (comp overlaps? :frame)) - (filter (comp overlaps-masks? :masks)) - (filter overlaps?)) - result) - - keyfn (if reverse? (comp - :z) :z)] - - (into (d/ordered-set) - (->> matching-shapes - (sort-by keyfn) - (map :id)))))) - -(defn create-mask-index - "Retrieves the mask information for an object" - [objects parents-index] - (let [retrieve-masks - (fn [id parents] - (->> parents - (map #(get objects %)) - (filter #(:masked-group? %)) - ;; Retrieve the masking element - (mapv #(get objects (->> % :shapes first)))))] - (->> parents-index - (d/mapm retrieve-masks)))) - -(defn- create-index - [objects] - (let [shapes (-> objects (dissoc uuid/zero) (vals)) - z-index (cp/calculate-z-index objects) - parents-index (cp/generate-child-all-parents-index objects) - masks-index (create-mask-index objects parents-index) - bounds (gsh/selection-rect shapes) - bounds #js {:x (:x bounds) - :y (:y bounds) - :width (:width bounds) - :height (:height bounds)}] - - (reduce (partial index-object objects z-index parents-index masks-index) - (qdt/create bounds) - shapes))) - -(defn- index-object - [objects z-index parents-index masks-index index obj] - (let [{:keys [x y width height]} (:selrect obj) - shape-bound #js {:x x :y y :width width :height height} - parents (get parents-index (:id obj)) - masks (get masks-index (:id obj)) - z (get z-index (:id obj)) - frame (when (and (not= :frame (:type obj)) - (not= (:frame-id obj) uuid/zero)) - (get objects (:frame-id obj)))] - (qdt/insert index - shape-bound - (assoc obj :frame frame :masks masks :parents parents :z z)))) + (query-index index rect frame-id include-frames? include-groups? disabled-masks reverse?))) +(defmethod impl/handler :selection/query-z-index + [{:keys [page-id objects ids]}] + (when-let [{z-index :z-index} (get @state page-id)] + (->> ids (map #(+ (get z-index %) + (get z-index (get-in objects [% :frame-id]))))))) diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index 9da49438b..a2c395448 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -6,48 +6,133 @@ (ns app.worker.snaps (:require - [okulary.core :as l] - [app.common.uuid :as uuid] - [app.common.pages :as cp] [app.common.data :as d] - [app.worker.impl :as impl] - [app.util.range-tree :as rt] + [app.common.pages :as cp] + [app.common.uuid :as uuid] + [app.util.geom.grid :as gg] [app.util.geom.snap-points :as snap] - [app.util.geom.grid :as gg])) + [app.util.range-tree :as rt] + [app.worker.impl :as impl] + [clojure.set :as set] + [okulary.core :as l])) (defonce state (l/atom {})) -(defn- create-coord-data - "Initializes the range tree given the shapes" - [frame-id shapes coord] - (let [process-shape (fn [coord] - (fn [shape] - (concat - (let [points (snap/shape-snap-points shape)] - (map #(vector % (:id shape)) points)) +(defn process-shape [frame-id coord] + (fn [shape] + (let [points (snap/shape-snap-points shape) + shape-data (->> points (mapv #(vector % (:id shape))))] + (if (= (:id shape) frame-id) + (d/concat + shape-data - ;; The grid points are only added by the "root" of the coord-dat - (when (= (:id shape) frame-id) - (let [points (gg/grid-snap-points shape coord)] - (map #(vector % :layout) points)))))) - into-tree (fn [tree [point _ :as data]] - (rt/insert tree (coord point) data))] + ;; The grid points are only added by the "root" of the coord-dat + (->> (gg/grid-snap-points shape coord) + (map #(vector % :layout)))) + + shape-data)))) + +(defn- add-coord-data + "Initializes the range tree given the shapes" + [data frame-id shapes coord] + (letfn [(into-tree [tree [point _ :as data]] + (rt/insert tree (coord point) data))] (->> shapes - (mapcat (process-shape coord)) - (reduce into-tree (rt/make-tree))))) + (mapcat (process-shape frame-id coord)) + (reduce into-tree (or data (rt/make-tree)))))) + +(defn remove-coord-data + [data frame-id shapes coord] + (letfn [(remove-tree [tree [point _ :as data]] + (rt/remove tree (coord point) data))] + (->> shapes + (mapcat (process-shape frame-id coord)) + (reduce remove-tree (or data (rt/make-tree)))))) + +(defn aggregate-data + ([objects] + (aggregate-data objects (keys objects))) + + ([objects ids] + (->> ids + (filter #(contains? objects %)) + (map #(get objects %)) + (filter :frame-id) + (group-by :frame-id) + ;; Adds the frame + (d/mapm #(conj %2 (get objects %1)))))) (defn- initialize-snap-data "Initialize the snap information with the current workspace information" [objects] - (let [frame-shapes (->> (vals objects) - (filter :frame-id) - (group-by :frame-id)) - frame-shapes (->> (cp/select-frames objects) - (reduce #(update %1 (:id %2) conj %2) frame-shapes))] + (let [shapes-data (aggregate-data objects) - (d/mapm (fn [frame-id shapes] {:x (create-coord-data frame-id shapes :x) - :y (create-coord-data frame-id shapes :y)}) - frame-shapes))) + create-index + (fn [frame-id shapes] + {:x (-> (rt/make-tree) (add-coord-data frame-id shapes :x)) + :y (-> (rt/make-tree) (add-coord-data frame-id shapes :y))})] + (d/mapm create-index shapes-data))) + +;; Attributes that will change the values of their snap +(def snap-attrs [:x :y :width :height :selrect :grids]) + +(defn- update-snap-data + [snap-data old-objects new-objects] + + (let [changed? (fn [id] + (let [oldv (get old-objects id) + newv (get new-objects id)] + ;; Check first without select-keys because is faster if they are + ;; the same reference + (and (not= oldv newv) + (not= (select-keys oldv snap-attrs) + (select-keys newv snap-attrs))))) + + is-deleted-frame? #(and (not= uuid/zero %) + (contains? old-objects %) + (not (contains? new-objects %)) + (= :frame (get-in old-objects [% :type]))) + is-new-frame? #(and (not= uuid/zero %) + (contains? new-objects %) + (not (contains? old-objects %)) + (= :frame (get-in new-objects [% :type]))) + + changed-ids (into #{} + (filter changed?) + (set/union (keys old-objects) (keys new-objects))) + + to-delete (aggregate-data old-objects changed-ids) + to-add (aggregate-data new-objects changed-ids) + + frames-to-delete (->> changed-ids (filter is-deleted-frame?)) + frames-to-add (->> changed-ids (filter is-new-frame?)) + + delete-data + (fn [snap-data [frame-id shapes]] + (-> snap-data + (update-in [frame-id :x] remove-coord-data frame-id shapes :x) + (update-in [frame-id :y] remove-coord-data frame-id shapes :y))) + + add-data + (fn [snap-data [frame-id shapes]] + (-> snap-data + (update-in [frame-id :x] add-coord-data frame-id shapes :x) + (update-in [frame-id :y] add-coord-data frame-id shapes :y))) + + delete-frames + (fn [snap-data frame-id] + (dissoc snap-data frame-id)) + + add-frames + (fn [snap-data frame-id] + (assoc snap-data frame-id {:x (rt/make-tree) + :y (rt/make-tree)}))] + + (as-> snap-data $ + (reduce delete-data $ to-delete) + (reduce add-frames $ frames-to-add) + (reduce add-data $ to-add) + (reduce delete-frames $ frames-to-delete)))) (defn- log-state "Helper function to print a friendly version of the snap tree. Debugging purposes" @@ -60,6 +145,16 @@ (let [snap-data (initialize-snap-data objects)] (assoc state page-id snap-data))) +(defn- update-page [state page-id old-objects new-objects] + (let [changed? #(not= (get old-objects %) (get new-objects %)) + changed-ids (into #{} + (filter changed?) + (set/union (keys old-objects) (keys new-objects))) + + snap-data (get state page-id) + snap-data (update-snap-data snap-data old-objects new-objects)] + (assoc state page-id snap-data))) + ;; Public API (defmethod impl/handler :snaps/initialize-index [{:keys [file-id data] :as message}] @@ -74,9 +169,11 @@ nil)) (defmethod impl/handler :snaps/update-index - [{:keys [page-id objects] :as message}] - ;; TODO: Check the difference and update the index acordingly - (swap! state index-page page-id objects) + [{:keys [page-id old-objects new-objects] :as message}] + (swap! state update-page page-id old-objects new-objects) + + ;; Uncomment this to regenerate the index everytime + #_(swap! state index-page page-id new-objects) ;; (log-state) nil) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index f8db65343..ddf65c6b1 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -34,7 +34,7 @@ (p/create (fn [resolve reject] (->> (http/send! {:uri uri - :query {:file-id file-id :id page-id} + :query {:file-id file-id :id page-id :strip-thumbnails true} :method :get}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response) @@ -50,7 +50,7 @@ (let [prev (get @cache ckey)] (if (= (:data prev) data) (:result prev) - (let [elem (mf/element exports/page-svg #js {:data data :width "290" :height "150"}) + (let [elem (mf/element exports/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}) result (rds/renderToStaticMarkup elem)] (swap! cache assoc ckey {:data data :result result}) result)))) diff --git a/frontend/tests/app/test_components_sync.cljs b/frontend/tests/app/test_components_sync.cljs index 5fa6bcd12..e93ab55ed 100644 --- a/frontend/tests/app/test_components_sync.cljs +++ b/frontend/tests/app/test_components_sync.cljs @@ -1,18 +1,18 @@ (ns app.test-components-sync - (:require [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]] - [clojure.stacktrace :as stk] - [beicon.core :as rx] - [linked.core :as lks] - [app.test-helpers.events :as the] - [app.test-helpers.pages :as thp] - [app.test-helpers.libraries :as thl] - [app.common.geom.point :as gpt] - [app.common.data :as d] - [app.common.pages.helpers :as cph] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.libraries-helpers :as dwlh])) + (:require + [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [beicon.core :as rx] + [linked.core :as lks] + [app.test-helpers.events :as the] + [app.test-helpers.pages :as thp] + [app.test-helpers.libraries :as thl] + [app.common.geom.point :as gpt] + [app.common.data :as d] + [app.common.pages.helpers :as cph] + [app.main.data.workspace.changes :as dwc] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.libraries-helpers :as dwlh])) (t/use-fixtures :each {:before thp/reset-idmap!}) diff --git a/frontend/tests/app/test_shapes.cljs b/frontend/tests/app/test_shapes.cljs index 157780071..2b473c02a 100644 --- a/frontend/tests/app/test_shapes.cljs +++ b/frontend/tests/app/test_shapes.cljs @@ -50,10 +50,6 @@ (dwl/add-recent-color color)) (rx/map (fn [new-state] - (t/is (= (get-in new-state [:workspace-file - :data - :recent-colors]) - [color])) (t/is (= (get-in new-state [:workspace-data :recent-colors]) [color])))) diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po new file mode 100644 index 000000000..86bac1358 --- /dev/null +++ b/frontend/translations/ar.po @@ -0,0 +1,232 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"Last-Translator: Amine Gdoura \n" +"Language-Team: Arabic " +"\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 4.7-dev\n" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "هل لديك حساب؟" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "تحقق من بريدك الإلكتروني وانقر على الرابط للتحقق والبدء في استخدام Penpot." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "تأكيد كلمة المرور" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "إنشاء حساب تجريبي" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "ترغب في التجربة فحسب؟" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "هذه خدمة تجريبية ، لا تستخدمها للعمل الحقيقي ، سيتم مسح المشاريع بشكل دوري." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "البريد الالكتروني" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "هل نسيت كلمة السر؟" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "الاسم بالكامل" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.go-back-to-login" +msgstr "الرجوع للخلف!" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "تسجيل الدخول هنا" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "تسجيل الدخول" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-subtitle" +msgstr "أدخل التفاصيل أدناه" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "سعيد برؤيتك مجددا!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "تسجيل الدخول عبر Github" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "تسجيل الدخول عبر Gitlab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "تسجيل الدخول عبر جوجل" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "تسجيل الدخول باستخدام LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "تسجيل الدخول باستخدام OpenID (SSO)" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "اكتب كلمة مرور جديدة" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "رمز الاسترداد غير صالح." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-succesfully" +msgstr "تم تغيير كلمة المرور بنجاح" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "تم الانضمام إلى الفريق بنجاح" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "كلمه السر" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "8 أحرف على الأقل" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "إستعادة كلمة المرور" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "سنرسل لك رسالة بريد إلكتروني تحتوي على التعليمات" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "نسيت كلمة المرور؟" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "تغيير كلمة المرور" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "لا تملك حساب بعد؟" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "إنشاء حساب" + +#: src/app/main/ui/auth/register.cljs +#, fuzzy +msgid "auth.register-subtitle" +msgstr "إنه مجاني ، مفتوح المصدر" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "إنشاء حساب" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "الحل (مفتوح المصدر) للتصميم والنمذجة." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "عند إنشاء حساب جديد ، فإنك توافق على شروط الخدمة وسياسة الخصوصية الخاصة بنا." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "لقد أرسلنا رسالة تحقق إلى بريدك الالكتروني" + +#, fuzzy, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"ستتم إضافة أي خط ويب تقوم بتحميله هنا إلى قائمة عائلة الخطوط المتوفرة في " +"خصائص النص الخاصة بملفات هذا الفريق. سيتم تجميع الخطوط التي لها نفس اسم " +"عائلة الخطوط على أنها ** عائلة خط واحدة **. يمكنك تحميل الخطوط بالتنسيقات " +"التالية: ** TTF و OTF و WOFF ** (ستحتاج إلى تنسيق واحد فقط)." + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"يجب عليك فقط تحميل الخطوط التي تمتلكها أو لديك ترخيص لاستخدامها في Penpot. " +"اكتشف المزيد في قسم حقوق المحتوى في [شروط خدمة Penpot] " +"(https://penpot.app/terms.html). قد ترغب أيضًا في القراءة عن [ترخيص الخطوط] " +"(2)." + +msgid "labels.custom-fonts" +msgstr "خطوط مخصصة" + +msgid "labels.font-family" +msgstr "عائلة الخط" + +#, fuzzy +msgid "labels.font-providers" +msgstr "موفرو الخطوط" + +#, fuzzy +msgid "labels.font-variant" +msgstr "نمط" + +msgid "labels.fonts" +msgstr "الخطوط" + +msgid "labels.images" +msgstr "الصور" + +msgid "labels.installed-fonts" +msgstr "الخطوط المتوفرة" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "الأعضاء" + +msgid "labels.search-font" +msgstr "البحث عن الخط" + +#, fuzzy +msgid "labels.upload" +msgstr "تحميل" + +msgid "labels.upload-custom-fonts" +msgstr "تحميل الخطوط المخصصة" + +msgid "labels.uploading" +msgstr "جارٍ التحميل ..." + +msgid "modals.delete-font.message" +msgstr "هل أنت متأكد أنك تريد حذف هذا الخط؟ لن يتم تحميله إذا تم استخدامه في ملف." + +msgid "modals.delete-font.title" +msgstr "حذف الخط" + +#: src/app/main/ui/dashboard/fonts.cljs +#, fuzzy +msgid "title.dashboard.font-providers" +msgstr "موفرو الخطوط - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "الخطوط -٪ s - Penpot" + +msgid "workspace.viewport.click-to-close-path" +msgstr "انقر لإغلاق المسار" \ No newline at end of file diff --git a/frontend/translations/da.po b/frontend/translations/da.po new file mode 100644 index 000000000..2847eee0e --- /dev/null +++ b/frontend/translations/da.po @@ -0,0 +1,484 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"Last-Translator: Simon Bechmann \n" +"Language-Team: Danish " +"\n" +"Language: da\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" +"X-Generator: Weblate 4.7-dev\n" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "Har du allerede en konto?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Tjek din mail og klik på linket for at bekræfte og starte med at bruge " +"Penpot." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Bekræft adgangskode" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Lav demokonto" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Vil du bare prøve det?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Det her er en DEMO service, BRUG IKKE for rigtigt arbejde, projekterne vil " +"blive slettet periodevis." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "Email" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Glemt adgangskode?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Fulde Navn" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.go-back-to-login" +msgstr "Gå tilbage!" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Log på her" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Log på" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-subtitle" +msgstr "Indtast dine oplysninger nedenunder" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Fedt at se dig igen!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "Log på med Github" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "Log på med Gitlab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Log på med Google" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "Log på med LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Log på med OpenID (SSO)" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Indtast et nyt kodeord" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "Genopretningspoletten er ugyldig." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-succesfully" +msgstr "Adgangskoden er blevet ændret" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "Profilen er ikke bekræftet, venligt verificer profilen før du går videre." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Gendannelseslink for adgangskoden er sendt til din indbakke." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Tilsluttet teamet med succes" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Adgangskode" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Mindst 8 karakterer" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Gendan Adgangskode" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Vi sender dig en mail med instruktioner" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Glemt adgangskode?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Skift din adgangskode" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Ingen konto?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Opret en konto" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Det er gratis, det er Open Source" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Opret en konto" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "Open-source løsningen for design og prototyping." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"Når du opretter en ny konto, accepterer du vores servicevilkår og " +"fortrolighedspolitik." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Vi har sendt en bekræftelsesmail til" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Tilføj som Delt Bibliotek" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Skift email" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(kopi)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "+ Opret nyt team" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Dit Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Slet team" + +msgid "dashboard.draft-title" +msgstr "Udkast" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Dublikér" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Dublikér %s filer" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.empty-files" +msgstr "Du har stadig ingen filer her" + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Alle web skrifttyper, som du uploader her, vil blive tilføjet til listen af " +"skrifttypefamilier tilgængelig ved tekstindstillingerne i filerne for dette " +"team. Skrifttyper med det samme skrifttypefamilienavn vil blive grupperet " +"som en **enkelt skrifttypefamilie**. Du kan uploade skrifttyper med " +"følgende formater: **TTF, OTF og WOFF** (kun én er nødvendig)." + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Du bør kun uploade skrifttyper som du ejer eller har licens til at bruge i " +"Penpot. Find ud af mere i sektionen om indholdsrettigheder i [Penpot's " +"Terms of Service] (https://penpot.app/terms.html). Du kan også læse om " +"[font licensing](2)." + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "Invitér til team" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "Forlad team" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "Delte Biblioteker" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "indlæser dine filer…" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "Flyt til" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "Flyt %s filer til" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "Flyt til andet team" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ Ny Fil" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Ny Fil" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ Nyt projekt" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Nyt Projekt" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "Intet match fundet for “%s“" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "Fastgjorte projekter bliver vist her" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "Din email-adresse er blevet opdateret med succes" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "Din email-adresse er blevet bekræftet med succes" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "Adgangskode gemt med succes!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s medlemmer" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "Åben fil i en ny fane" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "Skift adgangskode" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "Fastgør/Løsne" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "Projekter" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.promote-to-owner" +msgstr "Forfrem til ejer" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "Vil du slette din konto?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "Fjern som Delt Bibliotek" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "Søg…" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.searching-for" +msgstr "Søger efter “%s“…" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-language" +msgstr "Vælg UI sprog" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "Vælg tema" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "Vis alle filer" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "Din fil er blevet slettet med succes" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "Dit projekt er blevet slettet med succes" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "Din fil er blevet dublikeret med succes" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "Dit projekt er blevet dublikeret med succes" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "Din fil er blevet flyttet med succes" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "Dine filer er blevet flyttet med succes" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "Dit projekt er blevet flyttet med succes" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.switch-team" +msgstr "Skift team" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "Team info" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "Medlemmer" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "Team projekter" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.theme-change" +msgstr "UI tema" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.title-search" +msgstr "Søgeresultater" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.type-something" +msgstr "Skriv for at søge i resultater" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "Opdater indstillinger" + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "Din konto" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "Email" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "Dit navn" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "Dit Penpot" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "Fortryd" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "Ok" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "Er du sikker?" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "ds.updated-at" +msgstr "Opdateret: %s" + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "Din browser kan ikke gøre denne operation" + +msgid "labels.custom-fonts" +msgstr "Brugerdefinerede skrifttyper" + +msgid "labels.font-family" +msgstr "Skrifttypefamilie" + +msgid "labels.font-providers" +msgstr "Skrifttype udbydere" + +msgid "labels.font-variant" +msgstr "Stil" + +msgid "labels.fonts" +msgstr "Skrifttyper" + +msgid "labels.installed-fonts" +msgstr "Installeret skrifttyper" + +msgid "labels.search-font" +msgstr "Søg efter skrifttype" + +msgid "labels.upload" +msgstr "Upload" + +msgid "labels.upload-custom-fonts" +msgstr "Upload brugerdefinerede skrifttyper" + +msgid "labels.uploading" +msgstr "Uploader..." + +msgid "modals.delete-font.message" +msgstr "" +"Er du sikker på, at du vil slette denne skrifttype? Den vil ikke indlæse, " +"hvis den bliver brugt i en fil." + +msgid "modals.delete-font.title" +msgstr "Sletter skrifttype" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Skrifttype Udbydere - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Skrifttyper - %s - Penpot" \ No newline at end of file diff --git a/frontend/translations/de.po b/frontend/translations/de.po index ee2c448b3..c5179aefa 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,10 +1,15 @@ msgid "" msgstr "" +"PO-Revision-Date: 2021-05-13 08:44+0000\n" +"Last-Translator: Andrey Antukh \n" +"Language-Team: German " +"\n" "Language: de\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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -2250,15 +2255,15 @@ msgstr "Stift (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "Ellipse (E)" +msgstr "Ellipse (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Zeichenfläche (A)" +msgstr "Zeichenfläche (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Bild (K)" +msgstr "Bild (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -2266,15 +2271,15 @@ msgstr "Verschieben" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "Pfad (P)" +msgstr "Pfad (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "Rechteck (R)" +msgstr "Rechteck (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "Text (T)" +msgstr "Text (%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index 2aa22d3f1..d5b782250 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -2204,15 +2204,15 @@ msgstr "Στροφή (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "Έλλειψη (Ε)" +msgstr "Έλλειψη (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Artboard (Α)" +msgstr "Artboard (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Εικόνα (Κ)" +msgstr "Εικόνα (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -2220,15 +2220,15 @@ msgstr "" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "Path (Ρ)" +msgstr "Path (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "Ορθογώνιο (R)" +msgstr "Ορθογώνιο (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "Κείμενο (Τ)" +msgstr "Κείμενο (%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 6fb62455c..a0cd82b37 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-04-14 13:44+0000\n" -"Last-Translator: Andrés Moya \n" +"PO-Revision-Date: 2021-05-20 14:12+0000\n" +"Last-Translator: Jan C. Borchardt \n" "Language-Team: English " "\n" "Language: en\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.6-dev\n" +"X-Generator: Weblate 4.7-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -206,6 +206,29 @@ msgstr "Duplicate %s files" msgid "dashboard.empty-files" msgstr "You still have no files here" +msgid "dashboard.fonts.deleted-placeholder" +msgstr "Font deleted" + +msgid "dashboard.fonts.empty-placeholder" +msgstr "You still have no custom fonts installed." + +#, 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](https://www.typography.com/faq)." + #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Invite to team" @@ -222,6 +245,9 @@ msgstr "Shared Libraries" msgid "dashboard.loading-files" msgstr "loading your files …" +msgid "dashboard.loading-fonts" +msgstr "loading your fonts …" + #: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Move to" @@ -238,10 +264,18 @@ msgstr "Move to other team" msgid "dashboard.new-file" msgstr "+ New File" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "New File" + #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.new-project" msgstr "+ New project" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "New Project" + #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" msgstr "No matches found for “%s“" @@ -336,7 +370,7 @@ msgstr "Your file has been moved successfully" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-move-files" -msgstr "Your files has been moved successfully" +msgstr "Your files have been moved successfully" #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-move-project" @@ -765,6 +799,9 @@ msgstr "You are seeing version %s" msgid "labels.accept" msgstr "Accept" +msgid "labels.add-custom-font" +msgstr "Add custom font" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Admin" @@ -813,6 +850,9 @@ msgstr "Create new team" msgid "labels.create-team.placeholder" msgstr "Enter new team name" +msgid "labels.custom-fonts" +msgstr "Custom fonts" + #: src/app/main/ui/settings/sidebar.cljs msgid "labels.dashboard" msgstr "Dashboard" @@ -857,6 +897,18 @@ msgstr "Feedback disabled" msgid "labels.feedback-sent" msgstr "Feedback sent" +msgid "labels.font-family" +msgstr "Font Family" + +msgid "labels.font-providers" +msgstr "Font providers" + +msgid "labels.font-variants" +msgstr "Styles" + +msgid "labels.fonts" +msgstr "Fonts" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Give feedback" @@ -871,6 +923,9 @@ msgstr "Icons" msgid "labels.images" msgstr "Images" +msgid "labels.installed-fonts" +msgstr "Installed fonts" + #: src/app/main/ui/static.cljs msgid "labels.internal-error.desc-message" msgstr "" @@ -889,6 +944,9 @@ msgstr "Language" msgid "labels.logout" msgstr "Logout" +msgid "labels.manage-fonts" +msgstr "Manage fonts" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Members" @@ -987,13 +1045,16 @@ msgstr "Role" msgid "labels.save" msgstr "Save" +msgid "labels.search-font" +msgstr "Search font" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.send" msgstr "Send" #: src/app/main/ui/settings/feedback.cljs msgid "labels.sending" -msgstr "Sending..." +msgstr "Sending…" #: src/app/main/ui/static.cljs msgid "labels.service-unavailable.desc-message" @@ -1031,6 +1092,15 @@ msgstr "Update" msgid "labels.update-team" msgstr "Update team" +msgid "labels.upload" +msgstr "Upload" + +msgid "labels.upload-custom-fonts" +msgstr "Upload custom fonts" + +msgid "labels.uploading" +msgstr "Uploading…" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Viewer" @@ -1131,6 +1201,22 @@ msgstr "Are you sure you want to delete %s files?" msgid "modals.delete-file-multi-confirm.title" msgstr "Deleting %s files" +msgid "modals.delete-font-variant.message" +msgstr "" +"Are you sure you want to delete this font style? It will not load if is " +"used in a file." + +msgid "modals.delete-font-variant.title" +msgstr "Deleting font style" + +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 "modals.delete-font.title" +msgstr "Deleting font" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "modals.delete-page.body" msgstr "Are you sure you want to delete this page?" @@ -1294,6 +1380,14 @@ msgstr "Mixed" msgid "title.dashboard.files" msgstr "%s - Penpot" +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Font Providers - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Fonts - %s - Penpot" + #: src/app/main/ui/dashboard/projects.cljs msgid "title.dashboard.projects" msgstr "Projects - %s - Penpot" @@ -1504,7 +1598,7 @@ msgstr "Search assets" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.selected-count" msgid_plural "workspace.assets.selected-count" -msgstr[0] "%s items selected" +msgstr[0] "%s item selected" msgstr[1] "%s items selected" #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -1559,6 +1653,10 @@ msgstr "Radial gradient" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Disable dynamic alignment" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "Disable scale text" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-grid" msgstr "Disable snap to grid" @@ -1567,6 +1665,10 @@ msgstr "Disable snap to grid" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Enable dynamic aligment" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "Enable scale text" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-grid" msgstr "Snap to grid" @@ -2365,15 +2467,15 @@ msgstr "Curve (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "Ellipse (E)" +msgstr "Ellipse (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Artboard (A)" +msgstr "Artboard (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Image (K)" +msgstr "Image (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -2381,15 +2483,15 @@ msgstr "Move" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "Path (P)" +msgstr "Path (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "Rectangle (R)" +msgstr "Rectangle (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "Text (T)" +msgstr "Text (%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 44078530f..7063b5649 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -9,14 +9,13 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.6-dev\n" +"X-Generator: Weblate 4.7-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" msgstr "¿Tienes ya una cuenta?" #: src/app/main/ui/auth/register.cljs -#, fuzzy msgid "auth.check-your-email" msgstr "" "Comprueba tu email y haz click en el link de verificación para comenzar a " @@ -211,6 +210,29 @@ msgstr "Duplicar %s archivos" msgid "dashboard.empty-files" msgstr "Todavía no hay ningún archivo aquí" +msgid "dashboard.fonts.deleted-placeholder" +msgstr "Fuente eliminada." + +msgid "dashboard.fonts.empty-placeholder" +msgstr "Aun no tienes fuentes personalizadas." + +#, 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/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Invitar al equipo" @@ -227,6 +249,9 @@ msgstr "Bibliotecas Compartidas" msgid "dashboard.loading-files" msgstr "cargando tus ficheros …" +msgid "dashboard.loading-fonts" +msgstr "cargando tus fuentes …" + #: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to" msgstr "Mover a" @@ -243,10 +268,18 @@ msgstr "Mover a otro equipo" msgid "dashboard.new-file" msgstr "+ Nuevo Archivo" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Nuevo Archivo" + #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.new-project" msgstr "+ Nuevo proyecto" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Nuevo Proyecto" + #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" msgstr "No se encuentra “%s“" @@ -772,6 +805,9 @@ msgstr "Estás viendo la versión %s" msgid "labels.accept" msgstr "Aceptar" +msgid "labels.add-custom-font" +msgstr "Añadir fuentes personalizada" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Administración" @@ -816,6 +852,9 @@ msgstr "Crear" msgid "labels.create-team" msgstr "Crea un nuevo equipo" +msgid "labels.custom-fonts" +msgstr "Fuentes personalizadas" + #: src/app/main/ui/settings/sidebar.cljs msgid "labels.dashboard" msgstr "Panel" @@ -860,6 +899,18 @@ msgstr "El modulo de recepción de opiniones esta deshabilitado." msgid "labels.feedback-sent" msgstr "Opinión enviada" +msgid "labels.font-family" +msgstr "Familia de fuente" + +msgid "labels.font-providers" +msgstr "Proveedores de fuentes" + +msgid "labels.font-variants" +msgstr "Estilos" + +msgid "labels.fonts" +msgstr "Fuentes" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" msgstr "Danos tu opinión" @@ -874,6 +925,9 @@ msgstr "Iconos" msgid "labels.images" msgstr "Imágenes" +msgid "labels.installed-fonts" +msgstr "Fuentes instaladas" + #: src/app/main/ui/static.cljs msgid "labels.internal-error.desc-message" msgstr "" @@ -892,6 +946,9 @@ msgstr "Idioma" msgid "labels.logout" msgstr "Salir" +msgid "labels.manage-fonts" +msgstr "Administrar fuentes" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" msgstr "Integrantes" @@ -990,6 +1047,9 @@ msgstr "Cargo" msgid "labels.save" msgstr "Guardar" +msgid "labels.search-font" +msgstr "Buscar fuente" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.send" msgstr "Enviar" @@ -1034,6 +1094,15 @@ msgstr "Actualizar" msgid "labels.update-team" msgstr "Actualiza el equipo" +msgid "labels.upload" +msgstr "Subir" + +msgid "labels.upload-custom-fonts" +msgstr "Subir fuente" + +msgid "labels.uploading" +msgstr "Subiendo..." + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Visualizador" @@ -1134,6 +1203,22 @@ msgstr "¿Seguro que quieres eliminar %s archivos?" msgid "modals.delete-file-multi-confirm.title" msgstr "Eliminando %s archivos" +msgid "modals.delete-font-variant.message" +msgstr "" +"Estas seguro de querer eliminar esta estilo de fuente? Dejara de cargar si " +"es usada en algun fichero." + +msgid "modals.delete-font-variant.title" +msgstr "Eliminando estilo de fuente" + +msgid "modals.delete-font.message" +msgstr "" +"Estas seguro de querer eliminar esta fuente? Dejara de cargar si es usada " +"en algun fichero." + +msgid "modals.delete-font.title" +msgstr "Eliminando fuente" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "modals.delete-page.body" msgstr "¿Seguro que quieres borrar esta página?" @@ -1289,6 +1374,14 @@ msgstr "Verificación de email enviada a %s. Comprueba tu correo." msgid "settings.multiple" msgstr "Varios" +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Proveedores de fuentes - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Fuentes - %s - Penpot" + #: src/app/main/ui/dashboard/projects.cljs msgid "title.dashboard.projects" msgstr "Proyectos - %s - Penpot" @@ -1552,6 +1645,10 @@ msgstr "Degradado radial" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Desactivar alineamiento dinámico" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "Desactivar escalar texto" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-grid" msgstr "Desactivar alinear a la rejilla" @@ -1560,6 +1657,10 @@ msgstr "Desactivar alinear a la rejilla" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Activar alineamiento dinámico" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "Activar escalar texto" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-grid" msgstr "Alinear a la rejilla" @@ -2360,15 +2461,15 @@ msgstr "Curva (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "Elipse (E)" +msgstr "Elipse (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Tablero (A)" +msgstr "Tablero (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Imagen (K)" +msgstr "Imagen (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -2376,15 +2477,15 @@ msgstr "Mover" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "Ruta (P)" +msgstr "Ruta (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "Rectángulo (R)" +msgstr "Rectángulo (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "Texto (T)" +msgstr "Texto (%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 4f7151394..34e70bab4 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1,10 +1,15 @@ msgid "" msgstr "" +"PO-Revision-Date: 2021-05-13 08:47+0000\n" +"Last-Translator: Andrey Antukh \n" +"Language-Team: French " +"\n" "Language: fr\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" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"X-Generator: Weblate 4.7-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -2036,15 +2041,15 @@ msgstr "Courbe (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "Ellipse (E)" +msgstr "Ellipse (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Plan de travail (A)" +msgstr "Plan de travail (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Image (K)" +msgstr "Image (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -2052,15 +2057,15 @@ msgstr "Déplacer" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "Chemin (P)" +msgstr "Chemin (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "Rectangle (R)" +msgstr "Rectangle (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "Texte (T)" +msgstr "Texte (%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" diff --git a/frontend/translations/id.po b/frontend/translations/id.po new file mode 100644 index 000000000..b1fc23594 --- /dev/null +++ b/frontend/translations/id.po @@ -0,0 +1,6 @@ +msgid "" +msgstr "" +"X-Generator: Weblate\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" \ No newline at end of file diff --git a/frontend/translations/nb_NO.po b/frontend/translations/nb_NO.po new file mode 100644 index 000000000..833e3b94d --- /dev/null +++ b/frontend/translations/nb_NO.po @@ -0,0 +1,796 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-05-09 00:37+0000\n" +"Last-Translator: Allan Nordhøy \n" +"Language-Team: Norwegian Bokmål " +"\n" +"Language: nb_NO\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" +"X-Generator: Weblate 4.7-dev\n" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Bekreft passord" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +#, fuzzy +msgid "auth.email" +msgstr "E-post" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Glemt passordet?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Fullt navn" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Skriv inn et nytt passord" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "Gjenopprettelsessymbolet er ugyldig." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Passord" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Gjenopprett passord" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Endre passordet ditt" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Ingen konto enda?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +#, fuzzy +msgid "auth.register-submit" +msgstr "Opprett konto" + +#: src/app/main/ui/auth/register.cljs +#, fuzzy +msgid "auth.register-title" +msgstr "Opprett konto" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Legg til som delt bibliotek" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(kopi)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Din Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Slett lag" + +msgid "dashboard.draft-title" +msgstr "Kladd" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "Inviter til lag" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "Forlat lag" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "Delte biblioteker" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "Flytt til" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "Flytt %s filer til" + +#: src/app/main/ui/dashboard/file_menu.cljs +#, fuzzy +msgid "dashboard.move-to-other-team" +msgstr "Flytt til annet lag" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +#, fuzzy +msgid "dashboard.new-file" +msgstr "+ Ny fil" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ Nytt prosjekt" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s medlemmer" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "Åpne fil i ny fane" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "Endre passord" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "Fest/løsne" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "Prosjekter" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.promote-to-owner" +msgstr "Promoter til eier" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "Ønsker du å fjerne kontoen din?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#, fuzzy +msgid "dashboard.remove-shared" +msgstr "Fjern som delt bibliotek" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "Søk …" + +#: src/app/main/ui/dashboard/search.cljs +#, fuzzy +msgid "dashboard.searching-for" +msgstr "Şøker etter «%s» …" + +#: src/app/main/ui/settings/options.cljs +#, fuzzy +msgid "dashboard.select-ui-language" +msgstr "Velg grensesnittsspråk" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "Velg drakt" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "Vis alle filer" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.switch-team" +msgstr "Bytt lag" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "Laginfo" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "Lagmedlemmer" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "Lagprosjekter" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.theme-change" +msgstr "Grensesnittsdrakt" + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "Din konto" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "Ditt navn" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "Din Penpot" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "Avbryt" + +#: src/app/main/ui/confirm.cljs +#, fuzzy +msgid "ds.confirm-ok" +msgstr "OK" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "Er du sikker?" + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "Ugyldig farge" + +#, fuzzy +msgid "errors.media-format-unsupported" +msgstr "Bildeformatet støttes ikke (må være SVG, JPG, eller PNG)." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.unexpected-token" +msgstr "Ukjent symbol" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-start" +msgstr "Ta del i sludringen" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "Beskrivelse" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-go-to" +msgstr "Gå til diskusjoner" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-title" +msgstr "Lagdiskusjoner" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subject" +msgstr "Emne" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur.value" +msgstr "Verdi" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.download" +msgstr "Last ned kildebilde" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.height" +msgstr "Høyde" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.width" +msgstr "Bredde" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.height" +msgstr "Høyde" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.left" +msgstr "Venstre" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.width" +msgstr "Bredde" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow" +msgstr "Skygge" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke.width" +msgstr "Bredde" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography" +msgstr "Typografi" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-family" +msgstr "Skriftfamilie" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-size" +msgstr "Skriftstørrelse" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-style" +msgstr "Skriftstil" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "Kode" + +msgid "handoff.tabs.code.selected.circle" +msgstr "Sirkel" + +msgid "handoff.tabs.code.selected.group" +msgstr "Gruppe" + +msgid "handoff.tabs.code.selected.image" +msgstr "Bilde" + +msgid "handoff.tabs.code.selected.path" +msgstr "Sti" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.text" +msgstr "Tekst" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "Info" + +msgid "labels.accept" +msgstr "Godta" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "Avbryt" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "Bekreft passord" + +msgid "labels.content" +msgstr "Innhold" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "Opprett" + +#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "Opprett nytt lag" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "Skriv inn nytt lagnavn" + +msgid "labels.custom-fonts" +msgstr "Egendefinerte skrifter" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "Oversikt" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "Slett" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "Slett kommentar" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "Slett tråd" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "Slett %s filer" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "Kladder" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "Rediger" + +msgid "labels.font-family" +msgstr "Skriftfamilie" + +msgid "labels.font-providers" +msgstr "Skrifttilbydere" + +msgid "labels.font-variant" +msgstr "Stil" + +msgid "labels.fonts" +msgstr "Skrifter" + +msgid "labels.icons" +msgstr "Ikoner" + +msgid "labels.images" +msgstr "Bilder" + +msgid "labels.installed-fonts" +msgstr "Installerte skrifter" + +#: src/app/main/ui/settings/options.cljs +msgid "labels.language" +msgstr "Språk" + +#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs +#, fuzzy +msgid "labels.logout" +msgstr "Logg ut" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "Medlemmer" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.name" +msgstr "Navn" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "Nytt passord" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.auth-info" +msgstr "Du er innlogget som" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.old-password" +msgstr "Gammelt passord" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "Eier" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "Passord" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.permissions" +msgstr "Tilganger" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.profile" +msgstr "Profil" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.projects" +msgstr "Prosjekter" + +#, fuzzy +msgid "labels.recent" +msgstr "Nylige" + +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.remove" +msgstr "Fjern" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.rename" +msgstr "Gi nytt navn" + +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +msgid "labels.retry" +msgstr "Prøv igjen" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.role" +msgstr "Rolle" + +msgid "labels.save" +msgstr "Lagre" + +#, fuzzy +msgid "labels.search-font" +msgstr "Søk etter skrift" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.send" +msgstr "Send" + +#: src/app/main/ui/settings/feedback.cljs +#, fuzzy +msgid "labels.sending" +msgstr "Sender …" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.settings" +msgstr "Innstillinger" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.shared-libraries" +msgstr "Delte bibliotek" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-all-comments" +msgstr "Vis alle kommentarer" + +#: src/app/main/ui/static.cljs +msgid "labels.sign-out" +msgstr "Logg ut" + +msgid "labels.upload" +msgstr "Last opp" + +msgid "labels.upload-custom-fonts" +msgstr "Last opp egendefinerte skrifter" + +msgid "labels.uploading" +msgstr "Laster opp …" + +#: src/app/main/ui/comments.cljs +msgid "labels.write-new-comment" +msgstr "Skriv ny kommentar" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "media.loading" +msgstr "Laster inn bilde …" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#, fuzzy +msgid "modals.add-shared-confirm.accept" +msgstr "Legg til som delt bibliotek" + +#: src/app/main/ui/settings/change_email.cljs +#, fuzzy +msgid "modals.change-email.confirm-email" +msgstr "Bekreft ny e-postadresse" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.confirm" +msgstr "Ja, slett kontoen min" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.accept" +msgstr "Slett samtale" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.title" +msgstr "Slett samtale" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.accept" +msgstr "Slett fil" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.accept" +msgstr "Slett filer" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.title" +msgstr "Slett side" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.accept" +msgstr "Slett prosjekt" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.accept" +msgstr "Slett medlem" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.title" +msgstr "Slett lagmedlem" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "Forlat lag" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" +msgstr "Avbryt" + +#: src/app/main/ui/dashboard/team.cljs +#, fuzzy +msgid "notifications.invitation-email-sent" +msgstr "Invitasjon sendt" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +#, fuzzy +msgid "notifications.profile-saved" +msgstr "Profil lagret" + +#: src/app/main/ui/settings/options.cljs +#, fuzzy +msgid "title.settings.options" +msgstr "Innstillinger - Penpot" + +#: src/app/main/ui/settings/password.cljs +#, fuzzy +msgid "title.settings.password" +msgstr "Passord - Penpot" + +#: src/app/main/ui/settings/profile.cljs +#, fuzzy +msgid "title.settings.profile" +msgstr "Profil - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +#, fuzzy +msgid "title.team-settings" +msgstr "Innstillinger - %s - Penpot" + +#: src/app/main/ui/workspace.cljs +#, fuzzy +msgid "title.workspace" +msgstr "%s - Penpot" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.edit-page" +msgstr "Rediger side" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.create-link" +msgstr "Opprett lenke" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.remove-link" +msgstr "Fjern lenke" + +msgid "workspace.assets.box-filter-graphics" +msgstr "Grafikk" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.colors" +msgstr "Farger" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.components" +msgstr "Komponenter" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "Opprett en gruppe" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.delete" +msgstr "Slett" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.edit" +msgstr "Rediger" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.file-library" +msgstr "Filbibliotek" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.graphics" +msgstr "Grafikk" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "Gruppe" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "Gruppenavn" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.libraries" +msgstr "Bibliotek" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename" +msgstr "Gi nytt navn" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-id" +msgstr "Skrift" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-size" +msgstr "Størrelse" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-variant-id" +msgstr "Variant" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-grid" +msgstr "Fest til rutenett" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-grid" +msgstr "Vis rutenett" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-layers" +msgstr "Vis lag" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-rules" +msgstr "Vis regler" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.add" +msgstr "Legg til" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.colors" +msgstr "%s farger" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.file-library" +msgstr "Filbibliotek" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.recent-colors" +msgstr "Nylige farger" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.save-color" +msgstr "Lagre fargestil" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.small-thumbnails" +msgstr "Små miniatyrbilder" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.components" +msgstr "%s komponenter" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.file-library" +msgstr "Filbibliotek" + +msgid "workspace.options.blur-options.background-blur" +msgstr "Bakgrunn" + +msgid "workspace.options.blur-options.layer-blur" +msgstr "Lag" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "Komponent" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "Design" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.exporting-object" +msgstr "Eksporterer …" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "Kolonner" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "Høyde" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "Rader" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.set-default" +msgstr "Sett som forvalg" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "Størrelse" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "Type" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "Bruk forvalg" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "Bredde" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "Rader" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "Lysne" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "Skjerm" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "Alle hjørner" \ No newline at end of file diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po new file mode 100644 index 000000000..a00125a3d --- /dev/null +++ b/frontend/translations/pt_BR.po @@ -0,0 +1,928 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"Last-Translator: Eranot \n" +"Language-Team: Portuguese (Brazil) " +"\n" +"Language: pt_BR\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" +"X-Generator: Weblate 4.7-dev\n" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "Já tem uma conta?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Verifique seu e-mail e clique no link de verificação para começar a usar o " +"Penpot." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Confirmar senha" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Criar conta de demonstração" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Só quer experimentar?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Este é um serviço DEMONSTRATIVO, NÃO USE para trabalho real, os projetos " +"serão apagados periodicamente." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "E-mail" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Esqueceu a senha?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Nome completo" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.go-back-to-login" +msgstr "Voltar!" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Entrar aqui" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Entrar" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-subtitle" +msgstr "Insira seus dados abaixo" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Bom te ver de novo!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "Entrar com o Github" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "Entrar com o Gitlab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Entrar com o Google" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "Entrar com LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Entrar com OpenID (SSO)" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Digite uma nova senha" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "O token de recuperação é inválido." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-succesfully" +msgstr "Senha alterada com sucesso" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "Perfil não verificado. Por favor, verifique o perfil antes de continuar." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Link de recuperação de senha foi enviado para sua caixa de entrada." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Entrou para a equipe com sucesso" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Senha" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Pelo menos 8 caracteres" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Recuperar senha" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Enviaremos para você um e-mail com instruções" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Esqueceu a senha?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Mudar sua senha" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Ainda não tem uma conta?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Criar uma conta" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "É de graça, é código aberto" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Criar uma conta" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "A solução de código aberto para design e prototipagem." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"Ao criar uma nova conta, você concorda com nossos termos de serviço e " +"política de privacidade." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Enviamos um e-mail de verificação para" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Adicionar como Biblioteca Compartilhada" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Alterar e-mail" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(copiar)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "+ Criar nova equipe" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Sua Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Deletar equipe" + +msgid "dashboard.draft-title" +msgstr "Rascunho" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Duplicar" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Duplicar %s arquivos" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.empty-files" +msgstr "Você ainda não tem arquivos aqui" + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Qualquer fonte da web que você carregar aqui será adicionada à lista de " +"família de fontes disponível nas propriedades de texto dos arquivos desta " +"equipe. As fontes com o mesmo nome de família de fontes serão agrupadas " +"como uma **única família de fontes**. Você pode fazer upload de fontes com " +"os seguintes formatos: **TTF, OTF e WOFF** (apenas uma será necessária)." + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Você deve carregar apenas fontes que possui ou tem licença para usar na " +"Penpot. Descubra mais na seção de Direitos de conteúdo nos [Termos de " +"Serviço da Penpot](https://penpot.app/terms.html). Você pode também querer " +"ler sobre [licenciamento de fontes](2)." + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "Convidar para a equipe" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "Sair da equipe" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "Bibliotecas Compartilhadas" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "carregando seus arquivos…" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "Mover para" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "Mover %s arquivos para" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "Mover para outra equipe" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ Novo arquivo" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Novo arquivo" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ Novo projeto" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Novo projeto" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "Nenhuma correspondência encontrada para “%s“" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "Projetos fixados aparecerão aqui" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "Seu endereço de e-mail foi atualizado com sucesso" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "Seu endereço de e-mail foi verificado com sucesso" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "Senha salva com sucesso!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s membros" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "Abrir arquivo em uma nova guia" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "Alterar senha" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "Fixar/Desafixar" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "Projetos" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.promote-to-owner" +msgstr "Promover a proprietário" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "Quer remover sua conta?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "Remover como Biblioteca Compartilhada" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "Pesquisar…" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.searching-for" +msgstr "Pesquisando por “%s“…" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-language" +msgstr "Selecionar idioma da UI" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "Selecionar tema" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "Mostrar todos os arquivos" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "Seu arquivo foi excluído com sucesso" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "Seu projeto foi excluído com sucesso" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "Seu arquivo foi duplicado com sucesso" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "Seu projeto foi duplicado com sucesso" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "Seu arquivo foi movido com sucesso" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "Seus arquivos foram movidos com sucesso" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "Seu projeto foi movido com sucesso" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.switch-team" +msgstr "Trocar de equipe" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "Informação da equipe" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "Membros da equipe" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "Projetos da equipe" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.theme-change" +msgstr "Tema da UI" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.title-search" +msgstr "Resultados da pesquisa" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.type-something" +msgstr "Digite para pesquisar resultados" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "Atualizar configurações" + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "Sua conta" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "E-mail" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "Seu nome" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "Sua Penpot" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "Cancelar" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "Ok" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "Tem certeza?" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "ds.updated-at" +msgstr "Atualizado: %s" + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "Seu navegador não pode fazer esta operação" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +msgid "errors.email-already-exists" +msgstr "E-mail já utilizado" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.email-already-validated" +msgstr "E-mail já validado." + +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-invalid-confirmation" +msgstr "E-mail de confirmação deve ser o mesmo" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.generic" +msgstr "Algo errado aconteceu." + +#: src/app/main/ui/auth/login.cljs +msgid "errors.google-auth-not-enabled" +msgstr "Autenticação com google desativada no backend" + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "Cor inválida" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.ldap-disabled" +msgstr "Autenticação por LDAP está desativada." + +msgid "errors.media-format-unsupported" +msgstr "O formato da imagem não é compatível (deve ser svg, jpg ou png)." + +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.media-too-large" +msgstr "A imagem é muito grande para ser inserida (deve ter menos de 5mb)." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-mismatch" +msgstr "Parece que o conteúdo da imagem não corresponde à extensão do arquivo." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-not-allowed" +msgstr "Parece que esta não é uma imagem válida." + +msgid "errors.network" +msgstr "Não foi possível conectar ao servidor backend." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-invalid-confirmation" +msgstr "A senha de confirmação deve ser a mesma" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-too-short" +msgstr "A senha deve ter pelo menos 8 caracteres" + +msgid "errors.terms-privacy-agreement-invalid" +msgstr "Você deve aceitar nossos termos de serviço e política de privacidade." + +#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "errors.unexpected-error" +msgstr "Um erro inesperado ocorreu." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.unexpected-token" +msgstr "Token desconhecido" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.wrong-credentials" +msgstr "O nome de usuário ou a senha parecem estar errados." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.wrong-old-password" +msgstr "A senha antiga está incorreta" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-start" +msgstr "Junte-se ao chat" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-subtitle" +msgstr "Com vontade de falar? Converse conosco no Gitter" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "Descrição" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-go-to" +msgstr "Ir para discussões" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.line-height" +msgstr "Altura da linha" + +msgid "handoff.attributes.typography.text-decoration.none" +msgstr "Nenhum" + +msgid "handoff.attributes.typography.text-decoration.strikethrough" +msgstr "Riscado" + +msgid "handoff.attributes.typography.text-decoration.underline" +msgstr "Sublinhado" + +msgid "handoff.attributes.typography.text-transform.lowercase" +msgstr "Minúsculo" + +msgid "handoff.attributes.typography.text-transform.none" +msgstr "Nenhum" + +msgid "handoff.attributes.typography.text-transform.uppercase" +msgstr "Maiúsculo" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "Código" + +msgid "handoff.tabs.code.selected.circle" +msgstr "Círculo" + +msgid "handoff.tabs.code.selected.curve" +msgstr "Curva" + +msgid "handoff.tabs.code.selected.frame" +msgstr "Prancheta" + +msgid "handoff.tabs.code.selected.group" +msgstr "Grupo" + +msgid "handoff.tabs.code.selected.image" +msgstr "Imagem" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code.selected.multiple" +msgstr "%s selecionados" + +msgid "handoff.tabs.code.selected.rect" +msgstr "Retângulo" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.text" +msgstr "Texto" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "Informação" + +msgid "history.alert-message" +msgstr "Você está vendo a versão %s" + +msgid "labels.accept" +msgstr "Aceitar" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "Administrador" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.desc-message" +msgstr "" +"Parece que você precisa esperar um pouco e tentar novamente; estamos " +"realizando pequenas manutenções em nossos servidores." + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.main-message" +msgstr "Bad Gateway" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "Cancelar" + +msgid "labels.centered" +msgstr "Centro" + +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.comments" +msgstr "Comentários" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "Confirmar senha" + +msgid "labels.content" +msgstr "Conteúdo" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "Criar" + +#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "Criar nova equipe" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "Insira o nome da nova equipe" + +msgid "labels.custom-fonts" +msgstr "Fontes personalizadas" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "Painel" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "Excluir" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "Excluir comentário" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "Excluir tópico" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "Excluir %s arquivos" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "Rascunhos" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "Editar" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.editor" +msgstr "Editor" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.email" +msgstr "E-mail" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-disabled" +msgstr "Feedback desativado" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-sent" +msgstr "Feedback enviado" + +msgid "labels.font-family" +msgstr "Família da fonte" + +msgid "labels.font-providers" +msgstr "Provedores de fonte" + +msgid "labels.font-variant" +msgstr "Estilo" + +msgid "labels.fonts" +msgstr "Fontes" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.give-feedback" +msgstr "Enviar feedback" + +msgid "labels.icons" +msgstr "Ícones" + +msgid "labels.images" +msgstr "Imagens" + +msgid "labels.installed-fonts" +msgstr "Fontes instaladas" + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.desc-message" +msgstr "" +"Algo errado aconteceu. Repita a operação e se o problema persistir, entre " +"em contato com o suporte." + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.main-message" +msgstr "Erro interno" + +#: src/app/main/ui/settings/options.cljs +msgid "labels.language" +msgstr "Linguagem" + +#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.logout" +msgstr "Sair" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "Membros" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.name" +msgstr "Nome" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "Nova senha" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +msgid "labels.no-comments-available" +msgstr "Você não tem notificações de comentários pendentes" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.auth-info" +msgstr "Você está conectado como" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.desc-message" +msgstr "Esta página não existe ou você não tem permissão para acessá-la." + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.main-message" +msgstr "Oops!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-files" +msgid_plural "labels.num-of-files" +msgstr[0] "1 arquivo" +msgstr[1] "%s arquivos" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "1 projeto" +msgstr[1] "% projetos" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.old-password" +msgstr "Senha antiga" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.only-yours" +msgstr "Apenas seu" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "Proprietário" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "Senha" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.permissions" +msgstr "Permissões" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.profile" +msgstr "Perfil" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.projects" +msgstr "Projetos" + +msgid "labels.recent" +msgstr "Recente" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.release-notes" +msgstr "Notas de lançamento" + +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.remove" +msgstr "Excluir" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.rename" +msgstr "Renomear" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.rename-team" +msgstr "Renomear equipe" + +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +msgid "labels.retry" +msgstr "Tentar novamente" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.role" +msgstr "Cargo" + +msgid "labels.save" +msgstr "Salvar" + +msgid "labels.search-font" +msgstr "Buscar fonte" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.send" +msgstr "Enviar" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.sending" +msgstr "Enviando..." + +msgid "labels.upload" +msgstr "Carregar" + +msgid "labels.upload-custom-fonts" +msgstr "Carregar fontes personalizadas" + +msgid "labels.uploading" +msgstr "Carregando..." + +msgid "modals.delete-font.message" +msgstr "" +"Tem certeza que deseja excluir essa fonte? Ela não será carregada se for " +"utilizada em um arquivo." + +msgid "modals.delete-font.title" +msgstr "Excluindo fonte" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member-confirm.accept" +msgstr "Enviar convite" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member.title" +msgstr "Convidar para se juntar à equipe" + +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"Você não pode deixar a equipe se não houver outro membro para promover a " +"proprietário. Você pode excluir a equipe." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint2" +msgstr "Selecione outro membro para promover antes de sair" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "Promover e sair" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.select-memeber-to-promote" +msgstr "Selecione um membro para promover" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.title" +msgstr "Selecione um membro para promover" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "Sair da equipe" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.message" +msgstr "Tem certeza de que deseja sair deste equipe?" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.title" +msgstr "Saindo da equipe" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.accept" +msgstr "Promover" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.message" +msgstr "Tem certeza de que deseja promover este usuário a proprietário?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.title" +msgstr "Promover a proprietário" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.accept" +msgstr "Remover como Biblioteca Compartilhada" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.message" +msgstr "Remover “%s” como Biblioteca Compartilhada" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.accept" +msgstr "Atualizar componente" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" +msgstr "Cancelar" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Provedores de fonte - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Fontes - %s - Penpot" \ No newline at end of file diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po new file mode 100644 index 000000000..6823ed273 --- /dev/null +++ b/frontend/translations/ro.po @@ -0,0 +1,2610 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"Last-Translator: George Lemon \n" +"Language-Team: Romanian " +"\n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Weblate 4.7-dev\n" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "Ai deja un cont?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Verificați adresa de e-mail, faceți click pe link-ul de verificare și " +"începeți să utilizați Penpot." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Confirmați parola" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Creează un cont demo" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Vrei doar să încerci?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Acesta este un DEMO, NU UTILIZAȚI pentru lucrări reale, întrucât proiectele " +"vor fi șterse periodic." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "Adresă E-mail" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Ai uitat parola?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Numele complet" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.go-back-to-login" +msgstr "Întoarce-te!" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Conectează-te" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Intră în cont" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-subtitle" +msgstr "Introduceți detaliile dvs. mai jos" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Mă bucur să te văd din nou!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "Conectează-te cu Github" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "Conectează-te cu Gitlab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Conectează-te cu Google" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "Conectează-te cu LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Conectează-te cu OpenID (SSO)" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Introduceți o parolă nouă" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "Codul de recuperare nu este valid." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-succesfully" +msgstr "Parola a fost schimbată cu success" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "" +"Profilul nu este verificat, vă rugăm să verificați profilul înainte de a " +"continua." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Un link de recuperare a parolei s-a trimis pe e-mail." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Te-ai alăturat echipei cu success" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Parola" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Cel puțin 8 caractere" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Recuperare Parolă" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Îți vom trimite un email cu instrucțiunile" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Ai uitat parola?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Schimbă parola" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Nu aveți încă un cont?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Creează un cont" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Este gratuit, este Open Source" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Creează un cont" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "Soluția open-source pentru proiectare design și prototipare." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"Atunci când creați un cont nou, sunteți de acord cu termenii noștri de " +"servicii și politica de confidențialitate." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Am trimis un email de verificare la" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Adăugați ca bibliotecă partajată" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Schimbă adresa de e-mail" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(copiază)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "+ Creează o nouă echipă" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Contul Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Șterge echipa" + +msgid "dashboard.draft-title" +msgstr "Draft" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Duplicat" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Duplicați %s fișiere" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.empty-files" +msgstr "Încă nu aveți fișiere aici" + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Fonturile încărcate vor fi adăugate la familia de fonturi disponibilă " +"acestei echipe. Font-urile cu același nume vor fi grupate ca **o singură " +"familie de font-uri**. Tipurile de fişiere acceptate: **TTF, OTF și WOFF** " +"(se poate urca doar un singur tip)." + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Ar trebui să urcați doar fonturi la care aveți drept de folosință sau " +"fonturi personale. Află mai multe despre Dreptul de conținut la secțiunea " +"[Termenii și Condițiile Penpot](https://penpot.app/terms.html). De " +"asemenea, vă recomandăm să citiți și despre [licențierea fonturilor](2)." + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "Invită o echipă" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "Părăsește echipa" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "Librării Partajate" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "încărcarea fișierelor …" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "Mută la" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "Mutați %s fișiere la" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "Mutați la altă echipă" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ Fișier nou" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Fișer nou" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ Proiect nou" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Proiect nou" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "Nu există rezultate pentru “%s“" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "Proiectele fixate vor apărea aici" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "Adresa ta de email a fost actualizată cu success" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "Adresa ta de email este confirmată" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "Parolă actualizată cu success!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s membrii" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "Deschide fișier într-o pagină nouă" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "Schimbă parola" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "Pin/Unpin" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "Proiecte" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.promote-to-owner" +msgstr "Promovează la administrator" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "Doriți să vă ștergeți contul?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "Elimină ca şi Colecţie Distribuită" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "Căutare…" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.searching-for" +msgstr "Căutare pentru “%s“…" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-language" +msgstr "Selectați limbajul interfeței" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "Selectați o temă" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "Afișați toate fișierele" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "Fișierele s-au șters cu succes" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "Proiectul s-a șters cu succes" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "Fișierele s-au duplicat cu succes" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "Proiectul s-a duplicat cu succes" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "Fișierul a fost mutat cu succes" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "Fișerele au fost mutate cu succes" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "Proiectul a fost mutat cu succes" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.switch-team" +msgstr "Schimbă echipa" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "Informațiile echipei" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "Membrii echipei" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "Proiectele echipei" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.theme-change" +msgstr "Interfață temă" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.title-search" +msgstr "Rezultatele căutării" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.type-something" +msgstr "Scrie pentru a începe căutarea" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "Actualizare setări" + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "Contul tău" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "Email" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "Numele tău" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "Contul Penpot" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "Anulează" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "Ok" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "Ești sigur?" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "ds.updated-at" +msgstr "Actualizat: %s" + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "Bowser-ul tău nu permite clipboard" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +msgid "errors.email-already-exists" +msgstr "Email deja trimis" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.email-already-validated" +msgstr "Adresa de email este deja validată." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.email-has-permanent-bounces" +msgstr "Adresa de email «%s» are multe rapoarte permanente de respingere." + +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-invalid-confirmation" +msgstr "E-mailul de confirmare trebuie să se potrivească" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.generic" +msgstr "S-a întâmplat ceva în neregulă." + +#: src/app/main/ui/auth/login.cljs +msgid "errors.google-auth-not-enabled" +msgstr "Autentificarea cu Google nu este permisă" + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "Culoare invalidă" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.ldap-disabled" +msgstr "Autentificarea cu LDAP este dezactivată." + +msgid "errors.media-format-unsupported" +msgstr "Formatul imaginii nu este acceptat (poate fi svg, jpg sau png)." + +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.media-too-large" +msgstr "Imaginea este prea mare pentru a fi inserată (trebuie să fie sub 5mb)." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-mismatch" +msgstr "Se pare că conținutul imaginii nu se potrivește cu extensia de fișier." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-not-allowed" +msgstr "Se pare că aceasta nu este o imagine validă." + +#: src/app/main/ui/dashboard/team.cljs +msgid "errors.member-is-muted" +msgstr "" +"Profilul pe care încercați să îl invitați este dezactivat (din cauza spam " +"sau inactivitate)." + +msgid "errors.network" +msgstr "Nu s-a reușit conectarea la server." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-invalid-confirmation" +msgstr "Parola de confirmare trebuie să se potrivească" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-too-short" +msgstr "Parola trebuie să conțină cel puțin 8 caractere" + +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.profile-is-muted" +msgstr "" +"Profilul tău conține adrese de email dezactivate (rapoarte spam sau " +"inactive)." + +#: src/app/main/ui/auth/register.cljs +msgid "errors.registration-disabled" +msgstr "Înregistrarea este dezactivată în prezent." + +msgid "errors.terms-privacy-agreement-invalid" +msgstr "Trebuie să acceptați termenii serviciului și politica de confidențialitate." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.token-expired" +msgstr "Codul este expirat" + +#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "errors.unexpected-error" +msgstr "A apărut o eroare neașteptată." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.unexpected-token" +msgstr "Cod necunoscut" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.wrong-credentials" +msgstr "Numele de utilizator sau parola par a fi greșite." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.wrong-old-password" +msgstr "Parola veche este incorectă" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-start" +msgstr "Alătură-te chatului" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-subtitle" +msgstr "Te simți sociabil? Hai să vorbim pe Gitter" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "Descriere" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-go-to" +msgstr "Du-te la discuții" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle1" +msgstr "Alătură-te forumului de comunicare al echipei Penpot." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle2" +msgstr "" +"Poți pune întrebări, iei parte la discuții deschise și poți contribui la " +"dezvoltarea proiectului." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-title" +msgstr "Discuțiile echipei" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subject" +msgstr "Subiect" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subtitle" +msgstr "" +"Descrie motivul pentru care ne scrii, specificând eventuale probleme, idei " +"sau nelămuriri. Un membru al echipei noastre îți va răspunde în scurt timp." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.title" +msgstr "Adresă de Email" + +#: src/app/main/ui/settings/password.cljs +msgid "generic.error" +msgstr "Am întâmpinat o eroare" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur" +msgstr "Blur" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur.value" +msgstr "Valoare" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hex" +msgstr "HEX" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/handoff/attributes/fill.cljs +msgid "handoff.attributes.fill" +msgstr "Fill" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.download" +msgstr "Descarcă imaginea sursă" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.height" +msgstr "Înălțime" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.width" +msgstr "Lățime" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout" +msgstr "Layout" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.height" +msgstr "Înălțime" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.left" +msgstr "Stânga" + +#: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.radius" +msgstr "Rază" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.rotation" +msgstr "Rotație" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.top" +msgstr "Top" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.width" +msgstr "Lățime" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow" +msgstr "Umbră" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.blur" +msgstr "B" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-x" +msgstr "X" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-y" +msgstr "Y" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.spread" +msgstr "S" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke" +msgstr "Linie" + +#, permanent +msgid "handoff.attributes.stroke.alignment.center" +msgstr "Centru" + +#, permanent +msgid "handoff.attributes.stroke.alignment.inner" +msgstr "Interior" + +#, permanent +msgid "handoff.attributes.stroke.alignment.outer" +msgstr "Exterior" + +msgid "handoff.attributes.stroke.style.dotted" +msgstr "Punctat" + +msgid "handoff.attributes.stroke.style.mixed" +msgstr "Mixat" + +msgid "handoff.attributes.stroke.style.none" +msgstr "Niciunul" + +msgid "handoff.attributes.stroke.style.solid" +msgstr "Solid" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke.width" +msgstr "Lățime" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography" +msgstr "Tipografie" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-family" +msgstr "Familie de Fonturi" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-size" +msgstr "Dimensiune Font" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-style" +msgstr "Stil Font" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.letter-spacing" +msgstr "Spațiere" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.line-height" +msgstr "Înălțimea rândului" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-decoration" +msgstr "Decorare Text" + +msgid "handoff.attributes.typography.text-decoration.none" +msgstr "Niciunul" + +msgid "handoff.attributes.typography.text-decoration.strikethrough" +msgstr "Strikethrough" + +msgid "handoff.attributes.typography.text-decoration.underline" +msgstr "Subliniat" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-transform" +msgstr "Transformare Text" + +msgid "handoff.attributes.typography.text-transform.lowercase" +msgstr "Minuscule" + +msgid "handoff.attributes.typography.text-transform.none" +msgstr "Niciunul" + +msgid "handoff.attributes.typography.text-transform.titlecase" +msgstr "Title Case" + +msgid "handoff.attributes.typography.text-transform.uppercase" +msgstr "Majuscule" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "Cod" + +msgid "handoff.tabs.code.selected.circle" +msgstr "Cerc" + +msgid "handoff.tabs.code.selected.curve" +msgstr "Curbat" + +msgid "handoff.tabs.code.selected.frame" +msgstr "Planșă de lucru" + +msgid "handoff.tabs.code.selected.group" +msgstr "Grup" + +msgid "handoff.tabs.code.selected.image" +msgstr "Imagine" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code.selected.multiple" +msgstr "%s Selectate" + +msgid "handoff.tabs.code.selected.path" +msgstr "Traiectorie" + +msgid "handoff.tabs.code.selected.rect" +msgstr "Dreptunghi" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.text" +msgstr "Text" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "Info" + +msgid "history.alert-message" +msgstr "Utilizezi versiunea %s" + +msgid "labels.accept" +msgstr "Accept" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "Administrator" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.all" +msgstr "Toate" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.desc-message" +msgstr "Momentan serverele noastre sunt în mentenanță. Revino în scurt timp." + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.main-message" +msgstr "Eroare de Server" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "Anulează" + +msgid "labels.centered" +msgstr "Centru" + +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.comments" +msgstr "Comentarii" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "Confirmă parola" + +msgid "labels.content" +msgstr "Conținut" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "Creează" + +#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "Creează o echipă" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "Introduceți noul nume al echipei" + +msgid "labels.custom-fonts" +msgstr "Fonturi personalizate" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "Administrare" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "Șterge" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "Șterge comentariu" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "Șterge discuție" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "Șterge %s fișiere" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "Drafturi" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "Editează" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.editor" +msgstr "Editor" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.email" +msgstr "Adresă de Email" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-disabled" +msgstr "Feedback dezactivat" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-sent" +msgstr "Feedback trimis" + +msgid "labels.font-family" +msgstr "Familie de Fonturi" + +msgid "labels.font-providers" +msgstr "Provideri de Fonturi" + +msgid "labels.font-variant" +msgstr "Stil" + +msgid "labels.fonts" +msgstr "Fonturi" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.give-feedback" +msgstr "Lasă un feedback" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.hide-resolved-comments" +msgstr "Ascunde comentariile rezolvate" + +msgid "labels.icons" +msgstr "Iconițe" + +msgid "labels.images" +msgstr "Imagini" + +msgid "labels.installed-fonts" +msgstr "Fonturi instalate" + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.desc-message" +msgstr "" +"Am întâmpinat o eroare. Te rugăm, mai încearcă o dată. Dacă problema " +"persistă poți contacta echipa de suport." + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.main-message" +msgstr "Eroare internă" + +#: src/app/main/ui/settings/options.cljs +msgid "labels.language" +msgstr "Limbă" + +#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.logout" +msgstr "Deconectare" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "Membri" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.name" +msgstr "Nume" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "Parolă nouă" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +msgid "labels.no-comments-available" +msgstr "Nu există notificări de comentarii în aștepare" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.auth-info" +msgstr "Ești conectat ca fiind" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.desc-message" +msgstr "Această pagină nu există sau nu ai permisiunea să o accesezi." + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.main-message" +msgstr "Opa!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-files" +msgid_plural "labels.num-of-files" +msgstr[0] "1 fişier" +msgstr[1] "%s fişiere" +msgstr[2] "%s fişiere" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "1 proiecte" +msgstr[1] "%s proiecte" +msgstr[2] "%s proiecte" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.old-password" +msgstr "Parola veche" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.only-yours" +msgstr "Personale" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "Autor" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "Parola" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.permissions" +msgstr "Permisiuni" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.profile" +msgstr "Profil" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.projects" +msgstr "Proiecte" + +msgid "labels.recent" +msgstr "Recente" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.release-notes" +msgstr "Mențiuni" + +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.remove" +msgstr "Elimină" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.rename" +msgstr "Redenumire" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.rename-team" +msgstr "Modifică numele echipei" + +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +msgid "labels.retry" +msgstr "Încearcă din nou" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.role" +msgstr "Rol" + +msgid "labels.save" +msgstr "Salvează" + +msgid "labels.search-font" +msgstr "Caută font" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.send" +msgstr "Trimitere" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.sending" +msgstr "Se trimite..." + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.desc-message" +msgstr "Momentan suntem în mentenanță." + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.main-message" +msgstr "Serviciul nu este disponibil" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.settings" +msgstr "Setări" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.shared-libraries" +msgstr "Colecții distribuite" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-all-comments" +msgstr "Afișează toate comentariile" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-your-comments" +msgstr "Afișează doar comentariile mele" + +#: src/app/main/ui/static.cljs +msgid "labels.sign-out" +msgstr "Deconectare" + +#: src/app/main/ui/settings/profile.cljs +msgid "labels.update" +msgstr "Actualizare" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.update-team" +msgstr "Actualizare echipă" + +msgid "labels.upload" +msgstr "Încărcare" + +msgid "labels.upload-custom-fonts" +msgstr "Încarcă fonturi personalizate" + +msgid "labels.uploading" +msgstr "Se încarcă..." + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.viewer" +msgstr "Vizitator" + +#: src/app/main/ui/comments.cljs +msgid "labels.write-new-comment" +msgstr "Scrie un comentariu" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "media.loading" +msgstr "Încarcă imaginea…" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.accept" +msgstr "Adaugă la Colecții distribuite" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.hint" +msgstr "" +"O dată adăugat la Colecții distribuite, toate fișierele acestei colecții " +"vor deveni disponibile altora." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.message" +msgstr "Adaugă “%s” la Colecții Distribuite" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.confirm-email" +msgstr "Verifică-ți adresa de e-mail" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.info" +msgstr "Îți vom trimite un email pe adresa “%s” pentru identificare." + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.new-email" +msgstr "Mail nou" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.submit" +msgstr "Schimbă adresa de e-mail" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.title" +msgstr "Schimbă-ți adresa de E-mail" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.cancel" +msgstr "Anulează ștergerea contului" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.confirm" +msgstr "Confirm ștergerea contului" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.info" +msgstr "Prin ștergerea contului, se vor șterge toate proiectele și arhivele tale." + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.title" +msgstr "Ești sigur că dorești ștergerea contului?" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.accept" +msgstr "Șterge conversație" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.message" +msgstr "" +"Ești sigur că dorești să ștergi această conversație? Toate discuțiile din " +"cadrul subiect vor fi șterse." + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.title" +msgstr "Șterge conversație" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.accept" +msgstr "Șterge fișier" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.message" +msgstr "Ești sigur că dorești să ștergi acest fișier?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.title" +msgstr "Ștergere fișier" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.accept" +msgstr "Șterge fișiere" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.message" +msgstr "Ești sigur că dorești să ștergi aceste %s fișiere?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.title" +msgstr "Ștergere %s fișeiere în curs" + +msgid "modals.delete-font.message" +msgstr "" +"Ești sigur că dorești să ștergi acest font? O dată șters acesta nu se va " +"mai încărca în proiectele tale." + +msgid "modals.delete-font.title" +msgstr "Ștergere font" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.body" +msgstr "Ești sigur că dorești să ștergi această pagină?" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.title" +msgstr "Șterge pagină" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.accept" +msgstr "Șterge proiect" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.message" +msgstr "Ești sigur că dorești să ștergi acest proiect?" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.title" +msgstr "Șterge proiect" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.accept" +msgstr "Șterge echipă" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.message" +msgstr "" +"Ești sigur că dorești să ștergi această echipă? Toate proiectele și " +"fișierele asociate acesteia vor fi permanent șterse." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.title" +msgstr "Ștergere echipă în curs" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.accept" +msgstr "Elimină membru" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.message" +msgstr "Ești sigur că dorești să elimini acest membru din echipă?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.title" +msgstr "Elimină un membru al echipei" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member-confirm.accept" +msgstr "Trimite invitație" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member.title" +msgstr "Invită o persoană în echipă" + +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"Nu puteţi părăsi echipa dacă nu există un alt membru care să devină " +"administrator. Aţi putea şterge echipa." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint1" +msgstr "Ești administratorul echipei %s." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint2" +msgstr "Selectează un membru pentru a-l promova, înainte de a părăsi echipa" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "Promovează şi părăseşte echipa" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.select-memeber-to-promote" +msgstr "Selectează un membru pentru promovare" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.title" +msgstr "Selectează un membru pentru promovare" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "Părăsește echipa" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.message" +msgstr "Ești sigur că dorești să părăsești această echipă?" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.title" +msgstr "Părăsire echipă în curs" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.accept" +msgstr "Promovează" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.message" +msgstr "Ești sigur că dorești să promovezi acest utilizator ca deținător al echipei?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.title" +msgstr "Confirmare promovare" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.accept" +msgstr "Elimină din Colecțiile Distribuite" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.hint" +msgstr "" +"O dată șters din Colecțiile Distribuite, toate fișierele acestei colecții " +"nu vor mai fi disponibile altora." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.message" +msgstr "Șterge “%s” din Colecții Distribuite" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.accept" +msgstr "Actualizare componentă" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" +msgstr "Anulează" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.hint" +msgstr "" +"Actualizezi o componentă dintr-o colecţie distribuită. Pot fi afectate alte " +"fişiere ce o folosesc." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.message" +msgstr "Actualizaţi o componentă dintr-o colecţie distribuită" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-email-sent" +msgstr "Invitaţie trimisă cu succes" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "notifications.profile-deletion-not-allowed" +msgstr "Nu poţi şterge profilul. Reatribuie echipele înainte de a continua." + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +msgid "notifications.profile-saved" +msgstr "Profil salvat cu succes!" + +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "Un e-mail de verificare a fost trimis la %s. Verifică-ţi adresa de e-mail!" + +#: src/app/main/ui/auth/recovery.cljs +msgid "profile.recovery.go-to-login" +msgstr "Mergi la autentificare" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "settings.multiple" +msgstr "Multiple" + +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Furnizori de Fonturi - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Fonturi - %s - Penpot" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "title.dashboard.projects" +msgstr "Proiecte - %s - Penpot" + +#: src/app/main/ui/dashboard/search.cljs +msgid "title.dashboard.search" +msgstr "Caută - %s - Penpot" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "title.dashboard.shared-libraries" +msgstr "Colecţii Distribuite - %s - Penpot" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Penpot - Libertate în Design pentru Echipe" + +#: src/app/main/ui/settings/feedback.cljs +msgid "title.settings.feedback" +msgstr "Oferă feedback - Penpot" + +#: src/app/main/ui/settings/options.cljs +msgid "title.settings.options" +msgstr "Setări - Penpot" + +#: src/app/main/ui/settings/password.cljs +msgid "title.settings.password" +msgstr "Parolă - Penpot" + +#: src/app/main/ui/settings/profile.cljs +msgid "title.settings.profile" +msgstr "Profil - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-members" +msgstr "Membri - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-settings" +msgstr "Setări - %s - Penpot" + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "title.viewer" +msgstr "%s - Vizualizare - Penpot" + +#: src/app/main/ui/workspace.cljs +msgid "title.workspace" +msgstr "%s - Penpot" + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.empty-state" +msgstr "Nu există ferestre disponibile pe această pagină." + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.frame-not-found" +msgstr "Fereastra nu există." + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.dont-show-interactions" +msgstr "Nu afişa interacţiunile" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.edit-page" +msgstr "Editează pagina" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.fullscreen" +msgstr "Ecran complet" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.copy-link" +msgstr "Copiază link" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.create-link" +msgstr "Creează link" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.placeholder" +msgstr "Link-ul distribuit va apărea aici" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.remove-link" +msgstr "Elimină link" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.subtitle" +msgstr "Prin acest link se permite accesul public" + +#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.title" +msgstr "Distribuie link" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions" +msgstr "Afişează interacţiunile" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions-on-click" +msgstr "Afişează interacţiunile la click" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.sitemap" +msgstr "Harta site-ului" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hcenter" +msgstr "Aliniază orizontal" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hdistribute" +msgstr "Introdu spaţierea orizontală" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hleft" +msgstr "Aliniază la stânga" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hright" +msgstr "Aliniază la dreapta" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vbottom" +msgstr "Aliniază jos" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vcenter" +msgstr "Aliniază vertical" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vdistribute" +msgstr "Introdu spaţierea pe verticală" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vtop" +msgstr "Aliniază sus" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.assets" +msgstr "Obiecte" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.box-filter-all" +msgstr "Toate obiectele" + +msgid "workspace.assets.box-filter-graphics" +msgstr "Obiecte grafice" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.colors" +msgstr "Culori" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.components" +msgstr "Componente" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "Creează grup" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group-hint" +msgstr "Obiectele vor fi numite automat ca \"nume grup / nume obiect\"" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.delete" +msgstr "Şterge" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate" +msgstr "Duplică" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.edit" +msgstr "Editează" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.file-library" +msgstr "Colecţii" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.graphics" +msgstr "Obiecte grafice" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "Grup" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "Nume grup" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.libraries" +msgstr "Colecţii" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.not-found" +msgstr "Nu au fost găsite obiecte" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename" +msgstr "Redenumeşte" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.search" +msgstr "Caută obiecte" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "%s obiect selectat" +msgstr[1] "%s obiecte selectate" +msgstr[2] "%s obiecte selectate" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared" +msgstr "DISTRIBUITE" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.typography" +msgstr "Tipografii" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-id" +msgstr "Font" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-size" +msgstr "Dimensiune" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-variant-id" +msgstr "Variante" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.go-to-edit" +msgstr "Editează fişierul în Colecţia de stiluri" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.letter-spacing" +msgstr "Spaţiere Litere" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.line-height" +msgstr "Înălţime linie" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs +msgid "workspace.assets.typography.sample" +msgstr "Ag" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.text-transform" +msgstr "Transformare Text" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.linear" +msgstr "Gradient liniar" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.radial" +msgstr "Gradient radial" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "Dezactivează alinierea dinamică" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "Dezactivează dimensionarea textului" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-grid" +msgstr "Dezactivați snap-ul la grilă" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "Aliniere dinamică" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "Activează scalarea textului" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-grid" +msgstr "Aliniază per grilă" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-assets" +msgstr "Ascunde obiectele" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-grid" +msgstr "Ascunde grila de ghidaj" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-layers" +msgstr "Ascunde layere" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-palette" +msgstr "Ascunde paleta de culori" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-rules" +msgstr "Ascunde ghidul liniar" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.select-all" +msgstr "Selectează tot" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-assets" +msgstr "Afişează asset-uri" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-grid" +msgstr "Afişează sistemul grid" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-layers" +msgstr "Afişează layere" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-palette" +msgstr "Afişează paleta de culori" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-rules" +msgstr "Afişează Liniarul" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.save-error" +msgstr "Eroare în timpul salvării" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saved" +msgstr "Salvat" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saving" +msgstr "Salvare în curs" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.unsaved" +msgstr "Modificări nesalvate" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.viewer" +msgstr "Vizualizare (%s)" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.add" +msgstr "Adaugă" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.colors" +msgstr "%s culori" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.big-thumbnails" +msgstr "Thumbnail mare" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.file-library" +msgstr "Colecţie" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.recent-colors" +msgstr "Culori recente" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.save-color" +msgstr "Salvează stilul culorii" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.small-thumbnails" +msgstr "Thumbnail mic" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.components" +msgstr "%s componente" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.file-library" +msgstr "Colecţie" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.graphics" +msgstr "%s obiecte grafice" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.in-this-file" +msgstr "COLECŢIILE FIŞIERULUI" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.libraries" +msgstr "COLECŢII" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library" +msgstr "COLECŢIE" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "Nu există Colecţii Distribuite ce necesită update" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-matches-for" +msgstr "Nu au fost găsite asemănări pentru “%s“" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-shared-libraries-available" +msgstr "Nu există Colecţii Distribuite" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.search-shared-libraries" +msgstr "Caută în colecţiile distribuite" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.shared-libraries" +msgstr "COLECŢII DISTRIBUITE" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography" +msgstr "Tipografii" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography-tooltip" +msgstr "Decupleză tipograficele" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.typography" +msgstr "%s tipografice" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.update" +msgstr "Actualizaţi" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.updates" +msgstr "ACTUALIZĂRI" + +msgid "workspace.library.all" +msgstr "Toate colecţiile" + +msgid "workspace.library.libraries" +msgstr "Colecţii" + +msgid "workspace.library.own" +msgstr "Colecţiile mele" + +msgid "workspace.library.store" +msgstr "Stocaţi colecţiile" + +msgid "workspace.options.blur-options.background-blur" +msgstr "Fundal" + +msgid "workspace.options.blur-options.layer-blur" +msgstr "Strat" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title" +msgstr "Blur" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.group" +msgstr "Blur pe grup" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.multiple" +msgstr "Selecţie de blur" + +#: src/app/main/ui/workspace/sidebar/options/page.cljs +msgid "workspace.options.canvas-background" +msgstr "Fundal canvas" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "Componentă" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "Design" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export" +msgstr "Exportă" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export-object" +msgstr "Exportă forma obiectului" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +msgid "workspace.options.export.suffix" +msgstr "Sufix" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.exporting-object" +msgstr "Se exportă…" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill" +msgstr "Umple" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.auto" +msgstr "Auto" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.column" +msgstr "Coloane" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "Coloane" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.gutter" +msgstr "Spaţiere" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "Înălţime" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.margin" +msgstr "Margine" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "Rânduri" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.set-default" +msgstr "Setează ca predefinit" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "Mărime" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "Tip" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.bottom" +msgstr "Jos" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.center" +msgstr "Centru" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.left" +msgstr "Stânga" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.right" +msgstr "Dreapta" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.stretch" +msgstr "Întinde" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.top" +msgstr "Sus" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "Foloseşte default" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "Lăţime" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "Rânduri" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.square" +msgstr "Pătrat" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.title" +msgstr "Grilă & Layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.group-fill" +msgstr "Group fill" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.group-stroke" +msgstr "Group stroke" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "Culoare" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-burn" +msgstr "Color burn" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-dodge" +msgstr "Color dodge" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.darken" +msgstr "Întunecat" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "Difference" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.exclusion" +msgstr "Exclusion" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "Lumină Puternică" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "Hue" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "Luminat" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "Luminozitate" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "Multiplică" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "Normal" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "Overlay" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "Saturaţie" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "Screen" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "Soft light" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title" +msgstr "Layer" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.group" +msgstr "Grupează layere" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.multiple" +msgstr "Layere selectate" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.navigate-to" +msgstr "Navighează la" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.none" +msgstr "Nici unul" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "Poziţie" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.prototype" +msgstr "Prototip" + +msgid "workspace.options.radius" +msgstr "Rază" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "Toate colţurile" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.single-corners" +msgstr "Colţuri unice" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.rotation" +msgstr "Rotaţie" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-a-shape" +msgstr "Selectează o formă, o planşă sau grupează pentru a conecta o altă planşă." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-artboard" +msgstr "Selectează planşa" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.selection-fill" +msgstr "Selection fill" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.selection-stroke" +msgstr "Selection stroke" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.blur" +msgstr "Blur" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "Drop shadow" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "Inner shadow" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.spread" +msgstr "Accentuare" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title" +msgstr "Umbră" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.group" +msgstr "Group shadow" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.multiple" +msgstr "Selection shadows" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.size" +msgstr "Mărime" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +msgid "workspace.options.size-presets" +msgstr "Dimensiuni presetate" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke" +msgstr "Stroke" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.center" +msgstr "Centru" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dashed" +msgstr "Dashed" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dotted" +msgstr "Dotted" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.inner" +msgstr "Interior" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.mixed" +msgstr "Mixed" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.outer" +msgstr "Exterior" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.solid" +msgstr "Solid" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-bottom" +msgstr "Aliniază jos" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-center" +msgstr "Aliniază centru" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-justify" +msgstr "Justify" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-left" +msgstr "Aliniază la stânga" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-middle" +msgstr "Aliniază la mijloc" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-right" +msgstr "Aliniază la dreapta" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-top" +msgstr "Aliniază sus" + +msgid "workspace.options.text-options.decoration" +msgstr "Decorare text" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-ltr" +msgstr "LTR" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-rtl" +msgstr "RTL" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.google" +msgstr "Google" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-height" +msgstr "Înălţime auto" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-width" +msgstr "Lăţime auto" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-fixed" +msgstr "Fix" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.letter-spacing" +msgstr "Spaţiere Litere" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.line-height" +msgstr "Înălţime linii" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.lowercase" +msgstr "Minuscule" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.none" +msgstr "Nici unul" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.preset" +msgstr "Presetat" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.strikethrough" +msgstr "Strikethrough" + +msgid "workspace.options.text-options.text-case" +msgstr "Case" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title" +msgstr "Text" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-group" +msgstr "Grupează text" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-selection" +msgstr "Selecţie text" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.titlecase" +msgstr "Încadrare Titlu" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.underline" +msgstr "Subliniază" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.uppercase" +msgstr "Majuscule" + +msgid "workspace.options.text-options.vertical-align" +msgstr "Aliniere verticală" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.use-play-button" +msgstr "Foloseşte butonul play din header pentru a porni vizualizarea prototip." + +msgid "workspace.path.actions.add-node" +msgstr "Adaugă nod (%s)" + +msgid "workspace.path.actions.delete-node" +msgstr "Şterge nod (%s)" + +msgid "workspace.path.actions.draw-nodes" +msgstr "Desenează noduri (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "Adaugă noduri (%s)" + +msgid "workspace.path.actions.make-corner" +msgstr "În colţ (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "În curbă (%s)" + +msgid "workspace.path.actions.merge-nodes" +msgstr "Uneşte noduri (%s)" + +msgid "workspace.path.actions.move-nodes" +msgstr "Mută noduri (%s)" + +msgid "workspace.path.actions.separate-nodes" +msgstr "Separă noduri (%s)" + +msgid "workspace.path.actions.snap-nodes" +msgstr "Trage noduri (%s)" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.back" +msgstr "Trimite înapoi" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.backward" +msgstr "Trimite în urmă" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.copy" +msgstr "Copiază" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-component" +msgstr "Creează componentă" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.cut" +msgstr "Taie" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete" +msgstr "Şterge" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instance" +msgstr "Detaşează instanţă" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.duplicate" +msgstr "Duplică" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.edit" +msgstr "Editează" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-horizontal" +msgstr "Întoarce pe orizontală" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-vertical" +msgstr "Întoarce pe verticală" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.forward" +msgstr "Aduceţi înainte" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.front" +msgstr "Aduceţi în faţă" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.go-main" +msgstr "Mergi la componenta principală" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.group" +msgstr "Grupează" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.hide" +msgstr "Ascunde" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.lock" +msgstr "Blochează" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.mask" +msgstr "Maschează" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.paste" +msgstr "Lipeşte" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.reset-overrides" +msgstr "Resetează suprascrierile" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show" +msgstr "Afişează" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-main" +msgstr "Afişează componenta principală" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.ungroup" +msgstr "Degrupează" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unlock" +msgstr "Deblochează" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unmask" +msgstr "Demaschează" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-main" +msgstr "Actualizaţi principala componentă" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.history" +msgstr "Istoric (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.layers" +msgstr "Layere (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "Atribute SVG importate" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "workspace.sidebar.sitemap" +msgstr "Pagini" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.sitemap" +msgstr "Harta site-ului" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.assets" +msgstr "Obiecte (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.color-palette" +msgstr "Paletă de culori (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.comments" +msgstr "Comentarii (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.curve" +msgstr "Curbe (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.ellipse" +msgstr "Elipsă (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.frame" +msgstr "Planşă de lucru (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.image" +msgstr "Imagine (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.move" +msgstr "Poziţionează" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.path" +msgstr "Cale (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.rect" +msgstr "Dreptunghi (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text" +msgstr "Text (%s)" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.empty" +msgstr "Nu sunt modificări în istoric" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.delete" +msgstr "Şters %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.modify" +msgstr "Modificat %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.move" +msgstr "Obiecte mutate" + +msgid "workspace.undo.entry.multiple.circle" +msgstr "cercuri" + +msgid "workspace.undo.entry.multiple.color" +msgstr "Culori" + +msgid "workspace.undo.entry.multiple.component" +msgstr "componente" + +msgid "workspace.undo.entry.multiple.curve" +msgstr "curbe" + +msgid "workspace.undo.entry.multiple.frame" +msgstr "planşă de lucru" + +msgid "workspace.undo.entry.multiple.group" +msgstr "grupuri" + +msgid "workspace.undo.entry.multiple.media" +msgstr "obiecte grafice" + +msgid "workspace.undo.entry.multiple.multiple" +msgstr "obiecte" + +msgid "workspace.undo.entry.multiple.page" +msgstr "pagini" + +msgid "workspace.undo.entry.multiple.path" +msgstr "căi" + +msgid "workspace.undo.entry.multiple.rect" +msgstr "dreptunghiuri" + +msgid "workspace.undo.entry.multiple.shape" +msgstr "forme" + +msgid "workspace.undo.entry.multiple.text" +msgstr "text" + +msgid "workspace.undo.entry.multiple.typography" +msgstr "obiecte tipografice" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.new" +msgstr "Nou %s" + +msgid "workspace.undo.entry.single.circle" +msgstr "cerc" + +msgid "workspace.undo.entry.single.color" +msgstr "culoare" + +msgid "workspace.undo.entry.single.component" +msgstr "componentă" + +msgid "workspace.undo.entry.single.curve" +msgstr "curbă" + +msgid "workspace.undo.entry.single.frame" +msgstr "planşă de lucru" + +msgid "workspace.undo.entry.single.group" +msgstr "grup" + +msgid "workspace.undo.entry.single.image" +msgstr "imagine" + +msgid "workspace.undo.entry.single.media" +msgstr "obiect grafic" + +msgid "workspace.undo.entry.single.multiple" +msgstr "obiect" + +msgid "workspace.undo.entry.single.page" +msgstr "pagină" + +msgid "workspace.undo.entry.single.path" +msgstr "cale" + +msgid "workspace.undo.entry.single.rect" +msgstr "dreptunghi" + +msgid "workspace.undo.entry.single.shape" +msgstr "formă" + +msgid "workspace.undo.entry.single.text" +msgstr "text" + +msgid "workspace.undo.entry.single.typography" +msgstr "obiect tipografic" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.unknown" +msgstr "Operaţiune terminată %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.title" +msgstr "Istoric" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.dismiss" +msgstr "Renunţă" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.there-are-updates" +msgstr "Există actualizări în colecţiile distribuite" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.update" +msgstr "Actualizează" + +msgid "workspace.viewport.click-to-close-path" +msgstr "Click pentru a închide calea" \ No newline at end of file diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 3cb0f1e42..9118ae535 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1146,11 +1146,11 @@ msgstr "" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Рабочая область (A)" +msgstr "Рабочая область (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Изображение (K)" +msgstr "Изображение (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -1158,15 +1158,15 @@ msgstr "Вытеснить" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "Линия (P)" +msgstr "Линия (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "Прямоугольник (R)" +msgstr "Прямоугольник (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "Текст (T)" +msgstr "Текст (%s)" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.dismiss" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 97dfb17d1..a947e2223 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1,10 +1,15 @@ msgid "" msgstr "" +"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"Last-Translator: Gizem Akgüney \n" +"Language-Team: Turkish " +"\n" "Language: tr\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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -74,17 +79,25 @@ msgstr "Github ile giriş yap" msgid "auth.login-with-gitlab-submit" msgstr "Gitlab ile giriş yap" +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Google ile Giriş Yap" + #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-ldap-submit" msgstr "LDAP ile giriş yap" +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "OpenID (SSO) ile Giriş Yap" + #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" msgstr "Yeni bir parola gir" #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" -msgstr "Kurtarma bağlantısı geçerli değil" +msgstr "Kurtarma jetonu geçerli değil" #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.password-changed-succesfully" @@ -195,6 +208,15 @@ msgstr "%s dosyanın kopyasını oluştur" msgid "dashboard.empty-files" msgstr "Burada hiç dosyan yok" +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Sadece kendinize ait veya Penpot'ta kullanılabilecek bir lisansa sahip olan " +"fontları yükleyebilirsiniz. [Penpot's Terms of Service] içindeki İçerik " +"hakları bölümünden detaylı bilgi alabilirsiniz " +"(https://penpot.app/terms.html). Ayrıca [font licensing](2) hakkında daha " +"fazla bilgi almak isteyebilirsiniz." + #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Takıma davet et" @@ -227,10 +249,18 @@ msgstr "Başkta takıma taşı" msgid "dashboard.new-file" msgstr "Yeni Dosya" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Yeni Dosya" + #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.new-project" msgstr "Yeni Proje" +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Yeni Proje" + #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" msgstr "%s için hiç sonuç bulunamadı" @@ -249,7 +279,7 @@ msgstr "E-posta adresin başarıyla doğrulandı" #: src/app/main/ui/settings/password.cljs msgid "dashboard.notifications.password-saved" -msgstr "Parola kaydedildi" +msgstr "Parola başarıyla kaydedildi!" #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.num-of-members" @@ -289,7 +319,7 @@ msgstr "Ara…" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.searching-for" -msgstr "%s aranıyor" +msgstr "%s aranıyor…" #: src/app/main/ui/settings/options.cljs msgid "dashboard.select-ui-language" @@ -405,7 +435,7 @@ msgstr "E-posta zaten kullanımda" #: src/app/main/ui/auth/verify_token.cljs msgid "errors.email-already-validated" -msgstr "E-posta zaten doğrulandı" +msgstr "E-posta zaten doğrulandı." #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" @@ -423,6 +453,10 @@ msgstr "Bir şeyler ters gitti." msgid "errors.google-auth-not-enabled" msgstr "Google ile oturum açma devre dışı bırakıldı" +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "Geçersiz renk" + #: src/app/main/ui/auth/login.cljs msgid "errors.ldap-disabled" msgstr "LDAP ile oturum açma devre dışı bırakıldı." @@ -432,7 +466,7 @@ msgstr "Görsel formatı desteklenmiyor (svg, jpg veya png olmalı)." #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" -msgstr "Bu görsel eklemek için çok büyük (5MB altında olmalı)" +msgstr "Bu görsel eklemek için çok büyük (5MB altında olmalı)." #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-mismatch" @@ -449,7 +483,7 @@ msgstr "" "var veya spam olarak bildirilmiş." msgid "errors.network" -msgstr "Sunucuya bağlanılamıyor" +msgstr "Sunucuya bağlanılamıyor." #: src/app/main/ui/settings/password.cljs msgid "errors.password-invalid-confirmation" @@ -457,4 +491,433 @@ msgstr "Parolalar eşleşmedi" #: src/app/main/ui/settings/password.cljs msgid "errors.password-too-short" -msgstr "Parola en az 8 karakterden oluşmalı" \ No newline at end of file +msgstr "Parola en az 8 karakterden oluşmalı" + +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.profile-is-muted" +msgstr "" +"Profilinizde sessize alınmış e-postalar var (spam raporları veya yüksek " +"geri dönüşler sebebiyle)." + +#: src/app/main/ui/auth/register.cljs +msgid "errors.registration-disabled" +msgstr "Kayıt olma şu anda devre dışı." + +msgid "errors.terms-privacy-agreement-invalid" +msgstr "Hizmet şartlarımızı ve gizlilik politikamızı kabul etmelisin." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.token-expired" +msgstr "Jetonun süresi geçti" + +#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "errors.unexpected-error" +msgstr "Beklenmedik bir hata oluştu." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.unexpected-token" +msgstr "Bilinmeyen jeton" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.wrong-credentials" +msgstr "Kullanıcı adı veya parola yanlış gözüküyor." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.wrong-old-password" +msgstr "Eski parola yanlış" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-start" +msgstr "Sohbete katıl" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-subtitle" +msgstr "Sohbet etmek ister misin? Glitter'da bizimle sohbet edebilirsin" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "Açıklama" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-go-to" +msgstr "Tartışmalar bölümüne git" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-title" +msgstr "Takım tartışmaları" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subject" +msgstr "Konu" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.title" +msgstr "E-posta" + +#: src/app/main/ui/settings/password.cljs +msgid "generic.error" +msgstr "Bir hata oluştu" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur" +msgstr "Bulanıklaştır" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur.value" +msgstr "Değer" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hex" +msgstr "HEX" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/handoff/attributes/fill.cljs +msgid "handoff.attributes.fill" +msgstr "Doldur" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.download" +msgstr "Kaynak görselini indir" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.height" +msgstr "Yükseklik" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.width" +msgstr "Genişlik" + +msgid "handoff.attributes.typography.text-decoration.none" +msgstr "Hiçbiri" + +msgid "handoff.attributes.typography.text-transform.lowercase" +msgstr "Küçük Harf" + +msgid "handoff.attributes.typography.text-transform.none" +msgstr "Hiçbiri" + +msgid "handoff.attributes.typography.text-transform.uppercase" +msgstr "Büyük Harf" + +msgid "handoff.tabs.code.selected.circle" +msgstr "Daire" + +msgid "handoff.tabs.code.selected.curve" +msgstr "Eğri" + +msgid "handoff.tabs.code.selected.group" +msgstr "Grup" + +msgid "handoff.tabs.code.selected.image" +msgstr "Görsel" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code.selected.multiple" +msgstr "%s Seçildi" + +msgid "handoff.tabs.code.selected.path" +msgstr "Yol" + +msgid "handoff.tabs.code.selected.rect" +msgstr "Dikdörtgen" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.text" +msgstr "Metin" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "Bilgi" + +msgid "history.alert-message" +msgstr "%s sürümünü görüyorsun" + +msgid "labels.accept" +msgstr "Kabul et" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "Yönetici" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.all" +msgstr "Hepsi" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "İptal" + +msgid "labels.centered" +msgstr "Orta" + +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.comments" +msgstr "Yorumlar" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "Parolayı onayla" + +msgid "labels.content" +msgstr "İçerik" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "Oluştur" + +#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "Yeni takım oluştur" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "Yeni takım adı gir" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "Kontrol paneli" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "Sil" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "Yorumu sil" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "Mesaj dizisini sil" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "%s dosyayı sil" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "Taslak" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "Düzenle" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.editor" +msgstr "Editör" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.email" +msgstr "E-posta" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-disabled" +msgstr "Geri bildirim devre dışı bırakıldı" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-sent" +msgstr "Geri bildirim gönderildi" + +msgid "labels.font-family" +msgstr "Font Ailesi" + +msgid "labels.font-variant" +msgstr "Stil" + +msgid "labels.fonts" +msgstr "Fontlar" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.give-feedback" +msgstr "Geri bildirimde bulun" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.hide-resolved-comments" +msgstr "Çözülmüş yorumları gizle" + +msgid "labels.images" +msgstr "Görseller" + +msgid "labels.installed-fonts" +msgstr "Yüklenmiş fontlar" + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.desc-message" +msgstr "" +"Kötü bir şey oldu. Lütfen işlemi yeniden deneyin ve sorun devam ederse " +"destek ile iletişime geçin." + +#: src/app/main/ui/settings/options.cljs +msgid "labels.language" +msgstr "Dil" + +#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.logout" +msgstr "Çıkış Yap" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "Üyeler" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.name" +msgstr "Ad" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "Yeni parola" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +msgid "labels.no-comments-available" +msgstr "Bekleyen yorum bildirimi yok" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.auth-info" +msgstr "Şu şekilde oturum açtınız:" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.desc-message" +msgstr "Bu sayfa mevcut olmayabilir veya erişim izniniz olmayabilir." + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.main-message" +msgstr "Oops!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-files" +msgid_plural "labels.num-of-files" +msgstr[0] "1 dosya" +msgstr[1] "%s dosya" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "1 proje" +msgstr[1] "%s proje" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.old-password" +msgstr "Eski parola" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.only-yours" +msgstr "Sadece seninkiler" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "Sahip" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "Parola" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.permissions" +msgstr "İzinler" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.profile" +msgstr "Profil" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.projects" +msgstr "Projeler" + +msgid "labels.recent" +msgstr "Son" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.release-notes" +msgstr "Sürüm notları" + +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.remove" +msgstr "Kaldır" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.rename" +msgstr "Yeniden adlandır" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.rename-team" +msgstr "Takımı yeniden adlandır" + +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +msgid "labels.retry" +msgstr "Yeniden dene" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.role" +msgstr "Rol" + +msgid "labels.save" +msgstr "Kaydet" + +msgid "labels.search-font" +msgstr "Font ara" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.send" +msgstr "Gönder" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.sending" +msgstr "Gönderiliyor..." + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.desc-message" +msgstr "Sistemlerimizin programlı bakımını yapıyoruz." + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.main-message" +msgstr "Hizmet Kullanılamıyor" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.settings" +msgstr "Ayarlar" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.shared-libraries" +msgstr "Paylaşılan Kitaplıklar" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-all-comments" +msgstr "Tüm yorumları göster" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-your-comments" +msgstr "Yalnızca kendi yorumlarımı göster" + +#: src/app/main/ui/static.cljs +msgid "labels.sign-out" +msgstr "Çıkış yap" + +#: src/app/main/ui/settings/profile.cljs +msgid "labels.update" +msgstr "Güncelle" + +msgid "labels.upload" +msgstr "Yükle" + +msgid "labels.uploading" +msgstr "Yükleniyor..." + +msgid "modals.delete-font.message" +msgstr "" +"Bu fontu silmek istediğine emin misin? Bir dosyada kullanılıyorsa " +"yüklenmeyecektir." + +msgid "modals.delete-font.title" +msgstr "Fontu sil" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Fontlar - %s - Penpot" \ No newline at end of file diff --git a/frontend/translations/zh_cn.po b/frontend/translations/zh_CN.po similarity index 99% rename from frontend/translations/zh_cn.po rename to frontend/translations/zh_CN.po index 9ade5b982..481883c52 100644 --- a/frontend/translations/zh_cn.po +++ b/frontend/translations/zh_CN.po @@ -2066,15 +2066,15 @@ msgstr "曲线(%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.ellipse" -msgstr "椭圆(E)" +msgstr "椭圆(%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "画板(A)" +msgstr "画板(%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "图片(K)" +msgstr "图片(%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" @@ -2082,15 +2082,15 @@ msgstr "移动" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.path" -msgstr "路径(P)" +msgstr "路径(%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" -msgstr "矩形(R)" +msgstr "矩形(%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.text" -msgstr "文本(T)" +msgstr "文本(%s)" #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.empty" diff --git a/frontend/vendor/cuerdas/vendor/xregexp.cljs b/frontend/vendor/cuerdas/vendor/xregexp.cljs deleted file mode 100644 index 2883be3d7..000000000 --- a/frontend/vendor/cuerdas/vendor/xregexp.cljs +++ /dev/null @@ -1,4 +0,0 @@ -(ns cuerdas.vendor.xregexp - (:require ["xregexp" :as XRegExp])) - -(goog/exportSymbol "XRegExp" XRegExp) diff --git a/frontend/vendor/tubax/saxjs.cljs b/frontend/vendor/tubax/saxjs.cljs new file mode 100644 index 000000000..3dc98550b --- /dev/null +++ b/frontend/vendor/tubax/saxjs.cljs @@ -0,0 +1,4 @@ +(ns tubax.saxjs + (:require ["sax" :as sax])) + +(goog/exportSymbol "sax" sax) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 021d9aff6..caa556a85 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3,13 +3,20 @@ "@babel/runtime-corejs3@^7.12.1": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz#14c3f4c85de22ba88e8e86685d13e8861a82fe86" - integrity sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg== + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66" + integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" +"@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" + integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== + dependencies: + regenerator-runtime "^0.13.4" + "@dabh/diagnostics@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" @@ -548,15 +555,15 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.16.3: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^1.1.71" buffer-crc32@~0.2.3: version "0.2.13" @@ -662,10 +669,10 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001196: - version "1.0.30001205" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001205.tgz#d79bf6a6fb13196b4bb46e5143a22ca0242e0ef8" - integrity sha512-TL1GrS5V6LElbitPazidkBMD9sa448bQDDLrumDqaggmKFcuU2JW1wTOHJPukAcOMtEmLcmDJEzfRrf+GjM0Og== +caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219: + version "1.0.30001221" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001221.tgz#b916721ddf59066cfbe96c5c9a77cf7ae5c52e65" + integrity sha512-b9TOZfND3uGSLjMOrLh8XxSQ41x8mX+9MLJYDM4AAHLfaZHttrLNPrScWjVnBITRZbY5sPpCt7X85n7VSLZ+/g== caseless@~0.12.0: version "0.12.0" @@ -802,6 +809,11 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" +clsx@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" @@ -871,7 +883,7 @@ color@3.0.x: color-convert "^1.9.1" color-string "^1.5.2" -colorette@^1.2.1, colorette@^1.2.2: +colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== @@ -982,14 +994,14 @@ copy-props@^2.0.1: is-plain-object "^5.0.0" core-js-pure@^3.0.0: - version "3.10.2" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.10.2.tgz#065304f8547bf42008d4528dfff973c38bd6a332" - integrity sha512-uu18pVHQ21n4mzfuSlCXpucu5VKsck3j2m5fjrBOBqqdgWAxwdCgUuGWj6cDDPN1zLj/qtiqKvBMxWgDeeu49Q== + version "3.11.2" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.11.2.tgz#10e3b35788c00f431bc0d601d7551475ec3e792c" + integrity sha512-DQxdEKm+zFsnON7ZGOgUAQXBt1UJJ01tOzN/HgQ7cNf0oEHW1tcBLfCQQd1q6otdLu5gAdvKYxKHAoXGwE/kiQ== core-js@^3.6.4: - version "3.10.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.10.0.tgz#9a020547c8b6879f929306949e31496bbe2ae9b3" - integrity sha512-MQx/7TLgmmDVamSyfE+O+5BHvG1aUGj/gHhLn1wVtm2B5u1eVIPvh7vkfjwWKNCjrTJB8+He99IntSQ1qP+vYQ== + version "3.11.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.11.2.tgz#af087a43373fc6e72942917c4a4c3de43ed574d6" + integrity sha512-3tfrrO1JpJSYGKnd9LKTBPqgUES/UYiCzMKeqwR1+jF16q4kD1BY2NvqkfuzXwQ6+CIWm55V9cjD7PQd+hijdw== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -1038,9 +1050,9 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: sha.js "^2.4.8" cross-fetch@^3.0.4: - version "3.1.3" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.3.tgz#b8e7d5f19161c4a0ca916f707978848786043afb" - integrity sha512-2i6v88DTqVBNODyjD9U6Ycn/uSZNvyHe25cIbo2fFnAACAsaLTJsd23miRWiR5NuiGXR9wpJ9d40/9WAhjDIrw== + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== dependencies: node-fetch "2.6.1" @@ -1109,9 +1121,9 @@ css-tree@1.0.0-alpha.37: source-map "^0.6.1" css-tree@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5" - integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== dependencies: mdn-data "2.0.14" source-map "^0.6.1" @@ -1147,6 +1159,11 @@ cssom@^0.3.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== +csstype@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -1169,10 +1186,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.21.1: - version "2.21.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0" - integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA== +date-fns@^2.21.3: + version "2.21.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b" + integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw== dateformat@^3.0.3: version "3.0.3" @@ -1302,6 +1319,14 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dom-helpers@^5.1.3: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -1321,9 +1346,9 @@ domelementtype@1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== domutils@^1.7.0: version "1.7.0" @@ -1378,10 +1403,10 @@ editorconfig@^0.15.3: semver "^5.6.0" sigmund "^1.0.1" -electron-to-chromium@^1.3.649: - version "1.3.703" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.703.tgz#6d9b9a75c42a40775f5930329e642b22b227317f" - integrity sha512-SVBVhNB+4zPL+rvtWLw7PZQkw/Eqj1HQZs22xtcqW36+xoifzEOEEDEpkxSMfB6RFeSIOcG00w6z5mSqLr1Y6w== +electron-to-chromium@^1.3.723: + version "1.3.726" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz#6d3c577e5f5a48904ba891464740896c05e3bdb1" + integrity sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w== elliptic@^6.5.3: version "6.5.4" @@ -1696,9 +1721,9 @@ fd-slicer@~1.1.0: pend "~1.2.0" fecha@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" - integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== + version "4.2.1" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== file-uri-to-path@1.0.0: version "1.0.0" @@ -2320,9 +2345,9 @@ homedir-polyfill@^1.0.1: parse-passwd "^1.0.0" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== http-signature@~1.2.0: version "1.2.0" @@ -2501,9 +2526,9 @@ is-callable@^1.1.4, is-callable@^1.2.3: integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887" + integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw== dependencies: has "^1.0.3" @@ -3049,7 +3074,7 @@ logform@^2.2.0: ms "^2.1.1" triple-beam "^1.3.0" -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3120,6 +3145,11 @@ map-visit@^1.0.0: dependencies: 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: version "2.0.0" resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" @@ -3215,17 +3245,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.46.0: - version "1.46.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" - integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== +mime-db@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" + integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.29" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" - integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== + version "2.1.30" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" + integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== dependencies: - mime-db "1.46.0" + mime-db "1.47.0" mimic-fn@^2.0.0: version "2.1.0" @@ -3338,10 +3368,10 @@ nan@^2.12.1, nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@^3.1.22: - version "3.1.22" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" - integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== +nanoid@^3.1.23: + version "3.1.23" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" + integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== nanomatch@^1.2.9: version "1.2.13" @@ -3427,7 +3457,7 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.70: +node-releases@^1.1.71: version "1.1.71" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== @@ -3552,9 +3582,9 @@ object-copy@^0.1.0: kind-of "^3.0.3" object-inspect@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" - integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== + version "1.10.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" + integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -3644,6 +3674,14 @@ one-time@^1.0.0: dependencies: 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: version "1.0.1" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" @@ -3949,13 +3987,13 @@ postcss@^7.0.16: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.2.7: - version "8.2.10" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b" - integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw== +postcss@^8.2.15: + version "8.2.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65" + integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q== dependencies: colorette "^1.2.2" - nanoid "^3.1.22" + nanoid "^3.1.23" source-map "^0.6.1" pretty-hrtime@^1.0.0: @@ -3990,6 +4028,15 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -4106,6 +4153,28 @@ react-dom@~17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-virtualized@^9.22.3: + version "9.22.3" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421" + integrity sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw== + dependencies: + "@babel/runtime" "^7.7.2" + clsx "^1.0.4" + dom-helpers "^5.1.3" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.4" + react@~17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -4225,9 +4294,9 @@ remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== repeat-string@^1.6.1: version "1.6.1" @@ -4363,10 +4432,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rxjs@~7.0.0-beta.12: - version "7.0.0-rc.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.1.tgz#11f368e740e2b3cfe805891be127d07391673654" - integrity sha512-FVFOeT+eGdbcPe+uH+cWnEElrU4LiDMrlstNSUpI3MPErICLtVoUCbKrF+n+8DYemHDe7wPqYtuNEYTM3ur3xw== +rxjs@~7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.1.tgz#5f41c4f991cea550471fc5d215727390103702c7" + integrity sha512-wViQ4Vgps1xJwqWIBooMNN44usCSthL7wCUl4qWqrVjhGfWyVyXcxlYzfDKkJKACQvZMTOft/jJ3RkbwK1j9QQ== dependencies: tslib "~2.1.0" @@ -4403,13 +4472,13 @@ sass-graph@2.2.5: yargs "^13.3.2" sass@^1.32.8: - version "1.32.11" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.11.tgz#b236b3ea55c76602c2ef2bd0445f0db581baa218" - integrity sha512-O9tRcob/fegUVSIV1ihLLZcftIOh0AF1VpKgusUfLqnb2jQ0GLDwI5ivv1FYWivGv8eZ/AwntTyTzjcHu0c/qw== + version "1.32.12" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.12.tgz#a2a47ad0f1c168222db5206444a30c12457abb9f" + integrity sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA== dependencies: chokidar ">=3.0.0 <4.0.0" -sax@~1.2.4: +sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -4480,10 +4549,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@^2.11.20: - version "2.12.5" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4" - integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ== +shadow-cljs@2.12.6: + version "2.12.6" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.6.tgz#039539fdc35a19c2f2cd15792ae17e7928f97428" + integrity sha512-dNw989EFQki/59kD8Cd8b6HIpBTqPj9ksWIvSg6hI1bgezZHT0oHfJH5UIbXPD+dnVLvbOnDnfOMWYH6ozalcA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" @@ -4796,6 +4865,11 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.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: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" @@ -5021,6 +5095,11 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" 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: version "2.0.2" resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" @@ -5135,10 +5214,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -ua-parser-js@^0.7.18: - version "0.7.26" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.26.tgz#b3731860e241419abd5b542b1a0881070d92e0ce" - integrity sha512-VwIvGlFNmpKbjzRt51jpbbFTrKIEgGHxIwA8Y69K1Bqc6bTIV7TaGGABOkghSFQWsLmcRB4drGvpfv9z2szqoQ== +ua-parser-js@^0.7.18, ua-parser-js@^0.7.28: + version "0.7.28" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" + integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== ultron@~1.1.0: version "1.1.1" @@ -5482,23 +5561,15 @@ y18n@^3.2.1: integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yargs-parser@5.0.0-security.0: - version "5.0.0-security.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz#4ff7271d25f90ac15643b86076a2ab499ec9ee24" - integrity sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ== - dependencies: - camelcase "^3.0.0" - object.assign "^4.1.0" - yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" @@ -5515,6 +5586,14 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394" + integrity sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA== + dependencies: + camelcase "^3.0.0" + object.assign "^4.1.0" + yargs@^12.0.2: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" @@ -5550,9 +5629,9 @@ yargs@^13.3.2: yargs-parser "^13.1.2" yargs@^7.1.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6" - integrity sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g== + version "7.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db" + integrity sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA== dependencies: camelcase "^3.0.0" cliui "^3.2.0" @@ -5566,7 +5645,7 @@ yargs@^7.1.0: string-width "^1.0.2" which-module "^1.0.0" y18n "^3.2.1" - yargs-parser "5.0.0-security.0" + yargs-parser "^5.0.1" yauzl@^2.10.0: version "2.10.0" diff --git a/manage.sh b/manage.sh index 6ea162e26..80d9c6a85 100755 --- a/manage.sh +++ b/manage.sh @@ -98,6 +98,54 @@ Copyright (c) UXBOX Labs SL EOF } +function build-frontend-bundle { + echo ">> bundle frontend start"; + + local version=$(print-current-version); + local bundle_dir="./bundle-frontend"; + + build "frontend"; + + rm -rf $bundle_dir; + mv ./frontend/target/dist $bundle_dir; + echo $version > $bundle_dir/version.txt; + put-license-file $bundle_dir; + echo ">> bundle frontend end"; +} + +function build-backend-bundle { + echo ">> bundle backend start"; + + local version=$(print-current-version); + local bundle_dir="./bundle-backend"; + + build "backend"; + + rm -rf $bundle_dir; + mv ./backend/target/dist $bundle_dir; + echo $version > $bundle_dir/version.txt; + put-license-file $bundle_dir; + echo ">> bundle frontend end"; +} + +function build-exporter-bundle { + echo ">> bundle exporter start"; + local version=$(print-current-version); + local bundle_dir="./bundle-exporter"; + + build "exporter"; + + rm -rf $bundle_dir; + mv ./exporter/target $bundle_dir; + + echo $version > $bundle_dir/version.txt + put-license-file $bundle_dir; + + echo ">> bundle exporter end"; +} + +# DEPRECATED: temporary mantained for backward compatibilty. + function build-app-bundle { echo ">> bundle app start"; @@ -117,22 +165,6 @@ function build-app-bundle { echo ">> bundle app end"; } -function build-exporter-bundle { - echo ">> bundle exporter start"; - local version=$(print-current-version); - local bundle_dir="./bundle-exporter"; - - build "exporter"; - - rm -rf $bundle_dir; - mv ./exporter/target $bundle_dir; - - echo $version > $bundle_dir/version.txt - put-license-file $bundle_dir; - - echo ">> bundle exporter end"; -} - function usage { echo "PENPOT build & release manager" echo "USAGE: $0 OPTION" @@ -182,6 +214,14 @@ case $1 in build-app-bundle; ;; + build-frontend-bundle) + build-frontend-bundle; + ;; + + build-backend-bundle) + build-backend-bundle; + ;; + build-exporter-bundle) build-exporter-bundle; ;; diff --git a/version.txt b/version.txt index 8874652ad..f721a9b5e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.5.4-alpha +1.6.0-alpha