Merge branch 'master' into 33-downloader-re-work

This commit is contained in:
Jordan Petridis
2018-01-05 21:43:41 +02:00
18 changed files with 532 additions and 412 deletions
+61 -58
View File
@@ -6,24 +6,37 @@ use gio::{ActionMapExt, ApplicationExt, ApplicationExtManual, SimpleActionExt};
use hammond_data::utils::checkup;
use hammond_downloader::manager::Manager;
use hammond_data::Source;
use headerbar::Header;
use content::Content;
use utils;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::{channel, Receiver, Sender};
lazy_static! {
pub static ref DOWNLOADS_MANAGER: Arc<Mutex<Manager>> = Arc::new(Mutex::new(Manager::new()));
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub enum Action {
UpdateSources(Option<Source>),
RefreshViews,
RefreshEpisodesViewBGR,
HeaderBarShowTile(String),
HeaderBarNormal,
HeaderBarHideUpdateIndicator,
}
#[derive(Debug)]
pub struct App {
app_instance: gtk::Application,
window: gtk::Window,
header: Rc<Header>,
content: Rc<Content>,
header: Arc<Header>,
content: Arc<Content>,
receiver: Receiver<Action>,
sender: Sender<Action>,
}
impl App {
@@ -40,21 +53,19 @@ impl App {
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_default_size(860, 640);
window.set_title("Hammond");
window.connect_delete_event(|w, _| {
w.destroy();
let app_clone = application.clone();
window.connect_delete_event(move |_, _| {
app_clone.quit();
Inhibit(false)
});
// TODO: Refactor the initialization order.
// Create the headerbar
let header = Rc::new(Header::default());
let (sender, receiver) = channel();
// Create a content instance
let content = Content::new(header.clone());
let content = Content::new(sender.clone());
// Initialize the headerbar
header.init(content.clone());
// Create the headerbar
let header = Header::new(content.clone(), sender.clone());
// Add the Headerbar to the window.
window.set_titlebar(&header.container);
@@ -66,79 +77,49 @@ impl App {
window,
header,
content,
receiver,
sender,
}
}
pub fn setup_actions(&self) {
// Updates the database and refreshes every view.
let update = gio::SimpleAction::new("update", None);
let content = self.content.clone();
let header = self.header.clone();
let sender = self.sender.clone();
update.connect_activate(move |_, _| {
utils::refresh_feed(content.clone(), header.clone(), None);
utils::refresh_feed(header.clone(), None, sender.clone());
});
self.app_instance.add_action(&update);
// Refreshes the `Content`
let refresh = gio::SimpleAction::new("refresh", None);
let content = self.content.clone();
refresh.connect_activate(move |_, _| {
content.update();
});
self.app_instance.add_action(&refresh);
// Refreshes the `EpisodesStack`
let refresh_episodes = gio::SimpleAction::new("refresh_episodes", None);
let content = self.content.clone();
refresh_episodes.connect_activate(move |_, _| {
if content.get_stack().get_visible_child_name() != Some(String::from("episodes")) {
content.update_episode_view();
}
});
self.app_instance.add_action(&refresh_episodes);
// Refreshes the `ShowStack`
let refresh_shows = gio::SimpleAction::new("refresh_shows", None);
let content = self.content.clone();
refresh_shows.connect_activate(move |_, _| {
content.update_shows_view();
});
self.app_instance.add_action(&refresh_shows);
}
pub fn setup_timed_callbacks(&self) {
let content = self.content.clone();
let header = self.header.clone();
// Update 30 seconds after the Application is initialized.
gtk::timeout_add_seconds(
30,
clone!(content => move || {
utils::refresh_feed(content.clone(), header.clone(), None);
let sender = self.sender.clone();
// Update the feeds right after the Application is initialized.
gtk::timeout_add_seconds(2, move || {
utils::refresh_feed(header.clone(), None, sender.clone());
glib::Continue(false)
}),
);
});
let content = self.content.clone();
let header = self.header.clone();
let sender = self.sender.clone();
// Auto-updater, runs every hour.
// TODO: expose the interval in which it run to a user setting.
// TODO: show notifications.
gtk::timeout_add_seconds(
3600,
clone!(content => move || {
utils::refresh_feed(content.clone(), header.clone(), None);
gtk::timeout_add_seconds(3600, move || {
utils::refresh_feed(header.clone(), None, sender.clone());
glib::Continue(true)
}),
);
});
// Run a database checkup once the application is initialized.
gtk::idle_add(move || {
gtk::timeout_add(300, || {
let _ = checkup();
glib::Continue(false)
});
}
pub fn run(&self) {
pub fn run(self) {
let window = self.window.clone();
let app = self.app_instance.clone();
self.app_instance.connect_startup(move |_| {
@@ -147,6 +128,28 @@ impl App {
self.setup_timed_callbacks();
self.setup_actions();
let content = self.content.clone();
let headerbar = self.header.clone();
let sender = self.sender.clone();
let receiver = self.receiver;
gtk::timeout_add(250, move || {
match receiver.try_recv() {
Ok(Action::UpdateSources(source)) => {
if let Some(s) = source {
utils::refresh_feed(headerbar.clone(), Some(vec![s]), sender.clone())
}
}
Ok(Action::RefreshViews) => content.update(),
Ok(Action::RefreshEpisodesViewBGR) => content.update_episode_view_if_baground(),
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
_ => (),
}
Continue(true)
});
ApplicationExtManual::run(&self.app_instance, &[]);
}
}
+100 -32
View File
@@ -1,4 +1,5 @@
use gtk;
use gtk::Cast;
use gtk::prelude::*;
use hammond_data::Podcast;
@@ -9,30 +10,33 @@ use views::empty::EmptyView;
use views::episodes::EpisodesView;
use widgets::show::ShowWidget;
use headerbar::Header;
use app::Action;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct Content {
stack: gtk::Stack,
shows: Rc<ShowStack>,
episodes: Rc<EpisodeStack>,
shows: Arc<ShowStack>,
episodes: Arc<EpisodeStack>,
sender: Sender<Action>,
}
impl Content {
pub fn new(header: Rc<Header>) -> Rc<Content> {
pub fn new(sender: Sender<Action>) -> Arc<Content> {
let stack = gtk::Stack::new();
let episodes = EpisodeStack::new();
let shows = ShowStack::new(header, episodes.clone());
let episodes = EpisodeStack::new(sender.clone());
let shows = ShowStack::new(sender.clone());
stack.add_titled(&episodes.stack, "episodes", "Episodes");
stack.add_titled(&shows.stack, "shows", "Shows");
Rc::new(Content {
Arc::new(Content {
stack,
shows,
episodes,
sender,
})
}
@@ -45,6 +49,12 @@ impl Content {
self.episodes.update();
}
pub fn update_episode_view_if_baground(&self) {
if self.stack.get_visible_child_name() != Some("episodes".into()) {
self.episodes.update();
}
}
pub fn update_shows_view(&self) {
self.shows.update();
}
@@ -53,7 +63,7 @@ impl Content {
self.stack.clone()
}
pub fn get_shows(&self) -> Rc<ShowStack> {
pub fn get_shows(&self) -> Arc<ShowStack> {
self.shows.clone()
}
}
@@ -61,21 +71,19 @@ impl Content {
#[derive(Debug, Clone)]
pub struct ShowStack {
stack: gtk::Stack,
header: Rc<Header>,
epstack: Rc<EpisodeStack>,
sender: Sender<Action>,
}
impl ShowStack {
fn new(header: Rc<Header>, epstack: Rc<EpisodeStack>) -> Rc<ShowStack> {
fn new(sender: Sender<Action>) -> Arc<ShowStack> {
let stack = gtk::Stack::new();
let show = Rc::new(ShowStack {
let show = Arc::new(ShowStack {
stack,
header: header.clone(),
epstack,
sender: sender.clone(),
});
let pop = ShowsPopulated::new(show.clone(), header);
let pop = ShowsPopulated::new(show.clone(), sender.clone());
let widget = ShowWidget::default();
let empty = EmptyView::new();
@@ -103,10 +111,31 @@ impl ShowStack {
pub fn update_podcasts(&self) {
let vis = self.stack.get_visible_child_name().unwrap();
let old = self.stack.get_child_by_name("podcasts").unwrap();
let pop = ShowsPopulated::default();
pop.init(Rc::new(self.clone()), self.header.clone());
let old = self.stack
.get_child_by_name("podcasts")
// This is guaranted to exists, based on `ShowStack::new()`.
.unwrap()
.downcast::<gtk::Box>()
// This is guaranted to be a Box based on the `ShowsPopulated` impl.
.unwrap();
debug!("Name: {:?}", WidgetExt::get_name(&old));
let scrolled_window = old.get_children()
.first()
// This is guaranted to exist based on the show_widget.ui file.
.unwrap()
.clone()
.downcast::<gtk::ScrolledWindow>()
// This is guaranted based on the show_widget.ui file.
.unwrap();
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
let pop = ShowsPopulated::new(Arc::new(self.clone()), self.sender.clone());
// Copy the vertical scrollbar adjustment from the old view into the new one.
scrolled_window
.get_vadjustment()
.map(|x| pop.set_vadjustment(&x));
self.stack.remove(&old);
self.stack.add_named(&pop.container, "podcasts");
@@ -123,8 +152,30 @@ impl ShowStack {
}
pub fn replace_widget(&self, pd: &Podcast) {
let old = self.stack.get_child_by_name("widget").unwrap();
let new = ShowWidget::new(Rc::new(self.clone()), self.header.clone(), pd);
let old = self.stack
.get_child_by_name("widget")
// This is guaranted to exists, based on `ShowStack::new()`.
.unwrap()
.downcast::<gtk::Box>()
// This is guaranted to be a Box based on the `ShowWidget` impl.
.unwrap();
debug!("Name: {:?}", WidgetExt::get_name(&old));
let scrolled_window = old.get_children()
.first()
// This is guaranted to exist based on the show_widget.ui file.
.unwrap()
.clone()
.downcast::<gtk::ScrolledWindow>()
// This is guaranted based on the show_widget.ui file.
.unwrap();
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
let new = ShowWidget::new(Arc::new(self.clone()), pd, self.sender.clone());
// Copy the vertical scrollbar adjustment from the old view into the new one.
scrolled_window
.get_vadjustment()
.map(|x| new.set_vadjustment(&x));
self.stack.remove(&old);
self.stack.add_named(&new.container, "widget");
@@ -164,14 +215,13 @@ impl ShowStack {
#[derive(Debug, Clone)]
pub struct EpisodeStack {
// populated: RecentEpisodes,
// empty: EmptyView,
stack: gtk::Stack,
sender: Sender<Action>,
}
impl EpisodeStack {
fn new() -> Rc<EpisodeStack> {
let episodes = EpisodesView::new();
fn new(sender: Sender<Action>) -> Arc<EpisodeStack> {
let episodes = EpisodesView::new(sender.clone());
let empty = EmptyView::new();
let stack = gtk::Stack::new();
@@ -184,16 +234,34 @@ impl EpisodeStack {
stack.set_visible_child_name("episodes");
}
Rc::new(EpisodeStack {
// empty,
// populated: pop,
stack,
})
Arc::new(EpisodeStack { stack, sender })
}
pub fn update(&self) {
let old = self.stack.get_child_by_name("episodes").unwrap();
let eps = EpisodesView::new();
let old = self.stack
.get_child_by_name("episodes")
// This is guaranted to exists, based on `EpisodeStack::new()`.
.unwrap()
.downcast::<gtk::Box>()
// This is guaranted to be a Box based on the `EpisodesView` impl.
.unwrap();
debug!("Name: {:?}", WidgetExt::get_name(&old));
let scrolled_window = old.get_children()
.first()
// This is guaranted to exist based on the episodes_view.ui file.
.unwrap()
.clone()
.downcast::<gtk::ScrolledWindow>()
// This is guaranted based on the episodes_view.ui file.
.unwrap();
debug!("Name: {:?}", WidgetExt::get_name(&scrolled_window));
let eps = EpisodesView::new(self.sender.clone());
// Copy the vertical scrollbar adjustment from the old view into the new one.
scrolled_window
.get_vadjustment()
.map(|x| eps.set_vadjustment(&x));
self.stack.remove(&old);
self.stack.add_named(&eps.container, "episodes");
+15 -15
View File
@@ -3,9 +3,10 @@ use gtk::prelude::*;
use hammond_data::Source;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
use utils;
use app::Action;
use content::Content;
#[derive(Debug, Clone)]
@@ -48,13 +49,13 @@ impl Default for Header {
impl Header {
#[allow(dead_code)]
pub fn new(content: Rc<Content>) -> Rc<Header> {
pub fn new(content: Arc<Content>, sender: Sender<Action>) -> Arc<Header> {
let h = Header::default();
h.init(content);
Rc::new(h)
h.init(content, sender);
Arc::new(h)
}
pub fn init(&self, content: Rc<Content>) {
pub fn init(&self, content: Arc<Content>, sender: Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
@@ -66,12 +67,13 @@ impl Header {
println!("{:?}", url.get_text());
});
let header = Rc::new(self.clone());
add_button.connect_clicked(clone!(content, header, add_popover, new_url => move |_| {
on_add_bttn_clicked(content.clone(), header.clone(), &new_url);
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
on_add_bttn_clicked(&new_url, sender.clone());
add_popover.hide();
}));
self.add_toggle.set_popover(&add_popover);
let switch = &self.switch;
let add_toggle = &self.add_toggle;
let show_title = &self.show_title;
@@ -120,16 +122,14 @@ impl Header {
}
}
fn on_add_bttn_clicked(content: Rc<Content>, headerbar: Rc<Header>, entry: &gtk::Entry) {
fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) {
let url = entry.get_text().unwrap_or_default();
let source = Source::from_url(&url);
if let Ok(s) = source {
info!("{:?} feed added", url);
// update the db
utils::refresh_feed(content, headerbar, Some(vec![s]));
if source.is_ok() {
sender.send(Action::UpdateSources(source.ok())).unwrap();
} else {
error!("Feed probably already exists.");
error!("Something went wrong.");
error!("Error: {:?}", source.unwrap_err());
}
}
+10 -40
View File
@@ -1,5 +1,4 @@
use send_cell::SendCell;
use glib;
use gdk_pixbuf::Pixbuf;
use hammond_data::feed;
@@ -7,63 +6,34 @@ use hammond_data::{PodcastCoverQuery, Source};
use hammond_downloader::downloader;
use std::thread;
use std::cell::RefCell;
use std::sync::mpsc::{channel, Receiver};
use std::sync::Mutex;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use content::Content;
use headerbar::Header;
use app::Action;
type Foo = RefCell<Option<(Rc<Content>, Rc<Header>, Receiver<bool>)>>;
// Create a thread local storage that will store the arguments to be transfered.
thread_local!(static GLOBAL: Foo = RefCell::new(None));
/// Update the rss feed(s) originating from `Source`.
/// Update the rss feed(s) originating from `source`.
/// If `source` is None, Fetches all the `Source` entries in the database and updates them.
/// `delay` represents the desired time in seconds for the thread to sleep before executing.
/// When It's done,it queues up a `podcast_view` refresh.
pub fn refresh_feed(content: Rc<Content>, headerbar: Rc<Header>, source: Option<Vec<Source>>) {
/// When It's done,it queues up a `RefreshViews` action.
pub fn refresh_feed(headerbar: Arc<Header>, source: Option<Vec<Source>>, sender: Sender<Action>) {
headerbar.show_update_notification();
// Create a async channel.
let (sender, receiver) = channel();
// Pass the desired arguments into the Local Thread Storage.
GLOBAL.with(clone!(content, headerbar => move |global| {
*global.borrow_mut() = Some((content.clone(), headerbar.clone(), receiver));
}));
thread::spawn(move || {
if let Some(s) = source {
feed::index_loop(s);
} else {
let e = feed::index_all();
if let Err(err) = e {
if let Err(err) = feed::index_all() {
error!("Error While trying to update the database.");
error!("Error msg: {}", err);
};
}
};
sender.send(true).expect("Couldn't send data to channel");;
glib::idle_add(refresh_everything);
sender.send(Action::HeaderBarHideUpdateIndicator).unwrap();
sender.send(Action::RefreshViews).unwrap();
});
}
fn refresh_everything() -> glib::Continue {
GLOBAL.with(|global| {
if let Some((ref content, ref headerbar, ref reciever)) = *global.borrow() {
if reciever.try_recv().is_ok() {
content.update();
headerbar.hide_update_notification();
}
}
});
glib::Continue(false)
}
lazy_static! {
static ref CACHED_PIXBUFS: Mutex<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> = {
Mutex::new(HashMap::new())
+17 -6
View File
@@ -7,8 +7,10 @@ use hammond_data::EpisodeWidgetQuery;
use widgets::episode::EpisodeWidget;
use utils::get_pixbuf_from_path;
use app::Action;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
#[derive(Debug, Clone)]
enum ListSplit {
@@ -22,6 +24,7 @@ enum ListSplit {
#[derive(Debug, Clone)]
pub struct EpisodesView {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
frame_parent: gtk::Box,
today_box: gtk::Box,
yday_box: gtk::Box,
@@ -39,6 +42,7 @@ impl Default for EpisodesView {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let frame_parent: gtk::Box = builder.get_object("frame_parent").unwrap();
let today_box: gtk::Box = builder.get_object("today_box").unwrap();
let yday_box: gtk::Box = builder.get_object("yday_box").unwrap();
@@ -53,6 +57,7 @@ impl Default for EpisodesView {
EpisodesView {
container,
scrolled_window,
frame_parent,
today_box,
yday_box,
@@ -68,14 +73,15 @@ impl Default for EpisodesView {
}
}
// TODO: REFACTOR ME
impl EpisodesView {
pub fn new() -> Rc<EpisodesView> {
pub fn new(sender: Sender<Action>) -> Arc<EpisodesView> {
let view = EpisodesView::default();
let episodes = dbqueries::get_episodes_widgets_with_limit(100).unwrap();
let now_utc = Utc::now();
episodes.into_iter().for_each(|mut ep| {
let viewep = EpisodesViewWidget::new(&mut ep);
let viewep = EpisodesViewWidget::new(&mut ep, sender.clone());
let t = split(&now_utc, i64::from(ep.epoch()));
match t {
@@ -118,7 +124,7 @@ impl EpisodesView {
}
view.container.show_all();
Rc::new(view)
Arc::new(view)
}
pub fn is_empty(&self) -> bool {
@@ -144,6 +150,11 @@ impl EpisodesView {
true
}
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
@@ -187,7 +198,7 @@ impl Default for EpisodesViewWidget {
}
impl EpisodesViewWidget {
fn new(episode: &mut EpisodeWidgetQuery) -> EpisodesViewWidget {
fn new(episode: &mut EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodesViewWidget {
let builder =
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
@@ -200,7 +211,7 @@ impl EpisodesViewWidget {
}
}
let ep = EpisodeWidget::new(episode);
let ep = EpisodeWidget::new(episode, sender.clone());
container.pack_start(&ep.container, true, true, 6);
EpisodesViewWidget {
+16 -10
View File
@@ -7,51 +7,52 @@ use hammond_data::Podcast;
use utils::get_pixbuf_from_path;
use content::ShowStack;
use headerbar::Header;
use app::Action;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ShowsPopulated {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
flowbox: gtk::FlowBox,
viewport: gtk::Viewport,
}
impl Default for ShowsPopulated {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_view.ui");
let container: gtk::Box = builder.get_object("fb_parent").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
let viewport: gtk::Viewport = builder.get_object("viewport").unwrap();
ShowsPopulated {
container,
scrolled_window,
flowbox,
viewport,
}
}
}
impl ShowsPopulated {
pub fn new(show: Rc<ShowStack>, header: Rc<Header>) -> ShowsPopulated {
pub fn new(show: Arc<ShowStack>, sender: Sender<Action>) -> ShowsPopulated {
let pop = ShowsPopulated::default();
pop.init(show, header);
pop.init(show, sender);
pop
}
pub fn init(&self, show: Rc<ShowStack>, header: Rc<Header>) {
pub fn init(&self, show: Arc<ShowStack>, sender: Sender<Action>) {
use gtk::WidgetExt;
// TODO: handle unwraps.
self.flowbox
.connect_child_activated(clone!(show => move |_, child| {
.connect_child_activated(clone!(show, sender => move |_, child| {
// This is such an ugly hack...
let id = WidgetExt::get_name(child).unwrap().parse::<i32>().unwrap();
let pd = dbqueries::get_podcast_from_id(id).unwrap();
show.replace_widget(&pd);
header.switch_to_back(pd.title());
sender.send(Action::HeaderBarShowTile(pd.title().into())).unwrap();
show.switch_widget_animated();
}));
// Populate the flowbox with the Podcasts.
@@ -73,6 +74,11 @@ impl ShowsPopulated {
pub fn is_empty(&self) -> bool {
self.flowbox.get_children().is_empty()
}
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
#[derive(Debug)]
+96 -58
View File
@@ -15,6 +15,10 @@ use hammond_downloader::downloader;
use app::DOWNLOADS_MANAGER;
use app::Action;
use std::thread;
use std::sync::mpsc::Sender;
use std::path::Path;
#[derive(Debug, Clone)]
@@ -71,24 +75,30 @@ impl Default for EpisodeWidget {
}
impl EpisodeWidget {
pub fn new(episode: &mut EpisodeWidgetQuery) -> EpisodeWidget {
pub fn new(episode: &mut EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodeWidget {
let widget = EpisodeWidget::default();
widget.init(episode);
widget.init(episode, sender);
widget
}
// TODO: calculate lenght.
// TODO: wire the progress_bar to the downloader.
// TODO: wire the cancel button.
fn init(&self, episode: &mut EpisodeWidgetQuery) {
self.title.set_xalign(0.0);
self.title.set_text(episode.title());
fn init(&self, episode: &mut EpisodeWidgetQuery, sender: Sender<Action>) {
// Set the title label state.
self.set_title(episode);
if episode.played().is_some() {
self.title
.get_style_context()
.map(|c| c.add_class("dim-label"));
}
// Set the size label.
self.set_size(episode.length());
// 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.
self.show_buttons(episode.local_uri());
{
let m = DOWNLOADS_MANAGER.lock().unwrap();
@@ -98,6 +108,77 @@ impl EpisodeWidget {
};
}
let title = &self.title;
self.play
.connect_clicked(clone!(episode, title, sender => move |_| {
let mut episode = episode.clone();
on_play_bttn_clicked(episode.rowid());
if episode.set_played_now().is_ok() {
title
.get_style_context()
.map(|c| c.add_class("dim-label"));
sender.send(Action::RefreshEpisodesViewBGR).unwrap();
};
}));
let cancel = &self.cancel;
let progress = self.progress.clone();
self.download
.connect_clicked(clone!(episode, cancel, progress, sender => move |dl| {
on_download_clicked(
&mut episode.clone(),
dl,
&cancel,
progress.clone(),
sender.clone()
);
}));
}
/// Show or hide the play/delete/download buttons upon widget initialization.
fn show_buttons(&self, local_uri: Option<&str>) {
if local_uri.is_some() && Path::new(local_uri.unwrap()).exists() {
self.download.hide();
self.play.show();
}
}
/// Determine the title state.
fn set_title(&self, episode: &EpisodeWidgetQuery) {
self.title.set_xalign(0.0);
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"));
}
}
/// Set the date label depending on the current time.
fn set_date(&self, epoch: i32) {
let now = Utc::now();
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());
};
}
/// Set the duration label.
fn set_duration(&self, seconds: Option<i32>) {
if let Some(secs) = seconds {
self.duration.set_text(&format!("{} min", secs / 60));
self.duration.show();
self.separator1.show();
}
}
/// Set the Episode label dependings on its size
fn set_size(&self, bytes: Option<i32>) {
// Declare a custom humansize option struct
// See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html
let custom_options = size_opts::FileSizeOpts {
@@ -112,7 +193,7 @@ impl EpisodeWidget {
allow_negative: false,
};
if let Some(size) = episode.length() {
if let Some(size) = bytes {
if size != 0 {
let s = size.file_size(custom_options);
if let Ok(s) = s {
@@ -122,51 +203,6 @@ impl EpisodeWidget {
}
}
};
if let Some(secs) = episode.duration() {
self.duration.set_text(&format!("{} min", secs / 60));
self.duration.show();
self.separator1.show();
};
let now = Utc::now();
let date = Utc.timestamp(i64::from(episode.epoch()), 0);
if now.year() == date.year() {
self.date.set_text(&date.format("%e %b").to_string());
} else {
self.date.set_text(&date.format("%e %b %Y").to_string());
};
// Show or hide the play/delete/download buttons upon widget initialization.
let local_uri = episode.local_uri();
if local_uri.is_some() && Path::new(local_uri.unwrap()).exists() {
self.download.hide();
self.play.show();
}
let title = &self.title;
self.play
.connect_clicked(clone!(episode, title => move |_| {
let mut episode = episode.clone();
on_play_bttn_clicked(episode.rowid());
if episode.set_played_now().is_ok() {
title
.get_style_context()
.map(|c| c.add_class("dim-label"));
};
}));
let cancel = &self.cancel;
let progress = self.progress.clone();
self.download
.connect_clicked(clone!(episode, cancel, progress => move |dl| {
on_download_clicked(
&episode,
dl,
&cancel,
progress.clone()
);
}));
}
fn show_progess_bar(&self) {
@@ -187,6 +223,7 @@ fn on_download_clicked(
download_bttn: &gtk::Button,
cancel_bttn: &gtk::Button,
progress_bar: gtk::ProgressBar,
sender: Sender<Action>,
) {
let progress = progress_bar.clone();
@@ -206,6 +243,7 @@ fn on_download_clicked(
let man = DOWNLOADS_MANAGER.lock().unwrap();
man.add(ep.rowid(), &download_fold);
}
sender.send(Action::RefreshEpisodesViewBGR).unwrap();
}
fn on_play_bttn_clicked(episode_id: i32) {
@@ -240,13 +278,13 @@ fn on_play_bttn_clicked(episode_id: i32) {
// };
// }
pub fn episodes_listbox(pd: &Podcast) -> Result<gtk::ListBox> {
pub fn episodes_listbox(pd: &Podcast, sender: Sender<Action>) -> Result<gtk::ListBox> {
let mut episodes = dbqueries::get_pd_episodeswidgets(pd)?;
let list = gtk::ListBox::new();
episodes.iter_mut().for_each(|ep| {
let widget = EpisodeWidget::new(ep);
let widget = EpisodeWidget::new(ep, sender.clone());
list.add(&widget.container);
});
+73 -40
View File
@@ -12,14 +12,17 @@ use hammond_downloader::downloader;
use widgets::episode::episodes_listbox;
use utils::get_pixbuf_from_path;
use content::ShowStack;
use headerbar::Header;
use app::Action;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::thread;
use std::fs;
#[derive(Debug, Clone)]
pub struct ShowWidget {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
cover: gtk::Image,
description: gtk::Label,
link: gtk::Button,
@@ -32,6 +35,7 @@ impl Default for ShowWidget {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let episodes: gtk::Frame = builder.get_object("episodes").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
@@ -42,6 +46,7 @@ impl Default for ShowWidget {
ShowWidget {
container,
scrolled_window,
cover,
description,
unsub,
@@ -53,67 +58,95 @@ impl Default for ShowWidget {
}
impl ShowWidget {
pub fn new(shows: Rc<ShowStack>, header: Rc<Header>, pd: &Podcast) -> ShowWidget {
pub fn new(shows: Arc<ShowStack>, pd: &Podcast, sender: Sender<Action>) -> ShowWidget {
let pdw = ShowWidget::default();
pdw.init(shows, header, pd);
pdw.init(shows, pd, sender);
pdw
}
pub fn init(&self, shows: Rc<ShowStack>, header: Rc<Header>, pd: &Podcast) {
pub fn init(&self, shows: Arc<ShowStack>, pd: &Podcast, sender: Sender<Action>) {
// Hacky workaround so the pd.id() can be retrieved from the `ShowStack`.
WidgetExt::set_name(&self.container, &pd.id().to_string());
self.unsub.connect_clicked(clone!(shows, pd => move |bttn| {
on_unsub_button_clicked(shows.clone(), &pd, bttn);
header.switch_to_normal();
self.unsub
.connect_clicked(clone!(shows, pd, sender => move |bttn| {
on_unsub_button_clicked(shows.clone(), &pd, bttn, sender.clone());
sender.send(Action::HeaderBarNormal).unwrap();
}));
let listbox = episodes_listbox(pd);
if let Ok(l) = listbox {
self.episodes.add(&l);
}
// TODO: Temporary solution until we render html urls/bold/italic probably with markup.
let desc = dissolve::strip_html_tags(pd.description()).join(" ");
self.description.set_text(&replace_extra_spaces(&desc));
let img = get_pixbuf_from_path(&pd.clone().into(), 128);
if let Some(i) = img {
self.cover.set_from_pixbuf(&i);
}
self.setup_listbox(pd, sender.clone());
self.set_cover(pd);
self.set_description(pd.description());
let link = pd.link().to_owned();
self.link.set_tooltip_text(Some(link.as_str()));
self.link.connect_clicked(move |_| {
info!("Opening link: {}", &link);
let _ = open::that(&link);
});
}
// self.played.connect_clicked(clone!(shows, pd => move |_| {
// on_played_button_clicked(shows.clone(), &pd);
// }));
/// Populate the listbox with the shows episodes.
fn setup_listbox(&self, pd: &Podcast, sender: Sender<Action>) {
let listbox = episodes_listbox(pd, sender.clone());
if let Ok(l) = listbox {
self.episodes.add(&l);
}
}
/// Set the show cover.
fn set_cover(&self, pd: &Podcast) {
let img = get_pixbuf_from_path(&pd.clone().into(), 128);
if let Some(i) = img {
self.cover.set_from_pixbuf(&i);
}
}
/// Set the descripton text.
fn set_description(&self, text: &str) {
// TODO: Temporary solution until we render html urls/bold/italic probably with markup.
let desc = dissolve::strip_html_tags(text).join(" ");
self.description.set_text(&replace_extra_spaces(&desc));
}
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
fn on_unsub_button_clicked(shows: Rc<ShowStack>, pd: &Podcast, unsub_button: &gtk::Button) {
let res = dbqueries::remove_feed(pd);
if res.is_ok() {
info!("{} was removed succesfully.", pd.title());
// hack to get away without properly checking for none.
// if pressed twice would panic.
unsub_button.hide();
fn on_unsub_button_clicked(
shows: Arc<ShowStack>,
pd: &Podcast,
unsub_button: &gtk::Button,
sender: Sender<Action>,
) {
// hack to get away without properly checking for none.
// if pressed twice would panic.
unsub_button.hide();
// Spawn a thread so it won't block the ui.
thread::spawn(clone!(pd => move || {
let res = dbqueries::remove_feed(&pd);
if res.is_ok() {
info!("{} was removed succesfully.", pd.title());
let dl_fold = downloader::get_download_folder(pd.title());
if let Ok(fold) = dl_fold {
let res3 = fs::remove_dir_all(&fold);
if res3.is_ok() {
info!("All the content at, {} was removed succesfully", &fold);
}
};
}
let dl_fold = downloader::get_download_folder(pd.title());
if let Ok(fold) = dl_fold {
let res3 = fs::remove_dir_all(&fold);
// TODO: Show errors?
if res3.is_ok() {
info!("All the content at, {} was removed succesfully", &fold);
}
};
}
}));
shows.switch_podcasts_animated();
// Queue a refresh after the switch to avoid blocking the db.
sender.send(Action::RefreshViews).unwrap();
}
#[allow(dead_code)]
fn on_played_button_clicked(shows: Rc<ShowStack>, pd: &Podcast) {
fn on_played_button_clicked(shows: Arc<ShowStack>, pd: &Podcast) {
let _ = dbqueries::update_none_to_played_now(pd);
shows.update_widget();