diff --git a/CHANGES.md b/CHANGES.md index 55742ac05..570ad407c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,14 @@ ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) +## 1.13.1-beta + +### :bug: Bugs fixed + +- Fix problem with text positioning +- Fix issue with thumbnail generation before fonts loading +- Fix unable to hide artboards +- Fix problem with fonts cache causing hanging in certain pages ## 1.13.0-beta diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index f49c4347d..35e9ed27f 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -12,6 +12,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.db.sql :as sql] [app.rpc.mutations.files :as m.files] [app.rpc.queries.profile :as profile] [app.util.blob :as blob] @@ -59,7 +60,7 @@ "select revn, changes, data from file_change where file_id=? and revn = ?") (defn prepare-response - [{:keys [params] :as request} body] + [{:keys [params] :as request} body filename] (when-not body (ex/raise :type :not-found :code :enpty-data @@ -69,8 +70,7 @@ :body body :headers {"content-type" "application/transit+json"}) (contains? params :download) - (update :headers assoc "content-disposition" "attachment"))) - + (update :headers assoc "content-disposition" (str "attachment; filename=" filename)))) (defn- retrieve-file-data [{:keys [pool]} {:keys [params] :as request}] @@ -78,8 +78,9 @@ (ex/raise :type :authentication :code :only-admins-allowed)) - (let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid) - revn (some-> (get-in request [:params :revn]) d/parse-integer)] + (let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid) + revn (some-> (get-in request [:params :revn]) d/parse-integer) + filename (str file-id)] (when-not file-id (ex/raise :type :validation :code :missing-arguments)) @@ -88,9 +89,9 @@ (some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data) (some-> (db/get-by-id pool :file file-id) :data))] (if (contains? params :download) - (-> (prepare-response request data) + (-> (prepare-response request data filename) (update :headers assoc "content-type" "application/octet-stream")) - (prepare-response request (some-> data blob/decode)))))) + (prepare-response request (some-> data blob/decode) filename))))) (defn- upload-file-data [{:keys [pool]} {:keys [profile-id params] :as request}] @@ -98,12 +99,20 @@ data (some-> params :file :path fs/slurp-bytes blob/decode)] (if (and data project-id) - (let [fname (str "imported-file-" (dt/now))] - (m.files/create-file pool {:id (uuid/next) - :name fname - :project-id project-id - :profile-id profile-id - :data data}) + (let [fname (str "imported-file-" (dt/now)) + file-id (try + (uuid/uuid (-> params :file :filename)) + (catch Exception _ (uuid/next))) + file (db/exec-one! pool (sql/select :file {:id file-id}))] + (if file + (db/update! pool :file + {:data (blob/encode data)} + {:id file-id}) + (m.files/create-file pool {:id file-id + :name fname + :project-id project-id + :profile-id profile-id + :data data})) (yrs/response 200 "OK")) (yrs/response 500 "ERROR")))) @@ -121,8 +130,9 @@ (ex/raise :type :authentication :code :only-admins-allowed)) - (let [file-id (some-> (get-in request [:params :id]) uuid/uuid) - revn (or (get-in request [:params :revn]) "latest")] + (let [file-id (some-> (get-in request [:params :id]) uuid/uuid) + revn (or (get-in request [:params :revn]) "latest") + filename (str file-id)] (when (or (not file-id) (not revn)) (ex/raise :type :validation @@ -132,7 +142,7 @@ (cond (d/num-string? revn) (let [item (db/exec-one! pool [sql:retrieve-single-change file-id (d/parse-integer revn)])] - (prepare-response request (some-> item :changes blob/decode vec))) + (prepare-response request (some-> item :changes blob/decode vec) filename)) (str/includes? revn ":") (let [[start end] (->> (str/split revn #":") @@ -144,7 +154,8 @@ (map :changes) (map blob/decode) (mapcat identity) - (vec)))) + (vec)) + filename)) :else (ex/raise :type :validation :code :invalid-arguments)))) @@ -176,8 +187,7 @@ (some-> report :error :trace)) :params (:params report)}] (-> (io/resource "templates/error-report.tmpl") - (tmpl/render params)))) - ] + (tmpl/render params))))] (when-not (authorized? pool request) (ex/raise :type :authentication diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 26c2f80c4..05a43064a 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -81,14 +81,28 @@ :timeout 6000 :method :get})) - (validate-response [{:keys [status body] :as res}] - (when-not (= 200 status) + (retrieve-emails [] + (if (some? (:emails-uri provider)) + (http-client {:uri (:emails-uri provider) + :headers {"Authorization" (str (:type tdata) " " (:token tdata))} + :timeout 6000 + :method :get}) + (p/resolved {:status 200}))) + + (validate-response [[retrieve-res emails-res]] + (when-not (s/int-in-range? 200 300 (:status retrieve-res)) (ex/raise :type :internal :code :unable-to-retrieve-user-info :hint "unable to retrieve user info" - :http-status status - :http-body body)) - res) + :http-status (:status retrieve-res) + :http-body (:body retrieve-res))) + (when-not (s/int-in-range? 200 300 (:status emails-res)) + (ex/raise :type :internal + :code :unable-to-retrieve-user-info + :hint "unable to retrieve user info" + :http-status (:status emails-res) + :http-body (:body emails-res))) + [retrieve-res emails-res]) (get-email [info] (let [attr-kw (cf/get :oidc-email-attr :email)] @@ -98,10 +112,13 @@ (let [attr-kw (cf/get :oidc-name-attr :name)] (get info attr-kw))) - (process-response [{:keys [body]}] - (let [info (json/read body)] + (process-response [[retrieve-res emails-res]] + (let [info (json/read (:body retrieve-res)) + email (if (some? (:extract-email-callback provider)) + ((:extract-email-callback provider) emails-res) + (get-email info))] {:backend (:name provider) - :email (get-email info) + :email email :fullname (get-name info) :props (->> (dissoc info :name :email) (qualify-props provider))})) @@ -116,7 +133,7 @@ :info info)) info)] - (-> (retrieve) + (-> (p/all [(retrieve) (retrieve-emails)]) (p/then' validate-response) (p/then' process-response) (p/then' validate-info)))) @@ -386,15 +403,25 @@ (assoc-in cfg [:providers "google"] opts)) cfg))) +(defn extract-github-email + [response] + (let [emails (json/read (:body response)) + primary-email (->> emails + (filter #(:primary %)) + first)] + (:email primary-email))) + (defn- initialize-github-provider [cfg] - (let [opts {:client-id (cf/get :github-client-id) - :client-secret (cf/get :github-client-secret) - :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" - :name "github"}] + (let [opts {:client-id (cf/get :github-client-id) + :client-secret (cf/get :github-client-secret) + :scopes #{"read:user" "user:email"} + :auth-uri "https://github.com/login/oauth/authorize" + :token-uri "https://github.com/login/oauth/access_token" + :emails-uri "https://api.github.com/user/emails" + :extract-email-callback extract-github-email + :user-uri "https://api.github.com/user" + :name "github"}] (if (and (string? (:client-id opts)) (string? (:client-secret opts))) (do diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj index dbae70834..a8016c0bb 100644 --- a/backend/src/app/rpc/mutations/verify_token.clj +++ b/backend/src/app/rpc/mutations/verify_token.clj @@ -123,7 +123,7 @@ (defmethod process-token :team-invitation [cfg {:keys [profile-id token]} {:keys [member-id] :as claims}] (us/assert ::team-invitation-claims claims) - #_(let [conn (:conn cfg) + (let [conn (:conn cfg) team-id (:team-id claims) member-email (:member-email claims) invitation (db/get-by-params conn :team-invitation diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index fac42d0b0..6ec344559 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -99,6 +99,27 @@ (update data :pages-index d/update-vals update-page))) +(defn repair-idless-components + "There are some files that contains components with no :id attribute. + This function detects them and repairs it. + + Use it with the update-file function above." + [data] + (letfn [(update-component [id component] + (if (nil? (:id component)) + (do + (prn (:id data) "Broken component" (:name component) id) + (assoc component :id id)) + component))] + + (update data :components #(d/mapm update-component %)))) + +(defn analyze-idless-components + "Scan all files to check if there are any one with idless components. + (Does not save the changes, only used to detect affected files)." + [file _] + (repair-idless-components (:data file))) + ;; (defn check-image-shapes ;; [{:keys [data] :as file} stats] ;; (println "=> analizing file:" (:name file) (:id file)) @@ -138,9 +159,11 @@ (loop [cursor (dt/now) chunks 0] (when (< chunks max-chunks) - (when-let [chunk (retrieve-chunk conn cursor)] - (let [cursor (-> chunk last :modified-at)] - (process-chunk chunk) - (Thread/sleep (inst-ms (dt/duration sleep))) - (recur cursor (inc chunks)))))) + (let [chunk (retrieve-chunk conn cursor)] + (when-not (empty? chunk) + (let [cursor (-> chunk last :modified-at)] + (process-chunk chunk) + (Thread/sleep (inst-ms (dt/duration sleep))) + (recur cursor (inc chunks))))))) @stats)))) + diff --git a/common/src/app/common/geom/shapes/corners.cljc b/common/src/app/common/geom/shapes/corners.cljc index 16c6d17cd..5fc7103c9 100644 --- a/common/src/app/common/geom/shapes/corners.cljc +++ b/common/src/app/common/geom/shapes/corners.cljc @@ -4,7 +4,15 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.geom.shapes.corners) +(ns app.common.geom.shapes.corners + (:require + [app.common.math :as mth])) + +(defn- zero-div + [a b] + (if (mth/almost-zero? b) + ##Inf + (/ a b))) (defn fix-radius ;; https://www.w3.org/TR/css-backgrounds-3/#corner-overlap @@ -16,17 +24,19 @@ ;; > the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and ;; > Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f. ([width height r] - (let [f (min (/ width (* 2 r)) - (/ height (* 2 r)))] + (let [f (min 1 + (zero-div width (* 2 r)) + (zero-div height (* 2 r)))] (if (< f 1) (* r f) r))) ([width height r1 r2 r3 r4] - (let [f (min (/ width (+ r1 r2)) - (/ height (+ r2 r3)) - (/ width (+ r3 r4)) - (/ height (+ r4 r1)))] + (let [f (min 1 + (zero-div width (+ r1 r2)) + (zero-div height (+ r2 r3)) + (zero-div width (+ r3 r4)) + (zero-div height (+ r4 r1)))] (if (< f 1) [(* r1 f) (* r2 f) (* r3 f) (* r4 f)] [r1 r2 r3 r4])))) @@ -34,7 +44,7 @@ (defn shape-corners-1 "Retrieve the effective value for the corner given a single value for corner." [{:keys [width height rx] :as shape}] - (if (some? rx) + (if (and (some? rx) (not (mth/almost-zero? rx))) (fix-radius width height rx) 0)) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 30351a666..825558259 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -9,7 +9,7 @@ [app.common.colors :as clr] [app.common.uuid :as uuid])) -(def file-version 17) +(def file-version 18) (def default-color clr/gray-20) (def root uuid/zero) diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc index 95e0a1840..eb0f1c77f 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -400,5 +400,20 @@ (update :pages-index d/update-vals update-container) (update :components d/update-vals update-container)))) +;;Remove position-data to solve a bug with the text positioning +(defmethod migrate 18 + [data] + (letfn [(update-object [object] + (cond-> object + (cph/text-shape? object) + (dissoc :position-data))) + + (update-container [container] + (update container :objects d/update-vals update-object))] + + (-> data + (update :pages-index d/update-vals update-container) + (update :components d/update-vals update-container)))) + ;; TODO: pending to do a migration for delete already not used fill ;; and stroke props. This should be done for >1.14.x version. diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 45b1bc4a2..a7e7ed44f 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -44,8 +44,7 @@ marked.use({renderer}); // Templates function readLocales() { - // const langs = ["ar", "he", "ca", "de", "el", "en", "es", "fr", "it", "tr", "ru", "zh_CN", "pt_BR", "ro"]; - const langs = ["ar", "he", "ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_CN", "pt_BR", "ro"]; + const langs = ["ar", "ca", "de", "el", "en", "es", "fa", "fr", "he", "nb_NO", "pl", "pt_BR", "ro", "ru", "tr", "zh_CN", "zh_Hant"]; const result = {}; for (let lang of langs) { diff --git a/frontend/resources/styles/main/partials/dashboard-settings.scss b/frontend/resources/styles/main/partials/dashboard-settings.scss index d45d1661e..562671abd 100644 --- a/frontend/resources/styles/main/partials/dashboard-settings.scss +++ b/frontend/resources/styles/main/partials/dashboard-settings.scss @@ -50,6 +50,10 @@ justify-content: space-between; flex-direction: row; } + + h2 { + margin-bottom: 1rem; + } } .avatar-form { diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index c8f1f166c..fa2235c1a 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -107,6 +107,30 @@ (rx/of (update-indices page-id changes)))) (rx/empty)))))) +(defn changed-frames + "Extracts the frame-ids changed in the given changes" + [changes objects] + + (let [change->ids + (fn [change] + (case (:type change) + :add-obj + [(:parent-id change)] + + (:mod-obj :del-obj) + [(:id change)] + + :mov-objects + (d/concat-vec (:shapes change) [(:parent-id change)]) + + []))] + (into #{} + (comp (mapcat change->ids) + (keep #(if (= :frame (get-in objects [% :type])) + % + (get-in objects [% :frame-id]))) + (remove #(= uuid/zero %))) + changes))) (defn commit-changes [{:keys [redo-changes undo-changes @@ -115,15 +139,18 @@ (log/debug :msg "commit-changes" :js/redo-changes redo-changes :js/undo-changes undo-changes) - (let [error (volatile! nil)] + (let [error (volatile! nil) + page-id (:current-page-id @st/state) + frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state))] (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 redo-changes + :page-id page-id + :frames frames}) ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 492348b6a..d1243cbea 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -279,7 +279,10 @@ :always (d/without-nils))] - (assoc-in shape [:strokes index] new-attrs))))))))) + (-> shape + (cond-> (not (contains? shape :strokes)) + (assoc :strokes [])) + (assoc-in [:strokes index] new-attrs)))))))))) (defn change-shadow [ids attrs index] diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index fa88c508a..e13c80f73 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -195,13 +195,26 @@ (ptk/reify ::handle-file-change ptk/WatchEvent (watch [_ _ _] - (let [changes-by-pages (group-by :page-id changes) + (let [position-data-operation? + (fn [{:keys [type attr]}] + (and (= :set type) (= attr :position-data))) + + remove-update-position-data + (fn [change] + (cond-> change + (= :mod-obj (:type change)) + (update :operations #(filterv (comp not position-data-operation?) %)))) + process-page-changes (fn [[page-id changes]] - (dch/update-indices page-id changes))] + (dch/update-indices page-id changes)) + + ;; We remove `position-data` from the incomming message + changes (->> changes (mapv remove-update-position-data)) + changes-by-pages (group-by :page-id changes)] (rx/merge - (rx/of (dwp/shapes-changes-persisted file-id msg)) + (rx/of (dwp/shapes-changes-persisted file-id (assoc msg :changes changes))) (when-not (empty? changes-by-pages) (rx/from (map process-page-changes changes-by-pages)))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 788f31ca5..64c237b69 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -6,6 +6,7 @@ (ns app.main.data.workspace.persistence (:require + [app.common.data :as d] [app.common.logging :as log] [app.common.pages :as cp] [app.common.spec :as us] @@ -17,6 +18,7 @@ [app.main.data.fonts :as df] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.thumbnails :as dwt] [app.main.repo :as rp] [app.main.store :as st] [app.util.http :as http] @@ -138,16 +140,27 @@ :revn (:revn file) :session-id sid :changes-with-metadata (into [] changes)}] + (when (= file-id (:id params)) (->> (rp/mutation :update-file params) (rx/mapcat (fn [lagged] (log/debug :hint "changes persisted" :lagged (count lagged)) (let [lagged (cond->> lagged (= #{sid} (into #{} (map :session-id) lagged)) - (map #(assoc % :changes [])))] - (->> (rx/of lagged) - (rx/mapcat seq) - (rx/map #(shapes-changes-persisted file-id %)))))) + (map #(assoc % :changes []))) + + frame-updates + (-> (group-by :page-id changes) + (d/update-vals #(into #{} (mapcat :frames) %)))] + + (rx/merge + (->> (rx/from frame-updates) + (rx/flat-map (fn [[page-id frames]] + (->> frames (map #(vector page-id %))))) + (rx/map (fn [[page-id frame-id]] (dwt/update-thumbnail page-id frame-id)))) + (->> (rx/of lagged) + (rx/mapcat seq) + (rx/map #(shapes-changes-persisted file-id %))))))) (rx/catch (fn [cause] (rx/concat (rx/of (rt/assign-exception cause)) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 6789fe019..956c41c59 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -63,13 +63,17 @@ (ted/get-editor-current-content))] (if (ted/content-has-text? content) (let [content (d/merge (ted/export-content content) - (dissoc (:content shape) :children))] + (dissoc (:content shape) :children)) + modifiers (get-in state [:workspace-text-modifier id])] (rx/merge (rx/of (update-editor-state shape nil)) (when (and (not= content (:content shape)) (some? (:current-page-id state))) (rx/of - (dch/update-shapes [id] #(assoc % :content content)) + (dch/update-shapes [id] (fn [shape] + (-> shape + (assoc :content content) + (merge modifiers)))) (dwu/commit-undo-transaction))))) (when (some? id) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 8a5c117ff..7766494a1 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -6,7 +6,6 @@ (ns app.main.data.workspace.thumbnails (:require - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] @@ -14,6 +13,8 @@ [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] + [app.util.dom :as dom] + [app.util.webapi :as wapi] [beicon.core :as rx] [potok.core :as ptk])) @@ -26,45 +27,47 @@ (rx/filter #(= % id)) (rx/take 1))) +(defn thumbnail-stream + [object-id] + (rx/create + (fn [subs] + (let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'" object-id))] + (if (some? node) + (-> node + (.toBlob (fn [blob] + (rx/push! subs blob) + (rx/end! subs)) + "image/png")) + + ;; If we cannot find the node we send `nil` and the upsert will delete the thumbnail + (do (rx/push! subs nil) + (rx/end! subs))))))) + (defn update-thumbnail "Updates the thumbnail information for the given frame `id`" - [page-id frame-id data] - (let [lock (uuid/next) - object-id (dm/str page-id frame-id)] + [page-id frame-id] + (ptk/reify ::update-thumbnail + ptk/WatchEvent + (watch [_ state _] + (let [object-id (dm/str page-id frame-id) + file-id (:current-file-id state) + blob-result (thumbnail-stream object-id)] - (ptk/reify ::update-thumbnail - IDeref - (-deref [_] {:object-id object-id :data data}) - - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-file :thumbnails object-id] data) - (cond-> (nil? (get-in state [::update-thumbnail-lock object-id])) - (assoc-in [::update-thumbnail-lock object-id] lock)))) + (->> blob-result + (rx/merge-map + (fn [blob] + (if (some? blob) + (wapi/read-file-as-data-url blob) + (rx/of nil)))) - ptk/WatchEvent - (watch [_ state stream] - (when (= lock (get-in state [::update-thumbnail-lock object-id])) - (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize))) - params {:file-id (:current-file-id state) - :object-id object-id}] - ;; Sends the first event and debounce the rest. Will only make one update once - ;; the 2 second debounce is finished - (rx/merge - (->> stream - (rx/filter (ptk/type? ::update-thumbnail)) - (rx/map deref) - (rx/filter #(= object-id (:object-id %))) - (rx/debounce 2000) - (rx/take 1) - (rx/map :data) - (rx/flat-map #(rp/mutation! :upsert-file-object-thumbnail (assoc params :data %))) - (rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock object-id]))) - (rx/take-until stopper)) - - (->> (rx/of (update-thumbnail page-id frame-id data)) - (rx/observe-on :async))))))))) + (rx/merge-map + (fn [data] + (let [params {:file-id file-id :object-id object-id :data data}] + (rx/merge + ;; Update the local copy of the thumbnails so we don't need to request it again + (rx/of #(assoc-in % [:workspace-file :thumbnails object-id] data)) + (->> (rp/mutation! :upsert-file-object-thumbnail params) + (rx/ignore))))))))))) (defn- extract-frame-changes "Process a changes set in a commit to extract the frames that are changing" @@ -156,5 +159,3 @@ (let [page-id (get state :current-page-id) old-shape-thumbnail (get-in state [:workspace-file :thumbnails (dm/str page-id old-id)])] (-> state (assoc-in [:workspace-file :thumbnails (dm/str page-id new-id)] old-shape-thumbnail)))))) - - diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 38f5ab6b0..6022514f2 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -82,6 +82,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defonce loaded (l/atom #{})) +(defonce loading (l/atom {})) (defn- create-link-element [uri] @@ -199,11 +200,34 @@ (p/create (fn [resolve] (ensure-loaded! id resolve)))) ([id on-loaded] - (if (contains? @loaded id) - (on-loaded id) - (when-let [font (get @fontsdb id)] - (load-font (assoc font ::on-loaded on-loaded)) - (swap! loaded conj id))))) + (let [font (get @fontsdb id)] + (cond + ;; Font already loaded, we just continue + (contains? @loaded id) + (on-loaded id) + + ;; Font is currently downloading. We attach the caller to the promise + (contains? @loading id) + (-> (get @loading id) + (p/then #(on-loaded id))) + + ;; First caller, we create the promise and then wait + :else + (let [on-load (fn [resolve] + (swap! loaded conj id) + (swap! loading dissoc id) + (on-loaded id) + (resolve id)) + + load-p (p/create + (fn [resolve _] + (-> font + (assoc ::on-loaded (partial on-load resolve)) + (load-font))))] + + (swap! loading assoc id load-p) + + nil))))) (defn ready [cb] diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index e18f06c9d..8c9cc7b0a 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -75,7 +75,6 @@ on-succes (fn [data] - (prn "SUCCESS" data) (when-let [token (:invitation-token data)] (st/emit! (rt/nav :auth-verify-token {} {:token token})))) diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index a8ea4bd22..74d405ced 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -81,13 +81,12 @@ [:hr] - [:h2 (tr "feedback.discussions-title")] - [:p (tr "feedback.discussions-subtitle1")] - [:p (tr "feedback.discussions-subtitle2")] + [:h2 (tr "feedback.twitter-title")] + [:p (tr "feedback.twitter-subtitle1")] [:a.btn-secondary.btn-large - {:href "https://github.com/penpot/penpot/discussions" :target "_blank"} - (tr "feedback.discussions-go-to")] + {:href "https://twitter.com/PenpotSupport" :target "_blank"} + (tr "feedback.twitter-go-to")] [:hr] diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index ba4776f2b..e3e80e431 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -360,7 +360,7 @@ (-> props (obj/set! "style" style))) - (some? svg-attrs) + (and (some? svg-attrs) (empty? (:fills shape))) (let [style (-> (obj/get props "style") (obj/clone)) diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs index cb692b663..eb6a89b91 100644 --- a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -73,8 +73,12 @@ ;; Creates a style tag by replacing the urls with the data uri style (replace-embeds fonts-css fonts-urls fonts-embed)] - (when (d/not-empty? style) - [:style {:data-loading loading?} style]))) + (cond + (d/not-empty? style) + [:style {:data-loading loading?} style] + + (d/not-empty? fonts) + [:style {:data-loading true}]))) (defn shape->fonts [shape objects] diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index fb63acd0a..8082d00ff 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -110,7 +110,10 @@ (let [font-variant (d/seek #(= font-variant-id (:id %)) (:variants font))] [(str/quote (or (:family font) (:font-family data))) (or (:style font-variant) (:font-style data)) - (or (:weight font-variant) (:font-weight data))]))] + (or (:weight font-variant) (:font-weight data))])) + + base (-> base + (obj/set! "--font-id" font-id))] (cond-> base (some? fills) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 522c17258..9d420f3fb 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -71,11 +71,6 @@ frame-id (:id shape) page-id (mf/use-ctx ctx/current-page-id) - thumbnail-data-ref (mf/use-memo (mf/deps page-id frame-id) #(refs/thumbnail-frame-data page-id frame-id)) - thumbnail-data (mf/deref thumbnail-data-ref) - - thumbnail? (and thumbnail? (some? thumbnail-data)) - ;; References to the current rendered node and the its parentn node-ref (mf/use-var nil) @@ -88,11 +83,11 @@ disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers])) - [on-load-frame-dom thumb-renderer] - (ftr/use-render-thumbnail page-id shape node-ref rendered? thumbnail-data-ref disable-thumbnail?) + [on-load-frame-dom render-frame? thumbnail-renderer] + (ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail?) on-frame-load - (fns/use-node-store thumbnail? node-ref rendered?)] + (fns/use-node-store thumbnail? node-ref rendered? render-frame?)] (fdm/use-dynamic-modifiers objects @node-ref modifiers) @@ -115,9 +110,9 @@ (rx/dispose! sub))))) (mf/use-effect - (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render) + (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?) (fn [] - (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render)) + (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?)) (mf/mount (mf/element frame-shape #js {:ref on-load-frame-dom :shape shape :fonts fonts}) @@ -128,12 +123,11 @@ [:& (mf/provider ctx/render-ctx) {:value render-id} [:g.frame-container {:id (dm/str "frame-container-" (:id shape)) :key "frame-container" - :ref on-frame-load} + :ref on-frame-load + :opacity (when (:hidden shape) 0)} [:& ff/fontfaces-style {:fonts fonts}] - [:g.frame-thumbnail-wrapper {:id (dm/str "thumbnail-container-" (:id shape))} - [:> frame/frame-thumbnail {:key (dm/str (:id shape)) - :shape (cond-> shape - (some? thumbnail-data) - (assoc :thumbnail thumbnail-data))}] - - thumb-renderer]]])))) + [:g.frame-thumbnail-wrapper + {:id (dm/str "thumbnail-container-" (:id shape)) + ;; Hide the thumbnail when not displaying + :opacity (when (and @rendered? (not thumbnail?)) 0)} + thumbnail-renderer]]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs index 52c31d920..6d9730530 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs @@ -12,7 +12,7 @@ (defn use-node-store "Hook responsible of storing the rendered DOM node in memory while not being used" - [thumbnail? node-ref rendered?] + [thumbnail? node-ref rendered? render-frame?] (let [;; when `true` the node is in memory in-memory? (mf/use-var true) @@ -33,13 +33,13 @@ (swap! re-render inc)))))] (mf/use-effect - (mf/deps thumbnail?) + (mf/deps thumbnail? render-frame?) (fn [] - (when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?) + (when (and (some? @parent-ref) (some? @node-ref) @rendered? (and thumbnail? (not render-frame?))) (.removeChild @parent-ref @node-ref) (reset! in-memory? true)) - (when (and (some? @node-ref) @in-memory? (not thumbnail?)) + (when (and (some? @node-ref) @in-memory? (or (not thumbnail?) render-frame?)) (.appendChild @parent-ref @node-ref) (reset! in-memory? false)))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index e6096c18b..35bd2d49c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -9,19 +9,20 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] - [app.main.data.workspace :as dw] + [app.main.data.workspace.thumbnails :as dwt] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.hooks :as hooks] + [app.main.ui.shapes.frame :as frame] [app.util.dom :as dom] [app.util.object :as obj] [app.util.timers :as ts] [beicon.core :as rx] [cuerdas.core :as str] + [debug :refer [debug?]] [rumext.alpha :as mf])) -;; (def thumbnail-scale-factor 2) - -(defn- draw-thumbnail-canvas +(defn- draw-thumbnail-canvas! [canvas-node img-node] (try (when (and (some? canvas-node) (some? img-node)) @@ -29,19 +30,12 @@ canvas-width (.-width canvas-node) canvas-height (.-height canvas-node)] - ;; TODO: Expermient with different scale factors - ;; (set! (.-width canvas-node) (* thumbnail-scale-factor canvas-width)) - ;; (set! (.-height canvas-node) (* thumbnail-scale-factor canvas-height)) - ;; (.setTransform canvas-context thumbnail-scale-factor 0 0 thumbnail-scale-factor 0 0) - ;; (set! (.-imageSmoothingEnabled canvas-context) true) - ;; (set! (.-imageSmoothingQuality canvas-context) "high") - (.clearRect canvas-context 0 0 canvas-width canvas-height) (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) - (.toDataURL canvas-node "image/png" 1.0))) + true)) (catch :default err (.error js/console err) - nil))) + false))) (defn- remove-image-loading "Remove the changes related to change a url for its embed value. This is necessary @@ -59,7 +53,7 @@ (defn use-render-thumbnail "Hook that will create the thumbnail thata" - [page-id {:keys [id x y width height] :as shape} node-ref rendered? thumbnail-data-ref disable?] + [page-id {:keys [id x y width height] :as shape} node-ref rendered? disable?] (let [frame-canvas-ref (mf/use-ref nil) frame-image-ref (mf/use-ref nil) @@ -78,16 +72,25 @@ updates-str (mf/use-memo #(rx/subject)) + thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id)) + thumbnail-data (mf/deref thumbnail-data-ref) + + render-frame? (mf/use-state (not thumbnail-data)) + on-image-load (mf/use-callback (fn [] (ts/raf #(let [canvas-node (mf/ref-val frame-canvas-ref) - img-node (mf/ref-val frame-image-ref) - thumb-data (draw-thumbnail-canvas canvas-node img-node)] - (when (some? thumb-data) - (st/emit! (dw/update-thumbnail page-id id thumb-data)) - (reset! image-url nil)))))) + img-node (mf/ref-val frame-image-ref)] + (when (draw-thumbnail-canvas! canvas-node img-node) + (reset! image-url nil) + (reset! render-frame? false)) + + ;; If we don't have the thumbnail data saved (normaly the first load) we update the data + ;; when available + (when (not @thumbnail-data-ref) + (st/emit! (dwt/update-thumbnail page-id id) )))))) generate-thumbnail (mf/use-callback @@ -115,7 +118,7 @@ (fn [] (when (and (some? @node-ref) @regenerate-thumbnail) (let [loading-images? (some? (dom/query @node-ref "[data-loading='true']")) - loading-fonts? (some? (dom/query (dm/str "#frame-container-" (:id shape) " style[data-loading='true']")))] + loading-fonts? (some? (dom/query (dm/str "#frame-container-" (:id shape) " > style[data-loading='true']")))] (when (and (not loading-images?) (not loading-fonts?)) (generate-thumbnail) (reset! regenerate-thumbnail false)))))) @@ -172,18 +175,28 @@ (reset! observer-ref nil))))) [on-load-frame-dom - (when (some? @image-url) - (mf/html - [:g.thumbnail-rendering - [:foreignObject {:x x :y y :width width :height height} - [:canvas {:ref frame-canvas-ref - :width fixed-width - :height fixed-height}]] + @render-frame? + (mf/html + [:* + [:> frame/frame-thumbnail {:key (dm/str (:id shape)) + :shape (cond-> shape + (some? thumbnail-data) + (assoc :thumbnail thumbnail-data))}] + [:foreignObject {:x x :y y :width width :height height} + [:canvas.thumbnail-canvas + {:ref frame-canvas-ref + :data-object-id (dm/str page-id (:id shape)) + :width fixed-width + :height fixed-height + ;; DEBUG + :style {:filter (when (debug? :thumbnails) "invert(1)")}}]] + + (when (some? @image-url) [:image {:ref frame-image-ref :x (:x shape) :y (:y shape) :href @image-url :width (:width shape) :height (:height shape) - :on-load on-image-load}]]))])) + :on-load on-image-load}])])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs index 450c72070..77f9eb29f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -23,6 +23,7 @@ [app.util.text-editor :as ted] [app.util.text-svg-position :as utp] [app.util.timers :as ts] + [promesa.core :as p] [rumext.alpha :as mf])) (defn strip-position-data [shape] @@ -61,37 +62,39 @@ (assoc :content (d/txt-merge content editor-content))))) (defn- update-text-shape - [{:keys [grow-type id]} node] + [{:keys [grow-type id migrate]} node] ;; Check if we need to update the size because it's auto-width or auto-height - (when (contains? #{:auto-height :auto-width} grow-type) - (let [{:keys [width height]} - (-> (dom/query node ".paragraph-set") - (dom/get-client-size)) - width (mth/ceil width) - height (mth/ceil height)] - (when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) - (st/emit! (dwt/resize-text id width height))))) - ;; Update the position-data of every text fragment - (let [position-data (utp/calc-position-data node)] - (st/emit! (dwt/update-position-data id position-data))) + (p/let [position-data (utp/calc-position-data node)] + (st/emit! (dwt/update-position-data id position-data)) + + (when (contains? #{:auto-height :auto-width} grow-type) + (let [{:keys [width height]} + (-> (dom/query node ".paragraph-set") + (dom/get-client-size)) + width (mth/ceil width) + height (mth/ceil height)] + (when (and (not (mth/almost-zero? width)) + (not (mth/almost-zero? height)) + (not migrate)) + (st/emit! (dwt/resize-text id width height)))))) (st/emit! (dwt/clean-text-modifier id))) (defn- update-text-modifier [{:keys [grow-type id]} node] - (let [position-data (utp/calc-position-data node) - props {:position-data position-data} + (p/let [position-data (utp/calc-position-data node) + props {:position-data position-data} - props - (if (contains? #{:auto-height :auto-width} grow-type) - (let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size)) - width (mth/ceil width) - height (mth/ceil height)] - (if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) - (assoc props :width width :height height) - props)) - props)] + props + (if (contains? #{:auto-height :auto-width} grow-type) + (let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size)) + width (mth/ceil width) + height (mth/ceil height)] + (if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) + (assoc props :width width :height height) + props)) + props)] (st/emit! (dwt/update-text-modifier id props)))) @@ -228,7 +231,14 @@ (mf/deps text-shapes modifiers) #(d/update-vals text-shapes (partial process-shape modifiers))) - editing-shape (get text-shapes edition)] + editing-shape (get text-shapes edition) + + ;; This memo is necesary so the viewport-text-wrapper memoize its props correctly + text-shapes-wrapper + (mf/use-memo + (mf/deps text-shapes edition) + (fn [] + (dissoc text-shapes edition)))] ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are ;; edited @@ -242,5 +252,5 @@ (when editing-shape [:& viewport-text-editing {:shape editing-shape}]) - [:& viewport-texts-wrapper {:text-shapes (dissoc text-shapes edition) + [:& viewport-texts-wrapper {:text-shapes text-shapes-wrapper :modifiers modifiers}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 70437a9aa..cd673a628 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -71,8 +71,8 @@ focus (mf/deref refs/workspace-focus-selected) objects-ref (mf/use-memo #(refs/workspace-page-objects-by-id page-id)) - base-objects (-> (mf/deref objects-ref) - (ui-hooks/with-focus-objects focus)) + objects (mf/deref objects-ref) + base-objects (-> objects (ui-hooks/with-focus-objects focus)) modifiers (mf/deref refs/workspace-modifiers) @@ -245,7 +245,7 @@ [:g {:pointer-events "none" :opacity 0} [:& stv/viewport-texts {:key (dm/str "texts-" page-id) :page-id page-id - :objects base-objects + :objects objects :modifiers modifiers :edition edition}]]] diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index bff9296bd..80c600da1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -25,6 +25,7 @@ [app.util.globals :as globals] [app.util.timers :as timers] [beicon.core :as rx] + [debug :refer [debug?]] [goog.events :as events] [rumext.alpha :as mf]) (:import goog.events.EventType)) @@ -269,7 +270,11 @@ ;; We only allow active frames that are contained in the vbox (filter (partial inside-vbox vbox objects))) - all-frames)] + all-frames) + + ;; Debug only: Disable the thumbnails + new-active-frames + (if (debug? :disable-frame-thumbnails) (into #{} all-frames) new-active-frames)] (when (not= @active-frames new-active-frames) (reset! active-frames new-active-frames))))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 09e247c40..67c58faa9 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -135,35 +135,36 @@ (on-frame-leave (:id frame)))) text-pos-x (if (:use-for-thumbnail? frame) 15 0)] - [:* - (when (:use-for-thumbnail? frame) - [:g {:transform (str (when (and selected? modifiers) - (str (:displacement modifiers) " ")) - (text-transform label-pos zoom))} - [:svg {:x 0 - :y -9 - :width 12 - :height 12 - :class "workspace-frame-icon" + (when (not (:hidden frame)) + [:* + (when (:use-for-thumbnail? frame) + [:g {:transform (str (when (and selected? modifiers) + (str (:displacement modifiers) " ")) + (text-transform label-pos zoom))} + [:svg {:x 0 + :y -9 + :width 12 + :height 12 + :class "workspace-frame-icon" + :style {:fill (when selected? "var(--color-primary-dark)")} + :visibility (if show-artboard-names? "visible" "hidden")} + [:use {:href "#icon-set-thumbnail"}]]]) + [:text {:x text-pos-x + :y 0 + :width width + :height 20 + :class "workspace-frame-label" + :transform (str (when (and selected? modifiers) + (str (:displacement modifiers) " ")) + (text-transform label-pos zoom)) :style {:fill (when selected? "var(--color-primary-dark)")} - :visibility (if show-artboard-names? "visible" "hidden")} - [:use {:href "#icon-set-thumbnail"}]]]) - [:text {:x text-pos-x - :y 0 - :width width - :height 20 - :class "workspace-frame-label" - :transform (str (when (and selected? modifiers) - (str (:displacement modifiers) " ")) - (text-transform label-pos zoom)) - :style {:fill (when selected? "var(--color-primary-dark)")} - :visibility (if show-artboard-names? "visible" "hidden") - :on-mouse-down on-mouse-down - :on-double-click on-double-click - :on-context-menu on-context-menu - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} - (:name frame)]])) + :visibility (if show-artboard-names? "visible" "hidden") + :on-mouse-down on-mouse-down + :on-double-click on-double-click + :on-context-menu on-context-menu + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + (:name frame)]]))) (mf/defc frame-titles {::mf/wrap-props false diff --git a/frontend/src/app/util/cache.cljs b/frontend/src/app/util/cache.cljs index 53fa610cc..dc6d2b2c3 100644 --- a/frontend/src/app/util/cache.cljs +++ b/frontend/src/app/util/cache.cljs @@ -10,17 +10,34 @@ [beicon.core :as rx])) (defonce cache (atom {})) +(defonce pending (atom {})) (defn with-cache [{:keys [key max-age]} observable] (let [entry (get @cache key) + pending-entry (get @pending key) + age (when entry (dt/diff (dt/now) (:created-at entry)))] - (if (and (some? entry) (< age max-age)) + (cond + (and (some? entry) (< age max-age)) (rx/of (:data entry)) - (->> observable - (rx/tap - (fn [data] - (let [entry {:created-at (dt/now) :data data}] - (swap! cache assoc key entry)))))))) + + (some? pending-entry) + pending-entry + + :else + (let [subject (rx/subject)] + (swap! pending assoc key subject) + (->> observable + (rx/catch #(do (rx/error! subject %) + (swap! pending dissoc key) + (rx/throw %))) + (rx/tap + (fn [data] + (let [entry {:created-at (dt/now) :data data}] + (swap! cache assoc key entry)) + (rx/push! subject data) + (rx/end! subject) + (swap! pending dissoc key)))))))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index e28abba25..dfa9572fd 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -558,3 +558,11 @@ (seq (.-children node)))] (->> root-node (tree-seq branch? get-children)))) + +(defn check-font? [font] + (let [fonts (.-fonts globals/document)] + (.check fonts font))) + +(defn load-font [font] + (let [fonts (.-fonts globals/document)] + (.load fonts font))) diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index 37c8cac35..adfb5aece 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -173,32 +173,34 @@ (fetch-data-uri uri false)) ([uri throw-err?] - (c/with-cache {:key uri :max-age (dt/duration {:hours 4})} - (let [request-stream - (send! {:method :get - :uri uri - :response-type :blob - :omit-default-headers true}) + (let [request-str + (->> (send! {:method :get + :uri uri + :response-type :blob + :omit-default-headers true}) + (rx/tap + (fn [resp] + (when (or (< (:status resp) 200) (>= (:status resp) 300)) + (rx/throw (js/Error. "Error fetching data uri" #js {:cause (clj->js resp)}))))) - request-stream - (if throw-err? - (rx/tap #(when-not (and (>= (:status %) 200) (< (:status %) 300)) - ;; HTTP ERRROR - (throw (js/Error. "Error fetching data uri" #js {:cause (clj->js %)}))) - request-stream) - (rx/filter #(= 200 (:status %)) - request-stream))] - (->> request-stream - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/map #(hash-map uri %))))))) + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url) + (rx/map #(hash-map uri %)) + (c/with-cache {:key uri :max-age (dt/duration {:hours 4})}))] + + ;; We need to check `throw-err?` after the cache is resolved otherwise we cannot cache request + ;; with different values of throw-err. By default we throw always the exception and then we just + ;; ignore when `throw-err?` is `true` + (if (not throw-err?) + (->> request-str (rx/catch #(rx/empty))) + request-str)))) (defn fetch-text [url] - (c/with-cache {:key url :max-age (dt/duration {:hours 4})} - (->> (send! - {:method :get - :mode :cors - :omit-default-headers true - :uri url - :response-type :text}) - (rx/map :body)))) + (->> (send! + {:method :get + :mode :cors + :omit-default-headers true + :uri url + :response-type :text}) + (rx/map :body) + (c/with-cache {:key url :max-age (dt/duration {:hours 4})}))) diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index 61afc2450..3902c18a8 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -23,14 +23,18 @@ {:label "Français (community)" :value "fr"} {:label "Deutsch (community)" :value "de"} ;; {:label "Italiano (community)" :value "it"} + {:label "Norsk - Bokmål (community)" :value "nb_no"} + {:label "Portuguese - Brazil (community)" :value "pt_br"} + {:label "Polski (community)" :value "pl"} {:label "Русский (community)" :value "ru"} + {:label "Rumanian (community)" :value "ro"} {: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 "he"} {:label "عربي/عربى (community)" :value "ar"} - {:label "简体中文 (community)" :value "zh_cn"}]) + {:label "فارسی (community)" :value "fa"} + {:label "简体中文 (community)" :value "zh_cn"} + {:label "中国传统的 (community)" :value "zh_hant"}]) (defn- parse-locale [locale] diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index ba6a191a6..f494827ab 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -7,6 +7,7 @@ (ns app.util.import.parser (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.spec.interactions :as cti] @@ -213,16 +214,29 @@ (reduce add-attrs node-attrs)) (= type :frame) - (let [;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have + (let [;; Old .penpot files doesn't have "g" nodes. They have a clipPath reference as a node attribute + to-url #(dm/str "url(#" % ")") + frame-clip-rect-node (->> (find-all-nodes node :defs) + (mapcat #(find-all-nodes % :clipPath)) + (filter #(= (to-url (:id (:attrs %))) (:clip-path node-attrs))) + (mapcat #(find-all-nodes % #{:rect :path})) + (first)) + + ;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have g-nodes (find-all-nodes node :g) defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes)) gg-nodes (flatten (map #(find-all-nodes % :g) g-nodes)) + + rect-nodes (flatten [[(find-all-nodes node :rect)] (map #(find-all-nodes % #{:rect :path}) defs-nodes) (map #(find-all-nodes % #{:rect :path}) g-nodes) (map #(find-all-nodes % #{:rect :path}) gg-nodes)]) svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)] - (merge (add-attrs {} (:attrs svg-node)) node-attrs)) + (merge + (add-attrs {} (:attrs frame-clip-rect-node)) + (add-attrs {} (:attrs svg-node)) + node-attrs)) (= type :svg-raw) (let [svg-content (get-data node :penpot:svg-content) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index e25a70f6f..007e60cd7 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -9,9 +9,11 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.transit :as transit] + [app.main.fonts :as fonts] [app.main.store :as st] [app.util.dom :as dom] - [app.util.text-position-data :as tpd])) + [app.util.text-position-data :as tpd] + [promesa.core :as p])) (defn parse-text-nodes "Given a text node retrieves the rectangles for everyone of its paragraphs and its text." @@ -27,6 +29,27 @@ (map parse-entry) (tpd/parse-text-nodes parent-node text-node)))) +(def load-promises (atom {})) + +(defn load-font + [font] + (if (contains? @load-promises font) + (get @load-promises font) + (let [load-promise (dom/load-font font)] + (swap! load-promises assoc font load-promise) + load-promise))) + +(defn resolve-font + [^js node] + + (let [styles (js/getComputedStyle node) + font (.getPropertyValue styles "font")] + (if (dom/check-font? font) + (p/resolved font) + (let [font-id (.getPropertyValue styles "--font-id")] + (-> (fonts/ensure-loaded! font-id) + (p/then #(when (not (dom/check-font? font)) + (load-font font)))))))) (defn calc-text-node-positions [base-node viewport zoom] @@ -58,22 +81,25 @@ :width (- (:x p2) (:x p1)) :height (- (:y p2) (:y p1))))) - text-nodes (dom/query-all base-node ".text-node, span[data-text]")] - - (->> text-nodes - (mapcat - (fn [parent-node] - (let [direction (.-direction (js/getComputedStyle parent-node))] - (->> (.-childNodes parent-node) - (mapcat #(parse-text-nodes parent-node direction %)))))) - (mapv #(update % :position translate-rect)))))) + text-nodes (dom/query-all base-node ".text-node, span[data-text]") + load-fonts (->> text-nodes (map resolve-font))] + (-> (p/all load-fonts) + (p/then + (fn [] + (->> text-nodes + (mapcat + (fn [parent-node] + (let [direction (.-direction (js/getComputedStyle parent-node))] + (->> (.-childNodes parent-node) + (mapcat #(parse-text-nodes parent-node direction %)))))) + (mapv #(update % :position translate-rect))))))))) (defn calc-position-data [base-node] (let [viewport (dom/get-element "render") zoom (or (get-in @st/state [:workspace-local :zoom]) 1)] (when (and (some? base-node) (some? viewport)) - (let [text-data (calc-text-node-positions base-node viewport zoom)] + (p/let [text-data (calc-text-node-positions base-node viewport zoom)] (when (d/not-empty? text-data) (->> text-data (mapv (fn [{:keys [node position text direction]}] diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 138c79b06..692cf61ce 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -125,7 +125,8 @@ match-criteria? (fn [shape] (and (not (:hidden shape)) - (not (:blocked shape)) + (or (= :frame (:type shape)) ;; We return frames even if blocked + (not (:blocked shape))) (or (not frame-id) (= frame-id (:frame-id shape))) (case (:type shape) :frame include-frames? diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index c62f9fe55..ccf1bd49b 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -64,6 +64,9 @@ ;; Disable thumbnail cache :disable-thumbnail-cache + + ;; Disable frame thumbnails + :disable-frame-thumbnails }) ;; These events are excluded when we activate the :events flag diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index 1a4ffffa8..06a8a2868 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -567,24 +567,6 @@ msgstr "ترغب في الكلام؟ تحدث معنا في Gitter" msgid "feedback.description" msgstr "وصف" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "اذهب إلى المناقشات" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "انضم إلى منتدى التواصل التعاوني لفريق Penpot." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"يمكنك طرح الأسئلة والإجابة عليها، إجراء محادثات مفتوحة، ومتابعة القرارات " -"التي تؤثر على المشروع." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "مناقشات الفريق" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "موضوع" diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index a9e7886b2..66f55e803 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -729,24 +729,6 @@ msgstr "Voleu parlar? Xategeu amb nosaltres a Gitter" msgid "feedback.description" msgstr "Descripció" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "Ves als debats" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Uniu-vos al fòrum col·laboratiu del Penpot." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Podeu fer i respondre preguntes, tenir converses obertes i seguir les " -"decisions que afecten el projecte." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "Debats de l'equip" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "Tema" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 778557e27..3f7645b39 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -804,24 +804,6 @@ msgstr "Möchten Sie sprechen? Chatten Sie mit uns bei Gitter" msgid "feedback.description" msgstr "Beschreibung" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "Zur Diskussion" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Treten Sie dem kollaborativen Kommunikationsforum des Penpot-Teams bei." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Sie können Fragen stellen und beantworten, offene Gespräche führen und " -"Entscheidungen verfolgen, die das Projekt betreffen." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "Teambesprechungen" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "Betreff" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index 8f3507ea3..7db6d07fd 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -480,24 +480,6 @@ msgstr "Νιώθετε σαν να μιλάτε; Συνομιλήστε μαζί msgid "feedback.description" msgstr "Περιγραφή" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "Μετάβαση στις συζητήσεις" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Μπείτε στο συνεργατικό φόρουμ του Penpot" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Μπορείτε να κάνετε και να απαντήσετε σε ερωτήσεις, να έχετε συνομιλίες " -"ανοιχτού τύπου και να συνεχίσετε να επηρεάζετε το έργο" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "Ομαδικές συζητήσεις " - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "Επιχείρηση" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0a893fe19..08fdb7a1f 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -763,24 +763,6 @@ msgstr "Feeling like talking? Chat with us at Gitter" msgid "feedback.description" msgstr "Description" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "Go to discussions" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Join Penpot team collaborative communication forum." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"You can ask and answer questions, have open-ended conversations, and follow " -"along on decisions affecting the project." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "Team discussions" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "Subject" @@ -795,6 +777,18 @@ msgstr "" msgid "feedback.title" msgstr "Email" +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-go-to" +msgstr "Go to Twitter" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-subtitle1" +msgstr "Here to help with your technical queries." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-title" +msgstr "Twitter support account" + #: src/app/main/ui/settings/password.cljs msgid "generic.error" msgstr "An error has occurred" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 5a2e7c699..c99409728 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -606,8 +606,7 @@ msgstr "Resultados de búsqueda" msgid "dashboard.type-something" msgstr "Escribe algo para buscar" -#: src/app/main/ui/settings/password.cljs, -#: src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Actualizar opciones" @@ -837,6 +836,18 @@ msgstr "" msgid "feedback.title" msgstr "Correo electrónico" +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-go-to" +msgstr "Ir a Twitter" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-subtitle1" +msgstr "Cuenta habilitada para responder todas tus dudas técnicas." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.twitter-title" +msgstr "Cuenta de Twitter para soporte" + #: src/app/main/ui/settings/password.cljs msgid "generic.error" msgstr "Ha ocurrido un error" @@ -3168,18 +3179,15 @@ msgstr[1] "Exportar %s elementos" msgid "workspace.options.export.suffix" msgstr "Sufijo" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-complete" msgstr "Exportación completa" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object" msgstr "Exportando…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Exportación fallida" @@ -4326,4 +4334,4 @@ msgid "workspace.updates.update" msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" -msgstr "Pulsar para cerrar la ruta" \ No newline at end of file +msgstr "Pulsar para cerrar la ruta" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 75d6d2383..75778b523 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -657,24 +657,6 @@ msgstr "Envie de parler? Discutez avec nous sur Gitter" msgid "feedback.description" msgstr "Description" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "Aller aux discussions" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Rejoignez le forum de communication collaborative de l'équipe Penpot." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Vous pouvez poser des questions et y répondre, avoir des conversations " -"ouvertes et suivre les décisions affectant le projet." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "Discussions en équipe" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "Sujet" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index c7848c8d5..ca7d6b7a3 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -665,8 +665,7 @@ msgstr "כתובת הדוא״ל לאימות חייבת להיות תואמת" msgid "errors.email-spam-or-permanent-bounces" msgstr "כתובת הדוא״ל „%s” דווחה כספאם או שההודעות תוקפצנה לצמיתות." -#: src/app/main/ui/auth/verify_token.cljs, -#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: 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 "קרה משהו לא טוב." @@ -768,24 +767,6 @@ msgstr "מעניין אותך לדבר? מזמינים אותו ל־Gitter" msgid "feedback.description" msgstr "תיאור" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "מעבר לדיונים" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "מזמינים אותך להצטרף לפורום התקשורת של Penpot." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"ניתן לשאול ולענות על שאלות, לנהל דיונים פתוחים ולעקוב אחר החלטות שמשפיעות " -"על המיזם." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "דיונים צוותיים" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "נושא" @@ -1206,8 +1187,7 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "שגיאה פנימית" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, -#: src/app/main/ui/dashboard/sidebar.cljs +#: 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.invitations" msgstr "הזמנות" @@ -1229,11 +1209,11 @@ msgstr "יציאה" msgid "labels.manage-fonts" msgstr "ניהול גופנים" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/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.member" msgstr "חבר" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/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.members" msgstr "חברים" @@ -1349,9 +1329,7 @@ msgstr "הסרה" msgid "labels.remove-member" msgstr "הסרת חבר" -#: src/app/main/ui/dashboard/sidebar.cljs, -#: src/app/main/ui/dashboard/project_menu.cljs, -#: src/app/main/ui/dashboard/file_menu.cljs +#: 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 "שינוי שם" @@ -1363,7 +1341,7 @@ msgstr "שינוי שם לצוות" msgid "labels.resend-invitation" msgstr "שליחת ההזמנה מחדש" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "ניסיון חוזר" @@ -1455,7 +1433,7 @@ msgstr "סביבת עבודה" msgid "labels.write-new-comment" msgstr "כתיבת הערה חדשה" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/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.you" msgstr "(אני)" @@ -2277,8 +2255,7 @@ msgstr "מיקוד פעיל" msgid "workspace.focus.selection" msgstr "בחירה" -#: src/app/main/data/workspace/libraries.cljs, -#: src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "מדרג קווי" @@ -2648,13 +2625,11 @@ msgstr "עיצוב" msgid "workspace.options.export" msgstr "ייצוא" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-multiple" msgstr "ייצוא הבחירה" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs #, fuzzy msgid "workspace.options.export-object" msgstr "ייצוא רכיב" @@ -2668,18 +2643,15 @@ msgstr "סיומת" msgid "workspace.options.exporting-complete" msgstr "הייצוא הושלם" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs msgid "workspace.options.exporting-object" msgstr "מתבצע ייצוא…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "הייצוא נכשל" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "הייצוא אטי בהגזמה" @@ -3123,8 +3095,7 @@ msgstr "פינות בודדות" msgid "workspace.options.recent-fonts" msgstr "אחרונים" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "לנסות שוב" @@ -3634,8 +3605,7 @@ msgstr "צורות" msgid "workspace.sidebar.layers.texts" msgstr "טקסטים" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, -#: src/app/main/ui/handoff/attributes/svg.cljs +#: 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 "מאפייני SVG יובאו" @@ -3823,4 +3793,4 @@ msgid "workspace.updates.update" msgstr "עדכון" msgid "workspace.viewport.click-to-close-path" -msgstr "לחיצה תסגור את הנתיב" \ No newline at end of file +msgstr "לחיצה תסגור את הנתיב" diff --git a/frontend/translations/nb_NO.po b/frontend/translations/nb_NO.po index f7a099ad9..8d7b330e0 100644 --- a/frontend/translations/nb_NO.po +++ b/frontend/translations/nb_NO.po @@ -235,14 +235,6 @@ msgstr "Ta del i sludringen" 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" diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index b029f171d..ce1c152f2 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -546,24 +546,6 @@ msgstr "Com vontade de falar? Converse conosco no Gitter" 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/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Junte-se ao fórum de comunicação colaborativa da equipe da Penpot." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Você pode fazer e responder perguntas, ter conversas abertas e acompanhar " -"as decisões que afetam o projeto." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "Discussões da equipe" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "Assunto" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 47ec0003b..f0447a8d4 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -537,24 +537,6 @@ msgstr "Te simți sociabil? Hai să vorbim pe Gitter" 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" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 7ed461dcf..605a3c69a 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -720,25 +720,6 @@ msgstr "Поговорим? Заходите в чат Gitter!" msgid "feedback.description" msgstr "Подробности" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "Зайти в обсуждения" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "Посетите форум для совместного обсуждения планов на Penpot." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Там вы можете задавать вопросы, проследить за неоднозначными задачами, и " -"поучаствовать в принятии решений по проекту." - -#: src/app/main/ui/settings/feedback.cljs -#, fuzzy -msgid "feedback.discussions-title" -msgstr "Командный форум" - #: src/app/main/ui/settings/feedback.cljs #, fuzzy msgid "feedback.subject" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 3591e38dd..e777f23dd 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -682,8 +682,7 @@ msgstr "Doğrulama e-postası eşleşmiyor" msgid "errors.email-spam-or-permanent-bounces" msgstr "«%s» e-postasının spam veya kalıcı olarak geri döndüğü bildirildi." -#: src/app/main/ui/auth/verify_token.cljs, -#: src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +#: 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 "Bir şeyler ters gitti." @@ -789,24 +788,6 @@ msgstr "Sohbet etmek ister misin? Glitter'da bizimle sohbet edebilirsin" 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-subtitle1" -msgstr "Penpot takımı ortak iletişim forumuna katıl." - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "" -"Soru sorabilir ve soruları cevaplayabilir, açık uçlu tartışmalar yapabilir " -"ve projeyi etkileyen kararları takip edebilirsin." - -#: 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" @@ -1230,8 +1211,7 @@ msgstr "" msgid "labels.internal-error.main-message" msgstr "İç Hata" -#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, -#: src/app/main/ui/dashboard/sidebar.cljs +#: 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.invitations" msgstr "Davetler" @@ -1253,11 +1233,11 @@ msgstr "Oturumu kapat" msgid "labels.manage-fonts" msgstr "Yazı tiplerini yönet" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/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.member" msgstr "Üye" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/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.members" msgstr "Üyeler" @@ -1369,9 +1349,7 @@ msgstr "Kaldır" msgid "labels.remove-member" msgstr "Üyeyi 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 +#: 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" @@ -1383,7 +1361,7 @@ msgstr "Takımı yeniden adlandır" msgid "labels.resend-invitation" msgstr "Daveti yeniden gönder" -#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Yeniden dene" @@ -1476,7 +1454,7 @@ msgstr "Çalışma alanı" msgid "labels.write-new-comment" msgstr "Yeni yorum yaz" -#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/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.you" msgstr "(siz)" @@ -2324,8 +2302,7 @@ msgstr "Odaklanma açık" msgid "workspace.focus.selection" msgstr "Seçim" -#: src/app/main/data/workspace/libraries.cljs, -#: src/app/main/ui/components/color_bullet.cljs +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Doğrusal degrade" @@ -2695,6 +2672,15 @@ msgstr "Tasarım" msgid "workspace.options.export" msgstr "Dışa aktar" +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export-multiple" +msgstr "Seçimi dışa aktar" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +#, fuzzy +msgid "workspace.options.export-object" +msgstr "Dışa aktar" + #: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, #: src/app/main/ui/handoff/exports.cljs msgid "workspace.options.export-multiple" @@ -2720,13 +2706,11 @@ msgstr "Dışa aktarma tamamlandı" msgid "workspace.options.exporting-object" msgstr "Dışa aktarılıyor…" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-error" msgstr "Dışa aktarılamadı" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, #: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.exporting-object-slow" msgstr "Dışa aktarma beklenmedik şekilde yavaş" @@ -3170,8 +3154,7 @@ msgstr "Tek köşe" msgid "workspace.options.recent-fonts" msgstr "Son kullanılanlar" -#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, -#: src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs, src/app/main/ui/workspace/header.cljs msgid "workspace.options.retry" msgstr "Yeniden dene" @@ -3683,8 +3666,7 @@ msgstr "Şekiller" msgid "workspace.sidebar.layers.texts" msgstr "Metinler" -#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, -#: src/app/main/ui/handoff/attributes/svg.cljs +#: 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 "İçe Aktarılan SVG Öznitelikleri" @@ -3872,4 +3854,4 @@ msgid "workspace.updates.update" msgstr "Güncelle" msgid "workspace.viewport.click-to-close-path" -msgstr "Yolu kapatmak için tıklayın" \ No newline at end of file +msgstr "Yolu kapatmak için tıklayın" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 1d2d0cbc0..219906d33 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -715,22 +715,6 @@ msgstr "想说两句?来Gitter和我们聊聊" msgid "feedback.description" msgstr "描述" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "前往讨论" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "加入Penpot团队协作交流论坛。" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle2" -msgstr "你可以提问、回答问题,来一场开放的对话,并对影响项目的决策保持关注。" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "团队讨论" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" msgstr "话题" diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po index 1c09f5ae2..eade57f69 100644 --- a/frontend/translations/zh_Hant.po +++ b/frontend/translations/zh_Hant.po @@ -607,18 +607,6 @@ msgstr "加入聊天" msgid "feedback.description" msgstr "狀況描述" -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-go-to" -msgstr "前往討論" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-subtitle1" -msgstr "加入 Penpot 團隊的協作溝通論壇。" - -#: src/app/main/ui/settings/feedback.cljs -msgid "feedback.discussions-title" -msgstr "團隊討論" - #: src/app/main/ui/settings/feedback.cljs msgid "feedback.title" msgstr "電子郵件"