zoodex/src/ui/collatable_container/collated_grid.rs

183 lines
5.8 KiB
Rust

use std::cell::RefCell;
use std::env::var_os;
use std::iter::zip;
use gtk4::gdk::Texture;
use gtk4::gio::{IOErrorEnum, spawn_blocking};
use gtk4::glib::clone;
use gtk4::pango::{SCALE_LARGE, Weight};
use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt};
use gtk4::{Align, Button, FlowBox, Image, Justification, Label, Orientation, SelectionMode};
use crate::data_manager::MediaOverview;
use crate::ui::collatable_container::MediaAdapter;
use crate::ui::component::Component;
use crate::ui::utility::{OptChildExt, pango_attributes, view_expr};
use crate::utility::{concat_os_str, leak};
pub struct CollatedMediaGrid<A: MediaAdapter> {
media_widget_pairs: RefCell<Vec<(A::Overview, Button)>>,
grid_widget: FlowBox,
on_media_selected: &'static dyn Fn(A::Overview),
}
impl<A: MediaAdapter> CollatedMediaGrid<A> {
pub fn new(on_media_selected: impl Fn(A::Overview) + 'static) -> Self {
let grid_widget = view_expr! {
FlowBox {
set_homogeneous: true,
set_selection_mode: SelectionMode::None,
set_css_classes: &["collatable-container"],
set_orientation: Orientation::Horizontal,
}
};
let media_widget_pairs = RefCell::new(Vec::new());
let on_media_selected = leak(on_media_selected);
Self {
media_widget_pairs,
grid_widget,
on_media_selected,
}
}
pub async fn set_media(&self, media: Vec<A::Overview>, sorting: A::Sorting) {
// TODO: Check if we should use `MainContext :: invoke_local` here
let mut widgets = Vec::new();
for media in media.as_slice() {
widgets.push(self.create_media_entry(media).await);
}
self
.media_widget_pairs
.replace(zip(media, widgets).collect());
for (_, widget) in self.sort_media_widget_pairs(sorting) {
self.grid_widget.append(&widget);
}
}
async fn create_media_entry(&self, media: &A::Overview) -> Button {
view_expr! {
Button {
set_css_classes: &["flat", "collection-item-button"],
connect_clicked: clone!(
#[strong] media,
#[strong(rename_to = on_media_selected)] self.on_media_selected,
move |_| on_media_selected(media.clone()),
),
set_child: Some(&view_expr! {
gtk4::Box {
set_css_classes: &["collection-item-box"],
set_valign: Align::Center,
set_orientation: Orientation::Vertical,
// Poster
append_opt: &{
let home_directory = var_os("HOME").unwrap();
let xdg_data_home = var_os("XDG_DATA_HOME");
let data_dir = match xdg_data_home {
Some(xdg_data_home) => concat_os_str!(xdg_data_home, "/zoodex"),
None => concat_os_str!(home_directory, "/.local/share/zoodex"),
};
let poster_file_path = concat_os_str!(data_dir, "/posters/", media.get_uuid());
let poster_texture = spawn_blocking(move || Texture::from_filename(poster_file_path))
.await
.unwrap();
match poster_texture {
Ok(poster_texture) => Some(view_expr! {
Image {
set_paintable: Some(&poster_texture),
set_pixel_size: 300,
set_css_classes: &["collection-item-image"],
}
}),
Err(error) => {
if error.matches(IOErrorEnum::NotFound) {
// The file not existing simply means there is no poster for this piece of media
None
} else {
// Any other error means something unexpected went wrong
panic!("{}", error)
}
},
}
},
// Name
append: &view_expr! {
Label {
set_attributes: Some(&pango_attributes!(scale: SCALE_LARGE, weight: Weight::Bold)),
set_justify: Justification::Center,
// Not the actual limit, used instead to wrap more aggressively
set_max_width_chars: 1,
set_wrap: true,
set_label: media.get_name().as_str(),
}
},
// Original name
append_opt: &media.get_original_name().map(|original_name| view_expr! {
Label {
set_justify: Justification::Center,
set_max_width_chars: 1,
set_wrap: true,
set_label: original_name.as_str(),
}
}),
// Details
append: &view_expr! {
gtk4::Box {
set_spacing: 20,
set_halign: Align::Center,
set_orientation: Orientation::Horizontal,
// Release date
append: &view_expr! {
Label { set_label: media.get_release_date().split('-').next().unwrap() }
},
// Runtime
append_opt: &media.get_runtime_minutes().map(|runtime_minutes| view_expr! {
Label { set_label: format!("{}m", runtime_minutes).as_str() }
}),
}
},
}
}),
}
}
}
pub fn set_sorting(&self, sorting: A::Sorting) {
self.grid_widget.remove_all();
for (_, widget) in self.sort_media_widget_pairs(sorting) {
self.grid_widget.append(&widget);
}
}
fn sort_media_widget_pairs(&self, sorting: A::Sorting) -> Vec<(A::Overview, Button)> {
let mut sorted = Vec::from(self.media_widget_pairs.borrow().as_slice());
sorted.sort_by(|(media_1, _), (media_2, _)| A::compare_by(media_1, media_2, sorting));
// See it, say it, ...
sorted
}
}
impl<A: MediaAdapter> Component for CollatedMediaGrid<A> {
fn get_widget(&self) -> &FlowBox {
&self.grid_widget
}
}