432 lines
14 KiB
Rust
432 lines
14 KiB
Rust
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()
|
|
}
|