modde-ui 0.3.1

GUI application for modde
Documentation
use std::path::PathBuf;

use modde_core::profile::{ProfileManager, ReorderError, try_reorder};
use modde_core::resolver::GameId;

use super::{
    ExperimentWriteKind, ExperimentWriteOutcome, ProfileWriteOutcome, ReorderDirection,
    format_lock_reason,
};

pub(super) async fn create_profile(
    db: modde_core::db::ModdeDb,
    profile: modde_core::Profile,
) -> Result<ProfileWriteOutcome, String> {
    ProfileManager::with_db(db)
        .create(&profile)
        .await
        .map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some("Profile created".to_string()),
        reload: true,
    })
}

pub(super) async fn delete_profile(
    db: modde_core::db::ModdeDb,
    name: String,
) -> Result<ProfileWriteOutcome, String> {
    ProfileManager::with_db(db)
        .delete(&name, None)
        .await
        .map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some(format!("Profile '{name}' deleted")),
        reload: true,
    })
}

pub(super) async fn fork_profile(
    db: modde_core::db::ModdeDb,
    source: String,
    new_name: String,
    game_id: GameId,
) -> Result<ProfileWriteOutcome, String> {
    ProfileManager::with_db(db)
        .fork(&source, &new_name, &game_id)
        .await
        .map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some(format!("Profile forked as '{new_name}'")),
        reload: true,
    })
}

pub(super) async fn reorder_mod(
    db: modde_core::db::ModdeDb,
    profile_name: String,
    mod_id: String,
    direction: ReorderDirection,
) -> Result<ProfileWriteOutcome, String> {
    let pm = ProfileManager::with_db(db);
    let mut profile = pm
        .load(&profile_name, None)
        .await
        .map_err(|err| err.to_string())?;

    match try_reorder(&mut profile, &mod_id, direction) {
        Ok(()) => {
            pm.create_or_update(&profile)
                .await
                .map_err(|err| err.to_string())?;
            Ok(ProfileWriteOutcome {
                status_message: Some(format!(
                    "Moved '{mod_id}' {}",
                    match direction {
                        ReorderDirection::Up => "up",
                        ReorderDirection::Down => "down",
                    }
                )),
                reload: true,
            })
        }
        Err(ReorderError::ProfileLocked { reason }) => Ok(ProfileWriteOutcome {
            status_message: Some(format!(
                "Load order is locked by {} — unlock the profile to reorder.",
                format_lock_reason(&reason)
            )),
            reload: false,
        }),
        Err(ReorderError::ModPinned {
            mod_id: mid,
            reason,
        }) => Ok(ProfileWriteOutcome {
            status_message: Some(format!(
                "'{mid}' is pinned ({}) — unpin it to reorder.",
                format_lock_reason(&reason)
            )),
            reload: false,
        }),
        Err(ReorderError::AdjacentPinned { neighbor_id, .. }) => Ok(ProfileWriteOutcome {
            status_message: Some(format!("Cannot move past a pinned mod ('{neighbor_id}').")),
            reload: false,
        }),
        Err(ReorderError::ModNotFound { mod_id: mid }) => Ok(ProfileWriteOutcome {
            status_message: Some(format!("Mod not found in profile: {mid}")),
            reload: false,
        }),
        Err(ReorderError::AtBoundary) => Ok(ProfileWriteOutcome {
            status_message: None,
            reload: false,
        }),
    }
}

pub(super) async fn add_mod_to_profile(
    db: modde_core::db::ModdeDb,
    profile_name: String,
    mod_id: String,
) -> Result<ProfileWriteOutcome, String> {
    let pm = ProfileManager::with_db(db);
    let mut profile = pm
        .load(&profile_name, None)
        .await
        .map_err(|err| err.to_string())?;
    profile.mods.push(modde_core::EnabledMod {
        mod_id: mod_id.clone(),
        enabled: true,
        ..Default::default()
    });
    pm.create_or_update(&profile)
        .await
        .map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some(format!("Added mod: {mod_id}")),
        reload: true,
    })
}

pub(super) async fn remove_mod_from_profile(
    db: modde_core::db::ModdeDb,
    profile_name: String,
    index: usize,
) -> Result<ProfileWriteOutcome, String> {
    let pm = ProfileManager::with_db(db);
    let mut profile = pm
        .load(&profile_name, None)
        .await
        .map_err(|err| err.to_string())?;
    if index >= profile.mods.len() {
        return Ok(ProfileWriteOutcome {
            status_message: None,
            reload: false,
        });
    }
    let removed = profile.mods.remove(index);
    pm.create_or_update(&profile)
        .await
        .map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some(format!("Removed mod: {}", removed.mod_id)),
        reload: true,
    })
}

pub(super) async fn toggle_mod_enabled(
    db: modde_core::db::ModdeDb,
    profile_name: String,
    mod_id: String,
    enabled: bool,
) -> Result<ProfileWriteOutcome, String> {
    let pm = ProfileManager::with_db(db);
    let mut profile = pm
        .load(&profile_name, None)
        .await
        .map_err(|err| err.to_string())?;
    if let Some(mod_entry) = profile.mods.iter_mut().find(|entry| entry.mod_id == mod_id) {
        mod_entry.enabled = enabled;
    }
    pm.create_or_update(&profile)
        .await
        .map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some(format!(
            "Mod {mod_id} {}",
            if enabled { "enabled" } else { "disabled" }
        )),
        reload: true,
    })
}

pub(super) async fn set_mod_lock(
    db: modde_core::db::ModdeDb,
    profile_name: String,
    mod_id: String,
    locked: bool,
) -> Result<ProfileWriteOutcome, String> {
    let pm = ProfileManager::with_db(db);
    let mut profile = pm
        .load(&profile_name, None)
        .await
        .map_err(|err| err.to_string())?;
    let Some(mod_entry) = profile.mods.iter_mut().find(|entry| entry.mod_id == mod_id) else {
        return Ok(ProfileWriteOutcome {
            status_message: None,
            reload: false,
        });
    };

    mod_entry.lock = locked.then_some(modde_core::LockReason::Manual { note: None });
    pm.update(&profile).await.map_err(|err| err.to_string())?;
    Ok(ProfileWriteOutcome {
        status_message: Some(if locked {
            format!("Pinned '{mod_id}'")
        } else {
            format!("Unpinned '{mod_id}'")
        }),
        reload: true,
    })
}

pub(super) async fn run_experiment_write(
    db: modde_core::db::ModdeDb,
    kind: ExperimentWriteKind,
    profile_name: Option<String>,
    game_id: GameId,
    save_dir: Option<PathBuf>,
    current_depth: usize,
) -> Result<ExperimentWriteOutcome, String> {
    let pm = ProfileManager::with_db(db);
    match kind {
        ExperimentWriteKind::Try => {
            let profile_name =
                profile_name.ok_or_else(|| "No active profile selected".to_string())?;
            pm.try_profile(&profile_name, &game_id, save_dir.as_deref())
                .await
                .map_err(|err| err.to_string())?;
            let next_depth = current_depth.saturating_add(1);
            Ok(ExperimentWriteOutcome {
                previous_profile: None,
                status_message: format!("Experiment started (depth {next_depth})"),
                reload: true,
            })
        }
        ExperimentWriteKind::Rollback => {
            let previous_profile = pm
                .rollback(&game_id, save_dir.as_deref())
                .await
                .map_err(|err| err.to_string())?;
            Ok(ExperimentWriteOutcome {
                status_message: format!("Rolled back to '{previous_profile}'"),
                previous_profile: Some(previous_profile),
                reload: true,
            })
        }
        ExperimentWriteKind::Commit => {
            pm.commit(&game_id).await.map_err(|err| err.to_string())?;
            Ok(ExperimentWriteOutcome {
                previous_profile: None,
                status_message: "Experiment committed".to_string(),
                reload: true,
            })
        }
    }
}