podcasts/podcasts-gtk/src/widgets/show.rs
James Westman 8081990895 Improve show description UI
Instead of being a scroll window inside a scroll window, the show
description now shows just the first paragraph by default, then displays
a "Read More" button if there is more to the description. Clicking the
button reveals the rest.

Currently, to keep the button from glitching when updating it from the
size-allocate signal, a GtkRevealer with a transition-duration of
1 millisecond is used. It's a hacky workaround but I'm not quite sure
how to do it better.

Fixes #81
2019-12-08 19:26:25 +00:00

226 lines
7.1 KiB
Rust

// show.rs
//
// Copyright 2017 Jordan Petridis <jpetridis@gnome.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use glib;
use gtk::{self, prelude::*, Adjustment};
use crossbeam_channel::{bounded, Sender};
use failure::Error;
use fragile::Fragile;
use html2text;
use libhandy::{Column, ColumnExt};
use rayon;
use podcasts_data::dbqueries;
use podcasts_data::Show;
use crate::app::Action;
use crate::utils::{self, lazy_load};
use crate::widgets::{BaseView, EmptyShow, EpisodeWidget, ShowMenu};
use std::ops::Deref;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub(crate) struct ShowWidget {
pub(crate) view: BaseView,
cover: gtk::Image,
description: gtk::Label,
description_short: gtk::Label,
description_stack: gtk::Stack,
description_button: gtk::Button,
description_button_revealer: gtk::Revealer,
episodes: gtk::ListBox,
show_id: Option<i32>,
}
impl Default for ShowWidget {
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/Podcasts/gtk/show_widget.ui");
let sub_cont: gtk::Box = builder.get_object("sub_container").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
let description: gtk::Label = builder.get_object("description").unwrap();
let description_short: gtk::Label = builder.get_object("description_short").unwrap();
let description_stack: gtk::Stack = builder.get_object("description_stack").unwrap();
let description_button: gtk::Button = builder.get_object("description_button").unwrap();
let description_button_revealer =
builder.get_object("description_button_revealer").unwrap();
let episodes = builder.get_object("episodes").unwrap();
let view = BaseView::default();
let column = Column::new();
column.set_maximum_width(700);
// For some reason the Column is not seen as a gtk::container
// and therefore we can't call add() without the cast
let column = column.upcast::<gtk::Widget>();
let column = column.downcast::<gtk::Container>().unwrap();
column.add(&sub_cont);
view.add(&column);
column.show_all();
ShowWidget {
view,
cover,
description,
description_short,
description_stack,
description_button,
description_button_revealer,
episodes,
show_id: None,
}
}
}
impl ShowWidget {
pub(crate) fn new(
pd: Arc<Show>,
sender: Sender<Action>,
vadj: Option<Adjustment>,
) -> Rc<ShowWidget> {
let mut pdw = ShowWidget::default();
pdw.init(&pd);
let menu = ShowMenu::new(&pd, &pdw.episodes, &sender);
sender
.send(Action::InitShowMenu(Fragile::new(menu)))
.expect("Action channel blew up somehow");
let pdw = Rc::new(pdw);
let res = populate_listbox(&pdw, pd.clone(), sender, vadj);
debug_assert!(res.is_ok());
let weak = Rc::downgrade(&pdw);
pdw.description_short
.connect_size_allocate(clone!(weak => move |_, _2| {
weak.upgrade().map(|w| w.update_read_more());
}));
pdw.description_button
.connect_clicked(clone!(weak => move |_| {
weak.upgrade()
.map(|w| w.description_stack.set_visible_child_name("full"));
}));
pdw
}
pub(crate) fn init(&mut self, pd: &Arc<Show>) {
self.set_description(pd.description());
self.show_id = Some(pd.id());
let res = self.set_cover(&pd);
debug_assert!(res.is_ok());
}
/// Set the show cover.
fn set_cover(&self, pd: &Arc<Show>) -> Result<(), Error> {
utils::set_image_from_path(&self.cover, pd.id(), 256)
}
fn update_read_more(&self) {
if let Some(layout) = self.description_short.get_layout() {
let more = layout.is_ellipsized()
|| self.description.get_label() != self.description_short.get_label();
self.description_button_revealer.set_reveal_child(more);
}
}
/// Set the description text.
fn set_description(&self, text: &str) {
let markup = html2text::from_read(text.as_bytes(), text.as_bytes().len());
let markup = markup.trim();
let lines: Vec<&str> = markup.lines().collect();
if markup.is_empty() {
self.description_stack.set_visible(false);
} else {
self.description_stack.set_visible(true);
self.description.set_markup(markup);
debug_assert!(lines.len() > 0);
if lines.len() > 0 {
self.description_short.set_markup(lines[0]);
}
}
}
pub(crate) fn show_id(&self) -> Option<i32> {
self.show_id
}
}
/// Populate the listbox with the shows episodes.
fn populate_listbox(
show: &Rc<ShowWidget>,
pd: Arc<Show>,
sender: Sender<Action>,
vadj: Option<Adjustment>,
) -> Result<(), Error> {
use crossbeam_channel::TryRecvError;
let count = dbqueries::get_pd_episodes_count(&pd)?;
let (sender_, receiver) = bounded(1);
rayon::spawn(clone!(pd => move || {
if let Ok(episodes) = dbqueries::get_pd_episodeswidgets(&pd) {
// The receiver can be dropped if there's an early return
// like on show without episodes for example.
let _ = sender_.send(episodes);
}
}));
if count == 0 {
let empty = EmptyShow::default();
show.episodes.add(empty.deref());
return Ok(());
}
let show_weak = Rc::downgrade(&show);
let list_weak = show.episodes.downgrade();
gtk::idle_add(move || {
let episodes = match receiver.try_recv() {
Ok(e) => e,
Err(TryRecvError::Empty) => return glib::Continue(true),
Err(TryRecvError::Disconnected) => return glib::Continue(false),
};
debug_assert!(episodes.len() as i64 == count);
let constructor = clone!(sender => move |ep| {
EpisodeWidget::new(ep, &sender).container.clone()
});
let callback = clone!(show_weak, vadj => move || {
match (show_weak.upgrade(), &vadj) {
(Some(ref shows), Some(ref v)) => shows.view.set_adjutments(None, Some(v)),
_ => (),
};
});
lazy_load(episodes, list_weak.clone(), constructor, callback);
glib::Continue(false)
});
Ok(())
}