feat: first take at milestone M3
This commit is contained in:
431
crates/bds-ui/src/views/settings_view.rs
Normal file
431
crates/bds-ui/src/views/settings_view.rs
Normal file
@@ -0,0 +1,431 @@
|
||||
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<SettingsSection>,
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user