diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index e481214..06c8c5c 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -544,3 +544,13 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result Ok(info) } + +#[tracing::instrument(skip(state))] +pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { + let mod_dir = state.get_mod_dir().join(info.get_id()); + fs::remove_dir_all(&mod_dir) + .await + .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; + + Ok(()) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index c640350..a738fef 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -17,10 +17,12 @@ use druid::AppLauncher; use druid::ExtEventSink; use druid::SingleUse; use druid::Target; +use engine::delete_mod; use engine::import_mod; use serde::Deserialize; use serde::Serialize; use state::ACTION_FINISH_ADD_MOD; +use state::ACTION_FINISH_DELETE_SELECTED_MOD; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; @@ -83,6 +85,26 @@ fn work_thread( } } }), + AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { + if let Err(err) = delete_mod(state, &info).await { + tracing::error!( + "Failed to delete mod files. \ + You might want to clean up the data directory manually. \ + Reason: {:?}", + err + ); + } + + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_DELETE_SELECTED_MOD, + SingleUse::new(info), + Target::Auto, + ) + .expect("failed to send command"); + }), }; } }); diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index ecf6c2e..107b3aa 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -3,12 +3,14 @@ use druid::widget::{ Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, TextBox, ViewSwitcher, }; -use druid::{lens, FileDialogOptions, FileSpec, Insets, LensExt, Widget, WidgetExt, WindowDesc}; +use druid::{ + lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc, +}; -use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_ADD_MOD}; +use crate::state::{ModInfo, PathBufFormatter, State, View}; use crate::state::{ - ACTION_DELETE_SELECTED_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, - ACTION_SELECT_MOD, ACTION_START_DEPLOY, + ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, + ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, }; use crate::theme; use crate::widget::ExtraWidgetExt; @@ -163,7 +165,13 @@ fn build_mod_details() -> impl Widget { }); let button_delete_mod = Button::new("Delete Mod") - .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_DELETE_SELECTED_MOD)) + .on_click(|ctx, data: &mut Option, _env| { + if let Some(info) = data { + ctx.submit_command( + ACTION_START_DELETE_SELECTED_MOD.with(SingleUse::new(info.clone())), + ); + } + }) .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index f90b1d8..93afd61 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -16,8 +16,10 @@ pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); -pub(crate) const ACTION_DELETE_SELECTED_MOD: Selector = - Selector::new("dtmm.action.delete-selected-mod"); +pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector> = + Selector::new("dtmm.action.srart-delete-selected-mod"); +pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector> = + Selector::new("dtmm.action.finish-delete-selected-mod"); pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); @@ -284,6 +286,7 @@ impl Lens, Vector<(usize, T)>> for IndexedVectorLens { pub(crate) enum AsyncAction { DeployMods(State), AddMod((State, FileInfo)), + DeleteMod((State, ModInfo)), } pub(crate) struct Delegate { @@ -360,8 +363,34 @@ impl AppDelegate for Delegate { state.selected_mod_index = Some(i + 1); Handled::Yes } - cmd if cmd.is(ACTION_DELETE_SELECTED_MOD) => { - let Some(index) = state.selected_mod_index else { + cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { + let info = cmd + .get(ACTION_FINISH_DELETE_SELECTED_MOD) + .and_then(|info| info.take()) + .expect("command type matched but didn't contain the expected value"); + if self + .sender + .send(AsyncAction::DeleteMod((state.clone(), info))) + .is_ok() + { + state.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { + let info = cmd + .get(ACTION_FINISH_DELETE_SELECTED_MOD) + .and_then(|info| info.take()) + .expect("command type matched but didn't contain the expected value"); + let mods = state.get_mods(); + let found = mods + .iter() + .enumerate() + .find(|(_, i)| i.get_id() == info.get_id()); + let Some((index, _)) = found else { return Handled::No; };