Initial commit, part deux

This commit is contained in:
Reinout Meliesie 2024-11-20 16:32:37 +01:00
parent 4a7beace84
commit fd5ee1420c
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
10 changed files with 1463 additions and 0 deletions

39
src/collection.rs Normal file
View file

@ -0,0 +1,39 @@
use std :: path :: * ;
pub struct Collection {
pub films : Vec <Film> ,
pub series : Vec <Series> ,
}
pub struct Film {
pub uuid : String ,
pub name : String ,
pub original_name : Option <String> ,
pub release_date : String , // TODO: Switch to chrono types, I think rusqlite has crate option for it
pub runtime_minutes : u32 ,
pub poster_file_path : Option <PathBuf> ,
pub video_file_path : Option <PathBuf> ,
}
pub struct Series {
pub uuid : String ,
pub name : String ,
pub original_name : Option <String> ,
pub poster_file_path : Option <PathBuf> ,
}
pub struct Season <'l> {
pub uuid : String ,
pub name : Option <String> ,
pub series : & 'l Series ,
}
pub struct Episode <'l> {
pub uuid : String ,
pub name : Option <String> ,
pub release_date : String ,
pub season : & 'l Season <'l> ,
pub video_file_path : Option <PathBuf> ,
}

36
src/main.rs Normal file
View file

@ -0,0 +1,36 @@
mod collection ;
mod persistence ;
mod ui ;
mod utility ;
use {
gtk4 :: { gio :: spawn_blocking , glib :: * } ,
libadwaita :: { * , prelude :: * } ,
} ;
use crate :: { persistence :: * , ui :: * } ;
fn main () -> ExitCode {
let application = Application :: builder ()
. application_id ("com.kernelmaft.zoodex")
. build () ;
application . connect_activate (on_activate) ;
application . run ()
}
fn on_activate ( app : & Application ) {
let mut ui = UI :: new (app) ;
let collection_handle = spawn_blocking ( ||
read_collection_file () . unwrap ()
) ;
ui . show_window () ;
spawn_future_local ( async move {
let collection = collection_handle . await . unwrap () ;
ui . render_collection (collection) ;
} ) ;
}

51
src/persistence.rs Normal file
View file

@ -0,0 +1,51 @@
use { fallible_iterator :: * , rusqlite :: * } ;
use crate :: collection :: * ;
pub fn read_collection_file () -> Result <Collection> {
let sqlite_connection = Connection :: open ("zoodex.sqlite") ? ;
let mut films_query = sqlite_connection
. prepare ("select * from films order by release_date desc") ? ;
let films = films_query . query (()) ? . map ( |row| {
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 poster_file_path : Option <String> = row . get (5) ? ;
let video_file_path : Option <String> = row . get (6) ? ;
Ok ( Film {
uuid ,
name ,
original_name ,
release_date ,
runtime_minutes ,
poster_file_path : poster_file_path . map ( String :: into ) ,
video_file_path : video_file_path . map ( String :: into ) ,
} )
} ) ;
let mut series_query = sqlite_connection . prepare ("select * from series") ? ;
let series = series_query . query (()) ? . map ( |row| {
let uuid = row . get (0) ? ;
let name = row . get (1) ? ;
let original_name = row . get (2) ? ;
let poster_file_path : Option <String> = row . get (3) ? ;
Ok ( Series {
uuid ,
name ,
original_name ,
poster_file_path : poster_file_path . map ( String :: into ) ,
} )
} ) ;
Ok ( Collection {
films : films . collect () ? ,
series : series . collect () ? ,
} )
}

115
src/ui/collection_menu.rs Normal file
View file

@ -0,0 +1,115 @@
use {
gtk4 :: {
Image , Label , ListBox , ListBoxRow , Popover ,
Align :: * ,
Orientation :: * ,
prelude :: * ,
} ,
libadwaita :: * ,
} ;
use crate :: ui :: { internal :: * , utility :: * } ;
pub fn create_film_collection_menu <F> ( on_sort : F ) -> gtk4 :: Box
where F : Fn (FilmsSortedBy) + 'static {
let container = gtk4 :: Box :: builder ()
. orientation (Horizontal)
. halign (Center)
. spacing (20)
. css_classes ( ["toolbar"] )
. build () ;
container . append ( & create_sort_button ( & create_films_sort_menu (on_sort) ) ) ;
container . append ( & SplitButton :: builder () . label ("Filter") . build () ) ;
container
}
pub fn create_series_collection_menu ( on_sort : fn (SeriesSortedBy) ) -> gtk4 :: Box {
let container = gtk4 :: Box :: builder ()
. orientation (Horizontal)
. halign (Center)
. spacing (20)
. css_classes ( ["toolbar"] )
. build () ;
container . append ( & create_sort_button ( & create_series_sort_menu (on_sort) ) ) ;
container . append ( & SplitButton :: builder () . label ("Filter") . build () ) ;
container
}
fn create_sort_button ( sort_menu : & Popover ) -> SplitButton {
SplitButton :: builder ()
. child ( & create_label ("Sort") )
. popover (sort_menu)
. build ()
}
fn create_films_sort_menu <F> ( on_sort : F ) -> Popover
where F : Fn (FilmsSortedBy) + 'static {
let container = ListBox :: new () ;
container . append ( & create_sort_menu_entry ( "Name" , false , true ) ) ;
container . append ( & create_sort_menu_entry ( "Release date" , false , false ) ) ;
container . append ( & create_sort_menu_entry ( "Runtime" , false , false ) ) ;
container . select_row ( container . row_at_index (0) . as_ref () ) ;
container . connect_row_activated ( move | _ , row | on_sort ( match row . index () {
0 => FilmsSortedBy :: Name ,
1 => FilmsSortedBy :: ReleaseDate ,
2 => FilmsSortedBy :: Runtime ,
_ => panic ! () ,
} ) ) ;
Popover :: builder ()
. child ( & container )
. css_classes ( ["menu"] )
. build ()
}
fn create_series_sort_menu ( on_sort : fn (SeriesSortedBy) ) -> Popover {
let container = ListBox :: new () ;
container . append ( & create_sort_menu_entry ( "Name" , false , true ) ) ;
container . append ( & create_sort_menu_entry ( "First release date" , false , false ) ) ;
container . select_row ( container . row_at_index (0) . as_ref () ) ;
container . connect_row_activated ( move | _ , row | on_sort ( match row . index () {
0 => SeriesSortedBy :: Name ,
1 => SeriesSortedBy :: FirstReleaseDate ,
_ => panic ! () ,
} ) ) ;
Popover :: builder ()
. child ( & container )
. css_classes ( ["menu"] )
. build ()
}
fn create_sort_menu_entry ( label : & str , reverse : bool , selected : bool ) -> ListBoxRow {
let icon = match reverse {
false => Image :: from_icon_name ("view-sort-ascending-symbolic") ,
true => Image :: from_icon_name ("view-sort-descending-symbolic") ,
} ;
if ! selected { icon . set_opacity (0.0) }
let container = create_horizontal_box ! (
& Label :: builder ()
. label (label)
. hexpand (true)
. halign (Start)
. build () ,
& icon ,
) ;
container . set_spacing (20) ;
// TODO : Highlight `:selected` row using CSS, parent with `.menu` prevents default behaviour
ListBoxRow :: builder () . child ( & container ) . build ()
}

269
src/ui/internal.rs Normal file
View file

@ -0,0 +1,269 @@
use {
gtk4 :: {
FlowBox,
HeaderBar,
Image,
Justification,
Label,
ScrolledWindow,
SelectionMode,
Widget,
Align :: * ,
Orientation :: * ,
gdk :: Texture ,
prelude :: * ,
} ,
libadwaita :: { * , ViewSwitcherPolicy :: * } ,
std :: { cell :: * , path :: * } ,
} ;
use crate :: {
collection :: * ,
ui :: { collection_menu :: * , utility :: * } ,
utility :: * ,
} ;
pub fn create_window (
application : & Application ,
header_bar : & HeaderBar ,
collection_view_stack : & ViewStack ,
) -> ApplicationWindow {
let content = gtk4 :: Box :: builder () . orientation (Vertical) . build () ;
content . append (header_bar) ;
content . append (collection_view_stack) ;
ApplicationWindow :: builder ()
. application (application)
. content ( & content )
. title ("Zoödex")
. build ()
}
pub fn create_header_bar ( collection_view_stack : & ViewStack ) -> HeaderBar {
HeaderBar :: builder ()
. title_widget (
& create_collection_view_switcher ( collection_view_stack ) ,
)
. build ()
}
pub fn create_collection_view_switcher ( stack : & ViewStack ) -> ViewSwitcher {
ViewSwitcher :: builder ()
. policy (Wide)
. stack (stack)
. build ()
}
pub struct FilmsFlowBox { widget : FlowBox }
pub struct SeriesFlowBox { widget : FlowBox }
impl FilmsFlowBox {
pub fn new ( films : & [Film] ) -> Self {
let widget = create_collection_flow_box () ;
for film in films {
widget . append ( & create_film_item (film) ) ;
}
Self { widget }
}
pub fn set_films ( & self , films : & [Film] ) {
self . widget . remove_all () ;
for film in films {
self . widget . append ( & create_film_item (film) ) ;
}
}
pub fn get_widget ( & self ) -> & FlowBox { & self . widget }
}
impl SeriesFlowBox {
pub fn new ( series : & [Series] ) -> Self {
let widget = create_collection_flow_box () ;
for series in series {
widget . append ( & create_series_item (series) ) ;
}
Self { widget }
}
pub fn set_series ( & self , series : & [Series] ) {
self . widget . remove_all () ;
for series in series {
self . widget . append ( & create_series_item (series) ) ;
}
}
pub fn get_widget ( & self ) -> & FlowBox { & self . widget }
}
fn create_collection_flow_box () -> FlowBox {
FlowBox :: builder ()
. orientation (Horizontal)
. homogeneous (true)
. selection_mode ( SelectionMode :: None )
. build ()
}
pub enum FilmsSortedBy { Name , ReleaseDate , Runtime }
pub enum SeriesSortedBy { Name , FirstReleaseDate }
pub struct FilmsContainer {
films : Vec <Film> ,
flow_box : FilmsFlowBox ,
widget : gtk4 :: Box ,
sorted_by : & 'static Cell <FilmsSortedBy> ,
}
pub struct SeriesContainer {
series : Vec <Series> ,
flow_box : SeriesFlowBox ,
widget : gtk4 :: Box ,
}
impl FilmsContainer {
pub fn new ( films : Vec <Film> ) -> Self {
let flow_box = FilmsFlowBox :: new ( films . as_slice () ) ;
let sorted_by = leak ( Cell :: new ( FilmsSortedBy :: Name ) ) ;
let widget = create_vertical_box ! (
& create_film_collection_menu ( |new_sorted_by| {
sorted_by . replace (new_sorted_by) ;
} ) ,
& create_collection_scrolled_window ( flow_box . get_widget () ) ,
) ;
Self { films , flow_box , widget , sorted_by }
}
pub fn set_films ( & mut self , films : Vec <Film> ) {
self . films = films ;
self . flow_box . set_films ( self . films . as_slice () ) ;
}
pub fn get_widget ( & self ) -> & gtk4 :: Box { & self . widget }
}
impl SeriesContainer {
pub fn new ( series : Vec <Series> ) -> Self {
let flow_box = SeriesFlowBox :: new ( series . as_slice () ) ;
let widget = create_vertical_box ! (
& create_series_collection_menu ( |sorted_by| {
// TODO
} ) ,
& create_collection_scrolled_window ( flow_box . get_widget () ) ,
) ;
Self { series, flow_box , widget }
}
pub fn set_series ( & mut self , series : Vec <Series> ) {
self . series = series ;
self . flow_box . set_series ( self . series . as_slice () ) ;
}
pub fn get_widget ( & self ) -> & gtk4 :: Box { & self . widget }
}
fn create_collection_scrolled_window ( child : & impl IsA <Widget> ) -> ScrolledWindow {
ScrolledWindow :: builder ()
. child ( & create_vertical_filler_container (child) )
. propagate_natural_height (true)
. build ()
}
pub fn create_film_item ( film : & Film ) -> gtk4 :: Box {
create_collection_item (
film . name . as_str () ,
film . original_name . as_deref () ,
film . poster_file_path . as_deref () ,
& create_film_details (film) ,
)
}
pub fn create_series_item ( series : & Series ) -> gtk4 :: Box {
create_collection_item (
series . name . as_str () ,
series . original_name . as_deref () ,
series . poster_file_path . as_deref () ,
& create_series_details (series) ,
)
}
fn create_film_details ( film : & Film ) -> gtk4 :: Box {
let container = gtk4 :: Box :: builder ()
. orientation (Horizontal)
. halign (Center)
. spacing (20)
. build () ;
container . append (
& Label :: builder () . label ( & film . release_date ) . build ()
) ;
container . append (
& Label :: builder () . label ( format ! ( "{}m" , film . runtime_minutes ) ) . build ()
) ;
container
}
fn create_series_details ( series : & Series ) -> gtk4 :: Box {
let container = gtk4 :: Box :: builder ()
. orientation (Horizontal)
. halign (Center)
. spacing (20)
. build () ;
// TODO
container
}
fn create_collection_item (
name : & str ,
original_name : Option < & str > ,
poster_file_path : Option < & Path > ,
details_widget : & gtk4 :: Box ,
) -> gtk4 :: Box {
let container = gtk4 :: Box :: builder ()
. orientation (Vertical)
. margin_top (20)
. margin_bottom (20)
. build () ;
if let Some (poster_file_path) = poster_file_path {
if let Ok (poster_texture) = Texture :: from_filename (poster_file_path) {
container . append (
& Image :: builder ()
. paintable ( & poster_texture )
. width_request (300)
. height_request (300)
. margin_bottom (10)
. build ()
) ;
}
}
container . append (
& Label :: builder ()
. label ( format ! ( "<span size='large' weight='bold'>{}</span>" , name ) )
. use_markup (true)
. justify ( Justification :: Center )
. wrap (true)
. max_width_chars (1) // Not the actual limit, used instead to wrap more aggressively
. build ()
) ;
if let Some (original_name) = original_name {
container . append (
& Label :: builder ()
. label (original_name)
. justify ( Justification :: Center )
. wrap (true)
. max_width_chars (1) // Not the actual limit, used instead to wrap more aggressively
. build ()
) ;
}
container . append (details_widget) ;
container
}

37
src/ui/mod.rs Normal file
View file

@ -0,0 +1,37 @@
mod collection_menu ;
mod internal ;
mod utility ;
use { gtk4 :: prelude :: * , libadwaita :: * } ;
use crate :: { collection :: * , ui :: { internal :: * , utility :: * } } ;
pub struct UI {
window : ApplicationWindow ,
films_container : FilmsContainer ,
series_container : SeriesContainer ,
}
impl UI {
pub fn new ( application : & Application ) -> UI {
let films_container = FilmsContainer :: new ( vec ! () ) ;
let series_container = SeriesContainer :: new ( vec ! () ) ;
let collection_view_stack = create_view_stack ! (
"Films" , "camera-video-symbolic" , films_container . get_widget () ,
"Series" , "video-display-symbolic" , series_container . get_widget () ,
) ;
let header_bar = create_header_bar ( & collection_view_stack ) ;
let window = create_window ( application , & header_bar , & collection_view_stack ) ;
UI { window , films_container , series_container }
}
pub fn show_window ( & self ) { self . window . set_visible (true) }
pub fn render_collection ( & mut self , collection : Collection ) {
self . films_container . set_films ( collection . films ) ;
self . series_container . set_series ( collection . series ) ;
}
}

45
src/ui/utility.rs Normal file
View file

@ -0,0 +1,45 @@
use { gtk4 :: { Label , Widget , prelude :: * } , libadwaita :: * } ;
pub fn create_label ( label : & str ) -> Label {
Label :: builder () . label (label) . build ()
}
macro_rules ! create_horizontal_box { ( $ ( $ child : expr , ) * ) => { {
let container = gtk4 :: Box :: builder ()
. orientation ( gtk4 :: Orientation :: Horizontal )
. build () ;
$ ( container . append ( $ child ) ; ) *
container
} } }
macro_rules ! create_vertical_box { ( $ ( $ child : expr , ) * ) => { {
let container = gtk4 :: Box :: builder ()
. orientation ( gtk4 :: Orientation :: Vertical)
. build () ;
$ ( container . append ( $ child ) ; ) *
container
} } }
macro_rules ! create_view_stack { (
$ ( $ title : expr , $ icon : expr , $ widget : expr , ) *
) => { {
let container = ViewStack :: new () ;
$ ( container . add_titled_with_icon ( $ widget , None , $ title , $ icon ) ; ) *
container
} } }
pub fn create_vertical_filler_container ( child : & impl IsA <Widget> ) -> gtk4 :: Box {
create_vertical_box ! (
child ,
& Bin :: builder ()
. css_name ("filler")
. vexpand (true)
. build () ,
)
}
pub (crate) use { create_horizontal_box , create_vertical_box , create_view_stack } ;

3
src/utility.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn leak < 'l , T > ( inner : T ) -> & 'l mut T {
Box :: leak ( Box :: new (inner) )
}