diff options
Diffstat (limited to 'src/components/post_editor.rs')
-rw-r--r-- | src/components/post_editor.rs | 368 |
1 files changed, 186 insertions, 182 deletions
diff --git a/src/components/post_editor.rs b/src/components/post_editor.rs index c42b06a..021ba91 100644 --- a/src/components/post_editor.rs +++ b/src/components/post_editor.rs @@ -1,12 +1,16 @@ -use gettextrs::*; use crate::components::tag_pill::*; use adw::prelude::*; +use gettextrs::*; use glib::translate::IntoGlib; -use gtk::GridLayoutChild; -use relm4::{factory::FactoryVecDeque, gtk, prelude::{Controller, DynamicIndex}, Component, ComponentParts, ComponentSender, RelmWidgetExt}; #[cfg(feature = "smart-summary")] use relm4::prelude::ComponentController; +use relm4::{ + factory::FactoryVecDeque, + gtk, + prelude::{Controller, DynamicIndex}, + Component, ComponentParts, ComponentSender, RelmWidgetExt, +}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] #[enum_type(name = "MicropubVisibility")] @@ -19,7 +23,7 @@ impl std::fmt::Display for Visibility { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Public => "public", - Self::Private => "private" + Self::Private => "private", }) } } @@ -30,43 +34,52 @@ pub struct Post { pub summary: Option<String>, pub tags: Vec<String>, pub content: String, - pub visibility: Visibility + pub visibility: Visibility, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct PostConversionSettings { + pub send_html_directly: bool, } -impl From<Post> for microformats::types::Item { - fn from(post: Post) -> Self { - use microformats::types::{Item, Class, KnownClass, PropertyValue}; +impl Post { + pub fn into_mf2(self, settings: PostConversionSettings) -> microformats::types::Item { + use microformats::types::{Class, Fragment, Item, KnownClass, PropertyValue}; let mut mf2 = Item::new(vec![Class::Known(KnownClass::Entry)]); - if let Some(name) = post.name { - mf2.properties.insert( - "name".to_owned(), vec![PropertyValue::Plain(name)] - ); + if let Some(name) = self.name { + mf2.properties + .insert("name".to_owned(), vec![PropertyValue::Plain(name)]); } - if let Some(summary) = post.summary { - mf2.properties.insert( - "summary".to_owned(), - vec![PropertyValue::Plain(summary)] - ); + if let Some(summary) = self.summary { + mf2.properties + .insert("summary".to_owned(), vec![PropertyValue::Plain(summary)]); } - if !post.tags.is_empty() { + if !self.tags.is_empty() { mf2.properties.insert( "category".to_string(), - post.tags.into_iter().map(PropertyValue::Plain).collect() + self.tags.into_iter().map(PropertyValue::Plain).collect(), ); } mf2.properties.insert( "visibility".to_string(), - vec![PropertyValue::Plain(post.visibility.to_string())] + vec![PropertyValue::Plain(self.visibility.to_string())], ); - mf2.properties.insert( - "content".to_string(), - vec![PropertyValue::Plain(post.content)] - ); + let content = if settings.send_html_directly { + PropertyValue::Fragment(Fragment { + html: self.content.clone(), + value: self.content, + lang: None, + }) + } else { + PropertyValue::Plain(self.content) + }; + + mf2.properties.insert("content".to_string(), vec![content]); mf2 } @@ -75,22 +88,35 @@ impl From<Post> for microformats::types::Item { #[tracker::track] #[derive(Debug)] pub(crate) struct PostEditor<E> { - #[no_eq] smart_summary_busy_guard: Option<gtk::gio::ApplicationBusyGuard>, + #[no_eq] + smart_summary_busy_guard: Option<gtk::gio::ApplicationBusyGuard>, sending: bool, - - #[do_not_track] name_buffer: gtk::EntryBuffer, - #[do_not_track] summary_buffer: gtk::EntryBuffer, - #[do_not_track] content_buffer: gtk::TextBuffer, - - #[do_not_track] pending_tag_buffer: gtk::EntryBuffer, - #[do_not_track] tags: relm4::factory::FactoryVecDeque<TagPill>, + #[do_not_track] + #[allow(dead_code)] + spell_checker: spelling::Checker, + #[do_not_track] + spelling_adapter: spelling::TextBufferAdapter, + + #[do_not_track] + name_buffer: gtk::EntryBuffer, + #[do_not_track] + summary_buffer: gtk::EntryBuffer, + #[do_not_track] + content_buffer: sourceview5::Buffer, + + #[do_not_track] + pending_tag_buffer: gtk::EntryBuffer, + #[do_not_track] + tags: relm4::factory::FactoryVecDeque<TagPill>, visibility: Visibility, - #[do_not_track] wide_layout: gtk::GridLayout, + #[do_not_track] + narrow_layout: gtk::BoxLayout, #[cfg(feature = "smart-summary")] - #[do_not_track] smart_summary: Controller<crate::components::SmartSummaryButton>, - _err: std::marker::PhantomData<E> + #[do_not_track] + smart_summary: Controller<crate::components::SmartSummaryButton>, + _err: std::marker::PhantomData<E>, } impl<E> PostEditor<E> { @@ -107,13 +133,17 @@ impl<E> PostEditor<E> { #[allow(clippy::manual_non_exhaustive)] // false positive pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> { #[cfg(feature = "smart-summary")] - #[doc(hidden)] SmartSummary(crate::components::smart_summary::Output), - #[doc(hidden)] VisibilitySelected(Visibility), - #[doc(hidden)] AddTagFromBuffer, - #[doc(hidden)] RemoveTag(DynamicIndex), + #[doc(hidden)] + SmartSummary(crate::components::smart_summary::Output), + #[doc(hidden)] + VisibilitySelected(Visibility), + #[doc(hidden)] + AddTagFromBuffer, + #[doc(hidden)] + RemoveTag(DynamicIndex), Submit, SubmitDone(glib::Uri), - SubmitError(E) + SubmitError(E), } #[relm4::component(pub)] @@ -137,20 +167,21 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post gtk::ScrolledWindow { #[name = "content"] - gtk::Box { + gtk::Grid { set_orientation: gtk::Orientation::Vertical, - set_spacing: 5, + set_column_homogeneous: false, + set_row_spacing: 10, set_margin_all: 5, #[name = "name_label"] - gtk::Label { + attach[0, 0, 1, 1] = >k::Label { set_markup: &gettext("Name"), set_margin_horizontal: 10, set_halign: gtk::Align::Start, set_valign: gtk::Align::Center, }, #[name = "name_field"] - gtk::Entry { + attach[1, 0, 1, 1] = >k::Entry { set_hexpand: true, set_buffer: &model.name_buffer, #[track = "model.changed(Self::sending())"] @@ -158,14 +189,14 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post }, #[name = "summary_label"] - gtk::Label { + attach[0, 1, 1, 1] = >k::Label { set_markup: &gettext("Summary"), set_margin_horizontal: 10, set_halign: gtk::Align::Start, set_valign: gtk::Align::Center, }, #[name = "summary_field"] - gtk::Box { + attach[1, 1, 1, 1] = >k::Box { set_orientation: gtk::Orientation::Horizontal, add_css_class: "linked", @@ -178,14 +209,14 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post }, #[name = "tag_label"] - gtk::Label { + attach[0, 2, 1, 1] = >k::Label { set_markup: &gettext("Tags"), set_margin_horizontal: 10, set_halign: gtk::Align::Start, set_valign: gtk::Align::Center, }, #[name = "tag_holder"] - gtk::Box { + attach[1, 2, 1, 1] = >k::Box { set_hexpand: true, set_orientation: gtk::Orientation::Vertical, set_spacing: 5, @@ -211,22 +242,10 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post }, }, - #[name = "tag_viewport"] - gtk::ScrolledWindow { - set_height_request: 32, - set_valign: gtk::Align::Center, - - gtk::Viewport { - set_scroll_to_focus: true, - set_valign: gtk::Align::Center, - - #[wrap(Some)] - set_child = model.tags.widget(), - } - }, + attach[1, 3, 1, 1] = model.tags.widget(), #[name = "content_label"] - gtk::Label { + attach[0, 4, 1, 1] = >k::Label { set_markup: &gettext("Content"), set_halign: gtk::Align::Start, set_valign: gtk::Align::Start, @@ -235,12 +254,15 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post }, #[name = "content_textarea_wrapper"] - gtk::ScrolledWindow { + attach[1, 4, 1, 1] = >k::ScrolledWindow { set_vexpand: true, set_height_request: 200, #[name = "content_textarea"] gtk::TextView { set_buffer: Some(&model.content_buffer), + set_extra_menu: Some(&model.spelling_adapter.menu_model()), + insert_action_group: ("spelling", Some(&model.spelling_adapter)), + set_hexpand: true, #[iterate] add_css_class: &["frame", "view"], @@ -260,7 +282,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post }, #[name = "misc_prop_wrapper"] - gtk::Box { + attach[0, 5, 2, 1] = >k::Box { set_hexpand: true, gtk::FlowBox { @@ -315,19 +337,17 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post }, }, + // We could've used AdwMultiLayoutView, but a raw + // breakpoint makes a bit more sense since we need to + // change some properties along the way. add_breakpoint = adw::Breakpoint::new( adw::BreakpointCondition::new_length( - adw::BreakpointConditionLengthType::MinWidth, + adw::BreakpointConditionLengthType::MaxWidth, 512.0, adw::LengthUnit::Px ) ) { - add_setter: (&content, "layout_manager", Some(&model.wide_layout.to_value())), - add_setter: (&name_label, "halign", Some(>k::Align::End.to_value())), - add_setter: (&summary_label, "halign", Some(>k::Align::End.to_value())), - add_setter: (&tag_label, "halign", Some(>k::Align::End.to_value())), - add_setter: (&content_label, "halign", Some(>k::Align::End.to_value())), - add_setter: (&pending_tag_entry, "hexpand", Some(&false.to_value())), + add_setter: (&content, "layout_manager", Some(&model.narrow_layout.to_value())), }, } @@ -337,34 +357,46 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post fn init( init: Self::Init, root: Self::Root, - sender: ComponentSender<Self> + sender: ComponentSender<Self>, ) -> ComponentParts<Self> { #[cfg(feature = "smart-summary")] let (http, init) = init; + let spell_checker = spelling::Checker::default(); + let content_buffer = Default::default(); + let mut model = Self { smart_summary_busy_guard: None, sending: false, + spelling_adapter: spelling::TextBufferAdapter::new(&content_buffer, &spell_checker), + spell_checker, + name_buffer: gtk::EntryBuffer::default(), summary_buffer: gtk::EntryBuffer::default(), - content_buffer: gtk::TextBuffer::default(), - pending_tag_buffer: gtk::EntryBuffer::default(), + content_buffer, + pending_tag_buffer: gtk::EntryBuffer::default(), tags: FactoryVecDeque::builder() .launch({ let listbox = gtk::Box::default(); - listbox.set_orientation(gtk::Orientation::Horizontal); - listbox.set_spacing(5); + let layout = adw::WrapLayout::builder() + .child_spacing(5) + .line_spacing(5) + .build(); + + listbox.set_layout_manager(Some(layout)); listbox }) - .forward( - sender.input_sender(), - |del: TagPillDelete| Input::RemoveTag(del.0) - ), + .forward(sender.input_sender(), |del: TagPillDelete| { + Input::RemoveTag(del.0) + }), visibility: Visibility::Public, - wide_layout: gtk::GridLayout::new(), + narrow_layout: gtk::BoxLayout::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(5) + .build(), #[cfg(feature = "smart-summary")] smart_summary: crate::components::SmartSummaryButton::builder() @@ -378,19 +410,20 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post let visibility_model = adw::EnumListModel::new(Visibility::static_type()); let widgets = view_output!(); - #[cfg(feature = "smart-summary")] widgets.summary_field.append(model.smart_summary.widget()); - widgets.visibility_selector.set_expression(Some( - gtk::ClosureExpression::new::<String>( + model.spelling_adapter.set_enabled(true); + + widgets + .visibility_selector + .set_expression(Some(gtk::ClosureExpression::new::<String>( [] as [gtk::Expression; 0], glib::closure::RustClosure::new(|v| { let list_item = v[0].get::<adw::EnumListItem>().unwrap(); Some(gettext(list_item.name().as_str()).into()) - }) - ) - )); + }), + ))); if let Some(post) = init { if let Some(name) = post.name { @@ -401,108 +434,68 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post } let mut tags = model.tags.guard(); - post.tags.into_iter().for_each(|t| { tags.push_back(t.into_boxed_str()); }); + post.tags.into_iter().for_each(|t| { + tags.push_back(t.into_boxed_str()); + }); model.content_buffer.set_text(&post.content); - widgets.visibility_selector.set_selected( - visibility_model.find_position(post.visibility.into_glib()) - ); + widgets + .visibility_selector + .set_selected(visibility_model.find_position(post.visibility.into_glib())); model.visibility = post.visibility; } - let prev_layout = widgets.content.layout_manager().unwrap(); - let layout = &model.wide_layout; - widgets.content.set_layout_manager(Some(layout.clone())); - layout.set_column_homogeneous(false); - layout.set_row_spacing(10); - - enum Row<'a> { - TwoColumn(&'a gtk::Label, &'a gtk::Widget), - Span(&'a gtk::Widget), - SecondColumn(&'a gtk::Widget) - } - - for (row, content) in [ - Row::TwoColumn(&widgets.name_label, widgets.name_field.upcast_ref::<gtk::Widget>()), - Row::TwoColumn(&widgets.summary_label, widgets.summary_field.upcast_ref::<gtk::Widget>()), - Row::TwoColumn(&widgets.tag_label, widgets.tag_holder.upcast_ref::<gtk::Widget>()), - Row::SecondColumn(widgets.tag_viewport.upcast_ref::<gtk::Widget>()), - Row::TwoColumn(&widgets.content_label, widgets.content_textarea_wrapper.upcast_ref::<gtk::Widget>()), - Row::Span(widgets.misc_prop_wrapper.upcast_ref::<gtk::Widget>()), - ].into_iter().enumerate() { - match content { - Row::TwoColumn(label, field) => { - let label_layout = layout.layout_child(label) - .downcast::<GridLayoutChild>() - .unwrap(); - label_layout.set_row(row as i32); - label_layout.set_column(0); - - let field_layout = layout.layout_child(field) - .downcast::<GridLayoutChild>() - .unwrap(); - field_layout.set_row(row as i32); - field_layout.set_column(1); - }, - Row::Span(widget) => { - let widget_layout = layout.layout_child(widget) - .downcast::<GridLayoutChild>() - .unwrap(); - widget_layout.set_row(row as i32); - widget_layout.set_column_span(2); - }, - Row::SecondColumn(widget) => { - let widget_layout = layout.layout_child(widget) - .downcast::<GridLayoutChild>() - .unwrap(); - widget_layout.set_row(row as i32); - widget_layout.set_column(1); - } - } - } - - widgets.content.set_layout_manager(Some(prev_layout)); - ComponentParts { model, widgets } } - fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) { + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + msg: Self::Input, + sender: ComponentSender<Self>, + root: &Self::Root, + ) { self.reset(); match msg { #[cfg(feature = "smart-summary")] Input::SmartSummary(crate::components::SmartSummaryOutput::Start) => { widgets.content_textarea.set_sensitive(false); if self.content_buffer.char_count() == 0 { - let _ = self.smart_summary.sender().send( - crate::components::SmartSummaryInput::Cancel - ); + let _ = self + .smart_summary + .sender() + .send(crate::components::SmartSummaryInput::Cancel); } else { let text = self.content_buffer.text( &self.content_buffer.start_iter(), &self.content_buffer.end_iter(), - false + false, ); - self.set_smart_summary_busy_guard( - Some(relm4::main_adw_application().mark_busy()) - ); - if self.smart_summary.sender().send( - crate::components::SmartSummaryInput::Text(text.into()) - ).is_ok() { + self.set_smart_summary_busy_guard(Some( + relm4::main_adw_application().mark_busy(), + )); + if self + .smart_summary + .sender() + .send(crate::components::SmartSummaryInput::Text(text.into())) + .is_ok() + { self.summary_buffer.set_text(""); } } widgets.content_textarea.set_sensitive(true); - }, + } #[cfg(feature = "smart-summary")] Input::SmartSummary(crate::components::SmartSummaryOutput::Chunk(text)) => { - self.summary_buffer.insert_text(self.summary_buffer.length(), text); - }, + self.summary_buffer + .insert_text(self.summary_buffer.length(), text); + } #[cfg(feature = "smart-summary")] Input::SmartSummary(crate::components::SmartSummaryOutput::Done) => { self.set_smart_summary_busy_guard(None); - }, + } #[cfg(feature = "smart-summary")] Input::SmartSummary(crate::components::SmartSummaryOutput::Error(err)) => { self.set_smart_summary_busy_guard(None); @@ -511,44 +504,51 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post toast.set_timeout(0); toast.set_priority(adw::ToastPriority::High); root.add_toast(toast); - }, + } Input::VisibilitySelected(vis) => { log::debug!("Changed visibility: {}", vis); self.visibility = vis; - }, + } Input::AddTagFromBuffer => { let tag = String::from(self.pending_tag_buffer.text()); if !tag.is_empty() { - self.tags.guard().push_back( - tag.into_boxed_str() - ); + self.tags.guard().push_back(tag.into_boxed_str()); self.pending_tag_buffer.set_text(""); } - }, + } Input::RemoveTag(idx) => { self.tags.guard().remove(idx.current_index()); - }, + } Input::Submit => { self.sending = true; let post = if self.content_buffer.char_count() > 0 { Some(Post { name: if self.name_buffer.length() > 0 { Some(self.name_buffer.text().into()) - } else { None }, + } else { + None + }, summary: if self.summary_buffer.length() > 0 { Some(self.summary_buffer.text().into()) - } else { None }, + } else { + None + }, tags: self.tags.iter().map(|t| t.0.clone().into()).collect(), - content: self.content_buffer.text( - &self.content_buffer.start_iter(), - &self.content_buffer.end_iter(), - false - ).into(), + content: self + .content_buffer + .text( + &self.content_buffer.start_iter(), + &self.content_buffer.end_iter(), + false, + ) + .into(), visibility: self.visibility, }) - } else { None }; + } else { + None + }; let _ = sender.output(post); - }, + } Input::SubmitDone(location) => { self.name_buffer.set_text(""); self.summary_buffer.set_text(""); @@ -560,18 +560,22 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post gtk::UriLauncher::new(&location.to_string()).launch( None::<&adw::ApplicationWindow>, None::<&gio::Cancellable>, - glib::clone!(#[weak] toast, move |result| { - if let Err(err) = result { - log::warn!("Error opening post URI: {}", err); - } else { - toast.dismiss() + glib::clone!( + #[weak] + toast, + move |result| { + if let Err(err) = result { + log::warn!("Error opening post URI: {}", err); + } else { + toast.dismiss() + } } - }) + ), ); }); root.add_toast(toast); - }, + } Input::SubmitError(err) => { let toast = adw::Toast::new(&gettext!("Error sending post: {}", err)); toast.set_timeout(0); |