diff --git a/src/persist/sqlite_manager.rs b/src/persist/sqlite_manager.rs index d41ee9d..c79b9bb 100644 --- a/src/persist/sqlite_manager.rs +++ b/src/persist/sqlite_manager.rs @@ -79,9 +79,10 @@ impl SqliteManager { move |connection| { connection.execute( " - update watched_status - set watched = :watched - where watched_status.media_uuid is :uuid + insert into watched_status (media_uuid, watched) + values (:uuid, :watched) + on conflict (media_uuid) do + update set watched = :watched ", named_params! { ":uuid": uuid, ":watched": watched }, )?; diff --git a/src/ui/components/media_details/film_details.rs b/src/ui/components/media_details/film_details.rs index 82eeaba..5914f17 100644 --- a/src/ui/components/media_details/film_details.rs +++ b/src/ui/components/media_details/film_details.rs @@ -1,7 +1,9 @@ -use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt}; -use gtk4::{Button, Label, Orientation}; -use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component}; +use gtk4::glib::clone; +use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, ToggleButtonExt, WidgetExt}; +use gtk4::{Align, Button, IconSize, Image, Justification, Label, Orientation, ToggleButton}; +use relm4::{Component, ComponentParts, ComponentSender, RelmWidgetExt, component}; +use crate::persist::data_manager::{DataManager, DataManagerError}; use crate::views::overview::FilmOverview; @@ -10,11 +12,28 @@ pub struct FilmDetails { film_overview: FilmOverview, } +#[derive(Debug)] +pub enum FilmDetailsInput { + WatchedStatusChanged(bool), +} + +#[derive(Debug)] +pub enum FilmDetailsOutput { + WatchedStatusChanged(bool), +} + +#[derive(Debug)] +pub enum FilmDetailsCmdOutput { + WatchedStatusPersistSucceeded, + WatchedStatusPersistFailed(DataManagerError), +} + #[component(pub)] -impl SimpleComponent for FilmDetails { +impl Component for FilmDetails { type Init = FilmOverview; - type Input = (); - type Output = (); + type Input = FilmDetailsInput; + type Output = FilmDetailsOutput; + type CommandOutput = FilmDetailsCmdOutput; view! { gtk4::Box { @@ -23,24 +42,91 @@ impl SimpleComponent for FilmDetails { set_margin_all: 100, Label { + set_wrap: true, + // Not the actual limit, used instead to avoid the text width propagating to the + // requested widget width, behaving similarly to hexpand: false. + set_max_width_chars: 1, + // Keeps wrapped text centered. + set_justify: Justification::Center, set_css_classes: &["title-1"], set_label: model.film_overview.name.as_str(), }, - Button { - set_css_classes: &["suggested-action", "circular"], - set_icon_name: "media-playback-start-symbolic", + gtk4::Box { + set_align: Align::Center, + ToggleButton { + #[watch] + set_label: if model.film_overview.watched { "Watched" } else { "Watch" }, + set_active: model.film_overview.watched, + connect_toggled[sender] => move |watch_button| { + sender.input(FilmDetailsInput::WatchedStatusChanged(watch_button.is_active())); + }, + }, }, - } + + gtk4::Box { + set_align: Align::Center, + Button { + set_css_classes: &["suggested-action", "circular"], + Image { + set_icon_size: IconSize::Large, + set_margin_all: 15, + set_icon_name: Some("media-playback-start-symbolic"), + }, + }, + }, + }, } fn init( film_overview: FilmOverview, root: gtk4::Box, - _sender: ComponentSender, + sender: ComponentSender, ) -> ComponentParts { let model = FilmDetails { film_overview }; let widgets = view_output!(); ComponentParts { model, widgets } } + + fn update( + &mut self, + message: FilmDetailsInput, + sender: ComponentSender, + _root: >k4::Box, + ) { + match message { + FilmDetailsInput::WatchedStatusChanged(watched) => { + self.film_overview.watched = watched; + sender + .output_sender() + .emit(FilmDetailsOutput::WatchedStatusChanged(watched)); + + sender.oneshot_command(clone!( + #[strong(rename_to = uuid)] + self.film_overview.uuid, + async move { + let result = DataManager::set_film_watched_status(uuid.as_str(), watched).await; + match result { + Ok(()) => FilmDetailsCmdOutput::WatchedStatusPersistSucceeded, + Err(error) => FilmDetailsCmdOutput::WatchedStatusPersistFailed(error), + } + } + )); + } + } + } + + fn update_cmd( + &mut self, + message: FilmDetailsCmdOutput, + _sender: ComponentSender, + _root: >k4::Box, + ) { + match message { + FilmDetailsCmdOutput::WatchedStatusPersistSucceeded => {} + FilmDetailsCmdOutput::WatchedStatusPersistFailed(error) => { + println!("Watched status persist failed: {error:?}"); + } + } + } } diff --git a/src/ui/components/media_details/series_details.rs b/src/ui/components/media_details/series_details.rs index 9152fe7..1aa197b 100644 --- a/src/ui/components/media_details/series_details.rs +++ b/src/ui/components/media_details/series_details.rs @@ -1,5 +1,5 @@ use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt}; -use gtk4::{Label, Orientation}; +use gtk4::{Justification, Label, Orientation}; use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component}; use crate::views::overview::SeriesOverview; @@ -23,6 +23,12 @@ impl SimpleComponent for SeriesDetails { set_margin_all: 100, Label { + set_wrap: true, + // Not the actual limit, used instead to avoid the text width propagating to the + // requested widget width, behaving similarly to hexpand: false. + set_max_width_chars: 1, + // Keeps wrapped text centered. + set_justify: Justification::Center, set_css_classes: &["title-1"], set_label: model.series_overview.name.as_str(), } diff --git a/src/ui/components/media_grid_item/film_grid_item.rs b/src/ui/components/media_grid_item/film_grid_item.rs index 02ca9a9..4c28cf6 100644 --- a/src/ui/components/media_grid_item/film_grid_item.rs +++ b/src/ui/components/media_grid_item/film_grid_item.rs @@ -9,7 +9,7 @@ use relm4::factory::{DynamicIndex, FactoryComponent}; use relm4::{Component, ComponentController, Controller, FactorySender, RelmWidgetExt, factory}; use crate::persist::data_manager::{DataManager, DataManagerError}; -use crate::ui::components::media_details::FilmDetails; +use crate::ui::components::media_details::{FilmDetails, FilmDetailsOutput}; use crate::ui::widget_extensions::{AttrListExt, CondDialogExt, CondLabelExt}; use crate::views::overview::FilmOverview; @@ -38,13 +38,19 @@ pub enum FilmGridItemCmdOutput { pub enum FilmGridItemInput { ItemClicked, DetailsClosed, + WatchedStatusChanged(bool), +} + +#[derive(Debug)] +pub enum FilmGridItemOutput { + WatchedStatusChanged(String, bool), } #[factory(pub)] impl FactoryComponent for FilmGridItem { type Init = FilmOverview; type Input = FilmGridItemInput; - type Output = (); + type Output = FilmGridItemOutput; type CommandOutput = FilmGridItemCmdOutput; type ParentWidget = FlowBox; @@ -123,6 +129,8 @@ impl FactoryComponent for FilmGridItem { // TODO: Consider extracting this into its own component. Dialog { + set_content_height: 600, + set_content_width: 600, #[watch] cond_present: self.details.as_ref().map(|_| { root.toplevel_window().expect("root widget of FilmGridItem should be ancestor of a window") @@ -159,15 +167,28 @@ impl FactoryComponent for FilmGridItem { } } - fn update(&mut self, message: FilmGridItemInput, _sender: FactorySender) { + fn update(&mut self, message: FilmGridItemInput, sender: FactorySender) { match message { FilmGridItemInput::ItemClicked => { - let details_controller = FilmDetails::builder().launch(self.film.clone()).detach(); + let details_controller = FilmDetails::builder() + .launch(self.film.clone()) + .connect_receiver(clone!( + #[strong] + sender, + move |_, details_output| match details_output { + FilmDetailsOutput::WatchedStatusChanged(watched) => { + sender.input(FilmGridItemInput::WatchedStatusChanged(watched)); + } + } + )); self.details = Some(details_controller); } FilmGridItemInput::DetailsClosed => { self.details = None; } + FilmGridItemInput::WatchedStatusChanged(watched) => { + self.film.watched = watched; + } } } diff --git a/src/ui/components/media_grid_item/series_grid_item.rs b/src/ui/components/media_grid_item/series_grid_item.rs index d773371..478a371 100644 --- a/src/ui/components/media_grid_item/series_grid_item.rs +++ b/src/ui/components/media_grid_item/series_grid_item.rs @@ -104,6 +104,8 @@ impl FactoryComponent for SeriesGridItem { // TODO: Consider extracting this into its own component. Dialog { + set_content_height: 600, + set_content_width: 600, #[watch] cond_present: self.details.as_ref().map(|_| { root.toplevel_window().expect("root widget of FilmGridItem should be ancestor of a window")