diff --git a/Cargo.lock b/Cargo.lock index 6a119f8..9a789da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,7 +741,6 @@ dependencies = [ "reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "send-cell 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "take_mut 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1799,11 +1798,6 @@ name = "take" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "take_mut" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "tempdir" version = "0.3.7" @@ -2403,7 +2397,6 @@ 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.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9de21546595a0873061940d994bbbc5c35f024ae4fd61ec5c5b159115684f508" "checksum term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e6b677dd1e8214ea1ef4297f85dbcbed8e8cdddb561040cc998ca2551c37561" diff --git a/hammond-gtk/Cargo.toml b/hammond-gtk/Cargo.toml index 316ebd8..d8cd1bd 100644 --- a/hammond-gtk/Cargo.toml +++ b/hammond-gtk/Cargo.toml @@ -21,7 +21,6 @@ send-cell = "0.1.3" url = "1.7.0" failure = "0.1.1" failure_derive = "0.1.1" -take_mut = "0.2.2" regex = "1.0.0" reqwest = "0.8.5" serde_json = "1.0.17" diff --git a/hammond-gtk/resources/gtk/episode_widget.ui b/hammond-gtk/resources/gtk/episode_widget.ui index 4b55fc3..0323169 100644 --- a/hammond-gtk/resources/gtk/episode_widget.ui +++ b/hammond-gtk/resources/gtk/episode_widget.ui @@ -41,37 +41,26 @@ Tobias Bernard True False - + True False vertical 6 - + True False - - - True - False - Episode Title - end - 55 - True - False - 1 - 0 - - - False - False - 0 - - + Episode Title + end + 55 + True + False + 1 + 0 False - False + True 0 @@ -213,16 +202,16 @@ Tobias Bernard - + True False - 6 + center Cancel True False - True + center center @@ -237,8 +226,8 @@ Tobias Bernard True True True - True Download this episode + center center True @@ -252,8 +241,8 @@ Tobias Bernard False False - end - 2 + 1 + True @@ -262,6 +251,7 @@ Tobias Bernard True True Play this episode + center center @@ -274,15 +264,14 @@ Tobias Bernard False False - end - 3 + 2 + True False False - 6 end 1 diff --git a/hammond-gtk/src/main.rs b/hammond-gtk/src/main.rs index 5e11002..083f19c 100644 --- a/hammond-gtk/src/main.rs +++ b/hammond-gtk/src/main.rs @@ -38,7 +38,6 @@ extern crate regex; extern crate reqwest; extern crate send_cell; extern crate serde_json; -extern crate take_mut; extern crate url; use log::Level; diff --git a/hammond-gtk/src/widgets/episode.rs b/hammond-gtk/src/widgets/episode.rs index 5e2bf4e..582839d 100644 --- a/hammond-gtk/src/widgets/episode.rs +++ b/hammond-gtk/src/widgets/episode.rs @@ -2,11 +2,12 @@ use glib; use gtk; use gtk::prelude::*; +use chrono; +use chrono::prelude::*; use crossbeam_channel::Sender; use failure::Error; -use humansize::FileSize; +use humansize::{file_size_opts as size_opts, FileSize}; use open; -use take_mut; use hammond_data::dbqueries; use hammond_data::utils::get_download_folder; @@ -14,223 +15,417 @@ use hammond_data::EpisodeWidgetQuery; 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}; -#[derive(Debug)] +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, + }) + }; +} + +#[derive(Clone, Debug)] pub struct EpisodeWidget { pub container: gtk::Box, - date: DateMachine, - duration: DurationMachine, - title: Rc>, - media: Rc>, + info: InfoLabels, + buttons: Buttons, + progressbar: gtk::ProgressBar, +} + +#[derive(Clone, Debug)] +struct InfoLabels { + container: gtk::Box, + title: gtk::Label, + date: gtk::Label, + separator1: gtk::Label, + duration: gtk::Label, + separator2: gtk::Label, + local_size: gtk::Label, + size_separator: gtk::Label, + total_size: gtk::Label, +} + +#[derive(Clone, Debug)] +struct Buttons { + container: gtk::ButtonBox, + play: gtk::Button, + download: gtk::Button, + cancel: gtk::Button, +} + +impl InfoLabels { + fn init(&self, episode: &EpisodeWidgetQuery) { + // Set the title label state. + self.set_title(episode); + + // Set the date label. + self.set_date(episode.epoch()); + + // Set the duaration label. + self.set_duration(episode.duration()); + + // Set the total_size label. + self.set_size(episode.length()) + } + + fn set_title(&self, episode: &EpisodeWidgetQuery) { + self.title.set_text(episode.title()); + + if episode.played().is_some() { + self.title + .get_style_context() + .map(|c| c.add_class("dim-label")); + } else { + self.title + .get_style_context() + .map(|c| c.remove_class("dim-label")); + } + } + + // Set the date label of the episode widget. + fn set_date(&self, epoch: i32) { + lazy_static! { + static ref NOW: DateTime = Utc::now(); + }; + + let ts = Utc.timestamp(i64::from(epoch), 0); + + // If the episode is from a different year, print year as well + if NOW.year() != ts.year() { + self.date.set_text(ts.format("%e %b %Y").to_string().trim()); + // Else omit the year from the label + } else { + self.date.set_text(ts.format("%e %b").to_string().trim()); + } + } + + // Set the duration label of the episode widget. + fn set_duration(&self, seconds: Option) { + // If lenght is provided + if let Some(s) = seconds { + // Convert seconds to minutes + let minutes = chrono::Duration::seconds(s.into()).num_minutes(); + // If the lenght is 1 or more minutes + if minutes != 0 { + // Set the label and show them. + self.duration.set_text(&format!("{} min", minutes)); + self.duration.show(); + self.separator1.show(); + return; + } + } + + // Else hide the labels + self.separator1.hide(); + self.duration.hide(); + } + + // Set the size label of the episode widget. + fn set_size(&self, bytes: Option) { + // Convert the bytes to a String label + let size = || -> Option { + let s = bytes?; + if s == 0 { + return None; + } + + s.file_size(SIZE_OPTS.clone()).ok() + }; + + if let Some(s) = size() { + self.total_size.set_text(&s); + self.total_size.show(); + self.separator2.show(); + } else { + self.total_size.hide(); + self.separator2.hide(); + } + } } impl Default for EpisodeWidget { fn default() -> Self { let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/episode_widget.ui"); - let container: gtk::Box = builder.get_object("episode_container").unwrap(); - let progress: gtk::ProgressBar = builder.get_object("progress_bar").unwrap(); + let container = builder.get_object("episode_container").unwrap(); + let progressbar = builder.get_object("progress_bar").unwrap(); - let download: gtk::Button = builder.get_object("download_button").unwrap(); - let play: gtk::Button = builder.get_object("play_button").unwrap(); - let cancel: gtk::Button = builder.get_object("cancel_button").unwrap(); + let buttons_container = builder.get_object("button_box").unwrap(); + let download = builder.get_object("download_button").unwrap(); + let play = builder.get_object("play_button").unwrap(); + let cancel = builder.get_object("cancel_button").unwrap(); - let title: gtk::Label = builder.get_object("title_label").unwrap(); - let date: gtk::Label = builder.get_object("date_label").unwrap(); - let duration: gtk::Label = builder.get_object("duration_label").unwrap(); - let local_size: gtk::Label = builder.get_object("local_size").unwrap(); - let total_size: gtk::Label = builder.get_object("total_size").unwrap(); + let info_container = builder.get_object("info_container").unwrap(); + let title = builder.get_object("title_label").unwrap(); + let date = builder.get_object("date_label").unwrap(); + let duration = builder.get_object("duration_label").unwrap(); + let local_size = builder.get_object("local_size").unwrap(); + let total_size = builder.get_object("total_size").unwrap(); - let separator1: gtk::Label = builder.get_object("separator1").unwrap(); - let separator2: gtk::Label = builder.get_object("separator2").unwrap(); - let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap(); + let separator1 = builder.get_object("separator1").unwrap(); + let separator2 = builder.get_object("separator2").unwrap(); - let date_machine = DateMachine::new(date, 0); - let dur_machine = DurationMachine::new(duration, separator1, None); - let title_machine = Rc::new(RefCell::new(TitleMachine::new(title, false))); - let media = MediaMachine::new( - play, - download, - progress, - cancel, - total_size, - local_size, - separator2, - prog_separator, - ); - let media_machine = Rc::new(RefCell::new(media)); + let size_separator = builder.get_object("prog_separator").unwrap(); EpisodeWidget { + info: InfoLabels { + container: info_container, + title, + date, + separator1, + duration, + separator2, + local_size, + total_size, + size_separator, + }, + buttons: Buttons { + container: buttons_container, + play, + download, + cancel, + }, + progressbar, container, - title: title_machine, - duration: dur_machine, - date: date_machine, - media: media_machine, } } } impl EpisodeWidget { - pub fn new(episode: EpisodeWidgetQuery, sender: &Sender) -> EpisodeWidget { - let mut widget = EpisodeWidget::default(); - widget.init(episode, sender); + pub fn new(episode: &EpisodeWidgetQuery, sender: &Sender) -> Rc { + let widget = Rc::new(Self::default()); + widget.info.init(episode); + Self::determine_buttons_state(&widget, episode, sender) + .map_err(|err| error!("Error: {}", err)) + .ok(); widget } - fn init(&mut self, episode: EpisodeWidgetQuery, sender: &Sender) { - // Set the date label. - self.set_date(episode.epoch()); + // fn init(widget: Rc, sender: &Sender) {} - // Set the title label state. - self.set_title(&episode); + // InProgress State: + // * Show ProgressBar and Cancel Button. + // * Show `total_size`, `local_size` labels and `size_separator`. + // * Hide Download and Play Buttons + fn state_prog(&self) { + self.progressbar.show(); + self.buttons.cancel.show(); - // Set the duaration label. - self.set_duration(episode.duration()); + self.info.total_size.show(); + self.info.local_size.show(); + self.info.size_separator.show(); - // Determine what the state of the media widgets should be. - determine_media_state(&self.media, &episode) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Could not determine Media State")) - .ok(); - - let episode = Arc::new(Mutex::new(episode)); - self.connect_buttons(&episode, sender); + self.buttons.play.hide(); + self.buttons.download.hide(); } - fn connect_buttons(&self, episode: &Arc>, sender: &Sender) { - let title = self.title.clone(); - if let Ok(media) = self.media.try_borrow_mut() { - media.play_connect_clicked(clone!(episode, sender => move |_| { - if let Ok(mut ep) = episode.lock() { - on_play_bttn_clicked(&mut ep, &title, &sender) - .map_err(|err| error!("Error: {}", err)) - .ok(); + // Playable State: + // * Hide ProgressBar and Cancel, Download Buttons. + // * Hide `local_size` labels and `size_separator`. + // * Show Play Button and `total_size` label + fn state_playable(&self) { + self.progressbar.hide(); + self.buttons.cancel.hide(); + self.buttons.download.hide(); + self.info.local_size.hide(); + self.info.size_separator.hide(); + + self.info.total_size.show(); + self.buttons.play.show(); + } + + // ToDownload State: + // * Hide ProgressBar and Cancel, Play Buttons. + // * Hide `local_size` labels and `size_separator`. + // * Show Download Button + // * Determine `total_size` label state (Comes from `episode.lenght`). + fn state_download(&self) { + self.progressbar.hide(); + self.buttons.cancel.hide(); + self.buttons.play.hide(); + + self.info.local_size.hide(); + self.info.size_separator.hide(); + + self.buttons.download.show(); + } + + fn update_progress(&self, local_size: &str, fraction: f64) { + self.info.local_size.set_text(local_size); + self.progressbar.set_fraction(fraction); + } + + /// Change the state of the `EpisodeWidget`. + /// + /// Function Flowchart: + /// + /// ------------------- -------------- + /// | Is the Episode | YES | State: | + /// | currently being | ----> | InProgress | + /// | downloaded? | | | + /// ------------------- -------------- + /// | + /// | NO + /// | + /// \_/ + /// ------------------- -------------- + /// | is the episode | YES | State: | + /// | downloaded | ----> | Playable | + /// | already? | | | + /// ------------------- -------------- + /// | + /// | NO + /// | + /// \_/ + /// ------------------- + /// | State: | + /// | ToDownload | + /// ------------------- + fn determine_buttons_state( + widget: &Rc, + episode: &EpisodeWidgetQuery, + sender: &Sender, + ) -> Result<(), Error> { + // Reset the buttons state no matter the glade file. + // This is just to make it easier to port to relm in the future. + widget.buttons.cancel.hide(); + widget.buttons.play.hide(); + widget.buttons.download.hide(); + + // Check if the episode is being downloaded + let id = episode.rowid(); + let active_dl = move || -> Result, Error> { + let m = manager::ACTIVE_DOWNLOADS + .read() + .map_err(|_| format_err!("Failed to get a lock on the mutex."))?; + + Ok(m.get(&id).cloned()) + }; + + // State: InProggress + if let Some(prog) = active_dl()? { + // set a callback that will update the state when the download finishes + let callback = clone!(widget, sender => move || { + if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() { + if !guard.contains_key(&id) { + if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) { + Self::determine_buttons_state(&widget, &ep, &sender) + .map_err(|err| error!("Error: {}", err)) + .ok(); + + return glib::Continue(false) + } + } } + + glib::Continue(true) + }); + gtk::timeout_add(250, callback); + + // Wire the cancel button + widget + .buttons + .cancel + .connect_clicked(clone!(prog, widget, sender => move |_| { + // Cancel the download + if let Ok(mut m) = prog.lock() { + m.cancel(); + } + + // Cancel is not instant so we have to wait a bit + timeout_add(50, clone!(widget, sender => move || { + if let Ok(thing) = active_dl() { + if thing.is_none() { + // Recalculate the widget state + dbqueries::get_episode_widget_from_rowid(id) + .map_err(From::from) + .and_then(|ep| Self::determine_buttons_state(&widget, &ep, &sender)) + .map_err(|err| error!("Error: {}", err)) + .ok(); + + return glib::Continue(false) + } + } + + glib::Continue(true) + })); })); - let media_machine = self.media.clone(); - media.download_connect_clicked(clone!(media_machine, episode, sender => move |dl| { + // 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(&widget, &prog); + + // Setup a callback that will update the progress bar. + update_progressbar_callback(&widget, &prog, id); + + // Change the widget layout/state + widget.state_prog(); + + return Ok(()); + } + + // State: Playable + if episode.local_uri().is_some() { + // Change the widget layout/state + widget.state_playable(); + + // Wire the play button + widget + .buttons + .play + .connect_clicked(clone!(widget, sender => move |_| { + if let Ok(mut ep) = dbqueries::get_episode_widget_from_rowid(id) { + on_play_bttn_clicked(&widget, &mut ep, &sender) + .map_err(|err| error!("Error: {}", err)) + .ok(); + } + })); + + return Ok(()); + } + + // State: ToDownload + // Wire the download button + widget + .buttons + .download + .connect_clicked(clone!(widget, sender => move |dl| { // Make the button insensitive so it won't be pressed twice dl.set_sensitive(false); - if let Ok(ep) = episode.lock() { + if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) { on_download_clicked(&ep, &sender) .and_then(|_| { info!("Donwload started succesfully."); - determine_media_state(&media_machine, &ep) - }) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Could not determine Media State")) - .ok(); + Self::determine_buttons_state(&widget, &ep, &sender) + }) + .map_err(|err| error!("Error: {}", err)) + .ok(); } // Restore sensitivity after operations above complete dl.set_sensitive(true); })); - } + + // Change the widget state into `ToDownload` + widget.state_download(); + + Ok(()) } - - /// Determine the title state. - 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(&mut self, epoch: i32) { - let machine = &mut self.date; - take_mut::take(machine, |date| date.determine_state(i64::from(epoch))); - } - - /// Set the duration label. - fn set_duration(&mut self, seconds: Option) { - let machine = &mut self.duration; - take_mut::take(machine, |duration| duration.determine_state(seconds)); - } -} - -fn determine_media_state( - media_machine: &Rc>, - episode: &EpisodeWidgetQuery, -) -> Result<(), Error> { - let id = episode.rowid(); - let active_dl = || -> Result, Error> { - let m = manager::ACTIVE_DOWNLOADS - .read() - .map_err(|_| format_err!("Failed to get a lock on the mutex."))?; - - Ok(m.get(&id).cloned()) - }()?; - - let mut lock = media_machine.try_borrow_mut()?; - take_mut::take(lock.deref_mut(), |media| { - media.determine_state( - episode.length(), - active_dl.is_some(), - episode.local_uri().is_some(), - ) - }); - - // Show or hide the play/delete/download buttons upon widget initialization. - if let Some(prog) = active_dl { - // set a callback that will update the state when the download finishes - let id = episode.rowid(); - let callback = clone!(media_machine => move || { - if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() { - if !guard.contains_key(&id) { - if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) { - determine_media_state(&media_machine, &ep) - .map_err(|err| error!("Error: {}", err)) - .map_err(|_| error!("Could not determine Media State")) - .ok(); - - return glib::Continue(false) - } - } - } - - glib::Continue(true) - }); - gtk::timeout_add(250, callback); - - lock.cancel_connect_clicked(clone!(prog, media_machine => move |_| { - if let Ok(mut m) = prog.lock() { - m.cancel(); - } - - if let Ok(mut lock) = media_machine.try_borrow_mut() { - if let Ok(episode) = dbqueries::get_episode_widget_from_rowid(id) { - take_mut::take(lock.deref_mut(), |media| { - media.determine_state( - episode.length(), - false, - episode.local_uri().is_some(), - ) - }); - } - } - })); - drop(lock); - - // Setup a callback that will update the progress bar. - update_progressbar_callback(&prog, &media_machine, 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, &media_machine); - } - - Ok(()) } fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender) -> Result<(), Error> { @@ -241,26 +436,23 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender) -> Resu manager::add(ep.rowid(), download_fold)?; // Update Views - sender.send(Action::RefreshEpisodesViewBGR)?; - - Ok(()) + sender + .send(Action::RefreshEpisodesViewBGR) + .map_err(From::from) } fn on_play_bttn_clicked( + widget: &Rc, episode: &mut EpisodeWidgetQuery, - title: &Rc>, sender: &Sender, ) -> Result<(), Error> { open_uri(episode.rowid())?; episode.set_played_now()?; - 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(()) + widget.info.set_title(&episode); + sender + .send(Action::RefreshEpisodesViewBGR) + .map_err(From::from) } fn open_uri(rowid: i32) -> Result<(), Error> { @@ -281,21 +473,21 @@ fn open_uri(rowid: i32) -> Result<(), Error> { #[inline] #[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))] fn update_progressbar_callback( + widget: &Rc, prog: &Arc>, - media: &Rc>, episode_rowid: i32, ) { - let callback = clone!(prog, media => move || { - progress_bar_helper(&prog, &media, episode_rowid) + let callback = clone!(widget, prog => move || { + progress_bar_helper(&widget, &prog, episode_rowid) .unwrap_or(glib::Continue(false)) }); - timeout_add(300, callback); + timeout_add(150, callback); } #[allow(if_same_then_else)] fn progress_bar_helper( + widget: &Rc, prog: &Arc>, - media: &Rc>, episode_rowid: i32, ) -> Result { let (fraction, downloaded) = { @@ -313,9 +505,7 @@ fn progress_bar_helper( .file_size(SIZE_OPTS.clone()) .map_err(|err| format_err!("{}", err))?; - if let Ok(mut m) = media.try_borrow_mut() { - m.update_progress(&size, fraction); - } + widget.update_progress(&size, fraction); } // info!("Fraction: {}", progress_bar.get_fraction()); @@ -342,19 +532,16 @@ fn progress_bar_helper( // with the http ContentLength header number rather than // relying to the RSS feed. #[inline] -fn update_total_size_callback( - prog: &Arc>, - media: &Rc>, -) { - let callback = clone!(prog, media => move || { - total_size_helper(&prog, &media).unwrap_or(glib::Continue(true)) +fn update_total_size_callback(widget: &Rc, prog: &Arc>) { + let callback = clone!(prog, widget => move || { + total_size_helper(&widget, &prog).unwrap_or(glib::Continue(true)) }); timeout_add(500, callback); } fn total_size_helper( + widget: &Rc, prog: &Arc>, - media: &Rc>, ) -> Result { // Get the total_bytes. let total_bytes = { @@ -367,11 +554,7 @@ fn total_size_helper( debug!("Total Size: {}", total_bytes); if total_bytes != 0 { // Update the total_size label - if let Ok(mut m) = media.try_borrow_mut() { - take_mut::take(m.deref_mut(), |machine| { - machine.set_size(Some(total_bytes as i32)) - }); - } + widget.info.set_size(Some(total_bytes as i32)); // Do not call again the callback Ok(glib::Continue(false)) diff --git a/hammond-gtk/src/widgets/episode_states.rs b/hammond-gtk/src/widgets/episode_states.rs deleted file mode 100644 index a3e27c2..0000000 --- a/hammond-gtk/src/widgets/episode_states.rs +++ /dev/null @@ -1,879 +0,0 @@ -// 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; - -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, f: F) -> glib::SignalHandlerId { - self.cancel.connect_clicked(f) - } -} - -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