mirror of
https://github.com/penpot/penpot.git
synced 2025-05-16 01:16:38 +02:00
Merge pull request #3272 from penpot/azazeln28-thumbnail-renderer
🎉 Add thumbnail renderer service
This commit is contained in:
commit
74e8081574
37 changed files with 704 additions and 305 deletions
|
@ -38,6 +38,11 @@
|
||||||
|
|
||||||
;; --- FEATURES
|
;; --- FEATURES
|
||||||
|
|
||||||
|
(defn resolve-public-uri
|
||||||
|
[media-id]
|
||||||
|
(when media-id
|
||||||
|
(str (cf/get :public-uri) "/assets/by-id/" media-id)))
|
||||||
|
|
||||||
(def supported-features
|
(def supported-features
|
||||||
#{"storage/objects-map"
|
#{"storage/objects-map"
|
||||||
"storage/pointer-map"
|
"storage/pointer-map"
|
||||||
|
@ -413,15 +418,23 @@
|
||||||
f.modified_at,
|
f.modified_at,
|
||||||
f.name,
|
f.name,
|
||||||
f.revn,
|
f.revn,
|
||||||
f.is_shared
|
f.is_shared,
|
||||||
|
ft.media_id
|
||||||
from file as f
|
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 = ?
|
where f.project_id = ?
|
||||||
and f.deleted_at is null
|
and f.deleted_at is null
|
||||||
order by f.modified_at desc")
|
order by f.modified_at desc")
|
||||||
|
|
||||||
(defn get-project-files
|
(defn get-project-files
|
||||||
[conn project-id]
|
[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
|
(sv/defmethod ::get-project-files
|
||||||
"Get all files for the specified project."
|
"Get all files for the specified project."
|
||||||
|
@ -668,9 +681,11 @@
|
||||||
f.modified_at,
|
f.modified_at,
|
||||||
f.name,
|
f.name,
|
||||||
f.is_shared,
|
f.is_shared,
|
||||||
|
ft.media_id,
|
||||||
row_number() over w as row_num
|
row_number() over w as row_num
|
||||||
from file as f
|
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 = ?
|
where p.team_id = ?
|
||||||
and p.deleted_at is null
|
and p.deleted_at is null
|
||||||
and f.deleted_at is null
|
and f.deleted_at is null
|
||||||
|
@ -681,7 +696,13 @@
|
||||||
|
|
||||||
(defn get-team-recent-files
|
(defn get-team-recent-files
|
||||||
[conn team-id]
|
[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/def ::get-team-recent-files
|
||||||
(s/keys :req [::rpc/profile-id]
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.types.shape-tree :as ctt]
|
[app.common.types.shape-tree :as ctt]
|
||||||
[app.config :as cf]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
[app.loggers.audit :as-alias audit]
|
[app.loggers.audit :as-alias audit]
|
||||||
|
@ -39,10 +38,6 @@
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-object-thumbnails
|
;; --- 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
|
(defn- get-object-thumbnails
|
||||||
([conn file-id]
|
([conn file-id]
|
||||||
(let [sql (str/concat
|
(let [sql (str/concat
|
||||||
|
@ -52,7 +47,7 @@
|
||||||
res (db/exec! conn [sql file-id])]
|
res (db/exec! conn [sql file-id])]
|
||||||
(->> res
|
(->> res
|
||||||
(d/index-by :object-id (fn [row]
|
(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))))
|
(:data row))))
|
||||||
(d/without-nils))))
|
(d/without-nils))))
|
||||||
|
|
||||||
|
@ -65,7 +60,7 @@
|
||||||
res (db/exec! conn [sql file-id ids])]
|
res (db/exec! conn [sql file-id ids])]
|
||||||
(d/index-by :object-id
|
(d/index-by :object-id
|
||||||
(fn [row]
|
(fn [row]
|
||||||
(or (some-> row :media-id get-public-uri)
|
(or (some-> row :media-id files/resolve-public-uri)
|
||||||
(:data row)))
|
(:data row)))
|
||||||
res))))
|
res))))
|
||||||
|
|
||||||
|
@ -85,8 +80,6 @@
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-thumbnail
|
;; --- COMMAND QUERY: get-file-thumbnail
|
||||||
|
|
||||||
;; FIXME: refactor to support uploading data to storage
|
|
||||||
|
|
||||||
(defn get-file-thumbnail
|
(defn get-file-thumbnail
|
||||||
[conn file-id revn]
|
[conn file-id revn]
|
||||||
(let [sql (sql/select :file-thumbnail
|
(let [sql (sql/select :file-thumbnail
|
||||||
|
@ -95,10 +88,15 @@
|
||||||
{:limit 1
|
{:limit 1
|
||||||
:order-by [[:revn :desc]]})
|
:order-by [[:revn :desc]]})
|
||||||
row (db/exec-one! conn sql)]
|
row (db/exec-one! conn sql)]
|
||||||
|
|
||||||
(when-not row
|
(when-not row
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
:code :file-thumbnail-not-found))
|
:code :file-thumbnail-not-found))
|
||||||
|
|
||||||
|
(when-not (:data row)
|
||||||
|
(ex/raise :type :not-found
|
||||||
|
:code :file-thumbnail-not-found))
|
||||||
|
|
||||||
{:data (:data row)
|
{:data (:data row)
|
||||||
:props (some-> (:props row) db/decode-transit-pgobject)
|
:props (some-> (:props row) db/decode-transit-pgobject)
|
||||||
:revn (:revn row)
|
:revn (:revn row)
|
||||||
|
@ -113,20 +111,16 @@
|
||||||
:opt-un [::revn]))
|
:opt-un [::revn]))
|
||||||
|
|
||||||
(sv/defmethod ::get-file-thumbnail
|
(sv/defmethod ::get-file-thumbnail
|
||||||
"Method used in frontend for obtain the file thumbnail (used in the
|
{::doc/added "1.17"
|
||||||
dashboard)."
|
::doc/deprecated "1.19"}
|
||||||
{::doc/added "1.17"}
|
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
||||||
(dm/with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(files/check-read-permissions! conn profile-id file-id)
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
(-> (get-file-thumbnail conn file-id revn)
|
(-> (get-file-thumbnail conn file-id revn)
|
||||||
(rph/with-http-cache long-cache-duration))))
|
(rph/with-http-cache long-cache-duration))))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-data-for-thumbnail
|
;; --- 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
|
;; 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.
|
;; loading all pages into memory for find the frame set for thumbnail.
|
||||||
|
|
||||||
|
@ -310,14 +304,17 @@
|
||||||
(:id media) (:id media)])))
|
(:id media) (:id media)])))
|
||||||
|
|
||||||
|
|
||||||
(s/def ::media (s/nilable ::media/upload))
|
(def schema:create-file-object-thumbnail
|
||||||
(s/def ::create-file-object-thumbnail
|
[:map {:title "create-file-object-thumbnail"}
|
||||||
(s/keys :req [::rpc/profile-id]
|
[:file-id ::sm/uuid]
|
||||||
:req-un [::file-id ::object-id ::media]))
|
[:object-id :string]
|
||||||
|
[:media ::media/upload]])
|
||||||
|
|
||||||
(sv/defmethod ::create-file-object-thumbnail
|
(sv/defmethod ::create-file-object-thumbnail
|
||||||
{:doc/added "1.19"
|
{: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]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
|
@ -380,7 +377,6 @@
|
||||||
(db/exec-one! conn [sql:upsert-file-thumbnail
|
(db/exec-one! conn [sql:upsert-file-thumbnail
|
||||||
file-id revn data props data props])))
|
file-id revn data props data props])))
|
||||||
|
|
||||||
|
|
||||||
(s/def ::revn ::us/integer)
|
(s/def ::revn ::us/integer)
|
||||||
(s/def ::props map?)
|
(s/def ::props map?)
|
||||||
|
|
||||||
|
@ -427,24 +423,27 @@
|
||||||
:bucket "file-thumbnail"})]
|
:bucket "file-thumbnail"})]
|
||||||
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
|
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
|
||||||
(:id media) props
|
(:id media) props
|
||||||
(:id media) props])))
|
(:id media) props])
|
||||||
|
media))
|
||||||
(s/def ::media ::media/upload)
|
|
||||||
(s/def ::create-file-thumbnail
|
|
||||||
(s/keys :req [::rpc/profile-id]
|
|
||||||
:req-un [::file-id ::revn ::props ::media]))
|
|
||||||
|
|
||||||
(sv/defmethod ::create-file-thumbnail
|
(sv/defmethod ::create-file-thumbnail
|
||||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||||
grid thumbnails."
|
grid thumbnails."
|
||||||
{::doc/added "1.19"
|
{::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}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(when-not (db/read-only? conn)
|
(when-not (db/read-only? conn)
|
||||||
(-> cfg
|
(let [media (-> cfg
|
||||||
(update ::sto/storage media/configure-assets-storage)
|
(update ::sto/storage media/configure-assets-storage)
|
||||||
(assoc ::db/conn conn)
|
(assoc ::db/conn conn)
|
||||||
(create-file-thumbnail! params))
|
(create-file-thumbnail! params))]
|
||||||
nil)))
|
|
||||||
|
{:uri (files/resolve-public-uri (:id media))}))))
|
||||||
|
|
|
@ -184,7 +184,7 @@
|
||||||
(when (seq res)
|
(when (seq res)
|
||||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||||
;; Mark as deleted the storage object related with the
|
;; 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)
|
(l/trace :hint "mark storage object as deleted" :id media-id)
|
||||||
(sto/del-object! storage media-id))
|
(sto/del-object! storage media-id))
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest upsert-file-thumbnail
|
(t/deftest create-file-thumbnail
|
||||||
(let [storage (::sto/storage th/*system*)
|
(let [storage (::sto/storage th/*system*)
|
||||||
profile (th/create-profile* 1)
|
profile (th/create-profile* 1)
|
||||||
file (th/create-file* 1 {:profile-id (:id profile)
|
file (th/create-file* 1 {:profile-id (:id profile)
|
||||||
|
@ -159,7 +159,6 @@
|
||||||
data2 {::th/type :create-file-thumbnail
|
data2 {::th/type :create-file-thumbnail
|
||||||
::rpc/profile-id (:id profile)
|
::rpc/profile-id (:id profile)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:props {}
|
|
||||||
:revn 2
|
:revn 2
|
||||||
:media {:filename "sample.jpg"
|
:media {:filename "sample.jpg"
|
||||||
:size 7923
|
:size 7923
|
||||||
|
@ -169,7 +168,6 @@
|
||||||
data3 {::th/type :create-file-thumbnail
|
data3 {::th/type :create-file-thumbnail
|
||||||
::rpc/profile-id (:id profile)
|
::rpc/profile-id (:id profile)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:props {}
|
|
||||||
:revn 3
|
:revn 3
|
||||||
:media {:filename "sample.jpg"
|
:media {:filename "sample.jpg"
|
||||||
:size 312043
|
:size 312043
|
||||||
|
@ -183,11 +181,11 @@
|
||||||
(let [out (th/command! data2)]
|
(let [out (th/command! data2)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(t/is (nil? (:result out))))
|
(t/is (contains? (:result out) :uri)))
|
||||||
|
|
||||||
(let [out (th/command! data3)]
|
(let [out (th/command! data3)]
|
||||||
(t/is (nil? (:error out)))
|
(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
|
(let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail
|
||||||
{:file-id (:id file)}
|
{:file-id (:id file)}
|
||||||
|
|
|
@ -131,7 +131,8 @@ function readManifest() {
|
||||||
"polyfills": "js/polyfills.js",
|
"polyfills": "js/polyfills.js",
|
||||||
"main": "js/main.js",
|
"main": "js/main.js",
|
||||||
"shared": "js/shared.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
|
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() {
|
gulp.task("polyfills", function() {
|
||||||
return gulp.src(paths.resources + "polyfills/*.js")
|
return gulp.src(paths.resources + "polyfills/*.js")
|
||||||
|
|
|
@ -51,6 +51,10 @@
|
||||||
border-radius: $br3;
|
border-radius: $br3;
|
||||||
border: 2px solid lighten($color-gray-20, 15%);
|
border: 2px solid lighten($color-gray-20, 15%);
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dragged {
|
&.dragged {
|
||||||
|
|
26
frontend/resources/templates/thumbnail-renderer.mustache
Normal file
26
frontend/resources/templates/thumbnail-renderer.mustache
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Penpot - Thumbnail Renderer</title>
|
||||||
|
<link rel="icon" href="images/favicon.png" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.penpotVersion = "%version%";
|
||||||
|
window.penpotBuildDate = "%buildDate%";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{# manifest}}
|
||||||
|
<script>window.penpotWorkerURI="{{& worker}}"</script>
|
||||||
|
<script src="{{& config}}"></script>
|
||||||
|
<script src="{{& polyfills}}"></script>
|
||||||
|
{{/manifest}}
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{# manifest}}
|
||||||
|
<script src="{{& shared}}"></script>
|
||||||
|
<script src="{{& thumbnail-renderer}}"></script>
|
||||||
|
{{/manifest}}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -16,17 +16,25 @@
|
||||||
:modules
|
:modules
|
||||||
{:shared {:entries []}
|
{:shared {:entries []}
|
||||||
|
|
||||||
:main {:entries [app.main]
|
:main
|
||||||
|
{:entries [app.main]
|
||||||
:depends-on #{:shared}
|
:depends-on #{:shared}
|
||||||
:init-fn app.main/init}
|
:init-fn app.main/init}
|
||||||
|
|
||||||
:render {:entries [app.render]
|
:render
|
||||||
|
{:entries [app.render]
|
||||||
:depends-on #{:shared}
|
:depends-on #{:shared}
|
||||||
:init-fn app.render/init}
|
:init-fn app.render/init}
|
||||||
|
|
||||||
:worker {:entries [app.worker]
|
:worker
|
||||||
|
{:entries [app.worker]
|
||||||
:web-worker true
|
:web-worker true
|
||||||
:depends-on #{:shared}}}
|
:depends-on #{:shared}}
|
||||||
|
|
||||||
|
:thumbnail-renderer
|
||||||
|
{:entries [app.thumbnail-renderer]
|
||||||
|
:depends-on #{:shared}
|
||||||
|
:init-fn app.thumbnail-renderer/init}}
|
||||||
|
|
||||||
:compiler-options
|
:compiler-options
|
||||||
{:output-feature-set :es2020
|
{:output-feature-set :es2020
|
||||||
|
|
|
@ -79,21 +79,21 @@
|
||||||
"unknown"
|
"unknown"
|
||||||
date)))
|
date)))
|
||||||
|
|
||||||
|
|
||||||
;; --- Globar Config Vars
|
;; --- Globar Config Vars
|
||||||
|
|
||||||
(def default-theme "default")
|
(def default-theme "default")
|
||||||
(def default-language "en")
|
(def default-language "en")
|
||||||
|
|
||||||
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
|
|
||||||
(def translations (obj/get global "penpotTranslations"))
|
(def translations (obj/get global "penpotTranslations"))
|
||||||
(def themes (obj/get global "penpotThemes"))
|
(def themes (obj/get global "penpotThemes"))
|
||||||
|
|
||||||
(def build-date (parse-build-date global))
|
(def build-date (parse-build-date global))
|
||||||
(def flags (atom (parse-flags global)))
|
(def flags (parse-flags global))
|
||||||
(def version (atom (parse-version global)))
|
(def version (parse-version global))
|
||||||
(def target (atom (parse-target global)))
|
(def target (parse-target global))
|
||||||
(def browser (atom (parse-browser)))
|
(def browser (parse-browser))
|
||||||
(def platform (atom (parse-platform)))
|
(def platform (parse-platform))
|
||||||
|
|
||||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
|
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
|
||||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
|
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
|
||||||
|
@ -108,36 +108,43 @@
|
||||||
(update :path #(str % "/")))))
|
(update :path #(str % "/")))))
|
||||||
|
|
||||||
(def public-uri
|
(def public-uri
|
||||||
(atom
|
|
||||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||||
(.-origin ^js location)))))
|
(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
|
;; --- Helper Functions
|
||||||
|
|
||||||
(defn ^boolean check-browser? [candidate]
|
(defn ^boolean check-browser? [candidate]
|
||||||
(dm/assert! (contains? valid-browsers candidate))
|
(dm/assert! (contains? valid-browsers candidate))
|
||||||
(= candidate @browser))
|
(= candidate browser))
|
||||||
|
|
||||||
(defn ^boolean check-platform? [candidate]
|
(defn ^boolean check-platform? [candidate]
|
||||||
(dm/assert! (contains? valid-platforms candidate))
|
(dm/assert! (contains? valid-platforms candidate))
|
||||||
(= candidate @platform))
|
(= candidate platform))
|
||||||
|
|
||||||
(defn resolve-profile-photo-url
|
(defn resolve-profile-photo-url
|
||||||
[{:keys [photo-id fullname name] :as profile}]
|
[{:keys [photo-id fullname name] :as profile}]
|
||||||
(if (nil? photo-id)
|
(if (nil? photo-id)
|
||||||
(avatars/generate {:name (or fullname name)})
|
(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
|
(defn resolve-team-photo-url
|
||||||
[{:keys [photo-id name] :as team}]
|
[{:keys [photo-id name] :as team}]
|
||||||
(if (nil? photo-id)
|
(if (nil? photo-id)
|
||||||
(avatars/generate {:name name})
|
(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
|
(defn resolve-file-media
|
||||||
([media]
|
([media]
|
||||||
(resolve-file-media media false))
|
(resolve-file-media media false))
|
||||||
([{:keys [id] :as media} thumbnail?]
|
([{:keys [id] :as media} thumbnail?]
|
||||||
(str (cond-> (u/join @public-uri "assets/by-file-media-id/")
|
(dm/str
|
||||||
(true? thumbnail?) (u/join (str id "/thumbnail"))
|
(cond-> (u/join public-uri "assets/by-file-media-id/")
|
||||||
(false? thumbnail?) (u/join (str id))))))
|
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
|
||||||
|
(false? thumbnail?) (u/join (dm/str id))))))
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main
|
(ns app.main
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
[app.main.errors]
|
[app.main.errors]
|
||||||
[app.main.features :as feat]
|
[app.main.features :as feat]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.thumbnail-renderer :as tr]
|
||||||
[app.main.ui :as ui]
|
[app.main.ui :as ui]
|
||||||
[app.main.ui.alert]
|
[app.main.ui.alert]
|
||||||
[app.main.ui.confirm]
|
[app.main.ui.confirm]
|
||||||
|
@ -34,12 +36,12 @@
|
||||||
|
|
||||||
(log/setup! {:app :info})
|
(log/setup! {:app :info})
|
||||||
|
|
||||||
(when (= :browser @cf/target)
|
(when (= :browser cf/target)
|
||||||
(log/info :message "Welcome to penpot"
|
(log/info :message "Welcome to penpot"
|
||||||
:version (:full @cf/version)
|
:version (:full cf/version)
|
||||||
:asserts *assert*
|
:asserts *assert*
|
||||||
:build-date cf/build-date
|
:build-date cf/build-date
|
||||||
:public-uri (str @cf/public-uri)))
|
:public-uri (dm/str cf/public-uri)))
|
||||||
|
|
||||||
(declare reinit)
|
(declare reinit)
|
||||||
|
|
||||||
|
@ -80,6 +82,7 @@
|
||||||
(i18n/init! cf/translations)
|
(i18n/init! cf/translations)
|
||||||
(theme/init! cf/themes)
|
(theme/init! cf/themes)
|
||||||
(cur/init-styles)
|
(cur/init-styles)
|
||||||
|
(tr/init!)
|
||||||
(init-ui)
|
(init-ui)
|
||||||
(st/emit! (initialize)))
|
(st/emit! (initialize)))
|
||||||
|
|
||||||
|
|
|
@ -475,7 +475,7 @@
|
||||||
(rx/map (fn [params]
|
(rx/map (fn [params]
|
||||||
(rt/resolve router :auth-verify-token {} params)))
|
(rt/resolve router :auth-verify-token {} params)))
|
||||||
(rx/map (fn [fragment]
|
(rx/map (fn [fragment]
|
||||||
(assoc @cf/public-uri :fragment fragment)))
|
(assoc cf/public-uri :fragment fragment)))
|
||||||
(rx/tap (fn [uri]
|
(rx/tap (fn [uri]
|
||||||
(wapi/write-to-clipboard (str uri))))
|
(wapi/write-to-clipboard (str uri))))
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
|
@ -782,6 +782,15 @@
|
||||||
(->> (rp/cmd! :set-file-shared params)
|
(->> (rp/cmd! :set-file-shared params)
|
||||||
(rx/ignore))))))
|
(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
|
;; --- EVENT: create-file
|
||||||
|
|
||||||
(declare file-created)
|
(declare file-created)
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
[]
|
[]
|
||||||
(let [uagent (UAParser.)]
|
(let [uagent (UAParser.)]
|
||||||
(merge
|
(merge
|
||||||
{:app-version (:full @cf/version)
|
{:app-version (:full cf/version)
|
||||||
:locale @i18n/locale}
|
:locale @i18n/locale}
|
||||||
(let [browser (.getBrowser uagent)]
|
(let [browser (.getBrowser uagent)]
|
||||||
{:browser (obj/get browser "name")
|
{:browser (obj/get browser "name")
|
||||||
|
@ -215,7 +215,7 @@
|
||||||
(defn- persist-events
|
(defn- persist-events
|
||||||
[events]
|
[events]
|
||||||
(if (seq 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
|
params {:uri uri
|
||||||
:method :post
|
:method :post
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
|
@ -230,7 +230,7 @@
|
||||||
|
|
||||||
(defn initialize
|
(defn initialize
|
||||||
[]
|
[]
|
||||||
(when (contains? @cf/flags :audit-log)
|
(when (contains? cf/flags :audit-log)
|
||||||
(ptk/reify ::initialize
|
(ptk/reify ::initialize
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ _ stream]
|
(effect [_ _ stream]
|
||||||
|
|
|
@ -377,7 +377,7 @@
|
||||||
(ptk/reify ::mark-onboarding-as-viewed
|
(ptk/reify ::mark-onboarding-as-viewed
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(let [version (or version (:main @cf/version))
|
(let [version (or version (:main cf/version))
|
||||||
props {:onboarding-viewed true
|
props {:onboarding-viewed true
|
||||||
:release-notes-viewed version}]
|
:release-notes-viewed version}]
|
||||||
(->> (rp/cmd! :update-profile-props {:props props})
|
(->> (rp/cmd! :update-profile-props {:props props})
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
(defn- prepare-uri
|
(defn- prepare-uri
|
||||||
[params]
|
[params]
|
||||||
(let [base (-> @cf/public-uri
|
(let [base (-> cf/public-uri
|
||||||
(u/join "ws/notifications")
|
(u/join "ws/notifications")
|
||||||
(assoc :query (u/map->query-string params)))]
|
(assoc :query (u/map->query-string params)))]
|
||||||
(cond-> base
|
(cond-> base
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
(log/trace :hint "event:initialize" :fn "features")
|
(log/trace :hint "event:initialize" :fn "features")
|
||||||
(rx/concat
|
(rx/concat
|
||||||
;; Enable all features set on the configuration
|
;; Enable all features set on the configuration
|
||||||
(->> (rx/from @cf/flags)
|
(->> (rx/from cf/flags)
|
||||||
(rx/map name)
|
(rx/map name)
|
||||||
(rx/map (fn [flag]
|
(rx/map (fn [flag]
|
||||||
(when (str/starts-with? flag "frontend-feature-")
|
(when (str/starts-with? flag "frontend-feature-")
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
@ -81,8 +82,12 @@
|
||||||
;; FONTS LOADING
|
;; FONTS LOADING
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defonce loaded (l/atom #{}))
|
(defonce ^:dynamic loaded (l/atom #{}))
|
||||||
(defonce loading (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
|
(defn- create-link-element
|
||||||
[uri]
|
[uri]
|
||||||
|
@ -148,31 +153,26 @@
|
||||||
|
|
||||||
;; --- LOADER: CUSTOM
|
;; --- LOADER: CUSTOM
|
||||||
|
|
||||||
(def font-css-template
|
(def font-face-template
|
||||||
"@font-face {
|
"@font-face {
|
||||||
font-family: '%(family)s';
|
font-family: '%(family)s';
|
||||||
font-style: %(style)s;
|
font-style: %(style)s;
|
||||||
font-weight: %(weight)s;
|
font-weight: %(weight)s;
|
||||||
font-display: block;
|
font-display: block;
|
||||||
src: url(%(woff1-uri)s) format('woff'),
|
src: url(%(uri)s) format('woff');
|
||||||
url(%(ttf-uri)s) format('ttf'),
|
|
||||||
url(%(otf-uri)s) format('otf');
|
|
||||||
}")
|
}")
|
||||||
|
|
||||||
(defn- asset-id->uri
|
(defn- asset-id->uri
|
||||||
[asset-id]
|
[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
|
(defn generate-custom-font-variant-css
|
||||||
[family variant]
|
[family variant]
|
||||||
(str/fmt font-css-template
|
(str/fmt font-face-template
|
||||||
{:family family
|
{:family family
|
||||||
:style (:style variant)
|
:style (:style variant)
|
||||||
:weight (:weight variant)
|
:weight (:weight variant)
|
||||||
:woff2-uri (asset-id->uri (::woff2-file-id variant))
|
:uri (asset-id->uri (::woff1-file-id variant))}))
|
||||||
:woff1-uri (asset-id->uri (::woff1-file-id variant))
|
|
||||||
:ttf-uri (asset-id->uri (::ttf-file-id variant))
|
|
||||||
:otf-uri (asset-id->uri (::otf-file-id variant))}))
|
|
||||||
|
|
||||||
(defn- generate-custom-font-css
|
(defn- generate-custom-font-css
|
||||||
[{:keys [family variants] :as font}]
|
[{:keys [family variants] :as font}]
|
||||||
|
@ -194,34 +194,35 @@
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn ensure-loaded!
|
(defn ensure-loaded!
|
||||||
[id]
|
([font-id] (ensure-loaded! font-id nil))
|
||||||
(log/debug :action "try-ensure-loaded!" :font-id id)
|
([font-id variant-id]
|
||||||
|
(log/debug :action "try-ensure-loaded!" :font-id font-id :variant-id variant-id)
|
||||||
(if-not (exists? js/window)
|
(if-not (exists? js/window)
|
||||||
;; If we are in the worker environment, we just mark it as loaded
|
;; If we are in the worker environment, we just mark it as loaded
|
||||||
;; without really loading it.
|
;; without really loading it.
|
||||||
(do
|
(do
|
||||||
(swap! loaded conj id)
|
(swap! loaded-hints conj {:font-id font-id :font-variant-id variant-id})
|
||||||
(p/resolved id))
|
(p/resolved font-id))
|
||||||
|
|
||||||
(let [font (get @fontsdb id)]
|
(let [font (get @fontsdb font-id)]
|
||||||
(cond
|
(cond
|
||||||
(nil? font)
|
(nil? font)
|
||||||
(p/resolved id)
|
(p/resolved font-id)
|
||||||
|
|
||||||
;; Font already loaded, we just continue
|
;; Font already loaded, we just continue
|
||||||
(contains? @loaded id)
|
(contains? @loaded font-id)
|
||||||
(p/resolved id)
|
(p/resolved font-id)
|
||||||
|
|
||||||
;; Font is currently downloading. We attach the caller to the promise
|
;; Font is currently downloading. We attach the caller to the promise
|
||||||
(contains? @loading id)
|
(contains? @loading font-id)
|
||||||
(p/resolved (get @loading id))
|
(p/resolved (get @loading font-id))
|
||||||
|
|
||||||
;; First caller, we create the promise and then wait
|
;; First caller, we create the promise and then wait
|
||||||
:else
|
:else
|
||||||
(let [on-load (fn [resolve]
|
(let [on-load (fn [resolve]
|
||||||
(swap! loaded conj id)
|
(swap! loaded conj font-id)
|
||||||
(swap! loading dissoc id)
|
(swap! loading dissoc font-id)
|
||||||
(resolve id))
|
(resolve font-id))
|
||||||
|
|
||||||
load-p (p/create
|
load-p (p/create
|
||||||
(fn [resolve _]
|
(fn [resolve _]
|
||||||
|
@ -229,34 +230,27 @@
|
||||||
(assoc ::on-loaded (partial on-load resolve))
|
(assoc ::on-loaded (partial on-load resolve))
|
||||||
(load-font))))]
|
(load-font))))]
|
||||||
|
|
||||||
(swap! loading assoc id load-p)
|
(swap! loading assoc font-id load-p)
|
||||||
load-p)))))
|
load-p))))))
|
||||||
|
|
||||||
(defn ready
|
(defn ready
|
||||||
[cb]
|
[cb]
|
||||||
(-> (obj/get-in js/document ["fonts" "ready"])
|
(-> (obj/get-in js/document ["fonts" "ready"])
|
||||||
(p/then cb)))
|
(p/then cb)))
|
||||||
|
|
||||||
(defn get-default-variant [{:keys [variants]}]
|
(defn get-default-variant
|
||||||
(or
|
[{:keys [variants]}]
|
||||||
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants)
|
(or (d/seek #(or (= (:id %) "regular")
|
||||||
|
(= (:name %) "regular")) variants)
|
||||||
(first 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
|
;; 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
|
(defn get-content-fonts
|
||||||
"Extracts the fonts used by the content of a text shape"
|
"Extracts the fonts used by the content of a text shape"
|
||||||
[{font-id :font-id children :children :as content}]
|
[{font-id :font-id children :children :as content}]
|
||||||
|
@ -267,38 +261,45 @@
|
||||||
children-font (->> children (mapv get-content-fonts))]
|
children-font (->> children (mapv get-content-fonts))]
|
||||||
(reduce set/union (conj children-font current-font))))
|
(reduce set/union (conj children-font current-font))))
|
||||||
|
|
||||||
|
|
||||||
(defn fetch-font-css
|
(defn fetch-font-css
|
||||||
"Given a font and the variant-id, retrieves the fontface CSS"
|
"Given a font and the variant-id, retrieves the fontface CSS"
|
||||||
[{:keys [font-id font-variant-id]
|
[{:keys [font-id font-variant-id]
|
||||||
:or {font-variant-id "regular"}}]
|
: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
|
(cond
|
||||||
|
(nil? font)
|
||||||
|
(rx/empty)
|
||||||
|
|
||||||
(= :google backend)
|
(= :google backend)
|
||||||
(let [variant (d/seek #(= (:id %) font-variant-id) variants)]
|
(let [variant (get-variant font font-variant-id)]
|
||||||
(-> (generate-gfonts-url
|
(-> (generate-gfonts-url
|
||||||
{:family family
|
{:family family
|
||||||
:variants [variant]})
|
:variants [variant]})
|
||||||
(http/fetch-text)))
|
(http/fetch-text)))
|
||||||
|
|
||||||
(= :custom backend)
|
(= :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)]
|
result (generate-custom-font-variant-css family variant)]
|
||||||
(p/resolved result))
|
(rx/of result))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [{:keys [weight style suffix] :as variant}
|
(let [{:keys [weight style suffix]} (get-variant font font-variant-id)
|
||||||
(d/seek #(= (:id %) font-variant-id) variants)
|
suffix (or suffix font-variant-id)
|
||||||
font-data {:baseurl (str @cf/public-uri)
|
params {:uri (dm/str cf/public-uri "fonts/" family "-" suffix ".woff")
|
||||||
:family family
|
:family family
|
||||||
:style style
|
:style style
|
||||||
:suffix (or suffix font-variant-id)
|
|
||||||
:weight weight}]
|
:weight weight}]
|
||||||
(rx/of (str/fmt font-face-template font-data))))))
|
(rx/of (str/fmt font-face-template params))))))
|
||||||
|
|
||||||
(defn extract-fontface-urls
|
(defn extract-fontface-urls
|
||||||
"Parses the CSS and retrieves the font urls"
|
"Parses the CSS and retrieves the font urls"
|
||||||
[^string css]
|
[^string css]
|
||||||
(->> (re-seq #"url\(([^)]+)\)" css)
|
(->> (re-seq #"url\(([^)]+)\)" css)
|
||||||
(mapv second)))
|
(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)) "")))
|
||||||
|
|
|
@ -50,6 +50,11 @@
|
||||||
:upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
|
:upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
|
||||||
:create-file-object-thumbnail {:query-params [:file-id :object-id]
|
:create-file-object-thumbnail {:query-params [:file-id :object-id]
|
||||||
:form-data? true}
|
:form-data? true}
|
||||||
|
|
||||||
|
:create-file-thumbnail
|
||||||
|
{:query-params [:file-id :revn]
|
||||||
|
:form-data? true}
|
||||||
|
|
||||||
:export-binfile {:response-type :blob}
|
:export-binfile {:response-type :blob}
|
||||||
:import-binfile {:form-data? true}
|
:import-binfile {:form-data? true}
|
||||||
:retrieve-list-of-builtin-templates {:query-params :all}
|
:retrieve-list-of-builtin-templates {:query-params :all}
|
||||||
|
@ -79,7 +84,7 @@
|
||||||
:else :post)
|
:else :post)
|
||||||
|
|
||||||
request {:method method
|
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"
|
:credentials "include"
|
||||||
:headers {"accept" "application/transit+json"}
|
:headers {"accept" "application/transit+json"}
|
||||||
:body (when (= method :post)
|
:body (when (= method :post)
|
||||||
|
@ -105,7 +110,7 @@
|
||||||
|
|
||||||
(defmethod cmd! :login-with-oidc
|
(defmethod cmd! :login-with-oidc
|
||||||
[_ {:keys [provider] :as params}]
|
[_ {: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)]
|
params (dissoc params :provider)]
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri uri
|
:uri uri
|
||||||
|
@ -117,7 +122,7 @@
|
||||||
(defn- send-export
|
(defn- send-export
|
||||||
[{:keys [blob?] :as params}]
|
[{:keys [blob?] :as params}]
|
||||||
(->> (http/send! {:method :post
|
(->> (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?))
|
:body (http/transit-data (dissoc params :blob?))
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
:response-type (if blob? :blob :text)})
|
:response-type (if blob? :blob :text)})
|
||||||
|
@ -136,7 +141,7 @@
|
||||||
(defmethod cmd! ::multipart-upload
|
(defmethod cmd! ::multipart-upload
|
||||||
[id params]
|
[id params]
|
||||||
(->> (http/send! {:method :post
|
(->> (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"
|
:credentials "include"
|
||||||
:body (http/form-data params)})
|
:body (http/form-data params)})
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
|
|
93
frontend/src/app/main/thumbnail_renderer.cljs
Normal file
93
frontend/src/app/main/thumbnail_renderer.cljs
Normal file
|
@ -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)))
|
|
@ -98,9 +98,9 @@
|
||||||
[:& app.main.ui.onboarding/onboarding-modal {}]
|
[:& app.main.ui.onboarding/onboarding-modal {}]
|
||||||
|
|
||||||
(and (:onboarding-viewed props)
|
(and (:onboarding-viewed props)
|
||||||
(not= (:release-notes-viewed props) (:main @cf/version))
|
(not= (:release-notes-viewed props) (:main cf/version))
|
||||||
(not= "0.0" (:main @cf/version)))
|
(not= "0.0" (:main cf/version)))
|
||||||
[:& app.main.ui.releases/release-notes-modal {:version (:main @cf/version)}]))
|
[:& app.main.ui.releases/release-notes-modal {:version (:main cf/version)}]))
|
||||||
|
|
||||||
[:& dashboard {:route route :profile profile}]]
|
[:& dashboard {:route route :profile profile}]]
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def show-alt-login-buttons?
|
(def show-alt-login-buttons?
|
||||||
(some (partial contains? @cf/flags)
|
(some (partial contains? cf/flags)
|
||||||
[:login-with-google
|
[:login-with-google
|
||||||
:login-with-github
|
:login-with-github
|
||||||
:login-with-gitlab
|
:login-with-gitlab
|
||||||
|
@ -175,13 +175,13 @@
|
||||||
:label (tr "auth.password")}]]
|
:label (tr "auth.password")}]]
|
||||||
|
|
||||||
[:div.buttons-stack
|
[:div.buttons-stack
|
||||||
(when (or (contains? @cf/flags :login)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? @cf/flags :login-with-password))
|
(contains? cf/flags :login-with-password))
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
{:label (tr "auth.login-submit")
|
{:label (tr "auth.login-submit")
|
||||||
:data-test "login-submit"}])
|
:data-test "login-submit"}])
|
||||||
|
|
||||||
(when (contains? @cf/flags :login-with-ldap)
|
(when (contains? cf/flags :login-with-ldap)
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
{:label (tr "auth.login-with-ldap-submit")
|
{:label (tr "auth.login-with-ldap-submit")
|
||||||
:on-click on-submit-ldap}])]]]))
|
:on-click on-submit-ldap}])]]]))
|
||||||
|
@ -189,25 +189,25 @@
|
||||||
(mf/defc login-buttons
|
(mf/defc login-buttons
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
[:div.auth-buttons
|
[: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)
|
[:& bl/button-link {:action #(login-with-oidc % :google params)
|
||||||
:icon i/brand-google
|
:icon i/brand-google
|
||||||
:name (tr "auth.login-with-google-submit")
|
:name (tr "auth.login-with-google-submit")
|
||||||
:klass "btn-google-auth"}])
|
: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)
|
[:& bl/button-link {:action #(login-with-oidc % :github params)
|
||||||
:icon i/brand-github
|
:icon i/brand-github
|
||||||
:name (tr "auth.login-with-github-submit")
|
:name (tr "auth.login-with-github-submit")
|
||||||
:klass "btn-github-auth"}])
|
: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)
|
[:& bl/button-link {:action #(login-with-oidc % :gitlab params)
|
||||||
:icon i/brand-gitlab
|
:icon i/brand-gitlab
|
||||||
:name (tr "auth.login-with-gitlab-submit")
|
:name (tr "auth.login-with-gitlab-submit")
|
||||||
:klass "btn-gitlab-auth"}])
|
: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)
|
[:& bl/button-link {:action #(login-with-oidc % :oidc params)
|
||||||
:icon i/brand-openid
|
:icon i/brand-openid
|
||||||
:name (tr "auth.login-with-oidc-submit")
|
:name (tr "auth.login-with-oidc-submit")
|
||||||
|
@ -215,7 +215,7 @@
|
||||||
|
|
||||||
(mf/defc login-button-oidc
|
(mf/defc login-button-oidc
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
(when (contains? @cf/flags :login-with-oidc)
|
(when (contains? cf/flags :login-with-oidc)
|
||||||
[:div.link-entry.link-oidc
|
[:div.link-entry.link-oidc
|
||||||
[:a {:tab-index "0"
|
[:a {:tab-index "0"
|
||||||
:on-key-down (fn [event]
|
:on-key-down (fn [event]
|
||||||
|
@ -236,17 +236,17 @@
|
||||||
|
|
||||||
[:& login-buttons {:params params}]
|
[:& login-buttons {:params params}]
|
||||||
|
|
||||||
(when (or (contains? @cf/flags :login)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? @cf/flags :login-with-password)
|
(contains? cf/flags :login-with-password)
|
||||||
(contains? @cf/flags :login-with-ldap))
|
(contains? cf/flags :login-with-ldap))
|
||||||
[:span.separator
|
[:span.separator
|
||||||
[:span.line]
|
[:span.line]
|
||||||
[:span.text (tr "labels.or")]
|
[:span.text (tr "labels.or")]
|
||||||
[:span.line]])])
|
[:span.line]])])
|
||||||
|
|
||||||
(when (or (contains? @cf/flags :login)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? @cf/flags :login-with-password)
|
(contains? cf/flags :login-with-password)
|
||||||
(contains? @cf/flags :login-with-ldap))
|
(contains? cf/flags :login-with-ldap))
|
||||||
[:& login-form {:params params :on-success-callback on-success-callback}])])
|
[:& login-form {:params params :on-success-callback on-success-callback}])])
|
||||||
|
|
||||||
(mf/defc login-page
|
(mf/defc login-page
|
||||||
|
@ -258,21 +258,21 @@
|
||||||
[:& login-methods {:params params}]
|
[:& login-methods {:params params}]
|
||||||
|
|
||||||
[:div.links
|
[:div.links
|
||||||
(when (or (contains? @cf/flags :login)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? @cf/flags :login-with-password))
|
(contains? cf/flags :login-with-password))
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request))
|
[:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request))
|
||||||
:data-test "forgot-password"}
|
:data-test "forgot-password"}
|
||||||
(tr "auth.forgot-password")]])
|
(tr "auth.forgot-password")]])
|
||||||
|
|
||||||
(when (contains? @cf/flags :registration)
|
(when (contains? cf/flags :registration)
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:span (tr "auth.register") " "]
|
[:span (tr "auth.register") " "]
|
||||||
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params))
|
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params))
|
||||||
:data-test "register-submit"}
|
:data-test "register-submit"}
|
||||||
(tr "auth.register-submit")]])]
|
(tr "auth.register-submit")]])]
|
||||||
|
|
||||||
(when (contains? @cf/flags :demo-users)
|
(when (contains? cf/flags :demo-users)
|
||||||
[:div.links.demo
|
[:div.links.demo
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:span (tr "auth.create-demo-profile") " "]
|
[:span (tr "auth.create-demo-profile") " "]
|
||||||
|
|
|
@ -141,8 +141,8 @@
|
||||||
|
|
||||||
[:& login/login-buttons {:params params}]
|
[:& login/login-buttons {:params params}]
|
||||||
|
|
||||||
(when (or (contains? @cf/flags :login)
|
(when (or (contains? cf/flags :login)
|
||||||
(contains? @cf/flags :login-with-ldap))
|
(contains? cf/flags :login-with-ldap))
|
||||||
[:span.separator
|
[:span.separator
|
||||||
[:span.line]
|
[:span.line]
|
||||||
[:span.text (tr "labels.or")]
|
[:span.text (tr "labels.or")]
|
||||||
|
@ -157,7 +157,7 @@
|
||||||
[:h1 {:data-test "registration-title"} (tr "auth.register-title")]
|
[:h1 {:data-test "registration-title"} (tr "auth.register-title")]
|
||||||
[:div.subtitle (tr "auth.register-subtitle")]
|
[:div.subtitle (tr "auth.register-subtitle")]
|
||||||
|
|
||||||
(when (contains? @cf/flags :demo-warning)
|
(when (contains? cf/flags :demo-warning)
|
||||||
[:& demo-warning])
|
[:& demo-warning])
|
||||||
|
|
||||||
[:& register-methods {:params params}]
|
[:& register-methods {:params params}]
|
||||||
|
@ -170,7 +170,7 @@
|
||||||
:data-test "login-here-link"}
|
:data-test "login-here-link"}
|
||||||
(tr "auth.login-here")]]
|
(tr "auth.login-here")]]
|
||||||
|
|
||||||
(when (contains? @cf/flags :demo-users)
|
(when (contains? cf/flags :demo-users)
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:span (tr "auth.create-demo-profile") " "]
|
[:span (tr "auth.create-demo-profile") " "]
|
||||||
[:& lk/link {:action #(st/emit! (du/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-terms-and-privacy (s/and ::us/boolean true?))
|
||||||
(s/def ::accept-newsletter-subscription ::us/boolean)
|
(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/def ::register-validate-form
|
||||||
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
|
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
|
||||||
:opt-un [::accept-newsletter-subscription]))
|
:opt-un [::accept-newsletter-subscription]))
|
||||||
|
@ -244,7 +244,7 @@
|
||||||
:label (tr "auth.fullname")
|
:label (tr "auth.fullname")
|
||||||
:type "text"}]]
|
: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
|
[:div.fields-row.input-visible.accept-terms-and-privacy-wrapper
|
||||||
[:& fm/input {:name :accept-terms-and-privacy
|
[:& fm/input {:name :accept-terms-and-privacy
|
||||||
:class "check-primary"
|
:class "check-primary"
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.render :refer [component-svg]]
|
[app.main.render :refer [component-svg]]
|
||||||
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.thumbnail-renderer :as thr]
|
||||||
[app.main.ui.components.color-bullet :as bc]
|
[app.main.ui.components.color-bullet :as bc]
|
||||||
[app.main.ui.dashboard.file-menu :refer [file-menu]]
|
[app.main.ui.dashboard.file-menu :refer [file-menu]]
|
||||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||||
|
@ -30,7 +32,6 @@
|
||||||
[app.util.dom.dnd :as dnd]
|
[app.util.dom.dnd :as dnd]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.perf :as perf]
|
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
|
@ -41,44 +42,49 @@
|
||||||
|
|
||||||
;; --- Grid Item Thumbnail
|
;; --- 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"
|
"Creates some hooks to handle the files thumbnails cache"
|
||||||
[file]
|
[file-id revn]
|
||||||
(let [features (cond-> ffeat/enabled
|
(let [features (cond-> ffeat/enabled
|
||||||
(features/active-feature? :components-v2)
|
(features/active-feature? :components-v2)
|
||||||
(conj "components/v2"))]
|
(conj "components/v2"))]
|
||||||
|
|
||||||
(wrk/ask! {:cmd :thumbnails/generate-for-file
|
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
|
||||||
:revn (:revn file)
|
:revn revn
|
||||||
:file-id (:id file)
|
:file-id file-id
|
||||||
:file-name (:name file)
|
:features features})
|
||||||
: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/defc grid-item-thumbnail
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap-props false}
|
||||||
[{:keys [file] :as props}]
|
[{:keys [file-id revn thumbnail-uri background-color]}]
|
||||||
(let [container (mf/use-ref)
|
(let [container (mf/use-ref)
|
||||||
bgcolor (dm/get-in file [:data :options :background])
|
|
||||||
visible? (h/use-visible container :once? true)]
|
visible? (h/use-visible container :once? true)]
|
||||||
|
|
||||||
(mf/with-effect [file visible?]
|
(mf/with-effect [file-id revn visible? thumbnail-uri]
|
||||||
(when visible?
|
(when (and visible? (not thumbnail-uri))
|
||||||
(let [tp (perf/tpoint)]
|
(->> (ask-for-thumbnail file-id revn)
|
||||||
(->> (ask-for-thumbnail file)
|
(rx/subs (fn [url]
|
||||||
(rx/subscribe-on :af)
|
(st/emit! (dd/set-file-thumbnail file-id url)))))))
|
||||||
(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))))))))
|
|
||||||
|
|
||||||
[:div.grid-item-th
|
[:div.grid-item-th
|
||||||
{:style {:background-color bgcolor}
|
{:style {:background-color background-color}
|
||||||
:ref container}
|
:ref container}
|
||||||
i/loader-pencil]))
|
(when visible?
|
||||||
|
(if thumbnail-uri
|
||||||
|
[:img.grid-item-thumbnail-image {:src thumbnail-uri}]
|
||||||
|
i/loader-pencil))]))
|
||||||
|
|
||||||
;; --- Grid Item Library
|
;; --- Grid Item Library
|
||||||
|
|
||||||
|
@ -312,7 +318,12 @@
|
||||||
[:div.overlay]
|
[:div.overlay]
|
||||||
(if library-view?
|
(if library-view?
|
||||||
[:& grid-item-library {:file file}]
|
[:& 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?))
|
(when (and (:is-shared file) (not library-view?))
|
||||||
[:div.item-badge i/library])
|
[:div.item-badge i/library])
|
||||||
[:div.info-wrapper
|
[:div.info-wrapper
|
||||||
|
|
|
@ -376,7 +376,7 @@
|
||||||
:data-test "team-invitations"}
|
:data-test "team-invitations"}
|
||||||
(tr "labels.invitations")]
|
(tr "labels.invitations")]
|
||||||
|
|
||||||
(when (contains? @cf/flags :webhooks)
|
(when (contains? cf/flags :webhooks)
|
||||||
[:& dropdown-menu-item {:on-click go-webhooks
|
[:& dropdown-menu-item {:on-click go-webhooks
|
||||||
:on-key-down (fn [event]
|
:on-key-down (fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
|
@ -459,7 +459,7 @@
|
||||||
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
|
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
|
||||||
options-ids ["teams-options-members"
|
options-ids ["teams-options-members"
|
||||||
"teams-options-invitations"
|
"teams-options-invitations"
|
||||||
(when (contains? @cf/flags :webhooks)
|
(when (contains? cf/flags :webhooks)
|
||||||
"teams-options-webhooks")
|
"teams-options-webhooks")
|
||||||
"teams-options-settings"
|
"teams-options-settings"
|
||||||
(when can-rename?
|
(when can-rename?
|
||||||
|
@ -680,7 +680,7 @@
|
||||||
show-release-notes
|
show-release-notes
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [event]
|
(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}))
|
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||||
(st/emit! (modal/show {:type :onboarding}))
|
(st/emit! (modal/show {:type :onboarding}))
|
||||||
|
@ -769,7 +769,7 @@
|
||||||
(dom/open-new-window "https://penpot.app/terms")))}
|
(dom/open-new-window "https://penpot.app/terms")))}
|
||||||
[:span (tr "auth.terms-of-service")]]
|
[:span (tr "auth.terms-of-service")]]
|
||||||
|
|
||||||
(when (contains? @cf/flags :user-feedback)
|
(when (contains? cf/flags :user-feedback)
|
||||||
[:li.separator {:tab-index (if show
|
[:li.separator {:tab-index (if show
|
||||||
"0"
|
"0"
|
||||||
"-1")
|
"-1")
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
[:div.modal-left.welcome
|
[:div.modal-left.welcome
|
||||||
[:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
|
[:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
|
||||||
[:div.modal-right
|
[: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.right-content
|
||||||
[:div.modal-title
|
[:div.modal-title
|
||||||
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]]
|
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]]
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
[:div.modal-left.welcome
|
[:div.modal-left.welcome
|
||||||
[:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
|
[:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
|
||||||
[:div.modal-right
|
[: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.right-content
|
||||||
[:div.modal-title
|
[:div.modal-title
|
||||||
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]]
|
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]]
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
skip
|
skip
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
#(st/emit! (modal/hide)
|
#(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-newsletter-modal})
|
||||||
(modal/show {:type :onboarding-team}))
|
(modal/show {:type :onboarding-team}))
|
||||||
(du/mark-onboarding-as-viewed)))]
|
(du/mark-onboarding-as-viewed)))]
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main.ui.onboarding.templates
|
(ns app.main.ui.onboarding.templates
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.dashboard :as dd]
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
(mf/defc template-item
|
(mf/defc template-item
|
||||||
[{:keys [name path image project-id]}]
|
[{:keys [name path image project-id]}]
|
||||||
(let [downloading? (mf/use-state false)
|
(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
|
on-finish-import
|
||||||
(fn []
|
(fn []
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
show-release-notes
|
show-release-notes
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [event]
|
(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}))
|
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||||
(st/emit! (modal/show {:type :onboarding}))
|
(st/emit! (modal/show {:type :onboarding}))
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
i/tree
|
i/tree
|
||||||
[:span.element-title (tr "labels.settings")]]
|
[:span.element-title (tr "labels.settings")]]
|
||||||
|
|
||||||
(when (contains? @cf/flags :access-tokens)
|
(when (contains? cf/flags :access-tokens)
|
||||||
[:li {:class (when access-tokens? "current")
|
[:li {:class (when access-tokens? "current")
|
||||||
:on-click go-settings-access-tokens
|
:on-click go-settings-access-tokens
|
||||||
:data-test "settings-access-tokens"}
|
:data-test "settings-access-tokens"}
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
i/pencil
|
i/pencil
|
||||||
[:span.element-title (tr "labels.release-notes")]]
|
[:span.element-title (tr "labels.release-notes")]]
|
||||||
|
|
||||||
(when (contains? @cf/flags :user-feedback)
|
(when (contains? cf/flags :user-feedback)
|
||||||
[:li {:class (when feedback? "current")
|
[:li {:class (when feedback? "current")
|
||||||
:on-click go-settings-feedback}
|
:on-click go-settings-feedback}
|
||||||
i/msg-info
|
i/msg-info
|
||||||
|
|
|
@ -16,10 +16,13 @@
|
||||||
|
|
||||||
(defn- load-fonts!
|
(defn- load-fonts!
|
||||||
[content]
|
[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)
|
(->> (tree-seq map? :children content)
|
||||||
(into #{default} (keep :font-id))
|
(into #{default} (keep extract-fn))
|
||||||
(run! fonts/ensure-loaded!))))
|
(run! (fn [[font-id variant-id]]
|
||||||
|
(when (some? font-id)
|
||||||
|
(fonts/ensure-loaded! font-id variant-id)))))))
|
||||||
|
|
||||||
(mf/defc text-shape
|
(mf/defc text-shape
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
|
|
|
@ -145,7 +145,7 @@
|
||||||
(assoc qparams :zoom zoom-type))
|
(assoc qparams :zoom zoom-type))
|
||||||
|
|
||||||
href (rt/resolve router :viewer pparams qparams)]
|
href (rt/resolve router :viewer pparams qparams)]
|
||||||
(assoc @cf/public-uri :fragment href)))]
|
(assoc cf/public-uri :fragment href)))]
|
||||||
(reset! link (some-> href str)))))
|
(reset! link (some-> href str)))))
|
||||||
|
|
||||||
[:div.modal-overlay.transparent.share-modal
|
[:div.modal-overlay.transparent.share-modal
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
show-release-notes
|
show-release-notes
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event]
|
(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}))
|
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||||
(st/emit! (modal/show {:type :onboarding}))
|
(st/emit! (modal/show {:type :onboarding}))
|
||||||
|
@ -186,7 +186,7 @@
|
||||||
[:span (tr "label.shortcuts")]
|
[:span (tr "label.shortcuts")]
|
||||||
[:span.shortcut (sc/get-tooltip :show-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}
|
[:li.feedback {:on-click nav-to-feedback}
|
||||||
[:span (tr "labels.give-feedback")]]])]]))
|
[:span (tr "labels.give-feedback")]]])]]))
|
||||||
|
|
|
@ -10,35 +10,30 @@
|
||||||
[app.main.errors :as err]
|
[app.main.errors :as err]
|
||||||
[app.util.worker :as uw]))
|
[app.util.worker :as uw]))
|
||||||
|
|
||||||
(defonce instance (atom nil))
|
(defonce instance nil)
|
||||||
|
|
||||||
(defn- update-public-uri!
|
|
||||||
[instance val]
|
|
||||||
(uw/ask! instance {:cmd :configure
|
|
||||||
:key :public-uri
|
|
||||||
:val val}))
|
|
||||||
|
|
||||||
(defn init!
|
(defn init!
|
||||||
[]
|
[]
|
||||||
(let [worker (uw/init cf/worker-uri err/on-error)]
|
(let [worker (uw/init cf/worker-uri err/on-error)]
|
||||||
(update-public-uri! worker @cf/public-uri)
|
(uw/ask! worker {:cmd :configure
|
||||||
(add-watch cf/public-uri ::worker-public-uri (fn [_ _ _ val] (update-public-uri! worker val)))
|
:key :public-uri
|
||||||
(reset! instance worker)))
|
:val cf/public-uri})
|
||||||
|
(set! instance worker)))
|
||||||
|
|
||||||
(defn ask!
|
(defn ask!
|
||||||
([message]
|
([message]
|
||||||
(when @instance (uw/ask! @instance message)))
|
(when instance (uw/ask! instance message)))
|
||||||
([message transfer]
|
([message transfer]
|
||||||
(when @instance (uw/ask! @instance message transfer))))
|
(when instance (uw/ask! instance message transfer))))
|
||||||
|
|
||||||
(defn ask-buffered!
|
(defn ask-buffered!
|
||||||
([message]
|
([message]
|
||||||
(when @instance (uw/ask-buffered! @instance message)))
|
(when instance (uw/ask-buffered! instance message)))
|
||||||
([message transfer]
|
([message transfer]
|
||||||
(when @instance (uw/ask-buffered! @instance message transfer))))
|
(when instance (uw/ask-buffered! instance message transfer))))
|
||||||
|
|
||||||
(defn ask-many!
|
(defn ask-many!
|
||||||
([message]
|
([message]
|
||||||
(when @instance (uw/ask-many! @instance message)))
|
(when instance (uw/ask-many! instance message)))
|
||||||
([message transfer]
|
([message transfer]
|
||||||
(when @instance (uw/ask-many! @instance message transfer))))
|
(when instance (uw/ask-many! instance message transfer))))
|
||||||
|
|
246
frontend/src/app/thumbnail_renderer.cljs
Normal file
246
frontend/src/app/thumbnail_renderer.cljs
Normal file
|
@ -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 <style> node to an SVG."
|
||||||
|
[svg styles]
|
||||||
|
(let [doc (get-document-element svg)
|
||||||
|
style (dom/create-element svg "http://www.w3.org/2000/svg" "style")]
|
||||||
|
(dom/append-child! style (dom/create-text svg styles))
|
||||||
|
(dom/append-child! doc style)))
|
||||||
|
|
||||||
|
(defn- svg-resolve-styles!
|
||||||
|
"Resolves all fonts in an SVG to Data URIs."
|
||||||
|
[svg styles]
|
||||||
|
(->> (rx/from (re-seq #"url\((https?://[^)]+)\)" styles))
|
||||||
|
(rx/map second)
|
||||||
|
(rx/mapcat (fn [url]
|
||||||
|
(->> (fetch-as-data-uri url)
|
||||||
|
(rx/map (fn [uri] [url uri])))))
|
||||||
|
|
||||||
|
(rx/reduce (fn [styles [url uri]]
|
||||||
|
(str/replace styles url uri))
|
||||||
|
styles)
|
||||||
|
(rx/tap (partial svg-add-style! svg))
|
||||||
|
(rx/ignore)))
|
||||||
|
|
||||||
|
(defn- svg-resolve-all!
|
||||||
|
"Resolves all images and fonts in an SVG to Data URIs."
|
||||||
|
[svg styles]
|
||||||
|
(rx/concat
|
||||||
|
(svg-resolve-images! svg)
|
||||||
|
(svg-resolve-styles! svg styles)
|
||||||
|
(rx/of svg)))
|
||||||
|
|
||||||
|
(defn- svg-parse
|
||||||
|
"Parses an SVG string into an SVG DOM."
|
||||||
|
[data]
|
||||||
|
(let [parser (js/DOMParser.)]
|
||||||
|
(.parseFromString ^js parser data "image/svg+xml")))
|
||||||
|
|
||||||
|
(defn- svg-stringify
|
||||||
|
"Converts an SVG to a string."
|
||||||
|
[svg]
|
||||||
|
(let [doc (get-document-element svg)
|
||||||
|
serializer (js/XMLSerializer.)]
|
||||||
|
(.serializeToString ^js serializer doc)))
|
||||||
|
|
||||||
|
(defn- svg-prepare
|
||||||
|
"Prepares an SVG for rendering (resolves images to Data URIs and adds intrinsic size)."
|
||||||
|
[data styles]
|
||||||
|
(let [svg (svg-parse data)]
|
||||||
|
(->> (svg-resolve-all! svg styles)
|
||||||
|
(rx/map #(svg-set-intrinsic-size! % 300))
|
||||||
|
(rx/map svg-stringify))))
|
||||||
|
|
||||||
|
(defn- bitmap->blob
|
||||||
|
"Converts an ImageBitmap to a Blob."
|
||||||
|
[bitmap]
|
||||||
|
(rx/create
|
||||||
|
(fn [subs]
|
||||||
|
(let [canvas (dom/create-element "canvas")]
|
||||||
|
(set! (.-width ^js canvas) (.-width ^js bitmap))
|
||||||
|
(set! (.-height ^js canvas) (.-height ^js bitmap))
|
||||||
|
(let [context (.getContext ^js canvas "bitmaprenderer")]
|
||||||
|
(.transferFromImageBitmap ^js context bitmap)
|
||||||
|
(.toBlob canvas #(do (rx/push! subs %)
|
||||||
|
(rx/end! subs))))
|
||||||
|
|
||||||
|
(constantly nil)))))
|
||||||
|
|
||||||
|
(defn- render
|
||||||
|
"Renders a thumbnail using it's SVG and returns an ArrayBuffer of the image."
|
||||||
|
[payload]
|
||||||
|
(let [data (unchecked-get payload "data")
|
||||||
|
styles (unchecked-get payload "styles")]
|
||||||
|
(->> (svg-prepare data styles)
|
||||||
|
(rx/map #(wapi/create-blob % "image/svg+xml"))
|
||||||
|
(rx/map wapi/create-uri)
|
||||||
|
(rx/mapcat (fn [uri]
|
||||||
|
(->> (create-image uri)
|
||||||
|
(rx/mapcat wapi/create-image-bitmap)
|
||||||
|
(rx/tap #(wapi/revoke-uri uri)))))
|
||||||
|
(rx/mapcat bitmap->blob))))
|
||||||
|
|
||||||
|
(defn- on-message
|
||||||
|
"Handles messages from the main thread."
|
||||||
|
[event]
|
||||||
|
(let [evdata (unchecked-get event "data")
|
||||||
|
evorigin (unchecked-get event "origin")]
|
||||||
|
(when (str/starts-with? parent-origin evorigin)
|
||||||
|
(let [id (unchecked-get evdata "id")
|
||||||
|
payload (unchecked-get evdata "payload")
|
||||||
|
scope (unchecked-get evdata "scope")]
|
||||||
|
(when (and (some? payload)
|
||||||
|
(= scope "penpot/thumbnail-renderer"))
|
||||||
|
(->> (render payload)
|
||||||
|
(rx/subs (partial send-success! id)
|
||||||
|
(partial send-failure! id))))))))
|
||||||
|
|
||||||
|
(defn- listen
|
||||||
|
"Initializes the listener for messages from the main thread."
|
||||||
|
[]
|
||||||
|
(.addEventListener js/window "message" on-message))
|
||||||
|
|
||||||
|
(defn- send-answer!
|
||||||
|
"Sends an answer message."
|
||||||
|
[id type payload]
|
||||||
|
(let [message #js {:id id
|
||||||
|
:type type
|
||||||
|
:scope "penpot/thumbnail-renderer"
|
||||||
|
:payload payload}]
|
||||||
|
(when-not (identical? js/window js/parent)
|
||||||
|
(.postMessage js/parent message parent-origin))))
|
||||||
|
|
||||||
|
(defn- send-success!
|
||||||
|
"Sends a success message."
|
||||||
|
[id payload]
|
||||||
|
(send-answer! id "success" payload))
|
||||||
|
|
||||||
|
(defn- send-failure!
|
||||||
|
"Sends a failure message."
|
||||||
|
[id payload]
|
||||||
|
(send-answer! id "failure" payload))
|
||||||
|
|
||||||
|
(defn- send-ready!
|
||||||
|
"Sends a ready message."
|
||||||
|
[]
|
||||||
|
(send-answer! nil "ready" nil))
|
||||||
|
|
||||||
|
(defn ^:export init
|
||||||
|
[]
|
||||||
|
(listen)
|
||||||
|
(send-ready!)
|
||||||
|
(log/info :hint "initialized"
|
||||||
|
:public-uri (dm/str cf/public-uri)
|
||||||
|
:parent-uri (dm/str parent-origin)))
|
|
@ -254,7 +254,15 @@
|
||||||
([tag]
|
([tag]
|
||||||
(.createElement globals/document tag))
|
(.createElement globals/document tag))
|
||||||
([ns tag]
|
([ns tag]
|
||||||
(.createElementNS globals/document ns tag)))
|
(.createElementNS globals/document ns tag))
|
||||||
|
([document ns tag]
|
||||||
|
(.createElementNS document ns tag)))
|
||||||
|
|
||||||
|
(defn create-text
|
||||||
|
([^js text]
|
||||||
|
(create-text globals/document text))
|
||||||
|
([document ^js text]
|
||||||
|
(.createTextNode document text)))
|
||||||
|
|
||||||
(defn set-html!
|
(defn set-html!
|
||||||
[^js el html]
|
[^js el html]
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
(into {} (map vec) (seq (.entries ^js headers))))
|
(into {} (map vec) (seq (.entries ^js headers))))
|
||||||
|
|
||||||
(def default-headers
|
(def default-headers
|
||||||
{"x-frontend-version" (:full @cfg/version)})
|
{"x-frontend-version" (:full cfg/version)})
|
||||||
|
|
||||||
(defn fetch
|
(defn fetch
|
||||||
[{:keys [method uri query headers body mode omit-default-headers credentials]
|
[{:keys [method uri query headers body mode omit-default-headers credentials]
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
(let [router (:router state)
|
(let [router (:router state)
|
||||||
path (resolve router rname path-params query-params)
|
path (resolve router rname path-params query-params)
|
||||||
name (or name "_blank")
|
name (or name "_blank")
|
||||||
uri (assoc @cf/public-uri :fragment path)]
|
uri (assoc cf/public-uri :fragment path)]
|
||||||
(dom/open-new-window uri name nil)))))
|
(dom/open-new-window uri name nil)))))
|
||||||
|
|
||||||
(defn nav-back
|
(defn nav-back
|
||||||
|
|
|
@ -130,6 +130,10 @@
|
||||||
(map #(.item file-list %))
|
(map #(.item file-list %))
|
||||||
(filter #(str/starts-with? (.-type %) "image/"))))))
|
(filter #(str/starts-with? (.-type %) "image/"))))))
|
||||||
|
|
||||||
|
(defn create-image-bitmap
|
||||||
|
[image]
|
||||||
|
(js/createImageBitmap image))
|
||||||
|
|
||||||
(defn request-fullscreen
|
(defn request-fullscreen
|
||||||
[el]
|
[el]
|
||||||
(cond
|
(cond
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
(defmethod handler :configure
|
(defmethod handler :configure
|
||||||
[{:keys [key val]}]
|
[{:keys [key val]}]
|
||||||
(log/info :hint "configure worker" :key key :val val)
|
(log/info :hint "configure worker" :key key :val (dm/str val))
|
||||||
(case key
|
(case key
|
||||||
:public-uri
|
:public-uri
|
||||||
(reset! cf/public-uri val)))
|
(set! cf/public-uri val)))
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
[app.util.webapi :as wapi]
|
[app.util.webapi :as wapi]
|
||||||
[app.worker.impl :as impl]
|
[app.worker.impl :as impl]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[debug :refer [debug?]]
|
[okulary.core :as l]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
@ -45,15 +45,6 @@
|
||||||
:http-status status
|
:http-status status
|
||||||
:http-body body})))
|
:http-body body})))
|
||||||
|
|
||||||
(defn- not-found?
|
|
||||||
[{:keys [type]}]
|
|
||||||
(= :not-found type))
|
|
||||||
|
|
||||||
(defn- body-too-large?
|
|
||||||
[{:keys [type code]}]
|
|
||||||
(and (= :validation type)
|
|
||||||
(= :request-body-too-large code)))
|
|
||||||
|
|
||||||
(defn- request-data-for-thumbnail
|
(defn- request-data-for-thumbnail
|
||||||
[file-id revn features]
|
[file-id revn features]
|
||||||
(let [path "api/rpc/command/get-file-data-for-thumbnail"
|
(let [path "api/rpc/command/get-file-data-for-thumbnail"
|
||||||
|
@ -62,20 +53,7 @@
|
||||||
:strip-frames-with-thumbnails true
|
:strip-frames-with-thumbnails true
|
||||||
:features features}
|
:features features}
|
||||||
request {:method :get
|
request {:method :get
|
||||||
:uri (u/join @cf/public-uri path)
|
:uri (u/join cf/public-uri path)
|
||||||
:credentials "include"
|
|
||||||
:query params}]
|
|
||||||
(->> (http/send! request)
|
|
||||||
(rx/map http/conditional-decode-transit)
|
|
||||||
(rx/mapcat handle-response))))
|
|
||||||
|
|
||||||
(defn- request-thumbnail
|
|
||||||
[file-id revn]
|
|
||||||
(let [path "api/rpc/command/get-file-thumbnail"
|
|
||||||
params {:file-id file-id
|
|
||||||
:revn revn}
|
|
||||||
request {:method :get
|
|
||||||
:uri (u/join @cf/public-uri path)
|
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
:query params}]
|
:query params}]
|
||||||
(->> (http/send! request)
|
(->> (http/send! request)
|
||||||
|
@ -84,55 +62,23 @@
|
||||||
|
|
||||||
(defn- render-thumbnail
|
(defn- render-thumbnail
|
||||||
[{:keys [page file-id revn] :as params}]
|
[{:keys [page file-id revn] :as params}]
|
||||||
|
(binding [fonts/loaded-hints (l/atom #{})]
|
||||||
(let [objects (:objects page)
|
(let [objects (:objects page)
|
||||||
frame (some->> page :thumbnail-frame-id (get objects))
|
frame (some->> page :thumbnail-frame-id (get objects))
|
||||||
element (if frame
|
element (if frame
|
||||||
(mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true})
|
(mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true})
|
||||||
(mf/element render/page-svg #js {:data page :thumbnails? true}))
|
(mf/element render/page-svg #js {:data page :thumbnails? true :render-embed? true}))
|
||||||
data (rds/renderToStaticMarkup element)]
|
data (rds/renderToStaticMarkup element)]
|
||||||
|
|
||||||
{:data data
|
{:data data
|
||||||
:fonts (into @fonts/loaded (map first) @fonts/loading)
|
:fonts @fonts/loaded-hints
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:revn revn}))
|
:revn revn})))
|
||||||
|
|
||||||
(defn- persist-thumbnail
|
|
||||||
[{:keys [file-id data revn fonts]}]
|
|
||||||
(let [path "api/rpc/command/upsert-file-thumbnail"
|
|
||||||
params {:file-id file-id
|
|
||||||
:revn revn
|
|
||||||
:props {:fonts fonts}
|
|
||||||
:data data}
|
|
||||||
request {:method :post
|
|
||||||
:uri (u/join @cf/public-uri path)
|
|
||||||
:credentials "include"
|
|
||||||
:body (http/transit-data params)}]
|
|
||||||
|
|
||||||
(->> (http/send! request)
|
|
||||||
(rx/map http/conditional-decode-transit)
|
|
||||||
(rx/mapcat handle-response)
|
|
||||||
(rx/catch body-too-large? (constantly (rx/of nil)))
|
|
||||||
(rx/map (constantly params)))))
|
|
||||||
|
|
||||||
(defmethod impl/handler :thumbnails/generate-for-file
|
(defmethod impl/handler :thumbnails/generate-for-file
|
||||||
[{:keys [file-id revn features] :as message} _]
|
[{:keys [file-id revn features] :as message} _]
|
||||||
(letfn [(on-result [{:keys [data props]}]
|
|
||||||
{:data data
|
|
||||||
:fonts (:fonts props)})
|
|
||||||
|
|
||||||
(on-cache-miss [_]
|
|
||||||
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "miss")
|
|
||||||
(->> (request-data-for-thumbnail file-id revn features)
|
(->> (request-data-for-thumbnail file-id revn features)
|
||||||
(rx/map render-thumbnail)
|
(rx/map render-thumbnail)))
|
||||||
(rx/mapcat persist-thumbnail)))]
|
|
||||||
|
|
||||||
(if (debug? :disable-thumbnail-cache)
|
|
||||||
(->> (request-data-for-thumbnail file-id revn features)
|
|
||||||
(rx/map render-thumbnail))
|
|
||||||
(->> (request-thumbnail file-id revn)
|
|
||||||
(rx/tap (fn [_]
|
|
||||||
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "hit")))
|
|
||||||
(rx/catch not-found? on-cache-miss)
|
|
||||||
(rx/map on-result)))))
|
|
||||||
|
|
||||||
(defmethod impl/handler :thumbnails/render-offscreen-canvas
|
(defmethod impl/handler :thumbnails/render-offscreen-canvas
|
||||||
[_ ibpm]
|
[_ ibpm]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue