fix: more fixes on M3

This commit is contained in:
2026-04-05 17:09:40 +02:00
parent b2f7643dce
commit cf20f19fc0
2 changed files with 240 additions and 8 deletions

View File

@@ -1,14 +1,35 @@
use std::collections::HashMap;
use iced::widget::{button, column, container, row, scrollable, text, text_input, text_editor, Space};
use iced::widget::text::{Shaping, Wrapping};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use bds_core::model::{Post, PostStatus};
use bds_core::i18n::{self, UiLocale};
use bds_core::model::{Post, PostStatus, PostTranslation};
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
/// Per editor_post.allium TranslationFlag value.
#[derive(Debug, Clone)]
pub struct TranslationFlag {
pub language: String,
pub flag_emoji: String,
pub status: String, // "draft" | "published" | "missing"
pub is_active: bool,
}
/// Saved draft content for a single translation language.
#[derive(Debug, Clone)]
pub struct TranslationDraft {
pub title: String,
pub excerpt: String,
pub content: String,
pub status: PostStatus,
pub is_dirty: bool,
}
/// State for an open post editor.
pub struct PostEditorState {
pub post_id: String,
@@ -31,6 +52,17 @@ pub struct PostEditorState {
pub excerpt_expanded: bool,
pub tags_input: String,
pub categories_input: String,
// ── Translation flags ──
/// Currently-displayed language (canonical or translation).
pub active_language: String,
/// The post's own language (canonical).
pub canonical_language: String,
/// All blog languages from project metadata.
pub blog_languages: Vec<String>,
/// Saved canonical title/excerpt/content when viewing a translation.
pub saved_canonical: Option<TranslationDraft>,
/// Translation drafts keyed by language code.
pub translation_drafts: HashMap<String, TranslationDraft>,
}
impl std::fmt::Debug for PostEditorState {
@@ -38,6 +70,7 @@ impl std::fmt::Debug for PostEditorState {
f.debug_struct("PostEditorState")
.field("post_id", &self.post_id)
.field("title", &self.title)
.field("active_language", &self.active_language)
.finish_non_exhaustive()
}
}
@@ -65,14 +98,36 @@ impl Clone for PostEditorState {
excerpt_expanded: self.excerpt_expanded,
tags_input: self.tags_input.clone(),
categories_input: self.categories_input.clone(),
active_language: self.active_language.clone(),
canonical_language: self.canonical_language.clone(),
blog_languages: self.blog_languages.clone(),
saved_canonical: self.saved_canonical.clone(),
translation_drafts: self.translation_drafts.clone(),
}
}
}
impl PostEditorState {
pub fn from_post(post: &Post) -> Self {
pub fn from_post(
post: &Post,
blog_languages: &[String],
translations: &[PostTranslation],
) -> Self {
let title = post.title.clone();
let content = post.content.clone().unwrap_or_default();
let canonical_lang = post.language.clone().unwrap_or_else(|| "en".to_string());
let mut translation_drafts = HashMap::new();
for tr in translations {
translation_drafts.insert(tr.language.clone(), TranslationDraft {
title: tr.title.clone(),
excerpt: tr.excerpt.clone().unwrap_or_default(),
content: tr.content.clone().unwrap_or_default(),
status: tr.status.clone(),
is_dirty: false,
});
}
Self {
post_id: post.id.clone(),
slug: post.slug.clone(),
@@ -82,7 +137,7 @@ impl PostEditorState {
tags: post.tags.clone(),
categories: post.categories.clone(),
author: post.author.clone().unwrap_or_default(),
language: post.language.clone().unwrap_or_default(),
language: canonical_lang.clone(),
template_slug: post.template_slug.clone().unwrap_or_default(),
do_not_translate: post.do_not_translate,
status: post.status.clone(),
@@ -93,9 +148,112 @@ impl PostEditorState {
excerpt_expanded: false,
tags_input: String::new(),
categories_input: String::new(),
active_language: canonical_lang.clone(),
canonical_language: canonical_lang,
blog_languages: blog_languages.to_vec(),
saved_canonical: None,
translation_drafts,
title,
}
}
/// Switch the editor to display a different language.
/// Saves current fields and loads the target language's draft.
pub fn switch_language(&mut self, target_lang: &str) {
if target_lang == self.active_language {
return;
}
// Save current fields
if self.active_language == self.canonical_language {
// Switching away from canonical — stash canonical fields
self.saved_canonical = Some(TranslationDraft {
title: self.title.clone(),
excerpt: self.excerpt.clone(),
content: self.content.clone(),
status: self.status.clone(),
is_dirty: self.is_dirty,
});
} else {
// Switching away from a translation — save to drafts
self.translation_drafts.insert(self.active_language.clone(), TranslationDraft {
title: self.title.clone(),
excerpt: self.excerpt.clone(),
content: self.content.clone(),
status: PostStatus::Draft,
is_dirty: self.is_dirty,
});
}
// Load target fields
if target_lang == self.canonical_language {
// Restore canonical
if let Some(saved) = self.saved_canonical.take() {
self.title = saved.title;
self.excerpt = saved.excerpt;
self.content = saved.content.clone();
self.editor_content = text_editor::Content::with_text(&saved.content);
self.status = saved.status;
self.is_dirty = saved.is_dirty;
}
} else if let Some(draft) = self.translation_drafts.get(target_lang) {
// Load existing translation
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.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.is_dirty = false;
}
self.active_language = target_lang.to_string();
}
/// Build the translation flags list for the view.
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
if self.blog_languages.len() <= 1 {
return Vec::new();
}
let mut flags = Vec::new();
// Canonical language first
let canon = &self.canonical_language;
let canon_locale = i18n::normalize_language(canon);
flags.push(TranslationFlag {
language: canon.clone(),
flag_emoji: canon_locale.flag_emoji().to_string(),
status: "canonical".to_string(),
is_active: self.active_language == *canon,
});
// Each other blog language
for lang in &self.blog_languages {
if lang == canon {
continue;
}
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");
flags.push(TranslationFlag {
language: lang.clone(),
flag_emoji: locale.flag_emoji().to_string(),
status: status.to_string(),
is_active: self.active_language == *lang,
});
}
flags
}
}
/// Post editor messages.
@@ -110,6 +268,7 @@ pub enum PostEditorMsg {
ToggleDoNotTranslate(bool),
ToggleMetadata,
ToggleExcerpt,
SwitchLanguage(String),
TagsInputChanged(String),
TagsInputSubmit,
RemoveTag(String),
@@ -182,7 +341,31 @@ pub fn view<'a>(
..button::Style::default()
});
let metadata_section: Element<'a, Message> = if state.metadata_expanded {
// ── Translation Flags Bar (inline with metadata toggle) ──
let flags = state.translation_flags();
let on_translation = state.active_language != state.canonical_language;
let meta_toggle_row: Element<'a, Message> = if flags.is_empty() {
meta_toggle.into()
} else {
let mut flag_row = row![].spacing(2);
for flag in &flags {
let lang = flag.language.clone();
let label = format!("{}", flag.flag_emoji);
let btn = button(
text(label).size(14).shaping(Shaping::Advanced),
)
.on_press(Message::PostEditor(PostEditorMsg::SwitchLanguage(lang)))
.padding([2, 4])
.style(if flag.is_active { flag_active_style } else { flag_inactive_style });
flag_row = flag_row.push(btn);
}
row![meta_toggle, Space::with_width(Length::Fill), flag_row]
.spacing(8)
.align_y(iced::Alignment::Center)
.into()
};
let metadata_section: Element<'a, Message> = if state.metadata_expanded && !on_translation {
let title_input = inputs::labeled_input(
&t(locale, "editor.title"),
&t(locale, "editor.titlePlaceholder"),
@@ -298,7 +481,7 @@ pub fn view<'a>(
let top_pane = scrollable(
column![
header,
meta_toggle,
meta_toggle_row,
metadata_section,
excerpt_toggle,
excerpt_section,
@@ -426,3 +609,34 @@ fn status_badge<'a>(status: &PostStatus) -> Element<'a, Message> {
})
.into()
}
/// Active translation flag button style (highlighted).
fn flag_active_style(_theme: &Theme, _status: button::Status) -> button::Style {
button::Style {
background: Some(iced::Background::Color(Color::from_rgb(0.20, 0.35, 0.60))),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
width: 1.0,
color: Color::from_rgb(0.30, 0.50, 0.80),
},
..button::Style::default()
}
}
/// Inactive translation flag button style.
fn flag_inactive_style(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.22, 0.24, 0.30),
_ => Color::TRANSPARENT,
};
button::Style {
background: Some(iced::Background::Color(bg)),
text_color: Color::from_rgb(0.70, 0.72, 0.78),
border: iced::Border {
radius: 4.0.into(),
..iced::Border::default()
},
..button::Style::default()
}
}