//! Sync notebook theme to ColourScheme and Theme records. use jacquard::client::AgentSessionExt; use jacquard::types::aturi::AtUri; use jacquard::types::string::{Cid, RecordKey}; use jacquard::CowStr; use jacquard::IntoStatic; use weaver_api::com_atproto::repo::strong_ref::StrongRef; use weaver_api::sh_weaver::notebook::colour_scheme::ColourScheme; use weaver_api::sh_weaver::notebook::theme::{ Theme, ThemeDarkCodeTheme, ThemeFonts, ThemeLightCodeTheme, ThemeSpacing, }; use weaver_common::WeaverError; use weaver_renderer::colour_gen::{ detect_variant, generate_counterpart_palette, generate_palette, ThemeVariant, }; use crate::components::inline_theme_editor::InlineThemeValues; use crate::fetch::Fetcher; /// Result of syncing theme records. pub struct ThemeSyncResult { pub theme_uri: AtUri<'static>, pub theme_cid: Cid<'static>, } /// Create or update theme records for a notebook. /// /// Returns the Theme record's URI and CID for updating Book.theme. pub async fn sync_theme( fetcher: &Fetcher, existing_theme_ref: Option<&StrongRef<'_>>, values: &InlineThemeValues, ) -> Result { // Determine primary variant. let primary_variant = match values.default_mode.as_str() { "light" => ThemeVariant::Light, "dark" => ThemeVariant::Dark, _ => detect_variant(&values.background) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, }; // Generate palettes. let light_inputs = values.light_inputs(); let dark_inputs = values.dark_inputs(); let (light_palette, dark_palette) = match primary_variant { ThemeVariant::Light => { let light = generate_palette(&light_inputs, ThemeVariant::Light) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; let dark = if values.dark_background.is_some() { generate_palette(&dark_inputs, ThemeVariant::Dark) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? } else { generate_counterpart_palette(&light_inputs, ThemeVariant::Light) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? }; (light, dark) } ThemeVariant::Dark => { let dark = generate_palette(&dark_inputs, ThemeVariant::Dark) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; let light = if values.light_background.is_some() { generate_palette(&light_inputs, ThemeVariant::Light) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? } else { generate_counterpart_palette(&dark_inputs, ThemeVariant::Dark) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? }; (light, dark) } }; // Code theme values. let light_code = ThemeLightCodeTheme::CodeThemeName(Box::new(CowStr::from( values.light_code_theme.clone(), ))); let dark_code = ThemeDarkCodeTheme::CodeThemeName(Box::new(CowStr::from(values.dark_code_theme.clone()))); let default_theme: Option> = match values.default_mode.as_str() { "light" => Some(CowStr::from("light")), "dark" => Some(CowStr::from("dark")), _ => None, }; if let Some(theme_ref) = existing_theme_ref { // UPDATE existing records. let theme_uri = &theme_ref.uri; let existing_theme: Theme<'static> = fetcher .fetch_record(&Theme::uri(theme_uri.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .value .into_static(); // Update light ColourScheme. let light_scheme_rkey = existing_theme .light_scheme .uri .rkey() .ok_or_else(|| WeaverError::InvalidNotebook("Light scheme URI missing rkey".into()))?; let light_result = fetcher .put_record( RecordKey::any(light_scheme_rkey.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .into_static(), ColourScheme::new() .name(CowStr::from("Custom Light")) .variant(CowStr::from("light")) .colours(light_palette) .build(), ) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; // Update dark ColourScheme. let dark_scheme_rkey = existing_theme .dark_scheme .uri .rkey() .ok_or_else(|| WeaverError::InvalidNotebook("Dark scheme URI missing rkey".into()))?; let dark_result = fetcher .put_record( RecordKey::any(dark_scheme_rkey.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .into_static(), ColourScheme::new() .name(CowStr::from("Custom Dark")) .variant(CowStr::from("dark")) .colours(dark_palette) .build(), ) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; // Update Theme with new CIDs. let theme_rkey = theme_uri .rkey() .ok_or_else(|| WeaverError::InvalidNotebook("Theme URI missing rkey".into()))?; let theme_result = fetcher .put_record( RecordKey::any(theme_rkey.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .into_static(), Theme::new() .light_scheme( StrongRef::new() .uri(light_result.uri.clone().into_static()) .cid(light_result.cid.clone().into_static()) .build(), ) .dark_scheme( StrongRef::new() .uri(dark_result.uri.clone().into_static()) .cid(dark_result.cid.clone().into_static()) .build(), ) .light_code_theme(light_code) .dark_code_theme(dark_code) .fonts( ThemeFonts::new() .body(vec![]) .heading(vec![]) .monospace(vec![]) .build(), ) .spacing(ThemeSpacing { base_size: CowStr::from("1rem"), line_height: CowStr::from("1.6"), scale: CowStr::from("1.25"), extra_data: None, }) .maybe_default_theme(default_theme) .build(), ) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; Ok(ThemeSyncResult { theme_uri: theme_result.uri.into_static(), theme_cid: theme_result.cid.into_static(), }) } else { // CREATE new records with fresh TIDs. let light_scheme = ColourScheme::new() .name(CowStr::from("Custom Light")) .variant(CowStr::from("light")) .colours(light_palette) .build(); let light_result = fetcher .create_record(light_scheme, None) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; let dark_scheme = ColourScheme::new() .name(CowStr::from("Custom Dark")) .variant(CowStr::from("dark")) .colours(dark_palette) .build(); let dark_result = fetcher .create_record(dark_scheme, None) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; let theme = Theme::new() .light_scheme( StrongRef::new() .uri(light_result.uri.clone().into_static()) .cid(light_result.cid.clone().into_static()) .build(), ) .dark_scheme( StrongRef::new() .uri(dark_result.uri.clone().into_static()) .cid(dark_result.cid.clone().into_static()) .build(), ) .light_code_theme(light_code) .dark_code_theme(dark_code) .fonts( ThemeFonts::new() .body(vec![]) .heading(vec![]) .monospace(vec![]) .build(), ) .spacing(ThemeSpacing { base_size: CowStr::from("1rem"), line_height: CowStr::from("1.6"), scale: CowStr::from("1.25"), extra_data: None, }) .maybe_default_theme(default_theme) .build(); let theme_result = fetcher .create_record(theme, None) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?; Ok(ThemeSyncResult { theme_uri: theme_result.uri.into_static(), theme_cid: theme_result.cid.into_static(), }) } } /// Check if theme values have been customized from defaults. pub fn has_theme_customizations(values: &InlineThemeValues) -> bool { !values.background.is_empty() || !values.text.is_empty() || !values.primary.is_empty() || !values.link.is_empty() || values.light_background.is_some() || values.dark_background.is_some() } /// Load theme values from an existing theme reference. /// /// Fetches Theme and ColourScheme records, extracts the 4 base colours /// (base→background, text→text, primary→primary, link→link). pub async fn load_theme_values( fetcher: &Fetcher, theme_ref: &StrongRef<'_>, ) -> Result { // Fetch Theme record. let theme: Theme<'static> = fetcher .fetch_record( &Theme::uri(theme_ref.uri.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, ) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .value .into_static(); // Fetch light ColourScheme. let light_scheme: ColourScheme<'static> = fetcher .fetch_record( &ColourScheme::uri(theme.light_scheme.uri.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, ) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .value .into_static(); // Fetch dark ColourScheme. let dark_scheme: ColourScheme<'static> = fetcher .fetch_record( &ColourScheme::uri(theme.dark_scheme.uri.as_ref()) .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))?, ) .await .map_err(|e| WeaverError::InvalidNotebook(e.to_string().into()))? .value .into_static(); // Determine default mode. let default_mode = theme .default_theme .as_ref() .map(|s| s.to_string()) .unwrap_or_else(|| "auto".to_string()); // Extract code themes. let light_code_theme = match &theme.light_code_theme { ThemeLightCodeTheme::CodeThemeName(name) => name.as_ref().to_string(), ThemeLightCodeTheme::CodeThemeFile(file) => file.name.as_ref().to_string(), ThemeLightCodeTheme::Unknown(_) => "base16-ocean.light".to_string(), }; let dark_code_theme = match &theme.dark_code_theme { ThemeDarkCodeTheme::CodeThemeName(name) => name.as_ref().to_string(), ThemeDarkCodeTheme::CodeThemeFile(file) => file.name.as_ref().to_string(), ThemeDarkCodeTheme::Unknown(_) => "base16-ocean.dark".to_string(), }; // Determine primary variant and extract base colours. let (background, text, primary, link) = match default_mode.as_str() { "dark" => ( dark_scheme.colours.base.to_string(), dark_scheme.colours.text.to_string(), dark_scheme.colours.primary.to_string(), dark_scheme.colours.link.to_string(), ), _ => ( light_scheme.colours.base.to_string(), light_scheme.colours.text.to_string(), light_scheme.colours.primary.to_string(), light_scheme.colours.link.to_string(), ), }; // Check if variants differ (user customized per-variant colours). let light_differs = default_mode == "dark" && (light_scheme.colours.base.as_ref() != dark_scheme.colours.base.as_ref() || light_scheme.colours.text.as_ref() != dark_scheme.colours.text.as_ref() || light_scheme.colours.primary.as_ref() != dark_scheme.colours.primary.as_ref() || light_scheme.colours.link.as_ref() != dark_scheme.colours.link.as_ref()); let dark_differs = default_mode != "dark" && (dark_scheme.colours.base.as_ref() != light_scheme.colours.base.as_ref() || dark_scheme.colours.text.as_ref() != light_scheme.colours.text.as_ref() || dark_scheme.colours.primary.as_ref() != light_scheme.colours.primary.as_ref() || dark_scheme.colours.link.as_ref() != light_scheme.colours.link.as_ref()); Ok(InlineThemeValues { background, text, primary, link, light_background: if light_differs { Some(light_scheme.colours.base.to_string()) } else { None }, light_text: if light_differs { Some(light_scheme.colours.text.to_string()) } else { None }, light_primary: if light_differs { Some(light_scheme.colours.primary.to_string()) } else { None }, light_link: if light_differs { Some(light_scheme.colours.link.to_string()) } else { None }, dark_background: if dark_differs { Some(dark_scheme.colours.base.to_string()) } else { None }, dark_text: if dark_differs { Some(dark_scheme.colours.text.to_string()) } else { None }, dark_primary: if dark_differs { Some(dark_scheme.colours.primary.to_string()) } else { None }, dark_link: if dark_differs { Some(dark_scheme.colours.link.to_string()) } else { None }, light_code_theme, dark_code_theme, default_mode, }) }