From a1835be5fee496f6e43c284a6de7930ad1f1b0f5 Mon Sep 17 00:00:00 2001 From: Reinout Meliesie Date: Thu, 22 Jan 2026 23:22:15 +0100 Subject: [PATCH] Indicate which films have been watched, log unknown database errors Also use "is" instead of "=" in SQL statements where sensible. --- data/eye-outline-filled-symbolic.svg | 2 + data/zoodex.gresource.xml | 1 + src/main.rs | 16 +++--- src/persist/data_manager.rs | 6 +++ src/persist/sqlite_manager.rs | 52 ++++++++++++++++--- .../media_grid_item/film_grid_item.rs | 9 +++- src/views/overview.rs | 1 + 7 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 data/eye-outline-filled-symbolic.svg diff --git a/data/eye-outline-filled-symbolic.svg b/data/eye-outline-filled-symbolic.svg new file mode 100644 index 0000000..103af2f --- /dev/null +++ b/data/eye-outline-filled-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/zoodex.gresource.xml b/data/zoodex.gresource.xml index 8445e54..9d21967 100644 --- a/data/zoodex.gresource.xml +++ b/data/zoodex.gresource.xml @@ -2,5 +2,6 @@ application.css + eye-outline-filled-symbolic.svg diff --git a/src/main.rs b/src/main.rs index 6b69c7d..d936e26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use std::cmp::max; use std::thread; use gtk4::gdk::Display; -use gtk4::{CssProvider, Settings, gio}; +use gtk4::{CssProvider, IconTheme, Settings, gio}; use relm4::RelmApp; use crate::ui::App; @@ -40,19 +40,23 @@ fn include_app_css() { gio::resources_register_include!("zoodex.gresource") .expect("CSS resource bundle should have valid format"); - let provider = CssProvider::new(); let display = Display::default().expect("getting the default GDK4 display should never fail"); - provider.load_from_resource("/com/kernelmaft/zoodex/application.css"); + let css_provider = CssProvider::new(); + + css_provider.load_from_resource("/com/kernelmaft/zoodex/application.css"); gtk4::style_context_add_provider_for_display( &display, - &provider, + &css_provider, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, ); let settings = Settings::for_display(&display); - provider.set_prefers_color_scheme(settings.gtk_interface_color_scheme()); + css_provider.set_prefers_color_scheme(settings.gtk_interface_color_scheme()); settings.connect_gtk_interface_color_scheme_notify(move |settings| { - provider.set_prefers_color_scheme(settings.gtk_interface_color_scheme()); + css_provider.set_prefers_color_scheme(settings.gtk_interface_color_scheme()); }); + + let icon_theme = IconTheme::for_display(&display); + icon_theme.add_resource_path("/com/kernelmaft/zoodex"); } diff --git a/src/persist/data_manager.rs b/src/persist/data_manager.rs index c7e5c46..7154b38 100644 --- a/src/persist/data_manager.rs +++ b/src/persist/data_manager.rs @@ -66,6 +66,12 @@ impl DataManager { pub async fn poster(uuid: &str) -> Result, DataManagerError> { fs_manager!().poster(uuid).await } + + pub async fn set_film_watched_status(uuid: &str, watched: bool) -> Result<(), DataManagerError> { + sqlite_manager!() + .set_film_watched_status(uuid, watched) + .await + } } diff --git a/src/persist/sqlite_manager.rs b/src/persist/sqlite_manager.rs index 8286824..d41ee9d 100644 --- a/src/persist/sqlite_manager.rs +++ b/src/persist/sqlite_manager.rs @@ -3,7 +3,7 @@ use std::fmt; use std::fmt::{Debug, Formatter}; use async_sqlite::rusqlite::fallible_iterator::FallibleIterator; -use async_sqlite::rusqlite::{OpenFlags, Row}; +use async_sqlite::rusqlite::{OpenFlags, Row, named_params}; use async_sqlite::{Client, ClientBuilder, rusqlite}; use crate::persist::common::{ResultExt, concat_os_str}; @@ -30,8 +30,10 @@ impl SqliteManager { connection .prepare( " - select uuid, name, original_name, release_date, runtime_minutes - from films + select films.uuid, films.name, films.original_name, films.release_date, films.runtime_minutes, + coalesce(watched_status.watched, 0) as watched + from films left join watched_status + on films.uuid is watched_status.media_uuid ", )? .query(())? @@ -53,7 +55,7 @@ impl SqliteManager { select series.uuid, series.name, series.original_name, min(episodes.release_date) as first_release_date from series, seasons, episodes - where series.uuid = seasons.series and seasons.uuid = episodes.season + where series.uuid is seasons.series and seasons.uuid is episodes.season group by series.uuid ", )? @@ -64,6 +66,31 @@ impl SqliteManager { .await .map_err(convert_error) } + + pub async fn set_film_watched_status( + &self, + uuid: &str, + watched: bool, + ) -> Result<(), DataManagerError> { + self + .client + .conn({ + let uuid = uuid.to_string(); + move |connection| { + connection.execute( + " + update watched_status + set watched = :watched + where watched_status.media_uuid is :uuid + ", + named_params! { ":uuid": uuid, ":watched": watched }, + )?; + Ok(()) + } + }) + .await + .map_err(convert_error) + } } impl Debug for SqliteManager { @@ -112,6 +139,7 @@ fn row_to_film_overview(row: &Row) -> rusqlite::Result { let original_name = row.get("original_name")?; let release_date = row.get("release_date")?; let runtime = row.get("runtime_minutes")?; + let watched = row.get("watched")?; Ok(FilmOverview { uuid, @@ -119,6 +147,7 @@ fn row_to_film_overview(row: &Row) -> rusqlite::Result { original_name, release_date, runtime, + watched, }) } @@ -144,7 +173,10 @@ fn convert_error(async_sqlite_error: async_sqlite::Error) -> DataManagerError { async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code { rusqlite::ffi::ErrorCode::CannotOpen => DataManagerError::CannotOpenDB, - _ => DataManagerError::UnknownDBError, + _ => { + println!("{sqlite_error:?}"); + DataManagerError::UnknownDBError + } }, rusqlite::Error::InvalidColumnIndex(_) => { panic!("column indices obtained from query should exist") @@ -164,8 +196,14 @@ fn convert_error(async_sqlite_error: async_sqlite::Error) -> DataManagerError { rusqlite::Error::MultipleStatement => { panic!("multiple statements present when there should be one") } - _ => DataManagerError::UnknownDBError, + _ => { + println!("{rusqlite_error:?}"); + DataManagerError::UnknownDBError + } }, - _ => DataManagerError::UnknownDBError, + _ => { + println!("{async_sqlite_error:?}"); + DataManagerError::UnknownDBError + } } } diff --git a/src/ui/components/media_grid_item/film_grid_item.rs b/src/ui/components/media_grid_item/film_grid_item.rs index 74e786a..02ca9a9 100644 --- a/src/ui/components/media_grid_item/film_grid_item.rs +++ b/src/ui/components/media_grid_item/film_grid_item.rs @@ -108,7 +108,14 @@ impl FactoryComponent for FilmGridItem { #[name="runtime"] Label { set_label: format!("{} mins", self.film.runtime).as_str(), - } + }, + + #[name="watched"] + Image { + #[watch] + set_visible: self.film.watched, + set_icon_name: Some("eye-outline-filled-symbolic"), + }, }, }, } diff --git a/src/views/overview.rs b/src/views/overview.rs index 118a12d..853decd 100644 --- a/src/views/overview.rs +++ b/src/views/overview.rs @@ -6,6 +6,7 @@ pub struct FilmOverview { pub release_date: String, // In minutes. pub runtime: u32, + pub watched: bool, } #[derive(Clone, Debug)]