diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 40a2537597..9df9759375 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -190,10 +190,9 @@ [:type [:= :del-color]] [:id ::sm/uuid]]] + ;; DEPRECATED: remove before 2.3 [:add-recent-color - [:map {:title "AddRecentColorChange"} - [:type [:= :add-recent-color]] - [:color ::ctc/recent-color]]] + [:map {:title "AddRecentColorChange"}]] [:add-media [:map {:title "AddMediaChange"} @@ -656,18 +655,10 @@ [data {:keys [id]}] (ctcl/delete-color data id)) +;; DEPRECATED: remove before 2.3 (defmethod process-change :add-recent-color - [data {:keys [color]}] - ;; Moves the color to the top of the list and then truncates up to 15 - (update - data - :recent-colors - (fn [rc] - (let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color))) - rc (-> rc (conj color))] - (cond-> rc - (> (count rc) 15) - (subvec 1)))))) + [data _] + data) ;; -- Media diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index c3ecbd8a16..9c613a91d0 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -607,13 +607,6 @@ (reduce resize-parent changes all-parents))) ;; Library changes - -(defn add-recent-color - [changes color] - (-> changes - (update :redo-changes conj {:type :add-recent-color :color color}) - (apply-changes-local))) - (defn add-color [changes color] (-> changes diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index c0c400a9a7..78a7f81145 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -107,17 +107,16 @@ [::sm/contains-any {:strict true} [:color :gradient :image]]]) (sm/register! ::rgb-color type:rgb-color) - (sm/register! ::color schema:color) (sm/register! ::gradient schema:gradient) (sm/register! ::image-color schema:image-color) (sm/register! ::recent-color schema:recent-color) -(def check-color! - (sm/check-fn schema:color)) +(def valid-color? + (sm/lazy-validator schema:color)) -(def check-recent-color! - (sm/check-fn schema:recent-color)) +(def valid-recent-color? + (sm/lazy-validator schema:recent-color)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS @@ -392,13 +391,22 @@ (process-shape-colors shape sync-color))) -(defn eq-recent-color? +(defn- eq-recent-color? [c1 c2] (or (= c1 c2) (and (some? (:color c1)) (some? (:color c2)) (= (:color c1) (:color c2))))) +(defn add-recent-color + "Moves the color to the top of the list and then truncates up to 15" + [state file-id color] + (update state file-id (fn [colors] + (let [colors (d/removev (partial eq-recent-color? color) colors) + colors (conj colors color)] + (cond-> colors + (> (count colors) 15) + (subvec 1)))))) (defn stroke->color-att [stroke file-id shared-libs] diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index c59141867b..ec8d598f4a 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -21,7 +21,7 @@ [app.main.repo :as rp] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [app.util.storage :refer [storage]] + [app.util.storage :as s] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -51,14 +51,14 @@ (defn get-current-team-id [profile] - (let [team-id (::current-team-id @storage)] + (let [team-id (::current-team-id @s/storage)] (or team-id (:default-team-id profile)))) (defn set-current-team! [team-id] (if (nil? team-id) - (swap! storage dissoc ::current-team-id) - (swap! storage assoc ::current-team-id team-id))) + (swap! s/storage dissoc ::current-team-id) + (swap! s/storage assoc ::current-team-id team-id))) ;; --- EVENT: fetch-teams @@ -78,9 +78,9 @@ ;; if not, dissoc it from storage. (let [ids (into #{} (map :id) teams)] - (when-let [ctid (::current-team-id @storage)] + (when-let [ctid (::current-team-id @s/storage)] (when-not (contains? ids ctid) - (swap! storage dissoc ::current-team-id))))))) + (swap! s/storage dissoc ::current-team-id))))))) (defn fetch-teams [] @@ -131,10 +131,10 @@ (effect [_ state _] (let [profile (:profile state) email (:email profile) - previous-profile (:profile @storage) + previous-profile (:profile @s/storage) previous-email (:email previous-profile)] (when profile - (swap! storage assoc :profile profile) + (swap! s/storage assoc :profile profile) (i18n/set-locale! (:lang profile)) (when (not= previous-email email) (set-current-team! nil))))))) @@ -320,7 +320,7 @@ ptk/EffectEvent (effect [_ _ _] ;; We prefer to keek some stuff in the storage like the current-team-id and the profile - (set-current-team! nil))))) + (swap! s/storage (constantly {})))))) (defn logout ([] (logout {})) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index dfb18f0d63..dd62ff70d8 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -79,6 +79,7 @@ [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] + [app.util.storage :refer [storage]] [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] @@ -335,6 +336,7 @@ ptk/UpdateEvent (update [_ state] (assoc state + :recent-colors (:recent-colors @storage) :workspace-ready? false :current-file-id file-id :current-project-id project-id diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 8e3589b50b..a6c6cb8b33 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -48,6 +48,7 @@ [app.util.color :as uc] [app.util.i18n :refer [tr]] [app.util.router :as rt] + [app.util.storage :as s] [app.util.time :as dt] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -132,16 +133,21 @@ (defn add-recent-color [color] + (dm/assert! "expected valid recent color map" - (ctc/check-recent-color! color)) + (ctc/valid-recent-color? color)) (ptk/reify ::add-recent-color - ptk/WatchEvent - (watch [it _ _] - (let [changes (-> (pcb/empty-changes it) - (pcb/add-recent-color color))] - (rx/of (dch/commit-changes changes)))))) + ptk/UpdateEvent + (update [_ state] + (let [file-id (:current-file-id state)] + (update state :recent-colors ctc/add-recent-color file-id color))) + + ptk/EffectEvent + (effect [_ state _] + (let [recent-colors (:recent-colors state)] + (swap! s/storage assoc :recent-colors recent-colors))))) (def clear-color-for-rename (ptk/reify ::clear-color-for-rename @@ -168,8 +174,11 @@ (dm/assert! "expected valid parameters" - (and (ctc/check-color! color) - (uuid? file-id))) + (ctc/valid-color? color)) + + (dm/assert! + "expected file-id" + (uuid? file-id)) (ptk/reify ::update-color ptk/WatchEvent diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 36ec7a425d..c0f32f6436 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -236,9 +236,10 @@ =)) (def workspace-recent-colors - (l/derived (fn [data] - (get data :recent-colors [])) - workspace-data)) + (l/derived (fn [state] + (when-let [file-id (:current-file-id state)] + (dm/get-in state [:recent-colors file-id]))) + st/state)) (def workspace-recent-fonts (l/derived (fn [data] diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index bb05d2b1cc..14bff15591 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -294,19 +294,21 @@ `key` for new values." [key default] (let [id (mf/use-id) - state (mf/use-state (get @storage key default)) + state* (mf/use-state #(get @storage key default)) + state (deref state*) stream (mf/with-memo [id] (->> mbc/stream (rx/filter #(not= (:id %) id)) (rx/filter #(= (:type %) key)) (rx/map deref)))] - (mf/with-effect [@state key id] - (mbc/emit! id key @state) - (swap! storage assoc key @state)) + (mf/with-effect [state key id] + (mbc/emit! id key state) + (swap! storage assoc key state)) - (use-stream stream (partial reset! state)) - state)) + (use-stream stream (partial reset! state*)) + + state*)) (defonce ^:private intersection-subject (rx/subject)) (defonce ^:private intersection-observer diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index 148cdf773d..7b57c1345a 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.hooks.resize (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.logging :as log] @@ -20,6 +21,15 @@ (def last-resize-type nil) +(defn- get-initial-state + [initial file-id key] + (let [saved (dm/get-in @storage [::state file-id key])] + (d/nilv saved initial))) + +(defn- update-persistent-state + [data file-id key size] + (update-in data [::state file-id] assoc key size)) + (defn set-resize-type! [type] (set! last-resize-type type)) @@ -28,26 +38,28 @@ (use-resize-hook key initial min-val max-val axis negate? resize-type nil)) ([key initial min-val max-val axis negate? resize-type on-change-size] - (let [current-file-id (mf/use-ctx ctx/current-file-id) - size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial)) - parent-ref (mf/use-ref nil) + (let [file-id (mf/use-ctx ctx/current-file-id) - dragging-ref (mf/use-ref false) + current-size* (mf/use-state #(get-initial-state initial file-id key)) + current-size (deref current-size*) + + parent-ref (mf/use-ref nil) + dragging-ref (mf/use-ref false) start-size-ref (mf/use-ref nil) - start-ref (mf/use-ref nil) + start-ref (mf/use-ref nil) on-pointer-down - (mf/use-callback - (mf/deps @size-state) + (mf/use-fn + (mf/deps current-size) (fn [event] (dom/capture-pointer event) - (mf/set-ref-val! start-size-ref @size-state) + (mf/set-ref-val! start-size-ref current-size) (mf/set-ref-val! dragging-ref true) (mf/set-ref-val! start-ref (dom/get-client-position event)) (set! last-resize-type resize-type))) on-lost-pointer-capture - (mf/use-callback + (mf/use-fn (fn [event] (dom/release-pointer event) (mf/set-ref-val! start-size-ref nil) @@ -56,40 +68,39 @@ (set! last-resize-type nil))) on-pointer-move - (mf/use-callback - (mf/deps min-val max-val negate?) + (mf/use-fn + (mf/deps min-val max-val negate? file-id key) (fn [event] (when (mf/ref-val dragging-ref) (let [start (mf/ref-val start-ref) - pos (dom/get-client-position event) + pos (dom/get-client-position event) delta (-> (gpt/to-vec start pos) (cond-> negate? gpt/negate) (get axis)) + start-size (mf/ref-val start-size-ref) new-size (-> (+ start-size delta) (max min-val) (min max-val))] - (reset! size-state new-size) - (swap! storage assoc-in [::saved-resize current-file-id key] new-size) - (when on-change-size (on-change-size new-size)))))) + (reset! current-size* new-size) + (swap! storage update-persistent-state file-id key new-size))))) set-size - (mf/use-callback - (mf/deps on-change-size) + (mf/use-fn + (mf/deps on-change-size file-id key) (fn [new-size] (let [new-size (mth/clamp new-size min-val max-val)] - (reset! size-state new-size) - (swap! storage assoc-in [::saved-resize current-file-id key] new-size) - (when on-change-size (on-change-size new-size)))))] + (reset! current-size* new-size) + (swap! storage update-persistent-state file-id key new-size))))] - (mf/use-effect - (fn [] - (when on-change-size (on-change-size @size-state)))) + (mf/with-effect [on-change-size current-size] + (when on-change-size + (on-change-size current-size))) {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture :on-pointer-move on-pointer-move :parent-ref parent-ref :set-size set-size - :size @size-state}))) + :size current-size}))) (defn use-resize-observer [callback] diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs index cd9303edd2..80fd72f6e3 100644 --- a/frontend/src/app/util/storage.cljs +++ b/frontend/src/app/util/storage.cljs @@ -6,42 +6,80 @@ (ns app.util.storage (:require + ["lodash/debounce" :as ldebounce] [app.common.exceptions :as ex] [app.common.transit :as t] [app.util.globals :as g] - [app.util.timers :as tm])) + [cuerdas.core :as str])) -(defn- persist - [storage prev curr] - (run! (fn [key] - (let [prev* (get prev key) - curr* (get curr key)] - (when (not= curr* prev*) - (tm/schedule-on-idle - #(if (some? curr*) - (.setItem ^js storage (t/encode-str key) (t/encode-str curr*)) - (.removeItem ^js storage (t/encode-str key))))))) +;; Using ex/ignoring because can receive a DOMException like this when +;; importing the code as a library: Failed to read the 'localStorage' +;; property from 'Window': Storage is disabled inside 'data:' URLs. +(defonce ^:private local-storage + (ex/ignoring (unchecked-get g/global "localStorage"))) - (into #{} (concat (keys curr) - (keys prev))))) +(defn- encode-key + [k] + (assert (keyword? k) "key must be keyword") + (let [kns (namespace k) + kn (name k)] + (str "penpot:" kns "/" kn))) + +(defn- decode-key + [k] + (when (str/starts-with? k "penpot:") + (let [k (subs k 7)] + (if (str/starts-with? k "/") + (keyword (subs k 1)) + (let [[kns kn] (str/split k "/" 2)] + (keyword kns kn)))))) + +(defn- lookup-by-index + [result index] + (try + (let [key (.key ^js local-storage index) + key' (decode-key key)] + (if key' + (let [val (.getItem ^js local-storage key)] + (assoc! result key' (t/decode-str val))) + result)) + (catch :default _ + result))) (defn- load - [storage] - (when storage - (let [len (.-length ^js storage)] - (reduce (fn [res index] - (let [key (.key ^js storage index) - val (.getItem ^js storage key)] - (try - (assoc res (t/decode-str key) (t/decode-str val)) - (catch :default _e - res)))) - {} - (range len))))) + [] + (when (some? local-storage) + (let [length (.-length ^js local-storage)] + (loop [index 0 + result (transient {})] + (if (< index length) + (recur (inc index) + (lookup-by-index result index)) + (persistent! result)))))) -;; Using ex/ignoring because can receive a DOMException like this when importing the code as a library: -;; Failed to read the 'localStorage' property from 'Window': Storage is disabled inside 'data:' URLs. -(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage"))))) +(defonce ^:private latest-state (load)) -(add-watch storage :persistence #(persist js/localStorage %3 %4)) +(defn- on-change* + [curr-state] + (let [prev-state latest-state] + (try + (run! (fn [key] + (let [prev-val (get prev-state key) + curr-val (get curr-state key)] + (when-not (identical? curr-val prev-val) + (if (some? curr-val) + (.setItem ^js local-storage (encode-key key) (t/encode-str curr-val)) + (.removeItem ^js local-storage (encode-key key)))))) + (into #{} (concat (keys curr-state) + (keys prev-state)))) + (finally + (set! latest-state curr-state))))) +(defonce on-change + (ldebounce on-change* 2000 #js {:leading false :trailing true})) + + +(defonce storage (atom latest-state)) +(add-watch storage :persistence + (fn [_ _ _ curr-state] + (on-change curr-state)))