Use cargo fmt and conform to Style Guide (mostly)
This commit is contained in:
parent
1f1eac6369
commit
2982a13bc2
15 changed files with 980 additions and 888 deletions
|
|
@ -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
|
|
||||||
16
Cargo.toml
16
Cargo.toml
|
|
@ -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
18
rustfmt.toml
Normal 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
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ) ,
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
44
src/error.rs
44
src/error.rs
|
|
@ -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;
|
||||||
|
|
|
||||||
113
src/main.rs
113
src/main.rs
|
|
@ -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();
|
||||||
} ,
|
},
|
||||||
) ;
|
);
|
||||||
} ) ;
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
] )
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
249
src/ui/mod.rs
249
src/ui/mod.rs
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ,
|
|
||||||
} ;
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue