diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index d1da43c97..ff24bed62 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -38,6 +38,11 @@ ;; --- FEATURES +(defn resolve-public-uri + [media-id] + (when media-id + (str (cf/get :public-uri) "/assets/by-id/" media-id))) + (def supported-features #{"storage/objects-map" "storage/pointer-map" @@ -413,15 +418,23 @@ f.modified_at, f.name, f.revn, - f.is_shared + f.is_shared, + ft.media_id from file as f + left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) where f.project_id = ? and f.deleted_at is null order by f.modified_at desc") (defn get-project-files [conn project-id] - (db/exec! conn [sql:project-files project-id])) + (->> (db/exec! conn [sql:project-files project-id]) + (mapv (fn [row] + (if-let [media-id (:media-id row)] + (-> row + (dissoc :media-id) + (assoc :thumbnail-uri (resolve-public-uri media-id))) + (dissoc row :media-id)))))) (sv/defmethod ::get-project-files "Get all files for the specified project." @@ -668,9 +681,11 @@ f.modified_at, f.name, f.is_shared, + ft.media_id, row_number() over w as row_num from file as f - join project as p on (p.id = f.project_id) + inner join project as p on (p.id = f.project_id) + left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) where p.team_id = ? and p.deleted_at is null and f.deleted_at is null @@ -681,7 +696,13 @@ (defn get-team-recent-files [conn team-id] - (db/exec! conn [sql:team-recent-files team-id])) + (->> (db/exec! conn [sql:team-recent-files team-id]) + (mapv (fn [row] + (if-let [media-id (:media-id row)] + (-> row + (dissoc :media-id) + (assoc :thumbnail-uri (resolve-public-uri media-id))) + (dissoc row :media-id)))))) (s/def ::get-team-recent-files (s/keys :req [::rpc/profile-id] diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 8a1f5cb72..9b54efe68 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -14,7 +14,6 @@ [app.common.schema :as sm] [app.common.spec :as us] [app.common.types.shape-tree :as ctt] - [app.config :as cf] [app.db :as db] [app.db.sql :as sql] [app.loggers.audit :as-alias audit] @@ -39,10 +38,6 @@ ;; --- COMMAND QUERY: get-file-object-thumbnails -(defn- get-public-uri - [media-id] - (str (cf/get :public-uri) "/assets/by-id/" media-id)) - (defn- get-object-thumbnails ([conn file-id] (let [sql (str/concat @@ -52,7 +47,7 @@ res (db/exec! conn [sql file-id])] (->> res (d/index-by :object-id (fn [row] - (or (some-> row :media-id get-public-uri) + (or (some-> row :media-id files/resolve-public-uri) (:data row)))) (d/without-nils)))) @@ -65,7 +60,7 @@ res (db/exec! conn [sql file-id ids])] (d/index-by :object-id (fn [row] - (or (some-> row :media-id get-public-uri) + (or (some-> row :media-id files/resolve-public-uri) (:data row))) res)))) @@ -85,8 +80,6 @@ ;; --- COMMAND QUERY: get-file-thumbnail -;; FIXME: refactor to support uploading data to storage - (defn get-file-thumbnail [conn file-id revn] (let [sql (sql/select :file-thumbnail @@ -95,10 +88,15 @@ {:limit 1 :order-by [[:revn :desc]]}) row (db/exec-one! conn sql)] + (when-not row (ex/raise :type :not-found :code :file-thumbnail-not-found)) + (when-not (:data row) + (ex/raise :type :not-found + :code :file-thumbnail-not-found)) + {:data (:data row) :props (some-> (:props row) db/decode-transit-pgobject) :revn (:revn row) @@ -113,20 +111,16 @@ :opt-un [::revn])) (sv/defmethod ::get-file-thumbnail - "Method used in frontend for obtain the file thumbnail (used in the - dashboard)." - {::doc/added "1.17"} + {::doc/added "1.17" + ::doc/deprecated "1.19"} [{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}] (dm/with-open [conn (db/open pool)] (files/check-read-permissions! conn profile-id file-id) (-> (get-file-thumbnail conn file-id revn) (rph/with-http-cache long-cache-duration)))) - ;; --- COMMAND QUERY: get-file-data-for-thumbnail -;; FIXME: performance issue, handle new media_id -;; ;; We need to improve how we set frame for thumbnail in order to avoid ;; loading all pages into memory for find the frame set for thumbnail. @@ -310,14 +304,17 @@ (:id media) (:id media)]))) -(s/def ::media (s/nilable ::media/upload)) -(s/def ::create-file-object-thumbnail - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::object-id ::media])) +(def schema:create-file-object-thumbnail + [:map {:title "create-file-object-thumbnail"} + [:file-id ::sm/uuid] + [:object-id :string] + [:media ::media/upload]]) (sv/defmethod ::create-file-object-thumbnail {:doc/added "1.19" - ::audit/skip true} + ::audit/skip true + ::sm/params schema:create-file-object-thumbnail} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) @@ -380,7 +377,6 @@ (db/exec-one! conn [sql:upsert-file-thumbnail file-id revn data props data props]))) - (s/def ::revn ::us/integer) (s/def ::props map?) @@ -427,24 +423,27 @@ :bucket "file-thumbnail"})] (db/exec-one! conn [sql:create-file-thumbnail file-id revn (:id media) props - (:id media) props]))) - -(s/def ::media ::media/upload) -(s/def ::create-file-thumbnail - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::revn ::props ::media])) + (:id media) props]) + media)) (sv/defmethod ::create-file-thumbnail "Creates or updates the file thumbnail. Mainly used for paint the grid thumbnails." {::doc/added "1.19" - ::audit/skip true} + ::audit/skip true + ::sm/params [:map {:title "create-file-thumbnail"} + [:file-id ::sm/uuid] + [:revn :int] + [:media ::media/upload]] + } + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (when-not (db/read-only? conn) - (-> cfg - (update ::sto/storage media/configure-assets-storage) - (assoc ::db/conn conn) - (create-file-thumbnail! params)) - nil))) + (let [media (-> cfg + (update ::sto/storage media/configure-assets-storage) + (assoc ::db/conn conn) + (create-file-thumbnail! params))] + + {:uri (files/resolve-public-uri (:id media))})))) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 9eceac13d..9b9c2134a 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -184,7 +184,7 @@ (when (seq res) (doseq [media-id (into #{} (keep :media-id) res)] ;; Mark as deleted the storage object related with the - ;; photo-id field. + ;; media-id field. (l/trace :hint "mark storage object as deleted" :id media-id) (sto/del-object! storage media-id)) diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index 84687d04f..14b0f72da 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -141,7 +141,7 @@ ))) -(t/deftest upsert-file-thumbnail +(t/deftest create-file-thumbnail (let [storage (::sto/storage th/*system*) profile (th/create-profile* 1) file (th/create-file* 1 {:profile-id (:id profile) @@ -159,7 +159,6 @@ data2 {::th/type :create-file-thumbnail ::rpc/profile-id (:id profile) :file-id (:id file) - :props {} :revn 2 :media {:filename "sample.jpg" :size 7923 @@ -169,7 +168,6 @@ data3 {::th/type :create-file-thumbnail ::rpc/profile-id (:id profile) :file-id (:id file) - :props {} :revn 3 :media {:filename "sample.jpg" :size 312043 @@ -183,11 +181,11 @@ (let [out (th/command! data2)] ;; (th/print-result! out) (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) + (t/is (contains? (:result out) :uri))) (let [out (th/command! data3)] (t/is (nil? (:error out))) - (t/is (nil? (:result out)))) + (t/is (contains? (:result out) :uri))) (let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail {:file-id (:id file)} diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index f07eba915..2083b09df 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -131,7 +131,8 @@ function readManifest() { "polyfills": "js/polyfills.js", "main": "js/main.js", "shared": "js/shared.js", - "worker": "js/worker.js" + "worker": "js/worker.js", + "thumbnail-renderer": "js/thumbnail-renderer.js" }; } } @@ -242,7 +243,17 @@ gulp.task("template:render", templatePipeline({ output: paths.output })); -gulp.task("templates", gulp.series("svg:sprite:icons", "svg:sprite:cursors", "template:main", "template:render")); +gulp.task("template:thumbnail-renderer", templatePipeline({ + name: "thumbnail-renderer.html", + input: paths.resources + "templates/thumbnail-renderer.mustache", + output: paths.output +})); + +gulp.task("templates", gulp.series("svg:sprite:icons", + "svg:sprite:cursors", + "template:main", + "template:render", + "template:thumbnail-renderer")); gulp.task("polyfills", function() { return gulp.src(paths.resources + "polyfills/*.js") diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index 0ee252325..8f9747056 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -51,6 +51,10 @@ border-radius: $br3; border: 2px solid lighten($color-gray-20, 15%); text-align: initial; + + img { + object-fit: contain; + } } &.dragged { diff --git a/frontend/resources/templates/thumbnail-renderer.mustache b/frontend/resources/templates/thumbnail-renderer.mustache new file mode 100644 index 000000000..261cd05c0 --- /dev/null +++ b/frontend/resources/templates/thumbnail-renderer.mustache @@ -0,0 +1,26 @@ + + + + + Penpot - Thumbnail Renderer + + + + + {{# manifest}} + + + + {{/manifest}} + + + + {{# manifest}} + + + {{/manifest}} + + diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index bb641ad0c..ea6faa935 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -16,17 +16,25 @@ :modules {:shared {:entries []} - :main {:entries [app.main] - :depends-on #{:shared} - :init-fn app.main/init} + :main + {:entries [app.main] + :depends-on #{:shared} + :init-fn app.main/init} - :render {:entries [app.render] - :depends-on #{:shared} - :init-fn app.render/init} + :render + {:entries [app.render] + :depends-on #{:shared} + :init-fn app.render/init} - :worker {:entries [app.worker] - :web-worker true - :depends-on #{:shared}}} + :worker + {:entries [app.worker] + :web-worker true + :depends-on #{:shared}} + + :thumbnail-renderer + {:entries [app.thumbnail-renderer] + :depends-on #{:shared} + :init-fn app.thumbnail-renderer/init}} :compiler-options {:output-feature-set :es2020 diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 4f5191ab0..70a7e23d3 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -79,21 +79,21 @@ "unknown" date))) + ;; --- Globar Config Vars (def default-theme "default") (def default-language "en") -(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def translations (obj/get global "penpotTranslations")) (def themes (obj/get global "penpotThemes")) (def build-date (parse-build-date global)) -(def flags (atom (parse-flags global))) -(def version (atom (parse-version global))) -(def target (atom (parse-target global))) -(def browser (atom (parse-browser))) -(def platform (atom (parse-platform))) +(def flags (parse-flags global)) +(def version (parse-version global)) +(def target (parse-target global)) +(def browser (parse-browser)) +(def platform (parse-platform)) (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil)) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil)) @@ -108,36 +108,43 @@ (update :path #(str % "/"))))) (def public-uri - (atom - (normalize-uri (or (obj/get global "penpotPublicURI") - (.-origin ^js location))))) + (normalize-uri (or (obj/get global "penpotPublicURI") + (obj/get location "origin")))) + +(def thumbnail-renderer-uri + (or (some-> (obj/get global "penpotThumbnailRendererURI") normalize-uri) + public-uri)) + +(def worker-uri + (obj/get global "penpotWorkerURI" "/js/worker.js")) ;; --- Helper Functions (defn ^boolean check-browser? [candidate] (dm/assert! (contains? valid-browsers candidate)) - (= candidate @browser)) + (= candidate browser)) (defn ^boolean check-platform? [candidate] (dm/assert! (contains? valid-platforms candidate)) - (= candidate @platform)) + (= candidate platform)) (defn resolve-profile-photo-url [{:keys [photo-id fullname name] :as profile}] (if (nil? photo-id) (avatars/generate {:name (or fullname name)}) - (str (u/join @public-uri "assets/by-id/" photo-id)))) + (dm/str (u/join public-uri "assets/by-id/" photo-id)))) (defn resolve-team-photo-url [{:keys [photo-id name] :as team}] (if (nil? photo-id) (avatars/generate {:name name}) - (str (u/join @public-uri "assets/by-id/" photo-id)))) + (dm/str (u/join public-uri "assets/by-id/" photo-id)))) (defn resolve-file-media ([media] (resolve-file-media media false)) ([{:keys [id] :as media} thumbnail?] - (str (cond-> (u/join @public-uri "assets/by-file-media-id/") - (true? thumbnail?) (u/join (str id "/thumbnail")) - (false? thumbnail?) (u/join (str id)))))) + (dm/str + (cond-> (u/join public-uri "assets/by-file-media-id/") + (true? thumbnail?) (u/join (dm/str id "/thumbnail")) + (false? thumbnail?) (u/join (dm/str id)))))) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 362ca0a58..33e019b99 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -6,6 +6,7 @@ (ns app.main (:require + [app.common.data.macros :as dm] [app.common.logging :as log] [app.common.uuid :as uuid] [app.config :as cf] @@ -15,6 +16,7 @@ [app.main.errors] [app.main.features :as feat] [app.main.store :as st] + [app.main.thumbnail-renderer :as tr] [app.main.ui :as ui] [app.main.ui.alert] [app.main.ui.confirm] @@ -34,12 +36,12 @@ (log/setup! {:app :info}) -(when (= :browser @cf/target) +(when (= :browser cf/target) (log/info :message "Welcome to penpot" - :version (:full @cf/version) + :version (:full cf/version) :asserts *assert* :build-date cf/build-date - :public-uri (str @cf/public-uri))) + :public-uri (dm/str cf/public-uri))) (declare reinit) @@ -80,6 +82,7 @@ (i18n/init! cf/translations) (theme/init! cf/themes) (cur/init-styles) + (tr/init!) (init-ui) (st/emit! (initialize))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index fdf4224cf..4b6134665 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -475,7 +475,7 @@ (rx/map (fn [params] (rt/resolve router :auth-verify-token {} params))) (rx/map (fn [fragment] - (assoc @cf/public-uri :fragment fragment))) + (assoc cf/public-uri :fragment fragment))) (rx/tap (fn [uri] (wapi/write-to-clipboard (str uri)))) (rx/tap on-success) @@ -782,6 +782,15 @@ (->> (rp/cmd! :set-file-shared params) (rx/ignore)))))) +(defn set-file-thumbnail + [file-id thumbnail-uri] + (ptk/reify ::set-file-thumbnail + ptk/UpdateEvent + (update [_ state] + (-> state + (d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri) + (d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri))))) + ;; --- EVENT: create-file (declare file-created) diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index e4644d875..86d330859 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -39,7 +39,7 @@ [] (let [uagent (UAParser.)] (merge - {:app-version (:full @cf/version) + {:app-version (:full cf/version) :locale @i18n/locale} (let [browser (.getBrowser uagent)] {:browser (obj/get browser "name") @@ -215,7 +215,7 @@ (defn- persist-events [events] (if (seq events) - (let [uri (u/join @cf/public-uri "api/rpc/command/push-audit-events") + (let [uri (u/join cf/public-uri "api/rpc/command/push-audit-events") params {:uri uri :method :post :credentials "include" @@ -230,7 +230,7 @@ (defn initialize [] - (when (contains? @cf/flags :audit-log) + (when (contains? cf/flags :audit-log) (ptk/reify ::initialize ptk/EffectEvent (effect [_ _ stream] diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 5639d7580..9e042aa9b 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -377,7 +377,7 @@ (ptk/reify ::mark-onboarding-as-viewed ptk/WatchEvent (watch [_ _ _] - (let [version (or version (:main @cf/version)) + (let [version (or version (:main cf/version)) props {:onboarding-viewed true :release-notes-viewed version}] (->> (rp/cmd! :update-profile-props {:props props}) diff --git a/frontend/src/app/main/data/websocket.cljs b/frontend/src/app/main/data/websocket.cljs index 7b70ef8a7..7526ea8ca 100644 --- a/frontend/src/app/main/data/websocket.cljs +++ b/frontend/src/app/main/data/websocket.cljs @@ -22,7 +22,7 @@ (defn- prepare-uri [params] - (let [base (-> @cf/public-uri + (let [base (-> cf/public-uri (u/join "ws/notifications") (assoc :query (u/map->query-string params)))] (cond-> base diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index cc3332e19..4b0735c79 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -87,7 +87,7 @@ (log/trace :hint "event:initialize" :fn "features") (rx/concat ;; Enable all features set on the configuration - (->> (rx/from @cf/flags) + (->> (rx/from cf/flags) (rx/map name) (rx/map (fn [flag] (when (str/starts-with? flag "frontend-feature-") diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index a9386ca2f..96091475c 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -9,6 +9,7 @@ (:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.logging :as log] [app.common.text :as txt] [app.config :as cf] @@ -81,8 +82,12 @@ ;; FONTS LOADING ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defonce loaded (l/atom #{})) -(defonce loading (l/atom {})) +(defonce ^:dynamic loaded (l/atom #{})) +(defonce ^:dynamic loading (l/atom {})) + +;; NOTE: mainly used on worker, when you don't really need load font +;; only know if the font is needed or not +(defonce ^:dynamic loaded-hints (l/atom #{})) (defn- create-link-element [uri] @@ -148,31 +153,26 @@ ;; --- LOADER: CUSTOM -(def font-css-template +(def font-face-template "@font-face { font-family: '%(family)s'; font-style: %(style)s; font-weight: %(weight)s; font-display: block; - src: url(%(woff1-uri)s) format('woff'), - url(%(ttf-uri)s) format('ttf'), - url(%(otf-uri)s) format('otf'); + src: url(%(uri)s) format('woff'); }") (defn- asset-id->uri [asset-id] - (str (u/join @cf/public-uri "assets/by-id/" 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 + (str/fmt font-face-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))})) + :uri (asset-id->uri (::woff1-file-id variant))})) (defn- generate-custom-font-css [{:keys [family variants] :as font}] @@ -194,34 +194,35 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn ensure-loaded! - [id] - (log/debug :action "try-ensure-loaded!" :font-id id) - (if-not (exists? js/window) + ([font-id] (ensure-loaded! font-id nil)) + ([font-id variant-id] + (log/debug :action "try-ensure-loaded!" :font-id font-id :variant-id variant-id) + (if-not (exists? js/window) ;; If we are in the worker environment, we just mark it as loaded ;; without really loading it. (do - (swap! loaded conj id) - (p/resolved id)) + (swap! loaded-hints conj {:font-id font-id :font-variant-id variant-id}) + (p/resolved font-id)) - (let [font (get @fontsdb id)] + (let [font (get @fontsdb font-id)] (cond (nil? font) - (p/resolved id) + (p/resolved font-id) ;; Font already loaded, we just continue - (contains? @loaded id) - (p/resolved id) + (contains? @loaded font-id) + (p/resolved font-id) ;; Font is currently downloading. We attach the caller to the promise - (contains? @loading id) - (p/resolved (get @loading id)) + (contains? @loading font-id) + (p/resolved (get @loading font-id)) ;; First caller, we create the promise and then wait :else (let [on-load (fn [resolve] - (swap! loaded conj id) - (swap! loading dissoc id) - (resolve id)) + (swap! loaded conj font-id) + (swap! loading dissoc font-id) + (resolve font-id)) load-p (p/create (fn [resolve _] @@ -229,34 +230,27 @@ (assoc ::on-loaded (partial on-load resolve)) (load-font))))] - (swap! loading assoc id load-p) - load-p))))) + (swap! loading assoc font-id load-p) + load-p)))))) (defn ready [cb] (-> (obj/get-in js/document ["fonts" "ready"]) (p/then cb))) -(defn get-default-variant [{:keys [variants]}] - (or - (d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants) - (first variants))) +(defn get-default-variant + [{:keys [variants]}] + (or (d/seek #(or (= (:id %) "regular") + (= (:name %) "regular")) variants) + (first variants))) + +(defn get-variant + [{:keys [variants] :as font} font-variant-id] + (or (d/seek #(= (:id %) font-variant-id) variants) + (get-default-variant font))) ;; Font embedding functions -;; Template for a CSS font face - -(def font-face-template " -/* latin */ -@font-face { - font-family: '%(family)s'; - font-style: %(style)s; - font-weight: %(weight)s; - font-display: block; - src: url(%(baseurl)sfonts/%(family)s-%(suffix)s.woff) format('woff'); -} -") - (defn get-content-fonts "Extracts the fonts used by the content of a text shape" [{font-id :font-id children :children :as content}] @@ -267,38 +261,45 @@ children-font (->> children (mapv get-content-fonts))] (reduce set/union (conj children-font current-font)))) - (defn fetch-font-css "Given a font and the variant-id, retrieves the fontface CSS" [{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}] - (let [{:keys [backend family variants]} (get @fontsdb font-id)] + (let [{:keys [backend family] :as font} (get @fontsdb font-id)] (cond + (nil? font) + (rx/empty) + (= :google backend) - (let [variant (d/seek #(= (:id %) font-variant-id) variants)] + (let [variant (get-variant font font-variant-id)] (-> (generate-gfonts-url {:family family :variants [variant]}) (http/fetch-text))) (= :custom backend) - (let [variant (d/seek #(= (:id %) font-variant-id) variants) + (let [variant (get-variant font font-variant-id) result (generate-custom-font-variant-css family variant)] - (p/resolved result)) + (rx/of result)) :else - (let [{:keys [weight style suffix] :as variant} - (d/seek #(= (:id %) font-variant-id) variants) - font-data {:baseurl (str @cf/public-uri) - :family family - :style style - :suffix (or suffix font-variant-id) - :weight weight}] - (rx/of (str/fmt font-face-template font-data)))))) + (let [{:keys [weight style suffix]} (get-variant font font-variant-id) + suffix (or suffix font-variant-id) + params {:uri (dm/str cf/public-uri "fonts/" family "-" suffix ".woff") + :family family + :style style + :weight weight}] + (rx/of (str/fmt font-face-template params)))))) (defn extract-fontface-urls "Parses the CSS and retrieves the font urls" [^string css] (->> (re-seq #"url\(([^)]+)\)" css) (mapv second))) + +(defn render-font-styles + [font-refs] + (->> (rx/from font-refs) + (rx/mapcat fetch-font-css) + (rx/reduce (fn [acc css] (dm/str acc "\n" css)) ""))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index ca834ce20..348b606dc 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -50,6 +50,11 @@ :upsert-file-object-thumbnail {:query-params [:file-id :object-id]} :create-file-object-thumbnail {:query-params [:file-id :object-id] :form-data? true} + + :create-file-thumbnail + {:query-params [:file-id :revn] + :form-data? true} + :export-binfile {:response-type :blob} :import-binfile {:form-data? true} :retrieve-list-of-builtin-templates {:query-params :all} @@ -79,7 +84,7 @@ :else :post) request {:method method - :uri (u/join @cf/public-uri "api/rpc/command/" (name id)) + :uri (u/join cf/public-uri "api/rpc/command/" (name id)) :credentials "include" :headers {"accept" "application/transit+json"} :body (when (= method :post) @@ -105,7 +110,7 @@ (defmethod cmd! :login-with-oidc [_ {:keys [provider] :as params}] - (let [uri (u/join @cf/public-uri "api/auth/oauth/" (d/name provider)) + (let [uri (u/join cf/public-uri "api/auth/oauth/" (d/name provider)) params (dissoc params :provider)] (->> (http/send! {:method :post :uri uri @@ -117,7 +122,7 @@ (defn- send-export [{:keys [blob?] :as params}] (->> (http/send! {:method :post - :uri (u/join @cf/public-uri "api/export") + :uri (u/join cf/public-uri "api/export") :body (http/transit-data (dissoc params :blob?)) :credentials "include" :response-type (if blob? :blob :text)}) @@ -136,7 +141,7 @@ (defmethod cmd! ::multipart-upload [id params] (->> (http/send! {:method :post - :uri (u/join @cf/public-uri "api/rpc/command/" (name id)) + :uri (u/join cf/public-uri "api/rpc/command/" (name id)) :credentials "include" :body (http/form-data params)}) (rx/map http/conditional-decode-transit) diff --git a/frontend/src/app/main/thumbnail_renderer.cljs b/frontend/src/app/main/thumbnail_renderer.cljs new file mode 100644 index 000000000..54c668af7 --- /dev/null +++ b/frontend/src/app/main/thumbnail_renderer.cljs @@ -0,0 +1,93 @@ +;; 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) KALEIDOS INC + +(ns app.main.thumbnail-renderer + "A main entry point for the thumbnail renderer API interface. + + This ns is responsible to provide an API for create thumbnail + renderer iframes and interact with them using asyncrhonous + messages." + (:require + [app.common.data.macros :as dm] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.util.dom :as dom] + [beicon.core :as rx] + [cuerdas.core :as str])) + +(defonce ready? false) +(defonce queue #js []) +(defonce instance nil) +(defonce msgbus (rx/subject)) +(defonce origin + (dm/str (assoc cf/thumbnail-renderer-uri :path "/thumbnail-renderer.html"))) + +(declare send-message!) + +(defn- process-queued-messages! + [] + (loop [message (.shift ^js queue)] + (when (some? message) + (send-message! message) + (recur (.shift ^js queue))))) + +(defn- on-message + "Handles a message from the thumbnail renderer." + [event] + (let [evorigin (unchecked-get event "origin") + evdata (unchecked-get event "data")] + + (when (and (object? evdata) (str/starts-with? origin evorigin)) + (let [scope (unchecked-get evdata "scope") + type (unchecked-get evdata "type")] + (when (= "penpot/thumbnail-renderer" scope) + (when (= type "ready") + (set! ready? true) + (process-queued-messages!)) + (rx/push! msgbus evdata)))))) + +(defn- send-message! + "Sends a message to the thumbnail renderer." + [message] + (let [window (.-contentWindow ^js instance)] + (.postMessage ^js window message origin))) + +(defn- queue-message! + "Queues a message to be sent to the thumbnail renderer when it's ready." + [message] + (.push ^js queue message)) + +(defn render + "Renders a thumbnail." + [{:keys [data styles] :as params}] + (let [id (dm/str (uuid/next)) + payload #js {:data data :styles styles} + message #js {:id id + :scope "penpot/thumbnail-renderer" + :payload payload}] + + (if ^boolean ready? + (send-message! message) + (queue-message! message)) + + (->> msgbus + (rx/filter #(= id (unchecked-get % "id"))) + (rx/mapcat (fn [msg] + (case (unchecked-get msg "type") + "success" (rx/of (unchecked-get msg "payload")) + "failure" (rx/throw (unchecked-get msg "payload"))))) + (rx/take 1)))) + +(defn init! + "Initializes the thumbnail renderer." + [] + (let [iframe (dom/create-element "iframe")] + (dom/set-attribute! iframe "src" origin) + (dom/set-attribute! iframe "hidden" true) + (dom/append-child! js/document.body iframe) + + (set! instance iframe) + (.addEventListener js/window "message" on-message))) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 882de4b48..7528fdcd3 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -98,9 +98,9 @@ [:& app.main.ui.onboarding/onboarding-modal {}] (and (:onboarding-viewed props) - (not= (:release-notes-viewed props) (:main @cf/version)) - (not= "0.0" (:main @cf/version))) - [:& app.main.ui.releases/release-notes-modal {:version (:main @cf/version)}])) + (not= (:release-notes-viewed props) (:main cf/version)) + (not= "0.0" (:main cf/version))) + [:& app.main.ui.releases/release-notes-modal {:version (:main cf/version)}])) [:& dashboard {:route route :profile profile}]] diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index af9d206a0..cdb907ded 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -28,7 +28,7 @@ [rumext.v2 :as mf])) (def show-alt-login-buttons? - (some (partial contains? @cf/flags) + (some (partial contains? cf/flags) [:login-with-google :login-with-github :login-with-gitlab @@ -175,13 +175,13 @@ :label (tr "auth.password")}]] [:div.buttons-stack - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-password)) + (when (or (contains? cf/flags :login) + (contains? cf/flags :login-with-password)) [:& fm/submit-button {:label (tr "auth.login-submit") :data-test "login-submit"}]) - (when (contains? @cf/flags :login-with-ldap) + (when (contains? cf/flags :login-with-ldap) [:& fm/submit-button {:label (tr "auth.login-with-ldap-submit") :on-click on-submit-ldap}])]]])) @@ -189,25 +189,25 @@ (mf/defc login-buttons [{:keys [params] :as props}] [:div.auth-buttons - (when (contains? @cf/flags :login-with-google) + (when (contains? cf/flags :login-with-google) [:& bl/button-link {:action #(login-with-oidc % :google params) :icon i/brand-google :name (tr "auth.login-with-google-submit") :klass "btn-google-auth"}]) - (when (contains? @cf/flags :login-with-github) + (when (contains? cf/flags :login-with-github) [:& bl/button-link {:action #(login-with-oidc % :github params) :icon i/brand-github :name (tr "auth.login-with-github-submit") :klass "btn-github-auth"}]) - (when (contains? @cf/flags :login-with-gitlab) + (when (contains? cf/flags :login-with-gitlab) [:& bl/button-link {:action #(login-with-oidc % :gitlab params) :icon i/brand-gitlab :name (tr "auth.login-with-gitlab-submit") :klass "btn-gitlab-auth"}]) - (when (contains? @cf/flags :login-with-oidc) + (when (contains? cf/flags :login-with-oidc) [:& bl/button-link {:action #(login-with-oidc % :oidc params) :icon i/brand-openid :name (tr "auth.login-with-oidc-submit") @@ -215,7 +215,7 @@ (mf/defc login-button-oidc [{:keys [params] :as props}] - (when (contains? @cf/flags :login-with-oidc) + (when (contains? cf/flags :login-with-oidc) [:div.link-entry.link-oidc [:a {:tab-index "0" :on-key-down (fn [event] @@ -236,17 +236,17 @@ [:& login-buttons {:params params}] - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-password) - (contains? @cf/flags :login-with-ldap)) + (when (or (contains? cf/flags :login) + (contains? cf/flags :login-with-password) + (contains? cf/flags :login-with-ldap)) [:span.separator [:span.line] [:span.text (tr "labels.or")] [:span.line]])]) - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-password) - (contains? @cf/flags :login-with-ldap)) + (when (or (contains? cf/flags :login) + (contains? cf/flags :login-with-password) + (contains? cf/flags :login-with-ldap)) [:& login-form {:params params :on-success-callback on-success-callback}])]) (mf/defc login-page @@ -258,21 +258,21 @@ [:& login-methods {:params params}] [:div.links - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-password)) + (when (or (contains? cf/flags :login) + (contains? cf/flags :login-with-password)) [:div.link-entry [:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request)) :data-test "forgot-password"} (tr "auth.forgot-password")]]) - (when (contains? @cf/flags :registration) + (when (contains? cf/flags :registration) [:div.link-entry [:span (tr "auth.register") " "] [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params)) :data-test "register-submit"} (tr "auth.register-submit")]])] - (when (contains? @cf/flags :demo-users) + (when (contains? cf/flags :demo-users) [:div.links.demo [:div.link-entry [:span (tr "auth.create-demo-profile") " "] diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 114b36e0d..76ce3a1ac 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -141,8 +141,8 @@ [:& login/login-buttons {:params params}] - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-ldap)) + (when (or (contains? cf/flags :login) + (contains? cf/flags :login-with-ldap)) [:span.separator [:span.line] [:span.text (tr "labels.or")] @@ -157,7 +157,7 @@ [:h1 {:data-test "registration-title"} (tr "auth.register-title")] [:div.subtitle (tr "auth.register-subtitle")] - (when (contains? @cf/flags :demo-warning) + (when (contains? cf/flags :demo-warning) [:& demo-warning]) [:& register-methods {:params params}] @@ -170,7 +170,7 @@ :data-test "login-here-link"} (tr "auth.login-here")]] - (when (contains? @cf/flags :demo-users) + (when (contains? cf/flags :demo-users) [:div.link-entry [:span (tr "auth.create-demo-profile") " "] [:& lk/link {:action #(st/emit! (du/create-demo-profile))} @@ -207,7 +207,7 @@ (s/def ::accept-terms-and-privacy (s/and ::us/boolean true?)) (s/def ::accept-newsletter-subscription ::us/boolean) -(if (contains? @cf/flags :terms-and-privacy-checkbox) +(if (contains? cf/flags :terms-and-privacy-checkbox) (s/def ::register-validate-form (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy] :opt-un [::accept-newsletter-subscription])) @@ -244,7 +244,7 @@ :label (tr "auth.fullname") :type "text"}]] - (when (contains? @cf/flags :terms-and-privacy-checkbox) + (when (contains? cf/flags :terms-and-privacy-checkbox) [:div.fields-row.input-visible.accept-terms-and-privacy-wrapper [:& fm/input {:name :accept-terms-and-privacy :class "check-primary" diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 4384e97b5..27fc812ad 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -16,7 +16,9 @@ [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.render :refer [component-svg]] + [app.main.repo :as rp] [app.main.store :as st] + [app.main.thumbnail-renderer :as thr] [app.main.ui.components.color-bullet :as bc] [app.main.ui.dashboard.file-menu :refer [file-menu]] [app.main.ui.dashboard.import :refer [use-import-file]] @@ -30,7 +32,6 @@ [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.perf :as perf] [app.util.time :as dt] [app.util.timers :as ts] [beicon.core :as rx] @@ -41,44 +42,49 @@ ;; --- Grid Item Thumbnail -(defn ask-for-thumbnail +(defn- persist-thumbnail + [file-id revn blob] + (let [params {:file-id file-id :revn revn :media blob}] + (->> (rp/cmd! :create-file-thumbnail params) + (rx/map :uri)))) + +(defn- ask-for-thumbnail "Creates some hooks to handle the files thumbnails cache" - [file] + [file-id revn] (let [features (cond-> ffeat/enabled (features/active-feature? :components-v2) (conj "components/v2"))] - (wrk/ask! {:cmd :thumbnails/generate-for-file - :revn (:revn file) - :file-id (:id file) - :file-name (:name file) - :features features}))) + (->> (wrk/ask! {:cmd :thumbnails/generate-for-file + :revn revn + :file-id file-id + :features features}) + (rx/mapcat (fn [{:keys [fonts] :as result}] + (->> (fonts/render-font-styles fonts) + (rx/map (fn [styles] + (assoc result :styles styles)))))) + (rx/mapcat thr/render) + (rx/mapcat (partial persist-thumbnail file-id revn))))) (mf/defc grid-item-thumbnail - {::mf/wrap [mf/memo]} - [{:keys [file] :as props}] + {::mf/wrap-props false} + [{:keys [file-id revn thumbnail-uri background-color]}] (let [container (mf/use-ref) - bgcolor (dm/get-in file [:data :options :background]) visible? (h/use-visible container :once? true)] - (mf/with-effect [file visible?] - (when visible? - (let [tp (perf/tpoint)] - (->> (ask-for-thumbnail file) - (rx/subscribe-on :af) - (rx/subs (fn [{:keys [data fonts] :as params}] - (run! fonts/ensure-loaded! fonts) - (log/debug :hint "loaded thumbnail" - :file-id (dm/str (:id file)) - :file-name (:name file) - :elapsed (str/ffmt "%ms" (tp))) - (when-let [node (mf/ref-val container)] - (dom/set-html! node data)))))))) + (mf/with-effect [file-id revn visible? thumbnail-uri] + (when (and visible? (not thumbnail-uri)) + (->> (ask-for-thumbnail file-id revn) + (rx/subs (fn [url] + (st/emit! (dd/set-file-thumbnail file-id url))))))) [:div.grid-item-th - {:style {:background-color bgcolor} + {:style {:background-color background-color} :ref container} - i/loader-pencil])) + (when visible? + (if thumbnail-uri + [:img.grid-item-thumbnail-image {:src thumbnail-uri}] + i/loader-pencil))])) ;; --- Grid Item Library @@ -312,7 +318,12 @@ [:div.overlay] (if library-view? [:& grid-item-library {:file file}] - [:& grid-item-thumbnail {:file file}]) + [:& grid-item-thumbnail + {:file-id (:id file) + :revn (:revn file) + :thumbnail-uri (:thumbnail-uri file) + :background-color (dm/get-in file [:data :options :background])}]) + (when (and (:is-shared file) (not library-view?)) [:div.item-badge i/library]) [:div.info-wrapper diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 301a81684..a494b1b36 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -376,7 +376,7 @@ :data-test "team-invitations"} (tr "labels.invitations")] - (when (contains? @cf/flags :webhooks) + (when (contains? cf/flags :webhooks) [:& dropdown-menu-item {:on-click go-webhooks :on-key-down (fn [event] (when (kbd/enter? event) @@ -459,7 +459,7 @@ can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin])) options-ids ["teams-options-members" "teams-options-invitations" - (when (contains? @cf/flags :webhooks) + (when (contains? cf/flags :webhooks) "teams-options-webhooks") "teams-options-settings" (when can-rename? @@ -680,7 +680,7 @@ show-release-notes (mf/use-callback (fn [event] - (let [version (:main @cf/version)] + (let [version (:main cf/version)] (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (if (and (kbd/alt? event) (kbd/mod? event)) (st/emit! (modal/show {:type :onboarding})) @@ -769,7 +769,7 @@ (dom/open-new-window "https://penpot.app/terms")))} [:span (tr "auth.terms-of-service")]] - (when (contains? @cf/flags :user-feedback) + (when (contains? cf/flags :user-feedback) [:li.separator {:tab-index (if show "0" "-1") diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index e56599b83..f84f91f3f 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -38,7 +38,7 @@ [:div.modal-left.welcome [:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]] [:div.modal-right - [:div.release-container [:span.release "Version " (:main @cf/version)]] + [:div.release-container [:span.release "Version " (:main cf/version)]] [:div.right-content [:div.modal-title [:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]] @@ -73,7 +73,7 @@ [:div.modal-left.welcome [:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]] [:div.modal-right - [:div.release-container [:span.release "Version " (:main @cf/version)]] + [:div.release-container [:span.release "Version " (:main cf/version)]] [:div.right-content [:div.modal-title [:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]] @@ -112,7 +112,7 @@ skip (mf/use-fn #(st/emit! (modal/hide) - (if (contains? @cf/flags :newsletter-subscription) + (if (contains? cf/flags :newsletter-subscription) (modal/show {:type :onboarding-newsletter-modal}) (modal/show {:type :onboarding-team})) (du/mark-onboarding-as-viewed)))] diff --git a/frontend/src/app/main/ui/onboarding/templates.cljs b/frontend/src/app/main/ui/onboarding/templates.cljs index a958af807..01ed35345 100644 --- a/frontend/src/app/main/ui/onboarding/templates.cljs +++ b/frontend/src/app/main/ui/onboarding/templates.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.onboarding.templates (:require + [app.common.data.macros :as dm] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.modal :as modal] @@ -21,7 +22,7 @@ (mf/defc template-item [{:keys [name path image project-id]}] (let [downloading? (mf/use-state false) - link (str (assoc @cf/public-uri :path path)) + link (dm/str (assoc cf/public-uri :path path)) on-finish-import (fn [] diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index f2adb5712..8c077cb61 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -56,11 +56,11 @@ (mf/use-callback (mf/deps profile) #(st/emit! (rt/nav :settings-access-tokens))) - + show-release-notes (mf/use-callback (fn [event] - (let [version (:main @cf/version)] + (let [version (:main cf/version)] (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (if (and (kbd/alt? event) (kbd/mod? event)) (st/emit! (modal/show {:type :onboarding})) @@ -91,7 +91,7 @@ i/tree [:span.element-title (tr "labels.settings")]] - (when (contains? @cf/flags :access-tokens) + (when (contains? cf/flags :access-tokens) [:li {:class (when access-tokens? "current") :on-click go-settings-access-tokens :data-test "settings-access-tokens"} @@ -104,7 +104,7 @@ i/pencil [:span.element-title (tr "labels.release-notes")]] - (when (contains? @cf/flags :user-feedback) + (when (contains? cf/flags :user-feedback) [:li {:class (when feedback? "current") :on-click go-settings-feedback} i/msg-info diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 9dda49dad..89802ca81 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -16,10 +16,13 @@ (defn- load-fonts! [content] - (let [default (:font-id txt/default-text-attrs)] + (let [extract-fn (juxt :font-id :font-variant-id) + default (extract-fn txt/default-text-attrs)] (->> (tree-seq map? :children content) - (into #{default} (keep :font-id)) - (run! fonts/ensure-loaded!)))) + (into #{default} (keep extract-fn)) + (run! (fn [[font-id variant-id]] + (when (some? font-id) + (fonts/ensure-loaded! font-id variant-id))))))) (mf/defc text-shape {::mf/wrap-props false} diff --git a/frontend/src/app/main/ui/viewer/share_link.cljs b/frontend/src/app/main/ui/viewer/share_link.cljs index 4ab098fc4..8b0e69685 100644 --- a/frontend/src/app/main/ui/viewer/share_link.cljs +++ b/frontend/src/app/main/ui/viewer/share_link.cljs @@ -145,7 +145,7 @@ (assoc qparams :zoom zoom-type)) href (rt/resolve router :viewer pparams qparams)] - (assoc @cf/public-uri :fragment href)))] + (assoc cf/public-uri :fragment href)))] (reset! link (some-> href str))))) [:div.modal-overlay.transparent.share-modal diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 25ef1a9cd..918238947 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -158,7 +158,7 @@ show-release-notes (mf/use-fn (fn [event] - (let [version (:main @cf/version)] + (let [version (:main cf/version)] (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (if (and (kbd/alt? event) (kbd/mod? event)) (st/emit! (modal/show {:type :onboarding})) @@ -186,7 +186,7 @@ [:span (tr "label.shortcuts")] [:span.shortcut (sc/get-tooltip :show-shortcuts)]] - (when (contains? @cf/flags :user-feedback) + (when (contains? cf/flags :user-feedback) [:* [:li.feedback {:on-click nav-to-feedback} [:span (tr "labels.give-feedback")]]])]])) diff --git a/frontend/src/app/main/worker.cljs b/frontend/src/app/main/worker.cljs index ee5f399f3..c40ad0e46 100644 --- a/frontend/src/app/main/worker.cljs +++ b/frontend/src/app/main/worker.cljs @@ -10,35 +10,30 @@ [app.main.errors :as err] [app.util.worker :as uw])) -(defonce instance (atom nil)) - -(defn- update-public-uri! - [instance val] - (uw/ask! instance {:cmd :configure - :key :public-uri - :val val})) +(defonce instance nil) (defn init! [] (let [worker (uw/init cf/worker-uri err/on-error)] - (update-public-uri! worker @cf/public-uri) - (add-watch cf/public-uri ::worker-public-uri (fn [_ _ _ val] (update-public-uri! worker val))) - (reset! instance worker))) + (uw/ask! worker {:cmd :configure + :key :public-uri + :val cf/public-uri}) + (set! instance worker))) (defn ask! ([message] - (when @instance (uw/ask! @instance message))) + (when instance (uw/ask! instance message))) ([message transfer] - (when @instance (uw/ask! @instance message transfer)))) + (when instance (uw/ask! instance message transfer)))) (defn ask-buffered! ([message] - (when @instance (uw/ask-buffered! @instance message))) + (when instance (uw/ask-buffered! instance message))) ([message transfer] - (when @instance (uw/ask-buffered! @instance message transfer)))) + (when instance (uw/ask-buffered! instance message transfer)))) (defn ask-many! ([message] - (when @instance (uw/ask-many! @instance message))) + (when instance (uw/ask-many! instance message))) ([message transfer] - (when @instance (uw/ask-many! @instance message transfer)))) + (when instance (uw/ask-many! instance message transfer)))) diff --git a/frontend/src/app/thumbnail_renderer.cljs b/frontend/src/app/thumbnail_renderer.cljs new file mode 100644 index 000000000..aa6dab25a --- /dev/null +++ b/frontend/src/app/thumbnail_renderer.cljs @@ -0,0 +1,246 @@ +;; 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) KALEIDOS INC + +(ns app.thumbnail-renderer + "A main entry point for the thumbnail renderer process that is + executed on a separated iframe." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.logging :as log] + [app.config :as cf] + [app.util.dom :as dom] + [app.util.http :as http] + [app.util.object :as obj] + [app.util.webapi :as wapi] + [beicon.core :as rx] + [cuerdas.core :as str])) + +(log/set-level! :trace) + +(declare send-success!) +(declare send-failure!) + +(defonce parent-origin + (dm/str cf/public-uri)) + +(defn- get-document-element + [^js svg] + (.-documentElement svg)) + +(defn- create-image + [uri] + (rx/create + (fn [subs] + (let [image (js/Image.)] + (obj/set! image "onload" #(do + (rx/push! subs image) + (rx/end! subs))) + + (obj/set! image "crossOrigin" "anonymous") + (obj/set! image "onerror" #(rx/error! subs %)) + (obj/set! image "onabort" #(rx/error! subs (ex/error :type :internal + :code :abort + :hint "operation aborted"))) + (obj/set! image "src" uri) + (fn [] + (obj/set! image "src" "") + (obj/set! image "onload" nil) + (obj/set! image "onerror" nil) + (obj/set! image "onabort" nil)))))) + +(defn- svg-get-size + [svg max] + (let [doc (get-document-element svg) + vbox (dom/get-attribute doc "viewBox")] + (when (string? vbox) + (let [[_ _ width height] (str/split vbox #"\s+") + width (d/parse-integer width 0) + height (d/parse-integer height 0) + ratio (/ width height)] + (if (> width height) + [max (* max (/ 1 ratio))] + [(* max ratio) max]))))) + +(defn- svg-has-intrinsic-size? + "Returns true if the SVG has an intrinsic size." + [svg] + (let [doc (get-document-element svg) + width (dom/get-attribute doc "width") + height (dom/get-attribute doc "height")] + (d/num? width height))) + +(defn- svg-set-intrinsic-size! + "Sets the intrinsic size of an SVG to the given max size." + [^js svg max] + (when-not (svg-has-intrinsic-size? svg) + (let [doc (get-document-element svg) + [w h] (svg-get-size svg max)] + (dom/set-attribute! doc "width" (dm/str w)) + (dom/set-attribute! doc "height" (dm/str h)))) + svg) + +(defn- fetch-as-data-uri + "Fetches a URL as a Data URI." + [uri] + (->> (http/send! {:uri uri + :response-type :blob + :method :get + :mode :cors + :omit-default-headers true}) + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url))) + +(defn- svg-update-image! + "Updates an image in an SVG to a Data URI." + [image] + (when-let [href (dom/get-attribute image "href")] + (->> (fetch-as-data-uri href) + (rx/map (fn [url] + (dom/set-attribute! image "href" url) + image))))) + +(defn- svg-resolve-images! + "Resolves all images in an SVG to Data URIs." + [svg] + (->> (rx/from (dom/query-all svg "image")) + (rx/mapcat svg-update-image!) + (rx/ignore))) + +(defn- svg-add-style! + "Adds a