feat: added New/Updated badges to cluster package lists (#734)
This commit is contained in:
parent
159e1c7e96
commit
4aaf974f04
21 changed files with 378 additions and 23 deletions
12
.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json
generated
Normal file
12
.sqlx/query-011448f2ce79e981700ee75c8cd5db5e0fcb793b72671c20a925b73ee6fb9456.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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 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,17 +46,29 @@
|
|||
"name": "enabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seen_status",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_artifacts",
|
||||
"name": "seen_status"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca"
|
||||
"hash": "055b35798b158025888aba14bab7c8f5111c1786202f2399f6383d290abfe9cc"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
74
.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json
generated
Normal file
74
.sqlx/query-25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c.json
generated
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"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": [
|
||||
{
|
||||
"name": "cluster_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_artifacts",
|
||||
"name": "cluster_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hash",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_artifacts",
|
||||
"name": "hash"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cluster_file_name",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_artifacts",
|
||||
"name": "cluster_file_name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_artifacts",
|
||||
"name": "enabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seen_status",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_artifacts",
|
||||
"name": "seen_status"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "25f4eb2e9040bb8f1c9a38ca41a773225099f150c3fc8a41d31fd40c09711d0c"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
12
.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json
generated
Normal file
12
.sqlx/query-cd9609d707fa11bc4c246cd69c879159d23af897bdcb0bd9f04df73a7805a713.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -48,6 +49,8 @@ pub struct PackageEntry {
|
|||
pub hidden: bool,
|
||||
/// Only set for browser-installed content bundle packages use the bundle update flow
|
||||
pub update_available: bool,
|
||||
/// Recency badge state cleared once the user views the list
|
||||
pub seen_status: SeenStatus,
|
||||
}
|
||||
|
||||
impl PackageEntry {
|
||||
|
|
@ -63,6 +66,14 @@ impl PackageEntry {
|
|||
pub fn is_outdated(&self) -> bool {
|
||||
self.update_available && self.is_remote() && !self.in_bundle()
|
||||
}
|
||||
|
||||
pub fn recency_badge(&self) -> Option<Element> {
|
||||
match self.seen_status {
|
||||
SeenStatus::New => Some(new_badge()),
|
||||
SeenStatus::Updated => Some(updated_badge()),
|
||||
SeenStatus::Seen => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -274,10 +285,13 @@ fn grid_card(
|
|||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.content(Content::wrap_spacing(4.))
|
||||
.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();
|
||||
|
|
@ -374,7 +388,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(
|
||||
|
|
@ -465,6 +480,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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,6 +397,7 @@ mod tests {
|
|||
display_version: None,
|
||||
provider: Some(ProviderId::Modrinth),
|
||||
published_at: None,
|
||||
seen_status: oneclient_core::SeenStatus::Seen,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -376,6 +379,26 @@ impl Component for PackageManager {
|
|||
let cluster_id = self.cluster_id;
|
||||
let content_type = self.content_type;
|
||||
|
||||
// Cleared once on mount so the rows already rendered keep their badges for this visit
|
||||
use_hook(|| {
|
||||
spawn_forever(async move {
|
||||
let Ok(state) = crate::launcher::state() else {
|
||||
return;
|
||||
};
|
||||
match oneclient_content::packages::PackageStore::retire_seen_badges(
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(cleared) if cleared > 0 => {
|
||||
tracing::debug!(cleared, "retired package badges after the list was viewed");
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => tracing::warn!(%err, "failed to retire package badges"),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Minecraft reads its content once at startup so a toggle now cannot reach the running session
|
||||
let session_live = use_game_snapshot().is_active(cluster_id);
|
||||
let active = use_state(|| 0usize);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ use oneclient_db::dao::cluster as cluster_dao;
|
|||
use oneclient_db::dao::bundle as bundle_catalog_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;
|
||||
|
|
@ -584,6 +586,8 @@ async fn reconcile_update(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
artifact_dao::set_seen_status(&ctx.db, update.cluster_id, hash, SeenStatus::Updated).await?;
|
||||
|
||||
remove_artifact_from_cluster(update.cluster_id, &update.installed_hash, false, ctx).await
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +601,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))]
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ fn newest<'a>(copies: impl IntoIterator<Item = &'a Copy>) -> Option<String> {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<u64> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<ProviderId>,
|
||||
/// Provider publish time RFC 3339
|
||||
pub published_at: Option<String>,
|
||||
/// Drives the new/updated badge in the package manager
|
||||
pub seen_status: SeenStatus,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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,45 @@ 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();
|
||||
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE cluster_artifacts
|
||||
SET seen_status = ?
|
||||
WHERE cluster_id = ? AND hash = ?
|
||||
"#,
|
||||
status,
|
||||
cluster_id,
|
||||
hash
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
tracing::warn!(cluster_id, hash, "no cluster artifact matched the seen status update");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_all_seen(pool: &SqlitePool) -> Result<u64, sqlx::Error> {
|
||||
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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue