summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs413
1 files changed, 46 insertions, 367 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 87eed10..a9335bc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,16 +1,22 @@
 use std::sync::Arc;
 
 use adw::prelude::*;
-use gtk::GridLayoutChild;
-use relm4::{gtk, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
+use relm4::{gtk, prelude::{AsyncComponent, AsyncComponentParts, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
 
 pub mod components {
     pub(crate) mod smart_summary;
     pub(crate) use smart_summary::{
         SmartSummaryButton, Output as SmartSummaryOutput, Input as SmartSummaryInput
     };
+
+    pub(crate) mod post_editor;
+    pub(crate) use post_editor::{
+        PostEditor, Input as PostEditorInput
+    };
 }
-mod widgets;
+
+use components::post_editor::{Post, Visibility};
+
 pub mod secrets;
 pub mod micropub;
 pub mod util;
@@ -18,56 +24,31 @@ pub const APPLICATION_ID: &str = "xyz.vikanezrimaya.kittybox.Bowl";
 
 pub const VISIBILITY: [&str; 2] = ["public", "private"];
 
-#[tracker::track]
 #[derive(Debug)]
-pub struct PostComposerModel {
-    /// Busy guard for generating the summary using an LLM.
-    /// Makes the summary field read-only and blocks the Smart Summary button.
-    #[no_eq] smart_summary_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
-    #[no_eq] submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
-
-    #[do_not_track] name_buffer: gtk::EntryBuffer,
-    #[do_not_track] summary_buffer: gtk::EntryBuffer,
-    #[do_not_track] content_buffer: gtk::TextBuffer,
-
-    #[do_not_track] wide_layout: gtk::GridLayout,
-    #[do_not_track] narrow_layout: gtk::BoxLayout,
-
-    #[do_not_track] micropub: Arc<micropub::Client>,
-
-    #[do_not_track] smart_summary: Controller<components::SmartSummaryButton>,
-}
+pub struct App {
+    micropub: Arc<micropub::Client>,
+    submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
 
-impl PostComposerModel {
-    fn busy_changed(&self) -> bool {
-        self.changed(Self::submit_busy_guard() | Self::smart_summary_busy_guard())
-    }
-    fn busy(&self) -> bool {
-        self.submit_busy_guard.is_some() || self.smart_summary_busy_guard.is_some()
-    }
-}
-
-#[derive(Debug)]
-#[allow(private_interfaces)]
-pub enum PostComposerInput {
-    #[doc(hidden)] SmartSummary(components::smart_summary::Output),
-    Submit,
+    post_editor: Controller<components::PostEditor<micropub::Error>>
 }
 
 #[derive(Debug)]
-pub enum PostComposerCommandOutput {
+#[doc(hidden)]
+pub enum Input {
+    SubmitButtonPressed,
+    PostEditor(Option<Post>)
 }
 
 #[relm4::component(pub async)]
-impl AsyncComponent for PostComposerModel {
+impl AsyncComponent for App {
     /// The type of the messages that this component can receive.
-    type Input = PostComposerInput;
+    type Input = Input;
     /// The type of the messages that this component can send.
     type Output = ();
     /// The type of data with which this component will be initialized.
     type Init = micropub::Client;
     /// The type of the command outputs that this component can receive.
-    type CommandOutput = PostComposerCommandOutput;
+    type CommandOutput = ();
  
     view! {
         #[root]
@@ -83,9 +64,9 @@ impl AsyncComponent for PostComposerModel {
                             set_icon_name: "document-send-symbolic",
                             set_tooltip: "Send post",
 
-                            connect_clicked => Self::Input::Submit,
-                            #[track = "model.busy_changed()"]
-                            set_sensitive: !model.busy(),
+                            connect_clicked => Self::Input::SubmitButtonPressed,
+                            #[watch]
+                            set_sensitive: model.submit_busy_guard.is_none(),
                         },
 
                         bar = adw::HeaderBar::new() {
@@ -95,163 +76,8 @@ impl AsyncComponent for PostComposerModel {
 
                     bar
                 },
-                #[name = "toast_overlay"]
-                adw::ToastOverlay {
-                    #[name = "content_wrapper"]
-                    adw::BreakpointBin {
-                        set_width_request: 360,
-                        set_height_request: 480,
-
-                        #[name = "content"]
-                        gtk::Box {
-                            set_orientation: gtk::Orientation::Vertical,
-                            set_spacing: 5,
-                            set_margin_all: 5,
-
-                            #[name = "name_label"]
-                            gtk::Label {
-                                set_markup: "Name",
-                                set_margin_vertical: 10,
-                                set_margin_horizontal: 10,
-                                set_halign: gtk::Align::Start,
-                                set_valign: gtk::Align::Start,
-                            },
-                            #[name = "name_field"]
-                            gtk::Entry {
-                                set_hexpand: true,
-                                set_buffer: &model.name_buffer,
-                                #[track = "model.changed(Self::submit_busy_guard())"]
-                                set_sensitive: model.submit_busy_guard.is_none(),
-                            },
-
-                            #[name = "summary_label"]
-                            gtk::Label {
-                                set_markup: "Summary",
-                                set_margin_vertical: 10,
-                                set_margin_horizontal: 10,
-                                set_halign: gtk::Align::Start,
-                                set_valign: gtk::Align::Start,
-                            },
-                            #[name = "summary_field"]
-                            gtk::Box {
-                                set_orientation: gtk::Orientation::Horizontal,
-                                add_css_class: "linked",
-
-                                gtk::Entry {
-                                    set_hexpand: true,
-                                    set_buffer: &model.summary_buffer,
-                                    #[track = "model.busy_changed()"]
-                                    set_sensitive: !model.busy(),
-                                },
 
-                                model.smart_summary.widget(),
-                            },
-
-                            #[name = "tag_label"]
-                            gtk::Label {
-                                set_markup: "Tags",
-                                set_margin_vertical: 10,
-                                set_margin_horizontal: 10,
-                                set_halign: gtk::Align::Start,
-                                set_valign: gtk::Align::Start,
-                            },
-                            #[name = "tag_holder"]
-                            // TODO: tag component (because of complex logic)
-                            gtk::Box {
-                                add_css_class: "frame",
-                                set_hexpand: true,
-                                set_orientation: gtk::Orientation::Horizontal,
-                                set_spacing: 5,
-                                set_height_request: 36,
-                            },
-
-
-                            #[name = "content_label"]
-                            gtk::Label {
-                                set_markup: "Content",
-                                set_halign: gtk::Align::Start,
-                                set_valign: gtk::Align::Start,
-                                set_margin_vertical: 10,
-                                set_margin_horizontal: 10,
-                            },
-
-                            #[name = "content_textarea"]
-                            gtk::ScrolledWindow {
-                                set_vexpand: true,
-
-                                gtk::TextView {
-                                    set_buffer: Some(&model.content_buffer),
-                                    set_hexpand: true,
-                                    #[iterate]
-                                    add_css_class: &["frame", "view"],
-
-                                    set_monospace: true,
-                                    set_wrap_mode: gtk::WrapMode::Word,
-                                    set_vscroll_policy: gtk::ScrollablePolicy::Natural,
-
-                                    set_left_margin: 8,
-                                    set_right_margin: 8,
-                                    set_top_margin: 8,
-                                    set_bottom_margin: 8,
-
-                                    #[track = "model.changed(Self::submit_busy_guard())"]
-                                    set_sensitive: model.submit_busy_guard.is_none(),
-                                }
-                            },
-
-                            #[name = "misc_prop_wrapper"]
-                            gtk::Box {
-                                set_hexpand: true,
-
-                                gtk::FlowBox {
-                                    set_hexpand: false,
-                                    set_orientation: gtk::Orientation::Horizontal,
-                                    set_homogeneous: false,
-                                    set_column_spacing: 0,
-                                    set_min_children_per_line: 2,
-                                    set_max_children_per_line: 6,
-                                    set_selection_mode: gtk::SelectionMode::None,
-
-                                    append = &gtk::Box {
-                                        set_spacing: 5,
-
-                                        #[name = "visibility_label"]
-                                        gtk::Label {
-                                            set_markup: "Visibility",
-                                            set_halign: gtk::Align::Start,
-                                            set_valign: gtk::Align::Start,
-                                            set_margin_vertical: 10,
-                                            set_margin_horizontal: 10,
-                                        },
-                                        #[name = "visibility_selector"]
-                                        gtk::DropDown {
-                                            set_model: Some(&gtk::StringList::new(&VISIBILITY)),
-                                            set_hexpand: false,
-
-                                            #[track = "model.changed(Self::submit_busy_guard())"]
-                                            set_sensitive: model.submit_busy_guard.is_none(),
-                                        },
-                                    },
-                                },
-                            },
-                        },
-
-                        add_breakpoint = adw::Breakpoint::new(
-                            adw::BreakpointCondition::new_length(
-                                adw::BreakpointConditionLengthType::MinWidth,
-                                512.0,
-                                adw::LengthUnit::Px
-                            )
-                        ) {
-                            add_setter: (&content, "layout_manager", Some(&model.wide_layout.to_value())),
-                            add_setter: (&name_label, "halign", Some(&gtk::Align::End.to_value())),
-                            add_setter: (&summary_label, "halign", Some(&gtk::Align::End.to_value())),
-                            add_setter: (&tag_label, "halign", Some(&gtk::Align::End.to_value())),
-                            add_setter: (&content_label, "halign", Some(&gtk::Align::End.to_value())),
-                        },
-
-                    }
-                }
+                model.post_editor.widget(),
             }
         }
                 
@@ -262,24 +88,12 @@ impl AsyncComponent for PostComposerModel {
         window: Self::Root,
         sender: AsyncComponentSender<Self>,
     ) -> AsyncComponentParts<Self> {
-        let content_buffer = gtk::TextBuffer::default();
-        let model = PostComposerModel {
-            smart_summary_busy_guard: None,
+        let model = App {
             submit_busy_guard: None,
-
-            name_buffer: gtk::EntryBuffer::default(),
-            summary_buffer: gtk::EntryBuffer::default(),
-            content_buffer: content_buffer.clone(),
-
-            wide_layout: gtk::GridLayout::new(),
-            narrow_layout: gtk::BoxLayout::new(gtk::Orientation::Vertical),
-
             micropub: Arc::new(init),
-            smart_summary: components::SmartSummaryButton::builder()
-                .launch(())
-                .forward(sender.input_sender(), PostComposerInput::SmartSummary),
-
-            tracker: Default::default()
+            post_editor: components::PostEditor::builder()
+                .launch(None)
+                .forward(sender.input_sender(), Self::Input::PostEditor),
         };
 
         let widgets = view_output!();
@@ -287,176 +101,41 @@ impl AsyncComponent for PostComposerModel {
         #[cfg(debug_assertions)]
         window.add_css_class("devel");
 
-        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)
-        }
-
-        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::TwoColumn(&widgets.content_label, widgets.content_textarea.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);
-                }
-            }
-        }
-
-        widgets.content.set_layout_manager(Some(model.narrow_layout.clone()));
-
         AsyncComponentParts { model, widgets }
     }
 
-    async fn update_with_view(
+    async fn update(
         &mut self,
-        widgets: &mut Self::Widgets,
         message: Self::Input,
-        sender: AsyncComponentSender<Self>,
-        root: &Self::Root
+        _sender: AsyncComponentSender<Self>,
+        _root: &Self::Root
     ) {
-        self.reset(); // Reset the tracker
-
         match message {
-            PostComposerInput::SmartSummary(components::SmartSummaryOutput::Start) => {
-                widgets.content_textarea.set_sensitive(false);
-                if self.content_buffer.char_count() == 0 {
-                    let _ = self.smart_summary.sender().send(
-                        components::SmartSummaryInput::Cancel
-                    );
-                } else {
-                    let text = self.content_buffer.text(
-                        &self.content_buffer.start_iter(),
-                        &self.content_buffer.end_iter(),
-                        false
-                    );
-
-                    self.set_smart_summary_busy_guard(
-                        Some(relm4::main_adw_application().mark_busy())
-                    );
-                    if self.smart_summary.sender().send(
-                        components::SmartSummaryInput::Text(text.into())
-                    ).is_ok() {
-                        self.summary_buffer.set_text("");
-                    }
-                }
-                widgets.content_textarea.set_sensitive(true);
-            },
-            PostComposerInput::SmartSummary(components::SmartSummaryOutput::Chunk(text)) => {
-                self.summary_buffer.insert_text(self.summary_buffer.length(), text);
+            Input::SubmitButtonPressed => {
+                self.submit_busy_guard = Some(relm4::main_adw_application().mark_busy());
+                self.post_editor.sender().send(components::PostEditorInput::Submit).unwrap();
             },
-            PostComposerInput::SmartSummary(components::SmartSummaryOutput::Done) => {
-                self.set_smart_summary_busy_guard(None);
+            Input::PostEditor(None) => {
+                self.submit_busy_guard = None;
             }
-            PostComposerInput::SmartSummary(components::SmartSummaryOutput::Error(err)) => {
-                self.set_smart_summary_busy_guard(None);
-
-                let toast = adw::Toast::new(&format!("Smart Summary error: {}", err));
-                toast.set_timeout(0);
-                toast.set_priority(adw::ToastPriority::High);
-                widgets.toast_overlay.add_toast(toast);
-
-            },
-            PostComposerInput::Submit => {
-                if self.content_buffer.char_count() == 0 {
-                    self.update_view(widgets, sender);
-                    return
-                }
-                self.set_submit_busy_guard(
-                    Some(relm4::main_adw_application().mark_busy())
-                );
-                // Update view to lock the interface up
-                self.update_view(widgets, sender.clone());
-                self.reset();
-
-                use microformats::types::{Item, Class, KnownClass, PropertyValue};
-                let mut mf2 = Item::new(vec![Class::Known(KnownClass::Entry)]);
-                if self.name_buffer.length() > 0 {
-                    let proplist = mf2.properties.entry("name".to_owned()).or_default();
-                    proplist.push(PropertyValue::Plain(self.name_buffer.text().into()));
-                }
-                if self.summary_buffer.length() > 0 {
-                    let proplist = mf2.properties.entry("summary".to_owned()).or_default();
-                    proplist.push(PropertyValue::Plain(self.summary_buffer.text().into()));
-                }
-
-                // TODO: tags
-
-                {
-                    let proplist = mf2.properties.entry("content".to_owned()).or_default();
-                    proplist.push(PropertyValue::Plain(self.content_buffer.text(
-                        &self.content_buffer.start_iter(),
-                        &self.content_buffer.end_iter(),
-                        false
-                    ).into()));
-                }
-
-                {
-                    let proplist = mf2.properties.entry("visibility".to_owned()).or_default();
-                    let selected = VISIBILITY[widgets.visibility_selector.selected() as usize];
-                    proplist.push(PropertyValue::Plain(selected.to_owned()));
-                }
-
+            Input::PostEditor(Some(post)) => {
+                let mf2 = post.into();
+                log::debug!("Submitting post: {:#}", serde_json::to_string(&mf2).unwrap());
                 match self.micropub.send_post(mf2).await {
                     Ok(location) => {
-                        self.name_buffer.set_text("");
-                        self.summary_buffer.set_text("");
-                        // TODO: tags
-                        self.content_buffer.set_text("");
-                        let toast = adw::Toast::new("Post submitted");
-                        toast.set_button_label(Some("Open"));
-                        toast.connect_button_clicked(glib::clone!(#[strong] root, move |toast| {
-                            gtk::UriLauncher::new(&location.to_string()).launch(
-                                Some(root.upcast_ref::<gtk::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()
-                                    }
-                                })
-                            );
-                        }));
-                        widgets.toast_overlay.add_toast(toast);
+                        self.post_editor.sender()
+                            .send(components::PostEditorInput::SubmitDone(location))
+                            .unwrap();
                     },
                     Err(err) => {
                         log::warn!("Error sending post: {}", err);
-                        let toast = adw::Toast::new(&format!("Error sending post: {}", err));
-                        toast.set_timeout(0);
-                        toast.set_priority(adw::ToastPriority::High);
-                        widgets.toast_overlay.add_toast(toast);
+                        self.post_editor.sender()
+                            .send(components::PostEditorInput::SubmitError(err))
+                            .unwrap();
                     }
                 }
-                self.set_submit_busy_guard(None);
+                self.submit_busy_guard = None;
             },
         }
-
-        self.update_view(widgets, sender);
     }
 }