From 0a04a856da85b16fd755dcca05f0d4fd02ba4525 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 4 Mar 2022 15:36:17 +0100 Subject: [PATCH] :sparkles: Set an artboard as the file thumbnail --- CHANGES.md | 1 + backend/src/app/rpc/queries/files.clj | 41 +++++++++++++++++-- frontend/src/app/main/data/workspace.cljs | 30 ++++++++++++++ .../src/app/main/data/workspace/changes.cljs | 15 +++---- .../app/main/data/workspace/shortcuts.cljs | 7 +++- frontend/src/app/main/render.cljs | 25 +++++++++++ frontend/src/app/main/ui/dashboard/grid.cljs | 8 ++-- .../app/main/ui/workspace/context_menu.cljs | 17 ++++++++ frontend/src/app/worker/thumbnails.cljs | 20 +++++---- frontend/translations/en.po | 6 +++ frontend/translations/es.po | 6 +++ 11 files changed, 150 insertions(+), 26 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c322bd7eb..79fce9c8c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### :boom: Breaking changes ### :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) - 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) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index f7c666e6e..30e349e8f 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -249,6 +249,8 @@ (update :data assoc :pages [page-id])))) (declare strip-frames-with-thumbnails) +(declare extract-file-thumbnail) +(declare get-first-page-data) (s/def ::strip-frames-with-thumbnails ::us/boolean) @@ -256,16 +258,37 @@ (s/keys :req-un [::profile-id ::file-id] :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 "Retrieves the first page 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) - page-id (get-in file [:data :pages 0])] - (cond-> (get-in file [:data :pages-index page-id]) - (true? (:strip-frames-with-thumbnails props)) - (strip-frames-with-thumbnails)))) + data (get-first-page-data file props)] + data)) + +(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 "Remove unnecesary shapes from frames that have thumbnail." @@ -295,6 +318,16 @@ (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 (def ^:private sql:team-shared-files diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 6874f0ee1..608225f12 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1061,6 +1061,36 @@ (let [selected (wsh/lookup-selected state)] (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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index cd2aacd5a..2a7b058bb 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -71,9 +71,10 @@ (update :undo-changes conj (assoc change :operations uops))))) (defn update-shapes - ([ids f] (update-shapes ids f nil)) - ([ids f {:keys [reg-objects? save-undo? attrs ignore-tree] - :or {reg-objects? false save-undo? true attrs nil}}] + ([ids f] (update-shapes ids f nil nil)) + ([ids f keys] (update-shapes ids f nil keys)) + ([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 fn? f) @@ -81,8 +82,8 @@ (ptk/reify ::update-shapes ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state) + (let [page-id (or page-id (:current-page-id state)) + objects (wsh/lookup-page-objects state page-id) changes {:redo-changes [] :undo-changes [] :origin it @@ -91,8 +92,8 @@ ids (into [] (filter some?) ids) changes (reduce - #(update-shape-changes %1 page-id objects f attrs %2 (get ignore-tree %2)) - changes ids)] + #(update-shape-changes %1 page-id objects f attrs %2 (get ignore-tree %2)) + changes ids)] (when-not (empty? (:redo-changes changes)) (let [reg-objs {:type :reg-objects diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 5848bbb33..9a1e766f0 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -358,7 +358,12 @@ :toggle-focus-mode {:command "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 (into {} (->> diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index fa0864282..29d657e10 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -215,6 +215,31 @@ [:& shape-wrapper {:shape 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/wrap [mf/memo]} [{:keys [objects frame zoom] :or {zoom 1} :as props}] diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index c6d84161e..195fcee1d 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -38,12 +38,12 @@ (def ^:const CACHE-NAME "penpot") (def ^:const CACHE-URL "https://penpot.app/cache/") + (defn use-thumbnail-cache "Creates some hooks to handle the files thumbnails cache" [file] (let [cache-url (str CACHE-URL (:id file) "/" (:revn file) ".svg") - get-thumbnail (mf/use-callback (mf/deps cache-url) @@ -83,14 +83,12 @@ (if (some? thumb-data) (rx/of thumb-data) (->> (wrk/ask! {:cmd :thumbnails/generate - :file-id (:id file) - :page-id (get-in file [:data :pages 0])}) + :file-id (:id file)}) (rx/tap cache-thumbnail))))) ;; If we have a problem we delegate to the thumbnail generation (rx/catch #(wrk/ask! {:cmd :thumbnails/generate - :file-id (:id file) - :page-id (get-in file [:data :pages 0])}))))))) + :file-id (:id file)}))))))) (mf/defc grid-item-thumbnail {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 506726bb8..9b5f90038 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -164,6 +164,22 @@ :on-click do-flip-horizontal}] [:& 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 [{:keys [shapes]}] @@ -436,6 +452,7 @@ [:> context-menu-edit props] [:> context-menu-layer-position props] [:> context-menu-flip props] + [:> context-menu-thumbnail props] [:> context-menu-group props] [:> context-focus-mode-menu props] [:> context-menu-path props] diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index f5ad5c13d..f49e94b1e 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -29,11 +29,10 @@ (rx/throw {:type :unexpected :code (:error response)}))) -(defn- request-page - [file-id page-id] - (let [uri (u/join (cfg/get-public-uri) "api/rpc/query/page") +(defn- request-thumbnail + [file-id] + (let [uri (u/join (cfg/get-public-uri) "api/rpc/query/file-data-for-thumbnail") params {:file-id file-id - :id page-id :strip-frames-with-thumbnails true}] (->> (http/send! {:method :get @@ -45,20 +44,23 @@ (defonce cache (atom {})) -(defn render-page +(defn render-frame [data ckey] (let [prev (get @cache ckey)] (if (= (:data prev) data) (: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)] (swap! cache assoc ckey {:data data :result result}) result)))) (defmethod impl/handler :thumbnails/generate - [{:keys [file-id page-id] :as message}] - (->> (request-page file-id page-id) + [{:keys [file-id] :as message}] + (->> (request-thumbnail file-id) (rx/map (fn [data] - {:svg (render-page data #{file-id page-id}) + {:svg (render-frame data #{file-id}) :fonts @fonts/loaded})))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ad34b7d63..bcd036717 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3265,6 +3265,12 @@ msgstr "Flip horizontal" msgid "workspace.shape.menu.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 msgid "workspace.shape.menu.flow-start" msgstr "Flow start" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2483631d7..f557a462b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3279,6 +3279,12 @@ msgstr "Voltear horizontal" msgid "workspace.shape.menu.flip-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 msgid "workspace.shape.menu.flow-start" msgstr "Inicio de flujo"