diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 54e65d9..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*.rs] -charset = utf-8 -indent_style = tab -tab_width = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/Cargo.toml b/Cargo.toml index f12ced3..0872c82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ -[ package ] +[package] name = "zoodex" version = "1.0.0" -authors = [ "Reinout Meliesie " ] +authors = ["Reinout Meliesie "] edition = "2024" rust-version = "1.89.0" license = "GPL-3.0-or-later" -[ profile . release ] +[profile.release] lto = true -[ dependencies ] -async-sqlite = { version = "0.5.3" , default-features = false } +[dependencies] +async-sqlite = { version = "0.5.3", default-features = false } fallible-iterator = "0.3.0" # Must match version used by async-sqlite futures = "0.3.31" -gtk4 = { version = "0.10.3" , features = [ "v4_20" ] } -libadwaita = { version = "0.8.1" , features = [ "v1_8" ] } -relm4-macros = { version = "0.10.1" , default-features = false } +gtk4 = { version = "0.10.3", features = ["v4_20"] } +libadwaita = { version = "0.8.1", features = ["v1_8"] } +relm4-macros = { version = "0.10.1", default-features = false } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..ccb6fd2 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,18 @@ +edition = "2024" +style_edition = "2024" + +# Modifications to the standard style +blank_lines_upper_bound = 3 +tab_spaces = 2 +use_field_init_shorthand = true + +# Stricter subset of standard style +condense_wildcard_suffixes = true +format_macro_matchers = true +group_imports = "StdExternalCrate" +hex_literal_case = "Upper" +imports_granularity = "Module" +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +wrap_comments = true diff --git a/src/application.css b/src/application.css index 3909e08..1f47be8 100644 --- a/src/application.css +++ b/src/application.css @@ -1,29 +1,29 @@ /* TODO: Switch out CSS dynamically on `gtk-application-prefer-dark-theme` property change */ .collation-menu row:selected { - background-color : rgb( 0 0 0 / 0.08 ) ; + background-color: rgb(0 0 0 / 0.08); } .collation-menu row:not(:selected) image { - opacity : 0 ; + opacity: 0; } .collatable-container flowboxchild { - padding : 0 ; + padding: 0; } .collection-item-button { - font-weight : normal ; /* No bold text by default for this kind of button */ + font-weight: normal; /* No bold text by default for this kind of button */ } .collection-item-box { - margin-top : 20px ; - margin-bottom : 20px ; + margin-top: 20px; + margin-bottom: 20px; } .collection-item-image { - margin-bottom : 20px ; + margin-bottom: 20px; } .media-modal { - padding : 100px ; + padding: 100px; } diff --git a/src/data_manager.rs b/src/data_manager.rs index cec9efd..8eead16 100644 --- a/src/data_manager.rs +++ b/src/data_manager.rs @@ -1,255 +1,287 @@ -use async_sqlite :: * ; -use async_sqlite :: Error :: * ; -use async_sqlite :: rusqlite :: OpenFlags ; -use async_sqlite :: rusqlite :: Row ; -use async_sqlite :: rusqlite :: Error :: * ; -use async_sqlite :: rusqlite :: ffi :: ErrorCode :: * ; -use fallible_iterator :: * ; -use std :: env :: * ; -use std :: path :: * ; +use std::env::var_os; +use std::path::PathBuf; -use crate :: error :: * ; -use crate :: error :: ZoodexError :: * ; -use crate :: utility :: * ; +use async_sqlite::rusqlite::{OpenFlags, Row}; +use async_sqlite::{Client, ClientBuilder, rusqlite}; +use fallible_iterator::FallibleIterator; + +use crate::error::{Result, ZoodexError}; +use crate::utility::concat_os_str; pub struct DataManager { - sqlite_client_local : Client , - sqlite_client_shared : Client , + sqlite_client_local: Client, + sqlite_client_shared: Client, } impl DataManager { - pub async fn new () -> Result { - let home_directory = var_os ("HOME") . unwrap () ; - let xdg_data_home = var_os ("XDG_DATA_HOME") ; + pub async fn new() -> Result { + 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 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 sqlite_client_shared = ClientBuilder :: new () - . path ( concat_os_str ! ( & data_dir , "/shared.sqlite" ) ) - . flags ( OpenFlags :: SQLITE_OPEN_READ_ONLY | OpenFlags :: SQLITE_OPEN_NO_MUTEX ) - . open () - . await ? ; + let sqlite_client_shared = ClientBuilder::new() + .path(concat_os_str!(&data_dir, "/shared.sqlite")) + .flags(OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX) + .open() + .await?; - let sqlite_client_local = ClientBuilder :: new () - . path ( concat_os_str ! ( & data_dir , "/shared.sqlite" ) ) - . flags ( OpenFlags :: SQLITE_OPEN_READ_WRITE | OpenFlags :: SQLITE_OPEN_NO_MUTEX ) - . open () - . await ? ; + let sqlite_client_local = ClientBuilder::new() + .path(concat_os_str!(&data_dir, "/shared.sqlite")) + .flags(OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX) + .open() + .await?; - Ok ( Self { - sqlite_client_local , - sqlite_client_shared , - } ) - } + Ok(Self { + sqlite_client_local, + sqlite_client_shared, + }) + } - pub async fn get_collection_overview ( & self ) -> Result { - let collection_overview = self . sqlite_client_shared . conn ( |sqlite_connection| { - let films = sqlite_connection - . prepare ( " - select uuid , name , original_name , release_date , runtime_minutes - from films - " ) ? - . query (()) ? - . map (row_to_film_overview) - . collect () ? ; + pub async fn get_collection_overview(&self) -> Result { + let collection_overview = self + .sqlite_client_shared + .conn(|sqlite_connection| { + let films = sqlite_connection + .prepare( + " + select uuid , name , original_name , release_date , runtime_minutes + from films + ", + )? + .query(())? + .map(row_to_film_overview) + .collect()?; - let series = sqlite_connection - . prepare ( " - select series . uuid , series . name , series . original_name , - min ( episodes . release_date ) - from series , seasons , episodes - where series . uuid = seasons . series and seasons . uuid = episodes . season - group by series . uuid - " ) ? - . query (()) ? - . map (row_to_series_overview) - . collect () ? ; + let series = sqlite_connection + .prepare( + " + select series . uuid , series . name , series . original_name , + min ( episodes . release_date ) + from series , seasons , episodes + where series . uuid = seasons . series and seasons . uuid = episodes . season + group by series . uuid + ", + )? + .query(())? + .map(row_to_series_overview) + .collect()?; - Ok ( CollectionOverview { films , series } ) - } ) . await ? ; - Ok (collection_overview) - } + Ok(CollectionOverview { films, series }) + }) + .await?; + Ok(collection_overview) + } - pub async fn get_film_details ( & self , uuid : String ) -> Result { - let film_details = self . sqlite_client_shared . conn ( |sqlite_connection| { - let film_details = sqlite_connection - . prepare ( " - select - films . uuid , - films . name , - films . original_name , - films . release_date , - films . runtime_minutes , - sources . media_uuid , - sources . bittorrent_hash , - sources . file_path , - sources . audio_track , - sources . subtitle_track - from films left join sources - on films . uuid = sources . media_uuid - where films . uuid = (?1) - " ) ? - . query ( [ uuid ] ) ? - . map (row_to_film_details) - . next () ? - . unwrap () ; - Ok (film_details) - } ) . await ? ; - Ok (film_details) - } + pub async fn get_film_details(&self, uuid: String) -> Result { + let film_details = self + .sqlite_client_shared + .conn(|sqlite_connection| { + let film_details = sqlite_connection + .prepare( + " + select + films . uuid , + films . name , + films . original_name , + films . release_date , + films . runtime_minutes , + sources . media_uuid , + sources . bittorrent_hash , + sources . file_path , + sources . audio_track , + sources . subtitle_track + from films left join sources + on films . uuid = sources . media_uuid + where films . uuid = (?1) + ", + )? + .query([uuid])? + .map(row_to_film_details) + .next()? + .unwrap(); + Ok(film_details) + }) + .await?; + Ok(film_details) + } } pub struct CollectionOverview { - pub films : Vec , - pub series : Vec , + pub films: Vec, + pub series: Vec, } -pub trait MediaOverview : Clone { - fn get_uuid ( & self ) -> String ; - fn get_name ( & self ) -> String ; - fn get_original_name ( & self ) -> Option ; - fn get_release_date ( & self ) -> String ; - fn get_runtime_minutes ( & self ) -> Option ; +pub trait MediaOverview: Clone { + fn get_uuid(&self) -> String; + fn get_name(&self) -> String; + fn get_original_name(&self) -> Option; + fn get_release_date(&self) -> String; + fn get_runtime_minutes(&self) -> Option; } -# [ derive (Clone) ] pub struct FilmOverview { - pub uuid : String , - pub name : String , - pub original_name : Option , - pub release_date : String , // TODO: Switch to chrono types, I think rusqlite has crate option for it - pub runtime_minutes : u32 , +#[derive(Clone)] +pub struct FilmOverview { + pub uuid: String, + pub name: String, + pub original_name: Option, + // TODO: Switch to chrono types, I think rusqlite has crate option for it + pub release_date: String, + pub runtime_minutes: u32, } -# [ derive (Clone) ] pub struct SeriesOverview { - pub uuid : String , - pub name : String , - pub original_name : Option , - pub first_release_date : String , // TODO: Switch to chrono types, I think rusqlite has crate option for it +#[derive(Clone)] +pub struct SeriesOverview { + pub uuid: String, + pub name: String, + pub original_name: Option, + // TODO: Switch to chrono types, I think rusqlite has crate option for it + pub first_release_date: String, } impl MediaOverview for FilmOverview { - fn get_uuid ( & self ) -> String { self . uuid . clone () } - fn get_name ( & self ) -> String { self . name . clone () } - fn get_original_name ( & self ) -> Option { self . original_name . clone () } - fn get_release_date ( & self ) -> String { self . release_date . clone () } - fn get_runtime_minutes ( & self ) -> Option { Some ( self . runtime_minutes ) } + fn get_uuid(&self) -> String { + self.uuid.clone() + } + fn get_name(&self) -> String { + self.name.clone() + } + fn get_original_name(&self) -> Option { + self.original_name.clone() + } + fn get_release_date(&self) -> String { + self.release_date.clone() + } + fn get_runtime_minutes(&self) -> Option { + Some(self.runtime_minutes) + } } impl MediaOverview for SeriesOverview { - fn get_uuid ( & self ) -> String { self . uuid . clone () } - fn get_name ( & self ) -> String { self . name . clone () } - fn get_original_name ( & self ) -> Option { self . original_name . clone () } - fn get_release_date ( & self ) -> String { self . first_release_date . clone () } - fn get_runtime_minutes ( & self ) -> Option { None } + fn get_uuid(&self) -> String { + self.uuid.clone() + } + fn get_name(&self) -> String { + self.name.clone() + } + fn get_original_name(&self) -> Option { + self.original_name.clone() + } + fn get_release_date(&self) -> String { + self.first_release_date.clone() + } + fn get_runtime_minutes(&self) -> Option { + None + } } -fn row_to_film_overview ( row : & Row ) -> rusqlite :: Result { - let uuid = row . get (0) ? ; - let name = row . get (1) ? ; - let original_name = row . get (2) ? ; - let release_date = row . get (3) ? ; - let runtime_minutes = row . get (4) ? ; +fn row_to_film_overview(row: &Row) -> rusqlite::Result { + let uuid = row.get(0)?; + let name = row.get(1)?; + let original_name = row.get(2)?; + let release_date = row.get(3)?; + let runtime_minutes = row.get(4)?; - Ok ( FilmOverview { - uuid , - name , - original_name , - release_date , - runtime_minutes , - } ) + Ok(FilmOverview { + uuid, + name, + original_name, + release_date, + runtime_minutes, + }) } -fn row_to_series_overview ( row : & Row ) -> rusqlite :: Result { - let uuid = row . get (0) ? ; - let name = row . get (1) ? ; - let original_name = row . get (2) ? ; - let first_release_date = row . get (3) ? ; +fn row_to_series_overview(row: &Row) -> rusqlite::Result { + let uuid = row.get(0)?; + let name = row.get(1)?; + let original_name = row.get(2)?; + let first_release_date = row.get(3)?; - Ok ( SeriesOverview { - uuid , - name , - original_name , - first_release_date , - } ) + Ok(SeriesOverview { + uuid, + name, + original_name, + first_release_date, + }) } -# [ derive (Clone) ] pub struct FilmDetails { - pub uuid : String , - pub name : String , - pub original_name : Option , - pub release_date : String , - pub runtime_minutes : u32 , - pub source : Option , +#[derive(Clone)] +pub struct FilmDetails { + pub uuid: String, + pub name: String, + pub original_name: Option, + pub release_date: String, + pub runtime_minutes: u32, + pub source: Option, } -# [ derive (Clone) ] pub struct SourceDetails { - pub bittorrent_hash : String , - pub file_path : PathBuf , - pub audio_track : Option , - pub subtitle_track : Option , +#[derive(Clone)] +pub struct SourceDetails { + pub bittorrent_hash: String, + pub file_path: PathBuf, + pub audio_track: Option, + pub subtitle_track: Option, } -fn row_to_film_details ( row : & Row ) -> rusqlite :: Result { - let uuid = row . get (0) ? ; - let name = row . get (1) ? ; - let original_name = row . get (2) ? ; - let release_date = row . get (3) ? ; - let runtime_minutes = row . get (4) ? ; +fn row_to_film_details(row: &Row) -> rusqlite::Result { + let uuid = row.get(0)?; + let name = row.get(1)?; + let original_name = row.get(2)?; + let release_date = row.get(3)?; + let runtime_minutes = row.get(4)?; - let source_media_uuid = row . get :: < _ , Option > (5) ? ; - let source = match source_media_uuid { - Some (_) => { - let bittorrent_hash = row . get (6) ? ; - let file_path = PathBuf :: from ( row . get :: < _ , String > (7) ? ) ; - let audio_track = row . get (8) ? ; - let subtitle_track = row . get (9) ? ; + let source_media_uuid = row.get::<_, Option>(5)?; + let source = match source_media_uuid { + Some(_) => { + let bittorrent_hash = row.get(6)?; + let file_path = PathBuf::from(row.get::<_, String>(7)?); + let audio_track = row.get(8)?; + let subtitle_track = row.get(9)?; - Some ( SourceDetails { - bittorrent_hash , - file_path , - audio_track , - subtitle_track , - } ) - } , - None => None , - } ; + Some(SourceDetails { + bittorrent_hash, + file_path, + audio_track, + subtitle_track, + }) + } + None => None, + }; - Ok ( FilmDetails { - uuid , - name , - original_name , - release_date , - runtime_minutes , - source , - } ) + Ok(FilmDetails { + uuid, + name, + original_name, + release_date, + runtime_minutes, + source, + }) } -impl From for ZoodexError { - fn from ( error : Error ) -> Self { - match error { - Rusqlite (error) => ZoodexError :: from (error) , - _ => panic ! ( "{}" , error ) , - } - } +impl From for ZoodexError { + fn from(error: async_sqlite::Error) -> Self { + match error { + async_sqlite::Error::Rusqlite(error) => ZoodexError::from(error), + _ => panic!("{}", error), + } + } } -impl From < rusqlite :: Error > for ZoodexError { - fn from ( error : rusqlite :: Error ) -> Self { - match error { - SqliteFailure ( error , _ ) => { - match error . code { - CannotOpen => CollectionFileReadError , - _ => panic ! ( "{}" , error ) , - } - } , - _ => panic ! ( "{}" , error ) , - } - } +impl From for ZoodexError { + fn from(error: rusqlite::Error) -> Self { + match error { + rusqlite::Error::SqliteFailure(error, _) => match error.code { + rusqlite::ffi::ErrorCode::CannotOpen => ZoodexError::CollectionFileReadError, + _ => panic!("{}", error), + }, + _ => panic!("{}", error), + } + } } diff --git a/src/error.rs b/src/error.rs index 7318a47..8f43c4d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,33 +1,35 @@ -use std :: * ; -use std :: any :: * ; +use std::any::Any; +use std::result; -# [ derive (Debug) ] pub enum ZoodexError { - CollectionFileReadError , +#[derive(Debug)] +pub enum ZoodexError { + CollectionFileReadError, } -pub type Result = result :: Result < Success , ZoodexError > ; +pub type Result = result::Result; -impl From < Box < dyn Any + Send > > for ZoodexError { - fn from ( error : Box < dyn Any + Send > ) -> Self { - * error . downcast () . unwrap () - } +impl From> for ZoodexError { + fn from(error: Box) -> Self { + *error.downcast().unwrap() + } } -macro_rules ! async_result_context { ( - $ future : expr - $ ( , ok => $ on_success : expr ) ? - $ ( , err => $ on_failure : expr ) ? $ (,) ? +macro_rules! async_result_context {( + $future: expr + $(, ok => $on_success: expr)? + $(, err => $on_failure: expr)?$(,)? ) => { - # [ allow (unreachable_patterns) ] match $ future . await { - $ ( Ok (value) => $ on_success (value) , ) ? - Ok (_) => {} , - $ ( Err (error) => $ on_failure (error) , ) ? - Err (_) => {} , - } -} } + #[allow(unreachable_patterns)] + match $future.await { + $(Ok(value) => $on_success(value),)? + Ok(_) => {}, + $(Err(error) => $on_failure(error),)? + Err(_) => {}, + } +}} -pub (crate) use async_result_context ; +pub(crate) use async_result_context; diff --git a/src/main.rs b/src/main.rs index 417e095..09b6bf0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,69 @@ -mod error ; -mod data_manager ; -mod ui ; -mod utility ; +mod data_manager; +mod error; +mod ui; +mod utility; -use gtk4 :: CssProvider ; -use gtk4 :: style_context_add_provider_for_display ; -use gtk4 :: STYLE_PROVIDER_PRIORITY_APPLICATION ; -use gtk4 :: gdk :: * ; -use gtk4 :: glib :: * ; -use libadwaita :: Application ; -use libadwaita :: prelude :: * ; +use gtk4::gdk::Display; +use gtk4::glib::{ExitCode, spawn_future_local}; +use gtk4::prelude::{ApplicationExt, ApplicationExtManual}; +use gtk4::{ + CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, style_context_add_provider_for_display, +}; +use libadwaita::Application; -use crate :: data_manager :: * ; -use crate :: error :: * ; -use crate :: error :: ZoodexError :: * ; -use crate :: ui :: * ; -use crate :: utility :: * ; +use crate::data_manager::DataManager; +use crate::error::{ZoodexError, async_result_context}; +use crate::ui::{UI, Window}; +use crate::utility::leak; -fn main () -> ExitCode { - let application = Application :: builder () - . application_id ("com.kernelmaft.zoodex") - . build () ; - application . connect_startup (add_style_provider) ; - application . connect_activate (show_window) ; - application . run () +fn main() -> ExitCode { + let application = Application::builder() + .application_id("com.kernelmaft.zoodex") + .build(); + application.connect_startup(add_style_provider); + application.connect_activate(show_window); + application.run() } -fn add_style_provider ( _ : & Application ) { - let style_provider = CssProvider :: new () ; - style_provider . load_from_string ( include_str ! ("application.css") ) ; - style_context_add_provider_for_display ( - & Display :: default () . unwrap () , - & style_provider , - STYLE_PROVIDER_PRIORITY_APPLICATION , - ) ; +fn add_style_provider(_: &Application) { + let style_provider = CssProvider::new(); + style_provider.load_from_string(include_str!("application.css")); + style_context_add_provider_for_display( + &Display::default().unwrap(), + &style_provider, + STYLE_PROVIDER_PRIORITY_APPLICATION, + ); } -fn show_window ( application : & Application ) { - let window = leak ( Window :: new (application) ) ; +fn show_window(application: &Application) { + let window = leak(Window::new(application)); - spawn_future_local ( async move { - async_result_context ! ( - async { - let data_manager = leak ( DataManager :: new () . await ? ) ; + spawn_future_local(async move { + async_result_context!( + async { + let data_manager = leak(DataManager::new().await?); - let ui = UI :: new ( - window , - async |film_uuid| { - data_manager . get_film_details (film_uuid) . await - . expect ("A film with the given UUID should exist") - } , - ) ; - window . show () ; + let ui = UI::new( + window, + async |film_uuid| { + data_manager.get_film_details(film_uuid).await + .expect("A film with the given UUID should exist") + }, + ); + window.show(); - let collection = data_manager . get_collection_overview () . await ? ; - ui . render_collection_overview (collection) . await ; - Ok (()) - } , - err => |error| { - match error { - CollectionFileReadError => eprintln ! ("Could not read collection file") , - } ; - window . close () ; - } , - ) ; - } ) ; + let collection = data_manager.get_collection_overview().await?; + ui.render_collection_overview(collection).await; + Ok(()) + }, + err => |error| { + match error { + ZoodexError::CollectionFileReadError => eprintln!("Could not read collection file"), + }; + window.close(); + }, + ); + }); } diff --git a/src/ui/collatable_container/collated_grid.rs b/src/ui/collatable_container/collated_grid.rs index d34dd31..367318b 100644 --- a/src/ui/collatable_container/collated_grid.rs +++ b/src/ui/collatable_container/collated_grid.rs @@ -1,175 +1,183 @@ -use gtk4 :: { Button , FlowBox , Image , Justification , Label , SelectionMode } ; -use gtk4 :: Align :: * ; -use gtk4 :: Orientation :: * ; -use gtk4 :: gdk :: * ; -use gtk4 :: gio :: * ; -use gtk4 :: glib :: * ; -use gtk4 :: pango :: * ; -use gtk4 :: pango :: Weight :: * ; -use gtk4 :: prelude :: * ; -use std :: cell :: * ; -use std :: env :: * ; -use std :: iter :: * ; +use std::cell::RefCell; +use std::env::var_os; +use std::iter::zip; -use crate :: ui :: collatable_container :: * ; -use crate :: ui :: component :: * ; +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 ) , +pub struct CollatedMediaGrid { + media_widget_pairs: RefCell>, + grid_widget: FlowBox, + on_media_selected: &'static dyn Fn(A::Overview), } -impl < A : MediaAdapter > 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 : Horizontal , - } - } ; - let media_widget_pairs = RefCell :: new ( Vec :: new () ) ; - let on_media_selected = leak (on_media_selected) ; +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 } - } + 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 + 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 () ) ; + 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 ) ; - } - } + 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" ] , + 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 () ) , - ) , + 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 : Center , - set_orientation : Vertical , + 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") ; + // 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 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_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 () ; + 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 ) { - None // The file not existing simply means there is no poster for this piece of media - } else { - panic ! ( "{}" , error ) // Any other error means something unexpected went wrong - } - } , - } - } , + 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 : Bold ) ) , - set_justify : Justification :: Center , - set_max_width_chars : 1 , // Not the actual limit, used instead to wrap more aggressively - set_wrap : true , - set_label : media . get_name () . as_str () , - } - } , + // 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 () , - } - } ) , + // 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 : Center , - set_orientation : Horizontal , + // 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 () } - } , + // 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 () } - } ) , - } - } , - } - } ) , - } - } - } + // 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 () ; + 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 ) ; - } - } + 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 () , - ) ; + 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 ) , - ) ; + sorted.sort_by(|(media_1, _), (media_2, _)| A::compare_by(media_1, media_2, sorting)); - // See it, say it, ... - sorted - } + // See it, say it, ... + sorted + } } -impl < A : MediaAdapter > Component for CollatedMediaGrid { - fn get_widget ( & self ) -> & FlowBox { & self . grid_widget } +impl Component for CollatedMediaGrid { + fn get_widget(&self) -> &FlowBox { + &self.grid_widget + } } diff --git a/src/ui/collatable_container/collation_menu/mod.rs b/src/ui/collatable_container/collation_menu/mod.rs index abdac94..8a02d7a 100644 --- a/src/ui/collatable_container/collation_menu/mod.rs +++ b/src/ui/collatable_container/collation_menu/mod.rs @@ -1,39 +1,40 @@ -mod sort_button ; +mod sort_button; -use gtk4 :: Box ; -use gtk4 :: Align :: * ; -use gtk4 :: Orientation :: * ; -use gtk4 :: prelude :: * ; -use relm4_macros :: * ; -use std :: ops :: * ; +use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt}; +use gtk4::{Align, Box, Orientation}; +use relm4_macros::view; -use crate :: ui :: component :: * ; -use crate :: ui :: collatable_container :: * ; -use crate :: ui :: collatable_container :: collation_menu :: sort_button :: * ; +use crate::ui::collatable_container::MediaAdapter; +use crate::ui::collatable_container::collation_menu::sort_button::MediaSortButton; +use crate::ui::component::Component; -pub struct MediaCollationMenu { widget : Box } +pub struct MediaCollationMenu { + widget: Box, +} impl MediaCollationMenu { - pub fn new < A : MediaAdapter > ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self { - let sort_button = MediaSortButton :: :: new (on_sort) ; + pub fn new(on_sort: impl Fn(A::Sorting) + 'static) -> Self { + let sort_button = MediaSortButton::::new(on_sort); - view ! { - widget = gtk4 :: Box { - set_spacing : 20 , - set_css_classes : & [ "toolbar" , "collation-menu" ] , - set_halign : Center , - set_orientation : Horizontal , + view! { + widget = gtk4::Box { + set_spacing: 20, + set_css_classes: &["toolbar", "collation-menu"], + set_halign: Align::Center, + set_orientation: Orientation::Horizontal, - append : sort_button . get_widget () , - } , - } + append: sort_button.get_widget(), + }, + } - Self { widget } - } + Self { widget } + } } impl Component for MediaCollationMenu { - fn get_widget ( & self ) -> & Box { & self . widget } + fn get_widget(&self) -> &Box { + &self.widget + } } diff --git a/src/ui/collatable_container/collation_menu/sort_button.rs b/src/ui/collatable_container/collation_menu/sort_button.rs index 3665e79..9cd472c 100644 --- a/src/ui/collatable_container/collation_menu/sort_button.rs +++ b/src/ui/collatable_container/collation_menu/sort_button.rs @@ -1,116 +1,122 @@ -use gtk4 :: { Image , ListBox , Popover } ; -use gtk4 :: Align :: * ; -use libadwaita :: SplitButton ; -use relm4_macros :: * ; -use std :: cell :: * ; +use std::cell::RefCell; -use crate :: utility :: * ; -use crate :: ui :: * ; -use crate :: ui :: utility :: * ; -use crate :: ui :: collatable_container :: SortingDirection :: * ; +use gtk4::prelude::{BoxExt, ListBoxRowExt, OrientableExt, PopoverExt, WidgetExt}; +use gtk4::{Align, Image, Label, ListBox, Orientation, Popover}; +use libadwaita::SplitButton; +use relm4_macros::view; + +use crate::ui::collatable_container::{MediaAdapter, MediaSorting, SortingDirection}; +use crate::ui::component::Component; +use crate::ui::utility::view_expr; +use crate::utility::leak; -pub struct MediaSortButton < A : MediaAdapter > { - widget : SplitButton , - previous_sorting : & 'static RefCell < A :: Sorting > , +pub struct MediaSortButton { + widget: SplitButton, + previous_sorting: &'static RefCell, } -impl < A : MediaAdapter > MediaSortButton { - pub fn new ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self { - let previous_sorting = leak ( RefCell :: new ( A :: Sorting :: default () ) ) ; - let property_descriptions = A :: get_property_descriptions () ; +impl MediaSortButton { + pub fn new(on_sort: impl Fn(A::Sorting) + 'static) -> Self { + let previous_sorting = leak(RefCell::new(A::Sorting::default())); + let property_descriptions = A::get_property_descriptions(); - let sort_icons = { - let mut sort_icons = Vec :: new () ; - for _ in property_descriptions { - sort_icons . push ( view_expr ! { - Image { set_icon_name : Some ( "view-sort-ascending-symbolic" ) } - } ) ; - } - Box :: leak ( sort_icons . into_boxed_slice () ) as & 'static _ - } ; + let sort_icons = { + let mut sort_icons = Vec::new(); + for _ in property_descriptions { + sort_icons.push(view_expr! { + Image { set_icon_name: Some("view-sort-ascending-symbolic") } + }); + } + Box::leak(sort_icons.into_boxed_slice()) as &'static _ + }; - view ! { - list_box = ListBox { - connect_row_activated : move | _ , row | on_media_sort_activated :: ( - row . index () , - previous_sorting , - & on_sort , - sort_icons , - ) , - } , - widget = SplitButton { - set_popover : Some ( & view_expr ! { - Popover { - set_css_classes : & [ "menu" ] , - set_child : Some ( & list_box ) , - } - } ) , - set_child : Some ( & view_expr ! { - Label { set_label : "Sort" } - } ) , - } , - } + view! { + list_box = ListBox { + connect_row_activated: move |_, row| on_media_sort_activated::( + row.index(), + previous_sorting, + &on_sort, + sort_icons, + ), + }, + widget = SplitButton { + set_popover: Some(&view_expr! { + Popover { + set_css_classes: &["menu"], + set_child: Some(&list_box), + } + }), + set_child: Some(&view_expr! { + Label { set_label: "Sort" } + }), + }, + } - for ( index , ( _ , description ) ) in property_descriptions . iter () . enumerate () { - list_box . append ( & view_expr ! { - gtk4 :: Box { - set_spacing : 20 , - set_orientation : Horizontal , - append : & view_expr ! { - Label { - set_halign : Start , - set_hexpand : true , - set_label : description , - } - } , - append : & sort_icons [index] , - } - } ) ; - } + for (index, (_, description)) in property_descriptions.iter().enumerate() { + list_box.append(&view_expr! { + gtk4::Box { + set_spacing: 20, + set_orientation: Orientation::Horizontal, + append: &view_expr! { + Label { + set_halign: Align::Start, + set_hexpand: true, + set_label: description, + } + }, + append: &sort_icons[index], + } + }); + } - Self { widget , previous_sorting } - } + Self { + widget, + previous_sorting, + } + } } -impl < A : MediaAdapter > Component for MediaSortButton { - fn get_widget ( & self ) -> & SplitButton { & self . widget } +impl Component for MediaSortButton { + fn get_widget(&self) -> &SplitButton { + &self.widget + } } -fn on_media_sort_activated < A : MediaAdapter > ( - row : i32 , - previous_sorting_mut : & RefCell < A :: Sorting > , - on_sort : & impl Fn ( A :: Sorting ) , - sort_icons : & [ Image ] , +fn on_media_sort_activated( + row: i32, + previous_sorting_mut: &RefCell, + on_sort: &impl Fn(A::Sorting), + sort_icons: &[Image], ) { - let row = row as usize ; - debug_assert ! ( - row <= A :: get_property_descriptions () . len () , - "Sorting menu has more rows than media adapter has property descriptions" , - ) ; - let ( sorting_property , _ ) = A :: get_property_descriptions () [row] . clone () ; + let row = row as usize; + debug_assert!( + row <= A::get_property_descriptions().len(), + "Sorting menu has more rows than media adapter has property descriptions", + ); + let (sorting_property, _) = A::get_property_descriptions()[row].clone(); - let previous_sorting = * previous_sorting_mut . borrow () ; - if sorting_property == previous_sorting . get_property () { - match previous_sorting . get_direction () { - Ascending => { - let new_sorting = A :: Sorting :: new ( sorting_property , Descending ) ; - previous_sorting_mut . replace (new_sorting) ; - sort_icons [row] . set_icon_name ( Some ("view-sort-descending-symbolic") ) ; - on_sort (new_sorting) ; - } , - Descending => { - let new_sorting = A :: Sorting :: new ( sorting_property , Ascending ) ; - previous_sorting_mut . replace (new_sorting) ; - sort_icons [row] . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ; - on_sort (new_sorting) ; - } , - } - } else { - let new_sorting = A :: Sorting :: new ( sorting_property , Ascending ) ; - previous_sorting_mut . replace (new_sorting) ; - sort_icons [row] . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ; - on_sort (new_sorting) ; - } + let previous_sorting = *previous_sorting_mut.borrow(); + if sorting_property == previous_sorting.get_property() { + match previous_sorting.get_direction() { + SortingDirection::Ascending => { + let new_sorting = A::Sorting::new(sorting_property, SortingDirection::Descending); + previous_sorting_mut.replace(new_sorting); + sort_icons[row].set_icon_name(Some("view-sort-descending-symbolic")); + on_sort(new_sorting); + } + SortingDirection::Descending => { + let new_sorting = A::Sorting::new(sorting_property, SortingDirection::Ascending); + previous_sorting_mut.replace(new_sorting); + sort_icons[row].set_icon_name(Some("view-sort-ascending-symbolic")); + on_sort(new_sorting); + } + } + } else { + let new_sorting = A::Sorting::new(sorting_property, SortingDirection::Ascending); + previous_sorting_mut.replace(new_sorting); + sort_icons[row].set_icon_name(Some("view-sort-ascending-symbolic")); + on_sort(new_sorting); + } } diff --git a/src/ui/collatable_container/mod.rs b/src/ui/collatable_container/mod.rs index 24b9695..7a5a2ae 100644 --- a/src/ui/collatable_container/mod.rs +++ b/src/ui/collatable_container/mod.rs @@ -1,177 +1,202 @@ -mod collated_grid ; -mod collation_menu ; +mod collated_grid; +mod collation_menu; -use gtk4 :: { Box , ScrolledWindow } ; -use gtk4 :: Orientation :: * ; -use gtk4 :: prelude :: * ; -use relm4_macros :: * ; -use std :: cmp :: * ; -use std :: fmt :: * ; +use std::cmp::Ordering; +use std::fmt::Debug; -use crate :: data_manager :: * ; -use crate :: ui :: component :: * ; -use crate :: ui :: utility :: * ; -use crate :: ui :: collatable_container :: collated_grid :: * ; -use crate :: ui :: collatable_container :: collation_menu :: * ; -use crate :: utility :: * ; +use gtk4::prelude::{BoxExt, OrientableExt}; +use gtk4::{Box, Orientation, ScrolledWindow}; +use relm4_macros::view; + +use crate::data_manager::{FilmOverview, MediaOverview, SeriesOverview}; +use crate::ui::collatable_container::collated_grid::CollatedMediaGrid; +use crate::ui::collatable_container::collation_menu::MediaCollationMenu; +use crate::ui::component::Component; +use crate::ui::utility::{vertical_filler, view_expr}; +use crate::utility::leak; -pub trait MediaSorting < P : MediaProperty > : Clone + Copy + Debug + Default { - fn new ( property : P , direction : SortingDirection ) -> Self ; - fn get_property ( & self ) -> P ; - fn get_direction ( & self ) -> SortingDirection ; +pub trait MediaSorting: Clone + Copy + Debug + Default { + fn new(property: P, direction: SortingDirection) -> Self; + fn get_property(&self) -> P; + fn get_direction(&self) -> SortingDirection; } -pub trait MediaProperty : Clone + Copy + Debug + PartialEq {} +pub trait MediaProperty: Clone + Copy + Debug + PartialEq {} -# [ derive ( Clone , Copy , Debug , Default , PartialEq ) ] pub enum FilmProperty { - # [default] Name , ReleaseDate , Runtime , +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum FilmProperty { + #[default] + Name, + ReleaseDate, + Runtime, } -# [ derive ( Clone , Copy , Debug , Default , PartialEq ) ] pub enum SeriesProperty { - # [default] Name , FirstReleaseDate , +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum SeriesProperty { + #[default] + Name, + FirstReleaseDate, } -# [ derive ( Clone , Copy , Debug , Default , PartialEq ) ] pub enum SortingDirection { - # [default] Ascending , Descending , +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum SortingDirection { + #[default] + Ascending, + Descending, } -# [ derive ( Clone , Copy , Debug , Default ) ] pub struct FilmsSorting { - property : FilmProperty , - direction : SortingDirection , +#[derive(Clone, Copy, Debug, Default)] +pub struct FilmsSorting { + property: FilmProperty, + direction: SortingDirection, } -# [ derive ( Clone , Copy , Debug , Default ) ] pub struct SeriesSorting { - property : SeriesProperty , - direction : SortingDirection , +#[derive(Clone, Copy, Debug, Default)] +pub struct SeriesSorting { + property: SeriesProperty, + direction: SortingDirection, } -impl MediaSorting for FilmsSorting { - fn new ( property : FilmProperty , direction : SortingDirection ) -> Self { - Self { property , direction } - } - fn get_property ( & self ) -> FilmProperty { self . property } - fn get_direction ( & self ) -> SortingDirection { self . direction } +impl MediaSorting for FilmsSorting { + fn new(property: FilmProperty, direction: SortingDirection) -> Self { + Self { + property, + direction, + } + } + fn get_property(&self) -> FilmProperty { + self.property + } + fn get_direction(&self) -> SortingDirection { + self.direction + } } -impl MediaSorting for SeriesSorting { - fn new ( property : SeriesProperty , direction : SortingDirection ) -> Self { - Self { property , direction } - } - fn get_property ( & self ) -> SeriesProperty { self . property } - fn get_direction ( & self ) -> SortingDirection { self . direction } +impl MediaSorting for SeriesSorting { + fn new(property: SeriesProperty, direction: SortingDirection) -> Self { + Self { + property, + direction, + } + } + fn get_property(&self) -> SeriesProperty { + self.property + } + fn get_direction(&self) -> SortingDirection { + self.direction + } } impl MediaProperty for FilmProperty {} impl MediaProperty for SeriesProperty {} -pub struct CollatableMediaContainer < A : MediaAdapter > { - collated_grid : & 'static CollatedMediaGrid , - widget : Box , +pub struct CollatableMediaContainer { + collated_grid: &'static CollatedMediaGrid, + widget: Box, } -impl < A : MediaAdapter > CollatableMediaContainer { - pub fn new ( on_media_selected : impl Fn ( A :: Overview ) + 'static ) -> Self { - let collated_grid = leak ( CollatedMediaGrid :: new (on_media_selected) ) ; - let collation_menu = MediaCollationMenu :: new :: ( - |sorting| collated_grid . set_sorting (sorting) , - ) ; +impl CollatableMediaContainer { + pub fn new(on_media_selected: impl Fn(A::Overview) + 'static) -> Self { + let collated_grid = leak(CollatedMediaGrid::new(on_media_selected)); + let collation_menu = MediaCollationMenu::new::(|sorting| collated_grid.set_sorting(sorting)); - view ! { - widget = gtk4 :: Box { - set_orientation : Vertical , - append : collation_menu . get_widget () , - append : & view_expr ! { - ScrolledWindow { - set_propagate_natural_height : true , - set_child : Some ( & vertical_filler ( collated_grid . get_widget () ) ) , - } - } , - } - } + view! { + widget = gtk4::Box { + set_orientation: Orientation::Vertical, + append: collation_menu.get_widget(), + append: &view_expr! { + ScrolledWindow { + set_propagate_natural_height: true, + set_child: Some(&vertical_filler(collated_grid.get_widget())), + } + }, + } + } - Self { collated_grid, widget } - } + Self { + collated_grid, + widget, + } + } - pub async fn set_media ( & self , media : Vec < A :: Overview > ) { - self . collated_grid . set_media ( media , A :: Sorting :: default () ) . await ; - } + pub async fn set_media(&self, media: Vec) { + self + .collated_grid + .set_media(media, A::Sorting::default()) + .await; + } } -pub trait MediaAdapter : 'static { - type Overview : MediaOverview ; - type Sorting : MediaSorting < Self :: Property > ; - type Property : MediaProperty ; - fn compare_by ( - media_1 : & Self :: Overview , - media_2 : & Self :: Overview , - sorting : Self :: Sorting , - ) -> Ordering ; - fn get_property_descriptions () -> & 'static [ ( Self :: Property , & 'static str ) ] ; +pub trait MediaAdapter: 'static { + type Overview: MediaOverview; + type Sorting: MediaSorting; + type Property: MediaProperty; + fn compare_by( + media_1: &Self::Overview, + media_2: &Self::Overview, + sorting: Self::Sorting, + ) -> Ordering; + fn get_property_descriptions() -> &'static [(Self::Property, &'static str)]; } -impl < A : MediaAdapter > Component for CollatableMediaContainer { - fn get_widget ( & self ) -> & Box { & self . widget } +impl Component for CollatableMediaContainer { + fn get_widget(&self) -> &Box { + &self.widget + } } pub struct FilmsAdapter {} pub struct SeriesAdapter {} impl MediaAdapter for FilmsAdapter { - type Overview = FilmOverview ; - type Sorting = FilmsSorting ; - type Property = FilmProperty ; + type Overview = FilmOverview; + type Sorting = FilmsSorting; + type Property = FilmProperty; - fn compare_by ( - film_1 : & FilmOverview , - film_2 : & FilmOverview , - sorting : FilmsSorting , - ) -> Ordering { - let ordering = match sorting . property { - FilmProperty :: Name => - film_1 . name . cmp ( & film_2 . name ) , - FilmProperty :: ReleaseDate => - film_1 . release_date . cmp ( & film_2 . release_date ) , - FilmProperty :: Runtime => - film_1 . runtime_minutes . cmp ( & film_2 . runtime_minutes ) , - } ; - match sorting . direction { - SortingDirection :: Ascending => ordering , - SortingDirection :: Descending => ordering . reverse () , - } - } + fn compare_by(film_1: &FilmOverview, film_2: &FilmOverview, sorting: FilmsSorting) -> Ordering { + let ordering = match sorting.property { + FilmProperty::Name => film_1.name.cmp(&film_2.name), + FilmProperty::ReleaseDate => film_1.release_date.cmp(&film_2.release_date), + FilmProperty::Runtime => film_1.runtime_minutes.cmp(&film_2.runtime_minutes), + }; + match sorting.direction { + SortingDirection::Ascending => ordering, + SortingDirection::Descending => ordering.reverse(), + } + } - fn get_property_descriptions () -> & 'static [ ( FilmProperty , & 'static str ) ] { - leak ( [ - ( FilmProperty :: Name , "Name" ) , - ( FilmProperty :: ReleaseDate , "Release date" ) , - ( FilmProperty :: Runtime , "Runtime" ) , - ] ) - } + fn get_property_descriptions() -> &'static [(FilmProperty, &'static str)] { + leak([ + (FilmProperty::Name, "Name"), + (FilmProperty::ReleaseDate, "Release date"), + (FilmProperty::Runtime, "Runtime"), + ]) + } } impl MediaAdapter for SeriesAdapter { - type Overview = SeriesOverview ; - type Sorting = SeriesSorting ; - type Property = SeriesProperty ; + type Overview = SeriesOverview; + type Sorting = SeriesSorting; + type Property = SeriesProperty; - fn compare_by ( - series_1 : & SeriesOverview , - series_2 : & SeriesOverview , - sorting : SeriesSorting , - ) -> Ordering { - let ordering = match sorting . property { - SeriesProperty :: Name => - series_1 . name . cmp ( & series_2 . name ) , - SeriesProperty :: FirstReleaseDate => - series_1 . first_release_date . cmp ( & series_2 . first_release_date ) , - } ; - match sorting . direction { - SortingDirection :: Ascending => ordering , - SortingDirection :: Descending => ordering . reverse () , - } - } + fn compare_by( + series_1: &SeriesOverview, + series_2: &SeriesOverview, + sorting: SeriesSorting, + ) -> Ordering { + let ordering = match sorting.property { + SeriesProperty::Name => series_1.name.cmp(&series_2.name), + SeriesProperty::FirstReleaseDate => series_1 + .first_release_date + .cmp(&series_2.first_release_date), + }; + match sorting.direction { + SortingDirection::Ascending => ordering, + SortingDirection::Descending => ordering.reverse(), + } + } - fn get_property_descriptions () -> & 'static [ ( SeriesProperty , & 'static str ) ] { - leak ( [ - ( SeriesProperty :: Name , "Name" ) , - ( SeriesProperty :: FirstReleaseDate , "First release date" ) , - ] ) - } + fn get_property_descriptions() -> &'static [(SeriesProperty, &'static str)] { + leak([ + (SeriesProperty::Name, "Name"), + (SeriesProperty::FirstReleaseDate, "First release date"), + ]) + } } diff --git a/src/ui/component.rs b/src/ui/component.rs index 27c6468..43efe73 100644 --- a/src/ui/component.rs +++ b/src/ui/component.rs @@ -1,8 +1,8 @@ -use gtk4 :: * ; -use gtk4 :: prelude :: * ; +use gtk4::Widget; +use gtk4::prelude::IsA; pub trait Component { - fn get_widget ( & self ) -> & impl IsA ; + fn get_widget(&self) -> &impl IsA; } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3e853b2..76a1937 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,154 +1,163 @@ -mod collatable_container ; -mod component ; -mod utility ; +mod collatable_container; +mod component; +mod utility; -use futures :: * ; -use gtk4 :: { Button , Image , Label } ; -use gtk4 :: Orientation :: * ; -use gtk4 :: glib :: * ; -use gtk4 :: prelude :: * ; -use libadwaita :: { Application , ApplicationWindow , Dialog , HeaderBar , ToolbarView , ViewStack , ViewSwitcher } ; -use libadwaita :: ViewSwitcherPolicy :: * ; -use libadwaita :: prelude :: * ; -use relm4_macros :: * ; -use std :: process :: * ; +use std::process::Command; -use crate :: data_manager :: * ; -use crate :: ui :: collatable_container :: * ; -use crate :: ui :: component :: * ; -use crate :: ui :: utility :: * ; -use crate :: utility :: * ; +use futures::join; +use gtk4::glib::spawn_future_local; +use gtk4::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, WidgetExt}; +use gtk4::{Button, Image, Label, Orientation}; +use libadwaita::prelude::{AdwApplicationWindowExt, AdwDialogExt}; +use libadwaita::{ + Application, ApplicationWindow, Dialog, HeaderBar, ToolbarView, ViewStack, ViewSwitcher, + ViewSwitcherPolicy, +}; +use relm4_macros::view; + +use crate::data_manager::{CollectionOverview, FilmDetails}; +use crate::ui::collatable_container::{CollatableMediaContainer, FilmsAdapter, SeriesAdapter}; +use crate::ui::component::Component; +use crate::ui::utility::{OptChildExt, view_expr}; +use crate::utility::{concat_os_str, leak, to_os_string}; pub struct UI { - films_component : CollatableMediaContainer , - series_component : CollatableMediaContainer , + films_component: CollatableMediaContainer, + series_component: CollatableMediaContainer, } impl UI { - pub fn new ( - window : & 'static Window , - get_film_details : impl AsyncFn (String) -> FilmDetails + 'static , - ) -> UI { - let get_film_details = leak (get_film_details) ; + pub fn new( + window: &'static Window, + get_film_details: impl AsyncFn(String) -> FilmDetails + 'static, + ) -> UI { + let get_film_details = leak(get_film_details); - let films_component = CollatableMediaContainer :: :: new ( |film| { - spawn_future_local ( async { - let film_details = get_film_details ( film . uuid ) . await ; + let films_component = CollatableMediaContainer::::new(|film| { + spawn_future_local(async { + let film_details = get_film_details(film.uuid).await; - view ! { - Dialog { - present : Some ( & window . libadwaita_window ) , - set_child : Some ( & view_expr ! { - gtk4 :: Box { - set_spacing : 40 , - set_css_classes : & [ "media-modal" ] , - set_orientation : Vertical , + view! { + Dialog { + present: Some(&window.libadwaita_window), + set_child: Some(&view_expr! { + gtk4::Box { + set_spacing: 40, + set_css_classes: &["media-modal"], + set_orientation: Orientation::Vertical, - append : & view_expr ! { - Label { - set_css_classes : & [ "title-1" ] , - set_label : film_details . name . as_str () , - } - } , + append: &view_expr! { + Label { + set_css_classes: &["title-1"] , + set_label: film_details.name.as_str(), + } + }, - append_opt : & film_details . original_name . map ( |original_name| view_expr ! { - Label { set_label : original_name . as_str () } - } ) , + append_opt: &film_details.original_name.map(|original_name| view_expr! { + Label { set_label: original_name.as_str() } + }), - append : & view_expr ! { - Label { set_label : & format ! ( "Release date: {}" , film_details . release_date ) } - } , + append: &view_expr! { + Label { set_label: &format!("Release date: {}", film_details.release_date) } + }, - append_opt : & film_details . source . map ( |source| view_expr ! { - Button { - set_css_classes : & [ "suggested-action" , "circular" ] , + append_opt: &film_details.source.map(|source| view_expr! { + Button { + set_css_classes: &["suggested-action", "circular"], - connect_clicked : move |_| { - let arguments = [ - Some ( source . file_path . as_os_str () . to_owned () ) , - source . audio_track . map ( - |audio_track| concat_os_str ! ( "--mpv-aid=" , to_os_string (audio_track) ) , - ) , - source . subtitle_track . map ( - |subtitle_track| concat_os_str ! ( "--mpv-sid=" , to_os_string (subtitle_track) ) , - ) , - ] . iter () . filter_map ( Option :: clone ) . collect :: < Vec <_> > () ; + connect_clicked: move |_| { + let arguments = [ + Some(source.file_path.as_os_str().to_owned()), + source.audio_track.map( + |audio_track| concat_os_str!("--mpv-aid=", to_os_string(audio_track)), + ), + source.subtitle_track.map( + |subtitle_track| concat_os_str!("--mpv-sid=", to_os_string(subtitle_track)), + ), + ].iter().filter_map(Option::clone).collect::>(); - // TODO: Better error handling for UI callbacks in general - Command :: new ("/usr/bin/celluloid") . args (arguments) . spawn () . unwrap () ; - } , + // TODO: Better error handling for UI callbacks in general + Command::new("/usr/bin/celluloid").args(arguments).spawn().unwrap(); + }, - set_child : Some ( & view_expr ! { - Image { set_icon_name : Some ("media-playback-start-symbolic") } - } ) , - } - } ) , - } - } ) , - } - } - } ) ; - } ) ; - let series_component = CollatableMediaContainer :: :: new ( |series| { - view_expr ! { - Dialog { present : Some ( & window . libadwaita_window ) } - } ; - } ) ; + set_child: Some(&view_expr! { + Image { set_icon_name: Some("media-playback-start-symbolic") } + }), + } + }), + } + }), + } + } + }); + }); + let series_component = CollatableMediaContainer::::new(|series| { + view_expr! { + Dialog { present: Some(&window.libadwaita_window) } + }; + }); - view ! { - switched = ViewStack { - add_titled_with_icon : ( films_component . get_widget () , None , "Films" , "camera-video-symbolic" ) , - add_titled_with_icon : ( series_component . get_widget () , None , "Series" , "video-display-symbolic" ) , - } , - header_bar = HeaderBar { - set_title_widget : Some ( & view_expr ! { - ViewSwitcher { - set_policy : Wide , - set_stack : Some ( & switched ) , - } - } ) , - } , - } + view! { + switched = ViewStack { + add_titled_with_icon: (films_component.get_widget(), None, "Films", "camera-video-symbolic"), + add_titled_with_icon: (series_component.get_widget(), None, "Series", "video-display-symbolic"), + }, + header_bar = HeaderBar { + set_title_widget: Some(&view_expr! { + ViewSwitcher { + set_policy: ViewSwitcherPolicy::Wide, + set_stack: Some(&switched), + } + }), + }, + } - window . libadwaita_window . set_content ( Some ( & view_expr ! { - ToolbarView { - add_top_bar : & header_bar , - set_content : Some ( & switched ) , - } - } ) ) ; + window.libadwaita_window.set_content(Some(&view_expr! { + ToolbarView { + add_top_bar: &header_bar, + set_content: Some(&switched), + } + })); - UI { films_component , series_component } - } + UI { + films_component, + series_component, + } + } - pub async fn render_collection_overview ( & self , collection : CollectionOverview ) { - join ! ( - self . films_component . set_media ( collection . films ) , - self . series_component . set_media ( collection . series ) , - ) ; - } + pub async fn render_collection_overview(&self, collection: CollectionOverview) { + join!( + self.films_component.set_media(collection.films), + self.series_component.set_media(collection.series), + ); + } } pub struct Window { - libadwaita_window : ApplicationWindow , + libadwaita_window: ApplicationWindow, } impl Window { - pub fn new (application : & Application ) -> Self { - let libadwaita_window = view_expr ! { - ApplicationWindow { - set_application : Some ( application ) , - set_title : Some ( "Zoödex" ) , - } - } ; + pub fn new(application: &Application) -> Self { + let libadwaita_window = view_expr! { + ApplicationWindow { + set_application: Some(application), + set_title: Some("Zoödex"), + } + }; - Self { libadwaita_window } - } + Self { libadwaita_window } + } - pub fn show ( & self ) { self . libadwaita_window . set_visible (true) } + pub fn show(&self) { + self.libadwaita_window.set_visible(true) + } - pub fn close ( & self ) { self . libadwaita_window . close () } + pub fn close(&self) { + self.libadwaita_window.close() + } } diff --git a/src/ui/utility.rs b/src/ui/utility.rs index ec310b2..8f8bc98 100644 --- a/src/ui/utility.rs +++ b/src/ui/utility.rs @@ -1,66 +1,67 @@ -use gtk4 :: Widget ; -use gtk4 :: Orientation :: * ; -use gtk4 :: prelude :: * ; -use libadwaita :: Bin ; +use gtk4::prelude::{BoxExt, IsA, OrientableExt, WidgetExt}; +use gtk4::{Orientation, Widget}; +use libadwaita::Bin; // Convenience function to conditionally append child to a widget pub trait OptChildExt { - fn append_opt ( & self , child : & Option < impl IsA > ) ; + fn append_opt(&self, child: &Option>); } -impl OptChildExt for gtk4 :: Box { - fn append_opt ( & self , child : & Option < impl IsA > ) { - if let Some (child) = child { - self . append (child) ; - } - } +impl OptChildExt for gtk4::Box { + fn append_opt(&self, child: &Option>) { + if let Some(child) = child { + self.append(child); + } + } } -// The `view` macro from Relm4 as an expression instead of a variable declaration +// The `view` macro from Relm4 as an expression instead of a variable +// declaration -macro_rules ! view_expr { ( $ ( $ contents : tt ) * ) => { { - relm4_macros :: view ! { outer = $ ( $ contents ) * } - outer -} } } +macro_rules! view_expr {( + $($contents: tt)* +) => {{ + relm4_macros::view! { outer = $($contents)* } + outer +}}} -pub fn vertical_filler ( child : & impl IsA ) -> gtk4 :: Box { - view_expr ! { - gtk4 :: Box { - set_orientation : Vertical , - append : child , - append : & view_expr ! { - Bin { set_vexpand : true } - } , - } - } +pub fn vertical_filler(child: &impl IsA) -> gtk4::Box { + view_expr! { + gtk4::Box { + set_orientation: Orientation::Vertical, + append: child, + append: &view_expr! { + Bin { set_vexpand: true } + }, + } + } } -macro_rules ! pango_attributes { ( - $ ( scale : $ scale : expr ) ? - $ ( , weight : $ weight : expr $ (,) ? ) ? -) => { { - let attributes = gtk4 :: pango :: AttrList :: new () ; - # [ allow (unused_mut) ] let mut font_description = gtk4 :: pango :: FontDescription :: new () ; +macro_rules! pango_attributes {( + $(scale: $scale: expr)? + $(, weight: $weight: expr $(,)?)? +) => {{ + let attributes = gtk4::pango::AttrList::new(); + #[allow(unused_mut)] + let mut font_description = gtk4::pango::FontDescription::new(); - $ ( attributes . insert ( gtk4 :: pango :: AttrFloat :: new_scale ( $ scale ) ) ; ) ? - $ ( font_description . set_weight ( $ weight ) ; ) ? + $(attributes.insert(gtk4::pango::AttrFloat::new_scale($scale));)? + $(font_description.set_weight($weight);)? - attributes . insert ( gtk4 :: pango :: AttrFontDesc :: new ( & font_description ) ) ; - attributes -} } } + attributes.insert(gtk4::pango::AttrFontDesc::new(&font_description)); + attributes +}}} -# [ allow (unused_imports) ] pub (crate) use { - pango_attributes , - view_expr , -} ; +#[allow(unused_imports)] +pub(crate) use {pango_attributes, view_expr}; diff --git a/src/utility.rs b/src/utility.rs index a5d7323..0d6443b 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -1,29 +1,29 @@ -use std :: ffi :: * ; -use std :: fmt :: * ; +use std::ffi::OsString; +use std::fmt::Display; -macro_rules ! concat_os_str { ( - $ base : expr , - $ ( $ suffix : expr ) , + -) => { { - let mut base = std :: ffi :: OsString :: from ( $ base ) ; - $ ( base . push ( $ suffix ) ; ) + - base -} } } +macro_rules! concat_os_str {( + $base: expr, $($suffix: expr),+ +) => {{ + let mut base = std :: ffi :: OsString :: from ( $ base ) ; + $ ( base . push ( $ suffix ) ; ) + + base +}}} -pub fn leak < 'l , Type > ( inner : Type ) -> & 'l Type { - Box :: leak ( Box :: new (inner) ) +pub fn leak<'l, Type>(inner: Type) -> &'l Type { + Box::leak(Box::new(inner)) } -pub fn leak_mut < 'l , Type > ( inner : Type ) -> & 'l mut Type { - Box :: leak ( Box :: new (inner) ) +pub fn leak_mut<'l, Type>(inner: Type) -> &'l mut Type { + Box::leak(Box::new(inner)) } -pub fn to_os_string ( value : impl Display + Sized ) -> OsString { - OsString :: from ( ToString :: to_string ( & value ) ) +pub fn to_os_string(value: impl Display + Sized) -> OsString { + OsString::from(ToString::to_string(&value)) } -# [ allow (unused_imports) ] pub (crate) use concat_os_str ; +#[allow(unused_imports)] +pub(crate) use concat_os_str;