diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 81c1f6767..6e6edabed 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -112,18 +112,21 @@ (let [xform (comp (map :objects) (mapcat vals) - (keep (fn [{:keys [type] :as obj}] - (case type - :path (get-in obj [:fill-image :id]) - :bool (get-in obj [:fill-image :id]) + (mapcat (fn [obj] ;; NOTE: because of some bug, we ended with ;; many shape types having the ability to ;; have fill-image attribute (which initially ;; designed for :path shapes). - :group (get-in obj [:fill-image :id]) - :image (get-in obj [:metadata :id]) - - nil)))) + (sequence + (keep :id) + (concat [(:fill-image obj) + (:metadata obj)] + (map :fill-image (:fills obj)) + (map :stroke-image (:strokes obj)) + (->> (:content obj) + (tree-seq map? :children) + (mapcat :fills) + (map :fill-image))))))) pages (concat (vals (:pages-index data)) (vals (:components data)))] diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 653491c52..eac02558c 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -364,6 +364,161 @@ (t/is (nil? (sto/get-object storage (:thumbnail-id fmo1)))) ))) +(t/deftest file-gc-image-fills-and-strokes + (letfn [(add-file-media-object [& {:keys [profile-id file-id]}] + (let [mfile {:filename "sample.jpg" + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg" + :size 312043} + params {::th/type :upload-file-media-object + ::rpc/profile-id profile-id + :file-id file-id + :is-local true + :name "testfile" + :content mfile} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out))) + + (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] + (let [params {::th/type :update-file + ::rpc/profile-id profile-id + :id file-id + :session-id (uuid/random) + :revn revn + :components-v2 true + :changes changes} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out)))] + + (let [storage (:app.storage/storage th/*system*) + + profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + + fmo1 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo2 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo3 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo4 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + fmo5 (add-file-media-object :profile-id (:id profile) :file-id (:id file)) + s-shid (uuid/random) + t-shid (uuid/random) + + page-id (first (get-in file [:data :pages]))] + + ;; Update file inserting a new image object + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :changes + [{:type :add-obj + :page-id page-id + :id s-shid + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id s-shid + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :image + :metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"} + :fills [{:opacity 1 :fill-image {:id (:id fmo2) :width 100 :height 100 :mtype "image/jpeg"}}] + :strokes [{:opacity 1 :stroke-image {:id (:id fmo3) :width 100 :height 100 :mtype "image/jpeg"}}]})} + {:type :add-obj + :page-id page-id + :id t-shid + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id t-shid + :name "text" + :frame-id uuid/zero + :parent-id uuid/zero + :type :text + :content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" + :children [{:fills [{:fill-opacity 1 + :fill-image {:id (:id fmo4) + :width 417 + :height 354 + :mtype "image/png" + :name "text fill image"}}] + :text "hi"} + {:fills [{:fill-opacity 1 + :fill-color "#000000"}] + :text "bye"}]}]}]} + :strokes [{:opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}]) + + ;; run the file-gc task immediately without forced min-age + (let [res (th/run-task! "file-gc")] + (t/is (= 0 (:processed res)))) + + ;; run the task again + (let [res (th/run-task! "file-gc" {:min-age 0})] + (t/is (= 1 (:processed res)))) + + ;; retrieve file and check trimmed attribute + (let [row (th/db-get :file {:id (:id file)})] + (t/is (true? (:has-media-trimmed row)))) + + ;; check file media objects + (let [rows (th/db-exec! ["select * from file_media_object where file_id = ?" (:id file)])] + (t/is (= 5 (count rows)))) + + ;; The underlying storage objects are still available. + (t/is (some? (sto/get-object storage (:media-id fmo5)))) + (t/is (some? (sto/get-object storage (:media-id fmo4)))) + (t/is (some? (sto/get-object storage (:media-id fmo3)))) + (t/is (some? (sto/get-object storage (:media-id fmo2)))) + (t/is (some? (sto/get-object storage (:media-id fmo1)))) + + ;; proceed to remove usage of the file + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :changes [{:type :del-obj + :page-id (first (get-in file [:data :pages])) + :id s-shid} + {:type :del-obj + :page-id (first (get-in file [:data :pages])) + :id t-shid}]) + + ;; Now, we have deleted the usage of pointers to the + ;; file-media-objects, if we paste file-gc, they should be marked + ;; as deleted. + (let [task (:app.tasks.file-gc/handler th/*system*) + res (task {:min-age (dt/duration 0)})] + (t/is (= 1 (:processed res)))) + + ;; Now that file-gc have deleted the file-media-object usage, + ;; lets execute the touched-gc task, we should see that two of + ;; them are marked to be deleted. + (let [task (:app.storage/gc-touched-task th/*system*) + res (task {:min-age (dt/duration 0)})] + (t/is (= 0 (:freeze res))) + (t/is (= 2 (:delete res)))) + + ;; Finally, check that some of the objects that are marked as + ;; deleted we are unable to retrieve them using standard storage + ;; public api. + (t/is (nil? (sto/get-object storage (:media-id fmo5)))) + (t/is (nil? (sto/get-object storage (:media-id fmo4)))) + (t/is (nil? (sto/get-object storage (:media-id fmo3)))) + (t/is (nil? (sto/get-object storage (:media-id fmo2)))) + (t/is (nil? (sto/get-object storage (:media-id fmo1))))))) + (t/deftest permissions-checks-creating-file (let [profile1 (th/create-profile* 1) profile2 (th/create-profile* 2) diff --git a/common/src/app/common/svg/shapes_builder.cljc b/common/src/app/common/svg/shapes_builder.cljc index d14eafeca..632b1483d 100644 --- a/common/src/app/common/svg/shapes_builder.cljc +++ b/common/src/app/common/svg/shapes_builder.cljc @@ -329,7 +329,8 @@ image-url (or (:href attrs) (:xlink:href attrs)) image-data (dm/get-in svg-data [:image-data image-url]) - metadata {:width (:width image-data) + metadata {:name name + :width (:width image-data) :height (:height image-data) :mtype (:mtype image-data) :id (:id image-data)} @@ -344,9 +345,11 @@ (when (some? image-data) (cts/setup-shape (-> (calculate-rect-metadata rect transform) - (assoc :type :image) + (assoc :type :rect) (assoc :name name) (assoc :frame-id frame-id) + (assoc :fills [{:fill-opacity 1 + :fill-image metadata}]) (assoc :metadata metadata) (assoc :svg-viewbox rect) (assoc :svg-attrs props)))))) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index 7af88eb33..4c1b988f5 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -46,6 +46,14 @@ ::oapi/type "integer" ::oapi/format "int64"}}) +(sm/def! ::image-color + [:map {:title "ImageColor"} + [:name {:optional true} :string] + [:width :int] + [:height :int] + [:mtype {:optional true} [:maybe :string]] + [:id ::sm/uuid]]) + (sm/def! ::gradient [:map {:title "Gradient"} [:type [::sm/one-of #{:linear :radial "linear" "radial"}]] @@ -72,17 +80,19 @@ [:modified-at {:optional true} ::sm/inst] [:ref-id {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid] - [:gradient {:optional true} [:maybe ::gradient]]]) + [:gradient {:optional true} [:maybe ::gradient]] + [:image {:optional true} [:maybe ::image-color]]]) ;; FIXME: incomplete schema (sm/def! ::recent-color [:and - [:map {:title "RecentColot"} + [:map {:title "RecentColor"} [:opacity {:optional true} [:maybe ::sm/safe-number]] [:color {:optional true} [:maybe ::rgb-color]] - [:gradient {:optional true} [:maybe ::gradient]]] - [::sm/contains-any {:strict true} [:color :gradient]]]) + [:gradient {:optional true} [:maybe ::gradient]] + [:image {:optional true} [:maybe ::image-color]]] + [::sm/contains-any {:strict true} [:color :gradient :image]]]) (def color? (sm/pred-fn ::color)) @@ -102,17 +112,19 @@ {:color (:fill-color fill) :opacity (:fill-opacity fill) :gradient (:fill-color-gradient fill) + :image (:fill-image fill) :ref-id (:fill-color-ref-id fill) :ref-file (:fill-color-ref-file fill)})) (defn set-fill-color - [shape position color opacity gradient] + [shape position color opacity gradient image] (update-in shape [:fills position] (fn [fill] (d/without-nils (assoc fill :fill-color color :fill-opacity opacity - :fill-color-gradient gradient))))) + :fill-color-gradient gradient + :fill-image image))))) (defn attach-fill-color [shape position ref-id ref-file] @@ -133,17 +145,19 @@ (d/without-nils {:color (:stroke-color stroke) :opacity (:stroke-opacity stroke) :gradient (:stroke-color-gradient stroke) + :image (:stroke-image stroke) :ref-id (:stroke-color-ref-id stroke) :ref-file (:stroke-color-ref-file stroke)})) (defn set-stroke-color - [shape position color opacity gradient] + [shape position color opacity gradient image] (update-in shape [:strokes position] (fn [stroke] (d/without-nils (assoc stroke :stroke-color color :stroke-opacity opacity - :stroke-color-gradient gradient))))) + :stroke-color-gradient gradient + :stroke-image image))))) (defn attach-stroke-color [shape position ref-id ref-file] @@ -336,7 +350,8 @@ position (:color library-color) (:opacity library-color) - (:gradient library-color)) + (:gradient library-color) + (:image library-color)) (detach-fn shape position))) shape))] diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 37c8bbe31..3944f2af9 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -105,7 +105,8 @@ [:fill-opacity {:optional true} ::sm/safe-number] [:fill-color-gradient {:optional true} [:maybe ::ctc/gradient]] [:fill-color-ref-file {:optional true} [:maybe ::sm/uuid]] - [:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]]) + [:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]] + [:fill-image {:optional true} ::ctc/image-color]]) (sm/def! ::stroke [:map {:title "Stroke"} @@ -122,7 +123,8 @@ [::sm/one-of stroke-caps]] [:stroke-cap-end {:optional true} [::sm/one-of stroke-caps]] - [:stroke-color-gradient {:optional true} ::ctc/gradient]]) + [:stroke-color-gradient {:optional true} ::ctc/gradient] + [:stroke-image {:optional true} ::ctc/image-color]]) (sm/def! ::minimal-shape-attrs [:map {:title "ShapeMinimalRecord"} @@ -346,6 +348,13 @@ (def valid-shape? (sm/pred-fn ::shape)) + +(defn has-images? + [{:keys [fills strokes]}] + (or + (some :fill-image fills) + (some :stroke-image strokes))) + ;; --- Initialization (def ^:private minimal-rect-attrs diff --git a/frontend/resources/images/colorpicker-no-image.png b/frontend/resources/images/colorpicker-no-image.png new file mode 100644 index 000000000..2ba94601b Binary files /dev/null and b/frontend/resources/images/colorpicker-no-image.png differ diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index 74600eee5..2ad36757d 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -21,6 +21,7 @@ .top-actions { display: flex; margin-bottom: $size-1; + flex-direction: row-reverse; justify-content: space-between; .picker-btn { @@ -38,8 +39,44 @@ height: 14px; } } + + .element-set-content { + width: auto; + padding: 0.25rem 0; + .custom-select { + border: none; + &:hover { + border: none; + } + .custom-select-dropdown { + left: auto; + right: 0; + } + } + } } + .select-image { + .content { + display: flex; + justify-content: center; + background-image: url("/images/colorpicker-no-image.png"); + background-position: center; + background-size: auto 6.75rem; + height: 6.75rem; + img { + height: fit-content; + width: fit-content; + max-height: 100%; + max-width: 100%; + margin: auto; + } + } + button { + width: 100%; + margin-top: 10px; + } + } .gradients-buttons { .gradient { cursor: pointer; diff --git a/frontend/resources/styles/main/partials/inspect.scss b/frontend/resources/styles/main/partials/inspect.scss index b2f8a9648..bc83f6c05 100644 --- a/frontend/resources/styles/main/partials/inspect.scss +++ b/frontend/resources/styles/main/partials/inspect.scss @@ -143,6 +143,8 @@ .color-text { width: 3rem; text-transform: uppercase; + text-overflow: ellipsis; + overflow: hidden; } .attributes-color-display { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index dabc8ecf6..89e3c86a7 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1530,27 +1530,45 @@ ;; for retrieve the image data and convert it to the ;; data-url. (prepare-object [objects parent-frame-id {:keys [type] :as obj}] - (let [obj (maybe-translate obj objects parent-frame-id)] - (if (= type :image) - (let [url (cf/resolve-file-media (:metadata obj))] - (->> (http/send! {:method :get - :uri url - :response-type :blob}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/map #(assoc obj ::data %)) - (rx/take 1))) + (let [obj (maybe-translate obj objects parent-frame-id) + ;; Texts can have different fills for pieces of the text + fill-images-data (->> (or (:position-data obj) [obj]) + (map :fills) + (reduce into []) + (filter :fill-image) + (map :fill-image)) + + stroke-images-data (->> (:strokes obj) + (filter :stroke-image) + (map :stroke-image)) + images-data (concat + fill-images-data + stroke-images-data + (when (= type :image) + [(:metadata obj)]))] + + (if (> (count images-data) 0) + (->> (rx/from images-data) + (rx/mapcat (fn [image-data] + (let [url (cf/resolve-file-media image-data)] + (->> (http/send! {:method :get + :uri url + :response-type :blob}) + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url) + (rx/map #(assoc image-data :data %)))))) + (rx/reduce conj []) + (rx/map + (fn [images] + (assoc obj ::data images)))) (rx/of obj)))) ;; Collects all the items together and split images into a ;; separated data structure for a more easy paste process. - (collect-data [res {:keys [id metadata] :as item}] + (collect-data [res {:keys [id] :as item}] (let [res (update res :objects assoc id (dissoc item ::data))] - (if (= :image (:type item)) - (let [img-part {:id (:id metadata) - :name (:name item) - :file-data (::data item)}] - (update res :images conj img-part)) + (if (::data item) + (update res :images into (::data item)) res))) (maybe-translate [shape objects parent-frame-id] @@ -1713,7 +1731,7 @@ (letfn [;; Given a file-id and img (part generated by the ;; copy-selected event), uploads the new media. (upload-media [file-id imgpart] - (->> (http/send! {:uri (:file-data imgpart) + (->> (http/send! {:uri (:data imgpart) :response-type :blob :method :get}) (rx/map :body) @@ -1727,19 +1745,43 @@ (rx/map (fn [media] (assoc media :prev-id (:id imgpart)))))) + (translate-staled-media [mdata attribute media-idx] + (let [id (get-in mdata [attribute :id]) + mobj (get media-idx id)] + (if mobj + (update mdata attribute #(assoc % + :id (:id mobj) + :path (:path mobj))) + mdata))) + ;; Analyze the rchange and replace staled media and ;; references to the new uploaded media-objects. (process-rchange [media-idx item] - (if (and (= (:type item) :add-obj) - (= :image (get-in item [:obj :type]))) - (update-in item [:obj :metadata] - (fn [{:keys [id] :as mdata}] - (if-let [mobj (get media-idx id)] - (assoc mdata - :id (:id mobj) - :path (:path mobj)) - mdata))) - item)) + (let [;; Texts can have different fills for pieces of the text + obj (:obj item) + fills (mapv #(translate-staled-media % :fill-image media-idx) (:fills obj)) + strokes (mapv #(translate-staled-media % :stroke-image media-idx) (:strokes obj)) + position-data (->> (:position-data obj) + (mapv (fn [p-data] + (let [fills (mapv #(translate-staled-media % :fill-image media-idx) (:fills p-data))] + (assoc p-data :fills fills))))) + content (txt/transform-nodes #(translate-staled-media % :fill-image media-idx) (:content obj))] + + (if (= (:type item) :add-obj) + (-> item + (update-in [:obj :metadata] + (fn [{:keys [id] :as mdata}] + (if-let [mobj (get media-idx id)] + (assoc mdata + :id (:id mobj) + :path (:path mobj)) + mdata))) + (assoc-in [:obj :fills] fills) + (assoc-in [:obj :strokes] strokes) + (assoc-in [:obj :content] content) + (cond-> + (> (count position-data) 0) (assoc-in [:obj :position-data] position-data))) + item))) (calculate-paste-position [state mouse-pos in-viewport?] (let [page-objects (wsh/lookup-page-objects state) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 77a42e088..eba8fbab6 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -105,6 +105,9 @@ (contains? color :opacity) (assoc :fill-opacity (:opacity color)) + (contains? color :image) + (assoc :fill-image (:image color)) + :always (d/without-nils)) @@ -223,9 +226,15 @@ (assoc :stroke-color-gradient (:gradient attrs)) (contains? attrs :opacity) - (assoc :stroke-opacity (:opacity attrs))) + (assoc :stroke-opacity (:opacity attrs)) - attrs (merge attrs color-attrs)] + (contains? attrs :image) + (assoc :stroke-image (:image attrs))) + + attrs (-> + (merge attrs color-attrs) + (dissoc :image) + (dissoc :gradient))] (rx/of (dch/update-shapes ids @@ -455,7 +464,11 @@ (defn clear-color-components [data] - (dissoc data :hex :alpha :r :g :b :h :s :v)) + (dissoc data :hex :alpha :r :g :b :h :s :v :image)) + +(defn clear-image-components + [data] + (dissoc data :hex :alpha :r :g :b :h :s :v :color)) (defn- create-gradient [type] @@ -467,8 +480,14 @@ (defn get-color-from-colorpicker-state [{:keys [type current-color stops gradient] :as state}] - (if (= type :color) + (cond + (= type :color) (clear-color-components current-color) + + (= type :image) + (clear-image-components current-color) + + :else {:gradient (-> gradient (assoc :type (case type :linear-gradient :linear @@ -487,7 +506,7 @@ (on-change color))))) (defn initialize-colorpicker - [on-change] + [on-change tab] (ptk/reify ::initialize-colorpicker ptk/WatchEvent (watch [_ _ stream] @@ -502,7 +521,14 @@ (rx/filter (ptk/type? ::update-colorpicker-color) stream) (rx/filter (ptk/type? ::activate-colorpicker-gradient) stream)) (rx/map (constantly (colorpicker-onchange-runner on-change))) - (rx/take-until stoper)))))) + (rx/take-until stoper)))) + + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (-> state + (assoc :type tab))))))) (defn finalize-colorpicker [] @@ -522,13 +548,8 @@ (let [current-color (:current-color state)] (if (some? gradient) (let [stop (or (:editing-stop state) 0) - stops (mapv split-color-components (:stops gradient)) - type (case (:type gradient) - :linear :linear-gradient - :radial :radial-gradient - (:type state))] + stops (mapv split-color-components (:stops gradient))] (-> state - (assoc :type type) (assoc :current-color (nth stops stop)) (assoc :stops stops) (assoc :gradient (-> gradient @@ -537,7 +558,6 @@ (assoc :editing-stop stop))) (-> state - (assoc :type :color) (cond-> (or (nil? current-color) (not= (:color data) (:color current-color)) (not= (:opacity data) (:opacity current-color))) @@ -553,9 +573,11 @@ (update [_ state] (update state :colorpicker (fn [state] - (let [state (-> state + (let [type (:type state) + state (-> state (update :current-color merge changes) (update :current-color materialize-color-components) + (update :current-color #(if (not= type :image) (dissoc % :image) %)) ;; current color can be a library one I'm changing via colorpicker (d/dissoc-in [:current-color :id]) (d/dissoc-in [:current-color :file-id]))] @@ -564,7 +586,6 @@ (merge data) (materialize-color-components)))) (-> state - (assoc :type :color) (dissoc :gradient :stops :editing-stop))))))) ptk/WatchEvent (watch [_ state _] @@ -592,6 +613,17 @@ :editing-stop stop) state)))))) +(defn activate-colorpicker-color + [] + (ptk/reify ::activate-colorpicker-color + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (-> state + (assoc :type :color) + (dissoc :editing-stop :stops :gradient))))))) + (defn activate-colorpicker-gradient [type] (ptk/reify ::activate-colorpicker-gradient @@ -599,23 +631,32 @@ (update [_ state] (update state :colorpicker (fn [state] - (if (= type (:type state)) - (do - (-> state - (assoc :type :color) - (dissoc :editing-stop :stops :gradient))) - (let [gradient (create-gradient type) - color (:current-color state)] - (-> state - (assoc :type type) - (assoc :gradient gradient) - (cond-> (not (:stops state)) - (assoc :editing-stop 0 - :stops [(assoc color :offset 0) - (-> color - (assoc :alpha 0) - (assoc :offset 1) - (materialize-color-components))])))))))))) + (let [gradient (create-gradient type) + color (:current-color state)] + (-> state + (assoc :type type) + (assoc :gradient gradient) + (d/dissoc-in [:current-color :image]) + (cond-> (not (:stops state)) + (assoc :editing-stop 0 + :stops [(-> color + (assoc :offset 0) + (materialize-color-components)) + (-> color + (assoc :alpha 0) + (assoc :offset 1) + (materialize-color-components))]))))))))) + +(defn activate-colorpicker-image + [] + (ptk/reify ::activate-colorpicker-image + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (-> state + (assoc :type :image) + (dissoc :editing-stop :stops :gradient))))))) (defn select-color [position add-color] diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index c1fbac8df..e3e8b7004 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -97,7 +97,8 @@ (let [id (uuid/next) color (-> color (assoc :id id) - (assoc :name (or (:color color) + (assoc :name (or (get-in color [:image :name]) + (:color color) (uc/gradient-type->string (get-in color [:gradient :type])))))] (dm/assert! ::ctc/color color) (ptk/reify ::add-color diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index cac7ba07c..115a5c1da 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -78,11 +78,13 @@ :height height :x (mth/round (- x (/ width 2))) :y (mth/round (- y (/ height 2))) - :metadata {:width width - :height height - :mtype mtype - :id id}}] - (rx/of (dwsh/create-and-add-shape :image x y shape)))))) + :fills [{:fill-opacity 1 + :fill-image {:name name + :width width + :height height + :mtype mtype + :id id}}]}] + (rx/of (dwsh/create-and-add-shape :rect x y shape)))))) (defn svg-uploaded [svg-data file-id position] @@ -171,64 +173,64 @@ [:uris {:optional true} [:sequential :string]] [:mtype {:optional true} :string]]) +(defn handle-media-error [error on-error] + (if (ex/ex-info? error) + (handle-media-error (ex-data error) on-error) + (cond + (= (:code error) :invalid-svg-file) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :media-type-not-allowed) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :unable-to-access-to-url) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :invalid-image) + (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + + (= (:code error) :media-max-file-size-reached) + (rx/of (msg/error (tr "errors.media-too-large"))) + + (= (:code error) :media-type-mismatch) + (rx/of (msg/error (tr "errors.media-type-mismatch"))) + + (= (:code error) :unable-to-optimize) + (rx/of (msg/error (:hint error))) + + (fn? on-error) + (on-error error) + + :else + (do + (.error js/console "ERROR" error) + (rx/of (msg/error (tr "errors.cannot-upload"))))))) + (defn- process-media-objects [{:keys [uris on-error] :as params}] (dm/assert! - (and (sm/valid? schema:process-media-objects params) - (or (contains? params :blobs) - (contains? params :uris)))) + (and (sm/valid? schema:process-media-objects params) + (or (contains? params :blobs) + (contains? params :uris)))) - (letfn [(handle-error [error] - (if (ex/ex-info? error) - (handle-error (ex-data error)) - (cond - (= (:code error) :invalid-svg-file) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :media-type-not-allowed) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :unable-to-access-to-url) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :invalid-image) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) - - (= (:code error) :media-max-file-size-reached) - (rx/of (msg/error (tr "errors.media-too-large"))) - - (= (:code error) :media-type-mismatch) - (rx/of (msg/error (tr "errors.media-type-mismatch"))) - - (= (:code error) :unable-to-optimize) - (rx/of (msg/error (:hint error))) - - (fn? on-error) - (on-error error) - - :else - (do - (.error js/console "ERROR" error) - (rx/of (msg/error (tr "errors.cannot-upload")))))))] - - (ptk/reify ::process-media-objects - ptk/WatchEvent - (watch [_ _ _] - (rx/concat - (rx/of (msg/show {:content (tr "media.loading") - :type :info - :timeout nil - :tag :media-loading})) - (->> (if (seq uris) + (ptk/reify ::process-media-objects + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (rx/of (msg/show {:content (tr "media.loading") + :type :info + :timeout nil + :tag :media-loading})) + (->> (if (seq uris) ;; Media objects is a list of URL's pointing to the path - (process-uris params) + (process-uris params) ;; Media objects are blob of data to be upload - (process-blobs params)) + (process-blobs params)) ;; Every stream has its own sideeffect. We need to ignore the result - (rx/ignore) - (rx/catch handle-error) - (rx/finalize #(st/emit! (msg/hide-tag :media-loading))))))))) + (rx/ignore) + (rx/catch #(handle-media-error % on-error)) + (rx/finalize #(st/emit! (msg/hide-tag :media-loading)))))))) ;; Deprecated in components-v2 (defn upload-media-asset @@ -248,6 +250,35 @@ (process-media-objects params))) + +(defn upload-fill-image + [file on-success] + (dm/assert! + "expected a valid blob for `file` param" + (dmm/blob? file)) + (ptk/reify ::upload-fill-image + ptk/WatchEvent + (watch [_ state _] + (let [on-upload-success + (fn [image] + (on-success image) + (dmm/notify-finished-loading)) + + prepare + (fn [content] + {:file-id (get-in state [:workspace-file :id]) + :name (if (dmm/file? content) (.-name content) (tr "media.image")) + :is-local false + :content content})] + + (dmm/notify-start-loading) + (->> (rx/of file) + (rx/map dmm/validate-file) + (rx/map prepare) + (rx/mapcat #(rp/cmd! :upload-file-media-object %)) + (rx/do on-upload-success) + (rx/catch handle-media-error)))))) + ;; --- Upload File Media objects (defn load-and-parse-svg @@ -283,7 +314,7 @@ (defn create-shapes-img "Convert a media object that contains a bitmap image into shapes, - one shape of type :image and one group that contains it." + one shape of type :rect containing an image fill and one group that contains it." [pos {:keys [name width height id mtype] :as media-obj} & {:keys [wrapper-type] :or {wrapper-type :group}}] (let [group-shape (cts/setup-shape {:type wrapper-type @@ -296,15 +327,17 @@ :parent-id uuid/zero}) img-shape (cts/setup-shape - {:type :image + {:type :rect :x (:x pos) :y (:y pos) :width width :height height - :metadata {:id id - :width width - :height height - :mtype mtype} + :fills [{:fill-opacity 1 + :fill-image {:name name + :id id + :width width + :height height + :mtype mtype}}] :name name :frame-id uuid/zero :parent-id (:id group-shape)})] diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index d4a151435..23be4183a 100644 --- a/frontend/src/app/main/ui/components/color_bullet.cljs +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -6,8 +6,11 @@ (ns app.main.ui.components.color-bullet (:require + [app.config :as cfg] [app.util.color :as uc] [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc color-bullet @@ -31,8 +34,15 @@ :is-gradient (some? (:gradient color))) :on-click on-click :title (uc/get-color-name color)} - (if (:gradient color) + (cond + (:gradient color) [:div.color-bullet-wrapper {:style {:background (uc/color->background color)}}] + + (:image color) + (let [uri (cfg/resolve-file-media (:image color))] + [:div.color-bullet-wrapper {:style {:background-size "contain" :background-image (str/ffmt "url(%)" uri)}}]) + + :else [:div.color-bullet-wrapper [:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}] [:div.color-bullet-right {:style {:background (uc/color->background color)}}]])])))) @@ -40,10 +50,12 @@ (mf/defc color-name {::mf/wrap-props false} [{:keys [color size on-click on-double-click]}] - (let [{:keys [name color gradient]} (if (string? color) {:color color :opacity 1} color)] + (let [{:keys [name color gradient image]} (if (string? color) {:color color :opacity 1} color)] (when (or (not size) (= size :big)) [:span.color-text {:on-click on-click :on-double-click on-double-click :title name} - (or name color (uc/gradient-type->string (:type gradient)))]))) + (if (some? image) + (tr "media.image") + (or name color (uc/gradient-type->string (:type gradient))))]))) diff --git a/frontend/src/app/main/ui/components/color_bullet_new.cljs b/frontend/src/app/main/ui/components/color_bullet_new.cljs index 34686068e..4281edf62 100644 --- a/frontend/src/app/main/ui/components/color_bullet_new.cljs +++ b/frontend/src/app/main/ui/components/color_bullet_new.cljs @@ -7,7 +7,10 @@ (ns app.main.ui.components.color-bullet-new (:require-macros [app.main.style :as stl]) (:require + [app.config :as cfg] [app.util.color :as uc] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc color-bullet @@ -26,7 +29,8 @@ (let [color (if (string? color) {:color color :opacity 1} color) id (:id color) gradient (:gradient color) - opacity (:opacity color)] + opacity (:opacity color) + image (:image color)] [:div {:class (stl/css-case :color-bullet true @@ -38,21 +42,27 @@ :grid-area area) :on-click on-click} - (if (some? gradient) + (cond + (some? gradient) [:div {:class (stl/css :color-bullet-wrapper) :style {:background (uc/color->background color)}}] + (some? image) + (let [uri (cfg/resolve-file-media image)] + [:div {:class (stl/css :color-bullet-wrapper) + :style {:background-image (str/ffmt "url(%)" uri)}}]) + + :else [:div {:class (stl/css :color-bullet-wrapper)} [:div {:class (stl/css :color-bullet-left) :style {:background (uc/color->background (assoc color :opacity 1))}}] [:div {:class (stl/css :color-bullet-right) :style {:background (uc/color->background color)}}]])])))) - (mf/defc color-name {::mf/wrap-props false} [{:keys [color size on-click on-double-click]}] - (let [{:keys [name color gradient]} (if (string? color) {:color color :opacity 1} color)] + (let [{:keys [name color gradient image]} (if (string? color) {:color color :opacity 1} color)] (when (or (not size) (> size 64)) [:span {:class (stl/css-case :color-text (< size 72) @@ -60,4 +70,6 @@ :big-text (>= size 72)) :on-click on-click :on-double-click on-double-click} - (or name color (uc/gradient-type->string (:type gradient)))]))) + (if (some? image) + (tr "media.image") + (or name color (uc/gradient-type->string (:type gradient))))]))) diff --git a/frontend/src/app/main/ui/components/color_bullet_new.scss b/frontend/src/app/main/ui/components/color_bullet_new.scss index d47fa2321..bb8785b92 100644 --- a/frontend/src/app/main/ui/components/color_bullet_new.scss +++ b/frontend/src/app/main/ui/components/color_bullet_new.scss @@ -55,6 +55,9 @@ height: 100%; width: 100%; clip-path: circle(50%); + background-size: contain; + background-repeat: no-repeat; + background-position: center; } .color-bullet-wrapper > * { width: 100%; diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs index ab69c224d..af02b820f 100644 --- a/frontend/src/app/main/ui/components/shape_icon.cljs +++ b/frontend/src/app/main/ui/components/shape_icon.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.components.shape-icon (:require [app.common.types.component :as ctk] + [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.main.ui.icons :as i] [rumext.v2 :as mf])) @@ -32,10 +33,10 @@ :else i/artboard) :image i/image - :line i/line - :circle i/circle - :path i/curve - :rect i/box + :line (if (cts/has-images? shape) i/image i/line) + :circle (if (cts/has-images? shape) i/image i/circle) + :path (if (cts/has-images? shape) i/image i/curve) + :rect (if (cts/has-images? shape) i/image i/box) :text i/text :group (if (:masked-group shape) i/mask @@ -47,39 +48,3 @@ #_:default i/bool-union) :svg-raw i/file-svg nil))) - -(mf/defc element-icon-refactor - [{:keys [shape main-instance?] :as props}] - (if (ctk/instance-head? shape) - (if main-instance? - i/component-refactor - i/copy-refactor) - (case (:type shape) - :frame (cond - (and (ctl/flex-layout? shape) (ctl/col? shape)) - i/flex-vertical-refactor - - (and (ctl/flex-layout? shape) (ctl/row? shape)) - i/flex-horizontal-refactor - - (ctl/grid-layout? shape) - i/grid-refactor - - :else - i/board-refactor) - :image i/img-refactor - :line i/path-refactor - :circle i/elipse-refactor - :path i/curve-refactor - :rect i/rectangle-refactor - :text i/text-refactor - :group (if (:masked-group shape) - i/mask-refactor - i/group-refactor) - :bool (case (:bool-type shape) - :difference i/boolean-difference-refactor - :exclude i/boolean-exclude-refactor - :intersection i/boolean-intersection-refactor - #_:default i/boolean-union-refactor) - :svg-raw i/svg-refactor - nil))) diff --git a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs index 190117dd5..3ed99b6b9 100644 --- a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs +++ b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs @@ -7,11 +7,11 @@ (ns app.main.ui.components.shape-icon-refactor (:require [app.common.types.component :as ctk] + [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.main.ui.icons :as i] [rumext.v2 :as mf])) - (mf/defc element-icon-refactor {::mf/wrap-props false} [{:keys [shape main-instance?]}] @@ -33,10 +33,10 @@ i/board-refactor) ;; TODO -> THUMBNAIL ICON :image i/img-refactor - :line i/path-refactor - :circle i/elipse-refactor - :path i/path-refactor - :rect i/rectangle-refactor + :line (if (cts/has-images? shape) i/img-refactor i/path-refactor) + :circle (if (cts/has-images? shape) i/img-refactor i/elipse-refactor) + :path (if (cts/has-images? shape) i/img-refactor i/curve-refactor) + :rect (if (cts/has-images? shape) i/img-refactor i/rectangle-refactor) :text i/text-refactor :group (if (:masked-group shape) i/mask-refactor diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 164eff602..14bee9725 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -62,14 +62,14 @@ (defn add-fill! [attrs fill-data render-id index type] - (let [index (if (some? index) (dm/str "_" index) "")] + (let [index (if (some? index) (dm/str "-" index) "")] (cond (contains? fill-data :fill-image) (let [id (dm/str "fill-image-" render-id)] (obj/set! attrs "fill" (dm/str "url(#" id ")"))) (some? (:fill-color-gradient fill-data)) - (let [id (dm/str "fill-color-gradient_" render-id index)] + (let [id (dm/str "fill-color-gradient-" render-id index)] (obj/set! attrs "fill" (dm/str "url(#" id ")"))) (contains? fill-data :fill-color) @@ -100,7 +100,7 @@ (obj/set! attrs "strokeWidth" width) (when (some? gradient) - (let [gradient-id (dm/str "stroke-color-gradient_" render-id "_" index)] + (let [gradient-id (dm/str "stroke-color-gradient-" render-id "-" index)] (obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id)))) (when-not (some? gradient) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 339a7e669..6a0f1255a 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -13,8 +13,10 @@ [app.common.geom.shapes.bounds :as gsb] [app.common.geom.shapes.text :as gst] [app.common.pages.helpers :as cph] + [app.config :as cfg] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.gradients :as grad] [app.util.object :as obj] [cuerdas.core :as str] @@ -83,11 +85,18 @@ (let [id-prefix (dm/str "marker-" render-id) gradient (:stroke-color-gradient stroke) + image (:stroke-image stroke) cap-start (:stroke-cap-start stroke) cap-end (:stroke-cap-end stroke) - color (if (some? gradient) + color (cond + (some? gradient) (str/ffmt "url(#stroke-color-gradient-%s-%s)" render-id index) + + (some? image) + (str/ffmt "url(#stroke-fill-%-%)" render-id index) + + :else (:stroke-color stroke)) opacity (when-not (some? gradient) @@ -192,21 +201,57 @@ (mf/defc stroke-defs {::mf/wrap-props false} [{:keys [shape stroke render-id index]}] - (let [open-path? (and ^boolean (cph/path-shape? shape) - ^boolean (gsh/open-path? shape)) - gradient (:stroke-color-gradient stroke) - alignment (:stroke-alignment stroke :center) - width (:stroke-width stroke 0) + (let [open-path? (and ^boolean (cph/path-shape? shape) + ^boolean (gsh/open-path? shape)) + gradient (:stroke-color-gradient stroke) + alignment (:stroke-alignment stroke :center) + width (:stroke-width stroke 0) - props #js {:id (dm/str "stroke-color-gradient-" render-id "-" index) - :gradient gradient - :shape shape}] + props #js {:id (dm/str "stroke-color-gradient-" render-id "-" index) + :gradient gradient + :shape shape} + stroke-image (:stroke-image stroke) + uri (when stroke-image (cfg/resolve-file-media stroke-image)) + embed (embed/use-data-uris [uri]) + + stroke-width (case (:stroke-alignment stroke :center) + :center (/ (:stroke-width stroke 0) 2) + :outer (:stroke-width stroke 0) + 0) + margin (gsb/shape-stroke-margin stroke stroke-width) + + selrect (mf/with-memo [shape] + (if (cph/text-shape? shape) + (gst/shape->rect shape) + (grc/points->rect (:points shape)))) + + stroke-margin (+ stroke-width margin) + + w (+ (dm/get-prop selrect :width) (* 2 stroke-margin)) + h (+ (dm/get-prop selrect :height) (* 2 stroke-margin)) + image-props #js {:href (get embed uri uri) + :preserveAspectRatio "xMidYMid slice" + :width 1 + :height 1 + :id (dm/str "stroke-image-" render-id "-" index)}] [:* (when (some? gradient) (case (:type gradient) :linear [:> grad/linear-gradient props] :radial [:> grad/radial-gradient props])) + (when (:stroke-image stroke) + ;; We need to make the pattern size and the image fit so it's not repeated + [:pattern {:id (dm/str "stroke-fill-" render-id "-" index) + :patternContentUnits "objectBoundingBox" + :x (- (/ stroke-margin (dm/get-prop selrect :width))) + :y (- (/ stroke-margin (dm/get-prop selrect :height))) + :width (/ w (dm/get-prop selrect :width)) + :height (/ h (dm/get-prop selrect :height)) + :viewBox "0 0 1 1" + :preserveAspectRatio "xMidYMid slice"} + [:> :image image-props]]) + (cond (and (not open-path?) (= :inner alignment) @@ -345,6 +390,7 @@ index (unchecked-get props "index") render-id (mf/use-ctx muc/render-id) + render-id (d/nilv (unchecked-get props "render-id") render-id) stroke-width (:stroke-width stroke 0) stroke-style (:stroke-style stroke :none) @@ -385,7 +431,8 @@ url-fill? (or ^boolean (some? (:fill-image shape)) ^boolean (cph/image-shape? shape) ^boolean (> (count shape-fills) 1) - ^boolean (some? (some :fill-color-gradient shape-fills))) + ^boolean (some? (some :fill-color-gradient shape-fills)) + ^boolean (some? (some :fill-image shape-fills))) props (if (cph/frame-shape? shape) props @@ -447,6 +494,10 @@ (obj/set! "fillOpacity" "none") (obj/merge! (attrs/get-stroke-style value position render-id))) + style (if (:stroke-image value) + (obj/set! style "stroke" (dm/fmt "url(#stroke-fill-%-%)" render-id position)) + style) + props (-> (obj/clone props) (obj/unset! "fill") (obj/unset! "fillOpacity") diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 9aa49313a..9ae5a0490 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -10,6 +10,7 @@ importation." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.svg :as csvg] [app.main.ui.context :as muc] @@ -282,36 +283,47 @@ (defn- export-fills-data [{:keys [fills]}] - (when-let [fills (seq fills)] - (mf/html - [:> "penpot:fills" #js {} - (for [[index fill] (d/enumerate fills)] - [:> "penpot:fill" - #js {:penpot:fill-color (if (some? (:fill-color-gradient fill)) - (str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-id) "_" index)) - (d/name (:fill-color fill))) - :penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill)) - :penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill)) - :penpot:fill-opacity (d/name (:fill-opacity fill))}])]))) + (when-let [fills (seq fills)] + (let [render-id (mf/use-ctx muc/render-id)] + (mf/html + [:> "penpot:fills" #js {} + (for [[index fill] (d/enumerate fills)] + (let [fill-image-id (dm/str "fill-image-" render-id "-" index)] + [:> "penpot:fill" + #js {:penpot:fill-color (cond + (some? (:fill-color-gradient fill)) + (str/format "url(#%s)" (str "fill-color-gradient-" render-id "-" index)) + + :else + (d/name (:fill-color fill))) + :penpot:fill-image-id (when (:fill-image fill) fill-image-id) + :penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill)) + :penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill)) + :penpot:fill-opacity (d/name (:fill-opacity fill))}]))])))) (defn- export-strokes-data [{:keys [strokes]}] (when-let [strokes (seq strokes)] - (mf/html - [:> "penpot:strokes" #js {} - (for [[index stroke] (d/enumerate strokes)] - [:> "penpot:stroke" - #js {:penpot:stroke-color (if (some? (:stroke-color-gradient stroke)) - (str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-id) "_" index)) - (d/name (:stroke-color stroke))) - :penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke)) - :penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke)) - :penpot:stroke-opacity (d/name (:stroke-opacity stroke)) - :penpot:stroke-style (d/name (:stroke-style stroke)) - :penpot:stroke-width (d/name (:stroke-width stroke)) - :penpot:stroke-alignment (d/name (:stroke-alignment stroke)) - :penpot:stroke-cap-start (d/name (:stroke-cap-start stroke)) - :penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}])]))) + (let [render-id (mf/use-ctx muc/render-id)] + (mf/html + [:> "penpot:strokes" #js {} + (for [[index stroke] (d/enumerate strokes)] + (let [stroke-image-id (dm/str "stroke-image-" render-id "-" index)] + [:> "penpot:stroke" + #js {:penpot:stroke-color (cond + (some? (:stroke-color-gradient stroke)) + (str/format "url(#%s)" (str "stroke-color-gradient-" render-id "-" index)) + :else + (d/name (:stroke-color stroke))) + :penpot:stroke-image-id (when (:stroke-image stroke) stroke-image-id) + :penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke)) + :penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke)) + :penpot:stroke-opacity (d/name (:stroke-opacity stroke)) + :penpot:stroke-style (d/name (:stroke-style stroke)) + :penpot:stroke-width (d/name (:stroke-width stroke)) + :penpot:stroke-alignment (d/name (:stroke-alignment stroke)) + :penpot:stroke-cap-start (d/name (:stroke-cap-start stroke)) + :penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}]))])))) (defn- export-interactions-data [{:keys [interactions]}] (when-let [interactions (seq interactions)] @@ -461,5 +473,5 @@ (export-strokes-data shape) (export-grid-data shape) (export-layout-container-data shape) - (export-layout-item-data shape)])) + (export-layout-item-data shape)])) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index 30a6556ac..9be7b7187 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -36,21 +36,26 @@ height (dm/get-prop selrect :height) has-image? (or (some? metadata) - (some? image)) + (some? image)) - uri (cond - (some? metadata) - (cfg/resolve-file-media metadata) + uri (cond + (some? metadata) + (cfg/resolve-file-media metadata) - (some? image) - (cfg/resolve-file-media image)) + (some? image) + (cfg/resolve-file-media image)) - embed (embed/use-data-uris [uri]) + uris (into [uri] + (comp + (keep :fill-image) + (map cfg/resolve-file-media)) + fills) + + embed (embed/use-data-uris uris) transform (gsh/transform-str shape) - ;; When tru e the image has not loaded yet - loading? (and (some? uri) - (not (contains? embed uri))) + ;; When true the image has not loaded yet + loading? (not-any? (partial contains? embed) uris) pat-props #js {:patternUnits "userSpaceOnUse" :x x @@ -65,10 +70,10 @@ (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] [:* {:key (dm/str shape-index)} - (for [[fill-index value] (reverse (d/enumerate fills))] + (for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))] (when (some? (:fill-color-gradient value)) (let [gradient (:fill-color-gradient value) - props #js {:id (dm/str "fill-color-gradient_" render-id "_" fill-index) + props #js {:id (dm/str "fill-color-gradient-" render-id "-" fill-index) :key (dm/str fill-index) :gradient gradient :shape shape}] @@ -84,13 +89,23 @@ (-> (obj/set! "width" (* width no-repeat-padding)) (obj/set! "height" (* height no-repeat-padding))))) [:g - (for [[fill-index value] (reverse (d/enumerate fills))] + (for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))] (let [style (attrs/get-fill-style value fill-index render-id type) props #js {:key (dm/str fill-index) :width width :height height :style style}] - [:> :rect props])) + (if (:fill-image value) + (let [uri (cfg/resolve-file-media (:fill-image value)) + image-props #js {:id (dm/str "fill-image-" render-id "-" fill-index) + :href (get embed uri uri) + :preserveAspectRatio "xMidYMid slice" + :width width + :height height + :key (dm/str fill-index) + :opacity (:fill-opacity value)}] + [:> :image image-props]) + [:> :rect props]))) (when ^boolean has-image? [:g @@ -121,5 +136,6 @@ (or (= type :image) (= type :text)) (> (count fills) 1) - (some :fill-color-gradient fills)) + (some :fill-color-gradient fills) + (some :fill-image fills)) [:> fills* props]))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 2eb3b16cc..0cbea5950 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -125,7 +125,7 @@ id (if (some? id) id - (dm/str (name attr) "_" rid)) + (dm/str (name attr) "-" rid)) gradient (get shape attr) props #js {:id id diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index dfaaa560f..b608263cf 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -54,7 +54,7 @@ (attrs/add-border-props! shape)) get-gradient-id (fn [index] - (str render-id "_" (:id shape) "_" index))] + (str render-id "-" (:id shape) "-" index))] [:* ;; Definition of gradients for partial elements @@ -62,7 +62,7 @@ [:defs (for [[index data] (d/enumerate position-data)] (when (some? (:fill-color-gradient data)) - (let [id (dm/str "fill-color-gradient_" (get-gradient-id index))] + (let [id (dm/str "fill-color-gradient-" (get-gradient-id index))] [:& grad/gradient {:id id :key id :attr :fill-color-gradient @@ -99,6 +99,6 @@ (obj/merge! browser-props))) shape (assoc shape :fills (:fills data))] - [:& (mf/provider muc/render-id) {:key index :value (str render-id "_" (:id shape) "_" index)} + [:& (mf/provider muc/render-id) {:key index :value render-id} [:& shape-custom-strokes {:shape shape :position index :render-id render-id} [:> :text props (:text data)]]]))]])) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs index 7b2cc13b8..9fc7e6957 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.cljs @@ -8,6 +8,8 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as cc] + [app.common.media :as cm] + [app.config :as cf] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :refer [color-bullet color-name]] @@ -49,36 +51,64 @@ colors-library (get-colors-library color) file-colors (get-file-colors) color-library-name (get-in (or colors-library file-colors) [(:id color) :name]) - color (assoc color :color-library-name color-library-name)] + color (assoc color :color-library-name color-library-name) + image (:image color)] (if new-css-system - [:div {:class (stl/css :attributes-color-row)} - [:div {:class (stl/css :bullet-wrapper) - :style #js {"--bullet-size" "16px"}} - [:& cbn/color-bullet {:color color - :mini? true}]] + [:* + [:div {:class (stl/css :attributes-color-row)} + [:div {:class (stl/css :bullet-wrapper) + :style #js {"--bullet-size" "16px"}} + [:& cbn/color-bullet {:color color + :mini? true}]] - [:div {:class (stl/css :format-wrapper)} - (when-not (and on-change-format (:gradient color)) + (when-not image + [:div {:class (stl/css :format-wrapper)} + (when-not (and on-change-format (or (:gradient color) image)) + [:div {:class (stl/css :select-format-wrapper)} + [:& select + {:default-value format + :options [{:value :hex :label (tr "inspect.attributes.color.hex")} + {:value :rgba :label (tr "inspect.attributes.color.rgba")} + {:value :hsla :label (tr "inspect.attributes.color.hsla")}] + :on-change on-change-format}]]) + (when (:gradient color) + [:div {:class (stl/css :format-info)} "rgba"])]) - [:div {:class (stl/css :select-format-wrapper)} - [:& select - {:default-value format - :options [{:value :hex :label (tr "inspect.attributes.color.hex")} - {:value :rgba :label (tr "inspect.attributes.color.rgba")} - {:value :hsla :label (tr "inspect.attributes.color.hsla")}] - :on-change on-change-format}]]) - (when (:gradient color) - [:div {:class (stl/css :format-info)} "rgba"])] + (if (and copy-data (not image)) + [:& copy-button {:data copy-data + :class (stl/css :color-row-copy-btn)} + [:* + [:div {:class (stl/css :first-row)} + [:div {:class (stl/css :name-opacity)} + [:span {:class (stl/css-case :color-value-wrapper true + :gradient-name (:gradient color))} + (if (:gradient color) + [:& cbn/color-name {:color color + :size 80}] + (case format + :hex [:& cbn/color-name {:color color + :size 80}] + :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] + [:* (str/fmt "%s, %s, %s, %s" r g b a)]) + :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) + result (cc/format-hsla [h s l a])] + [:* result])))] - (if copy-data - [:& copy-button {:data copy-data - :class (stl/css :color-row-copy-btn)} - [:* + (when-not (:gradient color) + [:span {:class (stl/css :opacity-info)} + (str (* 100 (:opacity color)) "%")])]] + + (when color-library-name + [:div {:class (stl/css :second-row)} + [:div {:class (stl/css :color-name-library)} + color-library-name]])]] + + [:div {:class (stl/css :color-info)} [:div {:class (stl/css :first-row)} [:div {:class (stl/css :name-opacity)} [:span {:class (stl/css-case :color-value-wrapper true - :gradient-name (:gradient color))} + :gradient-name (:gradient color))} (if (:gradient color) [:& cbn/color-name {:color color :size 80}] @@ -98,90 +128,66 @@ (when color-library-name [:div {:class (stl/css :second-row)} [:div {:class (stl/css :color-name-library)} - color-library-name]])]] + color-library-name]])])] - [:div {:class (stl/css :color-info)} - [:div {:class (stl/css :first-row)} - [:div {:class (stl/css :name-opacity)} - [:span {:class (stl/css-case :color-value-wrapper true - :gradient-name (:gradient color))} - (if (:gradient color) - [:& cbn/color-name {:color color - :size 80}] - (case format - :hex [:& cbn/color-name {:color color - :size 80}] - :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] - [:* (str/fmt "%s, %s, %s, %s" r g b a)]) - :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) - result (cc/format-hsla [h s l a])] - [:* result])))] + (when image + (let [mtype (-> image :mtype) + name (or (:name image) (tr "media.image")) + extension (cm/mtype->extension mtype)] + [:a {:class (stl/css :download-button) + :target "_blank" + :download (cond-> name extension (str/concat extension)) + :href (cf/resolve-file-media image)} + (tr "inspect.attributes.image.download")]))] - (when-not (:gradient color) - [:span {:class (stl/css :opacity-info)} - (str (* 100 (:opacity color)) "%")])]] + [:* + [:div.attributes-color-row + (when color-library-name + [:div.attributes-color-id + [:& color-bullet {:color color}] + [:div color-library-name]]) - (when color-library-name - [:div {:class (stl/css :second-row)} - [:div {:class (stl/css :color-name-library)} - color-library-name]]) - ;; [:span {:class (stl/css-case :color-name-wrapper true - ;; :gradient-color (:gradient color))} + [:div.attributes-color-value {:class (when color-library-name "hide-color")} + [:& color-bullet {:color color}] - ;; [:div {:class (stl/css :color-value-wrapper)} - ;; (if (:gradient color) - ;; [:& cbn/color-name {:color color - ;; :size 80}] - ;; (case format - ;; :hex [:& cbn/color-name {:color color - ;; :size 80}] - ;; :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] - ;; [:* (str/fmt "%s, %s, %s, %s" r g b a)]) - ;; :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) - ;; result (cc/format-hsla [h s l a])] - ;; [:* result])))] + (cond + (:gradient color) + [:& color-name {:color color}] - ;; (when color-library-name - ;; [:div {:class (stl/css :color-name-library)} - ;; color-library-name])] + (= format :rgba) + (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] + [:div (str/fmt "%s, %s, %s, %s" r g b a)]) - ;; (when-not (:gradient color) - ;; [:div {:class (stl/css :opacity-info)} - ;; (str (* 100 (:opacity color)) "%")]) - ])] + (= format :hsla) + (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) + result (cc/format-hsla [h s l a])] + [:div result]) + :else + [:* + [:& color-name {:color color}] + (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])]) - [:div.attributes-color-row - (when color-library-name - [:div.attributes-color-id - [:& color-bullet {:color color}] - [:div color-library-name]]) + (when-not (and on-change-format (or (:gradient color) image)) + [:select.color-format-select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} + [:option {:value "hex"} + (tr "inspect.attributes.color.hex")] - [:div.attributes-color-value {:class (when color-library-name "hide-color")} - [:& color-bullet {:color color}] + [:option {:value "rgba"} + (tr "inspect.attributes.color.rgba")] - (if (:gradient color) - [:& color-name {:color color}] - (case format - :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))] - [:div (str/fmt "%s, %s, %s, %s" r g b a)]) - :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color)) - result (cc/format-hsla [h s l a])] - [:div result]) - [:* - [:& color-name {:color color}] - (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])) + [:option {:value "hsla"} + (tr "inspect.attributes.color.hsla")]])] - (when-not (and on-change-format (:gradient color)) - [:select.color-format-select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} - [:option {:value "hex"} - (tr "inspect.attributes.color.hex")] + (when (and copy-data (not image)) + [:& copy-button {:data copy-data}])] - [:option {:value "rgba"} - (tr "inspect.attributes.color.rgba")] - - [:option {:value "hsla"} - (tr "inspect.attributes.color.hsla")]])] - (when copy-data - [:& copy-button {:data copy-data}])]))) + (when image + (let [mtype (-> image :mtype) + name (or (:name image) (tr "media.image")) + extension (cm/mtype->extension mtype)] + [:a.download-button {:target "_blank" + :download (cond-> name extension (str/concat extension)) + :href (cf/resolve-file-media image)} + (tr "inspect.attributes.image.download")]))]))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss b/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss index 94e960cff..709ca251e 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/common.scss @@ -72,7 +72,6 @@ @include titleTipography; color: var(--menu-foreground-color); padding: $s-8 0; - height: $s-32; } button { @@ -129,3 +128,10 @@ } } } + +.download-button { + @extend .button-secondary; + @include tabTitleTipography; + height: $s-32; + margin-top: $s-4; +} diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs index e0982479a..13667f38a 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/fill.cljs @@ -21,7 +21,8 @@ :opacity (:fill-opacity shape) :gradient (:fill-color-gradient shape) :id (:fill-color-ref-id shape) - :file-id (:fill-color-ref-file shape)}) + :file-id (:fill-color-ref-file shape) + :image (:fill-image shape)}) (defn has-fill? [shape] (and diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs index 07fe6bb51..707c4d054 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/stroke.cljs @@ -25,7 +25,8 @@ :opacity (:stroke-opacity shape) :gradient (:stroke-color-gradient shape) :id (:stroke-color-ref-id shape) - :file-id (:stroke-color-ref-file shape)}) + :file-id (:stroke-color-ref-file shape) + :image (:stroke-image shape)}) (defn has-stroke? [shape] (seq (:strokes shape))) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs index 0bb48d41c..2cdc2a134 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes/text.cljs @@ -35,12 +35,13 @@ (get-in state [:viewer-libraries file-id :data :typographies]))] #(l/derived get-library st/state))) -(defn fill->color [{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-id fill-color-ref-file]}] +(defn fill->color [{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-id fill-color-ref-file fill-image]}] {:color fill-color :opacity fill-opacity :gradient fill-color-gradient :id fill-color-ref-id - :file-id fill-color-ref-file}) + :file-id fill-color-ref-file + :image fill-image}) (defn copy-style-data [style & properties] diff --git a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs index 90a0c155a..bf42b3e7b 100644 --- a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs @@ -11,6 +11,7 @@ [app.common.types.component :as ctk] [app.main.refs :as refs] [app.main.ui.components.shape-icon :as si] + [app.main.ui.components.shape-icon-refactor :as sir] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.components.tabs-container :refer [tabs-container tabs-element]] [app.main.ui.context :as ctx] @@ -97,7 +98,7 @@ [:span {:class (stl/css :layer-title)} (tr "inspect.tabs.code.selected.multiple" (count shapes))]] [:* [:span {:class (stl/css :shape-icon)} - [:& si/element-icon-refactor {:shape first-shape :main-instance? main-instance?}]] + [:& sir/element-icon-refactor {:shape first-shape :main-instance? main-instance?}]] ;; Execution time translation strings: ;; inspect.tabs.code.selected.circle ;; inspect.tabs.code.selected.component diff --git a/frontend/src/app/main/ui/workspace/color_palette.cljs b/frontend/src/app/main/ui/workspace/color_palette.cljs index b4db61613..8a191c763 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.cljs +++ b/frontend/src/app/main/ui/workspace/color_palette.cljs @@ -38,7 +38,7 @@ (mf/defc palette [{:keys [current-colors size width]}] (let [;; We had to do this due to a bug that leave some bugged colors - current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) current-colors)) + current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) current-colors)) state (mf/use-state {:show-menu false}) offset-step (cond (<= size 64) 40 diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index c0f5beb6c..fc8ce9ed8 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -37,7 +37,7 @@ (mf/defc palette [{:keys [current-colors recent-colors file-colors shared-libs selected on-select]}] (let [;; We had to do this due to a bug that leave some bugged colors - current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) current-colors)) + current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) current-colors)) state (mf/use-state {:show-menu false}) width (:width @state 0) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index c9e1dccab..3dab5d7be 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -8,12 +8,17 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.colors :as cc] + [app.common.data :as d] + [app.config :as cfg] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dc] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.media :as dwm] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.file-uploader :refer [file-uploader]] + [app.main.ui.components.select :refer [select]] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] @@ -46,22 +51,50 @@ ;; --- Color Picker Modal (mf/defc colorpicker - [{:keys [data disable-gradient disable-opacity on-change on-accept]}] - (let [new-css-system (mf/use-ctx ctx/new-css-system) - state (mf/deref refs/colorpicker) - node-ref (mf/use-ref) + [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}] + (let [new-css-system (mf/use-ctx ctx/new-css-system) + state (mf/deref refs/colorpicker) + node-ref (mf/use-ref) ;; TODO: I think we need to put all this picking state under ;; the same object for avoid creating adhoc refs for each ;; value - picking-color? (mf/deref picking-color?) - picked-color (mf/deref picked-color) - picked-color-select (mf/deref picked-color-select) + picking-color? (mf/deref picking-color?) + picked-color (mf/deref picked-color) + picked-color-select (mf/deref picked-color-select) - current-color (:current-color state) + current-color (:current-color state) - active-tab (mf/use-state (dc/get-active-color-tab)) - drag? (mf/use-state false) + active-fill-tab (if (:image data) + :image + (if-let [gradient (:gradient data)] + (case (:type gradient) + :linear :linear-gradient + :radial :radial-gradient) + :color)) + active-color-tab (mf/use-state (dc/get-active-color-tab)) + drag? (mf/use-state false) + + fill-image-ref (mf/use-ref nil) + + selected-mode (get state :type :color) + + disabled-color-accept? (and + (= selected-mode :image) + (not (:image current-color))) + + on-fill-image-success + (mf/use-fn + (fn [image] + (st/emit! (dc/update-colorpicker-color {:image (select-keys image [:id :width :height :mtype :name])} (not @drag?))))) + + on-fill-image-click + (mf/use-callback #(dom/click (mf/ref-val fill-image-ref))) + + on-fill-image-selected + (mf/use-fn + (fn [file] + (st/emit! (dwm/upload-fill-image file on-fill-image-success)))) set-tab! (mf/use-fn @@ -69,9 +102,18 @@ (let [tab (-> (dom/get-current-target event) (dom/get-data "tab") (keyword))] - (reset! active-tab tab) + (reset! active-color-tab tab) (dc/set-active-color-tab! tab)))) + handle-change-mode + (mf/use-fn + (fn [value] + (case value + :color (st/emit! (dc/activate-colorpicker-color)) + :linear-gradient (st/emit! (dc/activate-colorpicker-gradient :linear-gradient)) + :radial-gradient (st/emit! (dc/activate-colorpicker-gradient :radial-gradient)) + :image (st/emit! (dc/activate-colorpicker-image))))) + handle-change-color (mf/use-fn (mf/deps current-color @drag?) @@ -105,7 +147,7 @@ on-select-library-color (mf/use-fn (fn [state color] - (let [type-origin (:type state) + (let [type-origin selected-mode editig-stop-origin (:editing-stop state) is-gradient? (some? (:gradient color)) change-to (fn [new-color] @@ -149,12 +191,6 @@ (fn [_] (st/emit! (dwl/add-color (dc/get-color-from-colorpicker-state state))))) - on-activate-linear-gradient - (mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :linear-gradient))) - - on-activate-radial-gradient - (mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :radial-gradient))) - on-start-drag (mf/use-fn (mf/deps drag? node-ref) @@ -174,11 +210,21 @@ (mf/deps state) (fn [] (on-accept (dc/get-color-from-colorpicker-state state)) - (modal/hide!)))] + (modal/hide!))) + + options + (mf/with-memo [selected-mode disable-gradient disable-image] + (d/concat-vec + [{:value :color :label (tr "media.solid")}] + (when (not disable-gradient) + [{:value :linear-gradient :label (tr "media.linear")} + {:value :radial-gradient :label (tr "media.radial")}]) + (when (not disable-image) + [{:value :image :label (tr "media.image")}])))] ;; Initialize colorpicker state (mf/with-effect [] - (st/emit! (dc/initialize-colorpicker on-change)) + (st/emit! (dc/initialize-colorpicker on-change active-fill-tab)) (partial st/emit! (dc/finalize-colorpicker))) ;; Update colorpicker with external color changes @@ -220,181 +266,219 @@ :ref node-ref :style {:touch-action "none"}} [:div {:class (stl/css :top-actions)} - [:button {:class (stl/css-case :picker-btn true - :selected picking-color?) - :on-click handle-click-picker} - i/picker-refactor] - (when (not disable-gradient) - [:div {:class (stl/css :gradient-buttons)} - [:button - {:on-click on-activate-linear-gradient - :class (stl/css-case :gradient-btn true - :linear-gradient-btn true - :selected (= :linear-gradient (:type state)))}] + (when (or (not disable-gradient) (not disable-image)) + [:div {:class (stl/css :select)} + [:& select + {:default-value selected-mode + :options options + :on-change handle-change-mode}]]) + (when (not= selected-mode :image) + [:button {:class (stl/css-case :picker-btn true + :selected picking-color?) + :on-click handle-click-picker} + i/picker-refactor])] - [:button - {:on-click on-activate-radial-gradient - :class (stl/css-case :gradient-btn true - :radial-gradient-btn true - :selected (= :radial-gradient (:type state)))}]])] - - (when (or (= (:type state) :linear-gradient) - (= (:type state) :radial-gradient)) + (when (or (= selected-mode :linear-gradient) + (= selected-mode :radial-gradient)) [:& gradients {:stops (:stops state) :editing-stop (:editing-stop state) :on-select-stop handle-change-stop}]) - [:div {:class (stl/css :colorpicker-tabs)} - [:& tab-container - {:on-change-tab set-tab! - :selected @active-tab - :collapsable? false} + (if (= selected-mode :image) + (let [uri (cfg/resolve-file-media (:image current-color))] + [:div {:class (stl/css :select-image)} + [:div {:class (stl/css :content)} + (when (:image current-color) + [:img {:src uri}])] + [:button + {:class (stl/css :choose-image) + :title (tr "media.choose-image") + :aria-label (tr "media.choose-image") + :on-click on-fill-image-click} + (tr "media.choose-image") + [:& file-uploader + {:input-id "fill-image-upload" + :accept "image/jpeg,image/png" + :multi false + :ref fill-image-ref + :on-selected on-fill-image-selected}]]]) + [:* + [:div {:class (stl/css :colorpicker-tabs)} + [:& tab-container + {:on-change-tab set-tab! + :selected @active-color-tab + :collapsable? false} - [:& tab-element {:id :ramp :title i/rgba-refactor} - (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:width 256 :height 140}]] - [:& ramp-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}])] + [:& tab-element {:id :ramp :title i/rgba-refactor} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:width 256 :height 140}]] + [:& ramp-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])] - [:& tab-element {:id :harmony :title i/rgba-complementary-refactor} - (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:width 256 :height 140}]] - [:& harmony-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}])] + [:& tab-element {:id :harmony :title i/rgba-complementary-refactor} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:width 256 :height 140}]] + [:& harmony-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])] - [:& tab-element {:id :hsva :title i/hsva-refactor} - (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:width 256 :height 140}]] - [:& hsva-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}])]]] + [:& tab-element {:id :hsva :title i/hsva-refactor} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:width 256 :height 140}]] + [:& hsva-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])]]] - [:& color-inputs - {:type (if (= @active-tab :hsva) :hsv :rgb) - :disable-opacity disable-opacity - :color current-color - :on-change handle-change-color}] + [:& color-inputs + {:type (if (= @active-color-tab :hsva) :hsv :rgb) + :disable-opacity disable-opacity + :color current-color + :on-change handle-change-color}] - [:& libraries - {:state state - :current-color current-color - :on-select-color on-select-library-color - :on-add-library-color on-add-library-color}] + [:& libraries + {:state state + :current-color current-color + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :disable-image disable-image + :on-select-color on-select-library-color + :on-add-library-color on-add-library-color}]]) (when on-accept [:div {:class (stl/css :actions)} - [:button {:class (stl/css :accept-color) - :on-click on-color-accept} + [:button {:class (stl/css-case + :accept-color true + :btn-disabled disabled-color-accept?) + :on-click on-color-accept + :disabled disabled-color-accept?} (tr "workspace.libraries.colors.save-color")]])] [:div.colorpicker {:ref node-ref :style {:touch-action "none"}} [:div.colorpicker-content [:div.top-actions - [:button.picker-btn - {:class (when picking-color? "active") - :on-click handle-click-picker} - i/picker] - - (when (not disable-gradient) - [:div.gradients-buttons - [:button.gradient.linear-gradient - {:on-click on-activate-linear-gradient - :class (when (= :linear-gradient (:type state)) "active")}] - - [:button.gradient.radial-gradient - {:on-click on-activate-radial-gradient - :class (when (= :radial-gradient (:type state)) "active")}]])] + (when (or (not disable-gradient) (not disable-image)) + [:div.element-set-content + [:& select + {:default-value selected-mode + :options options + :on-change handle-change-mode}]]) + (when (not= selected-mode :image) + [:button.picker-btn + {:class (when picking-color? "active") + :on-click handle-click-picker} + i/picker])] (when (or (= (:type state) :linear-gradient) - (= (:type state) :radial-gradient)) + (= (:type state) :radial-gradient)) [:& gradients {:stops (:stops state) :editing-stop (:editing-stop state) :on-select-stop handle-change-stop}]) - [:div.colorpicker-tabs - [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand - {:class (when (= @active-tab :ramp) "active") - :alt (tr "workspace.libraries.colors.rgba") - :on-click set-tab! - :data-tab "ramp"} i/picker-ramp] - [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand - {:class (when (= @active-tab :harmony) "active") - :alt (tr "workspace.libraries.colors.rgb-complementary") - :on-click set-tab! - :data-tab "harmony"} i/picker-harmony] - [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand - {:class (when (= @active-tab :hsva) "active") - :alt (tr "workspace.libraries.colors.hsv") - :on-click set-tab! - :data-tab "hsva"} i/picker-hsv]] + (if (= selected-mode :image) + (let [uri (cfg/resolve-file-media (:image current-color))] + [:div.select-image + [:div.content + (when (:image current-color) + [:img {:src uri}])] + [:button.btn-secondary + {:title (tr "media.choose-image") + :aria-label (tr "media.choose-image") + :on-click on-fill-image-click} + (tr "media.choose-image") + [:& file-uploader + {:input-id "fill-image-upload" + :accept "image/jpeg,image/png" + :multi false + :ref fill-image-ref + :on-selected on-fill-image-selected}]]]) + [:* + [:div.colorpicker-tabs + [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand + {:class (when (= @active-color-tab :ramp) "active") + :alt (tr "workspace.libraries.colors.rgba") + :on-click set-tab! + :data-tab "ramp"} i/picker-ramp] + [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand + {:class (when (= @active-color-tab :harmony) "active") + :alt (tr "workspace.libraries.colors.rgb-complementary") + :on-click set-tab! + :data-tab "harmony"} i/picker-harmony] + [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand + {:class (when (= @active-color-tab :hsva) "active") + :alt (tr "workspace.libraries.colors.hsv") + :on-click set-tab! + :data-tab "hsva"} i/picker-hsv]] - (if picking-color? - [:div.picker-detail-wrapper - [:div.center-circle] - [:canvas#picker-detail {:width 200 :height 160}]] - (case @active-tab - :ramp - [:& ramp-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - :harmony - [:& harmony-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - :hsva - [:& hsva-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}] - nil)) + (if picking-color? + [:div.picker-detail-wrapper + [:div.center-circle] + [:canvas#picker-detail {:width 200 :height 160}]] + (case @active-color-tab + :ramp + [:& ramp-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] + :harmony + [:& harmony-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] + :hsva + [:& hsva-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] + nil)) - [:& color-inputs - {:type (if (= @active-tab :hsva) :hsv :rgb) - :disable-opacity disable-opacity - :color current-color - :on-change handle-change-color}] + [:& color-inputs + {:type (if (= @active-color-tab :hsva) :hsv :rgb) + :disable-opacity disable-opacity + :color current-color + :on-change handle-change-color}] - [:& libraries - {:state state - :current-color current-color - :disable-gradient disable-gradient - :disable-opacity disable-opacity - :on-select-color on-select-library-color - :on-add-library-color on-add-library-color}] + [:& libraries + {:state state + :current-color current-color + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :disable-image disable-image + :on-select-color on-select-library-color + :on-add-library-color on-add-library-color}]]) (when on-accept [:div.actions [:button.btn-primary.btn-large - {:on-click on-color-accept} + {:on-click on-color-accept + :disabled disabled-color-accept? + :class (dom/classnames + :btn-disabled disabled-color-accept?)} (tr "workspace.libraries.colors.save-color")]])]]))) (defn calculate-position @@ -420,6 +504,7 @@ [{:keys [x y data position disable-gradient disable-opacity + disable-image on-change on-close on-accept] :as props}] (let [new-css-system (mf/use-ctx ctx/new-css-system) vport (mf/deref viewport) @@ -445,6 +530,7 @@ [:& colorpicker {:data data :disable-gradient disable-gradient :disable-opacity disable-opacity + :disable-image disable-image :on-change handle-change :on-accept on-accept}]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss index f030e5a39..ae18f6abd 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -20,8 +20,9 @@ .top-actions { display: flex; align-items: flex-start; + flex-direction: row-reverse; justify-content: space-between; - height: $s-28; + height: $s-40; .picker-btn { @include buttonStyle; @include flexCenter; @@ -32,6 +33,7 @@ width: $s-20; border-radius: $br-4; padding: 0; + margin-top: $s-4; svg { @extend .button-icon; stroke: var(--button-tertiary-foreground-color-rest); @@ -96,7 +98,7 @@ display: flex; gap: $s-4; .accept-color { - @include titleTipography; + @include tabTitleTipography; @extend .button-secondary; width: 100%; height: $s-32; @@ -122,3 +124,36 @@ } } } + +.select { + width: $s-116; +} + +.select-image { + margin-top: $s-4; + .content { + border-radius: $br-8; + display: flex; + justify-content: center; + background-image: url("/images/colorpicker-no-image.png"); + background-position: center; + background-size: auto $s-140; + height: $s-140; + margin-bottom: $s-6; + margin-right: $s-1; + img { + height: fit-content; + width: fit-content; + max-height: 100%; + max-width: 100%; + margin: auto; + } + } + .choose-image { + @extend .button-secondary; + @include tabTitleTipography; + width: 100%; + margin-top: $s-12; + height: $s-32; + } +} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs index 7a6690eba..d184ba8b4 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -27,7 +27,7 @@ [rumext.v2 :as mf])) (mf/defc libraries - [{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity]}] + [{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}] (let [new-css-system (mf/use-ctx ctx/new-css-system) selected (h/use-shared-state mdc/colorpicker-selected-broadcast-key :recent) current-colors (mf/use-state []) @@ -35,7 +35,7 @@ shared-libs (mf/deref refs/workspace-libraries) file-colors (mf/deref refs/workspace-file-colors) recent-colors (mf/deref refs/workspace-recent-colors) - recent-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) recent-colors)) + recent-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) recent-colors)) on-library-change (mf/use-fn @@ -52,7 +52,8 @@ check-valid-color? (fn [color] (and (or (not disable-gradient) (not (:gradient color))) - (or (not disable-opacity) (= 1 (:opacity color))))) + (or (not disable-opacity) (= 1 (:opacity color))) + (or (not disable-image) (not (:image color))))) toggle-palette (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index 80fa49f47..4014d1dbd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -32,10 +32,12 @@ :opacity (:fill-opacity fill) :id color-id :file-id color-file-id - :gradient (:fill-color-gradient fill)}) + :gradient (:fill-color-gradient fill) + :image (:fill-image fill)}) (d/without-nils {:color (str/lower (:fill-color fill)) :opacity (:fill-opacity fill) - :gradient (:fill-color-gradient fill)}))] + :gradient (:fill-color-gradient fill) + :image (:fill-image fill)}))] {:attrs attrs :prop :fill :shape-id (:shape-id fill) @@ -47,16 +49,18 @@ color-id (:stroke-color-ref-id stroke) shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) is-shared? (contains? shared-libs-colors color-id) - has-color? (not (nil? (:stroke-color stroke))) + has-color? (or (not (nil? (:stroke-color stroke))) (not (nil? (:stroke-image stroke))) ) attrs (if (or is-shared? (= color-file-id file-id)) (d/without-nils {:color (str/lower (:stroke-color stroke)) :opacity (:stroke-opacity stroke) :id color-id :file-id color-file-id - :gradient (:stroke-color-gradient stroke)}) + :gradient (:stroke-color-gradient stroke) + :image (:stroke-image stroke)}) (d/without-nils {:color (str/lower (:stroke-color stroke)) :opacity (:stroke-opacity stroke) - :gradient (:stroke-color-gradient stroke)}))] + :gradient (:stroke-color-gradient stroke) + :image (:stroke-image stroke)}))] (when has-color? {:attrs attrs :prop :stroke diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index e4111beb8..2979eed23 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -163,7 +163,8 @@ :opacity (:fill-opacity value) :id (:fill-color-ref-id value) :file-id (:fill-color-ref-file value) - :gradient (:fill-color-gradient value)} + :gradient (:fill-color-gradient value) + :image (:fill-image value)} :key index :index index :title (tr "workspace.options.fill") @@ -215,7 +216,8 @@ :opacity (:fill-opacity value) :id (:fill-color-ref-id value) :file-id (:fill-color-ref-file value) - :gradient (:fill-color-gradient value)} + :gradient (:fill-color-gradient value) + :image (:fill-image value)} :key index :index index :title (tr "workspace.options.fill") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs index e108fd738..dcf8d5753 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs @@ -105,9 +105,10 @@ (mf/use-fn (mf/deps grid) (fn [color] - (-> grid - (update :params assoc :color color) - (on-change)))) + (let [color (dissoc color :id :file-id)] + (-> grid + (update :params assoc :color color) + (on-change))))) handle-detach-color (mf/use-fn @@ -189,6 +190,7 @@ [:& color-row {:color (:color params) :title (tr "workspace.options.grid.params.color") :disable-gradient true + :disable-image true :on-change handle-change-color :on-detach handle-detach-color}] [:button {:class (stl/css :show-more-options) @@ -228,6 +230,7 @@ [:& color-row {:color (:color params) :title (tr "workspace.options.grid.params.color") :disable-gradient true + :disable-image true :on-change handle-change-color :on-detach handle-detach-color}]]] @@ -384,6 +387,7 @@ [:& color-row {:color (:color params) :title (tr "workspace.options.grid.params.color") :disable-gradient true + :disable-image true :on-change handle-change-color :on-detach handle-detach-color}] [:div.row-flex diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index 9d101d6ee..97f329c5c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -115,7 +115,8 @@ (fn [color] (st/emit! (dch/update-shapes ids - #(assoc-in % [:shadow index :color] color))))) + #(assoc-in % [:shadow index :color] + (dissoc color :id :file-id)))))) detach-color (mf/use-fn @@ -242,6 +243,7 @@ (:color value)) :title (tr "workspace.options.shadow-options.color") :disable-gradient true + :disable-image true :on-change update-color :on-detach detach-color :on-open manage-on-open @@ -335,6 +337,7 @@ (:color value)) :title (tr "workspace.options.shadow-options.color") :disable-gradient true + :disable-image true :on-change update-color :on-detach detach-color :on-open manage-on-open diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index fa0f3d23d..dbdd28507 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -38,6 +38,7 @@ [:& color-row {:disable-gradient true :disable-opacity true + :disable-image true :title (tr "workspace.options.canvas-background") :color {:color (get options :background clr/canvas) :opacity 1} @@ -51,6 +52,7 @@ [:& color-row {:disable-gradient true :disable-opacity true + :disable-image true :title (tr "workspace.options.canvas-background") :color {:color (get options :background clr/canvas) :opacity 1} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index adce16118..09d22f7ed 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -62,6 +62,8 @@ gradient-color? (and (not multiple-colors?) (:gradient color) (get-in color [:gradient :type])) + image-color? (and (not multiple-colors?) + (:image color)) editing-text* (mf/use-state false) editing-text? (deref editing-text*) @@ -218,6 +220,12 @@ [:div {:class (stl/css :color-name)} (uc/gradient-type->string (get-in color [:gradient :type]))]] + ;; Rendering an image + image-color? + [:* + [:div {:class (stl/css :color-name)} + (tr "media.image")]] + ;; Rendering a plain color :else [:span {:class (stl/css :color-input-wrapper)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index d938043c3..08a30d709 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -188,12 +188,13 @@ :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot)) :ref dref} - ;; Stroke Color + ;; Stroke Color [:& color-row {:color {:color (:stroke-color stroke) :opacity (:stroke-opacity stroke) :id (:stroke-color-ref-id stroke) :file-id (:stroke-color-ref-file stroke) - :gradient (:stroke-color-gradient stroke)} + :gradient (:stroke-color-gradient stroke) + :image (:stroke-image stroke)} :index index :title title :on-change on-color-change-refactor @@ -263,7 +264,8 @@ :opacity (:stroke-opacity stroke) :id (:stroke-color-ref-id stroke) :file-id (:stroke-color-ref-file stroke) - :gradient (:stroke-color-gradient stroke)} + :gradient (:stroke-color-gradient stroke) + :image (:stroke-image stroke)} :index index :title title :on-change (on-color-change index) diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index f581a7742..5bc165580 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -74,7 +74,7 @@ (= file-id :multiple))) (def empty-color - (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity])) + (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity :image])) (defn get-color-name [color] diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 0300c1763..42063204f 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -101,6 +101,10 @@ (get-in (get-data m) [:attrs ns-att]))] (when val (val-fn val))))) +(defn find-node-by-metadata-value + [meta value coll] + (->> coll (d/seek #(= value (get-meta % meta))))) + (defn get-children [node] (cond-> (:content node) @@ -429,7 +433,7 @@ fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid) meta-fill-color (get-meta node :fill-color) meta-fill-opacity (get-meta node :fill-opacity) - meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url") + meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url#fill-color-gradient") (parse-gradient node meta-fill-color) (get-meta node :fill-color-gradient)) gradient (when (str/starts-with? fill "url") @@ -465,13 +469,14 @@ (defn add-stroke [props node svg-data] - (let [stroke-style (get-meta node :stroke-style keyword) + (let [stroke-style (get-meta node :stroke-style keyword) stroke-alignment (get-meta node :stroke-alignment keyword) - stroke (:stroke svg-data) - gradient (when (str/starts-with? stroke "url") - (parse-gradient node stroke)) + stroke (:stroke svg-data) + gradient (when (str/starts-with? stroke "url(#stroke-color-gradient") + (parse-gradient node stroke)) + stroke-cap-start (get-meta node :stroke-cap-start keyword) - stroke-cap-end (get-meta node :stroke-cap-end keyword)] + stroke-cap-end (get-meta node :stroke-cap-end keyword)] (cond-> props :always @@ -728,17 +733,22 @@ (defn parse-fills [node svg-data] (let [fills-node (get-data node :penpot:fills) + images (:images node) fills (->> (find-all-nodes fills-node :penpot:fill) (mapv (fn [fill-node] - {:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url")) - (get-meta fill-node :fill-color)) - :fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url") - (parse-gradient node (get-meta fill-node :fill-color))) - :fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid) - :fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid) - :fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})) + (let [fill-image-id (get-meta fill-node :fill-image-id)] + {:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url")) + (get-meta fill-node :fill-color)) + :fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url(#fill-color-gradient") + (parse-gradient node (get-meta fill-node :fill-color))) + :fill-image (when fill-image-id + (get images fill-image-id)) + :fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid) + :fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid) + :fill-opacity (get-meta fill-node :fill-opacity d/parse-double)}))) (mapv d/without-nils) (filterv #(not= (:fill-color %) "none")))] + (if (seq fills) fills (->> [(-> (add-fill {} node svg-data) @@ -748,22 +758,27 @@ (defn parse-strokes [node svg-data] (let [strokes-node (get-data node :penpot:strokes) + images (:images node) strokes (->> (find-all-nodes strokes-node :penpot:stroke) (mapv (fn [stroke-node] - {:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url")) - (get-meta stroke-node :stroke-color)) - :stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url") - (parse-gradient node (get-meta stroke-node :stroke-color))) - :stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid) - :stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid) - :stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double) - :stroke-style (get-meta stroke-node :stroke-style keyword) - :stroke-width (get-meta stroke-node :stroke-width d/parse-double) - :stroke-alignment (get-meta stroke-node :stroke-alignment keyword) - :stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword) - :stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)})) + (let [stroke-image-id (get-meta stroke-node :stroke-image-id)] + {:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url")) + (get-meta stroke-node :stroke-color)) + :stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url(#stroke-color-gradient") + (parse-gradient node (get-meta stroke-node :stroke-color))) + :stroke-image (when stroke-image-id + (get images stroke-image-id)) + :stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid) + :stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid) + :stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double) + :stroke-style (get-meta stroke-node :stroke-style keyword) + :stroke-width (get-meta stroke-node :stroke-width d/parse-double) + :stroke-alignment (get-meta stroke-node :stroke-alignment keyword) + :stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword) + :stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)}))) (mapv d/without-nils) (filterv #(not= (:stroke-color %) "none")))] + (if (seq strokes) strokes (->> [(-> (add-stroke {} node svg-data) @@ -804,6 +819,25 @@ (cond-> (d/not-empty? grids) (assoc :grids grids))))) +(defn get-stroke-images-data + [node] + (let [strokes + (-> node + (find-node :penpot:shape) + (find-node :penpot:strokes))] + (->> (find-all-nodes strokes :penpot:stroke) + (mapv (fn [stroke-node] + (let [id (get-in stroke-node [:attrs :penpot:stroke-image-id]) + image-node (->> node (node-seq) (find-node-by-id id))] + {:id id + :href (get-in image-node [:attrs :href])}))) + (filterv #(some? (:id %)))))) + +(defn has-stroke-images? + [node] + (let [stroke-images (get-stroke-images-data node)] + (> (count stroke-images) 0))) + (defn has-image? [node] (let [type (get-type node) @@ -812,7 +846,9 @@ (find-node :defs) (find-node :pattern) (find-node :g) + (find-node :g) (find-node :image))] + (or (= type :image) (some? pattern-image)))) @@ -827,12 +863,32 @@ (find-node :defs) (find-node :pattern) (find-node :g) + (find-node :g) (find-node :image) :attrs) image-data (get-svg-data :image node) - svg-data (or image-data pattern-data)] + svg-data (or pattern-data image-data)] (or (:href svg-data) (:xlink:href svg-data)))) +(defn get-fill-images-data + [node] + (let [fills + (-> node + (find-node :penpot:shape) + (find-node :penpot:fills))] + (->> (find-all-nodes fills :penpot:fill) + (mapv (fn [fill-node] + (let [id (get-in fill-node [:attrs :penpot:fill-image-id]) + image-node (->> node (node-seq) (find-node-by-id id))] + {:id id + :href (get-in image-node [:attrs :href])}))) + (filterv #(some? (:id %)))))) + +(defn has-fill-images? + [node] + (let [fill-images (get-fill-images-data node)] + (> (count fill-images) 0))) + (defn get-image-fill [node] (let [linear-gradient-node (-> node diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index b1ef6253c..9895aaf47 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -317,36 +317,59 @@ (defn resolve-media [context file-id node] - (if (and (not (cip/close? node)) - (cip/has-image? node)) - (let [name (cip/get-image-name node) - image-data (cip/get-image-data node) - image-fill (cip/get-image-fill node)] - (->> (upload-media-files context file-id name image-data) - (rx/catch #(do (.error js/console "Error uploading media: " name) - (rx/of node))) + (if (or (and (not (cip/close? node)) + (cip/has-image? node)) + (cip/has-stroke-images? node) + (cip/has-fill-images? node)) + (let [name (cip/get-image-name node) + has-image (cip/has-image? node) + image-data (cip/get-image-data node) + image-fill (cip/get-image-fill node) + fill-images-data (->> (cip/get-fill-images-data node) + (map #(assoc % :type :fill))) + stroke-images-data (->> (cip/get-stroke-images-data node) + (map #(assoc % :type :stroke))) + + images-data (concat + fill-images-data + stroke-images-data + (when has-image + [{:href image-data}]))] + (->> (rx/from images-data) + (rx/mapcat (fn [image-data] + (->> (upload-media-files context file-id name (:href image-data)) + (rx/catch #(do (.error js/console "Error uploading media: " name) + (rx/of node))) + (rx/map #(vector (:id image-data) %))))) + (rx/reduce (fn [acc [id data]] (assoc acc id data)) {}) (rx/map - (fn [media] - (-> node - (assoc-in [:attrs :penpot:media-id] (:id media)) - (assoc-in [:attrs :penpot:media-width] (:width media)) - (assoc-in [:attrs :penpot:media-height] (:height media)) - (assoc-in [:attrs :penpot:media-mtype] (:mtype media)) + (fn [images] + (let [media (get images nil)] + (-> node + (assoc :images images) + (cond-> (some? media) + (-> + (assoc-in [:attrs :penpot:media-id] (:id media)) + (assoc-in [:attrs :penpot:media-width] (:width media)) + (assoc-in [:attrs :penpot:media-height] (:height media)) + (assoc-in [:attrs :penpot:media-mtype] (:mtype media)) - (assoc-in [:attrs :penpot:fill-color] (:fill image-fill)) - (assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill)) - (assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill)) - (assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill)) - (assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill))))))) + (assoc-in [:attrs :penpot:fill-color] (:fill image-fill)) + (assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill)) + (assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill)) + (assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill)) + (assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill)))))))))) ;; If the node is not an image just return the node (->> (rx/of node) (rx/observe-on :async)))) (defn media-node? [node] - (and (cip/shape? node) - (cip/has-image? node) - (not (cip/close? node)))) + (or (and (cip/shape? node) + (cip/has-image? node) + (not (cip/close? node))) + (cip/has-stroke-images? node) + (cip/has-fill-images? node))) (defn import-page [context file [page-id page-name content]] @@ -379,7 +402,8 @@ (rx/mapcat (fn [node] (->> (resolve-media context file-id node) - (rx/map (fn [result] [node result]))))) + (rx/map (fn [result] + [node result]))))) (rx/reduce conj {}))] (->> pre-process-images diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 09683b188..7bb654829 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -4993,3 +4993,21 @@ msgstr "Click to close the path" #, markdown msgid "workspace.top-bar.read-only" msgstr "**Inspect mode** (View Only)" + +msgid "media.image" +msgstr "Image" + +msgid "media.solid" +msgstr "Solid" + +msgid "media.linear" +msgstr "Linear" + +msgid "media.radial" +msgstr "Radial" + +msgid "media.gradient" +msgstr "Gradient" + +msgid "media.choose-image" +msgstr "Choose image" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 9e145e578..68cc7036d 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5094,3 +5094,21 @@ msgstr "Pulsar para cerrar la ruta" #, markdown msgid "workspace.top-bar.read-only" msgstr "**Modo inspección** (View only)" + +msgid "media.image" +msgstr "Imagen" + +msgid "media.solid" +msgstr "Sólido" + +msgid "media.linear" +msgstr "Linear" + +msgid "media.radial" +msgstr "Radial" + +msgid "media.gradient" +msgstr "Gradiente" + +msgid "media.choose-image" +msgstr "Elegir imagen"