mirror of
https://github.com/penpot/penpot.git
synced 2025-05-12 01:36:37 +02:00
✨ Set an artboard as the file thumbnail
This commit is contained in:
parent
a588267fc2
commit
0a04a856da
11 changed files with 150 additions and 26 deletions
|
@ -5,6 +5,7 @@
|
||||||
### :boom: Breaking changes
|
### :boom: Breaking changes
|
||||||
### :sparkles: New features
|
### :sparkles: New features
|
||||||
|
|
||||||
|
- Set an artboard as the file thumbnail [Taiga #1526](https://tree.taiga.io/project/penpot/us/1526)
|
||||||
- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
|
- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
|
||||||
- Allow send multiple team invitations at once [Taiga #2798](https://tree.taiga.io/project/penpot/us/2798)
|
- Allow send multiple team invitations at once [Taiga #2798](https://tree.taiga.io/project/penpot/us/2798)
|
||||||
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
|
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
|
||||||
|
|
|
@ -249,6 +249,8 @@
|
||||||
(update :data assoc :pages [page-id]))))
|
(update :data assoc :pages [page-id]))))
|
||||||
|
|
||||||
(declare strip-frames-with-thumbnails)
|
(declare strip-frames-with-thumbnails)
|
||||||
|
(declare extract-file-thumbnail)
|
||||||
|
(declare get-first-page-data)
|
||||||
|
|
||||||
(s/def ::strip-frames-with-thumbnails ::us/boolean)
|
(s/def ::strip-frames-with-thumbnails ::us/boolean)
|
||||||
|
|
||||||
|
@ -256,16 +258,37 @@
|
||||||
(s/keys :req-un [::profile-id ::file-id]
|
(s/keys :req-un [::profile-id ::file-id]
|
||||||
:opt-un [::strip-frames-with-thumbnails]))
|
:opt-un [::strip-frames-with-thumbnails]))
|
||||||
|
|
||||||
|
(s/def ::file-data-for-thumbnail
|
||||||
|
(s/keys :req-un [::profile-id ::file-id]
|
||||||
|
:opt-un [::strip-frames-with-thumbnails]))
|
||||||
|
|
||||||
|
(sv/defmethod ::file-data-for-thumbnail
|
||||||
|
"Retrieves the data for generate the thumbnail of the file. Used mainly for render
|
||||||
|
thumbnails on dashboard."
|
||||||
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
|
||||||
|
(check-read-permissions! pool profile-id file-id)
|
||||||
|
(p/let [file (retrieve-file cfg file-id)
|
||||||
|
data (get-first-page-data file props)
|
||||||
|
file-thumbnail (extract-file-thumbnail (get-in file [:data :pages-index]))]
|
||||||
|
|
||||||
|
(assoc data :file-thumbnail file-thumbnail)))
|
||||||
|
|
||||||
(sv/defmethod ::page
|
(sv/defmethod ::page
|
||||||
"Retrieves the first page of the file. Used mainly for render
|
"Retrieves the first page of the file. Used mainly for render
|
||||||
thumbnails on dashboard."
|
thumbnails on dashboard."
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
|
||||||
(check-read-permissions! pool profile-id file-id)
|
(check-read-permissions! pool profile-id file-id)
|
||||||
(p/let [file (retrieve-file cfg file-id)
|
(p/let [file (retrieve-file cfg file-id)
|
||||||
page-id (get-in file [:data :pages 0])]
|
data (get-first-page-data file props)]
|
||||||
(cond-> (get-in file [:data :pages-index page-id])
|
data))
|
||||||
(true? (:strip-frames-with-thumbnails props))
|
|
||||||
(strip-frames-with-thumbnails))))
|
(defn get-first-page-data
|
||||||
|
[file props]
|
||||||
|
(let [page-id (get-in file [:data :pages 0])
|
||||||
|
data (cond-> (get-in file [:data :pages-index page-id])
|
||||||
|
(true? (:strip-frames-with-thumbnails props))
|
||||||
|
(strip-frames-with-thumbnails))]
|
||||||
|
data))
|
||||||
|
|
||||||
(defn strip-frames-with-thumbnails
|
(defn strip-frames-with-thumbnails
|
||||||
"Remove unnecesary shapes from frames that have thumbnail."
|
"Remove unnecesary shapes from frames that have thumbnail."
|
||||||
|
@ -295,6 +318,16 @@
|
||||||
(update data :objects update-objects)))
|
(update data :objects update-objects)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn extract-file-thumbnail
|
||||||
|
"Extract the frame marked as file-thumbnail"
|
||||||
|
[pages]
|
||||||
|
(->> pages
|
||||||
|
vals
|
||||||
|
(mapcat :objects)
|
||||||
|
vals
|
||||||
|
(filter :file-thumbnail)
|
||||||
|
first))
|
||||||
|
|
||||||
;; --- Query: Shared Library Files
|
;; --- Query: Shared Library Files
|
||||||
|
|
||||||
(def ^:private sql:team-shared-files
|
(def ^:private sql:team-shared-files
|
||||||
|
|
|
@ -1061,6 +1061,36 @@
|
||||||
(let [selected (wsh/lookup-selected state)]
|
(let [selected (wsh/lookup-selected state)]
|
||||||
(rx/of (dch/update-shapes selected #(update % :blocked not)))))))
|
(rx/of (dch/update-shapes selected #(update % :blocked not)))))))
|
||||||
|
|
||||||
|
(defn extract-file-thumbnails-from-page
|
||||||
|
[state selected page]
|
||||||
|
(let [extract-frames (fn [page-id]
|
||||||
|
(let [objects (wsh/lookup-page-objects state page-id)]
|
||||||
|
(cph/get-frames objects)))
|
||||||
|
page-id (key page)
|
||||||
|
frames-with-thumbnail (->> (extract-frames page-id)
|
||||||
|
(filter (comp true? :file-thumbnail))
|
||||||
|
(map :id)
|
||||||
|
(remove #(some #{%} selected))
|
||||||
|
(map #(into {} {:id % :page-id page-id})))]
|
||||||
|
(when frames-with-thumbnail frames-with-thumbnail)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn toggle-file-thumbnail-selected
|
||||||
|
[]
|
||||||
|
(ptk/reify ::toggle-file-thumbnail-selected
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [selected (wsh/lookup-selected state)
|
||||||
|
pages (get-in state [:workspace-data
|
||||||
|
:pages-index])
|
||||||
|
file-thumbnails (->> pages
|
||||||
|
(mapcat #(extract-file-thumbnails-from-page state selected %)))]
|
||||||
|
(rx/concat
|
||||||
|
(rx/from
|
||||||
|
(for [ft file-thumbnails]
|
||||||
|
(dch/update-shapes [(:id ft)] #(update % :file-thumbnail not) (:page-id ft) nil)))
|
||||||
|
(rx/of (dch/update-shapes selected #(update % :file-thumbnail not))))))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Navigation
|
;; Navigation
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -71,9 +71,10 @@
|
||||||
(update :undo-changes conj (assoc change :operations uops)))))
|
(update :undo-changes conj (assoc change :operations uops)))))
|
||||||
|
|
||||||
(defn update-shapes
|
(defn update-shapes
|
||||||
([ids f] (update-shapes ids f nil))
|
([ids f] (update-shapes ids f nil nil))
|
||||||
([ids f {:keys [reg-objects? save-undo? attrs ignore-tree]
|
([ids f keys] (update-shapes ids f nil keys))
|
||||||
:or {reg-objects? false save-undo? true attrs nil}}]
|
([ids f page-id {:keys [reg-objects? save-undo? attrs ignore-tree]
|
||||||
|
:or {reg-objects? false save-undo? true attrs nil}}]
|
||||||
|
|
||||||
(us/assert ::coll-of-uuid ids)
|
(us/assert ::coll-of-uuid ids)
|
||||||
(us/assert fn? f)
|
(us/assert fn? f)
|
||||||
|
@ -81,8 +82,8 @@
|
||||||
(ptk/reify ::update-shapes
|
(ptk/reify ::update-shapes
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [it state _]
|
(watch [it state _]
|
||||||
(let [page-id (:current-page-id state)
|
(let [page-id (or page-id (:current-page-id state))
|
||||||
objects (wsh/lookup-page-objects state)
|
objects (wsh/lookup-page-objects state page-id)
|
||||||
changes {:redo-changes []
|
changes {:redo-changes []
|
||||||
:undo-changes []
|
:undo-changes []
|
||||||
:origin it
|
:origin it
|
||||||
|
@ -91,8 +92,8 @@
|
||||||
ids (into [] (filter some?) ids)
|
ids (into [] (filter some?) ids)
|
||||||
|
|
||||||
changes (reduce
|
changes (reduce
|
||||||
#(update-shape-changes %1 page-id objects f attrs %2 (get ignore-tree %2))
|
#(update-shape-changes %1 page-id objects f attrs %2 (get ignore-tree %2))
|
||||||
changes ids)]
|
changes ids)]
|
||||||
|
|
||||||
(when-not (empty? (:redo-changes changes))
|
(when-not (empty? (:redo-changes changes))
|
||||||
(let [reg-objs {:type :reg-objects
|
(let [reg-objs {:type :reg-objects
|
||||||
|
|
|
@ -358,7 +358,12 @@
|
||||||
|
|
||||||
:toggle-focus-mode {:command "f"
|
:toggle-focus-mode {:command "f"
|
||||||
:tooltip "F"
|
:tooltip "F"
|
||||||
:fn #(st/emit! (dw/toggle-focus-mode))}})
|
:fn #(st/emit! (dw/toggle-focus-mode))}
|
||||||
|
|
||||||
|
:thumbnail-set {:tooltip (ds/shift "T")
|
||||||
|
:command "shift+t"
|
||||||
|
:fn #(st/emit! (dw/toggle-file-thumbnail-selected))}})
|
||||||
|
|
||||||
|
|
||||||
(def opacity-shortcuts
|
(def opacity-shortcuts
|
||||||
(into {} (->>
|
(into {} (->>
|
||||||
|
|
|
@ -215,6 +215,31 @@
|
||||||
[:& shape-wrapper {:shape item
|
[:& shape-wrapper {:shape item
|
||||||
:key (:id item)}])))]]]))
|
:key (:id item)}])))]]]))
|
||||||
|
|
||||||
|
(mf/defc file-thumbnail-svg
|
||||||
|
{::mf/wrap [mf/memo]}
|
||||||
|
[{:keys [data embed? include-metadata?] :as props
|
||||||
|
:or {embed? false include-metadata? false}}]
|
||||||
|
(let [data (assoc data :x 0 :y 0)
|
||||||
|
vbox (format-viewbox {:width (:width data 0) :height (:height data 0)})
|
||||||
|
background-color (get-in data [:options :background] default-color)]
|
||||||
|
|
||||||
|
[:& (mf/provider embed/context) {:value embed?}
|
||||||
|
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
|
||||||
|
[:svg {:view-box vbox
|
||||||
|
:version "1.1"
|
||||||
|
:xmlns "http://www.w3.org/2000/svg"
|
||||||
|
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||||
|
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
|
||||||
|
:style {:width "100%"
|
||||||
|
:height "100%"
|
||||||
|
:background background-color}}
|
||||||
|
|
||||||
|
(when include-metadata?
|
||||||
|
[:& export/export-page {:options (:options data)}])
|
||||||
|
|
||||||
|
[:> shape-container {:shape data}
|
||||||
|
[:& frame/frame-thumbnail {:shape data}]]]]]))
|
||||||
|
|
||||||
(mf/defc frame-svg
|
(mf/defc frame-svg
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
|
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
|
||||||
|
|
|
@ -38,12 +38,12 @@
|
||||||
(def ^:const CACHE-NAME "penpot")
|
(def ^:const CACHE-NAME "penpot")
|
||||||
(def ^:const CACHE-URL "https://penpot.app/cache/")
|
(def ^:const CACHE-URL "https://penpot.app/cache/")
|
||||||
|
|
||||||
|
|
||||||
(defn use-thumbnail-cache
|
(defn use-thumbnail-cache
|
||||||
"Creates some hooks to handle the files thumbnails cache"
|
"Creates some hooks to handle the files thumbnails cache"
|
||||||
[file]
|
[file]
|
||||||
|
|
||||||
(let [cache-url (str CACHE-URL (:id file) "/" (:revn file) ".svg")
|
(let [cache-url (str CACHE-URL (:id file) "/" (:revn file) ".svg")
|
||||||
|
|
||||||
get-thumbnail
|
get-thumbnail
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps cache-url)
|
(mf/deps cache-url)
|
||||||
|
@ -83,14 +83,12 @@
|
||||||
(if (some? thumb-data)
|
(if (some? thumb-data)
|
||||||
(rx/of thumb-data)
|
(rx/of thumb-data)
|
||||||
(->> (wrk/ask! {:cmd :thumbnails/generate
|
(->> (wrk/ask! {:cmd :thumbnails/generate
|
||||||
:file-id (:id file)
|
:file-id (:id file)})
|
||||||
:page-id (get-in file [:data :pages 0])})
|
|
||||||
(rx/tap cache-thumbnail)))))
|
(rx/tap cache-thumbnail)))))
|
||||||
|
|
||||||
;; If we have a problem we delegate to the thumbnail generation
|
;; If we have a problem we delegate to the thumbnail generation
|
||||||
(rx/catch #(wrk/ask! {:cmd :thumbnails/generate
|
(rx/catch #(wrk/ask! {:cmd :thumbnails/generate
|
||||||
:file-id (:id file)
|
:file-id (:id file)})))))))
|
||||||
:page-id (get-in file [:data :pages 0])})))))))
|
|
||||||
|
|
||||||
(mf/defc grid-item-thumbnail
|
(mf/defc grid-item-thumbnail
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
|
|
|
@ -164,6 +164,22 @@
|
||||||
:on-click do-flip-horizontal}]
|
:on-click do-flip-horizontal}]
|
||||||
[:& menu-separator]]))
|
[:& menu-separator]]))
|
||||||
|
|
||||||
|
(mf/defc context-menu-thumbnail
|
||||||
|
[{:keys [shapes]}]
|
||||||
|
(let [single? (= (count shapes) 1)
|
||||||
|
has-frame? (->> shapes (d/seek #(= :frame (:type %))))
|
||||||
|
is-frame? (and single? has-frame?)
|
||||||
|
do-toggle-thumbnail (st/emitf (dw/toggle-file-thumbnail-selected))]
|
||||||
|
(when is-frame?
|
||||||
|
[:*
|
||||||
|
(if (every? :file-thumbnail shapes)
|
||||||
|
[:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-remove")
|
||||||
|
:on-click do-toggle-thumbnail}]
|
||||||
|
[:& menu-entry {:title (tr "workspace.shape.menu.thumbnail-set")
|
||||||
|
:shortcut (sc/get-tooltip :thumbnail-set)
|
||||||
|
:on-click do-toggle-thumbnail}])
|
||||||
|
[:& menu-separator]])))
|
||||||
|
|
||||||
(mf/defc context-menu-group
|
(mf/defc context-menu-group
|
||||||
[{:keys [shapes]}]
|
[{:keys [shapes]}]
|
||||||
|
|
||||||
|
@ -436,6 +452,7 @@
|
||||||
[:> context-menu-edit props]
|
[:> context-menu-edit props]
|
||||||
[:> context-menu-layer-position props]
|
[:> context-menu-layer-position props]
|
||||||
[:> context-menu-flip props]
|
[:> context-menu-flip props]
|
||||||
|
[:> context-menu-thumbnail props]
|
||||||
[:> context-menu-group props]
|
[:> context-menu-group props]
|
||||||
[:> context-focus-mode-menu props]
|
[:> context-focus-mode-menu props]
|
||||||
[:> context-menu-path props]
|
[:> context-menu-path props]
|
||||||
|
|
|
@ -29,11 +29,10 @@
|
||||||
(rx/throw {:type :unexpected
|
(rx/throw {:type :unexpected
|
||||||
:code (:error response)})))
|
:code (:error response)})))
|
||||||
|
|
||||||
(defn- request-page
|
(defn- request-thumbnail
|
||||||
[file-id page-id]
|
[file-id]
|
||||||
(let [uri (u/join (cfg/get-public-uri) "api/rpc/query/page")
|
(let [uri (u/join (cfg/get-public-uri) "api/rpc/query/file-data-for-thumbnail")
|
||||||
params {:file-id file-id
|
params {:file-id file-id
|
||||||
:id page-id
|
|
||||||
:strip-frames-with-thumbnails true}]
|
:strip-frames-with-thumbnails true}]
|
||||||
(->> (http/send!
|
(->> (http/send!
|
||||||
{:method :get
|
{:method :get
|
||||||
|
@ -45,20 +44,23 @@
|
||||||
|
|
||||||
(defonce cache (atom {}))
|
(defonce cache (atom {}))
|
||||||
|
|
||||||
(defn render-page
|
(defn render-frame
|
||||||
[data ckey]
|
[data ckey]
|
||||||
(let [prev (get @cache ckey)]
|
(let [prev (get @cache ckey)]
|
||||||
(if (= (:data prev) data)
|
(if (= (:data prev) data)
|
||||||
(:result prev)
|
(:result prev)
|
||||||
(let [elem (mf/element render/page-svg #js {:data data :width "290" :height "150" :thumbnails? true})
|
(let [file-thumbnail (:file-thumbnail data)
|
||||||
|
elem (if file-thumbnail
|
||||||
|
(mf/element render/file-thumbnail-svg #js {:data file-thumbnail :width "290" :height "150"})
|
||||||
|
(mf/element render/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}))
|
||||||
result (rds/renderToStaticMarkup elem)]
|
result (rds/renderToStaticMarkup elem)]
|
||||||
(swap! cache assoc ckey {:data data :result result})
|
(swap! cache assoc ckey {:data data :result result})
|
||||||
result))))
|
result))))
|
||||||
|
|
||||||
(defmethod impl/handler :thumbnails/generate
|
(defmethod impl/handler :thumbnails/generate
|
||||||
[{:keys [file-id page-id] :as message}]
|
[{:keys [file-id] :as message}]
|
||||||
(->> (request-page file-id page-id)
|
(->> (request-thumbnail file-id)
|
||||||
(rx/map
|
(rx/map
|
||||||
(fn [data]
|
(fn [data]
|
||||||
{:svg (render-page data #{file-id page-id})
|
{:svg (render-frame data #{file-id})
|
||||||
:fonts @fonts/loaded}))))
|
:fonts @fonts/loaded}))))
|
||||||
|
|
|
@ -3265,6 +3265,12 @@ msgstr "Flip horizontal"
|
||||||
msgid "workspace.shape.menu.flip-vertical"
|
msgid "workspace.shape.menu.flip-vertical"
|
||||||
msgstr "Flip vertical"
|
msgstr "Flip vertical"
|
||||||
|
|
||||||
|
msgid "workspace.shape.menu.thumbnail-set"
|
||||||
|
msgstr "Set as thumbnail"
|
||||||
|
|
||||||
|
msgid "workspace.shape.menu.thumbnail-remove"
|
||||||
|
msgstr "Remove thumbnail"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/context_menu.cljs
|
#: src/app/main/ui/workspace/context_menu.cljs
|
||||||
msgid "workspace.shape.menu.flow-start"
|
msgid "workspace.shape.menu.flow-start"
|
||||||
msgstr "Flow start"
|
msgstr "Flow start"
|
||||||
|
|
|
@ -3279,6 +3279,12 @@ msgstr "Voltear horizontal"
|
||||||
msgid "workspace.shape.menu.flip-vertical"
|
msgid "workspace.shape.menu.flip-vertical"
|
||||||
msgstr "Voltear vertical"
|
msgstr "Voltear vertical"
|
||||||
|
|
||||||
|
msgid "workspace.shape.menu.thumbnail-set"
|
||||||
|
msgstr "Poner como miniatura"
|
||||||
|
|
||||||
|
msgid "workspace.shape.menu.thumbnail-remove"
|
||||||
|
msgstr "Quitar miniatura"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/context_menu.cljs
|
#: src/app/main/ui/workspace/context_menu.cljs
|
||||||
msgid "workspace.shape.menu.flow-start"
|
msgid "workspace.shape.menu.flow-start"
|
||||||
msgstr "Inicio de flujo"
|
msgstr "Inicio de flujo"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue