Implement watched toggle for films in details modal
- Watched status is stored in the local database - Gave details modals a set size - Added a for now useless play button to film details modal
This commit is contained in:
parent
406a423478
commit
629f2ba9d0
5 changed files with 135 additions and 19 deletions
|
|
@ -79,9 +79,10 @@ impl SqliteManager {
|
||||||
move |connection| {
|
move |connection| {
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"
|
"
|
||||||
update watched_status
|
insert into watched_status (media_uuid, watched)
|
||||||
set watched = :watched
|
values (:uuid, :watched)
|
||||||
where watched_status.media_uuid is :uuid
|
on conflict (media_uuid) do
|
||||||
|
update set watched = :watched
|
||||||
",
|
",
|
||||||
named_params! { ":uuid": uuid, ":watched": watched },
|
named_params! { ":uuid": uuid, ":watched": watched },
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt};
|
use gtk4::glib::clone;
|
||||||
use gtk4::{Button, Label, Orientation};
|
use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, ToggleButtonExt, WidgetExt};
|
||||||
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component};
|
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;
|
use crate::views::overview::FilmOverview;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,11 +12,28 @@ pub struct FilmDetails {
|
||||||
film_overview: FilmOverview,
|
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)]
|
#[component(pub)]
|
||||||
impl SimpleComponent for FilmDetails {
|
impl Component for FilmDetails {
|
||||||
type Init = FilmOverview;
|
type Init = FilmOverview;
|
||||||
type Input = ();
|
type Input = FilmDetailsInput;
|
||||||
type Output = ();
|
type Output = FilmDetailsOutput;
|
||||||
|
type CommandOutput = FilmDetailsCmdOutput;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
gtk4::Box {
|
gtk4::Box {
|
||||||
|
|
@ -23,24 +42,91 @@ impl SimpleComponent for FilmDetails {
|
||||||
set_margin_all: 100,
|
set_margin_all: 100,
|
||||||
|
|
||||||
Label {
|
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_css_classes: &["title-1"],
|
||||||
set_label: model.film_overview.name.as_str(),
|
set_label: model.film_overview.name.as_str(),
|
||||||
},
|
},
|
||||||
|
|
||||||
Button {
|
gtk4::Box {
|
||||||
set_css_classes: &["suggested-action", "circular"],
|
set_align: Align::Center,
|
||||||
set_icon_name: "media-playback-start-symbolic",
|
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(
|
fn init(
|
||||||
film_overview: FilmOverview,
|
film_overview: FilmOverview,
|
||||||
root: gtk4::Box,
|
root: gtk4::Box,
|
||||||
_sender: ComponentSender<FilmDetails>,
|
sender: ComponentSender<FilmDetails>,
|
||||||
) -> ComponentParts<FilmDetails> {
|
) -> ComponentParts<FilmDetails> {
|
||||||
let model = FilmDetails { film_overview };
|
let model = FilmDetails { film_overview };
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
message: FilmDetailsInput,
|
||||||
|
sender: ComponentSender<FilmDetails>,
|
||||||
|
_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<FilmDetails>,
|
||||||
|
_root: >k4::Box,
|
||||||
|
) {
|
||||||
|
match message {
|
||||||
|
FilmDetailsCmdOutput::WatchedStatusPersistSucceeded => {}
|
||||||
|
FilmDetailsCmdOutput::WatchedStatusPersistFailed(error) => {
|
||||||
|
println!("Watched status persist failed: {error:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt};
|
use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt};
|
||||||
use gtk4::{Label, Orientation};
|
use gtk4::{Justification, Label, Orientation};
|
||||||
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component};
|
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component};
|
||||||
|
|
||||||
use crate::views::overview::SeriesOverview;
|
use crate::views::overview::SeriesOverview;
|
||||||
|
|
@ -23,6 +23,12 @@ impl SimpleComponent for SeriesDetails {
|
||||||
set_margin_all: 100,
|
set_margin_all: 100,
|
||||||
|
|
||||||
Label {
|
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_css_classes: &["title-1"],
|
||||||
set_label: model.series_overview.name.as_str(),
|
set_label: model.series_overview.name.as_str(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use relm4::factory::{DynamicIndex, FactoryComponent};
|
||||||
use relm4::{Component, ComponentController, Controller, FactorySender, RelmWidgetExt, factory};
|
use relm4::{Component, ComponentController, Controller, FactorySender, RelmWidgetExt, factory};
|
||||||
|
|
||||||
use crate::persist::data_manager::{DataManager, DataManagerError};
|
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::ui::widget_extensions::{AttrListExt, CondDialogExt, CondLabelExt};
|
||||||
use crate::views::overview::FilmOverview;
|
use crate::views::overview::FilmOverview;
|
||||||
|
|
||||||
|
|
@ -38,13 +38,19 @@ pub enum FilmGridItemCmdOutput {
|
||||||
pub enum FilmGridItemInput {
|
pub enum FilmGridItemInput {
|
||||||
ItemClicked,
|
ItemClicked,
|
||||||
DetailsClosed,
|
DetailsClosed,
|
||||||
|
WatchedStatusChanged(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FilmGridItemOutput {
|
||||||
|
WatchedStatusChanged(String, bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[factory(pub)]
|
#[factory(pub)]
|
||||||
impl FactoryComponent for FilmGridItem {
|
impl FactoryComponent for FilmGridItem {
|
||||||
type Init = FilmOverview;
|
type Init = FilmOverview;
|
||||||
type Input = FilmGridItemInput;
|
type Input = FilmGridItemInput;
|
||||||
type Output = ();
|
type Output = FilmGridItemOutput;
|
||||||
type CommandOutput = FilmGridItemCmdOutput;
|
type CommandOutput = FilmGridItemCmdOutput;
|
||||||
type ParentWidget = FlowBox;
|
type ParentWidget = FlowBox;
|
||||||
|
|
||||||
|
|
@ -123,6 +129,8 @@ impl FactoryComponent for FilmGridItem {
|
||||||
|
|
||||||
// TODO: Consider extracting this into its own component.
|
// TODO: Consider extracting this into its own component.
|
||||||
Dialog {
|
Dialog {
|
||||||
|
set_content_height: 600,
|
||||||
|
set_content_width: 600,
|
||||||
#[watch]
|
#[watch]
|
||||||
cond_present: self.details.as_ref().map(|_| {
|
cond_present: self.details.as_ref().map(|_| {
|
||||||
root.toplevel_window().expect("root widget of FilmGridItem should be ancestor of a window")
|
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<FilmGridItem>) {
|
fn update(&mut self, message: FilmGridItemInput, sender: FactorySender<FilmGridItem>) {
|
||||||
match message {
|
match message {
|
||||||
FilmGridItemInput::ItemClicked => {
|
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);
|
self.details = Some(details_controller);
|
||||||
}
|
}
|
||||||
FilmGridItemInput::DetailsClosed => {
|
FilmGridItemInput::DetailsClosed => {
|
||||||
self.details = None;
|
self.details = None;
|
||||||
}
|
}
|
||||||
|
FilmGridItemInput::WatchedStatusChanged(watched) => {
|
||||||
|
self.film.watched = watched;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ impl FactoryComponent for SeriesGridItem {
|
||||||
|
|
||||||
// TODO: Consider extracting this into its own component.
|
// TODO: Consider extracting this into its own component.
|
||||||
Dialog {
|
Dialog {
|
||||||
|
set_content_height: 600,
|
||||||
|
set_content_width: 600,
|
||||||
#[watch]
|
#[watch]
|
||||||
cond_present: self.details.as_ref().map(|_| {
|
cond_present: self.details.as_ref().map(|_| {
|
||||||
root.toplevel_window().expect("root widget of FilmGridItem should be ancestor of a window")
|
root.toplevel_window().expect("root widget of FilmGridItem should be ancestor of a window")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue