feat: don't re-provision clusters the user deleted
This commit is contained in:
parent
13e52ab3ec
commit
51608f7d94
6 changed files with 171 additions and 10 deletions
|
|
@ -68,8 +68,9 @@ impl Component for ClusterSettings {
|
|||
let versions = loader_versions(&versions_query);
|
||||
let runtimes = java_runtimes(&runtimes_query);
|
||||
|
||||
let instance_row = cluster.user_created.then(|| {
|
||||
let instance_row = {
|
||||
InstanceRow {
|
||||
editable: cluster.user_created,
|
||||
facts: InstanceFacts {
|
||||
cluster_id,
|
||||
name: cluster.name.clone(),
|
||||
|
|
@ -83,7 +84,7 @@ impl Component for ClusterSettings {
|
|||
},
|
||||
}
|
||||
.into_element()
|
||||
});
|
||||
};
|
||||
|
||||
let mod_loader = !cluster.lacks_mod_loader();
|
||||
let loader_section: Vec<Element> = if mod_loader {
|
||||
|
|
@ -124,7 +125,7 @@ impl Component for ClusterSettings {
|
|||
.scrollbar_gutter(true)
|
||||
.spacing(4.)
|
||||
.child(section_header("INSTANCE"))
|
||||
.append_children(instance_row)
|
||||
.child(instance_row)
|
||||
.child(
|
||||
DedicatedDirRow {
|
||||
cluster_id,
|
||||
|
|
@ -311,6 +312,7 @@ impl Component for ToggleRow {
|
|||
|
||||
#[derive(PartialEq)]
|
||||
struct InstanceRow {
|
||||
editable: bool,
|
||||
facts: InstanceFacts,
|
||||
}
|
||||
|
||||
|
|
@ -322,16 +324,24 @@ impl Component for InstanceRow {
|
|||
let name = self.facts.name.clone();
|
||||
let facts = self.facts.clone();
|
||||
|
||||
let editable = self.editable;
|
||||
let description = if editable {
|
||||
"Change this instance's name, description, tags and cover image, or remove it from your list."
|
||||
} else {
|
||||
"Remove this version from your list."
|
||||
};
|
||||
|
||||
let buttons = rect()
|
||||
.horizontal()
|
||||
.spacing(8.)
|
||||
.child(
|
||||
.maybe_child(editable.then(|| {
|
||||
Button::new()
|
||||
.small()
|
||||
.secondary()
|
||||
.on_press(move |_| editing.set(true))
|
||||
.text("Edit"),
|
||||
)
|
||||
.text("Edit")
|
||||
.into_element()
|
||||
}))
|
||||
.child(
|
||||
Button::new()
|
||||
.small()
|
||||
|
|
@ -346,12 +356,11 @@ impl Component for InstanceRow {
|
|||
.child(settings_row(
|
||||
IconType::Pencil01,
|
||||
"Instance Details",
|
||||
"Change this instance's name, description, tags and cover image, or remove it from your list.",
|
||||
description,
|
||||
buttons,
|
||||
))
|
||||
.maybe_child(editing.read().then(|| {
|
||||
EditInstanceModal::new(facts.clone(), move |()| editing.set(false))
|
||||
.into_element()
|
||||
EditInstanceModal::new(facts.clone(), move |()| editing.set(false)).into_element()
|
||||
}))
|
||||
.maybe_child(deleting.read().then(|| {
|
||||
DeleteInstanceModal::new(cluster_id, name.clone(), move |()| deleting.set(false))
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ impl Component for DeleteInstanceModal {
|
|||
let game = use_game_snapshot();
|
||||
let cluster_id = self.cluster_id;
|
||||
let running = game.is_running(cluster_id);
|
||||
let dedicated = use_cluster(cluster_id).is_none_or(|c| c.uses_dedicated_dir());
|
||||
let cluster = use_cluster(cluster_id);
|
||||
let dedicated = cluster.as_ref().is_none_or(|c| c.uses_dedicated_dir());
|
||||
let provisioned = cluster.as_ref().is_some_and(|c| !c.user_created);
|
||||
let warning = if dedicated {
|
||||
format!(
|
||||
"{} and its files will be deleted, including its worlds, settings and installed content. This cannot be undone.",
|
||||
|
|
@ -82,6 +84,13 @@ impl Component for DeleteInstanceModal {
|
|||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.maybe_child(provisioned.then(|| {
|
||||
label()
|
||||
.text("It won't be added back automatically. To play this version again, create a OneClient instance for it.")
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary())
|
||||
.into_element()
|
||||
}))
|
||||
.maybe_child(running.then(|| {
|
||||
label()
|
||||
.text("Close the game before deleting this instance.")
|
||||
|
|
|
|||
|
|
@ -115,6 +115,12 @@ impl ClusterManager {
|
|||
)
|
||||
.await?
|
||||
.is_some()
|
||||
|| cluster_dao::is_provision_dismissed(
|
||||
&self.db,
|
||||
&options.mc_version,
|
||||
options.mc_loader as i64,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
|
@ -388,6 +394,15 @@ impl ClusterManager {
|
|||
pub async fn delete(&self, cluster_id: ClusterId, remove_files: bool) -> ClusterResult<()> {
|
||||
let cluster = self.get(cluster_id).await?;
|
||||
|
||||
if !cluster.user_created {
|
||||
cluster_dao::dismiss_provision(
|
||||
&self.db,
|
||||
&cluster.mc_version,
|
||||
cluster.mc_loader as i64,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let trashed = if remove_files && cluster.is_isolated() {
|
||||
move_to_trash(&cluster).await
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,16 @@ async fn apply_rule(state: &LauncherState, rule: &RemoteMigration) -> LauncherRe
|
|||
return Ok(false);
|
||||
};
|
||||
|
||||
if cluster_dao::is_provision_dismissed(db, &to.mc_version, to.loader as i64).await? {
|
||||
tracing::info!(
|
||||
migration_id = %rule.id,
|
||||
to = %to.mc_version,
|
||||
to_loader = %to.loader,
|
||||
"target version was dismissed; skipping migration"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if cluster_dao::find_by_version_loader(db, &to.mc_version, to.loader as i64)
|
||||
.await?
|
||||
.is_some()
|
||||
|
|
@ -432,6 +442,78 @@ mod tests {
|
|||
use super::*;
|
||||
use oneclient_common::domain::GameLoader;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[tokio::test]
|
||||
async fn remote_migration_does_not_restore_a_deleted_target() {
|
||||
use crate::clusters::CreateClusterOptions;
|
||||
use crate::versions::{MigrationSource, MigrationTarget};
|
||||
|
||||
let state = crate::dev::ephemeral_state().await.unwrap();
|
||||
let global = state.settings.read().global_game_settings.clone();
|
||||
let mut clusters = Vec::new();
|
||||
for version in ["26.1", "26.1.2"] {
|
||||
clusters.push(
|
||||
state
|
||||
.clusters
|
||||
.create_provisioned(
|
||||
&global,
|
||||
CreateClusterOptions::new(
|
||||
format!("{version} Fabric"),
|
||||
version,
|
||||
GameLoader::Fabric,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
let source = &clusters[0];
|
||||
let target = &clusters[1];
|
||||
let marker = source.dir().unwrap().join("keep.txt");
|
||||
polyio::write(&marker, b"keep").await.unwrap();
|
||||
state.clusters.delete(target.id, true).await.unwrap();
|
||||
|
||||
let mut rule = RemoteMigration {
|
||||
id: "dismissed-target".to_string(),
|
||||
from: MigrationSource {
|
||||
mc_version: source.mc_version.clone(),
|
||||
loader: "fabric".to_string(),
|
||||
},
|
||||
to: MigrationTarget {
|
||||
mc_version: target.mc_version.clone(),
|
||||
loader: None,
|
||||
},
|
||||
allow_without_bundles: false,
|
||||
};
|
||||
|
||||
for loader in [None, Some("fabric".to_string())] {
|
||||
rule.to.loader = loader;
|
||||
assert!(!apply_rule(&state, &rule).await.unwrap());
|
||||
let unchanged = state.clusters.get(source.id).await.unwrap();
|
||||
assert_eq!(unchanged.mc_version, source.mc_version);
|
||||
assert_eq!(unchanged.folder_name, source.folder_name);
|
||||
assert_eq!(polyio::read(&marker).await.unwrap(), b"keep");
|
||||
assert!(
|
||||
cluster_dao::find_by_version_loader(
|
||||
&state.services.db,
|
||||
&target.mc_version,
|
||||
target.mc_loader as i64,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
rule.to.mc_version = "26.1.3".to_string();
|
||||
assert!(apply_rule(&state, &rule).await.unwrap());
|
||||
assert_eq!(
|
||||
state.clusters.get(source.id).await.unwrap().mc_version,
|
||||
"26.1.3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retargets_generated_folder() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE dismissed_provisions (
|
||||
mc_version TEXT NOT NULL,
|
||||
mc_loader INTEGER NOT NULL,
|
||||
PRIMARY KEY (mc_version, mc_loader)
|
||||
);
|
||||
|
|
@ -86,6 +86,34 @@ pub async fn find_by_version_loader(
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn dismiss_provision(
|
||||
pool: &SqlitePool,
|
||||
mc_version: &str,
|
||||
mc_loader: i64,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query("INSERT OR IGNORE INTO dismissed_provisions (mc_version, mc_loader) VALUES (?, ?)")
|
||||
.bind(mc_version)
|
||||
.bind(mc_loader)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_provision_dismissed(
|
||||
pool: &SqlitePool,
|
||||
mc_version: &str,
|
||||
mc_loader: i64,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query_scalar::<_, i64>(
|
||||
"SELECT 1 FROM dismissed_provisions WHERE mc_version = ? AND mc_loader = ?",
|
||||
)
|
||||
.bind(mc_version)
|
||||
.bind(mc_loader)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map(|row| row.is_some())
|
||||
}
|
||||
|
||||
pub async fn insert(pool: &SqlitePool, new: &NewCluster<'_>) -> Result<ClusterRow, sqlx::Error> {
|
||||
let created_at = Utc::now().to_rfc3339();
|
||||
|
||||
|
|
@ -317,6 +345,19 @@ mod tests {
|
|||
.expect("insert cluster")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dismissed_provision_is_remembered_per_version_and_loader() {
|
||||
let pool = pool().await;
|
||||
assert!(!is_provision_dismissed(&pool, "26.1", 1).await.unwrap());
|
||||
|
||||
dismiss_provision(&pool, "26.1", 1).await.unwrap();
|
||||
dismiss_provision(&pool, "26.1", 1).await.unwrap();
|
||||
|
||||
assert!(is_provision_dismissed(&pool, "26.1", 1).await.unwrap());
|
||||
assert!(!is_provision_dismissed(&pool, "26.1", 2).await.unwrap());
|
||||
assert!(!is_provision_dismissed(&pool, "26.2", 1).await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_version_rewrites_identity_and_keeps_history() {
|
||||
let pool = pool().await;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue