Open quick actions in editor popovers
This commit is contained in:
@@ -6,7 +6,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
|
|
||||||
## Available Features
|
## Available Features
|
||||||
|
|
||||||
- Native Iced desktop workspace with localized menus, tabs, automatically paged post/media sidebars, dialogs, tasks, embedded Wry previews, and a live Pico CSS theme editor.
|
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, dialogs, tasks, embedded Wry previews, and a live Pico CSS theme editor.
|
||||||
- Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import.
|
- Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import.
|
||||||
- Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
|
- Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
|
||||||
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod inputs;
|
pub mod inputs;
|
||||||
pub mod native_edit;
|
pub mod native_edit;
|
||||||
|
pub mod popover;
|
||||||
pub mod webview;
|
pub mod webview;
|
||||||
|
|||||||
373
crates/bds-ui/src/components/popover.rs
Normal file
373
crates/bds-ui/src/components/popover.rs
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
use iced::advanced::layout;
|
||||||
|
use iced::advanced::overlay;
|
||||||
|
use iced::advanced::renderer;
|
||||||
|
use iced::advanced::widget::{Operation, Tree};
|
||||||
|
use iced::advanced::{Clipboard, Layout, Shell, Widget};
|
||||||
|
use iced::event;
|
||||||
|
use iced::keyboard::{self, Key, key};
|
||||||
|
use iced::mouse;
|
||||||
|
use iced::{Element, Event, Length, Point, Rectangle, Size, Vector};
|
||||||
|
|
||||||
|
/// An interactive popup anchored below the right edge of its trigger.
|
||||||
|
pub struct Popover<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> {
|
||||||
|
trigger: Element<'a, Message, Theme, Renderer>,
|
||||||
|
popup: Element<'a, Message, Theme, Renderer>,
|
||||||
|
is_open: bool,
|
||||||
|
on_dismiss: Message,
|
||||||
|
gap: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Popover<'a, Message, Theme, Renderer> {
|
||||||
|
pub fn new(
|
||||||
|
trigger: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
popup: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
is_open: bool,
|
||||||
|
on_dismiss: Message,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: trigger.into(),
|
||||||
|
popup: popup.into(),
|
||||||
|
is_open,
|
||||||
|
on_dismiss,
|
||||||
|
gap: 8.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popup_position(anchor: Rectangle, popup: Size, viewport: Size, gap: f32) -> Point {
|
||||||
|
let max_x = (viewport.width - popup.width).max(0.0);
|
||||||
|
let x = (anchor.x + anchor.width - popup.width).clamp(0.0, max_x);
|
||||||
|
let below = anchor.y + anchor.height + gap;
|
||||||
|
let y = if below + popup.height <= viewport.height {
|
||||||
|
below
|
||||||
|
} else {
|
||||||
|
(anchor.y - gap - popup.height).max(0.0)
|
||||||
|
};
|
||||||
|
Point::new(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for Popover<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone,
|
||||||
|
Renderer: renderer::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.trigger), Tree::new(&self.popup)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(&[self.trigger.as_widget(), self.popup.as_widget()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
self.trigger.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
self.trigger
|
||||||
|
.as_widget()
|
||||||
|
.layout(&mut tree.children[0], renderer, limits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
self.trigger
|
||||||
|
.as_widget()
|
||||||
|
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) -> event::Status {
|
||||||
|
self.trigger.as_widget_mut().on_event(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.trigger.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.trigger.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
let (trigger_tree, popup_tree) = tree.children.split_at_mut(1);
|
||||||
|
let trigger_overlay = self.trigger.as_widget_mut().overlay(
|
||||||
|
&mut trigger_tree[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
translation,
|
||||||
|
);
|
||||||
|
let popup_overlay = self.is_open.then(|| {
|
||||||
|
overlay::Element::new(Box::new(PopupOverlay {
|
||||||
|
anchor: layout.bounds() + translation,
|
||||||
|
popup: &mut self.popup,
|
||||||
|
state: &mut popup_tree[0],
|
||||||
|
on_dismiss: self.on_dismiss.clone(),
|
||||||
|
gap: self.gap,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
if trigger_overlay.is_some() || popup_overlay.is_some() {
|
||||||
|
Some(
|
||||||
|
overlay::Group::with_children(
|
||||||
|
trigger_overlay.into_iter().chain(popup_overlay).collect(),
|
||||||
|
)
|
||||||
|
.overlay(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Popover<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone + 'a,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: renderer::Renderer + 'a,
|
||||||
|
{
|
||||||
|
fn from(popover: Popover<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Element::new(popover)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PopupOverlay<'a, 'b, Message, Theme, Renderer> {
|
||||||
|
anchor: Rectangle,
|
||||||
|
popup: &'b mut Element<'a, Message, Theme, Renderer>,
|
||||||
|
state: &'b mut Tree,
|
||||||
|
on_dismiss: Message,
|
||||||
|
gap: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||||
|
for PopupOverlay<'_, '_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone,
|
||||||
|
Renderer: renderer::Renderer,
|
||||||
|
{
|
||||||
|
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||||
|
let popup = self.popup.as_widget().layout(
|
||||||
|
self.state,
|
||||||
|
renderer,
|
||||||
|
&layout::Limits::new(Size::ZERO, bounds),
|
||||||
|
);
|
||||||
|
let position = popup_position(self.anchor, popup.size(), bounds, self.gap);
|
||||||
|
layout::Node::with_children(popup.size(), vec![popup])
|
||||||
|
.translate(Vector::new(position.x, position.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
) {
|
||||||
|
self.popup.as_widget().draw(
|
||||||
|
self.state,
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
style,
|
||||||
|
layout.children().next().expect("popover child layout"),
|
||||||
|
cursor,
|
||||||
|
&Rectangle::with_size(Size::INFINITY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(&mut self, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
|
||||||
|
self.popup.as_widget().operate(
|
||||||
|
self.state,
|
||||||
|
layout.children().next().expect("popover child layout"),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) -> event::Status {
|
||||||
|
let popup_layout = layout.children().next().expect("popover child layout");
|
||||||
|
let status = self.popup.as_widget_mut().on_event(
|
||||||
|
self.state,
|
||||||
|
event.clone(),
|
||||||
|
popup_layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
&Rectangle::with_size(Size::INFINITY),
|
||||||
|
);
|
||||||
|
if status == event::Status::Captured {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dismiss = matches!(
|
||||||
|
event,
|
||||||
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
|
| Event::Touch(iced::touch::Event::FingerPressed { .. })
|
||||||
|
| Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key: Key::Named(key::Named::Escape),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if dismiss && !cursor.is_over(layout.bounds()) {
|
||||||
|
shell.publish(self.on_dismiss.clone());
|
||||||
|
event::Status::Captured
|
||||||
|
} else if dismiss
|
||||||
|
&& matches!(
|
||||||
|
event,
|
||||||
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key: Key::Named(key::Named::Escape),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
{
|
||||||
|
shell.publish(self.on_dismiss.clone());
|
||||||
|
event::Status::Captured
|
||||||
|
} else {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.popup.as_widget().mouse_interaction(
|
||||||
|
self.state,
|
||||||
|
layout.children().next().expect("popover child layout"),
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.popup.as_widget_mut().overlay(
|
||||||
|
self.state,
|
||||||
|
layout.children().next().expect("popover child layout"),
|
||||||
|
renderer,
|
||||||
|
Vector::ZERO,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn popover<'a, Message: Clone + 'a>(
|
||||||
|
trigger: impl Into<Element<'a, Message>>,
|
||||||
|
popup: impl Into<Element<'a, Message>>,
|
||||||
|
is_open: bool,
|
||||||
|
on_dismiss: Message,
|
||||||
|
) -> Popover<'a, Message> {
|
||||||
|
Popover::new(trigger, popup, is_open, on_dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn popup_is_right_aligned_below_its_trigger() {
|
||||||
|
assert_eq!(
|
||||||
|
popup_position(
|
||||||
|
Rectangle::new(Point::new(400.0, 20.0), Size::new(100.0, 30.0)),
|
||||||
|
Size::new(220.0, 180.0),
|
||||||
|
Size::new(800.0, 600.0),
|
||||||
|
8.0,
|
||||||
|
),
|
||||||
|
Point::new(280.0, 58.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn popup_flips_above_and_stays_inside_the_viewport() {
|
||||||
|
assert_eq!(
|
||||||
|
popup_position(
|
||||||
|
Rectangle::new(Point::new(10.0, 570.0), Size::new(40.0, 24.0)),
|
||||||
|
Size::new(220.0, 180.0),
|
||||||
|
Size::new(800.0, 600.0),
|
||||||
|
8.0,
|
||||||
|
),
|
||||||
|
Point::new(0.0, 382.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use bds_core::i18n::{self, UiLocale};
|
|||||||
use bds_core::model::{Media, MediaTranslation};
|
use bds_core::model::{Media, MediaTranslation};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::{inputs, popover};
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use crate::views::post_editor::TranslationFlag;
|
use crate::views::post_editor::TranslationFlag;
|
||||||
use crate::views::status_bar;
|
use crate::views::status_bar;
|
||||||
@@ -235,16 +235,48 @@ pub fn view<'a>(
|
|||||||
data_dir: Option<&Path>,
|
data_dir: Option<&Path>,
|
||||||
ai_enabled: bool,
|
ai_enabled: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
let mut quick_action_items: Vec<Element<'a, Message>> = Vec::new();
|
||||||
|
if state.mime_type.starts_with("image/") {
|
||||||
|
quick_action_items.push(quick_action_item(
|
||||||
|
t(locale, "editor.aiAnalyze"),
|
||||||
|
MediaEditorMsg::AnalyzeWithAi,
|
||||||
|
ai_enabled,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
quick_action_items.push(quick_action_item(
|
||||||
|
t(locale, "editor.detectLanguage"),
|
||||||
|
MediaEditorMsg::DetectLanguage,
|
||||||
|
ai_enabled,
|
||||||
|
));
|
||||||
|
quick_action_items.push(quick_action_item(
|
||||||
|
t(locale, "editor.translate"),
|
||||||
|
MediaEditorMsg::TranslateMetadata,
|
||||||
|
ai_enabled,
|
||||||
|
));
|
||||||
|
let quick_actions_menu: Element<'a, Message> = container(column(quick_action_items).spacing(4))
|
||||||
|
.padding(8)
|
||||||
|
.style(status_bar::dropdown_bg)
|
||||||
|
.into();
|
||||||
|
let quick_actions_button: Element<'a, Message> =
|
||||||
|
button(text(t(locale, "editor.quickActions")).size(13))
|
||||||
|
.on_press_maybe(
|
||||||
|
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::ToggleQuickActions)),
|
||||||
|
)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.padding([6, 16])
|
||||||
|
.into();
|
||||||
|
let quick_actions: Element<'a, Message> = popover::popover(
|
||||||
|
quick_actions_button,
|
||||||
|
quick_actions_menu,
|
||||||
|
state.quick_actions_open,
|
||||||
|
Message::MediaEditor(MediaEditorMsg::ToggleQuickActions),
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
|
||||||
let header_row = inputs::toolbar(
|
let header_row = inputs::toolbar(
|
||||||
vec![text(state.original_name.clone()).size(18).into()],
|
vec![text(state.original_name.clone()).size(18).into()],
|
||||||
vec![
|
vec![
|
||||||
button(text(t(locale, "editor.quickActions")).size(13))
|
quick_actions,
|
||||||
.on_press_maybe(
|
|
||||||
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::ToggleQuickActions)),
|
|
||||||
)
|
|
||||||
.style(inputs::secondary_button)
|
|
||||||
.padding([6, 16])
|
|
||||||
.into(),
|
|
||||||
button(text(t(locale, "editor.replaceFile")).size(13))
|
button(text(t(locale, "editor.replaceFile")).size(13))
|
||||||
.on_press(Message::MediaEditor(MediaEditorMsg::ReplaceFile))
|
.on_press(Message::MediaEditor(MediaEditorMsg::ReplaceFile))
|
||||||
.style(inputs::secondary_button)
|
.style(inputs::secondary_button)
|
||||||
@@ -263,38 +295,7 @@ pub fn view<'a>(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let header_menu: Element<'a, Message> = if state.quick_actions_open {
|
let header = header_row;
|
||||||
let mut items: Vec<Element<'a, Message>> = Vec::new();
|
|
||||||
if state.mime_type.starts_with("image/") {
|
|
||||||
items.push(quick_action_item(
|
|
||||||
t(locale, "editor.aiAnalyze"),
|
|
||||||
MediaEditorMsg::AnalyzeWithAi,
|
|
||||||
ai_enabled,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
items.push(quick_action_item(
|
|
||||||
t(locale, "editor.detectLanguage"),
|
|
||||||
MediaEditorMsg::DetectLanguage,
|
|
||||||
ai_enabled,
|
|
||||||
));
|
|
||||||
items.push(quick_action_item(
|
|
||||||
t(locale, "editor.translate"),
|
|
||||||
MediaEditorMsg::TranslateMetadata,
|
|
||||||
ai_enabled,
|
|
||||||
));
|
|
||||||
row![
|
|
||||||
Space::with_width(Length::Fill),
|
|
||||||
container(column(items).spacing(4))
|
|
||||||
.padding(8)
|
|
||||||
.style(status_bar::dropdown_bg),
|
|
||||||
]
|
|
||||||
.align_y(iced::Alignment::Start)
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Space::new(0, 0).into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = column![header_row, header_menu].spacing(6);
|
|
||||||
|
|
||||||
// Translation flags bar
|
// Translation flags bar
|
||||||
let flags = state.translation_flags();
|
let flags = state.translation_flags();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use bds_core::model::{Post, PostStatus, PostTranslation};
|
|||||||
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
|
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::{inputs, popover};
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use crate::views::status_bar;
|
use crate::views::status_bar;
|
||||||
|
|
||||||
@@ -409,9 +409,55 @@ pub fn view<'a>(
|
|||||||
.style(inputs::secondary_button)
|
.style(inputs::secondary_button)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
let quick_actions_menu: Element<'a, Message> = container(
|
||||||
|
column![
|
||||||
|
quick_action_item(
|
||||||
|
locale,
|
||||||
|
t(locale, "editor.aiAnalyze"),
|
||||||
|
PostEditorMsg::AnalyzeWithAi,
|
||||||
|
ai_enabled
|
||||||
|
),
|
||||||
|
quick_action_item(
|
||||||
|
locale,
|
||||||
|
t(locale, "editor.suggestTaxonomy"),
|
||||||
|
PostEditorMsg::AnalyzeTaxonomy,
|
||||||
|
ai_enabled
|
||||||
|
),
|
||||||
|
quick_action_item(
|
||||||
|
locale,
|
||||||
|
t(locale, "editor.translate"),
|
||||||
|
PostEditorMsg::Translate,
|
||||||
|
ai_enabled
|
||||||
|
),
|
||||||
|
quick_action_item(
|
||||||
|
locale,
|
||||||
|
t(locale, "editor.detectLanguage"),
|
||||||
|
PostEditorMsg::DetectLanguage,
|
||||||
|
ai_enabled
|
||||||
|
),
|
||||||
|
quick_action_item(
|
||||||
|
locale,
|
||||||
|
t(locale, "editor.addGalleryImages"),
|
||||||
|
PostEditorMsg::AddGalleryImages,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(4),
|
||||||
|
)
|
||||||
|
.padding(8)
|
||||||
|
.style(status_bar::dropdown_bg)
|
||||||
|
.into();
|
||||||
|
let quick_actions: Element<'a, Message> = popover::popover(
|
||||||
|
quick_actions_button,
|
||||||
|
quick_actions_menu,
|
||||||
|
state.quick_actions_open,
|
||||||
|
Message::PostEditor(PostEditorMsg::ToggleQuickActions),
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
|
||||||
let mut header_action_items: Vec<Element<'a, Message>> = vec![
|
let mut header_action_items: Vec<Element<'a, Message>> = vec![
|
||||||
status_badge(&state.status),
|
status_badge(&state.status),
|
||||||
quick_actions_button,
|
quick_actions,
|
||||||
button(
|
button(
|
||||||
text(t(locale, "common.save"))
|
text(t(locale, "common.save"))
|
||||||
.size(13)
|
.size(13)
|
||||||
@@ -478,54 +524,7 @@ pub fn view<'a>(
|
|||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
let header_menu: Element<'a, Message> = if state.quick_actions_open {
|
let header = inputs::card(header_row).padding(10);
|
||||||
row![
|
|
||||||
Space::with_width(Length::Fill),
|
|
||||||
container(
|
|
||||||
column![
|
|
||||||
quick_action_item(
|
|
||||||
locale,
|
|
||||||
t(locale, "editor.aiAnalyze"),
|
|
||||||
PostEditorMsg::AnalyzeWithAi,
|
|
||||||
ai_enabled
|
|
||||||
),
|
|
||||||
quick_action_item(
|
|
||||||
locale,
|
|
||||||
t(locale, "editor.suggestTaxonomy"),
|
|
||||||
PostEditorMsg::AnalyzeTaxonomy,
|
|
||||||
ai_enabled
|
|
||||||
),
|
|
||||||
quick_action_item(
|
|
||||||
locale,
|
|
||||||
t(locale, "editor.translate"),
|
|
||||||
PostEditorMsg::Translate,
|
|
||||||
ai_enabled
|
|
||||||
),
|
|
||||||
quick_action_item(
|
|
||||||
locale,
|
|
||||||
t(locale, "editor.detectLanguage"),
|
|
||||||
PostEditorMsg::DetectLanguage,
|
|
||||||
ai_enabled
|
|
||||||
),
|
|
||||||
quick_action_item(
|
|
||||||
locale,
|
|
||||||
t(locale, "editor.addGalleryImages"),
|
|
||||||
PostEditorMsg::AddGalleryImages,
|
|
||||||
true
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.spacing(4)
|
|
||||||
)
|
|
||||||
.padding(8)
|
|
||||||
.style(status_bar::dropdown_bg),
|
|
||||||
]
|
|
||||||
.align_y(iced::Alignment::Start)
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Space::new(0, 0).into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = inputs::card(column![header_row, header_menu].spacing(6)).padding(10);
|
|
||||||
|
|
||||||
// ── Collapsible Metadata Section ──
|
// ── Collapsible Metadata Section ──
|
||||||
let meta_toggle_label = if state.metadata_expanded {
|
let meta_toggle_label = if state.metadata_expanded {
|
||||||
|
|||||||
Reference in New Issue
Block a user