fix: another round of firxes for M3
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Column, Space};
|
||||
use iced::widget::text::{Shaping, Wrapping};
|
||||
use iced::{Color, Element, Length, Theme};
|
||||
|
||||
@@ -32,6 +32,13 @@ pub struct TranslationDraft {
|
||||
pub is_dirty: bool,
|
||||
}
|
||||
|
||||
/// Resolved post link for display in metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedPostLink {
|
||||
pub post_id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// State for an open post editor.
|
||||
pub struct PostEditorState {
|
||||
pub post_id: String,
|
||||
@@ -65,6 +72,10 @@ pub struct PostEditorState {
|
||||
pub saved_canonical: Option<TranslationDraft>,
|
||||
/// Translation drafts keyed by language code.
|
||||
pub translation_drafts: HashMap<String, TranslationDraft>,
|
||||
/// Outgoing links from this post to other posts.
|
||||
pub outlinks: Vec<ResolvedPostLink>,
|
||||
/// Incoming links from other posts to this post.
|
||||
pub backlinks: Vec<ResolvedPostLink>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostEditorState {
|
||||
@@ -105,6 +116,8 @@ impl Clone for PostEditorState {
|
||||
blog_languages: self.blog_languages.clone(),
|
||||
saved_canonical: self.saved_canonical.clone(),
|
||||
translation_drafts: self.translation_drafts.clone(),
|
||||
outlinks: self.outlinks.clone(),
|
||||
backlinks: self.backlinks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +127,8 @@ impl PostEditorState {
|
||||
post: &Post,
|
||||
blog_languages: &[String],
|
||||
translations: &[PostTranslation],
|
||||
outlinks: Vec<ResolvedPostLink>,
|
||||
backlinks: Vec<ResolvedPostLink>,
|
||||
) -> Self {
|
||||
let title = post.title.clone();
|
||||
let content = post.content.clone().unwrap_or_default();
|
||||
@@ -155,6 +170,8 @@ impl PostEditorState {
|
||||
blog_languages: blog_languages.to_vec(),
|
||||
saved_canonical: None,
|
||||
translation_drafts,
|
||||
outlinks,
|
||||
backlinks,
|
||||
title,
|
||||
}
|
||||
}
|
||||
@@ -218,13 +235,14 @@ impl PostEditorState {
|
||||
}
|
||||
|
||||
/// Build the translation flags list for the view.
|
||||
/// Flags are driven by the post's actual translations, not blog-level languages.
|
||||
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
|
||||
if self.blog_languages.len() <= 1 {
|
||||
if self.translation_drafts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut flags = Vec::new();
|
||||
|
||||
// Canonical language first
|
||||
// Canonical language first (always shown when translations exist)
|
||||
let canon = &self.canonical_language;
|
||||
let canon_locale = i18n::normalize_language(canon);
|
||||
flags.push(TranslationFlag {
|
||||
@@ -234,23 +252,20 @@ impl PostEditorState {
|
||||
is_active: self.active_language == *canon,
|
||||
});
|
||||
|
||||
// Each other blog language
|
||||
for lang in &self.blog_languages {
|
||||
if lang == canon {
|
||||
continue;
|
||||
}
|
||||
// Each existing translation for this post
|
||||
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
|
||||
langs.sort();
|
||||
for lang in langs {
|
||||
let locale = i18n::normalize_language(lang);
|
||||
let status = self.translation_drafts.get(lang)
|
||||
.map(|d| match d.status {
|
||||
PostStatus::Published => "published",
|
||||
_ => "draft",
|
||||
})
|
||||
.unwrap_or("missing");
|
||||
let status = match self.translation_drafts[lang].status {
|
||||
PostStatus::Published => "published",
|
||||
_ => "draft",
|
||||
};
|
||||
flags.push(TranslationFlag {
|
||||
language: lang.clone(),
|
||||
flag_emoji: locale.flag_emoji().to_string(),
|
||||
status: status.to_string(),
|
||||
is_active: self.active_language == *lang,
|
||||
is_active: self.active_language == **lang,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -427,7 +442,44 @@ pub fn view<'a>(
|
||||
|cat| Message::PostEditor(PostEditorMsg::RemoveCategory(cat)),
|
||||
);
|
||||
|
||||
column![meta_row1, meta_row2, tags_section, categories_section]
|
||||
// Post links sections
|
||||
let outlinks_section: Element<'a, Message> = if state.outlinks.is_empty() {
|
||||
Space::new(0, 0).into()
|
||||
} else {
|
||||
let mut items: Vec<Element<'a, Message>> = vec![
|
||||
text(t(locale, "editor.outlinks")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced).into(),
|
||||
];
|
||||
for link in &state.outlinks {
|
||||
items.push(
|
||||
text(format!("\u{2192} {}", link.title))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.55, 0.70, 0.90))
|
||||
.into()
|
||||
);
|
||||
}
|
||||
Column::with_children(items).spacing(2).into()
|
||||
};
|
||||
|
||||
let backlinks_section: Element<'a, Message> = if state.backlinks.is_empty() {
|
||||
Space::new(0, 0).into()
|
||||
} else {
|
||||
let mut items: Vec<Element<'a, Message>> = vec![
|
||||
text(t(locale, "editor.backlinks")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced).into(),
|
||||
];
|
||||
for link in &state.backlinks {
|
||||
items.push(
|
||||
text(format!("\u{2190} {}", link.title))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.55, 0.70, 0.90))
|
||||
.into()
|
||||
);
|
||||
}
|
||||
Column::with_children(items).spacing(2).into()
|
||||
};
|
||||
|
||||
column![meta_row1, meta_row2, tags_section, categories_section, outlinks_section, backlinks_section]
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct ScriptEditorState {
|
||||
pub script_id: String,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub file_path: String,
|
||||
pub kind: ScriptKind,
|
||||
pub entrypoint: String,
|
||||
pub enabled: bool,
|
||||
@@ -45,6 +46,7 @@ impl Clone for ScriptEditorState {
|
||||
script_id: self.script_id.clone(),
|
||||
title: self.title.clone(),
|
||||
slug: self.slug.clone(),
|
||||
file_path: self.file_path.clone(),
|
||||
kind: self.kind.clone(),
|
||||
entrypoint: self.entrypoint.clone(),
|
||||
enabled: self.enabled,
|
||||
@@ -69,6 +71,7 @@ impl ScriptEditorState {
|
||||
script_id: script.id.clone(),
|
||||
title: script.title.clone(),
|
||||
slug: script.slug.clone(),
|
||||
file_path: script.file_path.clone(),
|
||||
kind: script.kind.clone(),
|
||||
entrypoint: script.entrypoint.clone(),
|
||||
enabled: script.enabled,
|
||||
@@ -192,14 +195,15 @@ pub fn view<'a>(
|
||||
.spacing(16)
|
||||
.width(Length::Fill);
|
||||
|
||||
// Content editor (CodeEditor with Lua syntax highlighting)
|
||||
// Content editor (CodeEditor with syntax highlighting based on file extension)
|
||||
let syntax_ext = if state.file_path.ends_with(".py") { "py" } else { "lua" };
|
||||
let content_section: Element<'a, Message> = column![
|
||||
inputs::section_header(&t(locale, "editor.content")),
|
||||
Element::from(
|
||||
CodeEditor::new(
|
||||
&state.editor_buffer,
|
||||
highlighter(),
|
||||
"lua",
|
||||
syntax_ext,
|
||||
)
|
||||
.on_change(|msg| match msg {
|
||||
EditorMessage::ContentChanged(s) => Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)),
|
||||
@@ -209,6 +213,7 @@ pub fn view<'a>(
|
||||
]
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into();
|
||||
|
||||
// Validation error
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input};
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Alignment, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
@@ -49,13 +50,12 @@ impl SettingsSection {
|
||||
}
|
||||
|
||||
/// 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 project_description: text_editor::Content,
|
||||
pub data_path: String,
|
||||
pub public_url: String,
|
||||
pub default_author: String,
|
||||
@@ -72,7 +72,40 @@ pub struct SettingsViewState {
|
||||
pub ssh_remote_path: String,
|
||||
// AI
|
||||
pub offline_mode: bool,
|
||||
pub system_prompt: String,
|
||||
pub system_prompt: text_editor::Content,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SettingsViewState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SettingsViewState")
|
||||
.field("project_name", &self.project_name)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SettingsViewState {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
search_query: self.search_query.clone(),
|
||||
collapsed: self.collapsed.clone(),
|
||||
project_name: self.project_name.clone(),
|
||||
project_description: text_editor::Content::with_text(&self.project_description.text()),
|
||||
data_path: self.data_path.clone(),
|
||||
public_url: self.public_url.clone(),
|
||||
default_author: self.default_author.clone(),
|
||||
max_posts_per_page: self.max_posts_per_page.clone(),
|
||||
default_mode: self.default_mode.clone(),
|
||||
diff_view_style: self.diff_view_style.clone(),
|
||||
wrap_long_lines: self.wrap_long_lines,
|
||||
hide_unchanged_regions: self.hide_unchanged_regions,
|
||||
ssh_mode: self.ssh_mode.clone(),
|
||||
ssh_host: self.ssh_host.clone(),
|
||||
ssh_username: self.ssh_username.clone(),
|
||||
ssh_remote_path: self.ssh_remote_path.clone(),
|
||||
offline_mode: self.offline_mode,
|
||||
system_prompt: text_editor::Content::with_text(&self.system_prompt.text()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SettingsViewState {
|
||||
@@ -81,7 +114,7 @@ impl Default for SettingsViewState {
|
||||
search_query: String::new(),
|
||||
collapsed: Vec::new(),
|
||||
project_name: String::new(),
|
||||
project_description: String::new(),
|
||||
project_description: text_editor::Content::new(),
|
||||
data_path: String::new(),
|
||||
public_url: String::new(),
|
||||
default_author: String::new(),
|
||||
@@ -95,7 +128,7 @@ impl Default for SettingsViewState {
|
||||
ssh_username: String::new(),
|
||||
ssh_remote_path: String::new(),
|
||||
offline_mode: false,
|
||||
system_prompt: String::new(),
|
||||
system_prompt: text_editor::Content::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +140,7 @@ pub enum SettingsMsg {
|
||||
ToggleSection(SettingsSection),
|
||||
// Project
|
||||
ProjectNameChanged(String),
|
||||
ProjectDescriptionChanged(String),
|
||||
ProjectDescriptionAction(text_editor::Action),
|
||||
DataPathChanged(String),
|
||||
BrowseDataPath,
|
||||
ResetDataPath,
|
||||
@@ -130,7 +163,7 @@ pub enum SettingsMsg {
|
||||
ClearPublishing,
|
||||
// AI
|
||||
OfflineModeChanged(bool),
|
||||
SystemPromptChanged(String),
|
||||
SystemPromptAction(text_editor::Action),
|
||||
SaveSystemPrompt,
|
||||
ResetSystemPrompt,
|
||||
// Data maintenance
|
||||
@@ -228,12 +261,14 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
||||
&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 desc = column![
|
||||
text(t(locale, "settings.projectDescription")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced),
|
||||
text_editor(&state.project_description)
|
||||
.on_action(|a| Message::Settings(SettingsMsg::ProjectDescriptionAction(a)))
|
||||
.height(Length::Fixed(80.0))
|
||||
.size(14),
|
||||
]
|
||||
.spacing(4);
|
||||
let data_path = row![
|
||||
inputs::labeled_input(
|
||||
&t(locale, "settings.dataPath"),
|
||||
@@ -316,12 +351,14 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
|
||||
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 prompt = column![
|
||||
text(t(locale, "settings.systemPrompt")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced),
|
||||
text_editor(&state.system_prompt)
|
||||
.on_action(|a| Message::Settings(SettingsMsg::SystemPromptAction(a)))
|
||||
.height(Length::Fixed(200.0))
|
||||
.size(14),
|
||||
]
|
||||
.spacing(4);
|
||||
let btns = row![
|
||||
button(text(t(locale, "common.save")).size(13))
|
||||
.on_press(Message::Settings(SettingsMsg::SaveSystemPrompt))
|
||||
|
||||
@@ -187,6 +187,7 @@ pub fn view<'a>(
|
||||
]
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into();
|
||||
|
||||
// Validation error
|
||||
|
||||
Reference in New Issue
Block a user