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