synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;

use crate::persistence::app_state as prefs;
use crate::state::app_state::AppState;
use crate::theme::colors::Theme;

/// Font size option.
#[derive(Clone, Copy, Debug, PartialEq)]
enum FontSize {
    Small,  // 12
    Medium, // 14
    Large,  // 16
}

impl FontSize {
    fn value(self) -> u8 {
        match self {
            Self::Small => 12,
            Self::Medium => 14,
            Self::Large => 16,
        }
    }

    fn from_value(v: u8) -> Self {
        match v {
            0..=12 => Self::Small,
            13..=14 => Self::Medium,
            _ => Self::Large,
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Small => "Small",
            Self::Medium => "Medium",
            Self::Large => "Large",
        }
    }
}

/// Message layout option.
#[derive(Clone, Copy, Debug, PartialEq)]
enum MessageLayout {
    Modern,
    Compact,
    Bubble,
}

/// Time format option.
#[derive(Clone, Copy, Debug, PartialEq)]
enum TimeFormat {
    TwelveHour,
    TwentyFourHour,
}

/// Appearance settings panel (theme, font size, layout).
#[component]
pub fn AppearanceSettings() -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let current_theme = state.read().theme.clone();
    let mut font_size = use_signal(|| FontSize::Medium);
    let mut layout = use_signal(|| MessageLayout::Modern);
    let mut time_format = use_signal(|| TimeFormat::TwelveHour);
    let mut show_join_leave = use_signal(|| true);
    let mut loaded = use_signal(|| false);

    // Load saved preferences
    if !*loaded.read() {
        loaded.set(true);
        spawn(async move {
            if let Ok(p) = prefs::load_preferences().await {
                font_size.set(FontSize::from_value(p.font_size));
                if p.compact_mode {
                    layout.set(MessageLayout::Compact);
                }
            }
        });
    }

    let save_prefs = move |theme: Theme, fs: FontSize, ml: MessageLayout| {
        spawn(async move {
            let mut p = prefs::load_preferences().await.unwrap_or_default();
            p.theme = theme;
            p.font_size = fs.value();
            p.compact_mode = matches!(ml, MessageLayout::Compact);
            if let Err(e) = prefs::save_preferences(&p).await {
                tracing::error!("Failed to save appearance prefs: {e}");
            }
        });
    };

    rsx! {
        div {
            class: "appearance-settings",

            h3 { "Appearance" }

            // Theme selection
            div {
                class: "settings-section",
                h4 { "Theme" }
                div {
                    class: "theme-selector",
                    button {
                        class: if current_theme == Theme::Light {
                            "theme-selector__option theme-selector__option--active"
                        } else {
                            "theme-selector__option"
                        },
                        onclick: move |_| {
                            state.write().theme = Theme::Light;
                            save_prefs(Theme::Light, *font_size.read(), *layout.read());
                        },
                        div { class: "theme-selector__preview theme-selector__preview--light" }
                        span { "Light" }
                    }
                    button {
                        class: if current_theme == Theme::Dark {
                            "theme-selector__option theme-selector__option--active"
                        } else {
                            "theme-selector__option"
                        },
                        onclick: move |_| {
                            state.write().theme = Theme::Dark;
                            save_prefs(Theme::Dark, *font_size.read(), *layout.read());
                        },
                        div { class: "theme-selector__preview theme-selector__preview--dark" }
                        span { "Dark" }
                    }
                    button {
                        class: if current_theme == Theme::HighContrast {
                            "theme-selector__option theme-selector__option--active"
                        } else {
                            "theme-selector__option"
                        },
                        onclick: move |_| {
                            state.write().theme = Theme::HighContrast;
                            save_prefs(Theme::HighContrast, *font_size.read(), *layout.read());
                        },
                        div { class: "theme-selector__preview theme-selector__preview--highcontrast" }
                        span { "High Contrast" }
                    }
                    button {
                        class: if current_theme == Theme::System {
                            "theme-selector__option theme-selector__option--active"
                        } else {
                            "theme-selector__option"
                        },
                        onclick: move |_| {
                            state.write().theme = Theme::System;
                            save_prefs(Theme::System, *font_size.read(), *layout.read());
                        },
                        div { class: "theme-selector__preview theme-selector__preview--system" }
                        span { "System" }
                    }
                }
            }

            // Font size
            div {
                class: "settings-section",
                h4 { "Font Size" }
                div {
                    class: "font-size-selector",
                    for fs in [FontSize::Small, FontSize::Medium, FontSize::Large] {
                        {
                            let is_active = *font_size.read() == fs;
                            let label = fs.label();
                            rsx! {
                                button {
                                    class: if is_active { "btn btn--sm btn--primary" } else { "btn btn--sm" },
                                    onclick: move |_| {
                                        font_size.set(fs);
                                        save_prefs(state.read().theme.clone(), fs, *layout.read());
                                    },
                                    "{label}"
                                }
                            }
                        }
                    }
                }
            }

            // Message layout
            div {
                class: "settings-section",
                h4 { "Message Layout" }
                div {
                    class: "layout-selector",
                    label {
                        class: "layout-selector__option",
                        input {
                            r#type: "radio",
                            name: "layout",
                            value: "modern",
                            checked: *layout.read() == MessageLayout::Modern,
                            oninput: move |_| {
                                layout.set(MessageLayout::Modern);
                                save_prefs(state.read().theme.clone(), *font_size.read(), MessageLayout::Modern);
                            },
                        }
                        span { "Modern" }
                    }
                    label {
                        class: "layout-selector__option",
                        input {
                            r#type: "radio",
                            name: "layout",
                            value: "irc",
                            checked: *layout.read() == MessageLayout::Compact,
                            oninput: move |_| {
                                layout.set(MessageLayout::Compact);
                                save_prefs(state.read().theme.clone(), *font_size.read(), MessageLayout::Compact);
                            },
                        }
                        span { "IRC (compact)" }
                    }
                    label {
                        class: "layout-selector__option",
                        input {
                            r#type: "radio",
                            name: "layout",
                            value: "bubble",
                            checked: *layout.read() == MessageLayout::Bubble,
                            oninput: move |_| {
                                layout.set(MessageLayout::Bubble);
                                state.write().bubble_layout = true;
                                save_prefs(state.read().theme.clone(), *font_size.read(), MessageLayout::Bubble);
                            },
                        }
                        span { "Bubble" }
                    }
                }
            }

            // Time format
            div {
                class: "settings-section",
                h4 { "Time Format" }
                div {
                    class: "layout-selector",
                    label {
                        class: "layout-selector__option",
                        input {
                            r#type: "radio",
                            name: "time_format",
                            value: "12h",
                            checked: *time_format.read() == TimeFormat::TwelveHour,
                            oninput: move |_| {
                                time_format.set(TimeFormat::TwelveHour);
                                state.write().use_24h_time = false;
                            },
                        }
                        span { "12-hour (3:45 PM)" }
                    }
                    label {
                        class: "layout-selector__option",
                        input {
                            r#type: "radio",
                            name: "time_format",
                            value: "24h",
                            checked: *time_format.read() == TimeFormat::TwentyFourHour,
                            oninput: move |_| {
                                time_format.set(TimeFormat::TwentyFourHour);
                                state.write().use_24h_time = true;
                            },
                        }
                        span { "24-hour (15:45)" }
                    }
                }
            }

            // Show/hide join & leave events
            div {
                class: "settings-section",
                h4 { "Timeline" }
                label {
                    class: "settings-toggle",
                    input {
                        r#type: "checkbox",
                        checked: *show_join_leave.read(),
                        oninput: move |evt| {
                            show_join_leave.set(evt.checked());
                            state.write().show_join_leave = evt.checked();
                        },
                    }
                    div {
                        class: "settings-toggle__info",
                        span { class: "settings-toggle__label", "Show join/leave messages" }
                        span { class: "settings-toggle__description", "Show when people join or leave rooms" }
                    }
                }
            }
        }
    }
}