Merge branch 'master' into 33-downloader-re-work
This commit is contained in:
+61
-58
@@ -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
@@ -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");
|
||||
|
||||
@@ -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: >k::Entry) {
|
||||
fn on_add_bttn_clicked(entry: >k::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
@@ -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())
|
||||
|
||||
@@ -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: >k::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 {
|
||||
|
||||
@@ -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: >k::Adjustment) {
|
||||
self.scrolled_window.set_vadjustment(vadjustment)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -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: >k::Button,
|
||||
cancel_bttn: >k::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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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: >k::Adjustment) {
|
||||
self.scrolled_window.set_vadjustment(vadjustment)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_unsub_button_clicked(shows: Rc<ShowStack>, pd: &Podcast, unsub_button: >k::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: >k::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();
|
||||
|
||||
Reference in New Issue
Block a user