diff --git a/.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json b/.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json new file mode 100644 index 000000000..7031ab6c8 --- /dev/null +++ b/.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE cluster_artifacts SET seen_status = ? WHERE seen_status <> ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456" +} diff --git a/.sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json b/.sqlx/query-055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc.json similarity index 74% rename from .sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json rename to .sqlx/query-055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc.json index 0a86cb150..676324145 100644 --- a/.sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json +++ b/.sqlx/query-055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ?\n\t\t", + "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ?\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf" + "hash": "055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc" } diff --git a/packages/oneclient_db/.sqlx/query-b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af.json b/.sqlx/query-071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f.json similarity index 73% rename from packages/oneclient_db/.sqlx/query-b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af.json rename to .sqlx/query-071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f.json index c7ab17533..7ec4b4780 100644 --- a/packages/oneclient_db/.sqlx/query-b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af.json +++ b/.sqlx/query-071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t", + "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af" + "hash": "071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f" } diff --git a/.sqlx/query-24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51.json b/.sqlx/query-25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5.json similarity index 78% rename from .sqlx/query-24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51.json rename to .sqlx/query-25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5.json index f01cd4bca..0fdea98a5 100644 --- a/.sqlx/query-24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51.json +++ b/.sqlx/query-25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tINSERT INTO cluster_artifacts (cluster_id, hash, cluster_file_name, enabled)\n\t\tVALUES (?, ?, ?, 1)\n\t\tON CONFLICT(cluster_id, hash) DO UPDATE SET\n\t\t\tcluster_file_name = excluded.cluster_file_name\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled\n\t\t", + "query": "\n\t\tINSERT INTO cluster_artifacts (cluster_id, hash, cluster_file_name, enabled)\n\t\tVALUES (?, ?, ?, 1)\n\t\tON CONFLICT(cluster_id, hash) DO UPDATE SET\n\t\t\tcluster_file_name = excluded.cluster_file_name\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51" + "hash": "25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5" } diff --git a/packages/oneclient_db/.sqlx/query-bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca.json b/.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json similarity index 67% rename from packages/oneclient_db/.sqlx/query-bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca.json rename to .sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json index b79fa7195..11ce2107f 100644 --- a/packages/oneclient_db/.sqlx/query-bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca.json +++ b/.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled\n\t\tFROM cluster_artifacts ca\n\t\tJOIN provider_releases pr ON pr.hash = ca.hash\n\t\tWHERE ca.cluster_id = ?\n\t\t\tAND pr.provider = ?\n\t\t\tAND pr.project_id = ?\n\t\t\tAND ca.hash <> ?\n\t\t", + "query": "\n\t\tSELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled, ca.seen_status\n\t\tFROM cluster_artifacts ca\n\t\tJOIN provider_releases pr ON pr.hash = ca.hash\n\t\tWHERE ca.cluster_id = ?\n\t\t\tAND pr.provider = ?\n\t\t\tAND pr.project_id = ?\n\t\t\tAND ca.hash <> ?\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca" + "hash": "25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c" } diff --git a/packages/oneclient_db/.sqlx/query-4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f.json b/.sqlx/query-b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16.json similarity index 75% rename from packages/oneclient_db/.sqlx/query-4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f.json rename to .sqlx/query-b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16.json index 5eb036f5f..e229a717f 100644 --- a/packages/oneclient_db/.sqlx/query-4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f.json +++ b/.sqlx/query-b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET cluster_file_name = ?, enabled = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled\n\t\t", + "query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET cluster_file_name = ?, enabled = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f" + "hash": "b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16" } diff --git a/.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json b/.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json new file mode 100644 index 000000000..d265164b8 --- /dev/null +++ b/.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET seen_status = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713" +} diff --git a/packages/oneclient_app/src/components/package_row.rs b/packages/oneclient_app/src/components/package_row.rs index 65c0047b2..440eefdb0 100644 --- a/packages/oneclient_app/src/components/package_row.rs +++ b/packages/oneclient_app/src/components/package_row.rs @@ -1,6 +1,7 @@ use freya::prelude::*; use freya::router::RouterContext; use oneclient_content::packages::ProviderId; +use oneclient_core::SeenStatus; use crate::components::{Icon, IconType, toggle_controlled}; use crate::hooks::{ClusterAction, loaded_image, use_cached_image, use_cluster_mutation}; @@ -44,10 +45,9 @@ pub struct PackageEntry { pub installed: bool, pub hash: Option, pub manifest_default: bool, - /// Private bundle dependency only set for bundle rows pub hidden: bool, - /// Only set for browser-installed content bundle packages use the bundle update flow pub update_available: bool, + pub seen_status: SeenStatus, } impl PackageEntry { @@ -63,6 +63,14 @@ impl PackageEntry { pub fn is_outdated(&self) -> bool { self.update_available && self.is_remote() && !self.in_bundle() } + + pub fn recency_badge(&self) -> Option { + match self.seen_status { + SeenStatus::New => Some(new_badge()), + SeenStatus::Updated => Some(updated_badge()), + SeenStatus::Seen => None, + } + } } #[derive(PartialEq)] @@ -278,7 +286,8 @@ fn grid_card( .cross_align(Alignment::Center) .spacing(6.) .child(badge) - .maybe_child(item.is_outdated().then(outdated_badge)), + .maybe_child(item.is_outdated().then(outdated_badge)) + .maybe_child(item.recency_badge()), ), ) .into_element(); @@ -378,7 +387,8 @@ fn package_info( } else { local_badge() }) - .maybe_child(item.is_outdated().then(outdated_badge)), + .maybe_child(item.is_outdated().then(outdated_badge)) + .maybe_child(item.recency_badge()), ) .maybe(!item.author.is_empty(), |el| { el.child( @@ -469,6 +479,28 @@ fn outdated_badge() -> Element { ) } +fn new_badge() -> Element { + accent_badge( + Icon::new(IconType::Plus) + .size(12.) + .color(colors::success()) + .into_element(), + "New".to_string(), + colors::success(), + ) +} + +fn updated_badge() -> Element { + accent_badge( + Icon::new(IconType::RefreshCcw02) + .size(12.) + .color(colors::success()) + .into_element(), + "Updated".to_string(), + colors::success(), + ) +} + fn local_badge() -> Element { badge( Icon::new(IconType::File02) diff --git a/packages/oneclient_app/src/events.rs b/packages/oneclient_app/src/events.rs index 1b7e09a99..5201177f9 100644 --- a/packages/oneclient_app/src/events.rs +++ b/packages/oneclient_app/src/events.rs @@ -304,6 +304,16 @@ pub async fn start_launcher( ) -> Result<(), anyhow::Error> { let state = crate::launcher::install(oneclient_core::LauncherState::new(events).await?); + match oneclient_content::packages::PackageStore::retire_seen_badges(&state.services.content()) + .await + { + Ok(cleared) if cleared > 0 => { + tracing::debug!(cleared, "retired package badges from the previous session"); + } + Ok(_) => {} + Err(err) => tracing::warn!(%err, "failed to retire package badges"), + } + oneclient_net::status::start(state.services.requester.clone()); oneclient_polyplus::start(std::sync::Arc::clone(&state.auth)); oneclient_core::run_startup_tasks(&state); diff --git a/packages/oneclient_app/src/install.rs b/packages/oneclient_app/src/install.rs index 62961989a..16310ea52 100644 --- a/packages/oneclient_app/src/install.rs +++ b/packages/oneclient_app/src/install.rs @@ -301,7 +301,10 @@ pub async fn install_package( child.finish(); match result { - Ok(_) => installed_dependencies.push(dependency.project.name.clone()), + Ok(artifact) => { + mark_new(cluster_id, &artifact.hash, state).await; + installed_dependencies.push(dependency.project.name.clone()); + } Err(err) => { tracing::warn!( dependency = %dependency.project.name, @@ -333,6 +336,10 @@ pub async fn install_package( child.finish(); + if let Ok(artifact) = &result { + mark_new(cluster_id, &artifact.hash, state).await; + } + PackageInstall { session_id: Some(session.detach()), result: result.map(|_| project.name).map_err(anyhow::Error::from), @@ -340,3 +347,11 @@ pub async fn install_package( missing_dependencies, } } + +async fn mark_new(cluster_id: i64, hash: &str, state: &LauncherState) { + if let Err(err) = + PackageStore::mark_artifact_new(cluster_id, hash, &state.services.content()).await + { + tracing::warn!(cluster_id, hash, %err, "failed to badge package as new"); + } +} diff --git a/packages/oneclient_app/src/view/app/browser/mod.rs b/packages/oneclient_app/src/view/app/browser/mod.rs index 6264cda49..fde529ae6 100644 --- a/packages/oneclient_app/src/view/app/browser/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/mod.rs @@ -397,6 +397,7 @@ mod tests { display_version: None, provider: Some(ProviderId::Modrinth), published_at: None, + seen_status: oneclient_core::SeenStatus::Seen, } } diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs index 22587ce66..81e4abedc 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs @@ -243,6 +243,9 @@ fn make_row( hash: installed_info.map(|i| i.hash.clone()), update_available, hidden, + seen_status: installed_info + .map(|i| i.seen_status) + .unwrap_or_default(), } } diff --git a/packages/oneclient_content/src/bundles/updates.rs b/packages/oneclient_content/src/bundles/updates.rs index 944fd547a..fbe5ca296 100644 --- a/packages/oneclient_content/src/bundles/updates.rs +++ b/packages/oneclient_content/src/bundles/updates.rs @@ -6,7 +6,9 @@ use oneclient_db::dao::artifact as artifact_dao; use oneclient_db::dao::cluster as cluster_dao; use oneclient_db::dao::cluster_bundle as bundle_dao; use oneclient_db::models::ClusterPatch; -use oneclient_db::models::{BundleTrackedArtifactRow, ClusterBundleOverrideRow, OverrideType}; +use oneclient_db::models::{ + BundleTrackedArtifactRow, ClusterBundleOverrideRow, OverrideType, SeenStatus, +}; use tokio::sync::Mutex as AsyncMutex; use futures_util::StreamExt; @@ -564,6 +566,7 @@ async fn reconcile_update( let suppression = find_user_suppression(overrides, &file_id); let enabled = !disable_was_deliberate(update.new_file.hidden, suppression); set_artifact_enabled_to(update.cluster_id, hash, enabled, ctx).await?; + artifact_dao::set_seen_status(&ctx.db, update.cluster_id, hash, SeenStatus::Updated).await?; if hash == update.installed_hash { return Ok(()); @@ -582,7 +585,11 @@ async fn reconcile_addition( let file_id = addition.new_file.kind.package_id(); let suppression = find_user_suppression(overrides, &file_id); let enabled = !disable_was_deliberate(addition.new_file.hidden, suppression); - set_artifact_enabled_to(addition.cluster_id, hash, enabled, ctx).await + set_artifact_enabled_to(addition.cluster_id, hash, enabled, ctx).await?; + + artifact_dao::set_seen_status(&ctx.db, addition.cluster_id, hash, SeenStatus::New).await?; + + Ok(()) } #[tracing::instrument(level = "debug", skip(bundles, ctx))] diff --git a/packages/oneclient_content/src/packages/activity.rs b/packages/oneclient_content/src/packages/activity.rs index 6e0862d25..3ed1e2f0b 100644 --- a/packages/oneclient_content/src/packages/activity.rs +++ b/packages/oneclient_content/src/packages/activity.rs @@ -102,6 +102,7 @@ fn newest<'a>(copies: impl IntoIterator) -> Option { mod tests { use super::*; use oneclient_common::domain::ContentType; + use oneclient_db::models::SeenStatus; fn info( project_id: Option<&str>, @@ -121,6 +122,7 @@ mod tests { display_version: None, provider: project_id.map(|_| ProviderId::Modrinth), published_at: published_at.map(Into::into), + seen_status: SeenStatus::Seen, } } diff --git a/packages/oneclient_content/src/packages/store/mod.rs b/packages/oneclient_content/src/packages/store/mod.rs index 5362f5b72..6c519084d 100644 --- a/packages/oneclient_content/src/packages/store/mod.rs +++ b/packages/oneclient_content/src/packages/store/mod.rs @@ -13,7 +13,7 @@ pub use link::{link_or_copy, remove_entry, try_unlink_materialized}; pub use paths::{artifact_absolute_path, cache_file_path, relative_cache_path}; use oneclient_db::dao::{artifact as artifact_dao, cluster as cluster_dao}; -use oneclient_db::models::{ArtifactRow, ClusterRow}; +use oneclient_db::models::{ArtifactRow, ClusterRow, SeenStatus}; use oneclient_common::domain::{ContentType, GameLoader, ProviderId}; use super::error::PackageError; @@ -157,6 +157,7 @@ impl PackageStore { let content_type = ContentType::from_repr(artifact.content_type as u8) .unwrap_or(ContentType::Mod); let release = artifact_dao::get_release_by_hash(&ctx.db, &link.hash).await?; + let seen_status = link.status(); items.push(LinkedArtifactInfo { hash: link.hash, @@ -172,12 +173,28 @@ impl PackageStore { .as_ref() .and_then(|r| ProviderId::from_repr(r.provider as u8)), published_at: release.as_ref().and_then(|r| r.published_at.clone()), + seen_status, }); } Ok(items) } + #[tracing::instrument(level = "debug", skip(ctx))] + pub async fn mark_artifact_new( + cluster_id: i64, + hash: &str, + ctx: &ContentCtx, + ) -> ContentResult<()> { + artifact_dao::set_seen_status(&ctx.db, cluster_id, hash, SeenStatus::New).await?; + Ok(()) + } + + #[tracing::instrument(level = "debug", skip(ctx))] + pub async fn retire_seen_badges(ctx: &ContentCtx) -> ContentResult { + Ok(artifact_dao::mark_all_seen(&ctx.db).await?) + } + #[tracing::instrument(level = "debug", skip(ctx))] /// Only the flag is recorded /// the folder is rewritten at the next launch since a running game holds diff --git a/packages/oneclient_content/src/packages/types.rs b/packages/oneclient_content/src/packages/types.rs index 168427fb3..b5457a81e 100644 --- a/packages/oneclient_content/src/packages/types.rs +++ b/packages/oneclient_content/src/packages/types.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use oneclient_common::domain::{ContentType, GameLoader, ProviderId}; use oneclient_common::search::normalize_query; +use oneclient_db::models::SeenStatus; pub const DEFAULT_PAGE_SIZE: usize = 24; @@ -235,4 +236,6 @@ pub struct LinkedArtifactInfo { pub provider: Option, /// Provider publish time RFC 3339 pub published_at: Option, + /// Drives the new/updated badge in the package manager + pub seen_status: SeenStatus, } diff --git a/packages/oneclient_content/src/packages/updates.rs b/packages/oneclient_content/src/packages/updates.rs index c70d2cc67..e41e0465c 100644 --- a/packages/oneclient_content/src/packages/updates.rs +++ b/packages/oneclient_content/src/packages/updates.rs @@ -13,7 +13,7 @@ use oneclient_common::domain::ProviderId; use oneclient_db::dao::artifact as artifact_dao; use oneclient_db::dao::browser_package_update as update_dao; use oneclient_db::dao::cluster_bundle as bundle_dao; -use oneclient_db::models::{BrowserPackageUpdateRow, ClusterRow}; +use oneclient_db::models::{BrowserPackageUpdateRow, ClusterRow, SeenStatus}; use oneclient_events::GroupedProgressChild; use crate::ctx::ContentCtx; @@ -360,6 +360,14 @@ pub async fn apply_browser_package_update( PackageStore::set_artifact_enabled(update.cluster_id, &installed.hash, ctx).await?; } + artifact_dao::set_seen_status( + &ctx.db, + update.cluster_id, + &installed.hash, + SeenStatus::Updated, + ) + .await?; + update_dao::delete(&ctx.db, update.cluster_id, &update.hash).await?; Ok(installed.hash) @@ -457,6 +465,7 @@ mod tests { display_version: None, provider, published_at: None, + seen_status: SeenStatus::Seen, } } diff --git a/packages/oneclient_core/src/lib.rs b/packages/oneclient_core/src/lib.rs index 2cc161d29..4657e9809 100644 --- a/packages/oneclient_core/src/lib.rs +++ b/packages/oneclient_core/src/lib.rs @@ -55,6 +55,7 @@ pub use oneclient_cluster::screenshots::{ ScreenshotInfo, ScreenshotsError, delete_screenshot, list_cluster_screenshots, load_screenshot, }; pub use oneclient_content::packages::LinkedArtifactInfo; +pub use oneclient_db::models::SeenStatus; pub use oneclient_content::packages::updates::{ BrowserPackageUpdate, BrowserUpdateCheck, apply_browser_package_update, cached_browser_package_updates, check_browser_package_updates, diff --git a/packages/oneclient_db/.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json b/packages/oneclient_db/.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json new file mode 100644 index 000000000..7031ab6c8 --- /dev/null +++ b/packages/oneclient_db/.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE cluster_artifacts SET seen_status = ? WHERE seen_status <> ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456" +} diff --git a/packages/oneclient_db/.sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json b/packages/oneclient_db/.sqlx/query-055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc.json similarity index 74% rename from packages/oneclient_db/.sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json rename to packages/oneclient_db/.sqlx/query-055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc.json index 0a86cb150..676324145 100644 --- a/packages/oneclient_db/.sqlx/query-e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf.json +++ b/packages/oneclient_db/.sqlx/query-055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ?\n\t\t", + "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ?\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "e372eec993f6e82238fe95b1c74dbb377ee08e44c7e8c3b5eaa6f3b8b229fddf" + "hash": "055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc" } diff --git a/.sqlx/query-b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af.json b/packages/oneclient_db/.sqlx/query-071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f.json similarity index 73% rename from .sqlx/query-b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af.json rename to packages/oneclient_db/.sqlx/query-071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f.json index c7ab17533..7ec4b4780 100644 --- a/.sqlx/query-b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af.json +++ b/packages/oneclient_db/.sqlx/query-071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t", + "query": "\n\t\tSELECT cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\tFROM cluster_artifacts\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "b5935e0e5d751a7d33e898f7cc1e0f985a66434365f0939f7572434af40e43af" + "hash": "071d02aba0d2eb073b8ca1e6488087101255147c064612fad99500d3c993f61f" } diff --git a/packages/oneclient_db/.sqlx/query-24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51.json b/packages/oneclient_db/.sqlx/query-25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5.json similarity index 78% rename from packages/oneclient_db/.sqlx/query-24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51.json rename to packages/oneclient_db/.sqlx/query-25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5.json index f01cd4bca..0fdea98a5 100644 --- a/packages/oneclient_db/.sqlx/query-24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51.json +++ b/packages/oneclient_db/.sqlx/query-25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tINSERT INTO cluster_artifacts (cluster_id, hash, cluster_file_name, enabled)\n\t\tVALUES (?, ?, ?, 1)\n\t\tON CONFLICT(cluster_id, hash) DO UPDATE SET\n\t\t\tcluster_file_name = excluded.cluster_file_name\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled\n\t\t", + "query": "\n\t\tINSERT INTO cluster_artifacts (cluster_id, hash, cluster_file_name, enabled)\n\t\tVALUES (?, ?, ?, 1)\n\t\tON CONFLICT(cluster_id, hash) DO UPDATE SET\n\t\t\tcluster_file_name = excluded.cluster_file_name\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "24209a44c9a51e8a318b080aa0c79a00a6002a35b47ec3723e3aa6a327cb7f51" + "hash": "25259e9d6eda7f5113e0d88f00b24b28b98de1eda40a1e25d48bbcf212af42b5" } diff --git a/.sqlx/query-bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca.json b/packages/oneclient_db/.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json similarity index 67% rename from .sqlx/query-bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca.json rename to packages/oneclient_db/.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json index b79fa7195..11ce2107f 100644 --- a/.sqlx/query-bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca.json +++ b/packages/oneclient_db/.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tSELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled\n\t\tFROM cluster_artifacts ca\n\t\tJOIN provider_releases pr ON pr.hash = ca.hash\n\t\tWHERE ca.cluster_id = ?\n\t\t\tAND pr.provider = ?\n\t\t\tAND pr.project_id = ?\n\t\t\tAND ca.hash <> ?\n\t\t", + "query": "\n\t\tSELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled, ca.seen_status\n\t\tFROM cluster_artifacts ca\n\t\tJOIN provider_releases pr ON pr.hash = ca.hash\n\t\tWHERE ca.cluster_id = ?\n\t\t\tAND pr.provider = ?\n\t\t\tAND pr.project_id = ?\n\t\t\tAND ca.hash <> ?\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca" + "hash": "25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c" } diff --git a/.sqlx/query-4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f.json b/packages/oneclient_db/.sqlx/query-b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16.json similarity index 75% rename from .sqlx/query-4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f.json rename to packages/oneclient_db/.sqlx/query-b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16.json index 5eb036f5f..e229a717f 100644 --- a/.sqlx/query-4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f.json +++ b/packages/oneclient_db/.sqlx/query-b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET cluster_file_name = ?, enabled = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled\n\t\t", + "query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET cluster_file_name = ?, enabled = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\tRETURNING cluster_id, hash, cluster_file_name, enabled, seen_status\n\t\t", "describe": { "columns": [ { @@ -46,6 +46,17 @@ "name": "enabled" } } + }, + { + "name": "seen_status", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "cluster_artifacts", + "name": "seen_status" + } + } } ], "parameters": { @@ -55,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "4f209d73ce580a5e305b6ccdbe47ce139b331506d1c53ed0f4011596c810252f" + "hash": "b68194fc40d9fdd508ab6cc354fff662ba4b7ca17c1c4be3e51e30fcb3d0cb16" } diff --git a/packages/oneclient_db/.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json b/packages/oneclient_db/.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json new file mode 100644 index 000000000..d265164b8 --- /dev/null +++ b/packages/oneclient_db/.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\tUPDATE cluster_artifacts\n\t\tSET seen_status = ?\n\t\tWHERE cluster_id = ? AND hash = ?\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713" +} diff --git a/packages/oneclient_db/migrations/20260817130000_cluster_artifact_seen_status.sql b/packages/oneclient_db/migrations/20260817130000_cluster_artifact_seen_status.sql new file mode 100644 index 000000000..89e610224 --- /dev/null +++ b/packages/oneclient_db/migrations/20260817130000_cluster_artifact_seen_status.sql @@ -0,0 +1,19 @@ +-- Marks a linked artifact as newly installed or freshly updated, so the package +-- manager can badge it. The status rides on `cluster_artifacts` rather than a +-- side table because that row *is* the identity of "this version of this mod in +-- this cluster": an update installs a new hash and unlinks the old one, so the +-- status can never outlive the version it describes, and removing the mod or +-- the cluster takes it with them. +-- +-- Values are the `SeenStatus` discriminants: 0 = New, 1 = Updated, 2 = Seen. +-- +-- The default is `Seen`, not `New`. Every artifact already linked when this +-- migration runs, and every artifact laid down by a cluster's first install, +-- must start unbadged -- otherwise a freshly created cluster would light up +-- every single mod. Only the update flows promote a row out of `Seen`. +-- +-- The launcher resets every row back to `Seen` on startup, before it applies +-- pending updates, so a badge survives exactly until the next launch. +ALTER TABLE cluster_artifacts ADD COLUMN seen_status INTEGER NOT NULL DEFAULT 2; + +CREATE INDEX cluster_artifacts_seen_status_idx ON cluster_artifacts (cluster_id, seen_status); diff --git a/packages/oneclient_db/src/dao/artifact.rs b/packages/oneclient_db/src/dao/artifact.rs index f81606709..e69a6ea84 100644 --- a/packages/oneclient_db/src/dao/artifact.rs +++ b/packages/oneclient_db/src/dao/artifact.rs @@ -1,6 +1,6 @@ use sqlx::SqlitePool; -use crate::models::{ArtifactRow, ClusterArtifactRow, ProviderReleaseRow}; +use crate::models::{ArtifactRow, ClusterArtifactRow, ProviderReleaseRow, SeenStatus}; pub async fn get_artifact_by_hash( pool: &SqlitePool, @@ -192,7 +192,7 @@ pub async fn link_cluster_artifact( VALUES (?, ?, ?, 1) ON CONFLICT(cluster_id, hash) DO UPDATE SET cluster_file_name = excluded.cluster_file_name - RETURNING cluster_id, hash, cluster_file_name, enabled + RETURNING cluster_id, hash, cluster_file_name, enabled, seen_status "#, cluster_id, hash, @@ -214,7 +214,7 @@ pub async fn list_cluster_artifacts_for_project( sqlx::query_as!( ClusterArtifactRow, r#" - SELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled + SELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled, ca.seen_status FROM cluster_artifacts ca JOIN provider_releases pr ON pr.hash = ca.hash WHERE ca.cluster_id = ? @@ -270,7 +270,7 @@ pub async fn list_cluster_artifacts( sqlx::query_as!( ClusterArtifactRow, r#" - SELECT cluster_id, hash, cluster_file_name, enabled + SELECT cluster_id, hash, cluster_file_name, enabled, seen_status FROM cluster_artifacts WHERE cluster_id = ? "#, @@ -288,7 +288,7 @@ pub async fn get_cluster_artifact( sqlx::query_as!( ClusterArtifactRow, r#" - SELECT cluster_id, hash, cluster_file_name, enabled + SELECT cluster_id, hash, cluster_file_name, enabled, seen_status FROM cluster_artifacts WHERE cluster_id = ? AND hash = ? "#, @@ -312,7 +312,7 @@ pub async fn update_cluster_artifact( UPDATE cluster_artifacts SET cluster_file_name = ?, enabled = ? WHERE cluster_id = ? AND hash = ? - RETURNING cluster_id, hash, cluster_file_name, enabled + RETURNING cluster_id, hash, cluster_file_name, enabled, seen_status "#, cluster_file_name, enabled, @@ -322,3 +322,41 @@ pub async fn update_cluster_artifact( .fetch_one(pool) .await } + +pub async fn set_seen_status( + pool: &SqlitePool, + cluster_id: i64, + hash: &str, + status: SeenStatus, +) -> Result<(), sqlx::Error> { + let status = status.as_i64(); + + sqlx::query!( + r#" + UPDATE cluster_artifacts + SET seen_status = ? + WHERE cluster_id = ? AND hash = ? + "#, + status, + cluster_id, + hash + ) + .execute(pool) + .await?; + + Ok(()) +} + +pub async fn mark_all_seen(pool: &SqlitePool) -> Result { + let seen = SeenStatus::Seen.as_i64(); + + let result = sqlx::query!( + "UPDATE cluster_artifacts SET seen_status = ? WHERE seen_status <> ?", + seen, + seen + ) + .execute(pool) + .await?; + + Ok(result.rows_affected()) +} diff --git a/packages/oneclient_db/src/models/artifact.rs b/packages/oneclient_db/src/models/artifact.rs index 7734f472a..a0e6e2efd 100644 --- a/packages/oneclient_db/src/models/artifact.rs +++ b/packages/oneclient_db/src/models/artifact.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use sqlx::FromRow; #[derive(Debug, Clone, FromRow)] @@ -22,10 +23,41 @@ pub struct ProviderReleaseRow { pub mc_loaders: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[repr(i64)] +pub enum SeenStatus { + New = 0, + Updated = 1, + #[default] + Seen = 2, +} + +impl SeenStatus { + pub fn as_i64(self) -> i64 { + self as i64 + } + + pub fn from_repr(value: i64) -> Option { + match value { + 0 => Some(Self::New), + 1 => Some(Self::Updated), + 2 => Some(Self::Seen), + _ => None, + } + } +} + #[derive(Debug, Clone, FromRow)] pub struct ClusterArtifactRow { pub cluster_id: i64, pub hash: String, pub cluster_file_name: String, pub enabled: i64, + pub seen_status: i64, +} + +impl ClusterArtifactRow { + pub fn status(&self) -> SeenStatus { + SeenStatus::from_repr(self.seen_status).unwrap_or_default() + } } diff --git a/packages/oneclient_db/src/models/mod.rs b/packages/oneclient_db/src/models/mod.rs index f34ef93b3..9542d5ad1 100644 --- a/packages/oneclient_db/src/models/mod.rs +++ b/packages/oneclient_db/src/models/mod.rs @@ -8,7 +8,7 @@ mod java; mod package_metadata; mod setting_profile; -pub use artifact::{ArtifactRow, ClusterArtifactRow, ProviderReleaseRow}; +pub use artifact::{ArtifactRow, ClusterArtifactRow, ProviderReleaseRow, SeenStatus}; pub use browser_package_update::BrowserPackageUpdateRow; pub use package_metadata::PackageMetadataRow; pub use bundle::{BundleRow, NewBundle};