EpisodeWidget: Initial refactor.

State machines were a fun experiement but a nightmare to deal with
after the fact. This is the first steps for a refactor in a tradition
style with the goal to ultimatly making it easy to and port to relm.
This commit is contained in:
Jordan Petridis 2018-06-01 16:19:33 +03:00
parent d7a9d9ddc8
commit cfe79a73d6
No known key found for this signature in database
GPG Key ID: CEABAD9F5683B9A6
5 changed files with 304 additions and 568 deletions

View File

@ -41,15 +41,11 @@ Tobias Bernard
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child> <child>
<object class="GtkBox"> <object class="GtkBox" id="info_container">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child> <child>
<object class="GtkLabel" id="title_label"> <object class="GtkLabel" id="title_label">
<property name="visible">True</property> <property name="visible">True</property>
@ -64,14 +60,7 @@ Tobias Bernard
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
@ -213,16 +202,16 @@ Tobias Bernard
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkButtonBox" id="button_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">6</property> <property name="layout_style">center</property>
<child> <child>
<object class="GtkButton" id="cancel_button"> <object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property> <property name="label" translatable="yes">Cancel</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="no_show_all">True</property> <property name="halign">center</property>
<property name="valign">center</property> <property name="valign">center</property>
</object> </object>
<packing> <packing>
@ -237,8 +226,8 @@ Tobias Bernard
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Download this episode</property> <property name="tooltip_text" translatable="yes">Download this episode</property>
<property name="halign">center</property>
<property name="valign">center</property> <property name="valign">center</property>
<property name="always_show_image">True</property> <property name="always_show_image">True</property>
<child> <child>
@ -252,8 +241,8 @@ Tobias Bernard
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="pack_type">end</property> <property name="position">1</property>
<property name="position">2</property> <property name="non_homogeneous">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -262,6 +251,7 @@ Tobias Bernard
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="no_show_all">True</property> <property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Play this episode</property> <property name="tooltip_text" translatable="yes">Play this episode</property>
<property name="halign">center</property>
<property name="valign">center</property> <property name="valign">center</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
@ -274,15 +264,14 @@ Tobias Bernard
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="pack_type">end</property> <property name="position">2</property>
<property name="position">3</property> <property name="non_homogeneous">True</property>
</packing> </packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">False</property>
<property name="padding">6</property>
<property name="pack_type">end</property> <property name="pack_type">end</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>

View File

@ -1,10 +1,14 @@
#![allow(warnings)]
use glib; use glib;
use gtk; use gtk;
use gtk::prelude::*; use gtk::prelude::*;
use chrono;
use chrono::prelude::*;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use failure::Error; use failure::Error;
use humansize::FileSize; use humansize::{file_size_opts as size_opts, FileSize};
use open; use open;
use take_mut; use take_mut;
@ -22,13 +26,141 @@ use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct EpisodeWidget { pub struct EpisodeWidget {
pub container: gtk::Box, pub container: gtk::Box,
date: DateMachine, info: InfoLabels,
duration: DurationMachine, buttons: Buttons,
title: Rc<RefCell<TitleMachine>>, progressbar: gtk::ProgressBar,
media: Rc<RefCell<MediaMachine>>, }
#[derive(Clone, Debug)]
pub 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)]
pub 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> = 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<i32>) {
// 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<i32>) {
lazy_static! {
static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
// 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,
})
};
}
// Convert the bytes to a String label
let size = || -> Option<String> {
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 { impl Default for EpisodeWidget {
@ -36,12 +168,14 @@ impl Default for EpisodeWidget {
let builder = gtk::Builder::new_from_resource("/org/gnome/Hammond/gtk/episode_widget.ui"); 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 container: gtk::Box = builder.get_object("episode_container").unwrap();
let progress: gtk::ProgressBar = builder.get_object("progress_bar").unwrap(); let progressbar: gtk::ProgressBar = builder.get_object("progress_bar").unwrap();
let buttons_container: gtk::ButtonBox = builder.get_object("button_box").unwrap();
let download: gtk::Button = builder.get_object("download_button").unwrap(); let download: gtk::Button = builder.get_object("download_button").unwrap();
let play: gtk::Button = builder.get_object("play_button").unwrap(); let play: gtk::Button = builder.get_object("play_button").unwrap();
let cancel: gtk::Button = builder.get_object("cancel_button").unwrap(); let cancel: gtk::Button = builder.get_object("cancel_button").unwrap();
let info_container: gtk::Box = builder.get_object("info_container").unwrap();
let title: gtk::Label = builder.get_object("title_label").unwrap(); let title: gtk::Label = builder.get_object("title_label").unwrap();
let date: gtk::Label = builder.get_object("date_label").unwrap(); let date: gtk::Label = builder.get_object("date_label").unwrap();
let duration: gtk::Label = builder.get_object("duration_label").unwrap(); let duration: gtk::Label = builder.get_object("duration_label").unwrap();
@ -50,112 +184,165 @@ impl Default for EpisodeWidget {
let separator1: gtk::Label = builder.get_object("separator1").unwrap(); let separator1: gtk::Label = builder.get_object("separator1").unwrap();
let separator2: gtk::Label = builder.get_object("separator2").unwrap(); let separator2: gtk::Label = builder.get_object("separator2").unwrap();
let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap();
let date_machine = DateMachine::new(date, 0); let size_separator: gtk::Label = builder.get_object("prog_separator").unwrap();
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));
EpisodeWidget { 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, container,
title: title_machine,
duration: dur_machine,
date: date_machine,
media: media_machine,
} }
} }
} }
impl EpisodeWidget { impl EpisodeWidget {
pub fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodeWidget { pub fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> Rc<Self> {
let mut widget = EpisodeWidget::default(); let widget = Rc::new(Self::default());
widget.init(episode, sender); widget.info.init(&episode);
Self::determine_buttons_state(&widget, &episode, sender);
widget widget
} }
fn init(&mut self, episode: EpisodeWidgetQuery, sender: &Sender<Action>) { // fn init(widget: Rc<Self>, sender: &Sender<Action>) {}
// Set the date label.
self.set_date(episode.epoch());
// Set the title label state. // InProgress State:
self.set_title(&episode); // * 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.info.total_size.show();
self.set_duration(episode.duration()); self.info.local_size.show();
self.info.size_separator.show();
// Determine what the state of the media widgets should be. self.buttons.play.hide();
determine_media_state(&self.media, &episode) self.buttons.download.hide();
.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);
} }
fn connect_buttons(&self, episode: &Arc<Mutex<EpisodeWidgetQuery>>, sender: &Sender<Action>) { // Playable State:
let title = self.title.clone(); // * Hide ProgressBar and Cancel, Download Buttons.
if let Ok(media) = self.media.try_borrow_mut() { // * Hide `local_size` labels and `size_separator`.
media.play_connect_clicked(clone!(episode, sender => move |_| { // * Show Play Button and `total_size` label
if let Ok(mut ep) = episode.lock() { fn state_playable(&self) {
on_play_bttn_clicked(&mut ep, &title, &sender) self.progressbar.hide();
.map_err(|err| error!("Error: {}", err)) self.buttons.cancel.hide();
.ok(); self.buttons.download.hide();
} self.info.local_size.hide();
})); self.info.size_separator.hide();
let media_machine = self.media.clone(); self.info.total_size.show();
media.download_connect_clicked(clone!(media_machine, episode, sender => move |dl| { self.buttons.play.show();
// Make the button insensitive so it won't be pressed twice
dl.set_sensitive(false);
if let Ok(ep) = episode.lock() {
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();
} }
// Restore sensitivity after operations above complete // ToDownload State:
dl.set_sensitive(true); // * 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();
// FIXME?
// self.info.set_size(size);
} }
/// Determine the title state. fn determine_buttons_state(
fn set_title(&mut self, episode: &EpisodeWidgetQuery) { widget: &Rc<Self>,
let mut machine = self.title.borrow_mut(); episode: &EpisodeWidgetQuery,
machine.set_title(episode.title()); sender: &Sender<Action>,
take_mut::take(machine.deref_mut(), |title| { ) -> Result<(), Error> {
title.determine_state(episode.played().is_some()) // 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 = || -> Result<Option<_>, Error> {
let m = manager::ACTIVE_DOWNLOADS
.read()
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
Ok(m.get(&id).cloned())
}()?;
if let Some(_dl) = active_dl {
// FIXME: Wire cancel button
// FIXME: Wire Total Size label
widget.state_prog();
return Ok(());
} }
/// Set the date label depending on the current time. if let Some(path) = episode.local_uri() {
fn set_date(&mut self, epoch: i32) { // FIXME: Wire play button?
let machine = &mut self.date; widget.state_playable();
take_mut::take(machine, |date| date.determine_state(i64::from(epoch))); return Ok(());
} }
/// Set the duration label. widget.state_download();
fn set_duration(&mut self, seconds: Option<i32>) { Ok(())
let machine = &mut self.duration;
take_mut::take(machine, |duration| duration.determine_state(seconds));
} }
// fn connect_buttons(
// widget: Rc<Self>,
// episode: Arc<Mutex<EpisodeWidgetQuery>>,
// sender: &Sender<Action>,
// ) {
// widget
// .buttons
// .play
// .connect_clicked(clone!(widget, episode, sender => move |_| {
// if let Ok(mut ep) = episode.lock() {
// on_play_bttn_clicked(&widget, &mut ep, &sender)
// .map_err(|err| error!("Error: {}", err))
// .ok();
// }
// }));
// widget
// .buttons
// .download
// .connect_clicked(clone!(widget, episode, sender => move |dl| {
// // Make the button insensitive so it won't be pressed twice
// dl.set_sensitive(false);
// if let Ok(ep) = episode.lock() {
// // FIXME: Change the widget state too
// on_download_clicked(&ep, &sender)
// .map_err(|err| error!("Error: {}", err))
// .ok();
// }
// // Restore sensitivity after operations above complete
// dl.set_sensitive(true);
// }));
// }
} }
fn determine_media_state( fn determine_media_state(
@ -247,20 +434,17 @@ fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Resu
} }
fn on_play_bttn_clicked( fn on_play_bttn_clicked(
widget: &Rc<EpisodeWidget>,
episode: &mut EpisodeWidgetQuery, episode: &mut EpisodeWidgetQuery,
title: &Rc<RefCell<TitleMachine>>,
sender: &Sender<Action>, sender: &Sender<Action>,
) -> Result<(), Error> { ) -> Result<(), Error> {
open_uri(episode.rowid())?; open_uri(episode.rowid())?;
episode.set_played_now()?; episode.set_played_now()?;
let mut machine = title.try_borrow_mut()?; widget.info.set_title(&episode);
take_mut::take(machine.deref_mut(), |title| { sender
title.determine_state(episode.played().is_some()) .send(Action::RefreshEpisodesViewBGR)
}); .map_err(From::from)
sender.send(Action::RefreshEpisodesViewBGR)?;
Ok(())
} }
fn open_uri(rowid: i32) -> Result<(), Error> { fn open_uri(rowid: i32) -> Result<(), Error> {

View File

@ -5,11 +5,9 @@
// Wrap the types into Struct-tuples and imple deref so it won't be possible to pass // Wrap the types into Struct-tuples and imple deref so it won't be possible to pass
// the wrong argument to the wrong position. // the wrong argument to the wrong position.
use chrono;
use glib; use glib;
use gtk; use gtk;
use chrono::prelude::*;
use gtk::prelude::*; use gtk::prelude::*;
use humansize::{file_size_opts as size_opts, FileSize}; use humansize::{file_size_opts as size_opts, FileSize};
@ -31,13 +29,8 @@ lazy_static! {
allow_negative: false, allow_negative: false,
}) })
}; };
static ref NOW: DateTime<Utc> = Utc::now();
} }
#[derive(Debug, Clone)]
pub struct UnInitialized;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Shown; pub struct Shown;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -48,269 +41,6 @@ pub trait Visibility {}
impl Visibility for Shown {} impl Visibility for Shown {}
impl Visibility for Hidden {} impl Visibility for Hidden {}
#[derive(Debug, Clone)]
pub struct Normal;
#[derive(Debug, Clone)]
pub struct GreyedOut;
#[derive(Debug, Clone)]
pub struct Title<S> {
title: gtk::Label,
state: S,
}
impl<S> Title<S> {
#[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<Normal> {
fn new(title: gtk::Label) -> Self {
Title {
title,
state: Normal {},
}
}
}
impl From<Title<Normal>> for Title<GreyedOut> {
fn from(f: Title<Normal>) -> Self {
f.title
.get_style_context()
.map(|c| c.add_class("dim-label"));
Title {
title: f.title,
state: GreyedOut {},
}
}
}
impl From<Title<GreyedOut>> for Title<Normal> {
fn from(f: Title<GreyedOut>) -> 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<Normal>),
GreyedOut(Title<GreyedOut>),
}
impl TitleMachine {
pub fn new(label: gtk::Label, is_played: bool) -> Self {
let m = TitleMachine::Normal(Title::<Normal>::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<S> {
date: gtk::Label,
epoch: i64,
state: S,
}
impl<S> Date<S> {
fn into_usual(self, epoch: i64) -> Date<Usual> {
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<YearShown> {
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<UnInitialized> {
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<UnInitialized>),
Usual(Date<Usual>),
WithYear(Date<YearShown>),
}
impl DateMachine {
pub fn new(label: gtk::Label, epoch: i64) -> Self {
let m = DateMachine::UnInitialized(Date::<UnInitialized>::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<S: Visibility> {
// TODO: make duration and separator diff types
duration: gtk::Label,
separator: gtk::Label,
state: S,
}
impl<S: Visibility> Duration<S> {
// This needs a better name.
// TODO: make me mut
fn set_duration(&self, minutes: i64) {
self.duration.set_text(&format!("{} min", minutes));
}
}
impl Duration<Hidden> {
fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
duration.hide();
separator.hide();
Duration {
duration,
separator,
state: Hidden {},
}
}
}
impl From<Duration<Hidden>> for Duration<Shown> {
fn from(f: Duration<Hidden>) -> Self {
f.duration.show();
f.separator.show();
Duration {
duration: f.duration,
separator: f.separator,
state: Shown {},
}
}
}
impl From<Duration<Shown>> for Duration<Hidden> {
fn from(f: Duration<Shown>) -> Self {
f.duration.hide();
f.separator.hide();
Duration {
duration: f.duration,
separator: f.separator,
state: Hidden {},
}
}
}
#[derive(Debug, Clone)]
pub enum DurationMachine {
Hidden(Duration<Hidden>),
Shown(Duration<Shown>),
}
impl DurationMachine {
pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self {
let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator));
m.determine_state(seconds)
}
pub fn determine_state(self, seconds: Option<i32>) -> 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)] #[derive(Debug, Clone)]
pub struct Size<S> { pub struct Size<S> {
size: gtk::Label, size: gtk::Label,
@ -356,19 +86,6 @@ impl<S> Size<S> {
} }
} }
impl Size<UnInitialized> {
fn new(size: gtk::Label, separator: gtk::Label) -> Self {
size.hide();
separator.hide();
Size {
size,
separator,
state: UnInitialized {},
}
}
}
// pub trait Playable {} // pub trait Playable {}
// impl Playable for Download {} // impl Playable for Download {}
@ -390,8 +107,7 @@ pub struct DownloadPlay<S> {
} }
impl<S> DownloadPlay<S> { impl<S> DownloadPlay<S> {
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable // https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable // It might be possible to make a generic definition with Specialization.
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844 // https://github.com/rust-lang/rust/issues/31844
fn into_playable(self) -> DownloadPlay<Play> { fn into_playable(self) -> DownloadPlay<Play> {
self.play.show(); self.play.show();
@ -425,30 +141,6 @@ impl<S> DownloadPlay<S> {
state: Hidden {}, state: Hidden {},
} }
} }
fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.download.connect_clicked(f)
}
fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.play.connect_clicked(f)
}
}
impl DownloadPlay<UnInitialized> {
fn new(play: gtk::Button, download: gtk::Button) -> Self {
play.hide();
download.hide();
DownloadPlay {
play,
download,
state: UnInitialized {},
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -504,28 +196,6 @@ impl<S> Progress<S> {
} }
} }
impl Progress<UnInitialized> {
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)] #[derive(Debug, Clone)]
pub struct Media<X, Y, Z> { pub struct Media<X, Y, Z> {
dl: DownloadPlay<X>, dl: DownloadPlay<X>,
@ -607,24 +277,6 @@ impl<X, Z> Media<X, Hidden, Z> {
} }
} }
impl<X, Z> Media<X, UnInitialized, Z> {
fn into_progress(self, size: Option<String>) -> InProgress {
if let Some(s) = size {
Media {
dl: self.dl.into_hidden(),
size: self.size.set_size(&s),
progress: self.progress.into_shown(),
}
} else {
Media {
dl: self.dl.into_hidden(),
size: self.size.set_size("Unkown"),
progress: self.progress.into_shown(),
}
}
}
}
impl InProgress { impl InProgress {
#[allow(unused_must_use)] #[allow(unused_must_use)]
// This does not need to be &mut since gtk-rs does not model ownership // This does not need to be &mut since gtk-rs does not model ownership
@ -703,34 +355,6 @@ impl ButtonsState {
} }
} }
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::ButtonsState::*;
match *self {
New(ref val) => val.dl.download_connect_clicked(f),
NewWithoutSize(ref val) => val.dl.download_connect_clicked(f),
Playable(ref val) => val.dl.download_connect_clicked(f),
PlayableWithoutSize(ref val) => val.dl.download_connect_clicked(f),
}
}
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::ButtonsState::*;
match *self {
New(ref val) => val.dl.play_connect_clicked(f),
NewWithoutSize(ref val) => val.dl.play_connect_clicked(f),
Playable(ref val) => val.dl.play_connect_clicked(f),
PlayableWithoutSize(ref val) => val.dl.play_connect_clicked(f),
}
}
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId { fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
use self::ButtonsState::*; use self::ButtonsState::*;
@ -745,56 +369,11 @@ impl ButtonsState {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum MediaMachine { pub enum MediaMachine {
UnInitialized(Media<UnInitialized, UnInitialized, UnInitialized>),
Initialized(ButtonsState), Initialized(ButtonsState),
InProgress(Media<Hidden, Shown, Shown>), InProgress(Media<Hidden, Shown, Shown>),
} }
impl MediaMachine { impl MediaMachine {
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
pub fn new(
play: gtk::Button,
download: gtk::Button,
bar: gtk::ProgressBar,
cancel: gtk::Button,
total_size: gtk::Label,
local_size: gtk::Label,
separator: gtk::Label,
prog_separator: gtk::Label,
) -> Self {
let dl = DownloadPlay::<UnInitialized>::new(play, download);
let progress = Progress::<UnInitialized>::new(bar, cancel, local_size, prog_separator);
let size = Size::<UnInitialized>::new(total_size, separator);
MediaMachine::UnInitialized(Media { dl, progress, size })
}
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::MediaMachine::*;
match *self {
UnInitialized(ref val) => val.dl.download_connect_clicked(f),
Initialized(ref val) => val.download_connect_clicked(f),
InProgress(ref val) => val.dl.download_connect_clicked(f),
}
}
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::MediaMachine::*;
match *self {
UnInitialized(ref val) => val.dl.play_connect_clicked(f),
Initialized(ref val) => val.play_connect_clicked(f),
InProgress(ref val) => val.dl.play_connect_clicked(f),
}
}
pub fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>( pub fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self, &self,
f: F, f: F,
@ -802,7 +381,6 @@ impl MediaMachine {
use self::MediaMachine::*; use self::MediaMachine::*;
match *self { match *self {
UnInitialized(ref val) => val.progress.cancel_connect_clicked(f),
Initialized(ref val) => val.cancel_connect_clicked(f), Initialized(ref val) => val.cancel_connect_clicked(f),
InProgress(ref val) => val.progress.cancel_connect_clicked(f), InProgress(ref val) => val.progress.cancel_connect_clicked(f),
} }
@ -813,20 +391,6 @@ impl MediaMachine {
use self::MediaMachine::*; use self::MediaMachine::*;
match (self, size_helper(bytes), is_downloaded, is_active) { match (self, size_helper(bytes), is_downloaded, is_active) {
(UnInitialized(m), s, _, true) => InProgress(m.into_progress(s)),
// Into New
(UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
(UnInitialized(m), None, false, false) => {
Initialized(NewWithoutSize(m.into_new_without()))
}
// Into Playable
(UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
(UnInitialized(m), None, true, false) => {
Initialized(PlayableWithoutSize(m.into_playable_without()))
}
(Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)), (Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)),
(Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()), (Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()),
@ -854,7 +418,6 @@ impl MediaMachine {
(Initialized(bttn), s) => Initialized(bttn.set_size(s)), (Initialized(bttn), s) => Initialized(bttn.set_size(s)),
(InProgress(val), Some(s)) => InProgress(val.set_size(&s)), (InProgress(val), Some(s)) => InProgress(val.set_size(&s)),
(n @ InProgress(_), None) => n, (n @ InProgress(_), None) => n,
(n @ UnInitialized(_), _) => n,
} }
} }
@ -863,7 +426,6 @@ impl MediaMachine {
match *self { match *self {
Initialized(_) => (), Initialized(_) => (),
UnInitialized(_) => (),
InProgress(ref mut val) => val.update_progress(local_size, fraction), InProgress(ref mut val) => val.update_progress(local_size, fraction),
} }
} }

View File

@ -175,6 +175,7 @@ fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
struct HomeEpisode { struct HomeEpisode {
container: gtk::Box, container: gtk::Box,
image: gtk::Image, image: gtk::Image,
// FIXME: Change it to `EpisodeWidget` instead of a `Box`?
episode: gtk::Box, episode: gtk::Box,
} }
@ -207,7 +208,7 @@ impl HomeEpisode {
let view = HomeEpisode { let view = HomeEpisode {
container, container,
image, image,
episode: ep.container, episode: ep.container.clone(),
}; };
view.init(pid); view.init(pid);

View File

@ -207,7 +207,7 @@ fn populate_listbox(
let list = show_.episodes.clone(); let list = show_.episodes.clone();
let constructor = clone!(sender => move |ep| { let constructor = clone!(sender => move |ep| {
EpisodeWidget::new(ep, &sender).container EpisodeWidget::new(ep, &sender).container.clone()
}); });
let callback = clone!(pd, show_ => move || { let callback = clone!(pd, show_ => move || {