Merge branch 'master' into state-machines-experiements
This commit is contained in:
@@ -23,6 +23,9 @@ url = "1.6.0"
|
||||
failure = "0.1.1"
|
||||
failure_derive = "0.1.1"
|
||||
take_mut = "0.2.0"
|
||||
regex = "0.2.6"
|
||||
reqwest = "0.8.5"
|
||||
serde_json = "1.0"
|
||||
|
||||
[dependencies.gtk]
|
||||
features = ["v3_22"]
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.21.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkRevealer" id="revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">6</property>
|
||||
<property name="margin_right">6</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="text">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes">An in-app action notification</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">window-close-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat image-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="undo">
|
||||
<property name="label" translatable="yes">Undo</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<style>
|
||||
<class name="text-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="top"/>
|
||||
</style>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -207,7 +207,7 @@ Tobias Bernard
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -227,7 +227,7 @@ Tobias Bernard
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -266,4 +266,33 @@ Tobias Bernard
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkPopover" id="show_menu">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="position">bottom</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">6</property>
|
||||
<property name="margin_right">6</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkModelButton" id="mark_all_watched">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="text" translatable="yes">Mark all episodes as listened</property>
|
||||
<property name="centered">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<file preprocess="xml-stripblanks">gtk/shows_view.ui</file>
|
||||
<file preprocess="xml-stripblanks">gtk/shows_child.ui</file>
|
||||
<file preprocess="xml-stripblanks">gtk/headerbar.ui</file>
|
||||
<file preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
|
||||
<file compressed="true">gtk/style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
+30
-6
@@ -8,9 +8,11 @@ use gtk::prelude::*;
|
||||
use hammond_data::{Podcast, Source};
|
||||
use hammond_data::utils::checkup;
|
||||
|
||||
use appnotif::*;
|
||||
use headerbar::Header;
|
||||
use stacks::Content;
|
||||
use utils;
|
||||
use widgets::mark_all_watched;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
@@ -33,12 +35,14 @@ pub enum Action {
|
||||
HeaderBarNormal,
|
||||
HeaderBarShowUpdateIndicator,
|
||||
HeaderBarHideUpdateIndicator,
|
||||
MarkAllPlayerNotification(Arc<Podcast>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
app_instance: gtk::Application,
|
||||
window: gtk::Window,
|
||||
overlay: gtk::Overlay,
|
||||
header: Arc<Header>,
|
||||
content: Arc<Content>,
|
||||
receiver: Receiver<Action>,
|
||||
@@ -73,12 +77,17 @@ impl App {
|
||||
// Create the headerbar
|
||||
let header = Arc::new(Header::new(&content, &window, sender.clone()));
|
||||
|
||||
// Add the content main stack to the window.
|
||||
window.add(&content.get_stack());
|
||||
// Add the content main stack to the overlay.
|
||||
let overlay = gtk::Overlay::new();
|
||||
overlay.add(&content.get_stack());
|
||||
|
||||
// Add the overlay to the main window
|
||||
window.add(&overlay);
|
||||
|
||||
App {
|
||||
app_instance: application,
|
||||
window,
|
||||
overlay,
|
||||
header,
|
||||
content,
|
||||
receiver,
|
||||
@@ -86,7 +95,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_timed_callbacks(&self) {
|
||||
fn setup_timed_callbacks(&self) {
|
||||
let sender = self.sender.clone();
|
||||
// Update the feeds right after the Application is initialized.
|
||||
gtk::timeout_add_seconds(2, move || {
|
||||
@@ -114,15 +123,15 @@ impl App {
|
||||
|
||||
pub fn run(self) {
|
||||
let window = self.window.clone();
|
||||
let app = self.app_instance.clone();
|
||||
self.app_instance.connect_startup(move |_| {
|
||||
build_ui(&window, &app);
|
||||
self.app_instance.connect_startup(move |app| {
|
||||
build_ui(&window, app);
|
||||
});
|
||||
self.setup_timed_callbacks();
|
||||
|
||||
let content = self.content.clone();
|
||||
let headerbar = self.header.clone();
|
||||
let sender = self.sender.clone();
|
||||
let overlay = self.overlay.clone();
|
||||
let receiver = self.receiver;
|
||||
gtk::idle_add(move || {
|
||||
match receiver.recv_timeout(Duration::from_millis(10)) {
|
||||
@@ -152,6 +161,21 @@ impl App {
|
||||
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
|
||||
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
|
||||
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
|
||||
Ok(Action::MarkAllPlayerNotification(pd)) => {
|
||||
let callback = clone!(sender => move || {
|
||||
if let Err(err) = mark_all_watched(&pd, sender.clone()) {
|
||||
error!("Something went horribly wrong with the notif callback: {}", err);
|
||||
}
|
||||
glib::Continue(false)
|
||||
});
|
||||
|
||||
let text = "Marked all episodes as listened";
|
||||
let notif = InAppNotification::new(text.into(), callback, sender.clone());
|
||||
overlay.add_overlay(¬if.revealer);
|
||||
// We need to display the notification after the widget is added to the overlay
|
||||
// so there will be a nice animation.
|
||||
notif.show();
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use app::Action;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InAppNotification {
|
||||
pub revealer: gtk::Revealer,
|
||||
text: gtk::Label,
|
||||
undo: gtk::Button,
|
||||
close: gtk::Button,
|
||||
}
|
||||
|
||||
impl Default for InAppNotification {
|
||||
fn default() -> Self {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/inapp_notif.ui");
|
||||
|
||||
let revealer: gtk::Revealer = builder.get_object("revealer").unwrap();
|
||||
let text: gtk::Label = builder.get_object("text").unwrap();
|
||||
let undo: gtk::Button = builder.get_object("undo").unwrap();
|
||||
let close: gtk::Button = builder.get_object("close").unwrap();
|
||||
|
||||
InAppNotification {
|
||||
revealer,
|
||||
text,
|
||||
undo,
|
||||
close,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InAppNotification {
|
||||
pub fn new<F>(text: String, mut callback: F, sender: Sender<Action>) -> Self
|
||||
where
|
||||
F: FnMut() -> glib::Continue + 'static,
|
||||
{
|
||||
let notif = InAppNotification::default();
|
||||
notif.text.set_text(&text);
|
||||
|
||||
let revealer = notif.revealer.clone();
|
||||
let id = timeout_add_seconds(6, move || {
|
||||
revealer.set_reveal_child(false);
|
||||
callback()
|
||||
});
|
||||
let id = Rc::new(RefCell::new(Some(id)));
|
||||
|
||||
// Cancel the callback
|
||||
let revealer = notif.revealer.clone();
|
||||
notif.undo.connect_clicked(move |_| {
|
||||
let foo = id.borrow_mut().take();
|
||||
if let Some(id) = foo {
|
||||
glib::source::source_remove(id);
|
||||
}
|
||||
|
||||
// Hide the notification
|
||||
revealer.set_reveal_child(false);
|
||||
// Refresh the widget if visible
|
||||
if let Err(err) = sender.send(Action::RefreshWidgetIfVis) {
|
||||
error!(
|
||||
"Something went horribly wrong with the Action channel: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
// Hide the revealer when the close button is clicked
|
||||
let revealer = notif.revealer.clone();
|
||||
notif.close.connect_clicked(move |_| {
|
||||
revealer.set_reveal_child(false);
|
||||
});
|
||||
|
||||
notif
|
||||
}
|
||||
|
||||
// This is a seperate method cause in order to get a nice animation
|
||||
// the revealer should be attached to something that will display it.
|
||||
// Previouslyi we where doing it in the constructor, which had the result
|
||||
// of the animation being skipped cause there was no parent widget to display it.
|
||||
pub fn show(&self) {
|
||||
self.revealer.set_reveal_child(true);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use std::sync::mpsc::Sender;
|
||||
|
||||
use app::Action;
|
||||
use stacks::Content;
|
||||
use utils::itunes_to_rss;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Header {
|
||||
@@ -154,8 +155,18 @@ impl Header {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: THIS ALSO SUCKS!
|
||||
fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender<Action>) -> Result<(), Error> {
|
||||
let url = entry.get_text().unwrap_or_default();
|
||||
let url = if url.contains("itunes.com") || url.contains("apple.com") {
|
||||
info!("Detected itunes url.");
|
||||
let foo = itunes_to_rss(&url)?;
|
||||
info!("Resolved to {}", foo);
|
||||
foo
|
||||
} else {
|
||||
url.to_owned()
|
||||
};
|
||||
|
||||
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
|
||||
entry.set_text("");
|
||||
|
||||
@@ -165,6 +176,7 @@ fn on_add_bttn_clicked(entry: >k::Entry, sender: Sender<Action>) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: THIS SUCKS!
|
||||
fn on_url_change(
|
||||
entry: >k::Entry,
|
||||
result: >k::Label,
|
||||
|
||||
@@ -26,8 +26,11 @@ extern crate hammond_downloader;
|
||||
extern crate humansize;
|
||||
extern crate loggerv;
|
||||
extern crate open;
|
||||
extern crate regex;
|
||||
extern crate reqwest;
|
||||
extern crate send_cell;
|
||||
extern crate take_mut;
|
||||
extern crate serde_json;
|
||||
extern crate url;
|
||||
// extern crate rayon;
|
||||
|
||||
@@ -67,6 +70,7 @@ pub mod app;
|
||||
pub mod utils;
|
||||
pub mod manager;
|
||||
pub mod static_resource;
|
||||
pub mod appnotif;
|
||||
|
||||
use app::App;
|
||||
|
||||
|
||||
@@ -73,9 +73,8 @@ impl DownloadProgress for Progress {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> = {
|
||||
Arc::new(RwLock::new(HashMap::new()))
|
||||
};
|
||||
pub static ref ACTIVE_DOWNLOADS: Arc<RwLock<HashMap<i32, Arc<Mutex<Progress>>>>> =
|
||||
{ Arc::new(RwLock::new(HashMap::new())) };
|
||||
}
|
||||
|
||||
pub fn add(id: i32, directory: &str, sender: Sender<Action>) -> Result<(), Error> {
|
||||
@@ -175,6 +174,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// This test needs access to local system so we ignore it by default.
|
||||
#[ignore]
|
||||
fn test_dl_steal_the_stars() {
|
||||
let url =
|
||||
"https://web.archive.org/web/20180120104957if_/https://rss.art19.com/steal-the-stars";
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
use failure::Error;
|
||||
use gdk_pixbuf::Pixbuf;
|
||||
use regex::Regex;
|
||||
use reqwest;
|
||||
use send_cell::SendCell;
|
||||
use serde_json::Value;
|
||||
|
||||
// use hammond_data::feed;
|
||||
use hammond_data::{PodcastCoverQuery, Source};
|
||||
@@ -74,9 +77,8 @@ fn refresh_feed(source: Option<Vec<Source>>, sender: Sender<Action>) -> Result<(
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> = {
|
||||
RwLock::new(HashMap::new())
|
||||
};
|
||||
static ref CACHED_PIXBUFS: RwLock<HashMap<(i32, u32), Mutex<SendCell<Pixbuf>>>> =
|
||||
{ RwLock::new(HashMap::new()) };
|
||||
}
|
||||
|
||||
// Since gdk_pixbuf::Pixbuf is refference counted and every episode,
|
||||
@@ -107,6 +109,35 @@ pub fn get_pixbuf_from_path(pd: &PodcastCoverQuery, size: u32) -> Result<Pixbuf,
|
||||
Ok(px)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// FIXME: the signature should be `fn foo(s: Url) -> Result<Url, Error>`
|
||||
pub fn itunes_to_rss(url: &str) -> Result<String, Error> {
|
||||
let id = itunes_id_from_url(url).ok_or_else(|| format_err!("Failed to find an Itunes ID."))?;
|
||||
lookup_id(id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn itunes_id_from_url(url: &str) -> Option<u32> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"/id([0-9]+)").unwrap();
|
||||
}
|
||||
|
||||
// Get the itunes id from the url
|
||||
let foo = RE.captures_iter(url).nth(0)?.get(1)?.as_str();
|
||||
// Parse it to a u32, this *should* never fail
|
||||
foo.parse::<u32>().ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn lookup_id(id: u32) -> Result<String, Error> {
|
||||
let url = format!("https://itunes.apple.com/lookup?id={}&entity=podcast", id);
|
||||
let req: Value = reqwest::get(&url)?.json()?;
|
||||
// FIXME: First time using serde, this could be done better and avoid using [] for indexing.
|
||||
let feedurl = req["results"][0]["feedUrl"].as_str();
|
||||
let feedurl = feedurl.ok_or_else(|| format_err!("Failed to get url from itunes response"))?;
|
||||
Ok(feedurl.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -130,4 +161,25 @@ mod tests {
|
||||
let pxbuf = get_pixbuf_from_path(&pd.into(), 256);
|
||||
assert!(pxbuf.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_itunes_to_rss() {
|
||||
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
||||
let rss_url = String::from("http://feeds.feedburner.com/InterceptedWithJeremyScahill");
|
||||
assert_eq!(rss_url, itunes_to_rss(itunes_url).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_itunes_id() {
|
||||
let id = 1195206601;
|
||||
let itunes_url = "https://itunes.apple.com/podcast/id1195206601";
|
||||
assert_eq!(id, itunes_id_from_url(itunes_url).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_itunes_lookup_id() {
|
||||
let id = 1195206601;
|
||||
let rss_url = "http://feeds.feedburner.com/InterceptedWithJeremyScahill";
|
||||
assert_eq!(rss_url, lookup_id(id).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ mod episode_states;
|
||||
|
||||
pub use self::episode::EpisodeWidget;
|
||||
pub use self::show::ShowWidget;
|
||||
pub use self::show::mark_all_watched;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use dissolve;
|
||||
use failure::Error;
|
||||
// use glib;
|
||||
use gtk;
|
||||
use gtk::prelude::*;
|
||||
use open;
|
||||
@@ -62,6 +63,8 @@ impl ShowWidget {
|
||||
}
|
||||
|
||||
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
|
||||
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
|
||||
|
||||
// Hacky workaround so the pd.id() can be retrieved from the `ShowStack`.
|
||||
WidgetExt::set_name(&self.container, &pd.id().to_string());
|
||||
|
||||
@@ -88,6 +91,19 @@ impl ShowWidget {
|
||||
error!("Error: {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
|
||||
let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap();
|
||||
|
||||
let episodes = self.episodes.clone();
|
||||
mark_all.connect_clicked(clone!(pd, sender => move |_| {
|
||||
on_played_button_clicked(
|
||||
pd.clone(),
|
||||
&episodes,
|
||||
sender.clone()
|
||||
)
|
||||
}));
|
||||
self.settings.set_popover(&show_menu);
|
||||
}
|
||||
|
||||
/// Populate the listbox with the shows episodes.
|
||||
@@ -142,9 +158,47 @@ fn on_unsub_button_clicked(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn on_played_button_clicked(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
|
||||
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: >k::Frame, sender: Sender<Action>) {
|
||||
if dim_titles(episodes).is_none() {
|
||||
error!("Something went horribly wrong when dimming the titles.");
|
||||
warn!("RUN WHILE YOU STILL CAN!");
|
||||
}
|
||||
|
||||
sender.send(Action::MarkAllPlayerNotification(pd)).unwrap();
|
||||
}
|
||||
|
||||
pub fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
|
||||
dbqueries::update_none_to_played_now(pd)?;
|
||||
sender.send(Action::RefreshWidget)?;
|
||||
sender.send(Action::RefreshWidgetIfVis)?;
|
||||
sender.send(Action::RefreshEpisodesView)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Ideally if we had a custom widget this would have been as simple as:
|
||||
// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
|
||||
// But now I can't think of a better way to do it than hardcoding the title
|
||||
// position relative to the EpisodeWidget container gtk::Box.
|
||||
fn dim_titles(episodes: >k::Frame) -> Option<()> {
|
||||
let listbox = episodes
|
||||
.get_children()
|
||||
.remove(0)
|
||||
.downcast::<gtk::ListBox>()
|
||||
.ok()?;
|
||||
let children = listbox.get_children();
|
||||
|
||||
for row in children {
|
||||
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
|
||||
let container = row.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||
let foo = container
|
||||
.get_children()
|
||||
.remove(0)
|
||||
.downcast::<gtk::Box>()
|
||||
.ok()?;
|
||||
let bar = foo.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||
let baz = bar.get_children().remove(0).downcast::<gtk::Box>().ok()?;
|
||||
let title = baz.get_children().remove(0).downcast::<gtk::Label>().ok()?;
|
||||
|
||||
title.get_style_context().map(|c| c.add_class("dim-label"));
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user