Fully deduplicate shared logic between films and series

This commit is contained in:
Reinout Meliesie 2025-02-10 23:56:48 +01:00
parent c3dfa5b459
commit c3e2bd0f69
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
10 changed files with 275 additions and 452 deletions

View file

@ -1,15 +1,9 @@
/* TODO: Switch out CSS dynamically on `gtk-application-prefer-dark-theme` property change */ /* TODO: Switch out CSS dynamically on `gtk-application-prefer-dark-theme` property change */
#film-collation-menu row:selected { .collation-menu row:selected {
background-color : rgb( 0 0 0 / 0.08 ) ;
}
#series-collation-menu row:selected {
background-color : rgb( 0 0 0 / 0.08 ) ; background-color : rgb( 0 0 0 / 0.08 ) ;
} }
#film-collation-menu row:not(:selected) image { .collation-menu row:not(:selected) image {
opacity : 0 ;
}
#series-collation-menu row:not(:selected) image {
opacity : 0 ; opacity : 0 ;
} }

View file

@ -58,6 +58,15 @@ pub struct CollectionOverview {
pub series : Vec <SeriesOverview> , pub series : Vec <SeriesOverview> ,
} }
pub trait MediaOverview : Clone {
fn get_uuid ( & self ) -> String ;
fn get_name ( & self ) -> String ;
fn get_original_name ( & self ) -> Option <String> ;
fn get_release_date ( & self ) -> String ;
fn get_runtime_minutes ( & self ) -> Option <u32> ;
fn get_poster_file_path ( & self ) -> Option <PathBuf> ;
}
# [ derive (Clone) ] pub struct FilmOverview { # [ derive (Clone) ] pub struct FilmOverview {
pub uuid : String , pub uuid : String ,
pub name : String , pub name : String ,
@ -74,6 +83,23 @@ pub struct CollectionOverview {
pub poster_file_path : Option <PathBuf> , pub poster_file_path : Option <PathBuf> ,
} }
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 <String> { self . original_name . clone () }
fn get_release_date ( & self ) -> String { self . release_date . clone () }
fn get_runtime_minutes ( & self ) -> Option <u32> { Some ( self . runtime_minutes ) }
fn get_poster_file_path ( & self ) -> Option <PathBuf> { self . poster_file_path . clone () }
}
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 <String> { self . original_name . clone () }
fn get_release_date ( & self ) -> String { self . first_release_date . clone () }
fn get_runtime_minutes ( & self ) -> Option <u32> { None }
fn get_poster_file_path ( & self ) -> Option <PathBuf> { self . poster_file_path . clone () }
}
fn row_to_film_overview ( row : & Row ) -> rusqlite :: Result <FilmOverview> { fn row_to_film_overview ( row : & Row ) -> rusqlite :: Result <FilmOverview> {
let uuid = row . get (0) ? ; let uuid = row . get (0) ? ;
let name = row . get (1) ? ; let name = row . get (1) ? ;

View file

@ -1,23 +0,0 @@
use libadwaita :: * ;
use libadwaita :: ViewSwitcherPolicy :: * ;
use crate :: ui :: collection_view_stack :: * ;
use crate :: ui :: component :: * ;
use crate :: ui :: utility :: * ;
pub struct ApplicationHeaderBar { widget : HeaderBar }
impl ApplicationHeaderBar {
pub fn new ( collection_view_stack : & CollectionViewStack ) -> Self {
let widget = header_bar ! (
& view_switcher ! ( @ policy : Wide ; collection_view_stack . get_widget () ) ,
) ;
Self { widget }
}
}
impl Component <HeaderBar> for ApplicationHeaderBar {
fn get_widget ( & self ) -> & HeaderBar { & self . widget }
}

View file

@ -8,7 +8,6 @@ use gtk4 :: pango :: Weight :: * ;
use gtk4 :: prelude :: * ; use gtk4 :: prelude :: * ;
use std :: cell :: * ; use std :: cell :: * ;
use std :: iter :: * ; use std :: iter :: * ;
use std :: path :: Path ;
use std :: path :: PathBuf ; use std :: path :: PathBuf ;
use crate :: ui :: collatable_container :: * ; use crate :: ui :: collatable_container :: * ;
@ -16,159 +15,74 @@ use crate :: ui :: component :: * ;
pub struct CollatedFilmsGrid { pub struct CollatedMediaGrid < A : MediaAdapter > {
film_widget_pairs : RefCell < Vec < ( FilmOverview , Button ) > > , media_widget_pairs : RefCell < Vec < ( A :: Overview , Button ) > > ,
grid_widget : FlowBox ,
}
pub struct CollatedSeriesGrid {
series_widget_pairs : RefCell < Vec < ( SeriesOverview , Button ) > > ,
grid_widget : FlowBox , grid_widget : FlowBox ,
} }
impl CollatedFilmsGrid { impl < A : MediaAdapter > CollatedMediaGrid <A> {
pub fn new () -> Self { pub fn new () -> Self {
let grid_widget = flow_box ! ( let grid_widget = flow_box ! (
@ orientation : Horizontal ; @ orientation : Horizontal ;
@ homogeneous : true ; @ homogeneous : true ;
@ selection_mode : SelectionMode :: None ; @ selection_mode : SelectionMode :: None ;
) ; ) ;
let film_widget_pairs = RefCell :: new ( vec ! () ) ; let media_widget_pairs = RefCell :: new ( vec ! () ) ;
Self { film_widget_pairs , grid_widget } Self { media_widget_pairs , grid_widget }
} }
pub async fn set_films ( & self , films : Vec <FilmOverview> , sorting : FilmsSorting ) { 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 () ; let mut widgets = Vec :: new () ;
for film in films . as_slice () { for media in media . as_slice () {
widgets . push ( create_film_entry (film) . await ) ; widgets . push ( create_media_entry (media) . await ) ;
} }
self . film_widget_pairs . replace ( zip ( films , widgets ) . collect () ) ; self . media_widget_pairs . replace ( zip ( media , widgets ) . collect () ) ;
for ( _ , film_widget ) in self . sort_film_widget_pairs (sorting) { for ( _ , widget ) in self . sort_media_widget_pairs (sorting) {
self . grid_widget . append ( & film_widget ) ; self . grid_widget . append ( & widget ) ;
} }
} }
pub fn set_sorting ( & self , sorting : FilmsSorting ) { pub fn set_sorting ( & self , sorting : A :: Sorting ) {
self . grid_widget . remove_all () ; self . grid_widget . remove_all () ;
for ( _ , film_widget ) in self . sort_film_widget_pairs (sorting) { for ( _ , widget ) in self . sort_media_widget_pairs (sorting) {
self . grid_widget . append ( & film_widget ) ; self . grid_widget . append ( & widget ) ;
} }
} }
fn sort_film_widget_pairs ( & self , sorting : FilmsSorting ) -> Vec < ( FilmOverview , Button ) > { fn sort_media_widget_pairs ( & self , sorting : A :: Sorting ) -> Vec < ( A :: Overview , Button ) > {
let mut sorted = Vec :: from ( let mut sorted = Vec :: from (
self . film_widget_pairs . borrow () . as_slice () ) ; self . media_widget_pairs . borrow () . as_slice () ,
sorted . sort_by ( | ( film_1 , _ ) , ( film_2 , _ ) | 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 ) ,
} ) ;
if sorting . direction == SortingDirection :: Descending { sorted . reverse () }
// See it, say it, ...
sorted
}
}
impl CollatedSeriesGrid {
pub fn new () -> Self {
let grid_widget = flow_box ! (
@ orientation : Horizontal ;
@ homogeneous : true ;
@ selection_mode : SelectionMode :: None ;
) ; ) ;
let series_widget_pairs = RefCell :: new ( vec ! () ) ;
Self { series_widget_pairs , grid_widget } sorted . sort_by ( | ( media_1 , _ ) , ( media_2 , _ ) |
} A :: compare_by ( media_1 , media_2 , sorting ) ,
) ;
pub async fn set_series ( & self , series : Vec <SeriesOverview> , sorting : SeriesSorting ) {
let mut widgets = Vec :: new () ;
for series in series . as_slice () {
widgets . push ( create_series_entry (series) . await ) ;
}
self . series_widget_pairs . replace ( zip ( series , widgets ) . collect () ) ;
for ( _ , series_widget ) in self . sort_series_widget_pairs (sorting) {
self . grid_widget . append ( & series_widget ) ;
}
}
pub fn set_sorting ( & self , sorting : SeriesSorting ) {
self . grid_widget . remove_all () ;
for ( _ , series_widget ) in self . sort_series_widget_pairs (sorting) {
self . grid_widget . append ( & series_widget ) ;
}
}
fn sort_series_widget_pairs ( & self , sorting : SeriesSorting ) -> Vec < ( SeriesOverview , Button ) > {
let mut sorted = Vec :: from (
self . series_widget_pairs . borrow () . as_slice () ) ;
sorted . sort_by ( | ( series_1 , _ ) , ( series_2 , _ ) | 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 ) ,
} ) ;
if sorting . direction == SortingDirection :: Descending { sorted . reverse () }
// See it, say it, ... // See it, say it, ...
sorted sorted
} }
} }
impl Component <FlowBox> for CollatedFilmsGrid { impl < A : MediaAdapter > Component for CollatedMediaGrid <A> {
fn get_widget ( & self ) -> & FlowBox { & self . grid_widget }
}
impl Component <FlowBox> for CollatedSeriesGrid {
fn get_widget ( & self ) -> & FlowBox { & self . grid_widget } fn get_widget ( & self ) -> & FlowBox { & self . grid_widget }
} }
async fn create_film_entry ( film : & FilmOverview ) -> Button { async fn create_media_entry < M : MediaOverview > ( media : & M ) -> Button {
button ! ( button ! (
@ css_classes : & [ "flat" , "open-collection-item-button" ] ; @ css_classes : & [ "flat" , "open-collection-item-button" ] ;
@ connect_clicked : |_| todo ! () ; @ connect_clicked : |_| todo ! () ;
& create_collection_item ( & g_box ! (
film . name . as_str () ,
film . original_name . as_deref () ,
film . poster_file_path . as_deref () ,
& create_film_details (film) ,
) . await ,
)
}
async fn create_series_entry ( series : & SeriesOverview ) -> Button {
button ! (
@ css_classes : & [ "flat" , "open-collection-item-button" ] ;
@ connect_clicked : |_| todo ! () ;
& create_collection_item (
series . name . as_str () ,
series . original_name . as_deref () ,
series . poster_file_path . as_deref () ,
& create_series_details (series) ,
) . await ,
)
}
async fn create_collection_item (
name : & str ,
original_name : Option < & str > ,
poster_file_path : Option < & Path > ,
details_widget : & Box ,
) -> Box {
g_box ! (
@ option_children ; @ option_children ;
@ orientation : Vertical ; @ orientation : Vertical ;
@ margin_top : 20 ; @ margin_top : 20 ;
@ margin_bottom : 20 ; @ margin_bottom : 20 ;
match poster_file_path { match media . get_poster_file_path () {
Some (poster_file_path) => { Some (poster_file_path) => {
let poster_file_path = PathBuf :: from (poster_file_path) ; // God forbid `Path` would work with `clone ! ()` let poster_file_path = PathBuf :: from (poster_file_path) ; // God forbid `Path` would work with `clone ! ()`
let poster_texture = spawn_blocking ( move || let poster_texture = spawn_blocking ( move ||
@ -189,37 +103,32 @@ async fn create_collection_item (
@ wrap : true ; @ wrap : true ;
@ max_width_chars : 1 ; // Not the actual limit, used instead to wrap more aggressively @ max_width_chars : 1 ; // Not the actual limit, used instead to wrap more aggressively
@ attributes : & pango_attributes ! ( @ scale : SCALE_LARGE ; @ weight : Bold ; ) ; @ attributes : & pango_attributes ! ( @ scale : SCALE_LARGE ; @ weight : Bold ; ) ;
name , media . get_name () . as_str () ,
) ) , ) ) ,
match original_name { match media . get_original_name () {
Some (original_name) => Some ( label ! ( Some (original_name) => Some ( label ! (
@ justify : Justification :: Center ; @ justify : Justification :: Center ;
@ wrap : true ; @ wrap : true ;
@ max_width_chars : 1 ; // Not the actual limit, used instead to wrap more aggressively @ max_width_chars : 1 ; // Not the actual limit, used instead to wrap more aggressively
original_name , original_name . as_str () ,
) ) , ) ) ,
None => None , None => None ,
} . as_ref () , } . as_ref () ,
Some (details_widget) , Some ( & g_box ! (
) @ option_children ;
}
fn create_film_details ( film : & FilmOverview ) -> Box {
g_box ! (
@ orientation : Horizontal ; @ orientation : Horizontal ;
@ halign : Center ; @ halign : Center ;
@ spacing : 20 ; @ spacing : 20 ;
& label ! ( film . release_date . as_str () ) , Some ( & label ! ( media . get_release_date () . as_str () ) ) ,
& label ! ( format ! ( "{}m" , film . runtime_minutes ) . as_str () ) , match media . get_runtime_minutes () {
) Some (runtime_minutes) => Some (
} label ! ( format ! ( "{}m" , runtime_minutes ) . as_str () ) ,
fn create_series_details ( series : & SeriesOverview ) -> Box { ) ,
g_box ! ( None => None ,
@ orientation : Horizontal ; } . as_ref () ,
@ halign : Center ; ) ) ,
@ spacing : 20 ; ) ,
& label ! ( series . first_release_date . as_str () ) ,
) )
} }

View file

@ -13,45 +13,24 @@ use crate :: ui :: collatable_container :: collation_menu :: sort_button :: * ;
pub struct FilmCollationMenu { widget : Box } pub struct MediaCollationMenu { widget : Box }
pub struct SeriesCollationMenu { widget : Box }
impl FilmCollationMenu { impl MediaCollationMenu {
pub fn new < F : Fn (FilmsSorting) + 'static > ( on_sort : F ) -> Self { pub fn new < A : MediaAdapter + 'static > ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self {
let film_sort_button = FilmSortButton :: new (on_sort) ; let sort_button = MediaSortButton :: <A> :: new (on_sort) ;
let widget = g_box ! ( let widget = g_box ! (
@ orientation : Horizontal ; @ orientation : Horizontal ;
@ halign : Center ; @ halign : Center ;
@ spacing : 20 ; @ spacing : 20 ;
@ widget_name : "film-collation-menu" ; @ css_classes : & [ "toolbar" , "collation-menu" ] ;
@ css_classes : & [ "toolbar" ] ; sort_button . get_widget () ,
film_sort_button . get_widget () ,
) ;
Self { widget }
}
}
impl SeriesCollationMenu {
pub fn new < F : Fn (SeriesSorting) + 'static > ( on_sort : F ) -> Self {
let series_sort_button = SeriesSortButton :: new (on_sort) ;
let widget = g_box ! (
@ orientation : Horizontal ;
@ halign : Center ;
@ spacing : 20 ;
@ widget_name : "series-collation-menu" ;
@ css_classes : & [ "toolbar" ] ;
series_sort_button . get_widget () ,
) ; ) ;
Self { widget } Self { widget }
} }
} }
impl Component <Box> for FilmCollationMenu { impl Component for MediaCollationMenu {
fn get_widget ( & self ) -> & Box { & self . widget }
}
impl Component <Box> for SeriesCollationMenu {
fn get_widget ( & self ) -> & Box { & self . widget } fn get_widget ( & self ) -> & Box { & self . widget }
} }

View file

@ -1,5 +1,4 @@
use gtk4 :: Image ; use gtk4 :: Image ;
use gtk4 :: ListBoxRow ;
use gtk4 :: Align :: * ; use gtk4 :: Align :: * ;
use libadwaita :: * ; use libadwaita :: * ;
use std :: cell :: * ; use std :: cell :: * ;
@ -11,82 +10,37 @@ use crate :: ui :: collatable_container :: SortingDirection :: * ;
pub struct FilmSortButton { pub struct MediaSortButton < A : MediaAdapter + 'static > {
widget : SplitButton , widget : SplitButton ,
previous_sorting : & 'static RefCell <FilmsSorting> , previous_sorting : & 'static RefCell < A :: Sorting > ,
}
pub struct SeriesSortButton {
widget : SplitButton ,
previous_sorting : & 'static RefCell <SeriesSorting> ,
} }
impl FilmSortButton { impl < A : MediaAdapter > MediaSortButton <A> {
pub fn new < F : Fn (FilmsSorting) + 'static > ( on_sort : F ) -> Self { pub fn new ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self {
let previous_sorting = leak ( RefCell :: new ( FilmsSorting :: default () ) ) ; let previous_sorting = leak ( RefCell :: new ( A :: Sorting :: default () ) ) ;
let property_descriptions = A :: get_property_descriptions () ;
let sort_icons = leak ( [ let sort_icons = {
image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) , let mut sort_icons = Vec :: new () ;
image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) , for _ in property_descriptions {
image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) , sort_icons . push ( image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) ) ;
] ) ;
let widget = split_button ! (
@ popover : & popover ! (
@ css_classes : & [ "menu" ] ;
& list_box ! (
@ connect_row_activated : move | _ , row | {
on_film_sort_activated ( row , previous_sorting , & on_sort , sort_icons ) ;
} ;
& g_box ! (
@ orientation : Horizontal ; @ spacing : 20 ;
& label ! ( @ hexpand : true ; @ halign : Start ; "Name" ) ,
& sort_icons [0] ,
) ,
& g_box ! (
@ orientation : Horizontal ; @ spacing : 20 ;
& label ! ( @ hexpand : true ; @ halign : Start ; "Release date" ) ,
& sort_icons [1] ,
) ,
& g_box ! (
@ orientation : Horizontal ; @ spacing : 20 ;
& label ! ( @ hexpand : true ; @ halign : Start ; "Runtime" ) ,
& sort_icons [2] ,
) ,
) ,
) ;
& label ! ("Sort") ,
) ;
Self { widget , previous_sorting }
} }
} Box :: leak ( sort_icons . into_boxed_slice () ) as & 'static _
impl SeriesSortButton { } ;
pub fn new < F : Fn (SeriesSorting) + 'static > ( on_sort : F ) -> Self {
let previous_sorting = leak ( RefCell :: new ( SeriesSorting :: default () ) ) ;
let sort_icons = leak ( [ let list_box = list_box ! ( @ connect_row_activated : move | _ , row |
image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) , on_media_sort_activated :: <A> ( row . index () , previous_sorting , & on_sort , sort_icons ) ;
image ! ( @ icon_name : "view-sort-ascending-symbolic" ; ) , ) ;
] ) ; for ( index , ( _ , description ) ) in property_descriptions . iter () . enumerate () {
list_box . append ( & g_box ! (
@ orientation : Horizontal ; @ spacing : 20 ;
& label ! ( @ hexpand : true ; @ halign : Start ; description ) ,
& sort_icons [index] ,
) ) ;
}
let widget = split_button ! ( let widget = split_button ! (
@ popover : & popover ! ( @ popover : & popover ! ( @ css_classes : & [ "menu" ] ; & list_box ) ;
@ css_classes : & [ "menu" ] ;
& list_box ! (
@ connect_row_activated : move | _ , row |
on_series_sort_activated ( row , previous_sorting , & on_sort , sort_icons ) ;
& g_box ! (
@ orientation : Horizontal ; @ spacing : 20 ;
& label ! ( @ hexpand : true ; @ halign : Start ; "Name" ) ,
& sort_icons [0] ,
) ,
& g_box ! (
@ orientation : Horizontal ; @ spacing : 20 ;
& label ! ( @ hexpand : true ; @ halign : Start ; "First release date" ) ,
& sort_icons [1] ,
) ,
) ,
) ;
& label ! ("Sort") , & label ! ("Sort") ,
) ; ) ;
@ -94,87 +48,38 @@ impl SeriesSortButton {
} }
} }
impl Component <SplitButton> for FilmSortButton { impl < A : MediaAdapter > Component for MediaSortButton <A> {
fn get_widget ( & self ) -> & SplitButton { & self . widget }
}
impl Component <SplitButton> for SeriesSortButton {
fn get_widget ( & self ) -> & SplitButton { & self . widget } fn get_widget ( & self ) -> & SplitButton { & self . widget }
} }
fn on_film_sort_activated < F : Fn (FilmsSorting) > ( fn on_media_sort_activated < A : MediaAdapter + 'static > (
row : & ListBoxRow , row : i32 ,
previous_sorting : & RefCell <FilmsSorting> , previous_sorting_mut : & RefCell < A :: Sorting > ,
on_sort : & F , on_sort : & impl Fn ( A :: Sorting ) ,
sort_icons : & [ Image ] , sort_icons : & [ Image ] ,
) { ) {
let sorting_property = match row . index () { let ( sorting_property , _ ) = A :: get_property_descriptions () [ row as usize ] . clone () ; // TODO: Bounds checking
0 => FilmProperty :: Name ,
1 => FilmProperty :: ReleaseDate ,
2 => FilmProperty :: Runtime ,
_ => panic ! () ,
} ;
if sorting_property == previous_sorting . borrow () . property { let previous_sorting = * previous_sorting_mut . borrow () ;
let previous_sorting_direction = previous_sorting . borrow () . direction ; if sorting_property == previous_sorting . get_property () {
match previous_sorting_direction { match previous_sorting . get_direction () {
Ascending => { Ascending => {
previous_sorting . replace ( FilmsSorting :: new ( sorting_property , Descending ) ) ; let new_sorting = A :: Sorting :: new ( sorting_property , Descending ) ;
for icon in sort_icons { previous_sorting_mut . replace (new_sorting) ;
icon . set_icon_name ( Some ("view-sort-descending-symbolic") ) ; sort_icons [ row as usize ] . set_icon_name ( Some ("view-sort-descending-symbolic") ) ;
} on_sort (new_sorting) ;
on_sort ( FilmsSorting :: new ( sorting_property , Descending ) ) ;
} , } ,
Descending => { Descending => {
previous_sorting . replace ( FilmsSorting :: new ( sorting_property , Ascending ) ) ; let new_sorting = A :: Sorting :: new ( sorting_property , Ascending ) ;
for icon in sort_icons { previous_sorting_mut . replace (new_sorting) ;
icon . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ; sort_icons [ row as usize ] . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ;
} on_sort (new_sorting) ;
on_sort ( FilmsSorting :: new ( sorting_property , Ascending ) ) ;
} , } ,
} }
} else { } else {
previous_sorting . replace ( FilmsSorting :: new ( sorting_property , Ascending ) ) ; let new_sorting = A :: Sorting :: new ( sorting_property , Ascending ) ;
for icon in sort_icons { previous_sorting_mut . replace (new_sorting) ;
icon . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ; sort_icons [ row as usize ] . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ;
} on_sort (new_sorting) ;
on_sort ( FilmsSorting :: new ( sorting_property , Ascending ) ) ;
}
}
fn on_series_sort_activated < F : Fn (SeriesSorting) > (
row : & ListBoxRow ,
previous_sorting : & RefCell <SeriesSorting> ,
on_sort : & F ,
sort_icons : & [ Image ] ,
) {
let sorting_property = match row . index () {
0 => SeriesProperty :: Name ,
1 => SeriesProperty :: FirstReleaseDate ,
_ => panic ! () ,
} ;
if sorting_property == previous_sorting . borrow () . property {
let previous_sorting_direction = previous_sorting . borrow () . direction ;
match previous_sorting_direction {
Ascending => {
previous_sorting . replace ( SeriesSorting :: new ( sorting_property , Descending ) ) ;
for icon in sort_icons {
icon . set_icon_name ( Some ("view-sort-descending-symbolic") ) ;
}
on_sort ( SeriesSorting :: new ( sorting_property , Descending ) ) ;
} ,
Descending => {
previous_sorting . replace ( SeriesSorting :: new ( sorting_property , Ascending ) ) ;
for icon in sort_icons {
icon . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ;
}
on_sort ( SeriesSorting :: new ( sorting_property , Ascending ) ) ;
} ,
}
} else {
previous_sorting . replace ( SeriesSorting :: new ( sorting_property , Ascending ) ) ;
for icon in sort_icons {
icon . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ;
}
on_sort ( SeriesSorting :: new ( sorting_property , Ascending ) ) ;
} }
} }

View file

@ -1,9 +1,11 @@
mod collated_grid ; mod collated_grid ;
mod collation_menu ; mod collation_menu ;
use gtk4 :: * ; use gtk4 :: Box ;
use gtk4 :: Orientation :: * ; use gtk4 :: Orientation :: * ;
use gtk4 :: prelude :: * ; use gtk4 :: prelude :: * ;
use std :: cmp :: * ;
use std :: fmt :: * ;
use crate :: data_manager :: * ; use crate :: data_manager :: * ;
use crate :: ui :: component :: * ; use crate :: ui :: component :: * ;
@ -14,28 +16,40 @@ use crate :: utility :: * ;
# [ derive ( Clone , Copy , PartialEq ) ] pub enum FilmProperty { Name , ReleaseDate , Runtime } pub trait MediaSorting < P : MediaProperty > : Clone + Copy + Debug + Default {
# [ derive ( Clone , Copy , PartialEq ) ] pub enum SeriesProperty { Name , FirstReleaseDate } fn new ( property : P , direction : SortingDirection ) -> Self ;
# [ derive ( Clone , Copy , PartialEq ) ] pub enum SortingDirection { Ascending , Descending } fn get_property ( & self ) -> P ;
fn get_direction ( & self ) -> SortingDirection ;
}
# [ derive ( Clone , Copy ) ] pub struct FilmsSorting { pub trait MediaProperty : Clone + Copy + Debug + PartialEq {}
# [ derive ( Clone , Copy , Debug , PartialEq ) ] pub enum FilmProperty { Name , ReleaseDate , Runtime }
# [ derive ( Clone , Copy , Debug , PartialEq ) ] pub enum SeriesProperty { Name , FirstReleaseDate }
# [ derive ( Clone , Copy , Debug , PartialEq ) ] pub enum SortingDirection { Ascending , Descending }
# [ derive ( Clone , Copy , Debug ) ] pub struct FilmsSorting {
property : FilmProperty , property : FilmProperty ,
direction : SortingDirection , direction : SortingDirection ,
} }
# [ derive ( Clone , Copy ) ] pub struct SeriesSorting { # [ derive ( Clone , Copy , Debug ) ] pub struct SeriesSorting {
property : SeriesProperty , property : SeriesProperty ,
direction : SortingDirection , direction : SortingDirection ,
} }
impl FilmsSorting { impl MediaSorting <FilmProperty> for FilmsSorting {
pub fn new ( property : FilmProperty , direction : SortingDirection ) -> Self { fn new ( property : FilmProperty , direction : SortingDirection ) -> Self {
Self { property , direction } Self { property , direction }
} }
fn get_property ( & self ) -> FilmProperty { self . property }
fn get_direction ( & self ) -> SortingDirection { self . direction }
} }
impl SeriesSorting { impl MediaSorting <SeriesProperty> for SeriesSorting {
pub fn new ( property : SeriesProperty , direction : SortingDirection ) -> Self { fn new ( property : SeriesProperty , direction : SortingDirection ) -> Self {
Self { property , direction } Self { property , direction }
} }
fn get_property ( & self ) -> SeriesProperty { self . property }
fn get_direction ( & self ) -> SortingDirection { self . direction }
} }
impl Default for FilmsSorting { impl Default for FilmsSorting {
@ -51,65 +65,110 @@ impl Default for SeriesSorting {
} } } }
} }
impl MediaProperty for FilmProperty {}
impl MediaProperty for SeriesProperty {}
pub struct CollatableMediaContainer < A : MediaAdapter + 'static > {
pub struct CollatableFilmsContainer { collated_grid : & 'static CollatedMediaGrid <A> ,
collated_grid : & 'static CollatedFilmsGrid ,
widget : Box ,
}
pub struct CollatableSeriesContainer {
collated_grid : & 'static CollatedSeriesGrid ,
widget : Box , widget : Box ,
} }
impl CollatableFilmsContainer { impl < A : MediaAdapter > CollatableMediaContainer <A> {
pub fn new () -> Self { pub fn new () -> Self {
let collated_grid = leak ( CollatedFilmsGrid :: new () ) ; let collated_grid = leak ( CollatedMediaGrid :: new () ) ;
let film_collation_menu = FilmCollationMenu :: new ( |sorting| let collation_menu = MediaCollationMenu :: new :: <A> ( |sorting|
collated_grid . set_sorting (sorting) ) ; collated_grid . set_sorting (sorting) ,
) ;
let widget = g_box ! ( let widget = g_box ! (
@ orientation : Vertical ; @ orientation : Vertical ;
film_collation_menu . get_widget () , collation_menu . get_widget () ,
& scrolled_window ! ( & scrolled_window ! (
@ propagate_natural_height : true ; @ propagate_natural_height : true ;
& vertically_filling ! ( collated_grid . get_widget () ) , & vertically_filling ! ( collated_grid . get_widget () ) ,
) , ) ,
) ; ) ;
Self { collated_grid , widget } Self { collated_grid, widget }
} }
pub async fn set_films ( & self , films : Vec <FilmOverview> ) { pub async fn set_media ( & self , media : Vec < A :: Overview > ) {
self . collated_grid . set_films ( films , FilmsSorting :: default () ) . await ; self . collated_grid . set_media ( media , A :: Sorting :: default () ) . await ;
}
}
impl CollatableSeriesContainer {
pub fn new () -> Self {
let collated_grid = leak ( CollatedSeriesGrid :: new () ) ;
let series_collation_menu = SeriesCollationMenu :: new ( |sorting|
collated_grid . set_sorting (sorting) ) ;
let widget = g_box ! (
@ orientation : Vertical ;
series_collation_menu . get_widget () ,
& scrolled_window ! (
@ propagate_natural_height : true ;
& vertically_filling ! ( collated_grid . get_widget () ) ,
) ,
) ;
Self { collated_grid , widget }
}
pub async fn set_series ( & self , series : Vec <SeriesOverview> ) {
self . collated_grid . set_series ( series , SeriesSorting :: default () ) . await ;
} }
} }
impl Component <Box> for CollatableFilmsContainer { pub trait MediaAdapter {
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 ) ] ;
}
impl < A : MediaAdapter > Component for CollatableMediaContainer <A> {
fn get_widget ( & self ) -> & Box { & self . widget } fn get_widget ( & self ) -> & Box { & self . widget }
} }
impl Component <Box> for CollatableSeriesContainer {
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 ;
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" ) ,
] )
}
}
impl MediaAdapter for SeriesAdapter {
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 get_property_descriptions () -> & 'static [ ( SeriesProperty , & 'static str ) ] {
leak ( [
( SeriesProperty :: Name , "Name" ) ,
( SeriesProperty :: FirstReleaseDate , "First release date" ) ,
] )
}
} }

View file

@ -1,27 +0,0 @@
use libadwaita :: * ;
use crate :: ui :: collatable_container :: * ;
use crate :: ui :: component :: * ;
use crate :: ui :: utility :: * ;
pub struct CollectionViewStack { widget : ViewStack }
impl CollectionViewStack {
pub fn new (
films_container : & CollatableFilmsContainer ,
series_container : & CollatableSeriesContainer ,
) -> Self {
let widget = view_stack ! (
( "Films" , "camera-video-symbolic" , films_container . get_widget () ) ,
( "Series" , "video-display-symbolic" , series_container . get_widget () ) ,
) ;
Self { widget }
}
}
impl Component <ViewStack> for CollectionViewStack {
fn get_widget ( & self ) -> & ViewStack { & self . widget }
}

View file

@ -3,6 +3,6 @@ use gtk4 :: prelude :: * ;
pub trait Component < W : IsA <Widget> > { pub trait Component {
fn get_widget ( & self ) -> & W ; fn get_widget ( & self ) -> & impl IsA <Widget> ;
} }

View file

@ -1,17 +1,14 @@
mod application_header_bar ;
mod collatable_container ; mod collatable_container ;
mod collection_view_stack ;
mod component ; mod component ;
mod utility ; mod utility ;
use gtk4 :: Orientation :: * ; use gtk4 :: Orientation :: * ;
use gtk4 :: prelude :: * ; use gtk4 :: prelude :: * ;
use libadwaita :: * ; use libadwaita :: * ;
use libadwaita :: ViewSwitcherPolicy :: * ;
use crate :: data_manager :: * ; use crate :: data_manager :: * ;
use crate :: ui :: application_header_bar :: * ;
use crate :: ui :: collatable_container :: * ; use crate :: ui :: collatable_container :: * ;
use crate :: ui :: collection_view_stack :: * ;
use crate :: ui :: component :: * ; use crate :: ui :: component :: * ;
use crate :: ui :: utility :: * ; use crate :: ui :: utility :: * ;
@ -19,25 +16,29 @@ use crate :: ui :: utility :: * ;
pub struct UI { pub struct UI {
window : ApplicationWindow , window : ApplicationWindow ,
films_component : CollatableFilmsContainer , films_component : CollatableMediaContainer <FilmsAdapter> ,
series_component : CollatableSeriesContainer , series_component : CollatableMediaContainer <SeriesAdapter> ,
} }
impl UI { impl UI {
pub fn new ( application : & Application ) -> UI { pub fn new ( application : & Application ) -> UI {
let films_component = CollatableFilmsContainer :: new () ; let films_component = CollatableMediaContainer :: <FilmsAdapter> :: new () ;
let series_component = CollatableSeriesContainer :: new () ; let series_component = CollatableMediaContainer :: <SeriesAdapter> :: new () ;
let switch_component = CollectionViewStack :: new ( let switcher = view_stack ! (
& films_component , & series_component ) ; ( "Films" , "camera-video-symbolic" , films_component . get_widget () ) ,
let header_bar = ApplicationHeaderBar :: new ( & switch_component ) ; ( "Series" , "video-display-symbolic" , series_component . get_widget () ) ,
) ;
let header_bar = header_bar ! (
& view_switcher ! ( @ policy : Wide ; & switcher ) ,
) ;
let window = application_window ! ( let window = application_window ! (
@ application : application ; @ application : application ;
@ title : "Zoödex" ; @ title : "Zoödex" ;
& g_box ! ( & g_box ! (
@ orientation : Vertical ; @ orientation : Vertical ;
header_bar . get_widget () , & header_bar ,
switch_component . get_widget () , & switcher ,
) , ) ,
) ; ) ;
@ -50,7 +51,7 @@ impl UI {
pub async fn render_collection_overview ( & self , collection : CollectionOverview ) { pub async fn render_collection_overview ( & self , collection : CollectionOverview ) {
// TODO: Find a way to await these futures concurrently // TODO: Find a way to await these futures concurrently
self . films_component . set_films ( collection . films ) . await ; self . films_component . set_media ( collection . films ) . await ;
self . series_component . set_series ( collection . series ) . await ; self . series_component . set_media ( collection . series ) . await ;
} }
} }