fix: ux changes for sync settings/overrides

This commit is contained in:
Calum H. (IMB11)
2026-08-28 10:07:40 +01:00
parent a9b774005b
commit 98aadb1f6a
17 changed files with 934 additions and 513 deletions
@@ -0,0 +1,15 @@
UPDATE sync_feature_settings
SET globally_enabled = 0, new_instance_default = 1
WHERE feature IN (
'command_history',
'multiplayer_servers',
'creative_hotbars'
);
UPDATE instance_sync_preferences
SET enabled = 1
WHERE feature IN (
'command_history',
'multiplayer_servers',
'creative_hotbars'
);
@@ -67,9 +67,14 @@ pub async fn list_screenshots(
pub async fn list_synced_screenshots() -> crate::Result<Vec<InstanceScreenshot>>
{
if !super::super::synced_options::get_global_options()
.await?
.screenshots
{
return Ok(Vec::new());
}
let state = State::get().await?;
let sources =
instance_rows::list_synced_screenshot_sources(&state.pool).await?;
let sources = instance_rows::list_screenshot_sources(&state.pool).await?;
list_source_screenshot_sets(&state, sources).await
}
@@ -288,47 +288,28 @@ async fn version_capability(
pub async fn set_global_option(
option: SyncedOption,
enabled: bool,
base_instance_id: Option<&str>,
) -> crate::Result<GlobalSyncedOptions> {
let state = State::get().await?;
let _guard = state.lock_synced_options().await;
let reset_participation =
enabled && !canonical_exists(option, &state).await?;
let option_name = option.as_str();
sqlx::query!(
"
INSERT INTO sync_feature_settings
(feature, globally_enabled, new_instance_default)
VALUES (?, ?, 1)
ON CONFLICT(feature) DO UPDATE SET
globally_enabled = excluded.globally_enabled
",
option_name,
enabled,
)
.execute(&state.pool)
.await?;
if enabled && option != SyncedOption::Screenshots {
let base_instance_id = base_instance_id.ok_or_else(|| {
ErrorKind::InputError(
"Choose an instance to use as the sync source.".to_string(),
)
})?;
return enable_global_option_from_base(
option,
base_instance_id,
&state,
)
.await;
}
set_global_option_enabled(option, enabled, &state).await?;
let instances = crate::state::list_instances(&state.pool).await?;
if reset_participation {
for metadata in instances {
if instance_option_enabled(&metadata, option) {
instance_rows::set_instance_sync_preference(
&metadata.instance.id,
option,
false,
&state.pool,
)
.await?;
}
if !sync_files_are_protected(&metadata)
&& !instance_is_running(&metadata, &state).await?
{
detach_option(&metadata, option, &state).await?;
}
}
return get_global_options_with_state(&state).await;
}
for metadata in instances {
if sync_files_are_protected(&metadata)
|| instance_is_running(&metadata, &state).await?
@@ -353,6 +334,112 @@ pub async fn set_global_option(
get_global_options_with_state(&state).await
}
async fn set_global_option_enabled(
option: SyncedOption,
enabled: bool,
state: &State,
) -> crate::Result<()> {
let option_name = option.as_str();
sqlx::query!(
"
INSERT INTO sync_feature_settings
(feature, globally_enabled, new_instance_default)
VALUES (?, ?, 1)
ON CONFLICT(feature) DO UPDATE SET
globally_enabled = excluded.globally_enabled
",
option_name,
enabled,
)
.execute(&state.pool)
.await?;
Ok(())
}
async fn enable_global_option_from_base(
option: SyncedOption,
base_instance_id: &str,
state: &State,
) -> crate::Result<GlobalSyncedOptions> {
let source = crate::state::get_instance(base_instance_id, &state.pool)
.await?
.ok_or_else(|| {
ErrorKind::InputError("Unknown sync source instance.".to_string())
})?;
if sync_files_are_protected(&source)
|| instance_is_running(&source, state).await?
{
return Err(ErrorKind::InputError(
"Close the source instance before using it for syncing."
.to_string(),
)
.into());
}
match capability_status(&source, option, true, state).await {
CapabilityStatus::Supported => {}
CapabilityStatus::Unsupported(reason)
| CapabilityStatus::Indeterminate(reason) => {
return Err(ErrorKind::InputError(reason).into());
}
}
let instances = crate::state::list_instances(&state.pool).await?;
for metadata in &instances {
if instance_option_enabled(metadata, option)
&& (sync_files_are_protected(metadata)
|| instance_is_running(metadata, state).await?)
{
return Err(ErrorKind::InputError(
"Close all instances using this synced setting before choosing a new sync source."
.to_string(),
)
.into());
}
}
for metadata in &instances {
if instance_option_enabled(metadata, option) {
detach_option(metadata, option, state).await?;
}
}
if !instance_option_enabled(&source, option) {
detach_option(&source, option, state).await?;
}
seed_from_instance(&source, option, state).await?;
instance_rows::set_instance_sync_preference(
base_instance_id,
option,
true,
&state.pool,
)
.await?;
set_global_option_enabled(option, true, state).await?;
for metadata in crate::state::list_instances(&state.pool).await? {
if sync_files_are_protected(&metadata)
|| instance_is_running(&metadata, state).await?
{
continue;
}
if !instance_option_enabled(&metadata, option) {
detach_option(&metadata, option, state).await?;
continue;
}
match capability_status(&metadata, option, true, state).await {
CapabilityStatus::Supported => {
ensure_option(&metadata, option, state).await?
}
CapabilityStatus::Unsupported(_) => {
detach_option(&metadata, option, state).await?
}
CapabilityStatus::Indeterminate(_) => {}
}
}
get_global_options_with_state(state).await
}
pub async fn set_instance_option(
instance_id: &str,
option: SyncedOption,
@@ -172,7 +172,48 @@ pub(in crate::api::instance) async fn detach_servers(
) -> crate::Result<()> {
let generated = generated_path(state, &metadata.instance.id);
let local = instance_dir(metadata, state).join(SERVERS_FILE);
detach_link(&generated, &local).await
let Some(current_checkpoint) = checkpoint(
&metadata.instance.id,
SyncedOption::MultiplayerServers,
"default",
state,
)
.await?
else {
return detach_link(&generated, &local).await;
};
let linked_to_generated = tokio::fs::symlink_metadata(&local)
.await
.is_ok_and(|metadata| metadata.file_type().is_symlink())
&& tokio::fs::read_link(&local)
.await
.is_ok_and(|target| target == generated);
let matches_checkpoint = local.exists()
&& sha1_file(&local).await? == current_checkpoint.expected_sha1;
if current_checkpoint.status != CheckpointStatus::Ready
|| (!linked_to_generated && !matches_checkpoint)
{
return detach_link(&generated, &local).await;
}
let current = read_servers(&local).await?;
let projections =
load_projection_entries(&metadata.instance.id, state).await?;
let projection_matches = match_projection_entries(&current, &projections);
let instance_servers = current
.into_iter()
.zip(projection_matches)
.filter_map(|(server, projection)| {
projection
.is_none_or(|projection| {
projection.owner == ProjectionOwner::Instance
})
.then_some(server)
})
.collect::<Vec<_>>();
detach_link(&generated, &local).await?;
write_servers(&local, &instance_servers).await
}
pub(in crate::api::instance) async fn reconcile_servers(
@@ -494,32 +494,6 @@ pub(crate) async fn get_instance_screenshot_source(
Ok(source)
}
pub(crate) async fn list_synced_screenshot_sources(
pool: &SqlitePool,
) -> crate::Result<Vec<InstanceScreenshotSource>> {
let sources = sqlx::query_as!(
InstanceScreenshotSource,
"
SELECT instances.id, instances.name, instances.path
FROM instances
INNER JOIN instance_sync_preferences preferences
ON preferences.instance_id = instances.id
WHERE preferences.feature = 'screenshots'
AND preferences.enabled = 1
AND EXISTS (
SELECT 1
FROM sync_feature_settings
WHERE feature = 'screenshots' AND globally_enabled = 1
)
ORDER BY instances.name, instances.id
",
)
.fetch_all(pool)
.await?;
Ok(sources)
}
pub(crate) async fn list_screenshot_sources(
pool: &SqlitePool,
) -> crate::Result<Vec<InstanceScreenshotSource>> {