diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7dd84..f375e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] * Ability to mark all episodes of a Show as watched. [#47](https://gitlab.gnome.org/alatiera/Hammond/issues/47) -* Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.[#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49) +* Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show. [#49](https://gitlab.gnome.org/alatiera/Hammond/issues/49) +* EpisdeWidget has been reimplemented as a compile time state machine. [!18](https://gitlab.gnome.org/alatiera/Hammond/merge_requests/18) ## [0.3.0] - 2018-02-11 diff --git a/Cargo.lock b/Cargo.lock index c6f8ab0..965723b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,6 +711,7 @@ dependencies = [ "reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "send-cell 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "take_mut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1654,6 +1655,11 @@ name = "take" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "take_mut" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "tempdir" version = "0.3.6" @@ -2108,6 +2114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" "checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" "checksum take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" +"checksum take_mut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50b910a1174df4aeb5738e8a0e7253883cf7801de40d094175a5a557e487f4c5" "checksum tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f73eebdb68c14bcb24aef74ea96079830e7fa7b31a6106e42ea7ee887c1e134e" "checksum tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9de21546595a0873061940d994bbbc5c35f024ae4fd61ec5c5b159115684f508" "checksum term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" diff --git a/hammond-gtk/Cargo.toml b/hammond-gtk/Cargo.toml index a05fcad..d208950 100644 --- a/hammond-gtk/Cargo.toml +++ b/hammond-gtk/Cargo.toml @@ -22,6 +22,7 @@ send-cell = "0.1.2" url = "1.6.0" failure = "0.1.1" failure_derive = "0.1.1" +take_mut = "0.2.0" regex = "0.2.6" reqwest = "0.8.5" serde_json = "1.0" diff --git a/hammond-gtk/src/main.rs b/hammond-gtk/src/main.rs index ab85959..cd21890 100644 --- a/hammond-gtk/src/main.rs +++ b/hammond-gtk/src/main.rs @@ -1,5 +1,6 @@ #![cfg_attr(feature = "cargo-clippy", - allow(clone_on_ref_ptr, needless_pass_by_value, useless_format))] + allow(clone_on_ref_ptr, needless_pass_by_value, useless_format, blacklisted_name, + match_same_arms))] #![allow(unknown_lints)] #![deny(unused_extern_crates, unused)] @@ -29,6 +30,7 @@ extern crate regex; extern crate reqwest; extern crate send_cell; extern crate serde_json; +extern crate take_mut; extern crate url; // extern crate rayon; diff --git a/hammond-gtk/src/widgets/episode.rs b/hammond-gtk/src/widgets/episode.rs index b705e9e..3ce2b0c 100644 --- a/hammond-gtk/src/widgets/episode.rs +++ b/hammond-gtk/src/widgets/episode.rs @@ -1,13 +1,11 @@ use glib; use gtk; - -use chrono::prelude::*; use gtk::prelude::*; -use chrono::Duration; use failure::Error; -use humansize::{file_size_opts as size_opts, FileSize}; +use humansize::FileSize; use open; +use take_mut; use hammond_data::{EpisodeWidgetQuery, Podcast}; use hammond_data::dbqueries; @@ -15,46 +13,22 @@ use hammond_data::utils::get_download_folder; use app::Action; use manager; +use widgets::episode_states::*; +use std::cell::RefCell; +use std::ops::DerefMut; use std::path::Path; +use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::sync::mpsc::Sender; -lazy_static! { - static ref SIZE_OPTS: Arc = { - // Declare a custom humansize option struct - // See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html - Arc::new(size_opts::FileSizeOpts { - divider: size_opts::Kilo::Binary, - units: size_opts::Kilo::Decimal, - decimal_places: 0, - decimal_zeroes: 0, - fixed_at: size_opts::FixedAt::No, - long_units: false, - space: true, - suffix: "", - allow_negative: false, - }) - }; - - static ref NOW: DateTime = Utc::now(); -} - -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct EpisodeWidget { pub container: gtk::Box, - play: gtk::Button, - download: gtk::Button, - cancel: gtk::Button, - title: gtk::Label, - date: gtk::Label, - duration: gtk::Label, - progress: gtk::ProgressBar, - total_size: gtk::Label, - local_size: gtk::Label, - separator1: gtk::Label, - separator2: gtk::Label, - prog_separator: gtk::Label, + date: RefCell, + duration: RefCell, + title: Rc>, + media: Arc>, } impl Default for EpisodeWidget { @@ -78,75 +52,72 @@ impl Default for EpisodeWidget { let separator2: gtk::Label = builder.get_object("separator2").unwrap(); let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap(); - EpisodeWidget { - container, - progress, - download, + let date_machine = RefCell::new(DateMachine::new(date, 0)); + let dur_machine = RefCell::new(DurationMachine::new(duration, separator1, None)); + let title_machine = Rc::new(RefCell::new(TitleMachine::new(title, false))); + let media = MediaMachine::new( play, + download, + progress, cancel, - title, - duration, - date, total_size, local_size, - separator1, separator2, prog_separator, + ); + let media_machine = Arc::new(Mutex::new(media)); + + EpisodeWidget { + container, + title: title_machine, + duration: dur_machine, + date: date_machine, + media: media_machine, } } } impl EpisodeWidget { pub fn new(episode: EpisodeWidgetQuery, sender: Sender) -> EpisodeWidget { - let widget = EpisodeWidget::default(); + let mut widget = EpisodeWidget::default(); widget.init(episode, sender); widget } - fn init(&self, episode: EpisodeWidgetQuery, sender: Sender) { + fn init(&mut self, episode: EpisodeWidgetQuery, sender: Sender) { WidgetExt::set_name(&self.container, &episode.rowid().to_string()); + // Set the date label. + self.set_date(episode.epoch()); + // Set the title label state. self.set_title(&episode); // Set the duaration label. self.set_duration(episode.duration()); - // Set the date label. - self.set_date(episode.epoch()); - - // Show or hide the play/delete/download buttons upon widget initialization. - if let Err(err) = self.show_buttons(episode.local_uri()) { - debug!("Failed to determine play/download button state."); - debug!("Error: {}", err); - } - - // Set the size label. - if let Err(err) = self.set_total_size(episode.length()) { - error!("Failed to set the Size label."); - error!("Error: {}", err); - } - - // Determine what the state of the progress bar should be. - if let Err(err) = self.determine_progess_bar() { - error!("Something went wrong determining the ProgressBar State."); + // Determine what the state of the media widgets should be. + if let Err(err) = self.determine_media_state(&episode) { + error!("Something went wrong determining the Media State."); error!("Error: {}", err); } let episode = Arc::new(Mutex::new(episode)); + self.connect_buttons(episode, sender); + } + fn connect_buttons(&self, episode: Arc>, sender: Sender) { let title = self.title.clone(); - self.play - .connect_clicked(clone!(episode, sender => move |_| { + if let Ok(media) = self.media.lock() { + media.play_connect_clicked(clone!(episode, sender => move |_| { if let Ok(mut ep) = episode.lock() { - if let Err(err) = on_play_bttn_clicked(&mut ep, &title, sender.clone()){ + if let Err(err) = on_play_bttn_clicked(&mut ep, title.clone(), sender.clone()){ error!("Error: {}", err); }; } - })); + })); - self.download - .connect_clicked(clone!(episode, sender => move |dl| { + media.download_connect_clicked(clone!(episode, sender => move |dl| { dl.set_sensitive(false); if let Ok(ep) = episode.lock() { if let Err(err) = on_download_clicked(&ep, sender.clone()) { @@ -156,74 +127,32 @@ impl EpisodeWidget { info!("Donwload started succesfully."); } } - })); - } - - /// Show or hide the play/delete/download buttons upon widget - /// initialization. - fn show_buttons(&self, local_uri: Option<&str>) -> Result<(), Error> { - let path = local_uri.ok_or_else(|| format_err!("Path is None"))?; - if Path::new(path).exists() { - self.download.hide(); - self.play.show(); + })); } - Ok(()) } /// Determine the title state. - fn set_title(&self, episode: &EpisodeWidgetQuery) { - self.title.set_text(episode.title()); - - // Grey out the title if the episode is played. - if episode.played().is_some() { - self.title - .get_style_context() - .map(|c| c.add_class("dim-label")); - } + fn set_title(&mut self, episode: &EpisodeWidgetQuery) { + let mut machine = self.title.borrow_mut(); + machine.set_title(episode.title()); + take_mut::take(machine.deref_mut(), |title| { + title.determine_state(episode.played().is_some()) + }); } /// Set the date label depending on the current time. - fn set_date(&self, epoch: i32) { - let date = Utc.timestamp(i64::from(epoch), 0); - if NOW.year() == date.year() { - self.date.set_text(date.format("%e %b").to_string().trim()); - } else { - self.date - .set_text(date.format("%e %b %Y").to_string().trim()); - }; + fn set_date(&mut self, epoch: i32) { + let machine = self.date.get_mut(); + take_mut::take(machine, |date| date.determine_state(i64::from(epoch))); } /// Set the duration label. - fn set_duration(&self, seconds: Option) -> Option<()> { - let minutes = Duration::seconds(seconds?.into()).num_minutes(); - if minutes == 0 { - return None; - } - - self.duration.set_text(&format!("{} min", minutes)); - self.duration.show(); - self.separator1.show(); - Some(()) + fn set_duration(&mut self, seconds: Option) { + let machine = self.duration.get_mut(); + take_mut::take(machine, |duration| duration.determine_state(seconds)); } - /// Set the Episode label dependings on its size - fn set_total_size(&self, bytes: Option) -> Result<(), Error> { - let size = bytes.ok_or_else(|| format_err!("Size is None."))?; - if size == 0 { - bail!("Size is 0."); - } - - let s = size.file_size(SIZE_OPTS.clone()) - .map_err(|err| format_err!("{}", err))?; - self.total_size.set_text(&s); - self.total_size.show(); - self.separator2.show(); - Ok(()) - } - - // FIXME: REFACTOR ME - // Something Something State-Machine? - fn determine_progess_bar(&self) -> Result<(), Error> { + fn determine_media_state(&self, episode: &EpisodeWidgetQuery) -> Result<(), Error> { let id = WidgetExt::get_name(&self.container) .ok_or_else(|| format_err!("Failed to get widget Name"))? .parse::()?; @@ -236,40 +165,34 @@ impl EpisodeWidget { Ok(m.get(&id).cloned()) }()?; - if let Some(prog) = active_dl { - // FIXME: Document me? - self.download.hide(); - self.progress.show(); - self.local_size.show(); - self.total_size.show(); - self.separator2.show(); - self.prog_separator.show(); - self.cancel.show(); + let mut lock = self.media.lock().map_err(|err| format_err!("{}", err))?; + take_mut::take(lock.deref_mut(), |media| { + media.determine_state( + episode.length(), + active_dl.is_some(), + episode.local_uri().is_some(), + ) + }); - let progress_bar = self.progress.clone(); - let total_size = self.total_size.clone(); - let local_size = self.local_size.clone(); + // Show or hide the play/delete/download buttons upon widget initialization. + if let Some(prog) = active_dl { + lock.cancel_connect_clicked(prog.clone()); + drop(lock); // Setup a callback that will update the progress bar. - update_progressbar_callback(prog.clone(), id, &progress_bar, &local_size); + update_progressbar_callback(prog.clone(), self.media.clone(), id); // Setup a callback that will update the total_size label // with the http ContentLength header number rather than // relying to the RSS feed. - update_total_size_callback(prog.clone(), &total_size); - - self.cancel.connect_clicked(clone!(prog => move |cancel| { - if let Ok(mut m) = prog.lock() { - m.cancel(); - cancel.set_sensitive(false); - } - })); + update_total_size_callback(prog.clone(), self.media.clone()); } Ok(()) } } +#[inline] fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender) -> Result<(), Error> { let pd = dbqueries::get_podcast_from_id(ep.podcast_id())?; let download_fold = get_download_folder(&pd.title().to_owned())?; @@ -284,18 +207,21 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: Sender) -> Resul Ok(()) } +#[inline] fn on_play_bttn_clicked( episode: &mut EpisodeWidgetQuery, - title: >k::Label, + title: Rc>, sender: Sender, ) -> Result<(), Error> { open_uri(episode.rowid())?; + episode.set_played_now()?; - if episode.set_played_now().is_ok() { - title.get_style_context().map(|c| c.add_class("dim-label")); - sender.send(Action::RefreshEpisodesViewBGR)?; - }; + let mut machine = title.try_borrow_mut()?; + take_mut::take(machine.deref_mut(), |title| { + title.determine_state(episode.played().is_some()) + }); + sender.send(Action::RefreshEpisodesViewBGR)?; Ok(()) } @@ -314,28 +240,28 @@ fn open_uri(rowid: i32) -> Result<(), Error> { } // Setup a callback that will update the progress bar. +#[inline] #[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))] fn update_progressbar_callback( prog: Arc>, + media: Arc>, episode_rowid: i32, - progress_bar: >k::ProgressBar, - local_size: >k::Label, ) { timeout_add( 400, - clone!(prog, progress_bar, progress_bar, local_size=> move || { - progress_bar_helper(prog.clone(), episode_rowid, &progress_bar, &local_size) + clone!(prog, media => move || { + progress_bar_helper(prog.clone(), media.clone(), episode_rowid) .unwrap_or(glib::Continue(false)) }), ); } +#[inline] #[allow(if_same_then_else)] fn progress_bar_helper( prog: Arc>, + media: Arc>, episode_rowid: i32, - progress_bar: >k::ProgressBar, - local_size: >k::Label, ) -> Result { let (fraction, downloaded) = { let m = prog.lock() @@ -343,16 +269,16 @@ fn progress_bar_helper( (m.get_fraction(), m.get_downloaded()) }; - // Update local_size label - downloaded - .file_size(SIZE_OPTS.clone()) - .map_err(|err| format_err!("{}", err)) - .map(|x| local_size.set_text(&x))?; - // I hate floating points. // Update the progress_bar. if (fraction >= 0.0) && (fraction <= 1.0) && (!fraction.is_nan()) { - progress_bar.set_fraction(fraction); + // Update local_size label + let size = downloaded + .file_size(SIZE_OPTS.clone()) + .map_err(|err| format_err!("{}", err))?; + + let mut m = media.lock().unwrap(); + m.update_progress(&size, fraction); } // info!("Fraction: {}", progress_bar.get_fraction()); @@ -378,18 +304,23 @@ fn progress_bar_helper( // Setup a callback that will update the total_size label // with the http ContentLength header number rather than // relying to the RSS feed. -fn update_total_size_callback(prog: Arc>, total_size: >k::Label) { +#[inline] +fn update_total_size_callback( + prog: Arc>, + media: Arc>, +) { timeout_add( 500, - clone!(prog, total_size => move || { - total_size_helper(prog.clone(), &total_size).unwrap_or(glib::Continue(true)) + clone!(prog, media => move || { + total_size_helper(prog.clone(), media.clone()).unwrap_or(glib::Continue(true)) }), ); } +#[inline] fn total_size_helper( prog: Arc>, - total_size: >k::Label, + media: Arc>, ) -> Result { // Get the total_bytes. let total_bytes = { @@ -401,10 +332,12 @@ fn total_size_helper( debug!("Total Size: {}", total_bytes); if total_bytes != 0 { // Update the total_size label - total_bytes - .file_size(SIZE_OPTS.clone()) - .map_err(|err| format_err!("{}", err)) - .map(|x| total_size.set_text(&x))?; + if let Ok(mut m) = media.lock() { + take_mut::take(m.deref_mut(), |machine| { + machine.set_size(Some(total_bytes as i32)) + }); + } + // Do not call again the callback Ok(glib::Continue(false)) } else { diff --git a/hammond-gtk/src/widgets/episode_states.rs b/hammond-gtk/src/widgets/episode_states.rs new file mode 100644 index 0000000..4933e2f --- /dev/null +++ b/hammond-gtk/src/widgets/episode_states.rs @@ -0,0 +1,917 @@ +// TODO: Things that should be done. +// +// * Wherever there's a function that take 2 or more arguments of the same type, +// eg: fn new(total_size: gtk::Label, local_size: gtk::Label ..) +// Wrap the types into Struct-tuples and imple deref so it won't be possible to pass +// the wrong argument to the wrong position. + +use chrono; +use glib; +use gtk; + +use chrono::prelude::*; +use gtk::prelude::*; +use humansize::{file_size_opts as size_opts, FileSize}; + +use std::sync::{Arc, Mutex}; + +use manager::Progress as OtherProgress; + +lazy_static! { + pub static ref SIZE_OPTS: Arc = { + // Declare a custom humansize option struct + // See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html + Arc::new(size_opts::FileSizeOpts { + divider: size_opts::Kilo::Binary, + units: size_opts::Kilo::Decimal, + decimal_places: 0, + decimal_zeroes: 0, + fixed_at: size_opts::FixedAt::No, + long_units: false, + space: true, + suffix: "", + allow_negative: false, + }) + }; + + static ref NOW: DateTime = Utc::now(); +} + +#[derive(Debug, Clone)] +pub struct UnInitialized; + +#[derive(Debug, Clone)] +pub struct Shown; +#[derive(Debug, Clone)] +pub struct Hidden; + +pub trait Visibility {} + +impl Visibility for Shown {} +impl Visibility for Hidden {} + +#[derive(Debug, Clone)] +pub struct Normal; +#[derive(Debug, Clone)] +pub struct GreyedOut; + +#[derive(Debug, Clone)] +pub struct Title { + title: gtk::Label, + state: S, +} + +impl Title { + #[allow(unused_must_use)] + // This does not need to be &mut since gtk-rs does not model ownership + // But I think it wouldn't hurt if we treat it as a Rust api. + fn set_title(&mut self, s: &str) { + self.title.set_text(s); + } +} + +impl Title { + fn new(title: gtk::Label) -> Self { + Title { + title, + state: Normal {}, + } + } +} + +impl From> for Title { + fn from(f: Title) -> Self { + f.title + .get_style_context() + .map(|c| c.add_class("dim-label")); + + Title { + title: f.title, + state: GreyedOut {}, + } + } +} + +impl From> for Title { + fn from(f: Title) -> Self { + f.title + .get_style_context() + .map(|c| c.remove_class("dim-label")); + + Title { + title: f.title, + state: Normal {}, + } + } +} + +#[derive(Debug, Clone)] +pub enum TitleMachine { + Normal(Title), + GreyedOut(Title), +} + +impl TitleMachine { + pub fn new(label: gtk::Label, is_played: bool) -> Self { + let m = TitleMachine::Normal(Title::::new(label)); + m.determine_state(is_played) + } + + pub fn determine_state(self, is_played: bool) -> Self { + use self::TitleMachine::*; + + match (self, is_played) { + (title @ Normal(_), false) => title, + (title @ GreyedOut(_), true) => title, + (Normal(val), true) => GreyedOut(val.into()), + (GreyedOut(val), false) => Normal(val.into()), + } + } + + pub fn set_title(&mut self, s: &str) { + use self::TitleMachine::*; + + match *self { + Normal(ref mut val) => val.set_title(s), + GreyedOut(ref mut val) => val.set_title(s), + } + } +} + +#[derive(Debug, Clone)] +pub struct Usual; +#[derive(Debug, Clone)] +pub struct YearShown; + +#[derive(Debug, Clone)] +pub struct Date { + date: gtk::Label, + epoch: i64, + state: S, +} + +impl Date { + fn into_usual(self, epoch: i64) -> Date { + let ts = Utc.timestamp(epoch, 0); + self.date.set_text(ts.format("%e %b").to_string().trim()); + + Date { + date: self.date, + epoch: self.epoch, + state: Usual {}, + } + } + + fn into_year_shown(self, epoch: i64) -> Date { + let ts = Utc.timestamp(epoch, 0); + self.date.set_text(ts.format("%e %b %Y").to_string().trim()); + + Date { + date: self.date, + epoch: self.epoch, + state: YearShown {}, + } + } +} + +impl Date { + fn new(date: gtk::Label, epoch: i64) -> Self { + let ts = Utc.timestamp(epoch, 0); + date.set_text(ts.format("%e %b %Y").to_string().trim()); + + Date { + date, + epoch, + state: UnInitialized {}, + } + } +} + +#[derive(Debug, Clone)] +pub enum DateMachine { + UnInitialized(Date), + Usual(Date), + WithYear(Date), +} + +impl DateMachine { + pub fn new(label: gtk::Label, epoch: i64) -> Self { + let m = DateMachine::UnInitialized(Date::::new(label, epoch)); + m.determine_state(epoch) + } + + pub fn determine_state(self, epoch: i64) -> Self { + use self::DateMachine::*; + + let ts = Utc.timestamp(epoch, 0); + let is_old = !(NOW.year() == ts.year()); + + match (self, is_old) { + // Into Usual + (Usual(val), false) => Usual(val.into_usual(epoch)), + (WithYear(val), false) => Usual(val.into_usual(epoch)), + (UnInitialized(val), false) => Usual(val.into_usual(epoch)), + + // Into Year Shown + (Usual(val), true) => WithYear(val.into_year_shown(epoch)), + (WithYear(val), true) => WithYear(val.into_year_shown(epoch)), + (UnInitialized(val), true) => WithYear(val.into_year_shown(epoch)), + } + } +} + +#[derive(Debug, Clone)] +pub struct Duration { + // TODO: make duration and separator diff types + duration: gtk::Label, + separator: gtk::Label, + state: S, +} + +impl Duration { + // This needs a better name. + // TODO: make me mut + fn set_duration(&self, minutes: i64) { + self.duration.set_text(&format!("{} min", minutes)); + } +} + +impl Duration { + fn new(duration: gtk::Label, separator: gtk::Label) -> Self { + duration.hide(); + separator.hide(); + + Duration { + duration, + separator, + state: Hidden {}, + } + } +} + +impl From> for Duration { + fn from(f: Duration) -> Self { + f.duration.show(); + f.separator.show(); + + Duration { + duration: f.duration, + separator: f.separator, + state: Shown {}, + } + } +} + +impl From> for Duration { + fn from(f: Duration) -> Self { + f.duration.hide(); + f.separator.hide(); + + Duration { + duration: f.duration, + separator: f.separator, + state: Hidden {}, + } + } +} + +#[derive(Debug, Clone)] +pub enum DurationMachine { + Hidden(Duration), + Shown(Duration), +} + +impl DurationMachine { + pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option) -> Self { + let m = DurationMachine::Hidden(Duration::::new(duration, separator)); + m.determine_state(seconds) + } + + pub fn determine_state(self, seconds: Option) -> Self { + match (self, seconds) { + (d @ DurationMachine::Hidden(_), None) => d, + (DurationMachine::Shown(val), None) => DurationMachine::Hidden(val.into()), + (DurationMachine::Hidden(val), Some(s)) => { + let minutes = chrono::Duration::seconds(s.into()).num_minutes(); + if minutes == 0 { + DurationMachine::Hidden(val) + } else { + val.set_duration(minutes); + DurationMachine::Shown(val.into()) + } + } + (DurationMachine::Shown(val), Some(s)) => { + let minutes = chrono::Duration::seconds(s.into()).num_minutes(); + if minutes == 0 { + DurationMachine::Hidden(val.into()) + } else { + val.set_duration(minutes); + DurationMachine::Shown(val) + } + } + } + } +} + +#[derive(Debug, Clone)] +pub struct Size { + size: gtk::Label, + separator: gtk::Label, + state: S, +} + +impl Size { + fn set_size(self, s: &str) -> Size { + self.size.set_text(s); + self.size.show(); + self.separator.show(); + Size { + size: self.size, + separator: self.separator, + state: Shown {}, + } + } + + // https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable + // It might be possible to make a generic definition with Specialization. + // https://github.com/rust-lang/rust/issues/31844 + fn into_shown(self) -> Size { + self.size.show(); + self.separator.show(); + + Size { + size: self.size, + separator: self.separator, + state: Shown {}, + } + } + + fn into_hidden(self) -> Size { + self.size.hide(); + self.separator.hide(); + + Size { + size: self.size, + separator: self.separator, + state: Hidden {}, + } + } +} + +impl Size { + fn new(size: gtk::Label, separator: gtk::Label) -> Self { + size.hide(); + separator.hide(); + + Size { + size, + separator, + state: UnInitialized {}, + } + } +} + +// pub trait Playable {} + +// impl Playable for Download {} +// impl Playable for Play {} + +#[derive(Debug, Clone)] +pub struct Download; + +#[derive(Debug, Clone)] +pub struct Play; + +#[derive(Debug, Clone)] +// FIXME: Needs better name. +// Should each button also has it's own type and machine? +pub struct DownloadPlay { + play: gtk::Button, + download: gtk::Button, + state: S, +} + +impl DownloadPlay { + // https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable + // It might be possible to make a generic definition with Specialization. + // https://github.com/rust-lang/rust/issues/31844 + fn into_playable(self) -> DownloadPlay { + self.play.show(); + self.download.hide(); + + DownloadPlay { + play: self.play, + download: self.download, + state: Play {}, + } + } + + fn into_fetchable(self) -> DownloadPlay { + self.play.hide(); + self.download.show(); + + DownloadPlay { + play: self.play, + download: self.download, + state: Download {}, + } + } + + fn into_hidden(self) -> DownloadPlay { + self.play.hide(); + self.download.hide(); + + DownloadPlay { + play: self.play, + download: self.download, + state: Hidden {}, + } + } + + fn download_connect_clicked( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.download.connect_clicked(f) + } + + fn play_connect_clicked(&self, f: F) -> glib::SignalHandlerId { + self.play.connect_clicked(f) + } +} + +impl DownloadPlay { + fn new(play: gtk::Button, download: gtk::Button) -> Self { + play.hide(); + download.hide(); + + DownloadPlay { + play, + download, + state: UnInitialized {}, + } + } +} + +#[derive(Debug, Clone)] +pub struct Progress { + bar: gtk::ProgressBar, + cancel: gtk::Button, + local_size: gtk::Label, + prog_separator: gtk::Label, + state: S, +} + +impl Progress { + fn into_shown(self) -> Progress { + self.bar.show(); + self.cancel.show(); + self.local_size.show(); + self.prog_separator.show(); + + Progress { + bar: self.bar, + cancel: self.cancel, + local_size: self.local_size, + prog_separator: self.prog_separator, + state: Shown {}, + } + } + + fn into_hidden(self) -> Progress { + self.bar.hide(); + self.cancel.hide(); + self.local_size.hide(); + self.prog_separator.hide(); + + Progress { + bar: self.bar, + cancel: self.cancel, + local_size: self.local_size, + prog_separator: self.prog_separator, + state: Hidden {}, + } + } + + #[allow(unused_must_use)] + // This does not need to be &mut since gtk-rs does not model ownership + // But I think it wouldn't hurt if we treat it as a Rust api. + fn update_progress(&mut self, local_size: &str, fraction: f64) { + self.local_size.set_text(local_size); + self.bar.set_fraction(fraction); + } + + fn cancel_connect_clicked(&self, prog: Arc>) -> glib::SignalHandlerId { + self.cancel.connect_clicked(move |cancel| { + if let Ok(mut m) = prog.lock() { + m.cancel(); + cancel.set_sensitive(false); + } + }) + } +} + +impl Progress { + fn new( + bar: gtk::ProgressBar, + cancel: gtk::Button, + local_size: gtk::Label, + prog_separator: gtk::Label, + ) -> Self { + bar.hide(); + cancel.hide(); + local_size.hide(); + prog_separator.hide(); + + Progress { + bar, + cancel, + local_size, + prog_separator, + state: UnInitialized {}, + } + } +} + +#[derive(Debug, Clone)] +pub struct Media { + dl: DownloadPlay, + size: Size, + progress: Progress, +} + +type New = Media