From 9017ad84d6f83dd4ff1cbacefd9c6dac60c026de Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Mon, 4 Nov 2024 11:01:18 +0000 Subject: [PATCH] improv: message handling --- Cargo.lock | 80 ++--- src/app.rs | 853 +++++++++++++++++++++++++++-------------------------- 2 files changed, 479 insertions(+), 454 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9301ff1..153415d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -542,7 +542,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -764,7 +764,7 @@ checksum = "369cfaf2a5bed5d8f8202073b2e093c9f508251de1551a0deb4253e4c7d80909" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1311,7 +1311,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1322,7 +1322,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1395,7 +1395,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1443,7 +1443,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1583,7 +1583,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1939,7 +1939,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -2094,7 +2094,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -2432,7 +2432,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.66", + "syn 2.0.87", "unic-langid", ] @@ -2446,7 +2446,7 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3545,7 +3545,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3882,7 +3882,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3916,7 +3916,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -4031,7 +4031,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -4066,7 +4066,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -4226,7 +4226,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "version_check", "yansi", ] @@ -4507,7 +4507,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.66", + "syn 2.0.87", "walkdir", ] @@ -4671,7 +4671,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -4694,7 +4694,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -4994,7 +4994,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -5015,7 +5015,7 @@ dependencies = [ "sha2", "sqlx-core", "sqlx-sqlite", - "syn 2.0.66", + "syn 2.0.87", "tempfile", "url", ] @@ -5116,9 +5116,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -5215,22 +5215,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -5376,7 +5376,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -5475,7 +5475,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -5780,7 +5780,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -5814,7 +5814,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6250,7 +6250,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -6261,7 +6261,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -6805,7 +6805,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "zvariant_utils 2.0.0", ] @@ -6854,7 +6854,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -6916,7 +6916,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "zvariant_utils 2.0.0", ] @@ -6939,5 +6939,5 @@ checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] diff --git a/src/app.rs b/src/app.rs index aabe11b..8421ed9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -56,31 +56,41 @@ pub struct Tasks { #[derive(Debug, Clone)] pub enum Message { + Tasks(TasksAction), Content(content::Message), Details(details::Message), + Dialog(DialogAction), ToggleContextPage(ContextPage), - LaunchUrl(String), - FetchLists, + Application(ApplicationAction), +} + +#[derive(Debug, Clone)] +pub enum DialogAction { + Open(DialogPage), + Update(DialogPage), + Close, + Complete, +} + +#[derive(Debug, Clone)] +pub enum TasksAction { PopulateLists(Vec), + Export(Vec), + AddList(List), + DeleteList, + FetchLists, +} + +#[derive(Debug, Clone)] +pub enum ApplicationAction { + LaunchUrl(String), WindowClose, WindowNew, - DialogCancel, - DialogComplete, - DialogUpdate(DialogPage), Key(Modifiers, Key), Modifiers(Modifiers), AppTheme(usize), SystemThemeModeChange, - OpenNewListDialog, - OpenRenameListDialog, - OpenDeleteListDialog, - OpenIconDialog, - OpenCalendarDialog, - OpenExportDialog(String), - AddList(List), - DeleteList, Focus(widget::Id), - Export(Vec), NavMenuAction(NavMenuAction), } @@ -105,7 +115,7 @@ impl ContextPage { pub enum DialogPage { New(String), Icon(String), - Rename { to: String }, + Rename(String), Delete, Calendar(NaiveDate), Export(String), @@ -135,12 +145,14 @@ impl MenuAction for Action { match self { Action::About => Message::ToggleContextPage(ContextPage::About), Action::Settings => Message::ToggleContextPage(ContextPage::Settings), - Action::WindowClose => Message::WindowClose, - Action::WindowNew => Message::WindowNew, - Action::NewList => Message::OpenNewListDialog, - Action::Icon => Message::OpenIconDialog, - Action::RenameList => Message::OpenRenameListDialog, - Action::DeleteList => Message::OpenDeleteListDialog, + Action::WindowClose => Message::Application(ApplicationAction::WindowClose), + Action::WindowNew => Message::Application(ApplicationAction::WindowNew), + Action::NewList => Message::Dialog(DialogAction::Open(DialogPage::New(String::new()))), + Action::Icon => Message::Dialog(DialogAction::Open(DialogPage::Icon(String::new()))), + Action::RenameList => { + Message::Dialog(DialogAction::Open(DialogPage::Rename(String::new()))) + } + Action::DeleteList => Message::Dialog(DialogAction::Open(DialogPage::Delete)), } } } @@ -156,7 +168,9 @@ impl MenuAction for NavMenuAction { type Message = cosmic::app::Message; fn message(&self) -> Self::Message { - cosmic::app::Message::App(Message::NavMenuAction(*self)) + cosmic::app::Message::App(Message::Application(ApplicationAction::NavMenuAction( + *self, + ))) } } @@ -178,7 +192,9 @@ impl Tasks { .into(), widget::text::title3(fl!("tasks")).into(), widget::button::link(repository) - .on_press(Message::LaunchUrl(repository.to_string())) + .on_press(Message::Application(ApplicationAction::LaunchUrl( + repository.to_string(), + ))) .padding(spacing.space_none) .into(), widget::button::link(fl!( @@ -186,7 +202,9 @@ impl Tasks { hash = short_hash.as_str(), date = date )) - .on_press(Message::LaunchUrl(format!("{repository}/commits/{hash}"))) + .on_press(Message::Application(ApplicationAction::LaunchUrl(format!( + "{repository}/commits/{hash}" + )))) .padding(spacing.space_none) .into(), ]) @@ -208,7 +226,7 @@ impl Tasks { widget::settings::item::builder(fl!("theme")).control(widget::dropdown( &self.app_themes, Some(app_theme_selected), - Message::AppTheme, + |index| Message::Application(ApplicationAction::AppTheme(index)), )), ) .into()]) @@ -264,7 +282,7 @@ impl Application for Tasks { }; let mut commands = vec![Command::perform(TaskService::migrate(Self::APP_ID), |_| { - message::app(Message::FetchLists) + message::app(Message::Tasks(TasksAction::FetchLists)) })]; if let Some(id) = app.core.main_window_id() { @@ -286,153 +304,10 @@ impl Application for Tasks { }) } - fn dialog(&self) -> Option> { - let dialog_page = self.dialog_pages.front()?; - - let spacing = theme::active().cosmic().spacing; - - let dialog = match dialog_page { - DialogPage::New(name) => widget::dialog(fl!("create-list")) - .primary_action( - widget::button::suggested(fl!("save")) - .on_press_maybe(Some(Message::DialogComplete)), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ) - .control( - widget::column::with_children(vec![ - widget::text::body(fl!("list-name")).into(), - widget::text_input("", name.as_str()) - .id(self.dialog_text_input.clone()) - .on_input(move |name| Message::DialogUpdate(DialogPage::New(name))) - .on_submit(Message::DialogComplete) - .into(), - ]) - .spacing(spacing.space_xxs), - ), - DialogPage::Rename { to: name } => widget::dialog(fl!("rename-list")) - .primary_action( - widget::button::suggested(fl!("save")) - .on_press_maybe(Some(Message::DialogComplete)), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ) - .control( - widget::column::with_children(vec![ - widget::text::body(fl!("list-name")).into(), - widget::text_input("", name.as_str()) - .id(self.dialog_text_input.clone()) - .on_input(move |name| { - Message::DialogUpdate(DialogPage::Rename { to: name }) - }) - .on_submit(Message::DialogComplete) - .into(), - ]) - .spacing(spacing.space_xxs), - ), - DialogPage::Delete => widget::dialog(fl!("delete-list")) - .body(fl!("delete-list-confirm")) - .primary_action( - widget::button::suggested(fl!("ok")) - .on_press_maybe(Some(Message::DialogComplete)), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ), - DialogPage::Icon(icon) => { - let icon_buttons: Vec> = emojis::iter() - .map(|emoji| { - widget::button::custom( - widget::container(widget::text(emoji.to_string())) - .width(spacing.space_l) - .height(spacing.space_l) - .align_y(Vertical::Center) - .align_x(Horizontal::Center), - ) - .on_press(Message::DialogUpdate(DialogPage::Icon(emoji.to_string()))) - .into() - }) - .collect(); - let mut dialog = widget::dialog(fl!("icon-select")) - .body(fl!("icon-select-body")) - .primary_action( - widget::button::suggested(fl!("ok")) - .on_press_maybe(Some(Message::DialogComplete)), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ) - .control( - widget::container(scrollable(widget::row::with_children(vec![ - widget::flex_row(icon_buttons).into(), - horizontal_space().into(), - ]))) - .height(Length::Fixed(300.0)), - ); - - if !icon.is_empty() { - dialog = dialog.icon(widget::container( - widget::text(icon.as_str()).size(spacing.space_l), - )); - } - - dialog - } - DialogPage::Calendar(date) => { - let dialog = widget::dialog(fl!("select-date")) - .primary_action( - widget::button::suggested(fl!("ok")) - .on_press_maybe(Some(Message::DialogComplete)), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ) - .control( - widget::container(widget::calendar(date, |date| { - Message::DialogUpdate(DialogPage::Calendar(date)) - })) - .width(Length::Fill) - .align_x(Horizontal::Center) - .align_y(Vertical::Center), - ); - dialog - } - DialogPage::Export(contents) => { - let dialog = widget::dialog(fl!("export")) - .control( - widget::container(scrollable(widget::text(contents)).width(Length::Fill)) - .height(Length::Fixed(200.0)) - .width(Length::Fill), - ) - .primary_action( - widget::button::suggested(fl!("copy")) - .on_press_maybe(Some(Message::DialogComplete)), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ); - - dialog - } - }; - - Some(dialog.into()) - } - fn header_start(&self) -> Vec> { vec![menu::menu_bar(&self.key_binds)] } - fn header_center(&self) -> Vec> { - vec![] - } - - fn header_end(&self) -> Vec> { - vec![] - } - fn nav_context_menu( &self, id: widget::nav_bar::Id, @@ -478,57 +353,6 @@ impl Application for Tasks { Command::batch(commands) } - fn subscription(&self) -> Subscription { - struct ConfigSubscription; - struct ThemeSubscription; - - let mut subscriptions = vec![ - event::listen_with(|event, _status, _window_id| match event { - Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => { - Some(Message::Key(modifiers, key)) - } - Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { - Some(Message::Modifiers(modifiers)) - } - _ => None, - }), - cosmic_config::config_subscription( - TypeId::of::(), - Self::APP_ID.into(), - CONFIG_VERSION, - ) - .map(|update: Update| { - if !update.errors.is_empty() { - log::info!( - "errors loading config {:?}: {:?}", - update.keys, - update.errors - ); - } - Message::SystemThemeModeChange - }), - cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( - TypeId::of::(), - cosmic_theme::THEME_MODE_ID.into(), - cosmic_theme::ThemeMode::version(), - ) - .map(|update: Update| { - if !update.errors.is_empty() { - log::info!( - "errors loading theme mode {:?}: {:?}", - update.keys, - update.errors - ); - } - Message::SystemThemeModeChange - }), - ]; - - subscriptions.push(self.content.subscription().map(Message::Content)); - - Subscription::batch(subscriptions) - } - fn update(&mut self, message: Self::Message) -> Command> { // Helper for updating config values efficiently macro_rules! config_set { @@ -606,8 +430,7 @@ impl Application for Tasks { commands.push(command); } content::Command::Delete(id) => { - if let Some(list) = self.nav_model.data::(self.nav_model.active()) - { + if let Some(list) = self.nav_model.active_data::() { let command = Command::perform( todo::delete_task( list.id().clone(), @@ -631,7 +454,7 @@ impl Application for Tasks { commands.push(command); } content::Command::Export(tasks) => { - commands.push(self.update(Message::Export(tasks))); + commands.push(self.update(Message::Tasks(TasksAction::Export(tasks)))); } } } @@ -646,32 +469,19 @@ impl Application for Tasks { ))); } details::Command::OpenCalendarDialog => { - commands.push(self.update(Message::OpenCalendarDialog)); + commands.push(self.update(Message::Dialog(DialogAction::Open( + DialogPage::Calendar(Local::now().date_naive()), + )))); } details::Command::Focus(id) => { - commands.push(self.update(Message::Focus(id))); + commands.push( + self.update(Message::Application(ApplicationAction::Focus(id))), + ); } details::Command::Iced(command) => return command, } } } - Message::NavMenuAction(action) => match action { - NavMenuAction::Rename(entity) => { - if self.nav_model.data::(entity).is_some() { - commands.push(self.update(Message::OpenRenameListDialog)); - } - } - NavMenuAction::SetIcon(entity) => { - if self.nav_model.data::(entity).is_some() { - commands.push(self.update(Message::OpenIconDialog)); - } - } - NavMenuAction::Delete(entity) => { - if self.nav_model.data::(entity).is_some() { - commands.push(self.update(Message::OpenDeleteListDialog)); - } - } - }, Message::ToggleContextPage(context_page) => { if self.context_page == context_page { self.core.window.show_context = !self.core.window.show_context; @@ -681,208 +491,423 @@ impl Application for Tasks { } self.set_context_title(context_page.clone().title()); } - Message::WindowClose => { - if let Some(window_id) = self.core.main_window_id() { - return window::close(window_id); - } - } - Message::WindowNew => match env::current_exe() { - Ok(exe) => match process::Command::new(&exe).spawn() { - Ok(_) => {} - Err(err) => { - eprintln!("failed to execute {exe:?}: {err}"); + Message::Dialog(dialog_action) => match dialog_action { + DialogAction::Open(page) => { + match page { + DialogPage::Icon(icon) => { + if self.nav_model.active_data::().is_some() { + self.dialog_pages.push_back(DialogPage::Icon(icon)); + } + } + DialogPage::Rename(_) => { + if let Some(list) = self.nav_model.active_data::() { + self.dialog_pages + .push_back(DialogPage::Rename(list.name.clone())); + } + } + DialogPage::Delete => { + if self.nav_model.active_data::().is_some() { + self.dialog_pages.push_back(DialogPage::Delete); + } + } + page => self.dialog_pages.push_back(page), } - }, - Err(err) => { - eprintln!("failed to get current executable path: {err}"); + commands.push(self.update(Message::Application(ApplicationAction::Focus( + self.dialog_text_input.clone(), + )))); } - }, - Message::LaunchUrl(url) => match open::that_detached(&url) { - Ok(()) => {} - Err(err) => { - log::warn!("failed to open {:?}: {}", url, err); + DialogAction::Update(dialog_page) => { + self.dialog_pages[0] = dialog_page; } - }, - Message::AppTheme(index) => { - let app_theme = match index { - 1 => AppTheme::Dark, - 2 => AppTheme::Light, - _ => AppTheme::System, - }; - config_set!(app_theme, app_theme); - return self.update_config(); - } - Message::SystemThemeModeChange => { - return self.update_config(); - } - Message::FetchLists => { - commands.push(Command::perform( - todo::fetch_lists(self.service.clone()), - |result| match result { - Ok(data) => message::app(Message::PopulateLists(data)), - Err(_) => message::none(), - }, - )); - } - Message::PopulateLists(lists) => { - for list in lists { - self.create_nav_item(&list); + DialogAction::Close => { + self.dialog_pages.pop_front(); } - let Some(entity) = self.nav_model.iter().next() else { - return Command::none(); - }; - self.nav_model.activate(entity); - let command = self.on_nav_select(entity); - commands.push(command); - } - Message::Key(modifiers, key) => { - for (key_bind, action) in &self.key_binds { - if key_bind.matches(modifiers, &key) { - return self.update(action.message()); + DialogAction::Complete => { + if let Some(dialog_page) = self.dialog_pages.pop_front() { + match dialog_page { + DialogPage::New(name) => { + let list = List::new(&name); + commands.push(Command::perform( + todo::create_list(list, self.service.clone()), + |result| match result { + Ok(list) => { + message::app(Message::Tasks(TasksAction::AddList(list))) + } + Err(_) => message::none(), + }, + )); + } + DialogPage::Rename(name) => { + let entity = self.nav_model.active(); + self.nav_model.text_set(entity, name.clone()); + if let Some(list) = self.nav_model.active_data_mut::() { + list.name.clone_from(&name); + let command = Command::perform( + todo::update_list(list.clone(), self.service.clone()), + |_| message::none(), + ); + commands.push(command); + } + } + DialogPage::Delete => { + commands.push(self.update(Message::Tasks(TasksAction::DeleteList))); + } + DialogPage::Icon(icon) => { + if let Some(list) = self.nav_model.active_data::() { + let entity = self.nav_model.active(); + let title = format!("{} {}", icon.clone(), list.name.clone()); + self.nav_model.text_set(entity, title); + } + if let Some(list) = self.nav_model.active_data_mut::() { + list.icon = Some(icon); + let command = Command::perform( + todo::update_list(list.clone(), self.service.clone()), + |_| message::none(), + ); + commands.push(command); + } + } + DialogPage::Calendar(date) => { + self.details.update(details::Message::SetDueDate(date)); + } + DialogPage::Export(content) => { + let mut clipboard = ClipboardContext::new().unwrap(); + clipboard.set_contents(content).unwrap(); + } + } } } - } - Message::Modifiers(modifiers) => { - self.modifiers = modifiers; - } - Message::AddList(list) => { - self.create_nav_item(&list); - let Some(entity) = self.nav_model.iter().last() else { - return Command::none(); - }; - let command = self.on_nav_select(entity); - commands.push(command); - } - Message::DeleteList => { - if let Some(list) = self.nav_model.data::(self.nav_model.active()) { - let command = Command::perform( - todo::delete_list(list.id().clone(), self.service.clone()), + }, + Message::Tasks(tasks_action) => match tasks_action { + TasksAction::FetchLists => { + commands.push(Command::perform( + todo::fetch_lists(self.service.clone()), |result| match result { - Ok(()) | Err(_) => message::none(), + Ok(data) => { + message::app(Message::Tasks(TasksAction::PopulateLists(data))) + } + Err(_) => message::none(), }, - ); - - commands.push(self.update(Message::Content(content::Message::List(None)))); - - commands.push(command); + )); } - self.nav_model.remove(self.nav_model.active()); - } - Message::Export(tasks) => { - if let Some(list) = self.nav_model.data::(self.nav_model.active()) { - let exported_markdown = todo::export_list(list, &tasks); - commands.push(self.update(Message::OpenExportDialog(exported_markdown))); + TasksAction::PopulateLists(lists) => { + for list in lists { + self.create_nav_item(&list); + } + let Some(entity) = self.nav_model.iter().next() else { + return Command::none(); + }; + self.nav_model.activate(entity); + let command = self.on_nav_select(entity); + commands.push(command); } - } - Message::OpenNewListDialog => { - self.dialog_pages.push_back(DialogPage::New(String::new())); - return widget::text_input::focus(self.dialog_text_input.clone()); - } - Message::OpenRenameListDialog => { - if let Some(list) = self.nav_model.data::(self.nav_model.active()) { - self.dialog_pages.push_back(DialogPage::Rename { - to: list.name.clone(), - }); - return widget::text_input::focus(self.dialog_text_input.clone()); + TasksAction::AddList(list) => { + self.create_nav_item(&list); + let Some(entity) = self.nav_model.iter().last() else { + return Command::none(); + }; + let command = self.on_nav_select(entity); + commands.push(command); } - } - Message::OpenDeleteListDialog => { - if self - .nav_model - .data::(self.nav_model.active()) - .is_some() - { - self.dialog_pages.push_back(DialogPage::Delete); + TasksAction::DeleteList => { + if let Some(list) = self.nav_model.active_data::() { + let command = Command::perform( + todo::delete_list(list.id().clone(), self.service.clone()), + |result| match result { + Ok(()) | Err(_) => message::none(), + }, + ); + + commands.push(self.update(Message::Content(content::Message::List(None)))); + + commands.push(command); + } + self.nav_model.remove(self.nav_model.active()); } - } - Message::OpenIconDialog => { - if self - .nav_model - .data::(self.nav_model.active()) - .is_some() - { - self.dialog_pages.push_back(DialogPage::Icon(String::new())); + TasksAction::Export(tasks) => { + if let Some(list) = self.nav_model.active_data() { + let exported_markdown = todo::export_list(list, &tasks); + commands.push(self.update(Message::Dialog(DialogAction::Open( + DialogPage::Export(exported_markdown), + )))); + } } - } - Message::OpenCalendarDialog => { - self.dialog_pages - .push_back(DialogPage::Calendar(Local::now().date_naive())); - } - Message::OpenExportDialog(content) => { - self.dialog_pages.push_back(DialogPage::Export(content)); - } - Message::DialogCancel => { - self.dialog_pages.pop_front(); - } - Message::DialogComplete => { - if let Some(dialog_page) = self.dialog_pages.pop_front() { - match dialog_page { - DialogPage::New(name) => { - let list = List::new(&name); - commands.push(Command::perform( - todo::create_list(list, self.service.clone()), - |result| match result { - Ok(list) => message::app(Message::AddList(list)), - Err(_) => message::none(), - }, - )); + }, + Message::Application(application_action) => { + match application_action { + ApplicationAction::WindowClose => { + if let Some(window_id) = self.core.main_window_id() { + return window::close(window_id); } - DialogPage::Rename { to: name } => { - let entity = self.nav_model.active(); - self.nav_model.text_set(entity, name.clone()); - if let Some(list) = self.nav_model.active_data_mut::() { - list.name.clone_from(&name); - let command = Command::perform( - todo::update_list(list.clone(), self.service.clone()), - |_| message::none(), - ); - commands.push(command); + } + ApplicationAction::WindowNew => match env::current_exe() { + Ok(exe) => match process::Command::new(&exe).spawn() { + Ok(_) => {} + Err(err) => { + eprintln!("failed to execute {exe:?}: {err}"); } + }, + Err(err) => { + eprintln!("failed to get current executable path: {err}"); } - DialogPage::Delete => { - commands.push(self.update(Message::DeleteList)); + }, + ApplicationAction::LaunchUrl(url) => match open::that_detached(&url) { + Ok(()) => {} + Err(err) => { + log::warn!("failed to open {:?}: {}", url, err); } - DialogPage::Icon(icon) => { - if let Some(list) = self.nav_model.active_data::() { - let entity = self.nav_model.active(); - let title = format!("{} {}", icon.clone(), list.name.clone()); - self.nav_model.text_set(entity, title); + }, + ApplicationAction::AppTheme(index) => { + let app_theme = match index { + 1 => AppTheme::Dark, + 2 => AppTheme::Light, + _ => AppTheme::System, + }; + config_set!(app_theme, app_theme); + return self.update_config(); + } + ApplicationAction::SystemThemeModeChange => { + return self.update_config(); + } + ApplicationAction::Key(modifiers, key) => { + for (key_bind, action) in &self.key_binds { + if key_bind.matches(modifiers, &key) { + return self.update(action.message()); } - if let Some(list) = self.nav_model.active_data_mut::() { - list.icon = Some(icon); - let command = Command::perform( - todo::update_list(list.clone(), self.service.clone()), - |_| message::none(), - ); - commands.push(command); + } + } + ApplicationAction::Modifiers(modifiers) => { + self.modifiers = modifiers; + } + ApplicationAction::Focus(id) => commands.push(widget::text_input::focus(id)), + ApplicationAction::NavMenuAction(nav_menu_action) => match nav_menu_action { + NavMenuAction::Rename(entity) => { + if self.nav_model.data::(entity).is_some() { + commands.push(self.update(Message::Dialog(DialogAction::Open( + DialogPage::Rename(String::new()), + )))); } } - DialogPage::Calendar(date) => { - self.details.update(details::Message::SetDueDate(date)); + NavMenuAction::SetIcon(entity) => { + if self.nav_model.data::(entity).is_some() { + commands.push(self.update(Message::Dialog(DialogAction::Open( + DialogPage::Icon(String::new()), + )))); + } } - DialogPage::Export(content) => { - let mut clipboard = ClipboardContext::new().unwrap(); - clipboard.set_contents(content).unwrap(); + NavMenuAction::Delete(entity) => { + if self.nav_model.data::(entity).is_some() { + commands.push(self.update(Message::Dialog(DialogAction::Open( + DialogPage::Delete, + )))); + } } - } + }, } } - Message::DialogUpdate(dialog_page) => { - //TODO: panicless way to do this? - self.dialog_pages[0] = dialog_page; - } - Message::Focus(id) => return Command::batch(vec![widget::text_input::focus(id)]), - } - - if !commands.is_empty() { - return Command::batch(commands); } - Command::none() + Command::batch(commands) } fn view(&self) -> Element { let content_view = self.content.view().map(Message::Content); content_view } + + fn dialog(&self) -> Option> { + let dialog_page = self.dialog_pages.front()?; + + let spacing = theme::active().cosmic().spacing; + + let dialog = match dialog_page { + DialogPage::New(name) => widget::dialog(fl!("create-list")) + .primary_action( + widget::button::suggested(fl!("save")) + .on_press_maybe(Some(Message::Dialog(DialogAction::Complete))), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::Dialog(DialogAction::Close)), + ) + .control( + widget::column::with_children(vec![ + widget::text::body(fl!("list-name")).into(), + widget::text_input("", name.as_str()) + .id(self.dialog_text_input.clone()) + .on_input(move |name| { + Message::Dialog(DialogAction::Update(DialogPage::New(name))) + }) + .on_submit(Message::Dialog(DialogAction::Complete)) + .into(), + ]) + .spacing(spacing.space_xxs), + ), + DialogPage::Rename(name) => widget::dialog(fl!("rename-list")) + .primary_action( + widget::button::suggested(fl!("save")) + .on_press_maybe(Some(Message::Dialog(DialogAction::Complete))), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::Dialog(DialogAction::Close)), + ) + .control( + widget::column::with_children(vec![ + widget::text::body(fl!("list-name")).into(), + widget::text_input("", name.as_str()) + .id(self.dialog_text_input.clone()) + .on_input(move |name| { + Message::Dialog(DialogAction::Update(DialogPage::Rename(name))) + }) + .on_submit(Message::Dialog(DialogAction::Complete)) + .into(), + ]) + .spacing(spacing.space_xxs), + ), + DialogPage::Delete => widget::dialog(fl!("delete-list")) + .body(fl!("delete-list-confirm")) + .primary_action( + widget::button::suggested(fl!("ok")) + .on_press_maybe(Some(Message::Dialog(DialogAction::Complete))), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::Dialog(DialogAction::Close)), + ), + DialogPage::Icon(icon) => { + let icon_buttons: Vec> = emojis::iter() + .map(|emoji| { + widget::button::custom( + widget::container(widget::text(emoji.to_string())) + .width(spacing.space_l) + .height(spacing.space_l) + .align_y(Vertical::Center) + .align_x(Horizontal::Center), + ) + .on_press(Message::Dialog(DialogAction::Update(DialogPage::Icon( + emoji.to_string(), + )))) + .into() + }) + .collect(); + let mut dialog = widget::dialog(fl!("icon-select")) + .body(fl!("icon-select-body")) + .primary_action( + widget::button::suggested(fl!("ok")) + .on_press_maybe(Some(Message::Dialog(DialogAction::Complete))), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::Dialog(DialogAction::Close)), + ) + .control( + widget::container(scrollable(widget::row::with_children(vec![ + widget::flex_row(icon_buttons).into(), + horizontal_space().into(), + ]))) + .height(Length::Fixed(300.0)), + ); + + if !icon.is_empty() { + dialog = dialog.icon(widget::container( + widget::text(icon.as_str()).size(spacing.space_l), + )); + } + + dialog + } + DialogPage::Calendar(date) => { + let dialog = widget::dialog(fl!("select-date")) + .primary_action( + widget::button::suggested(fl!("ok")) + .on_press_maybe(Some(Message::Dialog(DialogAction::Complete))), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::Dialog(DialogAction::Close)), + ) + .control( + widget::container(widget::calendar(date, |date| { + Message::Dialog(DialogAction::Update(DialogPage::Calendar(date))) + })) + .width(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center), + ); + dialog + } + DialogPage::Export(contents) => { + let dialog = widget::dialog(fl!("export")) + .control( + widget::container(scrollable(widget::text(contents)).width(Length::Fill)) + .height(Length::Fixed(200.0)) + .width(Length::Fill), + ) + .primary_action( + widget::button::suggested(fl!("copy")) + .on_press_maybe(Some(Message::Dialog(DialogAction::Complete))), + ) + .secondary_action( + widget::button::standard(fl!("cancel")) + .on_press(Message::Dialog(DialogAction::Close)), + ); + + dialog + } + }; + + Some(dialog.into()) + } + + fn subscription(&self) -> Subscription { + struct ConfigSubscription; + struct ThemeSubscription; + + let mut subscriptions = vec![ + event::listen_with(|event, _status, _window_id| match event { + Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => { + Some(Message::Application(ApplicationAction::Key(modifiers, key))) + } + Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => Some( + Message::Application(ApplicationAction::Modifiers(modifiers)), + ), + _ => None, + }), + cosmic_config::config_subscription( + TypeId::of::(), + Self::APP_ID.into(), + CONFIG_VERSION, + ) + .map(|update: Update| { + if !update.errors.is_empty() { + log::info!( + "errors loading config {:?}: {:?}", + update.keys, + update.errors + ); + } + Message::Application(ApplicationAction::SystemThemeModeChange) + }), + cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( + TypeId::of::(), + cosmic_theme::THEME_MODE_ID.into(), + cosmic_theme::ThemeMode::version(), + ) + .map(|update: Update| { + if !update.errors.is_empty() { + log::info!( + "errors loading theme mode {:?}: {:?}", + update.keys, + update.errors + ); + } + Message::Application(ApplicationAction::SystemThemeModeChange) + }), + ]; + + subscriptions.push(self.content.subscription().map(Message::Content)); + + Subscription::batch(subscriptions) + } }