From f6962e8dd914eb4287082913f7977e3f58248c75 Mon Sep 17 00:00:00 2001 From: Maarifa Maarifa Date: Wed, 3 Jan 2024 01:49:04 +0300 Subject: [PATCH] Add show crew information in series page --- src/core/api/tv_maze/mod.rs | 2 +- .../tv_maze/{show_cast.rs => people/mod.rs} | 45 +-- src/core/api/tv_maze/people/show_cast.rs | 27 ++ src/core/api/tv_maze/people/show_crew.rs | 22 ++ src/core/caching.rs | 10 +- src/core/caching/people.rs | 44 +++ src/core/caching/show_cast.rs | 30 -- src/gui/series_page/series/mod.rs | 31 +- .../series/{ => people_widget}/cast_widget.rs | 26 +- .../series/people_widget/crew_widget.rs | 324 ++++++++++++++++++ .../series_page/series/people_widget/mod.rs | 138 ++++++++ src/gui/styles/colors.rs | 4 + src/gui/styles/mod.rs | 1 + src/gui/styles/toggler_styles.rs | 31 ++ 14 files changed, 638 insertions(+), 97 deletions(-) rename src/core/api/tv_maze/{show_cast.rs => people/mod.rs} (61%) create mode 100644 src/core/api/tv_maze/people/show_cast.rs create mode 100644 src/core/api/tv_maze/people/show_crew.rs create mode 100644 src/core/caching/people.rs delete mode 100644 src/core/caching/show_cast.rs rename src/gui/series_page/series/{ => people_widget}/cast_widget.rs (95%) create mode 100644 src/gui/series_page/series/people_widget/crew_widget.rs create mode 100644 src/gui/series_page/series/people_widget/mod.rs create mode 100644 src/gui/styles/toggler_styles.rs diff --git a/src/core/api/tv_maze/mod.rs b/src/core/api/tv_maze/mod.rs index 68e5db4..4b2db8d 100644 --- a/src/core/api/tv_maze/mod.rs +++ b/src/core/api/tv_maze/mod.rs @@ -3,10 +3,10 @@ use thiserror::Error; pub mod episodes_information; pub mod image; +pub mod people; pub mod seasons_list; pub mod series_information; pub mod series_searching; -pub mod show_cast; pub mod show_images; pub mod show_lookup; pub mod tv_schedule; diff --git a/src/core/api/tv_maze/show_cast.rs b/src/core/api/tv_maze/people/mod.rs similarity index 61% rename from src/core/api/tv_maze/show_cast.rs rename to src/core/api/tv_maze/people/mod.rs index adf2808..ea9f059 100644 --- a/src/core/api/tv_maze/show_cast.rs +++ b/src/core/api/tv_maze/people/mod.rs @@ -1,11 +1,12 @@ +use crate::core::api::tv_maze::Image; use serde::Deserialize; -use super::{get_pretty_json_from_url, ApiError, Image}; +pub mod show_cast; +pub mod show_crew; #[derive(Deserialize, Debug, Clone)] -pub struct Cast { - pub person: Person, - pub character: Character, +pub struct Country { + pub name: String, } #[derive(Deserialize, Debug, Clone)] @@ -30,23 +31,15 @@ pub enum AgeError { Parse(chrono::ParseError), } -impl Cast { +impl Person { pub fn birth_naive_date(&self) -> Result { - let date = self - .person - .birthday - .as_ref() - .ok_or(AgeError::BirthdateNotFound)?; + let date = self.birthday.as_ref().ok_or(AgeError::BirthdateNotFound)?; chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d").map_err(AgeError::Parse) } pub fn death_naive_date(&self) -> Result { - let date = self - .person - .deathday - .as_ref() - .ok_or(AgeError::DeathdateNotFound)?; + let date = self.deathday.as_ref().ok_or(AgeError::DeathdateNotFound)?; chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d").map_err(AgeError::Parse) } @@ -66,25 +59,3 @@ impl Cast { Ok(deathdate.signed_duration_since(birthdate)) } } - -#[derive(Deserialize, Debug, Clone)] -pub struct Country { - pub name: String, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct Character { - pub name: String, - pub image: Option, -} - -// replace ID with the actual show id -const SHOW_CAST_ADDRESS: &str = "https://api.tvmaze.com/shows/ID/cast"; - -pub async fn get_show_cast(series_id: u32) -> Result { - let url = SHOW_CAST_ADDRESS.replace("ID", &series_id.to_string()); - - get_pretty_json_from_url(url) - .await - .map_err(ApiError::Network) -} diff --git a/src/core/api/tv_maze/people/show_cast.rs b/src/core/api/tv_maze/people/show_cast.rs new file mode 100644 index 0000000..ad1651d --- /dev/null +++ b/src/core/api/tv_maze/people/show_cast.rs @@ -0,0 +1,27 @@ +use serde::Deserialize; + +pub use super::AgeError; +use crate::core::api::tv_maze::{get_pretty_json_from_url, ApiError, Image}; + +#[derive(Deserialize, Debug, Clone)] +pub struct Cast { + pub person: super::Person, + pub character: Character, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Character { + pub name: String, + pub image: Option, +} + +// replace ID with the actual show id +const SHOW_CAST_ADDRESS: &str = "https://api.tvmaze.com/shows/ID/cast"; + +pub async fn get_show_cast(series_id: u32) -> Result { + let url = SHOW_CAST_ADDRESS.replace("ID", &series_id.to_string()); + + get_pretty_json_from_url(url) + .await + .map_err(ApiError::Network) +} diff --git a/src/core/api/tv_maze/people/show_crew.rs b/src/core/api/tv_maze/people/show_crew.rs new file mode 100644 index 0000000..83cfdde --- /dev/null +++ b/src/core/api/tv_maze/people/show_crew.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; + +pub use super::AgeError; +use crate::core::api::tv_maze::{get_pretty_json_from_url, ApiError}; + +#[derive(Deserialize, Debug, Clone)] +pub struct Crew { + #[serde(rename = "type")] + pub kind: String, + pub person: super::Person, +} + +// replace ID with the actual show id +const SHOW_CREW_ADDRESS: &str = "https://api.tvmaze.com/shows/ID/crew"; + +pub async fn get_show_crew(series_id: u32) -> Result { + let url = SHOW_CREW_ADDRESS.replace("ID", &series_id.to_string()); + + get_pretty_json_from_url(url) + .await + .map_err(ApiError::Network) +} diff --git a/src/core/caching.rs b/src/core/caching.rs index 76f9970..64689e3 100644 --- a/src/core/caching.rs +++ b/src/core/caching.rs @@ -15,6 +15,7 @@ //! - `main-info`. The main series information. //! - `episode-list`. The list of all episode of the series. //! - `show-cast`. The list of top cast of the series. +//! - `show-crew`. The list of top crew of the series. //! - `image-list`. The list of all images of the series i.e posters, banners, backgrounds etc. //! //! ## Images cache directory @@ -39,10 +40,10 @@ use tracing::{error, info}; pub mod cache_updating; pub mod episode_list; +pub mod people; pub mod series_info_and_episode_list; pub mod series_information; pub mod series_list; -pub mod show_cast; pub mod show_images; pub mod tv_schedule; @@ -51,6 +52,7 @@ const IMAGES_CACHE_DIRECTORY: &str = "images-cache"; const EPISODE_LIST_FILENAME: &str = "episode-list"; const SERIES_MAIN_INFORMATION_FILENAME: &str = "main-info"; const SERIES_CAST_FILENAME: &str = "show-cast"; +const SERIES_CREW_FILENAME: &str = "show-crew"; const SERIES_IMAGE_LIST_FILENAME: &str = "image-list"; lazy_static! { @@ -66,6 +68,7 @@ pub enum CacheFilePath { SeriesMainInformation(u32), SeriesEpisodeList(u32), SeriesShowCast(u32), + SeriesShowCrew(u32), SeriesImageList(u32), } @@ -119,6 +122,11 @@ impl Cacher { cache_folder.push(SERIES_CAST_FILENAME); cache_folder } + CacheFilePath::SeriesShowCrew(series_id) => { + let mut cache_folder = self.get_series_cache_folder_path(series_id); + cache_folder.push(SERIES_CREW_FILENAME); + cache_folder + } CacheFilePath::SeriesImageList(series_id) => { let mut cache_folder = self.get_series_cache_folder_path(series_id); cache_folder.push(SERIES_IMAGE_LIST_FILENAME); diff --git a/src/core/caching/people.rs b/src/core/caching/people.rs new file mode 100644 index 0000000..da6678f --- /dev/null +++ b/src/core/caching/people.rs @@ -0,0 +1,44 @@ +use std::io::ErrorKind; + +use tracing::info; + +use super::{CacheFilePath, CACHER}; +use crate::core::api::tv_maze::deserialize_json; +use crate::core::api::tv_maze::people::show_cast::{self, Cast}; +use crate::core::api::tv_maze::people::show_crew::{self, Crew}; +use crate::core::api::tv_maze::ApiError; +use crate::core::caching::{read_cache, write_cache}; + +pub async fn get_show_cast(series_id: u32) -> Result, ApiError> { + let series_cast_filepath = CACHER.get_cache_file_path(CacheFilePath::SeriesShowCast(series_id)); + + let json_string = match read_cache(&series_cast_filepath).await { + Ok(json_string) => json_string, + Err(err) => { + info!("falling back online for 'show cast' for series id: {series_id}"); + let json_string = show_cast::get_show_cast(series_id).await?; + if err.kind() == ErrorKind::NotFound { + write_cache(&json_string, &series_cast_filepath).await; + } + json_string + } + }; + deserialize_json(&json_string) +} + +pub async fn get_show_crew(series_id: u32) -> Result, ApiError> { + let series_crew_filepath = CACHER.get_cache_file_path(CacheFilePath::SeriesShowCrew(series_id)); + + let json_string = match read_cache(&series_crew_filepath).await { + Ok(json_string) => json_string, + Err(err) => { + info!("falling back online for 'show crew' for series id: {series_id}"); + let json_string = show_crew::get_show_crew(series_id).await?; + if err.kind() == ErrorKind::NotFound { + write_cache(&json_string, &series_crew_filepath).await; + } + json_string + } + }; + deserialize_json(&json_string) +} diff --git a/src/core/caching/show_cast.rs b/src/core/caching/show_cast.rs deleted file mode 100644 index 55479e4..0000000 --- a/src/core/caching/show_cast.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::io::ErrorKind; - -use tracing::info; - -use super::{CacheFilePath, CACHER}; -use crate::core::{ - api::tv_maze::{ - deserialize_json, - show_cast::{self, Cast}, - ApiError, - }, - caching::{read_cache, write_cache}, -}; - -pub async fn get_show_cast(series_id: u32) -> Result, ApiError> { - let series_cast_filepath = CACHER.get_cache_file_path(CacheFilePath::SeriesShowCast(series_id)); - - let json_string = match read_cache(&series_cast_filepath).await { - Ok(json_string) => json_string, - Err(err) => { - info!("falling back online for 'show cast' for series id: {series_id}"); - let json_string = show_cast::get_show_cast(series_id).await?; - if err.kind() == ErrorKind::NotFound { - write_cache(&json_string, &series_cast_filepath).await; - } - json_string - } - }; - deserialize_json(&json_string) -} diff --git a/src/gui/series_page/series/mod.rs b/src/gui/series_page/series/mod.rs index ea367b9..10b64e4 100644 --- a/src/gui/series_page/series/mod.rs +++ b/src/gui/series_page/series/mod.rs @@ -7,8 +7,8 @@ use crate::core::api::tv_maze::series_information::SeriesMainInformation; use crate::core::api::tv_maze::Image; use crate::core::{caching, database}; use crate::gui::styles; -use cast_widget::{CastWidget, Message as CastWidgetMessage}; use data_widgets::*; +use people_widget::{Message as PeopleWidgetMessage, PeopleWidget}; use season_widget::{Message as SeasonsMessage, Seasons}; use series_suggestion_widget::{Message as SeriesSuggestionMessage, SeriesSuggestion}; @@ -17,8 +17,8 @@ use iced::widget::vertical_space; use iced::widget::{column, scrollable}; use iced::{Command, Element, Renderer}; -mod cast_widget; mod data_widgets; +mod people_widget; mod season_widget; mod series_suggestion_widget; @@ -27,7 +27,7 @@ pub enum Message { SeriesImageLoaded(Option), SeriesBackgroundLoaded(Option), Seasons(SeasonsMessage), - CastWidgetAction(CastWidgetMessage), + PeopleWidget(PeopleWidgetMessage), SeriesSuggestion(SeriesSuggestionMessage), PageScrolled(Viewport), TrackSeries, @@ -41,7 +41,7 @@ pub struct Series<'a> { series_image_blurred: Option, series_background: Option, seasons: Seasons, - casts_widget: CastWidget, + people_widget: PeopleWidget, series_suggestion_widget: SeriesSuggestion<'a>, scroll_offset: RelativeOffset, scroller_id: Id, @@ -54,7 +54,7 @@ impl<'a> Series<'a> { series_page_sender: mpsc::Sender, ) -> (Self, Command) { let series_id = series_information.id; - let (casts_widget, casts_widget_command) = CastWidget::new(series_id); + let (people_widget, people_widget_command) = PeopleWidget::new(series_id); let (seasons, seasons_command) = Seasons::new(series_id, series_information.name.clone()); let (series_suggestion_widget, series_suggestion_widget_command) = SeriesSuggestion::new( @@ -72,7 +72,7 @@ impl<'a> Series<'a> { series_image_blurred: None, series_background: None, seasons, - casts_widget, + people_widget, series_suggestion_widget, scroll_offset: RelativeOffset::default(), scroller_id: scroller_id.clone(), @@ -83,7 +83,7 @@ impl<'a> Series<'a> { let commands = [ Command::batch(load_images(series_image, series_id)), seasons_command.map(Message::Seasons), - casts_widget_command.map(Message::CastWidgetAction), + people_widget_command.map(Message::PeopleWidget), series_suggestion_widget_command.map(Message::SeriesSuggestion), scroller_command, ]; @@ -144,12 +144,6 @@ impl<'a> Series<'a> { series.mark_untracked(); } } - Message::CastWidgetAction(message) => { - return self - .casts_widget - .update(message) - .map(Message::CastWidgetAction) - } Message::SeriesBackgroundLoaded(background) => self.series_background = background, Message::SeriesSuggestion(message) => { return self @@ -160,6 +154,12 @@ impl<'a> Series<'a> { Message::PageScrolled(view_port) => { self.scroll_offset = view_port.relative_offset(); } + Message::PeopleWidget(message) => { + return self + .people_widget + .update(message) + .map(Message::PeopleWidget) + } } Command::none() } @@ -178,7 +178,8 @@ impl<'a> Series<'a> { let seasons_widget = self.seasons.view().map(Message::Seasons); - let casts_widget = self.casts_widget.view().map(Message::CastWidgetAction); + let people_widget = self.people_widget.view().map(Message::PeopleWidget); + let series_suggestion_widget = self .series_suggestion_widget .view() @@ -189,7 +190,7 @@ impl<'a> Series<'a> { series_metadata, vertical_space(10), seasons_widget, - casts_widget, + people_widget, series_suggestion_widget ]; diff --git a/src/gui/series_page/series/cast_widget.rs b/src/gui/series_page/series/people_widget/cast_widget.rs similarity index 95% rename from src/gui/series_page/series/cast_widget.rs rename to src/gui/series_page/series/people_widget/cast_widget.rs index 7d40e75..57b1d19 100644 --- a/src/gui/series_page/series/cast_widget.rs +++ b/src/gui/series_page/series/people_widget/cast_widget.rs @@ -3,7 +3,7 @@ use iced::widget::{button, column, container, horizontal_space, row, svg, text, use iced::{Command, Element, Length, Renderer}; use iced_aw::{Spinner, Wrap}; -use crate::core::{api::tv_maze::show_cast::Cast, caching}; +use crate::core::{api::tv_maze::people::show_cast::Cast, caching}; use crate::gui::assets::icons::{CHEVRON_DOWN, CHEVRON_UP}; use crate::gui::styles; @@ -36,7 +36,7 @@ impl CastWidget { is_expanded: false, }; - let cast_command = Command::perform(caching::show_cast::get_show_cast(series_id), |cast| { + let cast_command = Command::perform(caching::people::get_show_cast(series_id), |cast| { Message::CastReceived(cast.expect("Failed to get show cast")) }); @@ -71,19 +71,20 @@ impl CastWidget { } } - pub fn view(&self) -> Element<'_, Message, Renderer> { + pub fn view(&self) -> Option> { match self.load_state { LoadState::Loading => { - return container(Spinner::new()) + let spinner = container(Spinner::new()) .center_x() .center_y() .height(100) .width(Length::Fill) - .into() + .into(); + Some(spinner) } LoadState::Loaded => { if self.casts.is_empty() { - Space::new(0, 0).into() + None } else { let cast_posters: Vec<_> = self .casts @@ -93,16 +94,15 @@ impl CastWidget { .map(|(_, poster)| poster.view().map(Message::Cast)) .collect(); - column![ - text("Cast").size(21), + let content = column![ Wrap::with_elements(cast_posters) .padding(5.0) .line_spacing(10.0) .spacing(10.0), self.expansion_widget(), ] - .padding(5) - .into() + .into(); + Some(content) } } } @@ -165,7 +165,7 @@ mod cast_poster { use crate::{ core::{ api::tv_maze::{ - show_cast::{AgeError, Cast}, + people::show_cast::{AgeError, Cast}, Image, }, caching, @@ -291,7 +291,7 @@ mod cast_poster { cast_info = cast_info.push(cast_info_field("Gender: ", gender)); } - match self.cast.age_duration_before_death() { + match self.cast.person.age_duration_before_death() { Ok(age_duration_before_death) => { if let Some(age) = helpers::time::NaiveTime::new( age_duration_before_death.num_minutes() as u32, @@ -308,7 +308,7 @@ mod cast_poster { } } Err(AgeError::DeathdateNotFound) => { - if let Ok(duration_since_birth) = self.cast.duration_since_birth() { + if let Ok(duration_since_birth) = self.cast.person.duration_since_birth() { if let Some(age) = helpers::time::NaiveTime::new(duration_since_birth.num_minutes() as u32) .largest_part() diff --git a/src/gui/series_page/series/people_widget/crew_widget.rs b/src/gui/series_page/series/people_widget/crew_widget.rs new file mode 100644 index 0000000..b5a5b88 --- /dev/null +++ b/src/gui/series_page/series/people_widget/crew_widget.rs @@ -0,0 +1,324 @@ +use crew_poster::{CrewPoster, IndexedMessage, Message as CastMessage}; +use iced::widget::{button, column, container, horizontal_space, row, svg, text, Space}; +use iced::{Command, Element, Length, Renderer}; +use iced_aw::{Spinner, Wrap}; + +use crate::core::{api::tv_maze::people::show_crew::Crew, caching}; +use crate::gui::assets::icons::{CHEVRON_DOWN, CHEVRON_UP}; +use crate::gui::styles; + +const INITIAL_CREW_NUMBER: usize = 20; + +#[derive(Clone, Debug)] +pub enum Message { + CrewReceived(Vec), + Crew(IndexedMessage), + Expand, + Shrink, +} + +enum LoadState { + Loading, + Loaded, +} + +pub struct CrewWidget { + load_state: LoadState, + casts: Vec, + is_expanded: bool, +} + +impl CrewWidget { + pub fn new(series_id: u32) -> (Self, Command) { + let cast_widget = Self { + load_state: LoadState::Loading, + casts: vec![], + is_expanded: false, + }; + + let cast_command = Command::perform(caching::people::get_show_crew(series_id), |crew| { + Message::CrewReceived(crew.expect("failed to get show crew")) + }); + + (cast_widget, cast_command) + } + + pub fn update(&mut self, message: Message) -> Command { + match message { + Message::CrewReceived(cast) => { + self.load_state = LoadState::Loaded; + let mut cast_posters = Vec::with_capacity(cast.len()); + let mut posters_commands = Vec::with_capacity(cast.len()); + for (index, person) in cast.into_iter().enumerate() { + let (cast_poster, poster_command) = CrewPoster::new(index, person); + cast_posters.push(cast_poster); + posters_commands.push(poster_command); + } + self.casts = cast_posters; + Command::batch(posters_commands).map(Message::Crew) + } + Message::Crew(message) => self.casts[message.index()] + .update(message) + .map(Message::Crew), + Message::Expand => { + self.is_expanded = true; + Command::none() + } + Message::Shrink => { + self.is_expanded = false; + Command::none() + } + } + } + + pub fn view(&self) -> Option> { + match self.load_state { + LoadState::Loading => { + let spinner = container(Spinner::new()) + .center_x() + .center_y() + .height(100) + .width(Length::Fill) + .into(); + Some(spinner) + } + LoadState::Loaded => { + if self.casts.is_empty() { + None + } else { + let cast_posters: Vec<_> = self + .casts + .iter() + .enumerate() + .take_while(|(index, _)| self.is_expanded || *index < INITIAL_CREW_NUMBER) + .map(|(_, poster)| poster.view().map(Message::Crew)) + .collect(); + + let content = column![ + Wrap::with_elements(cast_posters) + .padding(5.0) + .line_spacing(10.0) + .spacing(10.0), + self.expansion_widget(), + ] + .into(); + Some(content) + } + } + } + } + + fn expansion_widget(&self) -> Element<'_, Message, Renderer> { + if self.casts.len() > INITIAL_CREW_NUMBER { + let (info, expansion_icon, message) = if self.is_expanded { + let svg_handle = svg::Handle::from_memory(CHEVRON_UP); + let up_icon = svg(svg_handle) + .width(Length::Shrink) + .style(styles::svg_styles::colored_svg_theme()); + (text("show less"), up_icon, Message::Shrink) + } else { + let svg_handle = svg::Handle::from_memory(CHEVRON_DOWN); + let down_icon = svg(svg_handle) + .width(Length::Shrink) + .style(styles::svg_styles::colored_svg_theme()); + (text("show more"), down_icon, Message::Expand) + }; + + let content = row![ + horizontal_space(5), + info, + expansion_icon, + horizontal_space(5), + ] + .spacing(10) + .align_items(iced::Alignment::Center); + + let content = button(content) + .on_press(message) + .style(styles::button_styles::transparent_button_theme()); + + container( + container(content) + .style(styles::container_styles::first_class_container_square_theme()), + ) + .center_x() + .width(Length::Fill) + .padding(20) + .into() + } else { + Space::new(0, 0).into() + } + } +} + +mod crew_poster { + use bytes::Bytes; + use iced::{ + font::Weight, + widget::{column, container, horizontal_space, image, row, text, Column, Row}, + Command, Element, Font, Renderer, + }; + + pub use crate::gui::message::IndexedMessage; + use crate::{ + core::{ + api::tv_maze::{ + people::show_crew::{AgeError, Crew}, + Image, + }, + caching, + }, + gui::{helpers, styles}, + }; + + #[derive(Debug, Clone)] + pub enum Message { + PersonImageLoaded(Option), + } + + pub struct CrewPoster { + index: usize, + crew: Crew, + person_image: Option, + } + + impl CrewPoster { + pub fn new(id: usize, cast: Crew) -> (Self, Command>) { + let image = cast.person.image.clone(); + let poster = Self { + index: id, + crew: cast, + person_image: None, + }; + let poster_command = Self::load_person_image(image); + ( + poster, + poster_command.map(move |message| IndexedMessage::new(id, message)), + ) + } + + pub fn update( + &mut self, + message: IndexedMessage, + ) -> Command> { + let command = match message.message() { + Message::PersonImageLoaded(image) => { + self.person_image = image; + Command::none() + } + }; + let index = self.index; + command.map(move |message| IndexedMessage::new(index, message)) + } + + pub fn view(&self) -> Element<'_, IndexedMessage, Renderer> { + let mut content = Row::new().spacing(10); + + let empty_image = helpers::empty_image::empty_image().width(100).height(140); + + if let Some(image_bytes) = self.person_image.clone() { + let image_handle = image::Handle::from_memory(image_bytes); + + let image = image(image_handle).width(100); + content = content.push(image); + } else { + content = content.push(empty_image); + }; + + let mut cast_info = Column::new().width(150).spacing(3); + + cast_info = cast_info.push(column![ + text(&self.crew.person.name) + .style(styles::text_styles::accent_color_theme()) + .size(15), + text(format!("as {}", &self.crew.kind)).size(11) + ]); + + // A little bit of space between cast name and other information + cast_info = cast_info.push(horizontal_space(20)); + + if let Some(gender) = self.crew.person.gender.as_ref() { + cast_info = cast_info.push(crew_info_field("Gender: ", gender)); + } + + match self.crew.person.age_duration_before_death() { + Ok(age_duration_before_death) => { + if let Some(age) = helpers::time::NaiveTime::new( + age_duration_before_death.num_minutes() as u32, + ) + .largest_part() + { + cast_info = cast_info.push(crew_info_field( + "Lived to: ", + format!("{} {}", age.0, age.1), + )); + } else { + cast_info = + cast_info.push(crew_info_field("Lived to: ", "Just passed away")); + } + } + Err(AgeError::DeathdateNotFound) => { + if let Ok(duration_since_birth) = self.crew.person.duration_since_birth() { + if let Some(age) = + helpers::time::NaiveTime::new(duration_since_birth.num_minutes() as u32) + .largest_part() + { + cast_info = cast_info + .push(crew_info_field("Age: ", format!("{} {}", age.0, age.1))); + } else { + cast_info = cast_info.push(crew_info_field("Age: ", "Just born!")); + } + } + } + Err(_) => {} + } + + if let Some(birthday) = self.crew.person.birthday.as_ref() { + cast_info = cast_info.push(crew_info_field("Birthday: ", birthday)); + } + + if let Some(deathday) = self.crew.person.deathday.as_ref() { + cast_info = cast_info.push(crew_info_field("Deathday: ", deathday)); + } + + if let Some(country) = self.crew.person.country.as_ref() { + cast_info = cast_info.push(crew_info_field("Born in: ", &country.name)); + } + + let content = content.push(cast_info); + + let element: Element<'_, Message, Renderer> = container(content) + .style(styles::container_styles::first_class_container_square_theme()) + .padding(7) + .into(); + element.map(|message| IndexedMessage::new(self.index, message)) + } + + fn load_person_image(image: Option) -> Command { + if let Some(image) = image { + Command::perform( + caching::load_image(image.medium_image_url, caching::ImageResolution::Medium), + Message::PersonImageLoaded, + ) + } else { + Command::none() + } + } + } + + fn crew_info_field( + title: &str, + value: impl std::fmt::Display, + ) -> Element<'_, Message, Renderer> { + row![ + text(title) + .font(Font { + weight: Weight::Bold, + ..Default::default() + }) + .size(12), + text(value).size(12) + ] + .into() + } +} diff --git a/src/gui/series_page/series/people_widget/mod.rs b/src/gui/series_page/series/people_widget/mod.rs new file mode 100644 index 0000000..3297130 --- /dev/null +++ b/src/gui/series_page/series/people_widget/mod.rs @@ -0,0 +1,138 @@ +use iced::widget::{column, container, text, toggler}; + +use iced::{Command, Element, Length, Renderer}; + +use cast_widget::{CastWidget, Message as CastWidgetMessage}; +use crew_widget::{CrewWidget, Message as CrewWidgetMessage}; + +use crate::gui::styles; + +mod cast_widget; +mod crew_widget; + +#[derive(Debug, Clone)] +pub enum Message { + PeopleToggled(bool), + CastWidget(CastWidgetMessage), + CrewWidget(CrewWidgetMessage), +} + +enum People { + Crew, + Cast, +} + +impl std::fmt::Display for People { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + People::Crew => "Crew", + People::Cast => "Cast", + }; + + write!(f, "{str}") + } +} + +pub struct PeopleWidget { + series_id: u32, + current_people: People, + cast_widget: CastWidget, + crew_widget: Option, + toggled: bool, +} + +impl PeopleWidget { + pub fn new(series_id: u32) -> (Self, Command) { + let (cast_widget, cast_widget_command) = CastWidget::new(series_id); + ( + Self { + series_id, + current_people: People::Cast, + cast_widget, + crew_widget: None, + toggled: false, + }, + cast_widget_command.map(Message::CastWidget), + ) + } + + fn fetch_crew(&mut self) -> Command { + let (crew_widget, crew_widget_command) = CrewWidget::new(self.series_id); + self.crew_widget = Some(crew_widget); + + crew_widget_command.map(Message::CrewWidget) + } + + pub fn update(&mut self, message: Message) -> Command { + match message { + Message::CastWidget(message) => { + self.cast_widget.update(message).map(Message::CastWidget) + } + Message::CrewWidget(message) => { + if let Some(ref mut crew_widget) = self.crew_widget { + crew_widget.update(message).map(Message::CrewWidget) + } else { + Command::none() + } + } + Message::PeopleToggled(toggled) => { + self.toggled = toggled; + match self.current_people { + People::Crew => { + self.current_people = People::Cast; + Command::none() + } + People::Cast => { + self.current_people = People::Crew; + if self.crew_widget.is_none() { + self.fetch_crew() + } else { + Command::none() + } + } + } + } + } + } + + pub fn view(&self) -> Element<'_, Message, Renderer> { + let current_people = match self.current_people { + People::Cast => self + .cast_widget + .view() + .map(|view| view.map(Message::CastWidget)), + People::Crew => self + .crew_widget + .as_ref() + .expect("crew should be set already") + .view() + .map(|view| view.map(Message::CrewWidget)), + }; + + let people_toggler = toggler( + Some(self.current_people.to_string()), + self.toggled, + Message::PeopleToggled, + ) + .spacing(5) + .text_size(21) + .style(styles::toggler_styles::always_colored_toggler_theme()) + .width(Length::Shrink); + + column![ + people_toggler, + current_people.unwrap_or(self.people_not_found()), + ] + .padding(5) + .into() + } + + fn people_not_found(&self) -> Element<'_, Message, Renderer> { + container(text(format!("No {} Found!", self.current_people))) + .center_x() + .center_y() + .width(Length::Fill) + .height(100) + .into() + } +} diff --git a/src/gui/styles/colors.rs b/src/gui/styles/colors.rs index 0456b08..53c9765 100644 --- a/src/gui/styles/colors.rs +++ b/src/gui/styles/colors.rs @@ -16,3 +16,7 @@ pub fn purple() -> Color { pub fn green() -> Color { color!(0x008000) } + +pub fn gray() -> Color { + color!(0x282828) +} diff --git a/src/gui/styles/mod.rs b/src/gui/styles/mod.rs index 49827d1..1a0d90e 100644 --- a/src/gui/styles/mod.rs +++ b/src/gui/styles/mod.rs @@ -5,3 +5,4 @@ pub mod scrollable_styles; pub mod svg_styles; pub mod text_styles; pub mod theme; +pub mod toggler_styles; diff --git a/src/gui/styles/toggler_styles.rs b/src/gui/styles/toggler_styles.rs new file mode 100644 index 0000000..96b5c35 --- /dev/null +++ b/src/gui/styles/toggler_styles.rs @@ -0,0 +1,31 @@ +use super::colors::{accent_color, gray}; +use iced::{ + theme::Toggler, + widget::toggler::{Appearance, StyleSheet}, +}; + +pub fn always_colored_toggler_theme() -> Toggler { + Toggler::Custom(Box::new(AlwaysColoredStyle) as Box>) +} + +struct AlwaysColoredStyle; + +impl StyleSheet for AlwaysColoredStyle { + type Style = iced::Theme; + + fn active(&self, _style: &Self::Style, _is_active: bool) -> Appearance { + appearance() + } + + fn hovered(&self, _style: &Self::Style, _is_active: bool) -> Appearance { + appearance() + } +} +fn appearance() -> Appearance { + Appearance { + background: accent_color(), + background_border: None, + foreground: gray(), + foreground_border: None, + } +}