Inline a bunch of stuff.

This commit is contained in:
Jordan Petridis 2018-04-17 09:04:18 +03:00
parent 627f06ea9f
commit 7c03266d16
No known key found for this signature in database
GPG Key ID: CEABAD9F5683B9A6
8 changed files with 101 additions and 0 deletions

View File

@ -147,6 +147,7 @@ impl App {
});
}
#[inline]
pub fn run(self) {
WindowGeometry::from_settings(&self.settings).apply(&self.window);

View File

@ -211,6 +211,7 @@ lazy_static! {
// GObjects do not implement Send trait, so SendCell is a way around that.
// Also lazy_static requires Sync trait, so that's what the mutexes are.
// TODO: maybe use something that would just scale to requested size?
#[inline]
pub fn set_image_from_path(
image: &gtk::Image,
pd: Arc<PodcastCoverQuery>,

View File

@ -6,6 +6,7 @@ pub struct EmptyView {
}
impl Default for EmptyView {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_view.ui");
let view: gtk::Box = builder.get_object("empty_view").unwrap();
@ -15,6 +16,7 @@ impl Default for EmptyView {
}
impl EmptyView {
#[inline]
pub fn new() -> EmptyView {
EmptyView::default()
}

View File

@ -40,6 +40,7 @@ pub struct EpisodesView {
}
impl Default for EpisodesView {
#[inline]
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();
@ -76,6 +77,7 @@ impl Default for EpisodesView {
// TODO: REFACTOR ME
impl EpisodesView {
#[inline]
pub fn new(sender: Sender<Action>) -> Result<EpisodesView, Error> {
let view = EpisodesView::default();
let ignore = get_ignored_shows()?;
@ -130,6 +132,7 @@ impl EpisodesView {
Ok(view)
}
#[inline]
pub fn is_empty(&self) -> bool {
if !self.today_list.get_children().is_empty() {
return false;
@ -154,12 +157,14 @@ impl EpisodesView {
true
}
#[inline]
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
#[inline]
fn split(now: &DateTime<Utc>, epoch: i64) -> ListSplit {
let ep = Utc.timestamp(epoch, 0);
@ -184,6 +189,7 @@ struct EpisodesViewWidget {
}
impl Default for EpisodesViewWidget {
#[inline]
fn default() -> Self {
let builder =
gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episodes_view_widget.ui");
@ -219,6 +225,7 @@ impl EpisodesViewWidget {
view
}
#[inline]
fn init(&self, podcast_id: i32) {
self.set_cover(podcast_id)
.map_err(|err| error!("Failed to set a cover: {}", err))
@ -227,6 +234,7 @@ impl EpisodesViewWidget {
self.container.pack_start(&self.episode, true, true, 6);
}
#[inline]
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
let pd = Arc::new(dbqueries::get_podcast_cover_from_id(podcast_id)?);
set_image_from_path(&self.image, pd, 64)

View File

@ -19,6 +19,7 @@ pub struct ShowsPopulated {
}
impl Default for ShowsPopulated {
#[inline]
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();
@ -34,12 +35,14 @@ impl Default for ShowsPopulated {
}
impl ShowsPopulated {
#[inline]
pub fn new(sender: Sender<Action>) -> Result<ShowsPopulated, Error> {
let pop = ShowsPopulated::default();
pop.init(sender)?;
Ok(pop)
}
#[inline]
pub fn init(&self, sender: Sender<Action>) -> Result<(), Error> {
self.flowbox.connect_child_activated(move |_, child| {
on_child_activate(child, sender.clone())
@ -50,6 +53,7 @@ impl ShowsPopulated {
self.populate_flowbox()
}
#[inline]
fn populate_flowbox(&self) -> Result<(), Error> {
let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
@ -62,16 +66,19 @@ impl ShowsPopulated {
Ok(())
}
#[inline]
pub fn is_empty(&self) -> bool {
self.flowbox.get_children().is_empty()
}
#[inline]
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
}
#[inline]
fn on_child_activate(child: &gtk::FlowBoxChild, sender: Sender<Action>) -> Result<(), Error> {
use gtk::WidgetExt;
@ -95,6 +102,7 @@ struct ShowsChild {
}
impl Default for ShowsChild {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_child.ui");
@ -113,12 +121,14 @@ impl Default for ShowsChild {
}
impl ShowsChild {
#[inline]
pub fn new(pd: Podcast) -> ShowsChild {
let child = ShowsChild::default();
child.init(pd);
child
}
#[inline]
fn init(&self, pd: Podcast) {
self.container.set_tooltip_text(pd.title());
WidgetExt::set_name(&self.child, &pd.id().to_string());
@ -128,6 +138,7 @@ impl ShowsChild {
.ok();
}
#[inline]
fn set_cover(&self, pd: Arc<PodcastCoverQuery>) -> Result<(), Error> {
set_image_from_path(&self.cover, pd, 256)
}

View File

@ -32,6 +32,7 @@ pub struct EpisodeWidget {
}
impl Default for EpisodeWidget {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
@ -78,12 +79,14 @@ impl Default for EpisodeWidget {
}
impl EpisodeWidget {
#[inline]
pub fn new(episode: EpisodeWidgetQuery, sender: Sender<Action>) -> EpisodeWidget {
let mut widget = EpisodeWidget::default();
widget.init(episode, sender);
widget
}
#[inline]
fn init(&mut self, episode: EpisodeWidgetQuery, sender: Sender<Action>) {
// Set the date label.
self.set_date(episode.epoch());
@ -104,6 +107,7 @@ impl EpisodeWidget {
self.connect_buttons(episode, sender);
}
#[inline]
fn connect_buttons(&self, episode: Arc<Mutex<EpisodeWidgetQuery>>, sender: Sender<Action>) {
let title = self.title.clone();
if let Ok(media) = self.media.lock() {
@ -136,6 +140,7 @@ impl EpisodeWidget {
}
}
#[inline]
/// Determine the title state.
fn set_title(&mut self, episode: &EpisodeWidgetQuery) {
let mut machine = self.title.borrow_mut();
@ -145,12 +150,14 @@ impl EpisodeWidget {
});
}
#[inline]
/// Set the date label depending on the current time.
fn set_date(&mut self, epoch: i32) {
let machine = &mut self.date;
take_mut::take(machine, |date| date.determine_state(i64::from(epoch)));
}
#[inline]
/// Set the duration label.
fn set_duration(&mut self, seconds: Option<i32>) {
let machine = &mut self.duration;
@ -158,6 +165,7 @@ impl EpisodeWidget {
}
}
#[inline]
fn determine_media_state(
media_machine: Arc<Mutex<MediaMachine>>,
episode: &EpisodeWidgetQuery,
@ -244,6 +252,7 @@ fn on_play_bttn_clicked(
Ok(())
}
#[inline]
fn open_uri(rowid: i32) -> Result<(), Error> {
let uri = dbqueries::get_episode_local_uri_from_id(rowid)?
.ok_or_else(|| format_err!("Expected Some found None."))?;

View File

@ -61,6 +61,7 @@ pub struct Title<S> {
impl<S> Title<S> {
#[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn set_title(&mut self, s: &str) {
@ -69,6 +70,7 @@ impl<S> Title<S> {
}
impl Title<Normal> {
#[inline]
fn new(title: gtk::Label) -> Self {
Title {
title,
@ -78,6 +80,7 @@ impl Title<Normal> {
}
impl From<Title<Normal>> for Title<GreyedOut> {
#[inline]
fn from(f: Title<Normal>) -> Self {
f.title
.get_style_context()
@ -91,6 +94,7 @@ impl From<Title<Normal>> for Title<GreyedOut> {
}
impl From<Title<GreyedOut>> for Title<Normal> {
#[inline]
fn from(f: Title<GreyedOut>) -> Self {
f.title
.get_style_context()
@ -110,11 +114,13 @@ pub enum TitleMachine {
}
impl TitleMachine {
#[inline]
pub fn new(label: gtk::Label, is_played: bool) -> Self {
let m = TitleMachine::Normal(Title::<Normal>::new(label));
m.determine_state(is_played)
}
#[inline]
pub fn determine_state(self, is_played: bool) -> Self {
use self::TitleMachine::*;
@ -126,6 +132,7 @@ impl TitleMachine {
}
}
#[inline]
pub fn set_title(&mut self, s: &str) {
use self::TitleMachine::*;
@ -149,6 +156,7 @@ pub struct Date<S> {
}
impl<S> Date<S> {
#[inline]
fn into_usual(self, epoch: i64) -> Date<Usual> {
let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b").to_string().trim());
@ -160,6 +168,7 @@ impl<S> Date<S> {
}
}
#[inline]
fn into_year_shown(self, epoch: i64) -> Date<YearShown> {
let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b %Y").to_string().trim());
@ -173,6 +182,7 @@ impl<S> Date<S> {
}
impl Date<UnInitialized> {
#[inline]
fn new(date: gtk::Label, epoch: i64) -> Self {
let ts = Utc.timestamp(epoch, 0);
date.set_text(ts.format("%e %b %Y").to_string().trim());
@ -193,11 +203,13 @@ pub enum DateMachine {
}
impl DateMachine {
#[inline]
pub fn new(label: gtk::Label, epoch: i64) -> Self {
let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch));
m.determine_state(epoch)
}
#[inline]
pub fn determine_state(self, epoch: i64) -> Self {
use self::DateMachine::*;
@ -227,6 +239,7 @@ pub struct Duration<S: Visibility> {
}
impl<S: Visibility> Duration<S> {
#[inline]
// This needs a better name.
// TODO: make me mut
fn set_duration(&self, minutes: i64) {
@ -235,6 +248,7 @@ impl<S: Visibility> Duration<S> {
}
impl Duration<Hidden> {
#[inline]
fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
duration.hide();
separator.hide();
@ -248,6 +262,7 @@ impl Duration<Hidden> {
}
impl From<Duration<Hidden>> for Duration<Shown> {
#[inline]
fn from(f: Duration<Hidden>) -> Self {
f.duration.show();
f.separator.show();
@ -261,6 +276,7 @@ impl From<Duration<Hidden>> for Duration<Shown> {
}
impl From<Duration<Shown>> for Duration<Hidden> {
#[inline]
fn from(f: Duration<Shown>) -> Self {
f.duration.hide();
f.separator.hide();
@ -280,11 +296,13 @@ pub enum DurationMachine {
}
impl DurationMachine {
#[inline]
pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self {
let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator));
m.determine_state(seconds)
}
#[inline]
pub fn determine_state(self, seconds: Option<i32>) -> Self {
match (self, seconds) {
(d @ DurationMachine::Hidden(_), None) => d,
@ -319,6 +337,7 @@ pub struct Size<S> {
}
impl<S> Size<S> {
#[inline]
fn set_size(self, s: &str) -> Size<Shown> {
self.size.set_text(s);
self.size.show();
@ -330,6 +349,7 @@ impl<S> Size<S> {
}
}
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844
@ -344,6 +364,7 @@ impl<S> Size<S> {
}
}
#[inline]
fn into_hidden(self) -> Size<Hidden> {
self.size.hide();
self.separator.hide();
@ -357,6 +378,7 @@ impl<S> Size<S> {
}
impl Size<UnInitialized> {
#[inline]
fn new(size: gtk::Label, separator: gtk::Label) -> Self {
size.hide();
separator.hide();
@ -390,6 +412,7 @@ pub struct DownloadPlay<S> {
}
impl<S> DownloadPlay<S> {
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844
@ -404,6 +427,7 @@ impl<S> DownloadPlay<S> {
}
}
#[inline]
fn into_fetchable(self) -> DownloadPlay<Download> {
self.play.hide();
self.download.show();
@ -415,6 +439,7 @@ impl<S> DownloadPlay<S> {
}
}
#[inline]
fn into_hidden(self) -> DownloadPlay<Hidden> {
self.play.hide();
self.download.hide();
@ -426,6 +451,7 @@ impl<S> DownloadPlay<S> {
}
}
#[inline]
fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@ -433,12 +459,14 @@ impl<S> DownloadPlay<S> {
self.download.connect_clicked(f)
}
#[inline]
fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.play.connect_clicked(f)
}
}
impl DownloadPlay<UnInitialized> {
#[inline]
fn new(play: gtk::Button, download: gtk::Button) -> Self {
play.hide();
download.hide();
@ -461,6 +489,7 @@ pub struct Progress<S> {
}
impl<S> Progress<S> {
#[inline]
fn into_shown(self) -> Progress<Shown> {
self.bar.show();
self.cancel.show();
@ -476,6 +505,7 @@ impl<S> Progress<S> {
}
}
#[inline]
fn into_hidden(self) -> Progress<Hidden> {
self.bar.hide();
self.cancel.hide();
@ -492,6 +522,7 @@ impl<S> Progress<S> {
}
#[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn update_progress(&mut self, local_size: &str, fraction: f64) {
@ -499,12 +530,14 @@ impl<S> Progress<S> {
self.bar.set_fraction(fraction);
}
#[inline]
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.cancel.connect_clicked(f)
}
}
impl Progress<UnInitialized> {
#[inline]
fn new(
bar: gtk::ProgressBar,
cancel: gtk::Button,
@ -538,6 +571,7 @@ type Playable<Y> = Media<Play, Y, Hidden>;
type InProgress = Media<Hidden, Shown, Shown>;
impl<X, Y, Z> Media<X, Y, Z> {
#[inline]
fn set_size(self, s: &str) -> Media<X, Shown, Z> {
Media {
dl: self.dl,
@ -546,6 +580,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn hide_size(self) -> Media<X, Hidden, Z> {
Media {
dl: self.dl,
@ -554,6 +589,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn into_new(self, size: &str) -> New<Shown> {
Media {
dl: self.dl.into_fetchable(),
@ -562,6 +598,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn into_new_without(self) -> New<Hidden> {
Media {
dl: self.dl.into_fetchable(),
@ -570,6 +607,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn into_playable(self, size: &str) -> Playable<Shown> {
Media {
dl: self.dl.into_playable(),
@ -578,6 +616,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
}
#[inline]
fn into_playable_without(self) -> Playable<Hidden> {
Media {
dl: self.dl.into_playable(),
@ -588,6 +627,7 @@ impl<X, Y, Z> Media<X, Y, Z> {
}
impl<X, Z> Media<X, Shown, Z> {
#[inline]
fn into_progress(self) -> InProgress {
Media {
dl: self.dl.into_hidden(),
@ -598,6 +638,7 @@ impl<X, Z> Media<X, Shown, Z> {
}
impl<X, Z> Media<X, Hidden, Z> {
#[inline]
fn into_progress(self) -> InProgress {
Media {
dl: self.dl.into_hidden(),
@ -608,6 +649,7 @@ impl<X, Z> Media<X, Hidden, Z> {
}
impl<X, Z> Media<X, UnInitialized, Z> {
#[inline]
fn into_progress(self, size: Option<String>) -> InProgress {
if let Some(s) = size {
Media {
@ -626,6 +668,7 @@ impl<X, Z> Media<X, UnInitialized, Z> {
}
impl InProgress {
#[inline]
#[allow(unused_must_use)]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
@ -643,6 +686,7 @@ pub enum ButtonsState {
}
impl ButtonsState {
#[inline]
pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self {
use self::ButtonsState::*;
@ -677,6 +721,7 @@ impl ButtonsState {
}
}
#[inline]
fn into_progress(self) -> InProgress {
use self::ButtonsState::*;
@ -688,6 +733,7 @@ impl ButtonsState {
}
}
#[inline]
fn set_size(self, size: Option<String>) -> Self {
use self::ButtonsState::*;
@ -703,6 +749,7 @@ impl ButtonsState {
}
}
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@ -717,6 +764,7 @@ impl ButtonsState {
}
}
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@ -731,6 +779,7 @@ impl ButtonsState {
}
}
#[inline]
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
use self::ButtonsState::*;
@ -752,6 +801,7 @@ pub enum MediaMachine {
impl MediaMachine {
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
#[inline]
pub fn new(
play: gtk::Button,
download: gtk::Button,
@ -769,6 +819,7 @@ impl MediaMachine {
MediaMachine::UnInitialized(Media { dl, progress, size })
}
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@ -782,6 +833,7 @@ impl MediaMachine {
}
}
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@ -795,6 +847,7 @@ impl MediaMachine {
}
}
#[inline]
pub fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
@ -808,6 +861,7 @@ impl MediaMachine {
}
}
#[inline]
pub fn determine_state(self, bytes: Option<i32>, is_active: bool, is_downloaded: bool) -> Self {
use self::ButtonsState::*;
use self::MediaMachine::*;
@ -846,6 +900,7 @@ impl MediaMachine {
}
}
#[inline]
pub fn set_size(self, bytes: Option<i32>) -> Self {
use self::MediaMachine::*;
let size = size_helper(bytes);
@ -858,6 +913,7 @@ impl MediaMachine {
}
}
#[inline]
pub fn update_progress(&mut self, local_size: &str, fraction: f64) {
use self::MediaMachine::*;

View File

@ -32,6 +32,7 @@ pub struct ShowWidget {
}
impl Default for ShowWidget {
#[inline]
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();
@ -58,12 +59,14 @@ impl Default for ShowWidget {
}
impl ShowWidget {
#[inline]
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> ShowWidget {
let pdw = ShowWidget::default();
pdw.init(pd, sender);
pdw
}
#[inline]
pub fn init(&self, pd: Arc<Podcast>, sender: Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
@ -109,21 +112,25 @@ impl ShowWidget {
self.settings.set_popover(&show_menu);
}
#[inline]
/// Set the show cover.
fn set_cover(&self, pd: Arc<Podcast>) -> Result<(), Error> {
utils::set_image_from_path(&self.cover, Arc::new(pd.into()), 128)
}
#[inline]
/// Set the descripton text.
fn set_description(&self, text: &str) {
self.description.set_markup(&markup_from_raw(text));
}
#[inline]
/// Set scrolled window vertical adjustment.
pub fn set_vadjustment(&self, vadjustment: &gtk::Adjustment) {
self.scrolled_window.set_vadjustment(vadjustment)
}
#[inline]
/// Populate the listbox with the shows episodes.
fn populate_listbox(&self, pd: Arc<Podcast>, sender: Sender<Action>) -> Result<(), Error> {
use crossbeam_channel::bounded;
@ -173,6 +180,7 @@ impl ShowWidget {
}
}
#[inline]
fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: &gtk::Button, sender: Sender<Action>) {
// hack to get away without properly checking for none.
// if pressed twice would panic.
@ -193,6 +201,7 @@ fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: &gtk::Button, sender:
unsub_button.set_sensitive(true);
}
#[inline]
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::ListBox, sender: Sender<Action>) {
if dim_titles(episodes).is_none() {
error!("Something went horribly wrong when dimming the titles.");
@ -205,6 +214,7 @@ fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::ListBox, sender: S
.ok();
}
#[inline]
fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
dbqueries::update_none_to_played_now(pd)?;
// Not all widgets migth have been loaded when the mark_all is hit
@ -213,6 +223,7 @@ fn mark_all_watched(pd: &Podcast, sender: Sender<Action>) -> Result<(), Error> {
sender.send(Action::RefreshEpisodesView).map_err(From::from)
}
#[inline]
pub fn mark_all_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
let id = pd.id();
let callback = clone!(sender => move || {
@ -232,6 +243,7 @@ pub fn mark_all_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotifica
InAppNotification::new(text, callback, undo_callback)
}
#[inline]
pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
let text = format!("Unsubscribed from {}", pd.title());
@ -270,6 +282,7 @@ pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotif
InAppNotification::new(text, callback, undo_callback)
}
#[inline]
// 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