use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use crate::components::modal::Modal;
#[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(),
}
}
}
#[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",
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 }
{
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()),
}
}
}
}
}
}