use iced::widget::{button, column, container, row, scrollable, text, text_input}; use iced::{Alignment, Color, Element, Length, Theme}; use bds_core::i18n::UiLocale; use crate::app::Message; use crate::components::inputs; use crate::i18n::t; /// Collapsible section identifiers per editor_settings.allium. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SettingsSection { Project, Editor, Content, AI, Technology, Publishing, Data, MCP, } impl SettingsSection { pub fn all() -> &'static [SettingsSection] { &[ Self::Project, Self::Editor, Self::Content, Self::AI, Self::Technology, Self::Publishing, Self::Data, Self::MCP, ] } pub fn i18n_key(&self) -> &'static str { match self { Self::Project => "settings.nav.project", Self::Editor => "settings.nav.editor", Self::Content => "settings.nav.content", Self::AI => "settings.nav.ai", Self::Technology => "settings.nav.technology", Self::Publishing => "settings.nav.publishing", Self::Data => "settings.nav.data", Self::MCP => "settings.nav.mcp", } } } /// State for the settings view. #[derive(Debug, Clone)] pub struct SettingsViewState { pub search_query: String, pub collapsed: Vec, // Project pub project_name: String, pub project_description: String, pub data_path: String, pub public_url: String, pub default_author: String, pub max_posts_per_page: String, // Editor pub default_mode: String, pub diff_view_style: String, pub wrap_long_lines: bool, pub hide_unchanged_regions: bool, // Publishing pub ssh_mode: String, pub ssh_host: String, pub ssh_username: String, pub ssh_remote_path: String, // AI pub offline_mode: bool, pub system_prompt: String, } impl Default for SettingsViewState { fn default() -> Self { Self { search_query: String::new(), collapsed: Vec::new(), project_name: String::new(), project_description: String::new(), data_path: String::new(), public_url: String::new(), default_author: String::new(), max_posts_per_page: "50".to_string(), default_mode: "markdown".to_string(), diff_view_style: "inline".to_string(), wrap_long_lines: true, hide_unchanged_regions: false, ssh_mode: "rsync".to_string(), ssh_host: String::new(), ssh_username: String::new(), ssh_remote_path: String::new(), offline_mode: false, system_prompt: String::new(), } } } /// Settings view messages. #[derive(Debug, Clone)] pub enum SettingsMsg { SearchChanged(String), ToggleSection(SettingsSection), // Project ProjectNameChanged(String), ProjectDescriptionChanged(String), DataPathChanged(String), BrowseDataPath, ResetDataPath, PublicUrlChanged(String), DefaultAuthorChanged(String), MaxPostsPerPageChanged(String), SaveProject, // Editor DefaultModeChanged(String), DiffViewStyleChanged(String), WrapLongLinesChanged(bool), HideUnchangedRegionsChanged(bool), SaveEditor, // Publishing SshModeChanged(String), SshHostChanged(String), SshUsernameChanged(String), SshRemotePathChanged(String), SavePublishing, ClearPublishing, // AI OfflineModeChanged(bool), SystemPromptChanged(String), SaveSystemPrompt, ResetSystemPrompt, // Data maintenance RebuildPosts, RebuildMedia, RebuildScripts, RebuildTemplates, RebuildLinks, RegenerateThumbnails, OpenDataFolder, } /// Render the settings view. pub fn view<'a>( state: &'a SettingsViewState, locale: UiLocale, ) -> Element<'a, Message> { let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query) .on_input(|s| Message::Settings(SettingsMsg::SearchChanged(s))) .size(14); let query_lower = state.search_query.to_lowercase(); let mut sections = column![].spacing(12).width(Length::Fill); for section in SettingsSection::all() { let label = t(locale, section.i18n_key()); if !query_lower.is_empty() && !label.to_lowercase().contains(&query_lower) { continue; } let collapsed = state.collapsed.contains(section); let section_el = render_section(state, section, &label, collapsed, locale); sections = sections.push(section_el); } let body = scrollable( column![search, sections] .spacing(16) .padding(16) .width(Length::Fill), ) .width(Length::Fill) .height(Length::Fill); container(body) .width(Length::Fill) .height(Length::Fill) .into() } fn render_section<'a>( state: &'a SettingsViewState, section: &SettingsSection, label: &str, collapsed: bool, locale: UiLocale, ) -> Element<'a, Message> { let toggle_char = if collapsed { "\u{25B6}" } else { "\u{25BC}" }; let header = button( row![ text(toggle_char).size(12), text(label.to_string()).size(14).color(Color::WHITE), ] .spacing(8) .align_y(Alignment::Center), ) .on_press(Message::Settings(SettingsMsg::ToggleSection(section.clone()))) .padding([6, 8]) .width(Length::Fill) .style(|_: &Theme, _| button::Style::default()); if collapsed { return column![header].into(); } let content: Element<'a, Message> = match section { SettingsSection::Project => section_project(state, locale), SettingsSection::Editor => section_editor(state, locale), SettingsSection::Content => section_content(locale), SettingsSection::AI => section_ai(state, locale), SettingsSection::Technology => section_technology(locale), SettingsSection::Publishing => section_publishing(state, locale), SettingsSection::Data => section_data(locale), SettingsSection::MCP => section_mcp(locale), }; column![header, content] .spacing(4) .into() } fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> { let name = inputs::labeled_input( &t(locale, "settings.projectName"), "", &state.project_name, |s| Message::Settings(SettingsMsg::ProjectNameChanged(s)), ); let desc = inputs::labeled_input( &t(locale, "settings.projectDescription"), "", &state.project_description, |s| Message::Settings(SettingsMsg::ProjectDescriptionChanged(s)), ); let data_path = row![ inputs::labeled_input( &t(locale, "settings.dataPath"), "", &state.data_path, |s| Message::Settings(SettingsMsg::DataPathChanged(s)), ), button(text(t(locale, "settings.browse")).size(12)) .on_press(Message::Settings(SettingsMsg::BrowseDataPath)) .padding([6, 12]), button(text(t(locale, "settings.reset")).size(12)) .on_press(Message::Settings(SettingsMsg::ResetDataPath)) .padding([6, 12]), ] .spacing(8) .align_y(Alignment::End); let url = inputs::labeled_input( &t(locale, "settings.publicUrl"), "https://", &state.public_url, |s| Message::Settings(SettingsMsg::PublicUrlChanged(s)), ); let author = inputs::labeled_input( &t(locale, "settings.defaultAuthor"), "", &state.default_author, |s| Message::Settings(SettingsMsg::DefaultAuthorChanged(s)), ); let max_posts = inputs::labeled_input( &t(locale, "settings.maxPostsPerPage"), "50", &state.max_posts_per_page, |s| Message::Settings(SettingsMsg::MaxPostsPerPageChanged(s)), ); let save = button(text(t(locale, "common.save")).size(13)) .on_press(Message::Settings(SettingsMsg::SaveProject)) .style(inputs::primary_button) .padding([6, 16]); column![name, desc, data_path, url, author, max_posts, save] .spacing(8) .padding([0, 16]) .into() } fn section_editor<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> { let wrap = inputs::labeled_checkbox( &t(locale, "settings.wrapLongLines"), state.wrap_long_lines, |b| Message::Settings(SettingsMsg::WrapLongLinesChanged(b)), ); let hide = inputs::labeled_checkbox( &t(locale, "settings.hideUnchangedRegions"), state.hide_unchanged_regions, |b| Message::Settings(SettingsMsg::HideUnchangedRegionsChanged(b)), ); let save = button(text(t(locale, "common.save")).size(13)) .on_press(Message::Settings(SettingsMsg::SaveEditor)) .style(inputs::primary_button) .padding([6, 16]); column![wrap, hide, save] .spacing(8) .padding([0, 16]) .into() } fn section_content<'a>(locale: UiLocale) -> Element<'a, Message> { // Categories table placeholder — full implementation in future iteration text(t(locale, "settings.contentPlaceholder")) .size(13) .color(Color::from_rgb(0.5, 0.5, 0.5)) .into() } fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> { let offline = inputs::labeled_checkbox( &t(locale, "settings.offlineMode"), state.offline_mode, |b| Message::Settings(SettingsMsg::OfflineModeChanged(b)), ); let prompt = inputs::labeled_input( &t(locale, "settings.systemPrompt"), "", &state.system_prompt, |s| Message::Settings(SettingsMsg::SystemPromptChanged(s)), ); let btns = row![ button(text(t(locale, "common.save")).size(13)) .on_press(Message::Settings(SettingsMsg::SaveSystemPrompt)) .style(inputs::primary_button) .padding([6, 16]), button(text(t(locale, "settings.resetToDefault")).size(13)) .on_press(Message::Settings(SettingsMsg::ResetSystemPrompt)) .padding([6, 16]), ] .spacing(8); column![offline, prompt, btns] .spacing(8) .padding([0, 16]) .into() } fn section_technology<'a>(locale: UiLocale) -> Element<'a, Message> { text(t(locale, "settings.technologyPlaceholder")) .size(13) .color(Color::from_rgb(0.5, 0.5, 0.5)) .into() } fn section_publishing<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> { let host = inputs::labeled_input( &t(locale, "settings.sshHost"), "", &state.ssh_host, |s| Message::Settings(SettingsMsg::SshHostChanged(s)), ); let user = inputs::labeled_input( &t(locale, "settings.sshUsername"), "", &state.ssh_username, |s| Message::Settings(SettingsMsg::SshUsernameChanged(s)), ); let path = inputs::labeled_input( &t(locale, "settings.sshRemotePath"), "", &state.ssh_remote_path, |s| Message::Settings(SettingsMsg::SshRemotePathChanged(s)), ); let btns = row![ button(text(t(locale, "common.save")).size(13)) .on_press(Message::Settings(SettingsMsg::SavePublishing)) .style(inputs::primary_button) .padding([6, 16]), button(text(t(locale, "settings.clear")).size(13)) .on_press(Message::Settings(SettingsMsg::ClearPublishing)) .style(inputs::danger_button) .padding([6, 16]), ] .spacing(8); column![host, user, path, btns] .spacing(8) .padding([0, 16]) .into() } fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> { let rebuild_btns = column![ button(text(t(locale, "settings.rebuildPosts")).size(13)) .on_press(Message::Settings(SettingsMsg::RebuildPosts)) .padding([6, 16]) .width(Length::Fill), button(text(t(locale, "settings.rebuildMedia")).size(13)) .on_press(Message::Settings(SettingsMsg::RebuildMedia)) .padding([6, 16]) .width(Length::Fill), button(text(t(locale, "settings.rebuildScripts")).size(13)) .on_press(Message::Settings(SettingsMsg::RebuildScripts)) .padding([6, 16]) .width(Length::Fill), button(text(t(locale, "settings.rebuildTemplates")).size(13)) .on_press(Message::Settings(SettingsMsg::RebuildTemplates)) .padding([6, 16]) .width(Length::Fill), button(text(t(locale, "settings.rebuildLinks")).size(13)) .on_press(Message::Settings(SettingsMsg::RebuildLinks)) .padding([6, 16]) .width(Length::Fill), button(text(t(locale, "settings.regenerateThumbnails")).size(13)) .on_press(Message::Settings(SettingsMsg::RegenerateThumbnails)) .padding([6, 16]) .width(Length::Fill), ] .spacing(4); let open = button(text(t(locale, "settings.openDataFolder")).size(13)) .on_press(Message::Settings(SettingsMsg::OpenDataFolder)) .padding([6, 16]); column![rebuild_btns, open] .spacing(12) .padding([0, 16]) .into() } fn section_mcp<'a>(locale: UiLocale) -> Element<'a, Message> { // MCP status and agent toggles — placeholder for M3 text(t(locale, "settings.mcpPlaceholder")) .size(13) .color(Color::from_rgb(0.5, 0.5, 0.5)) .into() }