Commit e9dd297b authored by Jordan Petridis's avatar Jordan Petridis 🌱

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

parents c67c6e46 eafad358
This diff is collapsed.
......@@ -14,7 +14,7 @@ itertools = "0.7.4"
lazy_static = "1.0.0"
log = "0.3.8"
r2d2 = "0.8.2"
r2d2-diesel = "0.99.0"
r2d2-diesel = "1.0.0"
rayon = "0.9.0"
reqwest = "0.8.2"
rfc822_sanitizer = "0.3.3"
......@@ -24,11 +24,11 @@ xdg = "2.1.0"
[dependencies.diesel]
features = ["sqlite"]
version = "0.99.0"
version = "1.0.0"
[dependencies.diesel_migrations]
features = ["sqlite"]
version = "0.99.0"
version = "1.0.0"
[dev-dependencies]
rand = "0.4.1"
......
......@@ -84,8 +84,10 @@ pub fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<()> {
/// Runs a cleaner for played Episode's that are pass the lifetime limit and
/// scheduled for removal.
pub fn checkup() -> Result<()> {
info!("Running database checks.");
download_checker()?;
played_cleaner()?;
info!("Checks completed.");
Ok(())
}
......
......@@ -16,7 +16,7 @@ lazy_static = "1.0.0"
[dependencies.diesel]
features = ["sqlite"]
version = "0.99.0"
version = "1.0.0"
[dependencies.hammond-data]
path = "../hammond-data"
......@@ -23,7 +23,7 @@ send-cell = "0.1.2"
[dependencies.diesel]
features = ["sqlite"]
version = "0.99.0"
version = "1.0.0"
[dependencies.gtk]
features = ["v3_22"]
......
......@@ -15,22 +15,22 @@
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="valign">start</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Episode Title</property>
<property name="ellipsize">end</property>
<property name="single_line_mode">True</property>
<property name="max_width_chars">60</property>
<property name="max_width_chars">64</property>
<property name="track_visited_links">False</property>
<property name="lines">1</property>
</object>
......@@ -51,12 +51,13 @@
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="date_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">1970/01/01</property>
<property name="label" translatable="yes">3 Jan</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<style>
......@@ -197,7 +198,6 @@
<property name="tooltip_text" translatable="yes">Download this episode</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="action_name">app.refresh_episodes</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage">
......@@ -221,7 +221,6 @@
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Play this episode</property>
<property name="valign">center</property>
<property name="action_name">app.refresh_episodes</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
......
......@@ -3,11 +3,13 @@
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="container">
<property name="name">container</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
......@@ -37,7 +39,7 @@
</child>
<child>
<object class="GtkBox" id="frame_parent">
<property name="width_request">600</property>
<property name="width_request">720</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
......
......@@ -125,7 +125,6 @@
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Add a new feed</property>
<property name="valign">center</property>
<property name="popover">add_popover</property>
<child>
<object class="GtkImage" id="add-button-image2">
<property name="visible">True</property>
......
......@@ -7,7 +7,8 @@
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
......@@ -39,7 +40,7 @@
</child>
<child>
<object class="GtkBox">
<property name="width_request">600</property>
<property name="width_request">624</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
......@@ -151,7 +152,6 @@
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="action_name">app.refresh</property>
<style>
<class name="destructive-action"/>
</style>
......
......@@ -3,16 +3,18 @@
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="fb_parent">
<property name="name">fb_parent</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="name">scrolled_window</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport" id="viewport">
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
......
......@@ -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, &[]);
}
}
......
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");
......
......@@ -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());
}
}
use send_cell::SendCell;
use glib;
use gdk_pixbuf::Pixbuf;
use hammond_data::feed;
......@@ -7,61 +6,32 @@ 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 || {