at main 399 lines 15 kB view raw
1//! Sync notebook theme to ColourScheme and Theme records. 2 3use jacquard::client::AgentSessionExt; 4use jacquard::types::aturi::AtUri; 5use jacquard::types::string::{Cid, RecordKey}; 6use jacquard::CowStr; 7use jacquard::IntoStatic; 8use weaver_api::com_atproto::repo::strong_ref::StrongRef; 9use weaver_api::sh_weaver::notebook::colour_scheme::ColourScheme; 10use weaver_api::sh_weaver::notebook::theme::{ 11 Theme, ThemeDarkCodeTheme, ThemeFonts, ThemeLightCodeTheme, ThemeSpacing, 12}; 13use weaver_common::WeaverError; 14use weaver_renderer::colour_gen::{ 15 detect_variant, generate_counterpart_palette, generate_palette, ThemeVariant, 16}; 17 18use crate::components::inline_theme_editor::InlineThemeValues; 19use crate::fetch::Fetcher; 20 21/// Result of syncing theme records. 22pub struct ThemeSyncResult { 23 pub theme_uri: AtUri<'static>, 24 pub theme_cid: Cid<'static>, 25} 26 27/// Create or update theme records for a notebook. 28/// 29/// Returns the Theme record's URI and CID for updating Book.theme. 30pub async fn sync_theme( 31 fetcher: &Fetcher, 32 existing_theme_ref: Option<&StrongRef<'_>>, 33 values: &InlineThemeValues, 34) -> Result<ThemeSyncResult, WeaverError> { 35 // Determine primary variant. 36 let primary_variant = match values.default_mode.as_str() { 37 "light" => ThemeVariant::Light, 38 "dark" => ThemeVariant::Dark, 39 _ => detect_variant(&values.background) 40 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, 41 }; 42 43 // Generate palettes. 44 let light_inputs = values.light_inputs(); 45 let dark_inputs = values.dark_inputs(); 46 47 let (light_palette, dark_palette) = match primary_variant { 48 ThemeVariant::Light => { 49 let light = generate_palette(&light_inputs, ThemeVariant::Light) 50 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 51 let dark = if values.dark_background.is_some() { 52 generate_palette(&dark_inputs, ThemeVariant::Dark) 53 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 54 } else { 55 generate_counterpart_palette(&light_inputs, ThemeVariant::Light) 56 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 57 }; 58 (light, dark) 59 } 60 ThemeVariant::Dark => { 61 let dark = generate_palette(&dark_inputs, ThemeVariant::Dark) 62 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 63 let light = if values.light_background.is_some() { 64 generate_palette(&light_inputs, ThemeVariant::Light) 65 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 66 } else { 67 generate_counterpart_palette(&dark_inputs, ThemeVariant::Dark) 68 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 69 }; 70 (light, dark) 71 } 72 }; 73 74 // Code theme values. 75 let light_code = ThemeLightCodeTheme::CodeThemeName(Box::new(CowStr::from( 76 values.light_code_theme.clone(), 77 ))); 78 let dark_code = 79 ThemeDarkCodeTheme::CodeThemeName(Box::new(CowStr::from(values.dark_code_theme.clone()))); 80 let default_theme: Option<CowStr<'static>> = match values.default_mode.as_str() { 81 "light" => Some(CowStr::from("light")), 82 "dark" => Some(CowStr::from("dark")), 83 _ => None, 84 }; 85 86 if let Some(theme_ref) = existing_theme_ref { 87 // UPDATE existing records. 88 let theme_uri = &theme_ref.uri; 89 let existing_theme: Theme<'static> = fetcher 90 .fetch_record(&Theme::uri(theme_uri.as_ref()) 91 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?) 92 .await 93 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 94 .value 95 .into_static(); 96 97 // Update light ColourScheme. 98 let light_scheme_rkey = existing_theme 99 .light_scheme 100 .uri 101 .rkey() 102 .ok_or_else(|| WeaverError::InvalidNotebook("Light scheme URI missing rkey".into()))?; 103 let light_result = fetcher 104 .put_record( 105 RecordKey::any(light_scheme_rkey.as_ref()) 106 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 107 .into_static(), 108 ColourScheme::new() 109 .name(CowStr::from("Custom Light")) 110 .variant(CowStr::from("light")) 111 .colours(light_palette) 112 .build(), 113 ) 114 .await 115 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 116 117 // Update dark ColourScheme. 118 let dark_scheme_rkey = existing_theme 119 .dark_scheme 120 .uri 121 .rkey() 122 .ok_or_else(|| WeaverError::InvalidNotebook("Dark scheme URI missing rkey".into()))?; 123 let dark_result = fetcher 124 .put_record( 125 RecordKey::any(dark_scheme_rkey.as_ref()) 126 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 127 .into_static(), 128 ColourScheme::new() 129 .name(CowStr::from("Custom Dark")) 130 .variant(CowStr::from("dark")) 131 .colours(dark_palette) 132 .build(), 133 ) 134 .await 135 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 136 137 // Update Theme with new CIDs. 138 let theme_rkey = theme_uri 139 .rkey() 140 .ok_or_else(|| WeaverError::InvalidNotebook("Theme URI missing rkey".into()))?; 141 let theme_result = fetcher 142 .put_record( 143 RecordKey::any(theme_rkey.as_ref()) 144 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 145 .into_static(), 146 Theme::new() 147 .light_scheme( 148 StrongRef::new() 149 .uri(light_result.uri.clone().into_static()) 150 .cid(light_result.cid.clone().into_static()) 151 .build(), 152 ) 153 .dark_scheme( 154 StrongRef::new() 155 .uri(dark_result.uri.clone().into_static()) 156 .cid(dark_result.cid.clone().into_static()) 157 .build(), 158 ) 159 .light_code_theme(light_code) 160 .dark_code_theme(dark_code) 161 .fonts( 162 ThemeFonts::new() 163 .body(vec![]) 164 .heading(vec![]) 165 .monospace(vec![]) 166 .build(), 167 ) 168 .spacing(ThemeSpacing { 169 base_size: CowStr::from("1rem"), 170 line_height: CowStr::from("1.6"), 171 scale: CowStr::from("1.25"), 172 extra_data: None, 173 }) 174 .maybe_default_theme(default_theme) 175 .build(), 176 ) 177 .await 178 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 179 180 Ok(ThemeSyncResult { 181 theme_uri: theme_result.uri.into_static(), 182 theme_cid: theme_result.cid.into_static(), 183 }) 184 } else { 185 // CREATE new records with fresh TIDs. 186 let light_scheme = ColourScheme::new() 187 .name(CowStr::from("Custom Light")) 188 .variant(CowStr::from("light")) 189 .colours(light_palette) 190 .build(); 191 192 let light_result = fetcher 193 .create_record(light_scheme, None) 194 .await 195 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 196 197 let dark_scheme = ColourScheme::new() 198 .name(CowStr::from("Custom Dark")) 199 .variant(CowStr::from("dark")) 200 .colours(dark_palette) 201 .build(); 202 203 let dark_result = fetcher 204 .create_record(dark_scheme, None) 205 .await 206 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 207 208 let theme = Theme::new() 209 .light_scheme( 210 StrongRef::new() 211 .uri(light_result.uri.clone().into_static()) 212 .cid(light_result.cid.clone().into_static()) 213 .build(), 214 ) 215 .dark_scheme( 216 StrongRef::new() 217 .uri(dark_result.uri.clone().into_static()) 218 .cid(dark_result.cid.clone().into_static()) 219 .build(), 220 ) 221 .light_code_theme(light_code) 222 .dark_code_theme(dark_code) 223 .fonts( 224 ThemeFonts::new() 225 .body(vec![]) 226 .heading(vec![]) 227 .monospace(vec![]) 228 .build(), 229 ) 230 .spacing(ThemeSpacing { 231 base_size: CowStr::from("1rem"), 232 line_height: CowStr::from("1.6"), 233 scale: CowStr::from("1.25"), 234 extra_data: None, 235 }) 236 .maybe_default_theme(default_theme) 237 .build(); 238 239 let theme_result = fetcher 240 .create_record(theme, None) 241 .await 242 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; 243 244 Ok(ThemeSyncResult { 245 theme_uri: theme_result.uri.into_static(), 246 theme_cid: theme_result.cid.into_static(), 247 }) 248 } 249} 250 251/// Check if theme values have been customized from defaults. 252pub fn has_theme_customizations(values: &InlineThemeValues) -> bool { 253 !values.background.is_empty() 254 || !values.text.is_empty() 255 || !values.primary.is_empty() 256 || !values.link.is_empty() 257 || values.light_background.is_some() 258 || values.dark_background.is_some() 259} 260 261/// Load theme values from an existing theme reference. 262/// 263/// Fetches Theme and ColourScheme records, extracts the 4 base colours 264/// (base→background, text→text, primary→primary, link→link). 265pub async fn load_theme_values( 266 fetcher: &Fetcher, 267 theme_ref: &StrongRef<'_>, 268) -> Result<InlineThemeValues, WeaverError> { 269 // Fetch Theme record. 270 let theme: Theme<'static> = fetcher 271 .fetch_record( 272 &Theme::uri(theme_ref.uri.as_ref()) 273 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, 274 ) 275 .await 276 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 277 .value 278 .into_static(); 279 280 // Fetch light ColourScheme. 281 let light_scheme: ColourScheme<'static> = fetcher 282 .fetch_record( 283 &ColourScheme::uri(theme.light_scheme.uri.as_ref()) 284 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, 285 ) 286 .await 287 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 288 .value 289 .into_static(); 290 291 // Fetch dark ColourScheme. 292 let dark_scheme: ColourScheme<'static> = fetcher 293 .fetch_record( 294 &ColourScheme::uri(theme.dark_scheme.uri.as_ref()) 295 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, 296 ) 297 .await 298 .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? 299 .value 300 .into_static(); 301 302 // Determine default mode. 303 let default_mode = theme 304 .default_theme 305 .as_ref() 306 .map(|s| s.to_string()) 307 .unwrap_or_else(|| "auto".to_string()); 308 309 // Extract code themes. 310 let light_code_theme = match &theme.light_code_theme { 311 ThemeLightCodeTheme::CodeThemeName(name) => name.as_ref().to_string(), 312 ThemeLightCodeTheme::CodeThemeFile(file) => file.name.as_ref().to_string(), 313 ThemeLightCodeTheme::Unknown(_) => "base16-ocean.light".to_string(), 314 }; 315 let dark_code_theme = match &theme.dark_code_theme { 316 ThemeDarkCodeTheme::CodeThemeName(name) => name.as_ref().to_string(), 317 ThemeDarkCodeTheme::CodeThemeFile(file) => file.name.as_ref().to_string(), 318 ThemeDarkCodeTheme::Unknown(_) => "base16-ocean.dark".to_string(), 319 }; 320 321 // Determine primary variant and extract base colours. 322 let (background, text, primary, link) = match default_mode.as_str() { 323 "dark" => ( 324 dark_scheme.colours.base.to_string(), 325 dark_scheme.colours.text.to_string(), 326 dark_scheme.colours.primary.to_string(), 327 dark_scheme.colours.link.to_string(), 328 ), 329 _ => ( 330 light_scheme.colours.base.to_string(), 331 light_scheme.colours.text.to_string(), 332 light_scheme.colours.primary.to_string(), 333 light_scheme.colours.link.to_string(), 334 ), 335 }; 336 337 // Check if variants differ (user customized per-variant colours). 338 let light_differs = default_mode == "dark" 339 && (light_scheme.colours.base.as_ref() != dark_scheme.colours.base.as_ref() 340 || light_scheme.colours.text.as_ref() != dark_scheme.colours.text.as_ref() 341 || light_scheme.colours.primary.as_ref() != dark_scheme.colours.primary.as_ref() 342 || light_scheme.colours.link.as_ref() != dark_scheme.colours.link.as_ref()); 343 344 let dark_differs = default_mode != "dark" 345 && (dark_scheme.colours.base.as_ref() != light_scheme.colours.base.as_ref() 346 || dark_scheme.colours.text.as_ref() != light_scheme.colours.text.as_ref() 347 || dark_scheme.colours.primary.as_ref() != light_scheme.colours.primary.as_ref() 348 || dark_scheme.colours.link.as_ref() != light_scheme.colours.link.as_ref()); 349 350 Ok(InlineThemeValues { 351 background, 352 text, 353 primary, 354 link, 355 light_background: if light_differs { 356 Some(light_scheme.colours.base.to_string()) 357 } else { 358 None 359 }, 360 light_text: if light_differs { 361 Some(light_scheme.colours.text.to_string()) 362 } else { 363 None 364 }, 365 light_primary: if light_differs { 366 Some(light_scheme.colours.primary.to_string()) 367 } else { 368 None 369 }, 370 light_link: if light_differs { 371 Some(light_scheme.colours.link.to_string()) 372 } else { 373 None 374 }, 375 dark_background: if dark_differs { 376 Some(dark_scheme.colours.base.to_string()) 377 } else { 378 None 379 }, 380 dark_text: if dark_differs { 381 Some(dark_scheme.colours.text.to_string()) 382 } else { 383 None 384 }, 385 dark_primary: if dark_differs { 386 Some(dark_scheme.colours.primary.to_string()) 387 } else { 388 None 389 }, 390 dark_link: if dark_differs { 391 Some(dark_scheme.colours.link.to_string()) 392 } else { 393 None 394 }, 395 light_code_theme, 396 dark_code_theme, 397 default_mode, 398 }) 399}