diff --git a/src/persist/common.rs b/src/persist/common.rs index 1ea4719..5a26a55 100644 --- a/src/persist/common.rs +++ b/src/persist/common.rs @@ -10,4 +10,24 @@ macro_rules! concat_os_str { +pub trait ResultExt { + async fn and_then_async(self, op: F) -> Result + where + F: AsyncFnOnce(T) -> Result; +} + +impl ResultExt for Result { + async fn and_then_async(self, op: F) -> Result + where + F: AsyncFnOnce(T) -> Result, + { + match self { + Ok(value) => op(value).await, + Err(error) => Err(error), + } + } +} + + + pub(crate) use concat_os_str; diff --git a/src/persist/data_manager.rs b/src/persist/data_manager.rs index 2e265a5..d4a6278 100644 --- a/src/persist/data_manager.rs +++ b/src/persist/data_manager.rs @@ -74,9 +74,7 @@ impl DataManager { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DataManagerError { NoHomeDir, - CannotOpenSharedDB, - UnknownSharedDBError, - CannotOpenLocalDB, - UnknownLocalDBError, + CannotOpenDB, + UnknownDBError, UnknownTextureError, } diff --git a/src/persist/sqlite_manager.rs b/src/persist/sqlite_manager.rs index 2599f44..f3ace8a 100644 --- a/src/persist/sqlite_manager.rs +++ b/src/persist/sqlite_manager.rs @@ -5,38 +5,29 @@ use std::fmt::{Debug, Formatter}; use async_sqlite::rusqlite::fallible_iterator::FallibleIterator; use async_sqlite::rusqlite::{OpenFlags, Row}; use async_sqlite::{Client, ClientBuilder, rusqlite}; -use tokio::try_join; -use crate::persist::common::concat_os_str; +use crate::persist::common::{ResultExt, concat_os_str}; use crate::persist::data_manager::DataManagerError; use crate::views::overview::{FilmOverview, SeriesOverview}; pub struct SqliteManager { - client_shared: Client, - client_local: Client, + client: Client, } impl SqliteManager { pub async fn new(data_dir: &OsStr) -> Result { - let (client_shared, client_local) = try_join!( - create_client(data_dir, DBType::Shared), - create_client(data_dir, DBType::Local), - )?; - - Ok(SqliteManager { - client_shared, - client_local, - }) + let client = create_client(data_dir).await?; + Ok(SqliteManager { client }) } // The order of the items is undefined. pub async fn films_overview(&self) -> Result, DataManagerError> { let overview = self - .client_shared + .client .conn(|connection| { - let overview = connection + connection .prepare( " select uuid, name, original_name, release_date, runtime_minutes @@ -47,15 +38,14 @@ impl SqliteManager { .query(()) .expect("parameters in films overview query should match those in its statement") .map(row_to_film_overview) - .collect()?; - Ok(overview) + .collect() }) .await; overview.map_err(|async_sqlite_error| match async_sqlite_error { - async_sqlite::Error::Closed => panic!( - "shared database connection should remain open as long as the application is running" - ), + async_sqlite::Error::Closed => { + panic!("database connection should remain open as long as the application is running") + } async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { rusqlite::Error::InvalidColumnIndex(_) => { panic!("column indices obtained from films overview query should exist") @@ -66,18 +56,18 @@ impl SqliteManager { rusqlite::Error::InvalidColumnType(..) => panic!( "values obtained from films overview query should have a type matching their column" ), - _ => DataManagerError::UnknownSharedDBError, + _ => DataManagerError::UnknownDBError, }, - _ => DataManagerError::UnknownSharedDBError, + _ => DataManagerError::UnknownDBError, }) } // The order of the items is undefined. pub async fn series_overview(&self) -> Result, DataManagerError> { let overview = self - .client_shared + .client .conn(|connection| { - let overview = connection + connection .prepare( " select series.uuid, series.name, series.original_name, @@ -91,15 +81,14 @@ impl SqliteManager { .query(()) .expect("parameters in series overview query should match those in its statement") .map(row_to_series_overview) - .collect()?; - Ok(overview) + .collect() }) .await; overview.map_err(|async_sqlite_error| match async_sqlite_error { - async_sqlite::Error::Closed => panic!( - "shared database connection should remain open as long as the application is running" - ), + async_sqlite::Error::Closed => { + panic!("database connection should remain open as long as the application is running") + } async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { rusqlite::Error::InvalidColumnIndex(_) => { panic!("column indices obtained from series overview query should exist") @@ -110,9 +99,9 @@ impl SqliteManager { rusqlite::Error::InvalidColumnType(..) => panic!( "values obtained from series overview query should have a type matching their column" ), - _ => DataManagerError::UnknownSharedDBError, + _ => DataManagerError::UnknownDBError, }, - _ => DataManagerError::UnknownSharedDBError, + _ => DataManagerError::UnknownDBError, }) } } @@ -123,39 +112,57 @@ impl Debug for SqliteManager { } } -async fn create_client(data_dir: &OsStr, db_type: DBType) -> Result { - let open_mode = match db_type { - DBType::Shared => OpenFlags::SQLITE_OPEN_READ_ONLY, - DBType::Local => OpenFlags::SQLITE_OPEN_READ_WRITE, - }; - let filename = match db_type { - DBType::Shared => "/shared.sqlite", - DBType::Local => "/local.sqlite", - }; - let cannot_open_err = match db_type { - DBType::Shared => DataManagerError::CannotOpenSharedDB, - DBType::Local => DataManagerError::CannotOpenLocalDB, - }; - let unknown_err = match db_type { - DBType::Shared => DataManagerError::UnknownSharedDBError, - DBType::Local => DataManagerError::UnknownLocalDBError, - }; - +async fn create_client(data_dir: &OsStr) -> Result { let client = ClientBuilder::new() - .path(concat_os_str!(data_dir, filename)) - .flags(open_mode | OpenFlags::SQLITE_OPEN_NO_MUTEX) + .path("") + .flags(OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX) .open() .await; + let data_dir = data_dir.to_os_string(); + let client = client + .and_then_async(async move |client| { + client + .conn(move |connection| { + let shared_path_os_str = concat_os_str!(&data_dir, "/shared.sqlite"); + let shared_path = shared_path_os_str + .to_str() + .expect("shared database path should be valid Unicode"); + connection + .execute("attach database :path as shared", &[(":path", shared_path)]) + .expect( + "shared database attaching statement should be valid SQL with matching parameters", + ); + + let local_path_os_str = concat_os_str!(&data_dir, "/local.sqlite"); + let local_path = local_path_os_str + .to_str() + .expect("local database path should be valid Unicode"); + connection + .execute("attach database :path as local", &[(":path", local_path)]) + .expect( + "local database attaching statement should be valid SQL with matching parameters", + ); + + Ok(()) + }) + .await + .map(|_| client) + }) + .await; + client.map_err(|async_sqlite_error| match async_sqlite_error { + async_sqlite::Error::Closed => { + panic!("database connection should remain open as long as the application is running") + } async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code { - rusqlite::ffi::ErrorCode::CannotOpen => cannot_open_err, - _ => unknown_err, + rusqlite::ffi::ErrorCode::CannotOpen => DataManagerError::CannotOpenDB, + _ => DataManagerError::UnknownDBError, }, - _ => unknown_err, + _ => DataManagerError::UnknownDBError, }, - _ => unknown_err, + _ => DataManagerError::UnknownDBError, }) } diff --git a/src/ui/components/media_details/film_details.rs b/src/ui/components/media_details/film_details.rs index a1e0e87..1638587 100644 --- a/src/ui/components/media_details/film_details.rs +++ b/src/ui/components/media_details/film_details.rs @@ -1,5 +1,5 @@ -use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt}; -use gtk4::{Label, Orientation}; +use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt}; +use gtk4::{Button, Label, Orientation}; use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component}; use crate::views::overview::FilmOverview; @@ -25,7 +25,12 @@ impl SimpleComponent for FilmDetails { Label { set_css_classes: &["title-1"], set_label: model.film_overview.name.as_str(), - } + }, + + Button { + set_css_classes: &["suggested-action", "circular"], + set_icon_name: "media-playback-start-symbolic", + }, } }