feat: more work on M4
This commit is contained in:
@@ -197,6 +197,10 @@ impl MediaEditorState {
|
||||
/// Media editor messages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MediaEditorMsg {
|
||||
AnalyzeWithAi,
|
||||
DetectLanguage,
|
||||
TranslateMetadata,
|
||||
TranslateTo(String),
|
||||
TitleChanged(String),
|
||||
AltChanged(String),
|
||||
CaptionChanged(String),
|
||||
@@ -224,6 +228,22 @@ pub fn view<'a>(
|
||||
text(state.original_name.clone()).size(18).into(),
|
||||
],
|
||||
vec![
|
||||
if state.mime_type.starts_with("image/") {
|
||||
button(text(t(locale, "editor.aiAnalyze")).size(13))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::AnalyzeWithAi))
|
||||
.padding([6, 16])
|
||||
.into()
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
},
|
||||
button(text(t(locale, "editor.detectLanguage")).size(13))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::DetectLanguage))
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "editor.translate")).size(13))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::TranslateMetadata))
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "common.save")).size(13))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::Save))
|
||||
.style(inputs::primary_button)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::widget::{button, column, container, image, row, text, text_input, Space};
|
||||
use iced::widget::{button, checkbox, column, container, image, row, scrollable, text, text_input, Space};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
@@ -20,6 +20,31 @@ pub struct InsertLinkResult {
|
||||
pub canonical_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AiEntityTarget {
|
||||
Post(String),
|
||||
Media(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AiSuggestionField {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub current_value: String,
|
||||
pub suggested_value: String,
|
||||
pub accepted: bool,
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LanguageTarget {
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub flag_emoji: String,
|
||||
pub has_existing_translation: bool,
|
||||
pub existing_status: Option<String>,
|
||||
}
|
||||
|
||||
/// Active modal state. Only one modal at a time.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ModalState {
|
||||
@@ -60,6 +85,15 @@ pub enum ModalState {
|
||||
media_list: Vec<bds_core::model::Media>,
|
||||
selected_index: Option<usize>,
|
||||
},
|
||||
AISuggestions {
|
||||
target: AiEntityTarget,
|
||||
fields: Vec<AiSuggestionField>,
|
||||
},
|
||||
LanguagePicker {
|
||||
target: AiEntityTarget,
|
||||
source_language: String,
|
||||
available_targets: Vec<LanguageTarget>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -874,6 +908,119 @@ pub fn view(state: ModalState, locale: UiLocale, data_dir: Option<&Path>) -> Ele
|
||||
.style(modal_box_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
ModalState::AISuggestions { target, fields } => {
|
||||
let title = text(t(locale, "modal.aiSuggestions.title"))
|
||||
.size(16)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::WHITE);
|
||||
|
||||
let rows = fields.iter().enumerate().map(|(index, field)| {
|
||||
let toggle = checkbox(field.label.clone(), field.accepted)
|
||||
.on_toggle_maybe((!field.locked)
|
||||
.then_some(move |value| Message::ToggleAiSuggestionField(index, value)))
|
||||
.size(16)
|
||||
.text_size(13);
|
||||
container(column![
|
||||
toggle,
|
||||
row![
|
||||
container(text(field.current_value.clone()).size(12).color(Color::from_rgb(0.58, 0.58, 0.64))).width(Length::FillPortion(1)),
|
||||
text("→").size(14).color(Color::from_rgb(0.62, 0.66, 0.74)),
|
||||
container(text(field.suggested_value.clone()).size(12).color(Color::WHITE)).width(Length::FillPortion(1)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center),
|
||||
]
|
||||
.spacing(8))
|
||||
.padding(10)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.18, 0.20, 0.25))),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}).collect::<Vec<Element<'static, Message>>>();
|
||||
|
||||
let buttons = row![
|
||||
button(text(t(locale, "common.cancel")).size(13))
|
||||
.on_press(Message::DismissModal)
|
||||
.padding([6, 16])
|
||||
.style(cancel_button_style),
|
||||
Space::with_width(Length::Fill),
|
||||
button(text(t(locale, "modal.aiSuggestions.applySelected")).size(13))
|
||||
.on_press(Message::ApplyAiSuggestions(target, fields))
|
||||
.padding([6, 16])
|
||||
.style(confirm_button_style),
|
||||
];
|
||||
|
||||
let content = column![
|
||||
title,
|
||||
Space::with_height(12.0),
|
||||
scrollable(column(rows).spacing(8)).height(Length::Fixed(320.0)),
|
||||
Space::with_height(16.0),
|
||||
buttons,
|
||||
]
|
||||
.spacing(4);
|
||||
|
||||
container(content.padding(20))
|
||||
.width(Length::Fixed(640.0))
|
||||
.style(modal_box_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
ModalState::LanguagePicker { target, source_language, available_targets } => {
|
||||
let title = text(t(locale, "modal.languagePicker.title"))
|
||||
.size(16)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::WHITE);
|
||||
let subtitle = text(format!("{}: {}", t(locale, "editor.language"), source_language))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.60, 0.60, 0.68));
|
||||
|
||||
let rows = if available_targets.is_empty() {
|
||||
vec![text(t(locale, "common.noResults")).size(12).into()]
|
||||
} else {
|
||||
available_targets.into_iter().map(|language| {
|
||||
let message = match &target {
|
||||
AiEntityTarget::Post(_) => Message::PostEditor(PostEditorMsg::TranslateTo(language.code.clone())),
|
||||
AiEntityTarget::Media(_) => Message::MediaEditor(crate::views::media_editor::MediaEditorMsg::TranslateTo(language.code.clone())),
|
||||
};
|
||||
let status = language.existing_status.clone().unwrap_or_default();
|
||||
button(row![
|
||||
text(format!("{} {}", language.flag_emoji, language.name)).size(13).color(Color::WHITE),
|
||||
Space::with_width(Length::Fill),
|
||||
text(status).size(11).color(Color::from_rgb(0.60, 0.60, 0.68)),
|
||||
].align_y(Alignment::Center))
|
||||
.on_press(message)
|
||||
.padding([8, 12])
|
||||
.style(cancel_button_style)
|
||||
.into()
|
||||
}).collect::<Vec<Element<'static, Message>>>()
|
||||
};
|
||||
|
||||
let content = column![
|
||||
title,
|
||||
subtitle,
|
||||
Space::with_height(12.0),
|
||||
column(rows).spacing(6),
|
||||
Space::with_height(16.0),
|
||||
button(text(t(locale, "common.cancel")).size(13))
|
||||
.on_press(Message::DismissModal)
|
||||
.padding([6, 16])
|
||||
.style(cancel_button_style),
|
||||
]
|
||||
.spacing(4);
|
||||
|
||||
container(content.padding(20))
|
||||
.width(Length::Fixed(420.0))
|
||||
.style(modal_box_style)
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
// Center the modal in a full-screen backdrop
|
||||
|
||||
@@ -322,6 +322,11 @@ impl PostEditorState {
|
||||
/// Post editor messages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PostEditorMsg {
|
||||
AnalyzeWithAi,
|
||||
AnalyzeTaxonomy,
|
||||
DetectLanguage,
|
||||
Translate,
|
||||
TranslateTo(String),
|
||||
TitleChanged(String),
|
||||
SlugChanged(String),
|
||||
ExcerptChanged(String),
|
||||
@@ -389,6 +394,18 @@ pub fn view<'a>(
|
||||
.into()],
|
||||
vec![
|
||||
status_badge(&state.status),
|
||||
button(text(t(locale, "editor.aiAnalyze")).size(13).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::AnalyzeWithAi))
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "editor.suggestTaxonomy")).size(13).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::AnalyzeTaxonomy))
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "editor.translate")).size(13).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::Translate))
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(
|
||||
text(t(locale, "common.save"))
|
||||
.size(13)
|
||||
@@ -523,6 +540,9 @@ pub fn view<'a>(
|
||||
Some(&state.language),
|
||||
|lang| Message::PostEditor(PostEditorMsg::LanguageChanged(lang)),
|
||||
);
|
||||
let detect_language = button(text(t(locale, "editor.detectLanguage")).size(12).shaping(Shaping::Advanced))
|
||||
.on_press(Message::PostEditor(PostEditorMsg::DetectLanguage))
|
||||
.padding([6, 12]);
|
||||
let template_input = inputs::labeled_input(
|
||||
&t(locale, "editor.templateSlug"),
|
||||
"",
|
||||
@@ -534,7 +554,8 @@ pub fn view<'a>(
|
||||
state.do_not_translate,
|
||||
|b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)),
|
||||
);
|
||||
let meta_row2 = row![author_input, language_input, template_input, dnt]
|
||||
let language_block = column![language_input, detect_language].spacing(6);
|
||||
let meta_row2 = row![author_input, language_block, template_input, dnt]
|
||||
.spacing(16)
|
||||
.width(Length::Fill);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user