fix another round of fixes of M3

This commit is contained in:
2026-04-05 18:06:57 +02:00
parent cf20f19fc0
commit 95cacdcd0e
8 changed files with 318 additions and 172 deletions

View File

@@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use iced::widget::text_editor;
use iced::{Element, Subscription, Task};
use bds_core::db::Database;
@@ -464,8 +463,12 @@ impl BdsApp {
}
// Extract content language from project metadata
if let Ok(meta) = engine::meta::read_project_json(&data_dir) {
self.content_language = meta.main_language.unwrap_or_else(|| "en".to_string());
let main_lang = meta.main_language.unwrap_or_else(|| "en".to_string());
self.content_language = main_lang.clone();
self.blog_languages = meta.blog_languages;
if !self.blog_languages.contains(&main_lang) {
self.blog_languages.insert(0, main_lang);
}
}
}
self.refresh_counts();
@@ -487,8 +490,12 @@ impl BdsApp {
if let Some(data_dir) = self.data_dir.clone() {
let _ = engine::meta::startup_sync(&data_dir);
if let Ok(meta) = engine::meta::read_project_json(&data_dir) {
self.content_language = meta.main_language.unwrap_or_else(|| "en".to_string());
let main_lang = meta.main_language.unwrap_or_else(|| "en".to_string());
self.content_language = main_lang.clone();
self.blog_languages = meta.blog_languages;
if !self.blog_languages.contains(&main_lang) {
self.blog_languages.insert(0, main_lang);
}
}
}
let name = self.active_project.as_ref().map(|p| p.name.clone()).unwrap_or_default();
@@ -928,13 +935,9 @@ impl BdsApp {
PostEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; }
PostEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; }
PostEditorMsg::ExcerptChanged(s) => { state.excerpt = s; state.is_dirty = true; }
PostEditorMsg::ContentAction(action) => {
let is_edit = matches!(action, text_editor::Action::Edit(_));
state.editor_content.perform(action);
if is_edit {
state.content = state.editor_content.text();
state.is_dirty = true;
}
PostEditorMsg::ContentChanged(new_text) => {
state.content = new_text;
state.is_dirty = true;
}
PostEditorMsg::AuthorChanged(s) => { state.author = s; state.is_dirty = true; }
PostEditorMsg::TemplateSlugChanged(s) => { state.template_slug = s; state.is_dirty = true; }
@@ -1026,7 +1029,10 @@ impl BdsApp {
TemplateEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; }
TemplateEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; }
TemplateEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; }
TemplateEditorMsg::ContentChanged(s) => { state.content = s; state.is_dirty = true; }
TemplateEditorMsg::ContentChanged(new_text) => {
state.content = new_text;
state.is_dirty = true;
}
TemplateEditorMsg::Save => {
return self.save_template_editor(&tab_id);
}
@@ -1065,9 +1071,9 @@ impl BdsApp {
ScriptEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; }
ScriptEditorMsg::EntrypointChanged(s) => { state.entrypoint = s; state.is_dirty = true; }
ScriptEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; }
ScriptEditorMsg::ContentChanged(s) => {
state.discovered_entrypoints = engine::script::discover_entrypoints(&s);
state.content = s;
ScriptEditorMsg::ContentChanged(new_text) => {
state.discovered_entrypoints = engine::script::discover_entrypoints(&new_text);
state.content = new_text;
state.is_dirty = true;
}
ScriptEditorMsg::Save => {
@@ -2065,7 +2071,17 @@ impl BdsApp {
TabType::Templates => {
if !self.template_editors.contains_key(&tab.id) {
match bds_core::db::queries::template::get_template_by_id(db.conn(), &tab.id) {
Ok(template) => {
Ok(mut template) => {
// Published templates: read content from file
if template.content.is_none() {
if let Some(ref data_dir) = self.data_dir {
let rel = bds_core::util::paths::template_file_path(&template.slug);
let path = data_dir.join(&rel);
if let Ok(body) = std::fs::read_to_string(&path) {
template.content = Some(body);
}
}
}
self.template_editors.insert(template.id.clone(), TemplateEditorState::from_template(&template));
}
Err(e) => {
@@ -2077,7 +2093,17 @@ impl BdsApp {
TabType::Scripts => {
if !self.script_editors.contains_key(&tab.id) {
match bds_core::db::queries::script::get_script_by_id(db.conn(), &tab.id) {
Ok(script) => {
Ok(mut script) => {
// Published scripts: read content from file
if script.content.is_none() {
if let Some(ref data_dir) = self.data_dir {
let rel = bds_core::util::paths::script_file_path(&script.slug);
let path = data_dir.join(&rel);
if let Ok(body) = std::fs::read_to_string(&path) {
script.content = Some(body);
}
}
}
self.script_editors.insert(script.id.clone(), ScriptEditorState::from_script(&script));
}
Err(e) => {
@@ -2096,7 +2122,29 @@ impl BdsApp {
}
}
TabType::Settings => {
// Settings state will be initialized lazily
if self.settings_state.is_none() {
let mut state = SettingsViewState::default();
if let Some(ref project) = self.active_project {
state.project_name = project.name.clone();
state.project_description = project.description.clone().unwrap_or_default();
state.data_path = project.data_path.clone().unwrap_or_default();
}
if let Some(ref data_dir) = self.data_dir {
if let Ok(meta) = engine::meta::read_project_json(data_dir) {
state.public_url = meta.public_url.unwrap_or_default();
state.default_author = meta.default_author.unwrap_or_default();
state.max_posts_per_page = meta.max_posts_per_page.to_string();
}
if let Ok(pub_prefs) = engine::meta::read_publishing_json(data_dir) {
state.ssh_host = pub_prefs.ssh_host.unwrap_or_default();
state.ssh_username = pub_prefs.ssh_user.unwrap_or_default();
state.ssh_remote_path = pub_prefs.ssh_remote_path.unwrap_or_default();
state.ssh_mode = format!("{:?}", pub_prefs.ssh_mode).to_lowercase();
}
}
state.offline_mode = self.offline_mode;
self.settings_state = Some(state);
}
}
_ => {}
}

View File

@@ -1,11 +1,13 @@
use std::cell::RefCell;
use std::collections::HashMap;
use iced::widget::{button, column, container, row, scrollable, text, text_input, text_editor, Space};
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::widget::text::{Shaping, Wrapping};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::{self, UiLocale};
use bds_core::model::{Post, PostStatus, PostTranslation};
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
use crate::app::Message;
use crate::components::inputs;
@@ -37,7 +39,7 @@ pub struct PostEditorState {
pub slug: String,
pub excerpt: String,
pub content: String,
pub editor_content: text_editor::Content,
pub editor_buffer: RefCell<EditorBuffer>,
pub tags: Vec<String>,
pub categories: Vec<String>,
pub author: String,
@@ -83,7 +85,7 @@ impl Clone for PostEditorState {
slug: self.slug.clone(),
excerpt: self.excerpt.clone(),
content: self.content.clone(),
editor_content: text_editor::Content::with_text(&self.content),
editor_buffer: RefCell::new(EditorBuffer::new(&self.content)),
tags: self.tags.clone(),
categories: self.categories.clone(),
author: self.author.clone(),
@@ -133,7 +135,7 @@ impl PostEditorState {
slug: post.slug.clone(),
excerpt: post.excerpt.clone().unwrap_or_default(),
content: content.clone(),
editor_content: text_editor::Content::with_text(&content),
editor_buffer: RefCell::new(EditorBuffer::new(&content)),
tags: post.tags.clone(),
categories: post.categories.clone(),
author: post.author.clone().unwrap_or_default(),
@@ -192,7 +194,7 @@ impl PostEditorState {
self.title = saved.title;
self.excerpt = saved.excerpt;
self.content = saved.content.clone();
self.editor_content = text_editor::Content::with_text(&saved.content);
self.editor_buffer = RefCell::new(EditorBuffer::new(&saved.content));
self.status = saved.status;
self.is_dirty = saved.is_dirty;
}
@@ -201,14 +203,14 @@ impl PostEditorState {
self.title = draft.title.clone();
self.excerpt = draft.excerpt.clone();
self.content = draft.content.clone();
self.editor_content = text_editor::Content::with_text(&draft.content);
self.editor_buffer = RefCell::new(EditorBuffer::new(&draft.content));
self.is_dirty = draft.is_dirty;
} else {
// No translation yet — blank fields
self.title = String::new();
self.excerpt = String::new();
self.content = String::new();
self.editor_content = text_editor::Content::with_text("");
self.editor_buffer = RefCell::new(EditorBuffer::new(""));
self.is_dirty = false;
}
@@ -262,7 +264,7 @@ pub enum PostEditorMsg {
TitleChanged(String),
SlugChanged(String),
ExcerptChanged(String),
ContentAction(text_editor::Action),
ContentChanged(String),
AuthorChanged(String),
TemplateSlugChanged(String),
ToggleDoNotTranslate(bool),
@@ -461,13 +463,17 @@ pub fn view<'a>(
};
// ── Content section (fills remaining space) ──
let content_placeholder = t(locale, "editor.contentPlaceholder");
let content_label = inputs::section_header(&t(locale, "editor.content"));
let editor_widget = text_editor(&state.editor_content)
.placeholder(content_placeholder)
.on_action(|action| Message::PostEditor(PostEditorMsg::ContentAction(action)))
.height(Length::Fill)
.style(editor_style);
let editor_widget: Element<'a, Message> = CodeEditor::new(
&state.editor_buffer,
highlighter(),
"md",
)
.on_change(|msg| match msg {
EditorMessage::ContentChanged(s) => Message::PostEditor(PostEditorMsg::ContentChanged(s)),
EditorMessage::SaveRequested => Message::PostEditor(PostEditorMsg::Save),
})
.into();
// ── Footer ──
let footer = row![
@@ -505,28 +511,6 @@ pub fn view<'a>(
.into()
}
/// Dark editor style for the text_editor widget.
fn editor_style(_theme: &Theme, status: text_editor::Status) -> text_editor::Style {
let bg = Color::from_rgb(0.10, 0.11, 0.14);
let border_color = match status {
text_editor::Status::Focused => Color::from_rgb(0.30, 0.50, 0.80),
text_editor::Status::Hovered => Color::from_rgb(0.30, 0.32, 0.40),
_ => Color::from_rgb(0.22, 0.24, 0.30),
};
text_editor::Style {
background: iced::Background::Color(bg),
border: iced::Border {
radius: 4.0.into(),
width: 1.0,
color: border_color,
},
icon: Color::from_rgb(0.50, 0.52, 0.58),
placeholder: Color::from_rgb(0.40, 0.42, 0.48),
value: Color::from_rgb(0.85, 0.87, 0.92),
selection: Color::from_rgba(0.30, 0.50, 0.80, 0.40),
}
}
/// Chip input: shows existing chips as removable buttons + a text input to add new ones.
fn chip_input_field<'a>(
label: &str,

View File

@@ -1,15 +1,17 @@
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use std::cell::RefCell;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use bds_core::model::{Script, ScriptKind, ScriptStatus};
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
/// State for an open script editor.
#[derive(Debug, Clone)]
pub struct ScriptEditorState {
pub script_id: String,
pub title: String,
@@ -18,6 +20,7 @@ pub struct ScriptEditorState {
pub entrypoint: String,
pub enabled: bool,
pub content: String,
pub editor_buffer: RefCell<EditorBuffer>,
pub status: ScriptStatus,
pub version: i32,
pub created_at: i64,
@@ -27,6 +30,37 @@ pub struct ScriptEditorState {
pub is_dirty: bool,
}
impl std::fmt::Debug for ScriptEditorState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScriptEditorState")
.field("script_id", &self.script_id)
.field("title", &self.title)
.finish_non_exhaustive()
}
}
impl Clone for ScriptEditorState {
fn clone(&self) -> Self {
Self {
script_id: self.script_id.clone(),
title: self.title.clone(),
slug: self.slug.clone(),
kind: self.kind.clone(),
entrypoint: self.entrypoint.clone(),
enabled: self.enabled,
content: self.content.clone(),
editor_buffer: RefCell::new(EditorBuffer::new(&self.content)),
status: self.status.clone(),
version: self.version,
created_at: self.created_at,
updated_at: self.updated_at,
discovered_entrypoints: self.discovered_entrypoints.clone(),
validation_error: self.validation_error.clone(),
is_dirty: self.is_dirty,
}
}
}
impl ScriptEditorState {
pub fn from_script(script: &Script) -> Self {
let content = script.content.clone().unwrap_or_default();
@@ -38,7 +72,8 @@ impl ScriptEditorState {
kind: script.kind.clone(),
entrypoint: script.entrypoint.clone(),
enabled: script.enabled,
content,
content: content.clone(),
editor_buffer: RefCell::new(EditorBuffer::new(&content)),
status: script.status.clone(),
version: script.version,
created_at: script.created_at,
@@ -157,16 +192,20 @@ pub fn view<'a>(
.spacing(16)
.width(Length::Fill);
// Content editor (placeholder — will use CodeEditor with Lua syntax)
// Content editor (CodeEditor with Lua syntax highlighting)
let content_section: Element<'a, Message> = column![
inputs::section_header(&t(locale, "editor.content")),
container(
text_input("", &state.content)
.on_input(|s| Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)))
.size(14)
)
.width(Length::Fill)
.height(Length::Fixed(300.0)),
Element::from(
CodeEditor::new(
&state.editor_buffer,
highlighter(),
"lua",
)
.on_change(|msg| match msg {
EditorMessage::ContentChanged(s) => Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)),
EditorMessage::SaveRequested => Message::ScriptEditor(ScriptEditorMsg::Save),
})
),
]
.spacing(8)
.width(Length::Fill)
@@ -193,24 +232,31 @@ pub fn view<'a>(
]
.padding(8);
let body = scrollable(
// Top pane: header + metadata (scrollable for overflow)
let top_pane = scrollable(
column![
header,
meta_row1,
meta_row2,
content_section,
validation,
footer,
]
.spacing(12)
.padding(16)
.width(Length::Fill)
);
)
.height(Length::Shrink);
container(body)
.width(Length::Fill)
.height(Length::Fill)
.into()
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
column![
top_pane,
content_section,
validation,
footer,
]
.spacing(4)
.padding([0, 16])
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn status_badge<'a>(status: &ScriptStatus) -> Element<'a, Message> {

View File

@@ -154,7 +154,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
})
.collect();
let cloud = iced::widget::Column::with_children(chips).spacing(6);
let cloud = row(chips).spacing(6).wrap();
scrollable(
container(cloud)

View File

@@ -1,15 +1,17 @@
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use std::cell::RefCell;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use bds_core::model::{Template, TemplateKind, TemplateStatus};
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
/// State for an open template editor.
#[derive(Debug, Clone)]
pub struct TemplateEditorState {
pub template_id: String,
pub title: String,
@@ -17,6 +19,7 @@ pub struct TemplateEditorState {
pub kind: TemplateKind,
pub enabled: bool,
pub content: String,
pub editor_buffer: RefCell<EditorBuffer>,
pub status: TemplateStatus,
pub version: i32,
pub created_at: i64,
@@ -25,15 +28,46 @@ pub struct TemplateEditorState {
pub is_dirty: bool,
}
impl std::fmt::Debug for TemplateEditorState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TemplateEditorState")
.field("template_id", &self.template_id)
.field("title", &self.title)
.finish_non_exhaustive()
}
}
impl Clone for TemplateEditorState {
fn clone(&self) -> Self {
Self {
template_id: self.template_id.clone(),
title: self.title.clone(),
slug: self.slug.clone(),
kind: self.kind.clone(),
enabled: self.enabled,
content: self.content.clone(),
editor_buffer: RefCell::new(EditorBuffer::new(&self.content)),
status: self.status.clone(),
version: self.version,
created_at: self.created_at,
updated_at: self.updated_at,
validation_error: self.validation_error.clone(),
is_dirty: self.is_dirty,
}
}
}
impl TemplateEditorState {
pub fn from_template(tpl: &Template) -> Self {
let content = tpl.content.clone().unwrap_or_default();
Self {
template_id: tpl.id.clone(),
title: tpl.title.clone(),
slug: tpl.slug.clone(),
kind: tpl.kind.clone(),
enabled: tpl.enabled,
content: tpl.content.clone().unwrap_or_default(),
content: content.clone(),
editor_buffer: RefCell::new(EditorBuffer::new(&content)),
status: tpl.status.clone(),
version: tpl.version,
created_at: tpl.created_at,
@@ -136,16 +170,20 @@ pub fn view<'a>(
);
let meta_row2 = row![kind_select, enabled_check].spacing(16).width(Length::Fill);
// Content editor (text area placeholder — will use CodeEditor widget for liquid)
// Content editor (CodeEditor with Liquid/HTML syntax highlighting)
let content_section: Element<'a, Message> = column![
inputs::section_header(&t(locale, "editor.content")),
container(
text_input("", &state.content)
.on_input(|s| Message::TemplateEditor(TemplateEditorMsg::ContentChanged(s)))
.size(14)
)
.width(Length::Fill)
.height(Length::Fixed(300.0)),
Element::from(
CodeEditor::new(
&state.editor_buffer,
highlighter(),
"liquid",
)
.on_change(|msg| match msg {
EditorMessage::ContentChanged(s) => Message::TemplateEditor(TemplateEditorMsg::ContentChanged(s)),
EditorMessage::SaveRequested => Message::TemplateEditor(TemplateEditorMsg::Save),
})
),
]
.spacing(8)
.width(Length::Fill)
@@ -172,24 +210,31 @@ pub fn view<'a>(
]
.padding(8);
let body = scrollable(
// Top pane: header + metadata (scrollable for overflow)
let top_pane = scrollable(
column![
header,
meta_row1,
meta_row2,
content_section,
validation,
footer,
]
.spacing(12)
.padding(16)
.width(Length::Fill)
);
)
.height(Length::Shrink);
container(body)
.width(Length::Fill)
.height(Length::Fill)
.into()
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
column![
top_pane,
content_section,
validation,
footer,
]
.spacing(4)
.padding([0, 16])
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn status_badge<'a>(status: &TemplateStatus) -> Element<'a, Message> {