synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};

use crate::components::modal::Modal;

/// A user-defined custom theme.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CustomTheme {
    pub name: String,
    pub bg_primary: String,
    pub bg_secondary: String,
    pub bg_tertiary: String,
    pub text_primary: String,
    pub text_secondary: String,
    pub accent_color: String,
    pub border_color: String,
    pub error_color: String,
    pub success_color: String,
}

impl Default for CustomTheme {
    fn default() -> Self {
        Self {
            name: "My Theme".to_string(),
            bg_primary: "#1a1a2e".to_string(),
            bg_secondary: "#16213e".to_string(),
            bg_tertiary: "#0f3460".to_string(),
            text_primary: "#e0e0e0".to_string(),
            text_secondary: "#a0a0a0".to_string(),
            accent_color: "#4a9eff".to_string(),
            border_color: "#333".to_string(),
            error_color: "#ff5b55".to_string(),
            success_color: "#0dbd8b".to_string(),
        }
    }
}

/// Custom theme editor/import dialog.
#[component]
pub fn CustomThemeDialog(on_close: EventHandler<()>, on_apply: EventHandler<CustomTheme>) -> Element {
    let mut theme = use_signal(CustomTheme::default);
    let mut import_json = use_signal(|| String::new());
    let mut import_error = use_signal(|| Option::<String>::None);
    let mut mode = use_signal(|| ThemeEditorMode::Editor);

    let on_export = move |_| {
        let t = theme.read().clone();
        if let Ok(json) = serde_json::to_string_pretty(&t) {
            import_json.set(json);
            mode.set(ThemeEditorMode::ImportExport);
        }
    };

    let on_import = move |_| {
        let json = import_json.read().clone();
        match serde_json::from_str::<CustomTheme>(&json) {
            Ok(imported) => {
                theme.set(imported);
                import_error.set(None);
                mode.set(ThemeEditorMode::Editor);
            }
            Err(e) => {
                import_error.set(Some(format!("Invalid theme JSON: {e}")));
            }
        }
    };

    rsx! {
        Modal {
            title: "Custom Theme".to_string(),
            on_close: move |_| on_close.call(()),

            div {
                class: "custom-theme-dialog",

                // Mode tabs
                div {
                    class: "custom-theme-dialog__tabs",
                    button {
                        class: if *mode.read() == ThemeEditorMode::Editor { "btn btn--sm btn--primary" } else { "btn btn--sm" },
                        onclick: move |_| mode.set(ThemeEditorMode::Editor),
                        "Editor"
                    }
                    button {
                        class: if *mode.read() == ThemeEditorMode::ImportExport { "btn btn--sm btn--primary" } else { "btn btn--sm" },
                        onclick: move |_| mode.set(ThemeEditorMode::ImportExport),
                        "Import / Export"
                    }
                }

                if *mode.read() == ThemeEditorMode::Editor {
                    div {
                        class: "custom-theme-dialog__editor",
                        ColorField { label: "Theme Name", value: theme.read().name.clone(), is_text: true,
                            on_change: move |v: String| theme.write().name = v }
                        ColorField { label: "Background Primary", value: theme.read().bg_primary.clone(), is_text: false,
                            on_change: move |v: String| theme.write().bg_primary = v }
                        ColorField { label: "Background Secondary", value: theme.read().bg_secondary.clone(), is_text: false,
                            on_change: move |v: String| theme.write().bg_secondary = v }
                        ColorField { label: "Text Primary", value: theme.read().text_primary.clone(), is_text: false,
                            on_change: move |v: String| theme.write().text_primary = v }
                        ColorField { label: "Text Secondary", value: theme.read().text_secondary.clone(), is_text: false,
                            on_change: move |v: String| theme.write().text_secondary = v }
                        ColorField { label: "Accent Color", value: theme.read().accent_color.clone(), is_text: false,
                            on_change: move |v: String| theme.write().accent_color = v }
                        ColorField { label: "Border Color", value: theme.read().border_color.clone(), is_text: false,
                            on_change: move |v: String| theme.write().border_color = v }

                        // Preview
                        {
                            let bg = theme.read().bg_primary.clone();
                            let fg = theme.read().text_primary.clone();
                            let border = theme.read().border_color.clone();
                            let sec = theme.read().text_secondary.clone();
                            let acc = theme.read().accent_color.clone();
                            rsx! {
                                div {
                                    class: "custom-theme-dialog__preview",
                                    style: "background: {bg}; color: {fg}; padding: 16px; border-radius: 8px; border: 1px solid {border};",
                                    h4 { "Preview" }
                                    p {
                                        style: "color: {sec};",
                                        "Secondary text preview"
                                    }
                                    button {
                                        style: "background: {acc}; color: white; border: none; padding: 6px 12px; border-radius: 4px;",
                                        "Accent Button"
                                    }
                                }
                            }
                        }
                    }
                } else {
                    div {
                        class: "custom-theme-dialog__import",
                        p { "Paste theme JSON to import, or export your current theme." }
                        textarea {
                            class: "settings-textarea",
                            rows: "12",
                            placeholder: "Paste theme JSON here...",
                            value: "{import_json}",
                            oninput: move |evt| import_json.set(evt.value()),
                        }
                        if let Some(ref err) = *import_error.read() {
                            div { class: "custom-theme-dialog__error", "{err}" }
                        }
                        div {
                            class: "custom-theme-dialog__actions",
                            button { class: "btn btn--secondary", onclick: on_export, "Export Current" }
                            button { class: "btn btn--primary", onclick: on_import, "Import" }
                        }
                    }
                }

                div {
                    class: "custom-theme-dialog__footer",
                    button { class: "btn btn--secondary", onclick: move |_| on_close.call(()), "Cancel" }
                    button {
                        class: "btn btn--primary",
                        onclick: move |_| {
                            on_apply.call(theme.read().clone());
                            on_close.call(());
                        },
                        "Apply Theme"
                    }
                }
            }
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
enum ThemeEditorMode {
    Editor,
    ImportExport,
}

#[component]
fn ColorField(label: String, value: String, is_text: bool, on_change: EventHandler<String>) -> Element {
    let input_type = if is_text { "text" } else { "color" };
    rsx! {
        div {
            class: "custom-theme-dialog__field",
            label { "{label}" }
            div {
                class: "custom-theme-dialog__input-row",
                input {
                    r#type: input_type,
                    value: "{value}",
                    oninput: move |evt| on_change.call(evt.value()),
                }
                if !is_text {
                    input {
                        r#type: "text",
                        class: "custom-theme-dialog__hex-input",
                        value: "{value}",
                        oninput: move |evt| on_change.call(evt.value()),
                    }
                }
            }
        }
    }
}