Use cargo fmt and conform to Style Guide (mostly)

This commit is contained in:
Reinout Meliesie 2026-01-08 14:00:01 +01:00
commit 2982a13bc2
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
15 changed files with 980 additions and 888 deletions

View file

@ -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

View file

@ -1,18 +1,18 @@
[ package ] [package]
name = "zoodex" name = "zoodex"
version = "1.0.0" version = "1.0.0"
authors = [ "Reinout Meliesie <zedfrigg@kernelmaft.com>" ] authors = ["Reinout Meliesie <zedfrigg@kernelmaft.com>"]
edition = "2024" edition = "2024"
rust-version = "1.89.0" rust-version = "1.89.0"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[ profile . release ] [profile.release]
lto = true lto = true
[ dependencies ] [dependencies]
async-sqlite = { version = "0.5.3" , default-features = false } async-sqlite = { version = "0.5.3", default-features = false }
fallible-iterator = "0.3.0" # Must match version used by async-sqlite fallible-iterator = "0.3.0" # Must match version used by async-sqlite
futures = "0.3.31" futures = "0.3.31"
gtk4 = { version = "0.10.3" , features = [ "v4_20" ] } gtk4 = { version = "0.10.3", features = ["v4_20"] }
libadwaita = { version = "0.8.1" , features = [ "v1_8" ] } libadwaita = { version = "0.8.1", features = ["v1_8"] }
relm4-macros = { version = "0.10.1" , default-features = false } relm4-macros = { version = "0.10.1", default-features = false }

18
rustfmt.toml Normal file
View file

@ -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

View file

@ -1,29 +1,29 @@
/* 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 */
.collation-menu row:selected { .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 { .collation-menu row:not(:selected) image {
opacity : 0 ; opacity: 0;
} }
.collatable-container flowboxchild { .collatable-container flowboxchild {
padding : 0 ; padding: 0;
} }
.collection-item-button { .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 { .collection-item-box {
margin-top : 20px ; margin-top: 20px;
margin-bottom : 20px ; margin-bottom: 20px;
} }
.collection-item-image { .collection-item-image {
margin-bottom : 20px ; margin-bottom: 20px;
} }
.media-modal { .media-modal {
padding : 100px ; padding: 100px;
} }

View file

@ -1,255 +1,287 @@
use async_sqlite :: * ; use std::env::var_os;
use async_sqlite :: Error :: * ; use std::path::PathBuf;
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 crate :: error :: * ; use async_sqlite::rusqlite::{OpenFlags, Row};
use crate :: error :: ZoodexError :: * ; use async_sqlite::{Client, ClientBuilder, rusqlite};
use crate :: utility :: * ; use fallible_iterator::FallibleIterator;
use crate::error::{Result, ZoodexError};
use crate::utility::concat_os_str;
pub struct DataManager { pub struct DataManager {
sqlite_client_local : Client , sqlite_client_local: Client,
sqlite_client_shared : Client , sqlite_client_shared: Client,
} }
impl DataManager { impl DataManager {
pub async fn new () -> Result <Self> { pub async fn new() -> Result<Self> {
let home_directory = var_os ("HOME") . unwrap () ; let home_directory = var_os("HOME").unwrap();
let xdg_data_home = var_os ("XDG_DATA_HOME") ; let xdg_data_home = var_os("XDG_DATA_HOME");
let data_dir = match xdg_data_home { let data_dir = match xdg_data_home {
Some (xdg_data_home) => concat_os_str ! ( xdg_data_home , "/zoodex" ) , Some(xdg_data_home) => concat_os_str!(xdg_data_home, "/zoodex"),
None => concat_os_str ! ( home_directory , "/.local/share/zoodex" ) , None => concat_os_str!(home_directory, "/.local/share/zoodex"),
} ; };
let sqlite_client_shared = ClientBuilder :: new () let sqlite_client_shared = ClientBuilder::new()
. path ( concat_os_str ! ( & data_dir , "/shared.sqlite" ) ) .path(concat_os_str!(&data_dir, "/shared.sqlite"))
. flags ( OpenFlags :: SQLITE_OPEN_READ_ONLY | OpenFlags :: SQLITE_OPEN_NO_MUTEX ) .flags(OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX)
. open () .open()
. await ? ; .await?;
let sqlite_client_local = ClientBuilder :: new () let sqlite_client_local = ClientBuilder::new()
. path ( concat_os_str ! ( & data_dir , "/shared.sqlite" ) ) .path(concat_os_str!(&data_dir, "/shared.sqlite"))
. flags ( OpenFlags :: SQLITE_OPEN_READ_WRITE | OpenFlags :: SQLITE_OPEN_NO_MUTEX ) .flags(OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX)
. open () .open()
. await ? ; .await?;
Ok ( Self { Ok(Self {
sqlite_client_local , sqlite_client_local,
sqlite_client_shared , sqlite_client_shared,
} ) })
} }
pub async fn get_collection_overview ( & self ) -> Result <CollectionOverview> { pub async fn get_collection_overview(&self) -> Result<CollectionOverview> {
let collection_overview = self . sqlite_client_shared . conn ( |sqlite_connection| { let collection_overview = self
let films = sqlite_connection .sqlite_client_shared
. prepare ( " .conn(|sqlite_connection| {
select uuid , name , original_name , release_date , runtime_minutes let films = sqlite_connection
from films .prepare(
" ) ? "
. query (()) ? select uuid , name , original_name , release_date , runtime_minutes
. map (row_to_film_overview) from films
. collect () ? ; ",
)?
.query(())?
.map(row_to_film_overview)
.collect()?;
let series = sqlite_connection let series = sqlite_connection
. prepare ( " .prepare(
select series . uuid , series . name , series . original_name , "
min ( episodes . release_date ) select series . uuid , series . name , series . original_name ,
from series , seasons , episodes min ( episodes . release_date )
where series . uuid = seasons . series and seasons . uuid = episodes . season from series , seasons , episodes
group by series . uuid where series . uuid = seasons . series and seasons . uuid = episodes . season
" ) ? group by series . uuid
. query (()) ? ",
. map (row_to_series_overview) )?
. collect () ? ; .query(())?
.map(row_to_series_overview)
.collect()?;
Ok ( CollectionOverview { films , series } ) Ok(CollectionOverview { films, series })
} ) . await ? ; })
Ok (collection_overview) .await?;
} Ok(collection_overview)
}
pub async fn get_film_details ( & self , uuid : String ) -> Result <FilmDetails> { pub async fn get_film_details(&self, uuid: String) -> Result<FilmDetails> {
let film_details = self . sqlite_client_shared . conn ( |sqlite_connection| { let film_details = self
let film_details = sqlite_connection .sqlite_client_shared
. prepare ( " .conn(|sqlite_connection| {
select let film_details = sqlite_connection
films . uuid , .prepare(
films . name , "
films . original_name , select
films . release_date , films . uuid ,
films . runtime_minutes , films . name ,
sources . media_uuid , films . original_name ,
sources . bittorrent_hash , films . release_date ,
sources . file_path , films . runtime_minutes ,
sources . audio_track , sources . media_uuid ,
sources . subtitle_track sources . bittorrent_hash ,
from films left join sources sources . file_path ,
on films . uuid = sources . media_uuid sources . audio_track ,
where films . uuid = (?1) sources . subtitle_track
" ) ? from films left join sources
. query ( [ uuid ] ) ? on films . uuid = sources . media_uuid
. map (row_to_film_details) where films . uuid = (?1)
. next () ? ",
. unwrap () ; )?
Ok (film_details) .query([uuid])?
} ) . await ? ; .map(row_to_film_details)
Ok (film_details) .next()?
} .unwrap();
Ok(film_details)
})
.await?;
Ok(film_details)
}
} }
pub struct CollectionOverview { pub struct CollectionOverview {
pub films : Vec <FilmOverview> , pub films: Vec<FilmOverview>,
pub series : Vec <SeriesOverview> , pub series: Vec<SeriesOverview>,
} }
pub trait MediaOverview : Clone { pub trait MediaOverview: Clone {
fn get_uuid ( & self ) -> String ; fn get_uuid(&self) -> String;
fn get_name ( & self ) -> String ; fn get_name(&self) -> String;
fn get_original_name ( & self ) -> Option <String> ; fn get_original_name(&self) -> Option<String>;
fn get_release_date ( & self ) -> String ; fn get_release_date(&self) -> String;
fn get_runtime_minutes ( & self ) -> Option <u32> ; fn get_runtime_minutes(&self) -> Option<u32>;
} }
# [ derive (Clone) ] pub struct FilmOverview { #[derive(Clone)]
pub uuid : String , pub struct FilmOverview {
pub name : String , pub uuid: String,
pub original_name : Option <String> , pub name: String,
pub release_date : String , // TODO: Switch to chrono types, I think rusqlite has crate option for it pub original_name: Option<String>,
pub runtime_minutes : u32 , // 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 { #[derive(Clone)]
pub uuid : String , pub struct SeriesOverview {
pub name : String , pub uuid: String,
pub original_name : Option <String> , pub name: String,
pub first_release_date : String , // TODO: Switch to chrono types, I think rusqlite has crate option for it pub original_name: Option<String>,
// TODO: Switch to chrono types, I think rusqlite has crate option for it
pub first_release_date: String,
} }
impl MediaOverview for FilmOverview { impl MediaOverview for FilmOverview {
fn get_uuid ( & self ) -> String { self . uuid . clone () } fn get_uuid(&self) -> String {
fn get_name ( & self ) -> String { self . name . clone () } self.uuid.clone()
fn get_original_name ( & self ) -> Option <String> { self . original_name . clone () } }
fn get_release_date ( & self ) -> String { self . release_date . clone () } fn get_name(&self) -> String {
fn get_runtime_minutes ( & self ) -> Option <u32> { Some ( self . runtime_minutes ) } 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)
}
} }
impl MediaOverview for SeriesOverview { impl MediaOverview for SeriesOverview {
fn get_uuid ( & self ) -> String { self . uuid . clone () } fn get_uuid(&self) -> String {
fn get_name ( & self ) -> String { self . name . clone () } self.uuid.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_name(&self) -> String {
fn get_runtime_minutes ( & self ) -> Option <u32> { None } 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 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)?;
let original_name = row . get (2) ? ; let original_name = row.get(2)?;
let release_date = row . get (3) ? ; let release_date = row.get(3)?;
let runtime_minutes = row . get (4) ? ; let runtime_minutes = row.get(4)?;
Ok ( FilmOverview { Ok(FilmOverview {
uuid , uuid,
name , name,
original_name , original_name,
release_date , release_date,
runtime_minutes , runtime_minutes,
} ) })
} }
fn row_to_series_overview ( row : & Row ) -> rusqlite :: Result <SeriesOverview> { fn row_to_series_overview(row: &Row) -> rusqlite::Result<SeriesOverview> {
let uuid = row . get (0) ? ; let uuid = row.get(0)?;
let name = row . get (1) ? ; let name = row.get(1)?;
let original_name = row . get (2) ? ; let original_name = row.get(2)?;
let first_release_date = row . get (3) ? ; let first_release_date = row.get(3)?;
Ok ( SeriesOverview { Ok(SeriesOverview {
uuid , uuid,
name , name,
original_name , original_name,
first_release_date , first_release_date,
} ) })
} }
# [ derive (Clone) ] pub struct FilmDetails { #[derive(Clone)]
pub uuid : String , pub struct FilmDetails {
pub name : String , pub uuid: String,
pub original_name : Option <String> , pub name: String,
pub release_date : String , pub original_name: Option<String>,
pub runtime_minutes : u32 , pub release_date: String,
pub source : Option <SourceDetails> , pub runtime_minutes: u32,
pub source: Option<SourceDetails>,
} }
# [ derive (Clone) ] pub struct SourceDetails { #[derive(Clone)]
pub bittorrent_hash : String , pub struct SourceDetails {
pub file_path : PathBuf , pub bittorrent_hash: String,
pub audio_track : Option <u32> , pub file_path: PathBuf,
pub subtitle_track : Option <u32> , pub audio_track: Option<u32>,
pub subtitle_track: Option<u32>,
} }
fn row_to_film_details ( row : & Row ) -> rusqlite :: Result <FilmDetails> { fn row_to_film_details(row: &Row) -> rusqlite::Result<FilmDetails> {
let uuid = row . get (0) ? ; let uuid = row.get(0)?;
let name = row . get (1) ? ; let name = row.get(1)?;
let original_name = row . get (2) ? ; let original_name = row.get(2)?;
let release_date = row . get (3) ? ; let release_date = row.get(3)?;
let runtime_minutes = row . get (4) ? ; let runtime_minutes = row.get(4)?;
let source_media_uuid = row . get :: < _ , Option <String> > (5) ? ; let source_media_uuid = row.get::<_, Option<String>>(5)?;
let source = match source_media_uuid { let source = match source_media_uuid {
Some (_) => { Some(_) => {
let bittorrent_hash = row . get (6) ? ; let bittorrent_hash = row.get(6)?;
let file_path = PathBuf :: from ( row . get :: < _ , String > (7) ? ) ; let file_path = PathBuf::from(row.get::<_, String>(7)?);
let audio_track = row . get (8) ? ; let audio_track = row.get(8)?;
let subtitle_track = row . get (9) ? ; let subtitle_track = row.get(9)?;
Some ( SourceDetails { Some(SourceDetails {
bittorrent_hash , bittorrent_hash,
file_path , file_path,
audio_track , audio_track,
subtitle_track , subtitle_track,
} ) })
} , }
None => None , None => None,
} ; };
Ok ( FilmDetails { Ok(FilmDetails {
uuid , uuid,
name , name,
original_name , original_name,
release_date , release_date,
runtime_minutes , runtime_minutes,
source , source,
} ) })
} }
impl From <Error> for ZoodexError { impl From<async_sqlite::Error> for ZoodexError {
fn from ( error : Error ) -> Self { fn from(error: async_sqlite::Error) -> Self {
match error { match error {
Rusqlite (error) => ZoodexError :: from (error) , async_sqlite::Error::Rusqlite(error) => ZoodexError::from(error),
_ => panic ! ( "{}" , error ) , _ => panic!("{}", error),
} }
} }
} }
impl From < rusqlite :: Error > for ZoodexError { impl From<rusqlite::Error> for ZoodexError {
fn from ( error : rusqlite :: Error ) -> Self { fn from(error: rusqlite::Error) -> Self {
match error { match error {
SqliteFailure ( error , _ ) => { rusqlite::Error::SqliteFailure(error, _) => match error.code {
match error . code { rusqlite::ffi::ErrorCode::CannotOpen => ZoodexError::CollectionFileReadError,
CannotOpen => CollectionFileReadError , _ => panic!("{}", error),
_ => panic ! ( "{}" , error ) , },
} _ => panic!("{}", error),
} , }
_ => panic ! ( "{}" , error ) , }
}
}
} }

View file

@ -1,33 +1,35 @@
use std :: * ; use std::any::Any;
use std :: any :: * ; use std::result;
# [ derive (Debug) ] pub enum ZoodexError { #[derive(Debug)]
CollectionFileReadError , pub enum ZoodexError {
CollectionFileReadError,
} }
pub type Result <Success> = result :: Result < Success , ZoodexError > ; pub type Result<Success> = result::Result<Success, ZoodexError>;
impl From < Box < dyn Any + Send > > for ZoodexError { impl From<Box<dyn Any + Send>> for ZoodexError {
fn from ( error : Box < dyn Any + Send > ) -> Self { fn from(error: Box<dyn Any + Send>) -> Self {
* error . downcast () . unwrap () *error.downcast().unwrap()
} }
} }
macro_rules ! async_result_context { ( macro_rules! async_result_context {(
$ future : expr $future: expr
$ ( , ok => $ on_success : expr ) ? $(, ok => $on_success: expr)?
$ ( , err => $ on_failure : expr ) ? $ (,) ? $(, err => $on_failure: expr)?$(,)?
) => { ) => {
# [ allow (unreachable_patterns) ] match $ future . await { #[allow(unreachable_patterns)]
$ ( Ok (value) => $ on_success (value) , ) ? match $future.await {
Ok (_) => {} , $(Ok(value) => $on_success(value),)?
$ ( Err (error) => $ on_failure (error) , ) ? Ok(_) => {},
Err (_) => {} , $(Err(error) => $on_failure(error),)?
} Err(_) => {},
} } }
}}
pub (crate) use async_result_context ; pub(crate) use async_result_context;

View file

@ -1,70 +1,69 @@
mod error ; mod data_manager;
mod data_manager ; mod error;
mod ui ; mod ui;
mod utility ; mod utility;
use gtk4 :: CssProvider ; use gtk4::gdk::Display;
use gtk4 :: style_context_add_provider_for_display ; use gtk4::glib::{ExitCode, spawn_future_local};
use gtk4 :: STYLE_PROVIDER_PRIORITY_APPLICATION ; use gtk4::prelude::{ApplicationExt, ApplicationExtManual};
use gtk4 :: gdk :: * ; use gtk4::{
use gtk4 :: glib :: * ; CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, style_context_add_provider_for_display,
use libadwaita :: Application ; };
use libadwaita :: prelude :: * ; use libadwaita::Application;
use crate :: data_manager :: * ; use crate::data_manager::DataManager;
use crate :: error :: * ; use crate::error::{ZoodexError, async_result_context};
use crate :: error :: ZoodexError :: * ; use crate::ui::{UI, Window};
use crate :: ui :: * ; use crate::utility::leak;
use crate :: utility :: * ;
fn main () -> ExitCode { fn main() -> ExitCode {
let application = Application :: builder () let application = Application::builder()
. application_id ("com.kernelmaft.zoodex") .application_id("com.kernelmaft.zoodex")
. build () ; .build();
application . connect_startup (add_style_provider) ; application.connect_startup(add_style_provider);
application . connect_activate (show_window) ; application.connect_activate(show_window);
application . run () application.run()
} }
fn add_style_provider ( _ : & Application ) { fn add_style_provider(_: &Application) {
let style_provider = CssProvider :: new () ; let style_provider = CssProvider::new();
style_provider . load_from_string ( include_str ! ("application.css") ) ; style_provider.load_from_string(include_str!("application.css"));
style_context_add_provider_for_display ( style_context_add_provider_for_display(
& Display :: default () . unwrap () , &Display::default().unwrap(),
& style_provider , &style_provider,
STYLE_PROVIDER_PRIORITY_APPLICATION , STYLE_PROVIDER_PRIORITY_APPLICATION,
) ; );
} }
fn show_window ( application : & Application ) { fn show_window(application: &Application) {
let window = leak ( Window :: new (application) ) ; let window = leak(Window::new(application));
spawn_future_local ( async move { spawn_future_local(async move {
async_result_context ! ( async_result_context!(
async { async {
let data_manager = leak ( DataManager :: new () . await ? ) ; let data_manager = leak(DataManager::new().await?);
let ui = UI :: new ( let ui = UI::new(
window , window,
async |film_uuid| { async |film_uuid| {
data_manager . get_film_details (film_uuid) . await data_manager.get_film_details(film_uuid).await
. expect ("A film with the given UUID should exist") .expect("A film with the given UUID should exist")
} , },
) ; );
window . show () ; window.show();
let collection = data_manager . get_collection_overview () . await ? ; let collection = data_manager.get_collection_overview().await?;
ui . render_collection_overview (collection) . await ; ui.render_collection_overview(collection).await;
Ok (()) Ok(())
} , },
err => |error| { err => |error| {
match error { match error {
CollectionFileReadError => eprintln ! ("Could not read collection file") , ZoodexError::CollectionFileReadError => eprintln!("Could not read collection file"),
} ; };
window . close () ; window.close();
} , },
) ; );
} ) ; });
} }

View file

@ -1,175 +1,183 @@
use gtk4 :: { Button , FlowBox , Image , Justification , Label , SelectionMode } ; use std::cell::RefCell;
use gtk4 :: Align :: * ; use std::env::var_os;
use gtk4 :: Orientation :: * ; use std::iter::zip;
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 crate :: ui :: collatable_container :: * ; use gtk4::gdk::Texture;
use crate :: ui :: component :: * ; 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 > { pub struct CollatedMediaGrid<A: MediaAdapter> {
media_widget_pairs : RefCell < Vec < ( A :: Overview , Button ) > > , media_widget_pairs: RefCell<Vec<(A::Overview, Button)>>,
grid_widget : FlowBox , grid_widget: FlowBox,
on_media_selected : & 'static dyn Fn ( A :: Overview ) , on_media_selected: &'static dyn Fn(A::Overview),
} }
impl < A : MediaAdapter > CollatedMediaGrid <A> { impl<A: MediaAdapter> CollatedMediaGrid<A> {
pub fn new ( on_media_selected : impl Fn ( A :: Overview ) + 'static ) -> Self { pub fn new(on_media_selected: impl Fn(A::Overview) + 'static) -> Self {
let grid_widget = view_expr ! { let grid_widget = view_expr! {
FlowBox { FlowBox {
set_homogeneous : true , set_homogeneous: true,
set_selection_mode : SelectionMode :: None , set_selection_mode: SelectionMode::None,
set_css_classes : & [ "collatable-container" ] , set_css_classes: &["collatable-container"],
set_orientation : Horizontal , set_orientation: Orientation::Horizontal,
} }
} ; };
let media_widget_pairs = RefCell :: new ( Vec :: new () ) ; let media_widget_pairs = RefCell::new(Vec::new());
let on_media_selected = leak (on_media_selected) ; 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 ) { pub async fn set_media(&self, media: Vec<A::Overview>, sorting: A::Sorting) {
// TODO: Check if we should use `MainContext :: invoke_local` here // TODO: Check if we should use `MainContext :: invoke_local` here
let mut widgets = Vec :: new () ; let mut widgets = Vec::new();
for media in media . as_slice () { for media in media.as_slice() {
widgets . push ( self . create_media_entry (media) . await ) ; widgets.push(self.create_media_entry(media).await);
} }
self . media_widget_pairs . replace ( zip ( media , widgets ) . collect () ) ; self
.media_widget_pairs
.replace(zip(media, widgets).collect());
for ( _ , widget ) in self . sort_media_widget_pairs (sorting) { for (_, widget) in self.sort_media_widget_pairs(sorting) {
self . grid_widget . append ( & widget ) ; self.grid_widget.append(&widget);
} }
} }
async fn create_media_entry ( & self , media : & A :: Overview ) -> Button { async fn create_media_entry(&self, media: &A::Overview) -> Button {
view_expr ! { view_expr! {
Button { Button {
set_css_classes : & [ "flat" , "collection-item-button" ] , set_css_classes: &["flat", "collection-item-button"],
connect_clicked : clone ! ( connect_clicked: clone!(
# [ strong ] media , #[strong] media,
# [ strong ( rename_to = on_media_selected ) ] self . on_media_selected , #[strong(rename_to = on_media_selected)] self.on_media_selected,
move |_| on_media_selected ( media . clone () ) , move |_| on_media_selected(media.clone()),
) , ),
set_child : Some ( & view_expr ! { set_child: Some(&view_expr! {
gtk4 :: Box { gtk4::Box {
set_css_classes : & [ "collection-item-box" ] , set_css_classes: &["collection-item-box"],
set_valign : Center , set_valign: Align::Center,
set_orientation : Vertical , set_orientation: Orientation::Vertical,
// Poster // Poster
append_opt : & { append_opt: &{
let home_directory = var_os ("HOME") . unwrap () ; let home_directory = var_os("HOME").unwrap();
let xdg_data_home = var_os ("XDG_DATA_HOME") ; let xdg_data_home = var_os("XDG_DATA_HOME");
let data_dir = match xdg_data_home { let data_dir = match xdg_data_home {
Some (xdg_data_home) => concat_os_str ! ( xdg_data_home , "/zoodex" ) , Some(xdg_data_home) => concat_os_str!(xdg_data_home, "/zoodex"),
None => concat_os_str ! ( home_directory , "/.local/share/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 ( let poster_texture = spawn_blocking(move || Texture::from_filename(poster_file_path))
move || Texture :: from_filename (poster_file_path) , .await
) . await . unwrap () ; .unwrap();
match poster_texture { match poster_texture {
Ok (poster_texture) => Some ( view_expr ! { Ok(poster_texture) => Some(view_expr! {
Image { Image {
set_paintable : Some ( & poster_texture ) , set_paintable: Some(&poster_texture),
set_pixel_size : 300 , set_pixel_size: 300,
set_css_classes : & [ "collection-item-image" ] , set_css_classes: &["collection-item-image"],
} }
} ) , }),
Err (error) => { Err(error) => {
if error . matches ( IOErrorEnum :: NotFound ) { if error.matches(IOErrorEnum::NotFound) {
None // The file not existing simply means there is no poster for this piece of media // The file not existing simply means there is no poster for this piece of media
} else { None
panic ! ( "{}" , error ) // Any other error means something unexpected went wrong } else {
} // Any other error means something unexpected went wrong
} , panic!("{}", error)
} }
} , },
}
},
// Name // Name
append : & view_expr ! { append: &view_expr! {
Label { Label {
set_attributes : Some ( & pango_attributes ! ( scale : SCALE_LARGE , weight : Bold ) ) , set_attributes: Some(&pango_attributes!(scale: SCALE_LARGE, weight: Weight::Bold)),
set_justify : Justification :: Center , set_justify: Justification::Center,
set_max_width_chars : 1 , // Not the actual limit, used instead to wrap more aggressively // Not the actual limit, used instead to wrap more aggressively
set_wrap : true , set_max_width_chars: 1,
set_label : media . get_name () . as_str () , set_wrap: true,
} set_label: media.get_name().as_str(),
} , }
},
// Original name // Original name
append_opt : & media . get_original_name () . map ( |original_name| view_expr ! { append_opt: &media.get_original_name().map(|original_name| view_expr! {
Label { Label {
set_justify : Justification :: Center , set_justify: Justification::Center,
set_max_width_chars : 1 , set_max_width_chars: 1,
set_wrap : true , set_wrap: true,
set_label : original_name . as_str () , set_label: original_name.as_str(),
} }
} ) , }),
// Details // Details
append : & view_expr ! { append: &view_expr! {
gtk4 :: Box { gtk4::Box {
set_spacing : 20 , set_spacing: 20,
set_halign : Center , set_halign: Align::Center,
set_orientation : Horizontal , set_orientation: Orientation::Horizontal,
// Release date // Release date
append : & view_expr ! { append: &view_expr! {
Label { set_label : media . get_release_date () . split ('-') . next () . unwrap () } Label { set_label: media.get_release_date().split('-').next().unwrap() }
} , },
// Runtime // Runtime
append_opt : & media . get_runtime_minutes () . map ( |runtime_minutes| view_expr ! { append_opt: &media.get_runtime_minutes().map(|runtime_minutes| view_expr! {
Label { set_label : format ! ( "{}m" , runtime_minutes ) . as_str () } Label { set_label: format!("{}m", runtime_minutes).as_str() }
} ) , }),
} }
} , },
} }
} ) , }),
} }
} }
} }
pub fn set_sorting ( & self , sorting : A :: Sorting ) { pub fn set_sorting(&self, sorting: A::Sorting) {
self . grid_widget . remove_all () ; self.grid_widget.remove_all();
for ( _ , widget ) in self . sort_media_widget_pairs (sorting) { for (_, widget) in self.sort_media_widget_pairs(sorting) {
self . grid_widget . append ( & widget ) ; self.grid_widget.append(&widget);
} }
} }
fn sort_media_widget_pairs ( & self , sorting : A :: Sorting ) -> Vec < ( A :: Overview , 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.media_widget_pairs.borrow().as_slice());
self . media_widget_pairs . borrow () . as_slice () ,
) ;
sorted . sort_by ( sorted.sort_by(|(media_1, _), (media_2, _)| A::compare_by(media_1, media_2, sorting));
| ( media_1 , _ ) , ( media_2 , _ ) | A :: compare_by ( media_1 , media_2 , sorting ) ,
) ;
// See it, say it, ... // See it, say it, ...
sorted sorted
} }
} }
impl < A : MediaAdapter > Component for CollatedMediaGrid <A> { impl<A: MediaAdapter> Component for CollatedMediaGrid<A> {
fn get_widget ( & self ) -> & FlowBox { & self . grid_widget } fn get_widget(&self) -> &FlowBox {
&self.grid_widget
}
} }

View file

@ -1,39 +1,40 @@
mod sort_button ; mod sort_button;
use gtk4 :: Box ; use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt};
use gtk4 :: Align :: * ; use gtk4::{Align, Box, Orientation};
use gtk4 :: Orientation :: * ; use relm4_macros::view;
use gtk4 :: prelude :: * ;
use relm4_macros :: * ;
use std :: ops :: * ;
use crate :: ui :: component :: * ; use crate::ui::collatable_container::MediaAdapter;
use crate :: ui :: collatable_container :: * ; use crate::ui::collatable_container::collation_menu::sort_button::MediaSortButton;
use crate :: ui :: collatable_container :: collation_menu :: sort_button :: * ; use crate::ui::component::Component;
pub struct MediaCollationMenu { widget : Box } pub struct MediaCollationMenu {
widget: Box,
}
impl MediaCollationMenu { impl MediaCollationMenu {
pub fn new < A : MediaAdapter > ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self { pub fn new<A: MediaAdapter>(on_sort: impl Fn(A::Sorting) + 'static) -> Self {
let sort_button = MediaSortButton :: <A> :: new (on_sort) ; let sort_button = MediaSortButton::<A>::new(on_sort);
view ! { view! {
widget = gtk4 :: Box { widget = gtk4::Box {
set_spacing : 20 , set_spacing: 20,
set_css_classes : & [ "toolbar" , "collation-menu" ] , set_css_classes: &["toolbar", "collation-menu"],
set_halign : Center , set_halign: Align::Center,
set_orientation : Horizontal , set_orientation: Orientation::Horizontal,
append : sort_button . get_widget () , append: sort_button.get_widget(),
} , },
} }
Self { widget } Self { widget }
} }
} }
impl Component for MediaCollationMenu { impl Component for MediaCollationMenu {
fn get_widget ( & self ) -> & Box { & self . widget } fn get_widget(&self) -> &Box {
&self.widget
}
} }

View file

@ -1,116 +1,122 @@
use gtk4 :: { Image , ListBox , Popover } ; use std::cell::RefCell;
use gtk4 :: Align :: * ;
use libadwaita :: SplitButton ;
use relm4_macros :: * ;
use std :: cell :: * ;
use crate :: utility :: * ; use gtk4::prelude::{BoxExt, ListBoxRowExt, OrientableExt, PopoverExt, WidgetExt};
use crate :: ui :: * ; use gtk4::{Align, Image, Label, ListBox, Orientation, Popover};
use crate :: ui :: utility :: * ; use libadwaita::SplitButton;
use crate :: ui :: collatable_container :: SortingDirection :: * ; 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 > { pub struct MediaSortButton<A: MediaAdapter> {
widget : SplitButton , widget: SplitButton,
previous_sorting : & 'static RefCell < A :: Sorting > , previous_sorting: &'static RefCell<A::Sorting>,
} }
impl < A : MediaAdapter > MediaSortButton <A> { impl<A: MediaAdapter> MediaSortButton<A> {
pub fn new ( on_sort : impl Fn ( A :: Sorting ) + 'static ) -> Self { pub fn new(on_sort: impl Fn(A::Sorting) + 'static) -> Self {
let previous_sorting = leak ( RefCell :: new ( A :: Sorting :: default () ) ) ; let previous_sorting = leak(RefCell::new(A::Sorting::default()));
let property_descriptions = A :: get_property_descriptions () ; let property_descriptions = A::get_property_descriptions();
let sort_icons = { let sort_icons = {
let mut sort_icons = Vec :: new () ; let mut sort_icons = Vec::new();
for _ in property_descriptions { for _ in property_descriptions {
sort_icons . push ( view_expr ! { sort_icons.push(view_expr! {
Image { set_icon_name : Some ( "view-sort-ascending-symbolic" ) } Image { set_icon_name: Some("view-sort-ascending-symbolic") }
} ) ; });
} }
Box :: leak ( sort_icons . into_boxed_slice () ) as & 'static _ Box::leak(sort_icons.into_boxed_slice()) as &'static _
} ; };
view ! { view! {
list_box = ListBox { list_box = ListBox {
connect_row_activated : move | _ , row | on_media_sort_activated :: <A> ( connect_row_activated: move |_, row| on_media_sort_activated::<A>(
row . index () , row.index(),
previous_sorting , previous_sorting,
& on_sort , &on_sort,
sort_icons , sort_icons,
) , ),
} , },
widget = SplitButton { widget = SplitButton {
set_popover : Some ( & view_expr ! { set_popover: Some(&view_expr! {
Popover { Popover {
set_css_classes : & [ "menu" ] , set_css_classes: &["menu"],
set_child : Some ( & list_box ) , set_child: Some(&list_box),
} }
} ) , }),
set_child : Some ( & view_expr ! { set_child: Some(&view_expr! {
Label { set_label : "Sort" } Label { set_label: "Sort" }
} ) , }),
} , },
} }
for ( index , ( _ , description ) ) in property_descriptions . iter () . enumerate () { for (index, (_, description)) in property_descriptions.iter().enumerate() {
list_box . append ( & view_expr ! { list_box.append(&view_expr! {
gtk4 :: Box { gtk4::Box {
set_spacing : 20 , set_spacing: 20,
set_orientation : Horizontal , set_orientation: Orientation::Horizontal,
append : & view_expr ! { append: &view_expr! {
Label { Label {
set_halign : Start , set_halign: Align::Start,
set_hexpand : true , set_hexpand: true,
set_label : description , set_label: description,
} }
} , },
append : & sort_icons [index] , append: &sort_icons[index],
} }
} ) ; });
} }
Self { widget , previous_sorting } Self {
} widget,
previous_sorting,
}
}
} }
impl < A : MediaAdapter > Component for MediaSortButton <A> { impl<A: MediaAdapter> Component for MediaSortButton<A> {
fn get_widget ( & self ) -> & SplitButton { & self . widget } fn get_widget(&self) -> &SplitButton {
&self.widget
}
} }
fn on_media_sort_activated < A : MediaAdapter > ( fn on_media_sort_activated<A: MediaAdapter>(
row : i32 , row: i32,
previous_sorting_mut : & RefCell < A :: Sorting > , previous_sorting_mut: &RefCell<A::Sorting>,
on_sort : & impl Fn ( A :: Sorting ) , on_sort: &impl Fn(A::Sorting),
sort_icons : & [ Image ] , sort_icons: &[Image],
) { ) {
let row = row as usize ; let row = row as usize;
debug_assert ! ( debug_assert!(
row <= A :: get_property_descriptions () . len () , row <= A::get_property_descriptions().len(),
"Sorting menu has more rows than media adapter has property descriptions" , "Sorting menu has more rows than media adapter has property descriptions",
) ; );
let ( sorting_property , _ ) = A :: get_property_descriptions () [row] . clone () ; let (sorting_property, _) = A::get_property_descriptions()[row].clone();
let previous_sorting = * previous_sorting_mut . borrow () ; let previous_sorting = *previous_sorting_mut.borrow();
if sorting_property == previous_sorting . get_property () { if sorting_property == previous_sorting.get_property() {
match previous_sorting . get_direction () { match previous_sorting.get_direction() {
Ascending => { SortingDirection::Ascending => {
let new_sorting = A :: Sorting :: new ( sorting_property , Descending ) ; let new_sorting = A::Sorting::new(sorting_property, SortingDirection::Descending);
previous_sorting_mut . replace (new_sorting) ; previous_sorting_mut.replace(new_sorting);
sort_icons [row] . set_icon_name ( Some ("view-sort-descending-symbolic") ) ; sort_icons[row].set_icon_name(Some("view-sort-descending-symbolic"));
on_sort (new_sorting) ; on_sort(new_sorting);
} , }
Descending => { SortingDirection::Descending => {
let new_sorting = A :: Sorting :: new ( sorting_property , Ascending ) ; let new_sorting = A::Sorting::new(sorting_property, SortingDirection::Ascending);
previous_sorting_mut . replace (new_sorting) ; previous_sorting_mut.replace(new_sorting);
sort_icons [row] . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ; sort_icons[row].set_icon_name(Some("view-sort-ascending-symbolic"));
on_sort (new_sorting) ; on_sort(new_sorting);
} , }
} }
} else { } else {
let new_sorting = A :: Sorting :: new ( sorting_property , Ascending ) ; let new_sorting = A::Sorting::new(sorting_property, SortingDirection::Ascending);
previous_sorting_mut . replace (new_sorting) ; previous_sorting_mut.replace(new_sorting);
sort_icons [row] . set_icon_name ( Some ("view-sort-ascending-symbolic") ) ; sort_icons[row].set_icon_name(Some("view-sort-ascending-symbolic"));
on_sort (new_sorting) ; on_sort(new_sorting);
} }
} }

View file

@ -1,177 +1,202 @@
mod collated_grid ; mod collated_grid;
mod collation_menu ; mod collation_menu;
use gtk4 :: { Box , ScrolledWindow } ; use std::cmp::Ordering;
use gtk4 :: Orientation :: * ; use std::fmt::Debug;
use gtk4 :: prelude :: * ;
use relm4_macros :: * ;
use std :: cmp :: * ;
use std :: fmt :: * ;
use crate :: data_manager :: * ; use gtk4::prelude::{BoxExt, OrientableExt};
use crate :: ui :: component :: * ; use gtk4::{Box, Orientation, ScrolledWindow};
use crate :: ui :: utility :: * ; use relm4_macros::view;
use crate :: ui :: collatable_container :: collated_grid :: * ;
use crate :: ui :: collatable_container :: collation_menu :: * ; use crate::data_manager::{FilmOverview, MediaOverview, SeriesOverview};
use crate :: utility :: * ; 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 { pub trait MediaSorting<P: MediaProperty>: Clone + Copy + Debug + Default {
fn new ( property : P , direction : SortingDirection ) -> Self ; fn new(property: P, direction: SortingDirection) -> Self;
fn get_property ( & self ) -> P ; fn get_property(&self) -> P;
fn get_direction ( & self ) -> SortingDirection ; 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 { #[derive(Clone, Copy, Debug, Default, PartialEq)]
# [default] Name , ReleaseDate , Runtime , pub enum FilmProperty {
#[default]
Name,
ReleaseDate,
Runtime,
} }
# [ derive ( Clone , Copy , Debug , Default , PartialEq ) ] pub enum SeriesProperty { #[derive(Clone, Copy, Debug, Default, PartialEq)]
# [default] Name , FirstReleaseDate , pub enum SeriesProperty {
#[default]
Name,
FirstReleaseDate,
} }
# [ derive ( Clone , Copy , Debug , Default , PartialEq ) ] pub enum SortingDirection { #[derive(Clone, Copy, Debug, Default, PartialEq)]
# [default] Ascending , Descending , pub enum SortingDirection {
#[default]
Ascending,
Descending,
} }
# [ derive ( Clone , Copy , Debug , Default ) ] pub struct FilmsSorting { #[derive(Clone, Copy, Debug, Default)]
property : FilmProperty , pub struct FilmsSorting {
direction : SortingDirection , property: FilmProperty,
direction: SortingDirection,
} }
# [ derive ( Clone , Copy , Debug , Default ) ] pub struct SeriesSorting { #[derive(Clone, Copy, Debug, Default)]
property : SeriesProperty , pub struct SeriesSorting {
direction : SortingDirection , property: SeriesProperty,
direction: SortingDirection,
} }
impl MediaSorting <FilmProperty> for FilmsSorting { impl MediaSorting<FilmProperty> for FilmsSorting {
fn new ( property : FilmProperty , direction : SortingDirection ) -> Self { fn new(property: FilmProperty, direction: SortingDirection) -> Self {
Self { property , direction } Self {
} property,
fn get_property ( & self ) -> FilmProperty { self . property } direction,
fn get_direction ( & self ) -> SortingDirection { self . direction } }
}
fn get_property(&self) -> FilmProperty {
self.property
}
fn get_direction(&self) -> SortingDirection {
self.direction
}
} }
impl MediaSorting <SeriesProperty> for SeriesSorting { impl MediaSorting<SeriesProperty> for SeriesSorting {
fn new ( property : SeriesProperty , direction : SortingDirection ) -> Self { fn new(property: SeriesProperty, direction: SortingDirection) -> Self {
Self { property , direction } Self {
} property,
fn get_property ( & self ) -> SeriesProperty { self . property } direction,
fn get_direction ( & self ) -> SortingDirection { self . direction } }
}
fn get_property(&self) -> SeriesProperty {
self.property
}
fn get_direction(&self) -> SortingDirection {
self.direction
}
} }
impl MediaProperty for FilmProperty {} impl MediaProperty for FilmProperty {}
impl MediaProperty for SeriesProperty {} impl MediaProperty for SeriesProperty {}
pub struct CollatableMediaContainer < A : MediaAdapter > { pub struct CollatableMediaContainer<A: MediaAdapter> {
collated_grid : & 'static CollatedMediaGrid <A> , collated_grid: &'static CollatedMediaGrid<A>,
widget : Box , widget: Box,
} }
impl < A : MediaAdapter > CollatableMediaContainer <A> { impl<A: MediaAdapter> CollatableMediaContainer<A> {
pub fn new ( on_media_selected : impl Fn ( A :: Overview ) + 'static ) -> Self { pub fn new(on_media_selected: impl Fn(A::Overview) + 'static) -> Self {
let collated_grid = leak ( CollatedMediaGrid :: new (on_media_selected) ) ; let collated_grid = leak(CollatedMediaGrid::new(on_media_selected));
let collation_menu = MediaCollationMenu :: new :: <A> ( let collation_menu = MediaCollationMenu::new::<A>(|sorting| collated_grid.set_sorting(sorting));
|sorting| collated_grid . set_sorting (sorting) ,
) ;
view ! { view! {
widget = gtk4 :: Box { widget = gtk4::Box {
set_orientation : Vertical , set_orientation: Orientation::Vertical,
append : collation_menu . get_widget () , append: collation_menu.get_widget(),
append : & view_expr ! { append: &view_expr! {
ScrolledWindow { ScrolledWindow {
set_propagate_natural_height : true , set_propagate_natural_height: true,
set_child : Some ( & vertical_filler ( collated_grid . get_widget () ) ) , 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 > ) { pub async fn set_media(&self, media: Vec<A::Overview>) {
self . collated_grid . set_media ( media , A :: Sorting :: default () ) . await ; self
} .collated_grid
.set_media(media, A::Sorting::default())
.await;
}
} }
pub trait MediaAdapter : 'static { pub trait MediaAdapter: 'static {
type Overview : MediaOverview ; type Overview: MediaOverview;
type Sorting : MediaSorting < Self :: Property > ; type Sorting: MediaSorting<Self::Property>;
type Property : MediaProperty ; type Property: MediaProperty;
fn compare_by ( fn compare_by(
media_1 : & Self :: Overview , media_1: &Self::Overview,
media_2 : & Self :: Overview , media_2: &Self::Overview,
sorting : Self :: Sorting , sorting: Self::Sorting,
) -> Ordering ; ) -> Ordering;
fn get_property_descriptions () -> & 'static [ ( Self :: Property , & 'static str ) ] ; fn get_property_descriptions() -> &'static [(Self::Property, &'static str)];
} }
impl < A : MediaAdapter > Component for CollatableMediaContainer <A> { impl<A: MediaAdapter> Component for CollatableMediaContainer<A> {
fn get_widget ( & self ) -> & Box { & self . widget } fn get_widget(&self) -> &Box {
&self.widget
}
} }
pub struct FilmsAdapter {} pub struct FilmsAdapter {}
pub struct SeriesAdapter {} pub struct SeriesAdapter {}
impl MediaAdapter for FilmsAdapter { impl MediaAdapter for FilmsAdapter {
type Overview = FilmOverview ; type Overview = FilmOverview;
type Sorting = FilmsSorting ; type Sorting = FilmsSorting;
type Property = FilmProperty ; type Property = FilmProperty;
fn compare_by ( fn compare_by(film_1: &FilmOverview, film_2: &FilmOverview, sorting: FilmsSorting) -> Ordering {
film_1 : & FilmOverview , let ordering = match sorting.property {
film_2 : & FilmOverview , FilmProperty::Name => film_1.name.cmp(&film_2.name),
sorting : FilmsSorting , FilmProperty::ReleaseDate => film_1.release_date.cmp(&film_2.release_date),
) -> Ordering { FilmProperty::Runtime => film_1.runtime_minutes.cmp(&film_2.runtime_minutes),
let ordering = match sorting . property { };
FilmProperty :: Name => match sorting.direction {
film_1 . name . cmp ( & film_2 . name ) , SortingDirection::Ascending => ordering,
FilmProperty :: ReleaseDate => SortingDirection::Descending => ordering.reverse(),
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 ) ] { fn get_property_descriptions() -> &'static [(FilmProperty, &'static str)] {
leak ( [ leak([
( FilmProperty :: Name , "Name" ) , (FilmProperty::Name, "Name"),
( FilmProperty :: ReleaseDate , "Release date" ) , (FilmProperty::ReleaseDate, "Release date"),
( FilmProperty :: Runtime , "Runtime" ) , (FilmProperty::Runtime, "Runtime"),
] ) ])
} }
} }
impl MediaAdapter for SeriesAdapter { impl MediaAdapter for SeriesAdapter {
type Overview = SeriesOverview ; type Overview = SeriesOverview;
type Sorting = SeriesSorting ; type Sorting = SeriesSorting;
type Property = SeriesProperty ; type Property = SeriesProperty;
fn compare_by ( fn compare_by(
series_1 : & SeriesOverview , series_1: &SeriesOverview,
series_2 : & SeriesOverview , series_2: &SeriesOverview,
sorting : SeriesSorting , sorting: SeriesSorting,
) -> Ordering { ) -> Ordering {
let ordering = match sorting . property { let ordering = match sorting.property {
SeriesProperty :: Name => SeriesProperty::Name => series_1.name.cmp(&series_2.name),
series_1 . name . cmp ( & series_2 . name ) , SeriesProperty::FirstReleaseDate => series_1
SeriesProperty :: FirstReleaseDate => .first_release_date
series_1 . first_release_date . cmp ( & series_2 . first_release_date ) , .cmp(&series_2.first_release_date),
} ; };
match sorting . direction { match sorting.direction {
SortingDirection :: Ascending => ordering , SortingDirection::Ascending => ordering,
SortingDirection :: Descending => ordering . reverse () , SortingDirection::Descending => ordering.reverse(),
} }
} }
fn get_property_descriptions () -> & 'static [ ( SeriesProperty , & 'static str ) ] { fn get_property_descriptions() -> &'static [(SeriesProperty, &'static str)] {
leak ( [ leak([
( SeriesProperty :: Name , "Name" ) , (SeriesProperty::Name, "Name"),
( SeriesProperty :: FirstReleaseDate , "First release date" ) , (SeriesProperty::FirstReleaseDate, "First release date"),
] ) ])
} }
} }

View file

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

View file

@ -1,154 +1,163 @@
mod collatable_container ; mod collatable_container;
mod component ; mod component;
mod utility ; mod utility;
use futures :: * ; use std::process::Command;
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 crate :: data_manager :: * ; use futures::join;
use crate :: ui :: collatable_container :: * ; use gtk4::glib::spawn_future_local;
use crate :: ui :: component :: * ; use gtk4::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, WidgetExt};
use crate :: ui :: utility :: * ; use gtk4::{Button, Image, Label, Orientation};
use crate :: utility :: * ; 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 { pub struct UI {
films_component : CollatableMediaContainer <FilmsAdapter> , films_component: CollatableMediaContainer<FilmsAdapter>,
series_component : CollatableMediaContainer <SeriesAdapter> , series_component: CollatableMediaContainer<SeriesAdapter>,
} }
impl UI { impl UI {
pub fn new ( pub fn new(
window : & 'static Window , window: &'static Window,
get_film_details : impl AsyncFn (String) -> FilmDetails + 'static , get_film_details: impl AsyncFn(String) -> FilmDetails + 'static,
) -> UI { ) -> UI {
let get_film_details = leak (get_film_details) ; let get_film_details = leak(get_film_details);
let films_component = CollatableMediaContainer :: <FilmsAdapter> :: new ( |film| { let films_component = CollatableMediaContainer::<FilmsAdapter>::new(|film| {
spawn_future_local ( async { spawn_future_local(async {
let film_details = get_film_details ( film . uuid ) . await ; let film_details = get_film_details(film.uuid).await;
view ! { view! {
Dialog { Dialog {
present : Some ( & window . libadwaita_window ) , present: Some(&window.libadwaita_window),
set_child : Some ( & view_expr ! { set_child: Some(&view_expr! {
gtk4 :: Box { gtk4::Box {
set_spacing : 40 , set_spacing: 40,
set_css_classes : & [ "media-modal" ] , set_css_classes: &["media-modal"],
set_orientation : Vertical , set_orientation: Orientation::Vertical,
append : & view_expr ! { append: &view_expr! {
Label { Label {
set_css_classes : & [ "title-1" ] , set_css_classes: &["title-1"] ,
set_label : film_details . name . as_str () , set_label: film_details.name.as_str(),
} }
} , },
append_opt : & film_details . original_name . map ( |original_name| view_expr ! { append_opt: &film_details.original_name.map(|original_name| view_expr! {
Label { set_label : original_name . as_str () } Label { set_label: original_name.as_str() }
} ) , }),
append : & view_expr ! { append: &view_expr! {
Label { set_label : & format ! ( "Release date: {}" , film_details . release_date ) } Label { set_label: &format!("Release date: {}", film_details.release_date) }
} , },
append_opt : & film_details . source . map ( |source| view_expr ! { append_opt: &film_details.source.map(|source| view_expr! {
Button { Button {
set_css_classes : & [ "suggested-action" , "circular" ] , set_css_classes: &["suggested-action", "circular"],
connect_clicked : move |_| { connect_clicked: move |_| {
let arguments = [ let arguments = [
Some ( source . file_path . as_os_str () . to_owned () ) , Some(source.file_path.as_os_str().to_owned()),
source . audio_track . map ( source.audio_track.map(
|audio_track| concat_os_str ! ( "--mpv-aid=" , to_os_string (audio_track) ) , |audio_track| concat_os_str!("--mpv-aid=", to_os_string(audio_track)),
) , ),
source . subtitle_track . map ( source.subtitle_track.map(
|subtitle_track| concat_os_str ! ( "--mpv-sid=" , to_os_string (subtitle_track) ) , |subtitle_track| concat_os_str!("--mpv-sid=", to_os_string(subtitle_track)),
) , ),
] . iter () . filter_map ( Option :: clone ) . collect :: < Vec <_> > () ; ].iter().filter_map(Option::clone).collect::<Vec<_>>();
// TODO: Better error handling for UI callbacks in general // TODO: Better error handling for UI callbacks in general
Command :: new ("/usr/bin/celluloid") . args (arguments) . spawn () . unwrap () ; Command::new("/usr/bin/celluloid").args(arguments).spawn().unwrap();
} , },
set_child : Some ( & view_expr ! { set_child: Some(&view_expr! {
Image { set_icon_name : Some ("media-playback-start-symbolic") } Image { set_icon_name: Some("media-playback-start-symbolic") }
} ) , }),
} }
} ) , }),
} }
} ) , }),
} }
} }
} ) ; });
} ) ; });
let series_component = CollatableMediaContainer :: <SeriesAdapter> :: new ( |series| { let series_component = CollatableMediaContainer::<SeriesAdapter>::new(|series| {
view_expr ! { view_expr! {
Dialog { present : Some ( & window . libadwaita_window ) } Dialog { present: Some(&window.libadwaita_window) }
} ; };
} ) ; });
view ! { view! {
switched = ViewStack { switched = ViewStack {
add_titled_with_icon : ( films_component . get_widget () , None , "Films" , "camera-video-symbolic" ) , 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" ) , add_titled_with_icon: (series_component.get_widget(), None, "Series", "video-display-symbolic"),
} , },
header_bar = HeaderBar { header_bar = HeaderBar {
set_title_widget : Some ( & view_expr ! { set_title_widget: Some(&view_expr! {
ViewSwitcher { ViewSwitcher {
set_policy : Wide , set_policy: ViewSwitcherPolicy::Wide,
set_stack : Some ( & switched ) , set_stack: Some(&switched),
} }
} ) , }),
} , },
} }
window . libadwaita_window . set_content ( Some ( & view_expr ! { window.libadwaita_window.set_content(Some(&view_expr! {
ToolbarView { ToolbarView {
add_top_bar : & header_bar , add_top_bar: &header_bar,
set_content : Some ( & switched ) , set_content: Some(&switched),
} }
} ) ) ; }));
UI { films_component , series_component } UI {
} films_component,
series_component,
}
}
pub async fn render_collection_overview ( & self , collection : CollectionOverview ) { pub async fn render_collection_overview(&self, collection: CollectionOverview) {
join ! ( join!(
self . films_component . set_media ( collection . films ) , self.films_component.set_media(collection.films),
self . series_component . set_media ( collection . series ) , self.series_component.set_media(collection.series),
) ; );
} }
} }
pub struct Window { pub struct Window {
libadwaita_window : ApplicationWindow , libadwaita_window: ApplicationWindow,
} }
impl Window { impl Window {
pub fn new (application : & Application ) -> Self { pub fn new(application: &Application) -> Self {
let libadwaita_window = view_expr ! { let libadwaita_window = view_expr! {
ApplicationWindow { ApplicationWindow {
set_application : Some ( application ) , set_application: Some(application),
set_title : Some ( "Zoödex" ) , 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()
}
} }

View file

@ -1,66 +1,67 @@
use gtk4 :: Widget ; use gtk4::prelude::{BoxExt, IsA, OrientableExt, WidgetExt};
use gtk4 :: Orientation :: * ; use gtk4::{Orientation, Widget};
use gtk4 :: prelude :: * ; use libadwaita::Bin;
use libadwaita :: Bin ;
// Convenience function to conditionally append child to a widget // Convenience function to conditionally append child to a widget
pub trait OptChildExt { pub trait OptChildExt {
fn append_opt ( & self , child : & Option < impl IsA <Widget> > ) ; fn append_opt(&self, child: &Option<impl IsA<Widget>>);
} }
impl OptChildExt for gtk4 :: Box { impl OptChildExt for gtk4::Box {
fn append_opt ( & self , child : & Option < impl IsA <Widget> > ) { fn append_opt(&self, child: &Option<impl IsA<Widget>>) {
if let Some (child) = child { if let Some(child) = child {
self . append (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 ) * ) => { { macro_rules! view_expr {(
relm4_macros :: view ! { outer = $ ( $ contents ) * } $($contents: tt)*
outer ) => {{
} } } relm4_macros::view! { outer = $($contents)* }
outer
}}}
pub fn vertical_filler ( child : & impl IsA <Widget> ) -> gtk4 :: Box { pub fn vertical_filler(child: &impl IsA<Widget>) -> gtk4::Box {
view_expr ! { view_expr! {
gtk4 :: Box { gtk4::Box {
set_orientation : Vertical , set_orientation: Orientation::Vertical,
append : child , append: child,
append : & view_expr ! { append: &view_expr! {
Bin { set_vexpand : true } Bin { set_vexpand: true }
} , },
} }
} }
} }
macro_rules ! pango_attributes { ( macro_rules! pango_attributes {(
$ ( scale : $ scale : expr ) ? $(scale: $scale: expr)?
$ ( , weight : $ weight : expr $ (,) ? ) ? $(, weight: $weight: expr $(,)?)?
) => { { ) => {{
let attributes = gtk4 :: pango :: AttrList :: new () ; let attributes = gtk4::pango::AttrList::new();
# [ allow (unused_mut) ] let mut font_description = gtk4 :: pango :: FontDescription :: new () ; #[allow(unused_mut)]
let mut font_description = gtk4::pango::FontDescription::new();
$ ( attributes . insert ( gtk4 :: pango :: AttrFloat :: new_scale ( $ scale ) ) ; ) ? $(attributes.insert(gtk4::pango::AttrFloat::new_scale($scale));)?
$ ( font_description . set_weight ( $ weight ) ; ) ? $(font_description.set_weight($weight);)?
attributes . insert ( gtk4 :: pango :: AttrFontDesc :: new ( & font_description ) ) ; attributes.insert(gtk4::pango::AttrFontDesc::new(&font_description));
attributes attributes
} } } }}}
# [ allow (unused_imports) ] pub (crate) use { #[allow(unused_imports)]
pango_attributes , pub(crate) use {pango_attributes, view_expr};
view_expr ,
} ;

View file

@ -1,29 +1,29 @@
use std :: ffi :: * ; use std::ffi::OsString;
use std :: fmt :: * ; use std::fmt::Display;
macro_rules ! concat_os_str { ( macro_rules! concat_os_str {(
$ base : expr , $base: expr, $($suffix: expr),+
$ ( $ suffix : expr ) , + ) => {{
) => { { let mut base = std :: ffi :: OsString :: from ( $ base ) ;
let mut base = std :: ffi :: OsString :: from ( $ base ) ; $ ( base . push ( $ suffix ) ; ) +
$ ( base . push ( $ suffix ) ; ) + base
base }}}
} } }
pub fn leak < 'l , Type > ( inner : Type ) -> & 'l Type { pub fn leak<'l, Type>(inner: Type) -> &'l Type {
Box :: leak ( Box :: new (inner) ) Box::leak(Box::new(inner))
} }
pub fn leak_mut < 'l , Type > ( inner : Type ) -> & 'l mut Type { pub fn leak_mut<'l, Type>(inner: Type) -> &'l mut Type {
Box :: leak ( Box :: new (inner) ) Box::leak(Box::new(inner))
} }
pub fn to_os_string ( value : impl Display + Sized ) -> OsString { pub fn to_os_string(value: impl Display + Sized) -> OsString {
OsString :: from ( ToString :: to_string ( & value ) ) OsString::from(ToString::to_string(&value))
} }
# [ allow (unused_imports) ] pub (crate) use concat_os_str ; #[allow(unused_imports)]
pub(crate) use concat_os_str;