Indicate which films have been watched, log unknown database errors

Also use "is" instead of "=" in SQL statements where sensible.
This commit is contained in:
Reinout Meliesie 2026-01-22 23:22:15 +01:00
commit a1835be5fe
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
7 changed files with 73 additions and 14 deletions

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 0.0625 8.375 c -0.0429688 0.164062 -0.0429688 0.339844 0 0.503906 c 0.941406 3.605469 4.199219 6.117188 7.9375 6.121094 c 3.742188 -0.003906 7.003906 -2.527344 7.9375 -6.140625 c 0.042969 -0.167969 0.042969 -0.339844 0 -0.503906 c -0.933594 -3.574219 -4.175781 -6.351563 -7.9375 -6.355469 c -3.769531 0.003906 -7.015625 2.792969 -7.9375 6.375 z m 13.9375 0.484375 v -0.5 c -0.703125 2.730469 -3.164062 4.636719 -6 4.640625 c -2.832031 -0.003906 -5.292969 -1.902344 -6 -4.625 v 0.5 c 0.707031 -2.742188 3.210938 -4.871094 6 -4.875 c 2.78125 0.003906 5.289062 2.121094 6 4.859375 z m 0 0"/><path d="m 11 8.5 c 0 1.65625 -1.34375 3 -3 3 s -3 -1.34375 -3 -3 s 1.34375 -3 3 -3 s 3 1.34375 3 3 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 864 B

View file

@ -2,5 +2,6 @@
<gresources> <gresources>
<gresource prefix="/com/kernelmaft/zoodex/"> <gresource prefix="/com/kernelmaft/zoodex/">
<file>application.css</file> <file>application.css</file>
<file>eye-outline-filled-symbolic.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View file

@ -8,7 +8,7 @@ use std::cmp::max;
use std::thread; use std::thread;
use gtk4::gdk::Display; use gtk4::gdk::Display;
use gtk4::{CssProvider, Settings, gio}; use gtk4::{CssProvider, IconTheme, Settings, gio};
use relm4::RelmApp; use relm4::RelmApp;
use crate::ui::App; use crate::ui::App;
@ -40,19 +40,23 @@ fn include_app_css() {
gio::resources_register_include!("zoodex.gresource") gio::resources_register_include!("zoodex.gresource")
.expect("CSS resource bundle should have valid format"); .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"); 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( gtk4::style_context_add_provider_for_display(
&display, &display,
&provider, &css_provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
); );
let settings = Settings::for_display(&display); 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| { 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");
} }

View file

@ -66,6 +66,12 @@ impl DataManager {
pub async fn poster(uuid: &str) -> Result<Option<Texture>, DataManagerError> { pub async fn poster(uuid: &str) -> Result<Option<Texture>, DataManagerError> {
fs_manager!().poster(uuid).await 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
}
} }

View file

@ -3,7 +3,7 @@ use std::fmt;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use async_sqlite::rusqlite::fallible_iterator::FallibleIterator; 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 async_sqlite::{Client, ClientBuilder, rusqlite};
use crate::persist::common::{ResultExt, concat_os_str}; use crate::persist::common::{ResultExt, concat_os_str};
@ -30,8 +30,10 @@ impl SqliteManager {
connection connection
.prepare( .prepare(
" "
select uuid, name, original_name, release_date, runtime_minutes select films.uuid, films.name, films.original_name, films.release_date, films.runtime_minutes,
from films coalesce(watched_status.watched, 0) as watched
from films left join watched_status
on films.uuid is watched_status.media_uuid
", ",
)? )?
.query(())? .query(())?
@ -53,7 +55,7 @@ impl SqliteManager {
select series.uuid, series.name, series.original_name, select series.uuid, series.name, series.original_name,
min(episodes.release_date) as first_release_date min(episodes.release_date) as first_release_date
from series, seasons, episodes 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 group by series.uuid
", ",
)? )?
@ -64,6 +66,31 @@ impl SqliteManager {
.await .await
.map_err(convert_error) .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 { impl Debug for SqliteManager {
@ -112,6 +139,7 @@ fn row_to_film_overview(row: &Row) -> rusqlite::Result<FilmOverview> {
let original_name = row.get("original_name")?; let original_name = row.get("original_name")?;
let release_date = row.get("release_date")?; let release_date = row.get("release_date")?;
let runtime = row.get("runtime_minutes")?; let runtime = row.get("runtime_minutes")?;
let watched = row.get("watched")?;
Ok(FilmOverview { Ok(FilmOverview {
uuid, uuid,
@ -119,6 +147,7 @@ fn row_to_film_overview(row: &Row) -> rusqlite::Result<FilmOverview> {
original_name, original_name,
release_date, release_date,
runtime, 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 { async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error {
rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code { rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code {
rusqlite::ffi::ErrorCode::CannotOpen => DataManagerError::CannotOpenDB, rusqlite::ffi::ErrorCode::CannotOpen => DataManagerError::CannotOpenDB,
_ => DataManagerError::UnknownDBError, _ => {
println!("{sqlite_error:?}");
DataManagerError::UnknownDBError
}
}, },
rusqlite::Error::InvalidColumnIndex(_) => { rusqlite::Error::InvalidColumnIndex(_) => {
panic!("column indices obtained from query should exist") 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 => { rusqlite::Error::MultipleStatement => {
panic!("multiple statements present when there should be one") panic!("multiple statements present when there should be one")
} }
_ => DataManagerError::UnknownDBError, _ => {
println!("{rusqlite_error:?}");
DataManagerError::UnknownDBError
}
}, },
_ => DataManagerError::UnknownDBError, _ => {
println!("{async_sqlite_error:?}");
DataManagerError::UnknownDBError
}
} }
} }

View file

@ -108,7 +108,14 @@ impl FactoryComponent for FilmGridItem {
#[name="runtime"] #[name="runtime"]
Label { Label {
set_label: format!("{} mins", self.film.runtime).as_str(), 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"),
},
}, },
}, },
} }

View file

@ -6,6 +6,7 @@ pub struct FilmOverview {
pub release_date: String, pub release_date: String,
// In minutes. // In minutes.
pub runtime: u32, pub runtime: u32,
pub watched: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]