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 { media_widget_pairs: RefCell>, grid_widget: FlowBox, on_media_selected: &'static dyn Fn(A::Overview), } impl CollatedMediaGrid { 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, 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 Component for CollatedMediaGrid { fn get_widget(&self) -> &FlowBox { &self.grid_widget } }