183 lines
5.8 KiB
Rust
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
|
|
}
|
|
}
|