use std::{path::PathBuf, process::Command, sync::Arc}; use chrono::Utc; use gtk::prelude::*; use relm4::{ component::{AsyncComponent, AsyncComponentParts, AsyncController}, gtk::{gdk, Adjustment}, prelude::*, AsyncComponentSender, }; use relm4_icons::icon_names; use crate::{ database::{self, Database}, sheet::{I64DateTime, Sheet}, sheet_dao, sheet_validation, ui::mcdu::McduOutput, }; use super::{ mcdu::McduModel, sheet_edit_dialog::{SheetEditDialogInit, SheetEditDialogModel}, sheet_listing::{SheetListingInput, SheetListingModel, SheetListingOutput}, }; pub struct AppModel { database: Arc, directory: Arc, mcdu: Controller, sheets_listing: Controller, click_mode: ClickMode, scroll_adjustment: Adjustment, sheet_edit_dialog: Option>, } #[derive(Debug)] pub enum ClickMode { Open, Edit, Annotate, } #[derive(Debug)] pub enum AppInput { SearchStarted(String), SheetPressed(Sheet), Refresh, Sort, Shuffle, SetClickMode(ClickMode), SheetListingContentsChanged, } pub struct AppInitData { pub sheets: Vec, pub database: Database, pub directory: PathBuf, } #[relm4::component(pub, async)] impl AsyncComponent for AppModel { type Input = AppInput; type Output = (); type Init = AppInitData; type CommandOutput = Vec; view! { #[root] gtk::Window{ set_default_width: 300, set_default_height: 300, set_title: Some("Play music!!!"), set_maximized: true, gtk::Box { set_orientation: gtk::Orientation::Horizontal, gtk::Box { set_orientation: gtk::Orientation::Vertical, set_hexpand: true, gtk::Box { set_orientation: gtk::Orientation::Horizontal, set_margin_all: 10, set_spacing: 3, gtk::Button { set_icon_name: icon_names::REFRESH, set_margin_end: 10, connect_clicked[sender] => move |_| sender.input(AppInput::Refresh), }, #[name = "button_sort"] gtk::ToggleButton { set_icon_name: icon_names::ARROW_SORT_REGULAR, set_active: true, connect_clicked[sender] => move |_| sender.input(AppInput::Sort), }, gtk::ToggleButton { set_icon_name: icon_names::PLAYLIST_SHUFFLE, set_group: Some(&button_sort), set_margin_end: 10, connect_clicked[sender] => move |_| sender.input(AppInput::Shuffle), }, #[name = "button_open"] gtk::ToggleButton { set_icon_name: icon_names::OPEN_FILLED, set_active: true, connect_clicked[sender] => move |button| if button.is_active() { sender.input(AppInput::SetClickMode(ClickMode::Open)) }, }, gtk::ToggleButton { set_icon_name: icon_names::DOCUMENT_SETTINGS_FILLED, set_group: Some(&button_open), connect_clicked[sender] => move |button| if button.is_active() { sender.input(AppInput::SetClickMode(ClickMode::Edit)) }, }, gtk::ToggleButton { set_icon_name: icon_names::EDIT, set_group: Some(&button_open), connect_clicked[sender] => move |button| if button.is_active() { sender.input(AppInput::SetClickMode(ClickMode::Annotate)) }, }, }, gtk::ScrolledWindow { model.sheets_listing.widget(), set_vexpand: true, set_hexpand: true, set_vadjustment: Some(&model.scroll_adjustment), }, }, model.mcdu.widget() { set_margin_all: 10, }, } } } async fn init( init_data: Self::Init, window: Self::Root, sender: AsyncComponentSender, ) -> AsyncComponentParts { relm4_icons::initialize_icons(); gtk::init().unwrap(); let display = gdk::Display::default().unwrap(); let theme = gtk::IconTheme::for_display(&display); theme.add_resource_path("/org/gtkrs/icons/"); // theme.add_resource_path("/org/gtkrs/icons/scalable/actions/"); let mcdu = McduModel::builder() .launch(()) .forward(sender.input_sender(), |response| match response { McduOutput::SearchStarted(query) => AppInput::SearchStarted(query), }); let mut sheets = init_data.sheets; sheets.sort_by(|a, b| a.cmp(b).reverse()); let sheets_listing = SheetListingModel::builder().launch(sheets).forward( sender.input_sender(), |response| match response { SheetListingOutput::SheetModelSelected(sheet) => AppInput::SheetPressed(sheet), SheetListingOutput::ContentsChanged => AppInput::SheetListingContentsChanged, }, ); let model = AppModel { database: Arc::new(init_data.database), directory: Arc::new(init_data.directory), mcdu, sheets_listing, click_mode: ClickMode::Open, scroll_adjustment: Adjustment::builder().build(), sheet_edit_dialog: None, }; let widgets = view_output!(); AsyncComponentParts { model, widgets } } async fn update( &mut self, message: Self::Input, sender: AsyncComponentSender, root: &Self::Root, ) { match message { AppInput::SearchStarted(query) => { self.sheets_listing .emit(SheetListingInput::Query(query.clone())); } AppInput::SheetPressed(sheet) => { match self.click_mode { ClickMode::Open => open_sheet(&sheet, &self.database).await, ClickMode::Edit => { self.sheet_edit_dialog = Some( SheetEditDialogModel::builder() .transient_for(root) .launch(SheetEditDialogInit { sheet, database: Arc::clone(&self.database), }) .forward(sender.input_sender(), |_| todo!()), ); } ClickMode::Annotate => annotate_sheet(&sheet).await, }; } AppInput::Refresh => { let db = Arc::clone(&self.database); let dir = Arc::clone(&self.directory); sender.oneshot_command(async move { sheet_validation::load_and_validate_sheets(&db, dir.as_ref()).await }); } AppInput::Sort => self.sheets_listing.emit(SheetListingInput::Sort), AppInput::Shuffle => self.sheets_listing.emit(SheetListingInput::Shuffle), AppInput::SetClickMode(click_mode) => self.click_mode = click_mode, AppInput::SheetListingContentsChanged => self.scroll_adjustment.set_value(0.0), } } async fn update_cmd( &mut self, message: Self::CommandOutput, _sender: AsyncComponentSender, _: &Self::Root, ) { let mut sheets = message; sheets.sort_by(|a, b| a.cmp(b).reverse()); self.sheets_listing .emit(SheetListingInput::ReloadSheets(sheets)); } } async fn open_sheet(sheet: &Sheet, database: &Database) { sheet.open_file_or_annotated_version_if_exists(); let mut sheet = sheet.to_owned(); sheet.last_opened = I64DateTime(Utc::now()); sheet_dao::update_sheet_last_opened(database, &sheet) .await .unwrap(); } async fn annotate_sheet(sheet: &Sheet) { Command::new("xournalpp") .arg(&sheet.pdf.path) .spawn() .expect("failed to execute process"); }