Skip to main content

leptos_next_metadata/themes/
mod.rs

1//! Theme Support for OG Images
2//!
3//! Provides a comprehensive theming system for customizable OG image generation
4//! with predefined themes, custom theme creation, and dynamic theme switching.
5
6pub mod builder;
7pub mod predefined;
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12
13use crate::canvas_types::{
14    CanvasOgParams, GradientType, OgImageLayer, TextGradient, TextOutline, TextShadow,
15};
16use crate::error::{ErrorKind, MetadataError};
17
18/// Theme identifier
19pub type ThemeId = String;
20
21/// Theme version for compatibility tracking
22pub type ThemeVersion = String;
23
24/// Theme category for organization
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub enum ThemeCategory {
27    Business,
28    Technology,
29    Creative,
30    Minimalist,
31    Bold,
32    Elegant,
33    Modern,
34    Classic,
35    Custom,
36}
37
38impl fmt::Display for ThemeCategory {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            ThemeCategory::Business => write!(f, "Business"),
42            ThemeCategory::Technology => write!(f, "Technology"),
43            ThemeCategory::Creative => write!(f, "Creative"),
44            ThemeCategory::Minimalist => write!(f, "Minimalist"),
45            ThemeCategory::Bold => write!(f, "Bold"),
46            ThemeCategory::Elegant => write!(f, "Elegant"),
47            ThemeCategory::Modern => write!(f, "Modern"),
48            ThemeCategory::Classic => write!(f, "Classic"),
49            ThemeCategory::Custom => write!(f, "Custom"),
50        }
51    }
52}
53
54/// Color palette for themes
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ColorPalette {
57    /// Primary color (hex)
58    pub primary: String,
59    /// Secondary color (hex)
60    pub secondary: String,
61    /// Background color (hex)
62    pub background: String,
63    /// Text color (hex)
64    pub text: String,
65    /// Accent color (hex)
66    pub accent: String,
67    /// Additional colors for gradients and effects
68    pub additional: Vec<String>,
69}
70
71impl Default for ColorPalette {
72    fn default() -> Self {
73        Self {
74            primary: "#4f46e5".to_string(),
75            secondary: "#7c3aed".to_string(),
76            background: "#ffffff".to_string(),
77            text: "#1f2937".to_string(),
78            accent: "#f59e0b".to_string(),
79            additional: vec!["#10b981".to_string(), "#ef4444".to_string()],
80        }
81    }
82}
83
84/// Typography settings for themes
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Typography {
87    /// Primary font family
88    pub primary_font: String,
89    /// Secondary font family
90    pub secondary_font: String,
91    /// Title font size
92    pub title_size: u32,
93    /// Description font size
94    pub description_size: u32,
95    /// Font weights
96    pub weights: FontWeights,
97}
98
99/// Font weight settings
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct FontWeights {
102    pub light: u32,
103    pub normal: u32,
104    pub medium: u32,
105    pub bold: u32,
106}
107
108impl Default for FontWeights {
109    fn default() -> Self {
110        Self {
111            light: 300,
112            normal: 400,
113            medium: 500,
114            bold: 700,
115        }
116    }
117}
118
119impl Default for Typography {
120    fn default() -> Self {
121        Self {
122            primary_font: "Inter, sans-serif".to_string(),
123            secondary_font: "Georgia, serif".to_string(),
124            title_size: 48,
125            description_size: 24,
126            weights: FontWeights::default(),
127        }
128    }
129}
130
131/// Layout configuration for themes
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct Layout {
134    /// Padding around content
135    pub padding: u32,
136    /// Spacing between elements
137    pub spacing: u32,
138    /// Content alignment
139    pub alignment: ContentAlignment,
140    /// Logo position
141    pub logo_position: LogoPosition,
142    /// Text alignment
143    pub text_alignment: TextAlignment,
144}
145
146/// Content alignment options
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub enum ContentAlignment {
149    Left,
150    Center,
151    Right,
152    Justify,
153}
154
155/// Logo position options
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum LogoPosition {
158    TopLeft,
159    TopRight,
160    BottomLeft,
161    BottomRight,
162    Center,
163    None,
164}
165
166/// Text alignment options
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub enum TextAlignment {
169    Left,
170    Center,
171    Right,
172}
173
174impl Default for Layout {
175    fn default() -> Self {
176        Self {
177            padding: 60,
178            spacing: 30,
179            alignment: ContentAlignment::Center,
180            logo_position: LogoPosition::None,
181            text_alignment: TextAlignment::Center,
182        }
183    }
184}
185
186/// Visual effects for themes
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct VisualEffects {
189    /// Background effects
190    pub background: BackgroundEffects,
191    /// Text effects
192    pub text: TextEffects,
193    /// Border effects
194    pub border: BorderEffects,
195}
196
197/// Background effects
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct BackgroundEffects {
200    /// Background type
201    pub background_type: BackgroundType,
202    /// Gradient configuration
203    pub gradient: Option<GradientConfig>,
204    /// Pattern configuration
205    pub pattern: Option<PatternConfig>,
206    /// Blur effect
207    pub blur: Option<f64>,
208}
209
210/// Background type
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub enum BackgroundType {
213    Solid,
214    Gradient,
215    Pattern,
216    Image,
217}
218
219/// Gradient configuration
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct GradientConfig {
222    pub gradient_type: GradientType,
223    pub colors: Vec<String>,
224    pub angle: f64,
225    pub stops: Vec<f64>,
226}
227
228/// Pattern configuration
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct PatternConfig {
231    pub pattern_type: PatternType,
232    pub color: String,
233    pub opacity: f64,
234    pub size: u32,
235}
236
237/// Pattern types
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum PatternType {
240    Dots,
241    Lines,
242    Grid,
243    Waves,
244    Geometric,
245}
246
247/// Text effects
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct TextEffects {
250    /// Text shadow
251    pub shadow: Option<TextShadow>,
252    /// Text outline
253    pub outline: Option<TextOutline>,
254    /// Text gradient
255    pub gradient: Option<TextGradient>,
256    /// Text glow
257    pub glow: Option<GlowEffect>,
258}
259
260/// Glow effect
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct GlowEffect {
263    pub color: String,
264    pub intensity: f64,
265    pub radius: f64,
266}
267
268/// Border effects
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct BorderEffects {
271    /// Border width
272    pub width: f64,
273    /// Border color
274    pub color: String,
275    /// Border radius
276    pub radius: f64,
277    /// Border style
278    pub style: BorderStyle,
279}
280
281/// Border styles
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub enum BorderStyle {
284    Solid,
285    Dashed,
286    Dotted,
287    Double,
288}
289
290impl Default for VisualEffects {
291    fn default() -> Self {
292        Self {
293            background: BackgroundEffects {
294                background_type: BackgroundType::Solid,
295                gradient: None,
296                pattern: None,
297                blur: None,
298            },
299            text: TextEffects {
300                shadow: None,
301                outline: None,
302                gradient: None,
303                glow: None,
304            },
305            border: BorderEffects {
306                width: 0.0,
307                color: "#000000".to_string(),
308                radius: 0.0,
309                style: BorderStyle::Solid,
310            },
311        }
312    }
313}
314
315/// Complete theme definition
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct Theme {
318    /// Unique theme identifier
319    pub id: ThemeId,
320    /// Theme name
321    pub name: String,
322    /// Theme description
323    pub description: String,
324    /// Theme version
325    pub version: ThemeVersion,
326    /// Theme category
327    pub category: ThemeCategory,
328    /// Color palette
329    pub colors: ColorPalette,
330    /// Typography settings
331    pub typography: Typography,
332    /// Layout configuration
333    pub layout: Layout,
334    /// Visual effects
335    pub effects: VisualEffects,
336    /// Custom layers for advanced themes
337    pub custom_layers: Vec<OgImageLayer>,
338    /// Theme metadata
339    pub metadata: ThemeMetadata,
340}
341
342/// Theme metadata
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct ThemeMetadata {
345    /// Theme author
346    pub author: String,
347    /// Creation date
348    pub created_at: String,
349    /// Last modified date
350    pub modified_at: String,
351    /// Tags for search and categorization
352    pub tags: Vec<String>,
353    /// License information
354    pub license: String,
355    /// Theme preview URL
356    pub preview_url: Option<String>,
357}
358
359impl Default for ThemeMetadata {
360    fn default() -> Self {
361        Self {
362            author: "leptos-next-metadata".to_string(),
363            created_at: chrono::Utc::now().to_rfc3339(),
364            modified_at: chrono::Utc::now().to_rfc3339(),
365            tags: vec![],
366            license: "MIT".to_string(),
367            preview_url: None,
368        }
369    }
370}
371
372impl Theme {
373    /// Create a new theme
374    pub fn new(id: ThemeId, name: String, description: String, category: ThemeCategory) -> Self {
375        Self {
376            id,
377            name,
378            description,
379            version: "1.0.0".to_string(),
380            category,
381            colors: ColorPalette::default(),
382            typography: Typography::default(),
383            layout: Layout::default(),
384            effects: VisualEffects::default(),
385            custom_layers: vec![],
386            metadata: ThemeMetadata::default(),
387        }
388    }
389
390    /// Apply theme to canvas parameters
391    pub fn apply_to_canvas_params(&self, mut params: CanvasOgParams) -> CanvasOgParams {
392        // Apply colors
393        params.background_color = Some(self.colors.background.clone());
394        params.text_color = Some(self.colors.text.clone());
395
396        // Apply typography
397        params.font_family = Some(self.typography.primary_font.clone());
398        params.title_font_size = Some(self.typography.title_size);
399        params.description_font_size = Some(self.typography.description_size);
400
401        // Apply layout
402        params.padding = Some(self.layout.padding);
403        params.text_align = Some(match self.layout.text_alignment {
404            TextAlignment::Left => crate::canvas_types::TextAlign::Left,
405            TextAlignment::Center => crate::canvas_types::TextAlign::Center,
406            TextAlignment::Right => crate::canvas_types::TextAlign::Right,
407        });
408
409        // Apply visual effects
410        if let Some(gradient) = &self.effects.text.gradient {
411            params.text_gradient = Some(gradient.clone());
412        }
413        if let Some(shadow) = &self.effects.text.shadow {
414            params.text_shadow = Some(shadow.clone());
415        }
416        if let Some(outline) = &self.effects.text.outline {
417            params.text_outline = Some(outline.clone());
418        }
419
420        // Add custom layers
421        if !self.custom_layers.is_empty() {
422            params.layers = Some(self.custom_layers.clone());
423        }
424
425        params
426    }
427
428    /// Generate theme preview
429    pub fn generate_preview(&self) -> Result<String, MetadataError> {
430        let _preview_params = CanvasOgParams {
431            title: format!("{} Theme Preview", self.name),
432            description: Some(self.description.clone()),
433            width: Some(1200),
434            height: Some(630),
435            background_color: Some(self.colors.background.clone()),
436            text_color: Some(self.colors.text.clone()),
437            font_family: Some(self.typography.primary_font.clone()),
438            title_font_size: Some(self.typography.title_size),
439            description_font_size: Some(self.typography.description_size),
440            logo_url: None,
441            font_urls: None,
442            default_font_family: None,
443            layers: Some(self.custom_layers.clone()),
444            background_image_url: None,
445            background_image_opacity: None,
446            text_gradient: self.effects.text.gradient.clone(),
447            text_shadow: self.effects.text.shadow.clone(),
448            text_outline: self.effects.text.outline.clone(),
449            logo_position: Some(match self.layout.logo_position {
450                LogoPosition::TopLeft => crate::canvas_types::LogoPosition::TopLeft,
451                LogoPosition::TopRight => crate::canvas_types::LogoPosition::TopRight,
452                LogoPosition::BottomLeft => crate::canvas_types::LogoPosition::BottomLeft,
453                LogoPosition::BottomRight => crate::canvas_types::LogoPosition::BottomRight,
454                LogoPosition::Center => crate::canvas_types::LogoPosition::Center,
455                LogoPosition::None => crate::canvas_types::LogoPosition::TopLeft,
456            }),
457            text_align: Some(match self.layout.text_alignment {
458                TextAlignment::Left => crate::canvas_types::TextAlign::Left,
459                TextAlignment::Center => crate::canvas_types::TextAlign::Center,
460                TextAlignment::Right => crate::canvas_types::TextAlign::Right,
461            }),
462            padding: Some(self.layout.padding),
463        };
464
465        // In a real implementation, this would generate an actual image
466        // For now, we'll return a placeholder
467        let svg_content = format!(
468            r#"<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
469                <rect width="100%" height="100%" fill="{}"/>
470                <text x="50%" y="50%" text-anchor="middle" dy=".3em" font-family="{}" font-size="{}" fill="{}">
471                    {} Theme Preview
472                </text>
473            </svg>"#,
474            self.colors.background,
475            self.typography.primary_font,
476            self.typography.title_size,
477            self.colors.text,
478            self.name
479        );
480
481        Ok(format!(
482            "data:image/svg+xml;base64,{}",
483            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, svg_content)
484        ))
485    }
486
487    /// Validate theme configuration
488    pub fn validate(&self) -> Result<(), MetadataError> {
489        if self.id.is_empty() {
490            return Err(MetadataError::new(
491                ErrorKind::Validation,
492                "Theme ID cannot be empty".to_string(),
493            ));
494        }
495
496        if self.name.is_empty() {
497            return Err(MetadataError::new(
498                ErrorKind::Validation,
499                "Theme name cannot be empty".to_string(),
500            ));
501        }
502
503        // Validate colors
504        if !self.is_valid_hex_color(&self.colors.primary) {
505            return Err(MetadataError::new(
506                ErrorKind::Validation,
507                "Invalid primary color format".to_string(),
508            ));
509        }
510
511        if !self.is_valid_hex_color(&self.colors.background) {
512            return Err(MetadataError::new(
513                ErrorKind::Validation,
514                "Invalid background color format".to_string(),
515            ));
516        }
517
518        // Validate typography
519        if self.typography.title_size < 12 || self.typography.title_size > 120 {
520            return Err(MetadataError::new(
521                ErrorKind::Validation,
522                "Title font size must be between 12 and 120".to_string(),
523            ));
524        }
525
526        Ok(())
527    }
528
529    /// Check if a string is a valid hex color
530    fn is_valid_hex_color(&self, color: &str) -> bool {
531        if color.starts_with('#') && color.len() == 7 {
532            color[1..].chars().all(|c| c.is_ascii_hexdigit())
533        } else {
534            false
535        }
536    }
537
538    /// Clone theme with new ID
539    pub fn clone_with_id(&self, new_id: ThemeId, new_name: String) -> Self {
540        let mut cloned = self.clone();
541        cloned.id = new_id;
542        cloned.name = new_name;
543        cloned.metadata.modified_at = chrono::Utc::now().to_rfc3339();
544        cloned
545    }
546
547    /// Update theme version
548    pub fn update_version(&mut self, new_version: ThemeVersion) {
549        self.version = new_version;
550        self.metadata.modified_at = chrono::Utc::now().to_rfc3339();
551    }
552}
553
554/// Theme manager for handling multiple themes
555#[derive(Debug, Clone)]
556pub struct ThemeManager {
557    /// Available themes
558    themes: HashMap<ThemeId, Theme>,
559    /// Default theme ID
560    default_theme_id: Option<ThemeId>,
561}
562
563impl ThemeManager {
564    /// Create a new theme manager
565    pub fn new() -> Self {
566        Self {
567            themes: HashMap::new(),
568            default_theme_id: None,
569        }
570    }
571
572    /// Add a theme to the manager
573    pub fn add_theme(&mut self, theme: Theme) -> Result<(), MetadataError> {
574        theme.validate()?;
575        let theme_id = theme.id.clone();
576        self.themes.insert(theme_id, theme);
577        Ok(())
578    }
579
580    /// Get a theme by ID
581    pub fn get_theme(&self, theme_id: &ThemeId) -> Option<&Theme> {
582        self.themes.get(theme_id)
583    }
584
585    /// Get all themes
586    pub fn get_all_themes(&self) -> Vec<&Theme> {
587        self.themes.values().collect()
588    }
589
590    /// Get themes by category
591    pub fn get_themes_by_category(&self, category: &ThemeCategory) -> Vec<&Theme> {
592        self.themes
593            .values()
594            .filter(|theme| &theme.category == category)
595            .collect()
596    }
597
598    /// Set default theme
599    pub fn set_default_theme(&mut self, theme_id: ThemeId) -> Result<(), MetadataError> {
600        if self.themes.contains_key(&theme_id) {
601            self.default_theme_id = Some(theme_id);
602            Ok(())
603        } else {
604            Err(MetadataError::new(
605                ErrorKind::Unknown,
606                format!("Theme with ID '{}' not found", theme_id),
607            ))
608        }
609    }
610
611    /// Get default theme
612    pub fn get_default_theme(&self) -> Option<&Theme> {
613        if let Some(ref default_id) = self.default_theme_id {
614            self.get_theme(default_id)
615        } else {
616            None
617        }
618    }
619
620    /// Remove a theme
621    pub fn remove_theme(&mut self, theme_id: &ThemeId) -> Option<Theme> {
622        if let Some(ref default_id) = self.default_theme_id {
623            if default_id == theme_id {
624                self.default_theme_id = None;
625            }
626        }
627        self.themes.remove(theme_id)
628    }
629
630    /// Search themes by name or tags
631    pub fn search_themes(&self, query: &str) -> Vec<&Theme> {
632        let query_lower = query.to_lowercase();
633        self.themes
634            .values()
635            .filter(|theme| {
636                theme.name.to_lowercase().contains(&query_lower)
637                    || theme.description.to_lowercase().contains(&query_lower)
638                    || theme
639                        .metadata
640                        .tags
641                        .iter()
642                        .any(|tag| tag.to_lowercase().contains(&query_lower))
643            })
644            .collect()
645    }
646
647    /// Get theme count
648    pub fn theme_count(&self) -> usize {
649        self.themes.len()
650    }
651
652    /// Clear all themes
653    pub fn clear(&mut self) {
654        self.themes.clear();
655        self.default_theme_id = None;
656    }
657}
658
659impl Default for ThemeManager {
660    fn default() -> Self {
661        Self::new()
662    }
663}
664
665// Re-export builder and predefined modules
666pub use builder::{QuickThemes, ThemeBuilder, ThemeHelpers};
667pub use predefined::create_predefined_themes;