fix: several fixes for the first M3 shot
This commit is contained in:
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -365,13 +366,13 @@ impl BdsApp {
|
||||
let old_view = self.sidebar_view;
|
||||
self.sidebar_view = new_view;
|
||||
self.sidebar_visible = new_visible;
|
||||
// When switching between Posts/Pages, re-query with correct filter
|
||||
let switched_post_scope = matches!(
|
||||
(old_view, new_view),
|
||||
(SidebarView::Posts, SidebarView::Pages)
|
||||
| (SidebarView::Pages, SidebarView::Posts)
|
||||
);
|
||||
if switched_post_scope {
|
||||
// When switching to/from Posts/Pages, re-query with correct filter
|
||||
let needs_post_refresh = old_view != new_view
|
||||
&& matches!(
|
||||
new_view,
|
||||
SidebarView::Posts | SidebarView::Pages
|
||||
);
|
||||
if needs_post_refresh {
|
||||
self.refresh_sidebar_posts();
|
||||
}
|
||||
Task::none()
|
||||
@@ -922,10 +923,45 @@ 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::ContentChanged(s) => { state.content = 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::AuthorChanged(s) => { state.author = s; state.is_dirty = true; }
|
||||
PostEditorMsg::TemplateSlugChanged(s) => { state.template_slug = s; state.is_dirty = true; }
|
||||
PostEditorMsg::ToggleDoNotTranslate(b) => { state.do_not_translate = b; state.is_dirty = true; }
|
||||
PostEditorMsg::ToggleMetadata => { state.metadata_expanded = !state.metadata_expanded; }
|
||||
PostEditorMsg::ToggleExcerpt => { state.excerpt_expanded = !state.excerpt_expanded; }
|
||||
PostEditorMsg::TagsInputChanged(s) => { state.tags_input = s; }
|
||||
PostEditorMsg::TagsInputSubmit => {
|
||||
let tag = state.tags_input.trim().to_string();
|
||||
if !tag.is_empty() && !state.tags.contains(&tag) {
|
||||
state.tags.push(tag);
|
||||
state.is_dirty = true;
|
||||
}
|
||||
state.tags_input.clear();
|
||||
}
|
||||
PostEditorMsg::RemoveTag(tag) => {
|
||||
state.tags.retain(|t| t != &tag);
|
||||
state.is_dirty = true;
|
||||
}
|
||||
PostEditorMsg::CategoriesInputChanged(s) => { state.categories_input = s; }
|
||||
PostEditorMsg::CategoriesInputSubmit => {
|
||||
let cat = state.categories_input.trim().to_string();
|
||||
if !cat.is_empty() && !state.categories.contains(&cat) {
|
||||
state.categories.push(cat);
|
||||
state.is_dirty = true;
|
||||
}
|
||||
state.categories_input.clear();
|
||||
}
|
||||
PostEditorMsg::RemoveCategory(cat) => {
|
||||
state.categories.retain(|c| c != &cat);
|
||||
state.is_dirty = true;
|
||||
}
|
||||
PostEditorMsg::Save => {
|
||||
return self.save_post_editor(&tab_id);
|
||||
}
|
||||
@@ -1970,7 +2006,24 @@ impl BdsApp {
|
||||
TabType::Post => {
|
||||
if !self.post_editors.contains_key(&tab.id) {
|
||||
match bds_core::db::queries::post::get_post_by_id(db.conn(), &tab.id) {
|
||||
Ok(post) => {
|
||||
Ok(mut post) => {
|
||||
// Published posts don't store body in DB — read from file
|
||||
if post.content.is_none() {
|
||||
if let Some(ref data_dir) = self.data_dir {
|
||||
let rel = bds_core::util::paths::post_file_path(
|
||||
post.created_at,
|
||||
&post.slug,
|
||||
);
|
||||
let path = data_dir.join(&rel);
|
||||
if let Ok(raw) = std::fs::read_to_string(&path) {
|
||||
if let Ok((_fm, body)) =
|
||||
bds_core::util::frontmatter::read_post_file(&raw)
|
||||
{
|
||||
post.content = Some(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.post_editors.insert(post.id.clone(), PostEditorState::from_post(&post));
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
||||
|
||||
/// Standard form field label color.
|
||||
const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75);
|
||||
pub const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75);
|
||||
const _FIELD_BG: Color = Color::from_rgb(0.15, 0.16, 0.20);
|
||||
const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58);
|
||||
pub const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58);
|
||||
const DANGER_BG: Color = Color::from_rgb(0.60, 0.15, 0.15);
|
||||
const DANGER_HOVER: Color = Color::from_rgb(0.70, 0.20, 0.20);
|
||||
const PRIMARY_BG: Color = Color::from_rgb(0.20, 0.40, 0.70);
|
||||
@@ -18,7 +19,7 @@ pub fn labeled_input<'a, Message: Clone + 'a>(
|
||||
on_change: impl Fn(String) -> Message + 'a,
|
||||
) -> Element<'a, Message> {
|
||||
column![
|
||||
text(label.to_string()).size(12).color(LABEL_COLOR),
|
||||
text(label.to_string()).size(12).color(LABEL_COLOR).shaping(Shaping::Advanced),
|
||||
text_input(placeholder, value).on_input(on_change).size(14),
|
||||
]
|
||||
.spacing(4)
|
||||
@@ -38,7 +39,7 @@ where
|
||||
{
|
||||
let list: Vec<T> = options.to_vec();
|
||||
column![
|
||||
text(label.to_string()).size(12).color(LABEL_COLOR),
|
||||
text(label.to_string()).size(12).color(LABEL_COLOR).shaping(Shaping::Advanced),
|
||||
pick_list(list, selected.cloned(), on_select),
|
||||
]
|
||||
.spacing(4)
|
||||
@@ -63,7 +64,8 @@ pub fn section_header<'a, Message: 'a>(label: &str) -> Element<'a, Message> {
|
||||
column![
|
||||
text(label.to_string())
|
||||
.size(11)
|
||||
.color(SECTION_COLOR),
|
||||
.color(SECTION_COLOR)
|
||||
.shaping(Shaping::Advanced),
|
||||
container(Space::new(0, 0))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(1.0))
|
||||
@@ -131,15 +133,21 @@ pub fn toolbar<'a, Message: 'a>(
|
||||
left: Vec<Element<'a, Message>>,
|
||||
right: Vec<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
let left_row = iced::widget::Row::with_children(left)
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center);
|
||||
let left_row = container(
|
||||
iced::widget::Row::with_children(left)
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.clip(true)
|
||||
.width(Length::Fill);
|
||||
let right_row = iced::widget::Row::with_children(right)
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center);
|
||||
.align_y(Alignment::Center)
|
||||
.wrap();
|
||||
|
||||
row![left_row, Space::with_width(Length::Fill), right_row]
|
||||
row![left_row, right_row]
|
||||
.padding(8)
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
@@ -149,8 +157,8 @@ pub fn toolbar<'a, Message: 'a>(
|
||||
pub fn date_label<'a, Message: 'a>(label: &str, timestamp_ms: i64) -> Element<'a, Message> {
|
||||
let date_str = format_timestamp(timestamp_ms);
|
||||
row![
|
||||
text(label.to_string()).size(12).color(LABEL_COLOR),
|
||||
text(date_str).size(12).color(Color::from_rgb(0.55, 0.58, 0.65)),
|
||||
text(label.to_string()).size(12).color(LABEL_COLOR).shaping(Shaping::Advanced),
|
||||
text(date_str).size(12).color(Color::from_rgb(0.55, 0.58, 0.65)).shaping(Shaping::Advanced),
|
||||
]
|
||||
.spacing(8)
|
||||
.into()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
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;
|
||||
@@ -9,13 +10,13 @@ use crate::components::inputs;
|
||||
use crate::i18n::t;
|
||||
|
||||
/// State for an open post editor.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostEditorState {
|
||||
pub post_id: String,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub excerpt: String,
|
||||
pub content: String,
|
||||
pub editor_content: text_editor::Content,
|
||||
pub tags: Vec<String>,
|
||||
pub categories: Vec<String>,
|
||||
pub author: String,
|
||||
@@ -26,16 +27,58 @@ pub struct PostEditorState {
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub is_dirty: bool,
|
||||
pub metadata_expanded: bool,
|
||||
pub excerpt_expanded: bool,
|
||||
pub tags_input: String,
|
||||
pub categories_input: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostEditorState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PostEditorState")
|
||||
.field("post_id", &self.post_id)
|
||||
.field("title", &self.title)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for PostEditorState {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
post_id: self.post_id.clone(),
|
||||
title: self.title.clone(),
|
||||
slug: self.slug.clone(),
|
||||
excerpt: self.excerpt.clone(),
|
||||
content: self.content.clone(),
|
||||
editor_content: text_editor::Content::with_text(&self.content),
|
||||
tags: self.tags.clone(),
|
||||
categories: self.categories.clone(),
|
||||
author: self.author.clone(),
|
||||
language: self.language.clone(),
|
||||
template_slug: self.template_slug.clone(),
|
||||
do_not_translate: self.do_not_translate,
|
||||
status: self.status.clone(),
|
||||
created_at: self.created_at,
|
||||
updated_at: self.updated_at,
|
||||
is_dirty: self.is_dirty,
|
||||
metadata_expanded: self.metadata_expanded,
|
||||
excerpt_expanded: self.excerpt_expanded,
|
||||
tags_input: self.tags_input.clone(),
|
||||
categories_input: self.categories_input.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PostEditorState {
|
||||
pub fn from_post(post: &Post) -> Self {
|
||||
let title = post.title.clone();
|
||||
let content = post.content.clone().unwrap_or_default();
|
||||
Self {
|
||||
post_id: post.id.clone(),
|
||||
title: post.title.clone(),
|
||||
slug: post.slug.clone(),
|
||||
excerpt: post.excerpt.clone().unwrap_or_default(),
|
||||
content: post.content.clone().unwrap_or_default(),
|
||||
content: content.clone(),
|
||||
editor_content: text_editor::Content::with_text(&content),
|
||||
tags: post.tags.clone(),
|
||||
categories: post.categories.clone(),
|
||||
author: post.author.clone().unwrap_or_default(),
|
||||
@@ -46,6 +89,11 @@ impl PostEditorState {
|
||||
created_at: post.created_at,
|
||||
updated_at: post.updated_at,
|
||||
is_dirty: false,
|
||||
metadata_expanded: title.is_empty(),
|
||||
excerpt_expanded: false,
|
||||
tags_input: String::new(),
|
||||
categories_input: String::new(),
|
||||
title,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,10 +104,18 @@ pub enum PostEditorMsg {
|
||||
TitleChanged(String),
|
||||
SlugChanged(String),
|
||||
ExcerptChanged(String),
|
||||
ContentChanged(String),
|
||||
ContentAction(text_editor::Action),
|
||||
AuthorChanged(String),
|
||||
TemplateSlugChanged(String),
|
||||
ToggleDoNotTranslate(bool),
|
||||
ToggleMetadata,
|
||||
ToggleExcerpt,
|
||||
TagsInputChanged(String),
|
||||
TagsInputSubmit,
|
||||
RemoveTag(String),
|
||||
CategoriesInputChanged(String),
|
||||
CategoriesInputSubmit,
|
||||
RemoveCategory(String),
|
||||
Save,
|
||||
Publish,
|
||||
Delete,
|
||||
@@ -70,23 +126,39 @@ pub fn view<'a>(
|
||||
state: &'a PostEditorState,
|
||||
locale: UiLocale,
|
||||
) -> Element<'a, Message> {
|
||||
// ── Header bar ──
|
||||
let dirty_indicator = if state.is_dirty { " \u{25CF}" } else { "" };
|
||||
let title_display = if state.title.is_empty() {
|
||||
t(locale, "editor.untitled")
|
||||
} else {
|
||||
format!("{}{}", state.title, dirty_indicator)
|
||||
};
|
||||
|
||||
let header = inputs::toolbar(
|
||||
vec![
|
||||
text(state.title.clone()).size(18).into(),
|
||||
status_badge(&state.status),
|
||||
text(title_display)
|
||||
.size(18)
|
||||
.wrapping(Wrapping::None)
|
||||
.shaping(Shaping::Advanced)
|
||||
.into(),
|
||||
],
|
||||
vec![
|
||||
button(text(t(locale, "common.save")).size(13))
|
||||
status_badge(&state.status),
|
||||
button(text(t(locale, "common.save")).size(13).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::Save))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "editor.publish")).size(13))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::Publish))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
|
||||
if state.status == PostStatus::Draft {
|
||||
button(text(t(locale, "editor.publish")).size(13).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::Publish))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16])
|
||||
.into()
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
},
|
||||
button(text(t(locale, "modal.confirmDelete.delete")).size(13).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::Delete))
|
||||
.style(inputs::danger_button)
|
||||
.padding([6, 16])
|
||||
@@ -94,64 +166,127 @@ pub fn view<'a>(
|
||||
],
|
||||
);
|
||||
|
||||
// Metadata section
|
||||
let title_input = inputs::labeled_input(
|
||||
&t(locale, "editor.title"),
|
||||
&t(locale, "editor.titlePlaceholder"),
|
||||
&state.title,
|
||||
|s| Message::PostEditor(PostEditorMsg::TitleChanged(s)),
|
||||
);
|
||||
let slug_input = inputs::labeled_input(
|
||||
&t(locale, "editor.slug"),
|
||||
&t(locale, "editor.slugPlaceholder"),
|
||||
&state.slug,
|
||||
|s| Message::PostEditor(PostEditorMsg::SlugChanged(s)),
|
||||
);
|
||||
let meta_row1 = row![title_input, slug_input].spacing(16).width(Length::Fill);
|
||||
// ── Collapsible Metadata Section ──
|
||||
let meta_toggle_label = if state.metadata_expanded {
|
||||
format!("\u{25BC} {}", t(locale, "editor.metadata"))
|
||||
} else {
|
||||
format!("\u{25B6} {}", t(locale, "editor.metadata"))
|
||||
};
|
||||
let meta_toggle = button(
|
||||
text(meta_toggle_label).size(12).color(inputs::SECTION_COLOR).shaping(Shaping::Advanced),
|
||||
)
|
||||
.on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata))
|
||||
.padding([4, 0])
|
||||
.style(|_, _| button::Style {
|
||||
background: None,
|
||||
..button::Style::default()
|
||||
});
|
||||
|
||||
let author_input = inputs::labeled_input(
|
||||
&t(locale, "editor.author"),
|
||||
"",
|
||||
&state.author,
|
||||
|s| Message::PostEditor(PostEditorMsg::AuthorChanged(s)),
|
||||
);
|
||||
let template_input = inputs::labeled_input(
|
||||
&t(locale, "editor.templateSlug"),
|
||||
"",
|
||||
&state.template_slug,
|
||||
|s| Message::PostEditor(PostEditorMsg::TemplateSlugChanged(s)),
|
||||
);
|
||||
let dnt = inputs::labeled_checkbox(
|
||||
&t(locale, "editor.doNotTranslate"),
|
||||
state.do_not_translate,
|
||||
|b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)),
|
||||
);
|
||||
let meta_row2 = row![author_input, template_input, dnt].spacing(16).width(Length::Fill);
|
||||
let metadata_section: Element<'a, Message> = if state.metadata_expanded {
|
||||
let title_input = inputs::labeled_input(
|
||||
&t(locale, "editor.title"),
|
||||
&t(locale, "editor.titlePlaceholder"),
|
||||
&state.title,
|
||||
|s| Message::PostEditor(PostEditorMsg::TitleChanged(s)),
|
||||
);
|
||||
let slug_input = inputs::labeled_input(
|
||||
&t(locale, "editor.slug"),
|
||||
&t(locale, "editor.slugPlaceholder"),
|
||||
&state.slug,
|
||||
|s| Message::PostEditor(PostEditorMsg::SlugChanged(s)),
|
||||
);
|
||||
let meta_row1 = row![title_input, slug_input]
|
||||
.spacing(16)
|
||||
.width(Length::Fill);
|
||||
|
||||
// Excerpt
|
||||
let excerpt_input = inputs::labeled_input(
|
||||
&t(locale, "editor.excerpt"),
|
||||
&t(locale, "editor.excerptPlaceholder"),
|
||||
&state.excerpt,
|
||||
|s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)),
|
||||
);
|
||||
let author_input = inputs::labeled_input(
|
||||
&t(locale, "editor.author"),
|
||||
"",
|
||||
&state.author,
|
||||
|s| Message::PostEditor(PostEditorMsg::AuthorChanged(s)),
|
||||
);
|
||||
let template_input = inputs::labeled_input(
|
||||
&t(locale, "editor.templateSlug"),
|
||||
"",
|
||||
&state.template_slug,
|
||||
|s| Message::PostEditor(PostEditorMsg::TemplateSlugChanged(s)),
|
||||
);
|
||||
let dnt = inputs::labeled_checkbox(
|
||||
&t(locale, "editor.doNotTranslate"),
|
||||
state.do_not_translate,
|
||||
|b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)),
|
||||
);
|
||||
let meta_row2 = row![author_input, template_input, dnt]
|
||||
.spacing(16)
|
||||
.width(Length::Fill);
|
||||
|
||||
// Content (text area placeholder — full editor will use CodeEditor widget)
|
||||
let content_section: Element<'a, Message> = column![
|
||||
inputs::section_header(&t(locale, "editor.content")),
|
||||
container(
|
||||
text_input(&t(locale, "editor.contentPlaceholder"), &state.content)
|
||||
.on_input(|s| Message::PostEditor(PostEditorMsg::ContentChanged(s)))
|
||||
.size(14)
|
||||
// Tags chip input
|
||||
let tags_section = chip_input_field(
|
||||
&t(locale, "editor.tags"),
|
||||
&t(locale, "editor.tagsPlaceholder"),
|
||||
&state.tags,
|
||||
&state.tags_input,
|
||||
|s| Message::PostEditor(PostEditorMsg::TagsInputChanged(s)),
|
||||
Message::PostEditor(PostEditorMsg::TagsInputSubmit),
|
||||
|tag| Message::PostEditor(PostEditorMsg::RemoveTag(tag)),
|
||||
);
|
||||
|
||||
// Categories chip input
|
||||
let categories_section = chip_input_field(
|
||||
&t(locale, "editor.categories"),
|
||||
&t(locale, "editor.categoriesPlaceholder"),
|
||||
&state.categories,
|
||||
&state.categories_input,
|
||||
|s| Message::PostEditor(PostEditorMsg::CategoriesInputChanged(s)),
|
||||
Message::PostEditor(PostEditorMsg::CategoriesInputSubmit),
|
||||
|cat| Message::PostEditor(PostEditorMsg::RemoveCategory(cat)),
|
||||
);
|
||||
|
||||
column![meta_row1, meta_row2, tags_section, categories_section]
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
};
|
||||
|
||||
// ── Collapsible Excerpt Section ──
|
||||
let excerpt_toggle_label = if state.excerpt_expanded {
|
||||
format!("\u{25BC} {}", t(locale, "editor.excerpt"))
|
||||
} else {
|
||||
format!("\u{25B6} {}", t(locale, "editor.excerpt"))
|
||||
};
|
||||
let excerpt_toggle = button(
|
||||
text(excerpt_toggle_label).size(12).color(inputs::SECTION_COLOR).shaping(Shaping::Advanced),
|
||||
)
|
||||
.on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt))
|
||||
.padding([4, 0])
|
||||
.style(|_, _| button::Style {
|
||||
background: None,
|
||||
..button::Style::default()
|
||||
});
|
||||
|
||||
let excerpt_section: Element<'a, Message> = if state.excerpt_expanded {
|
||||
inputs::labeled_input(
|
||||
&t(locale, "editor.excerpt"),
|
||||
&t(locale, "editor.excerptPlaceholder"),
|
||||
&state.excerpt,
|
||||
|s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(300.0)),
|
||||
]
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.into();
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
};
|
||||
|
||||
// Footer
|
||||
// ── 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);
|
||||
|
||||
// ── Footer ──
|
||||
let footer = row![
|
||||
inputs::date_label(&t(locale, "editor.createdAt"), state.created_at),
|
||||
Space::with_width(Length::Fixed(24.0)),
|
||||
@@ -159,24 +294,112 @@ pub fn view<'a>(
|
||||
]
|
||||
.padding(8);
|
||||
|
||||
let body = scrollable(
|
||||
// ── Top pane: header + collapsible sections (scrollable for overflow) ──
|
||||
let top_pane = scrollable(
|
||||
column![
|
||||
header,
|
||||
meta_row1,
|
||||
meta_row2,
|
||||
excerpt_input,
|
||||
content_section,
|
||||
footer,
|
||||
meta_toggle,
|
||||
metadata_section,
|
||||
excerpt_toggle,
|
||||
excerpt_section,
|
||||
]
|
||||
.spacing(12)
|
||||
.padding(16)
|
||||
.spacing(4)
|
||||
.width(Length::Fill)
|
||||
);
|
||||
)
|
||||
.height(Length::Shrink);
|
||||
|
||||
container(body)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
// ── Full layout: top pane (shrink), editor (fill), footer (shrink) ──
|
||||
column![
|
||||
top_pane,
|
||||
content_label,
|
||||
editor_widget,
|
||||
footer,
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(16)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.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,
|
||||
placeholder: &str,
|
||||
chips: &[String],
|
||||
input_value: &str,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
on_submit: Message,
|
||||
on_remove: impl Fn(String) -> Message + 'a,
|
||||
) -> Element<'a, Message> {
|
||||
let chip_elements: Vec<Element<'a, Message>> = chips
|
||||
.iter()
|
||||
.map(|chip| {
|
||||
let label = format!("{} \u{2715}", chip);
|
||||
let chip_val = chip.clone();
|
||||
button(text(label).size(11).shaping(Shaping::Advanced))
|
||||
.on_press(on_remove(chip_val))
|
||||
.padding([2, 6])
|
||||
.style(chip_button_style)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut chip_row = row![].spacing(4);
|
||||
for el in chip_elements {
|
||||
chip_row = chip_row.push(el);
|
||||
}
|
||||
|
||||
column![
|
||||
text(label.to_string()).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced),
|
||||
chip_row.wrap(),
|
||||
text_input(placeholder, input_value)
|
||||
.on_input(on_input)
|
||||
.on_submit(on_submit)
|
||||
.size(13)
|
||||
.padding([4, 6]),
|
||||
]
|
||||
.spacing(4)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn chip_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.30, 0.35, 0.45),
|
||||
_ => Color::from_rgb(0.22, 0.25, 0.32),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(iced::Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.80, 0.82, 0.88),
|
||||
border: iced::Border {
|
||||
radius: 3.0.into(),
|
||||
..iced::Border::default()
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn status_badge<'a>(status: &PostStatus) -> Element<'a, Message> {
|
||||
@@ -185,15 +408,21 @@ fn status_badge<'a>(status: &PostStatus) -> Element<'a, Message> {
|
||||
PostStatus::Published => ("Published", Color::from_rgb(0.2, 0.7, 0.3)),
|
||||
PostStatus::Archived => ("Archived", Color::from_rgb(0.5, 0.5, 0.5)),
|
||||
};
|
||||
container(text(label).size(11).color(color))
|
||||
.padding([2, 8])
|
||||
.style(move |_: &Theme| container::Style {
|
||||
border: iced::Border {
|
||||
radius: 4.0.into(),
|
||||
width: 1.0,
|
||||
color,
|
||||
},
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
container(
|
||||
text(label)
|
||||
.size(11)
|
||||
.color(color)
|
||||
.wrapping(Wrapping::None)
|
||||
.shaping(Shaping::Advanced),
|
||||
)
|
||||
.padding([2, 8])
|
||||
.style(move |_: &Theme| container::Style {
|
||||
border: iced::Border {
|
||||
radius: 4.0.into(),
|
||||
width: 1.0,
|
||||
color,
|
||||
},
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -194,6 +194,11 @@
|
||||
"editor.altPlaceholder": "Bild beschreiben...",
|
||||
"editor.caption": "Bildunterschrift",
|
||||
"editor.metadata": "Metadaten",
|
||||
"editor.tags": "Tags",
|
||||
"editor.tagsPlaceholder": "Tag hinzufügen...",
|
||||
"editor.categories": "Kategorien",
|
||||
"editor.categoriesPlaceholder": "Kategorie hinzufügen...",
|
||||
"editor.untitled": "Ohne Titel",
|
||||
"editor.createdAt": "Erstellt",
|
||||
"editor.updatedAt": "Aktualisiert",
|
||||
"tags.noTags": "Noch keine Tags",
|
||||
|
||||
@@ -194,6 +194,11 @@
|
||||
"editor.altPlaceholder": "Describe the image...",
|
||||
"editor.caption": "Caption",
|
||||
"editor.metadata": "Metadata",
|
||||
"editor.tags": "Tags",
|
||||
"editor.tagsPlaceholder": "Add tag...",
|
||||
"editor.categories": "Categories",
|
||||
"editor.categoriesPlaceholder": "Add category...",
|
||||
"editor.untitled": "Untitled",
|
||||
"editor.createdAt": "Created",
|
||||
"editor.updatedAt": "Updated",
|
||||
"tags.noTags": "No tags yet",
|
||||
|
||||
@@ -194,6 +194,11 @@
|
||||
"editor.altPlaceholder": "Describir la imagen...",
|
||||
"editor.caption": "Leyenda",
|
||||
"editor.metadata": "Metadatos",
|
||||
"editor.tags": "Etiquetas",
|
||||
"editor.tagsPlaceholder": "Añadir etiqueta...",
|
||||
"editor.categories": "Categorías",
|
||||
"editor.categoriesPlaceholder": "Añadir categoría...",
|
||||
"editor.untitled": "Sin título",
|
||||
"editor.createdAt": "Creado",
|
||||
"editor.updatedAt": "Actualizado",
|
||||
"tags.noTags": "Sin etiquetas",
|
||||
|
||||
@@ -194,6 +194,11 @@
|
||||
"editor.altPlaceholder": "Décrivez l'image...",
|
||||
"editor.caption": "Légende",
|
||||
"editor.metadata": "Métadonnées",
|
||||
"editor.tags": "Tags",
|
||||
"editor.tagsPlaceholder": "Ajouter un tag...",
|
||||
"editor.categories": "Catégories",
|
||||
"editor.categoriesPlaceholder": "Ajouter une catégorie...",
|
||||
"editor.untitled": "Sans titre",
|
||||
"editor.createdAt": "Créé",
|
||||
"editor.updatedAt": "Mis à jour",
|
||||
"tags.noTags": "Aucun tag",
|
||||
|
||||
@@ -194,6 +194,11 @@
|
||||
"editor.altPlaceholder": "Descrivi l'immagine...",
|
||||
"editor.caption": "Didascalia",
|
||||
"editor.metadata": "Metadati",
|
||||
"editor.tags": "Tag",
|
||||
"editor.tagsPlaceholder": "Aggiungi tag...",
|
||||
"editor.categories": "Categorie",
|
||||
"editor.categoriesPlaceholder": "Aggiungi categoria...",
|
||||
"editor.untitled": "Senza titolo",
|
||||
"editor.createdAt": "Creato",
|
||||
"editor.updatedAt": "Aggiornato",
|
||||
"tags.noTags": "Nessun tag",
|
||||
|
||||
Reference in New Issue
Block a user