1use blockworx_paint::{FontChoice, Luminance, Mode, Palette, Scheme};
15use blockworx_store::record::Identity;
16use blockworx_store::storage::DocumentRef;
17use serde::{Deserialize, Serialize};
18use web_sys::{Storage, Window};
19
20use crate::mode::Prefers;
21
22const KEY: &str = "blockworx.preferences";
24
25const RECENT: &str = "blockworx.recent";
27
28#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
32#[serde(default)]
33pub struct Preferences {
34 pub scheme: Scheme,
37 pub mode: Mode,
38 pub font: FontChoice,
39 pub profile: Option<String>,
42}
43
44impl Preferences {
45 #[must_use]
47 pub fn luminance(&self, prefers: Prefers) -> Luminance {
48 crate::mode::luminance(self.mode, prefers)
49 }
50
51 #[must_use]
54 pub fn palette(&self, prefers: Prefers) -> Palette {
55 self.scheme.palette(self.luminance(prefers))
56 }
57
58 #[must_use]
62 pub fn identity(&self) -> Identity {
63 match self.profile.as_deref().map(str::trim) {
64 Some(name) if !name.is_empty() => Identity::new(name),
65 _ => Identity::from_environment(),
66 }
67 }
68}
69
70#[derive(Clone, Default)]
72pub struct Store(Option<Storage>);
73
74impl Store {
75 #[must_use]
78 pub fn of(window: Option<&Window>) -> Self {
79 Self(window.and_then(|window| window.local_storage().ok().flatten()))
80 }
81
82 #[must_use]
84 pub fn read(&self) -> Preferences {
85 self.0
86 .as_ref()
87 .and_then(|store| store.get_item(KEY).ok().flatten())
88 .and_then(|blob| serde_json::from_str(&blob).ok())
89 .unwrap_or_default()
90 }
91
92 pub fn write(&self, prefs: &Preferences) {
95 self.keeps(KEY, prefs, "the preferences");
96 }
97
98 #[must_use]
100 pub fn recent(&self) -> Vec<DocumentRef> {
101 self.0
102 .as_ref()
103 .and_then(|store| store.get_item(RECENT).ok().flatten())
104 .and_then(|blob| serde_json::from_str(&blob).ok())
105 .unwrap_or_default()
106 }
107
108 pub fn remembers(&self, recent: &[DocumentRef]) {
109 self.keeps(RECENT, recent, "the recent list");
110 }
111
112 fn keeps(&self, key: &str, what: &(impl Serialize + ?Sized), called: &str) {
113 let (Some(store), Ok(blob)) = (self.0.as_ref(), serde_json::to_string(what)) else {
114 return;
115 };
116 if let Err(refused) = store.set_item(key, &blob) {
117 tracing::debug!("the store would not keep {called}: {refused:?}");
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn a_blocked_store_answers_the_defaults() {
128 let nothing = Store::default();
129 assert_eq!(nothing.read(), Preferences::default());
130 nothing.write(&Preferences {
132 mode: Mode::Light,
133 ..Preferences::default()
134 });
135 assert_eq!(nothing.read(), Preferences::default());
136 nothing.remembers(&[DocumentRef::new("engine.bwx")]);
137 assert!(nothing.recent().is_empty());
138 }
139
140 #[test]
141 fn preferences_round_trip_through_json() {
142 let prefs = Preferences {
143 scheme: Scheme::RosePine,
144 mode: Mode::Light,
145 font: FontChoice::Monospace,
146 profile: Some("Ada Lovelace".to_owned()),
147 };
148 let blob = serde_json::to_string(&prefs).expect("the preferences serialize");
149 assert_eq!(
150 serde_json::from_str::<Preferences>(&blob).expect("and read back"),
151 prefs,
152 );
153 }
154
155 #[test]
157 fn a_partial_blob_fills_the_rest_in() {
158 let loaded: Preferences =
159 serde_json::from_str(r#"{"mode":"Light"}"#).expect("a partial blob loads");
160 assert_eq!(loaded.mode, Mode::Light);
161 assert_eq!(loaded.scheme, Scheme::default());
162 assert_eq!(loaded.font, FontChoice::default());
163 }
164
165 #[test]
166 fn a_blank_profile_is_not_a_name() {
167 let blank = Preferences {
168 profile: Some(" ".to_owned()),
169 ..Preferences::default()
170 };
171 assert_eq!(blank.identity(), Identity::from_environment());
172 let named = Preferences {
173 profile: Some("Ada Lovelace".to_owned()),
174 ..Preferences::default()
175 };
176 assert_eq!(named.identity().name, "Ada Lovelace");
177 }
178
179 #[test]
182 fn the_chrome_and_the_diagram_agree_on_the_axis() {
183 for mode in Mode::ALL {
184 for prefers in [Prefers::Dark, Prefers::Light, Prefers::Unstated] {
185 let prefs = Preferences {
186 mode,
187 ..Preferences::default()
188 };
189 assert_eq!(
190 prefs.palette(prefers).is_dark(),
191 prefs.luminance(prefers).is_dark(),
192 );
193 }
194 }
195 }
196}