use std::{borrow::Borrow, sync::Arc}; use adw::prelude::*; 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 }; pub(crate) mod tag_pill; pub(crate) use tag_pill::{TagPill, TagPillDelete}; } use components::post_editor::Post; pub mod secrets; pub mod micropub; pub mod util; pub const APPLICATION_ID: &str = "xyz.vikanezrimaya.kittybox.Bowl"; pub const VISIBILITY: [&str; 2] = ["public", "private"]; #[derive(Debug)] pub struct App { state: AuthState } #[derive(Debug)] enum AuthState { LoggedOut(gtk::EntryBuffer), LoggedIn { submit_busy_guard: Option, post_editor: Controller>, micropub: micropub::Client } } #[derive(Debug)] #[doc(hidden)] pub enum Input { SubmitButtonPressed, Authorize, PostEditor(Option) } #[derive(Default, Debug)] pub struct AppRootWidgets { root: adw::ApplicationWindow, toolbar_view: adw::ToolbarView, top_bar: adw::HeaderBar, top_bar_btn: gtk::Button, login_box: gtk::Box, login_label: gtk::Label, login_button: gtk::Button, } //#[relm4::component(pub async)] impl AsyncComponent for App { /// The type of the messages that this component can receive. 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 = (); /// The type of the command outputs that this component can receive. type CommandOutput = (); type Widgets = AppRootWidgets; type Root = adw::ApplicationWindow; fn init_root() -> Self::Root { let window = Self::Root::default(); window.set_size_request(360, 294); #[cfg(debug_assertions)] window.add_css_class("devel"); window } /// Initialize the UI and model. async fn init( _init: Self::Init, window: Self::Root, sender: AsyncComponentSender, ) -> AsyncComponentParts { let model = App { state: AuthState::LoggedOut(Default::default()) }; let mut widgets = Self::Widgets { root: window, ..Self::Widgets::default() }; widgets.toolbar_view.add_top_bar(&widgets.top_bar); widgets.top_bar.pack_end(&widgets.top_bar_btn); widgets.top_bar_btn.set_icon_name("document-send-symbolic"); widgets.top_bar_btn.set_tooltip("Send post"); widgets.top_bar_btn.connect_clicked(glib::clone!( #[strong] sender, move |_button| sender.input(Self::Input::SubmitButtonPressed) )); widgets.login_box.set_orientation(gtk::Orientation::Vertical); widgets.login_box.append(&widgets.login_label); widgets.login_box.append(&widgets.login_button); widgets.login_label.set_text("You need to authorize first."); widgets.login_button.set_label("Pretend to authorize"); widgets.login_button.set_tooltip("(check MICROPUB_URI and MICROPUB_TOKEN environment variables)"); widgets.login_button.connect_clicked(glib::clone!( #[strong] sender, move |_button| sender.input(Self::Input::Authorize) )); widgets.root.set_content(Some(&widgets.toolbar_view)); // Separate component choosing logic from initialization. We // already have all the parts here, might as well use them. model.update_view(&mut widgets, sender); AsyncComponentParts { model, widgets } } fn update_view(&self, widgets: &mut Self::Widgets, _sender: AsyncComponentSender) { // Bind the child component, if any, here. match &self.state { AuthState::LoggedOut(_entry_buffer) => { widgets.root.set_title(Some("Sign in with your website")); widgets.toolbar_view.set_content(None::<>k::Box>); widgets.top_bar_btn.set_visible(false); widgets.toolbar_view.set_content(Some(&widgets.login_box)); }, AuthState::LoggedIn { post_editor, submit_busy_guard, .. } => { widgets.root.set_title(Some("Create post")); widgets.toolbar_view.set_content(Some(post_editor.widget())); widgets.top_bar_btn.set_sensitive(submit_busy_guard.is_none()); widgets.top_bar_btn.set_visible(true); } } } async fn update( &mut self, message: Self::Input, _sender: AsyncComponentSender, _root: &Self::Root ) { match message { Input::Authorize => { self.state = AuthState::LoggedIn { post_editor: components::PostEditor::builder() .launch(None) .forward(_sender.clone().input_sender(), Self::Input::PostEditor), micropub: std::env::var("MICROPUB_TOKEN").ok() .and_then(|token| { Some((token, glib::Uri::parse( &std::env::var("MICROPUB_URI").ok()?, glib::UriFlags::NONE ).ok()?)) }) .map(|(token, uri)| crate::micropub::Client::new( uri, token )) .unwrap(), submit_busy_guard: None }; }, Input::SubmitButtonPressed => { if let AuthState::LoggedIn { ref mut submit_busy_guard, ref post_editor, .. } = &mut self.state { *submit_busy_guard = Some(relm4::main_adw_application().mark_busy()); post_editor.sender().emit(components::PostEditorInput::Submit); }; }, Input::PostEditor(None) => { if let AuthState::LoggedIn { ref mut submit_busy_guard, .. } = &mut self.state { *submit_busy_guard = None; } } Input::PostEditor(Some(post)) => { if let AuthState::LoggedIn { ref mut submit_busy_guard, ref post_editor, ref micropub, } = &mut self.state { let mf2 = post.into(); log::debug!("Submitting post: {:#}", serde_json::to_string(&mf2).unwrap()); match micropub.send_post(mf2).await { Ok(uri) => { post_editor.sender() .emit(components::PostEditorInput::SubmitDone(uri)); }, Err(err) => { log::warn!("Error sending post: {}", err); post_editor.sender() .emit(components::PostEditorInput::SubmitError(err)); } } *submit_busy_guard = None; } }, } } }