440 lines
16 KiB
Rust
440 lines
16 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use color_eyre::Report;
|
|
use druid::im::Vector;
|
|
use druid::{
|
|
AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target,
|
|
WindowHandle, WindowId,
|
|
};
|
|
use tokio::sync::mpsc::UnboundedSender;
|
|
|
|
use crate::ui::window;
|
|
use crate::util::ansi::ansi_to_rich_text;
|
|
use crate::util::config::Config;
|
|
|
|
use super::{ModInfo, State};
|
|
|
|
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
|
|
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_START_DELETE_SELECTED_MOD: Selector<SingleUse<Arc<ModInfo>>> =
|
|
Selector::new("dtmm.action.srart-delete-selected-mod");
|
|
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<Arc<ModInfo>>> =
|
|
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");
|
|
|
|
pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector =
|
|
Selector::new("dtmm.action.start-reset-deployment");
|
|
pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector =
|
|
Selector::new("dtmm.action.finish-reset-deployment");
|
|
|
|
pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action.add-mod");
|
|
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
|
|
Selector::new("dtmm.action.finish-add-mod");
|
|
|
|
pub(crate) const ACTION_LOG: Selector<SingleUse<Vec<u8>>> = Selector::new("dtmm.action.log");
|
|
|
|
pub(crate) const ACTION_START_SAVE_SETTINGS: Selector =
|
|
Selector::new("dtmm.action.start-save-settings");
|
|
pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
|
|
Selector::new("dtmm.action.finish-save-settings");
|
|
|
|
pub(crate) const ACTION_START_CHECK_UPDATE: Selector =
|
|
Selector::new("dtmm.action.start-check-update");
|
|
pub(crate) const ACTION_FINISH_CHECK_UPDATE: Selector<SingleUse<Vec<ModInfo>>> =
|
|
Selector::new("dtmm.action.finish-check-update");
|
|
|
|
pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty");
|
|
|
|
pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
|
|
Selector::new("dtmm.action.show-error-dialog");
|
|
|
|
pub(crate) const ACTION_SET_WINDOW_HANDLE: Selector<SingleUse<(WindowId, WindowHandle)>> =
|
|
Selector::new("dtmm.action.set-window-handle");
|
|
|
|
pub(crate) type InitialLoadResult = (Config, Vector<Arc<ModInfo>>);
|
|
pub(crate) const ACTION_FINISH_LOAD_INITIAL: Selector<SingleUse<Option<InitialLoadResult>>> =
|
|
Selector::new("dtmm.action.finish-load-initial");
|
|
|
|
// A sub-selection of `State`'s fields that are required in `AsyncAction`s and that are
|
|
// `Send + Sync`
|
|
pub(crate) struct ActionState {
|
|
pub mods: Vector<Arc<ModInfo>>,
|
|
pub game_dir: Arc<PathBuf>,
|
|
pub data_dir: Arc<PathBuf>,
|
|
pub mod_dir: Arc<PathBuf>,
|
|
pub config_path: Arc<PathBuf>,
|
|
pub ctx: Arc<sdk::Context>,
|
|
pub nexus_api_key: Arc<String>,
|
|
pub is_io_enabled: bool,
|
|
}
|
|
|
|
impl From<State> for ActionState {
|
|
fn from(state: State) -> Self {
|
|
Self {
|
|
mods: state.mods,
|
|
game_dir: state.game_dir,
|
|
mod_dir: Arc::new(state.data_dir.join("mods")),
|
|
data_dir: state.data_dir,
|
|
config_path: state.config_path,
|
|
ctx: state.ctx,
|
|
nexus_api_key: state.nexus_api_key,
|
|
is_io_enabled: state.is_io_enabled,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) enum AsyncAction {
|
|
DeployMods(ActionState),
|
|
ResetDeployment(ActionState),
|
|
AddMod(ActionState, FileInfo),
|
|
DeleteMod(ActionState, Arc<ModInfo>),
|
|
SaveSettings(ActionState),
|
|
CheckUpdates(ActionState),
|
|
LoadInitial((PathBuf, bool)),
|
|
Log((ActionState, Vec<u8>)),
|
|
}
|
|
|
|
impl std::fmt::Debug for AsyncAction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
AsyncAction::DeployMods(_) => write!(f, "AsyncAction::DeployMods(_state)"),
|
|
AsyncAction::ResetDeployment(_) => write!(f, "AsyncAction::ResetDeployment(_state)"),
|
|
AsyncAction::AddMod(_, info) => write!(f, "AsyncAction::AddMod(_state, {:?})", info),
|
|
AsyncAction::DeleteMod(_, info) => {
|
|
write!(f, "AsyncAction::DeleteMod(_state, {:?})", info)
|
|
}
|
|
AsyncAction::SaveSettings(_) => write!(f, "AsyncAction::SaveSettings(_state)"),
|
|
AsyncAction::CheckUpdates(_) => write!(f, "AsyncAction::CheckUpdates(_state)"),
|
|
AsyncAction::LoadInitial((path, is_default)) => write!(
|
|
f,
|
|
"AsyncAction::LoadInitial(({:?}, {:?}))",
|
|
path, is_default
|
|
),
|
|
AsyncAction::Log(_) => write!(f, "AsyncAction::Log(_)"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct Delegate {
|
|
sender: UnboundedSender<AsyncAction>,
|
|
}
|
|
|
|
impl Delegate {
|
|
pub fn new(sender: UnboundedSender<AsyncAction>) -> Self {
|
|
Self { sender }
|
|
}
|
|
}
|
|
|
|
impl AppDelegate<State> for Delegate {
|
|
#[tracing::instrument(name = "Delegate", skip_all)]
|
|
fn command(
|
|
&mut self,
|
|
ctx: &mut DelegateCtx,
|
|
_target: Target,
|
|
cmd: &Command,
|
|
state: &mut State,
|
|
_env: &Env,
|
|
) -> Handled {
|
|
if cfg!(debug_assertions) && !cmd.is(ACTION_LOG) {
|
|
tracing::trace!(?cmd);
|
|
}
|
|
|
|
match cmd {
|
|
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
|
if self
|
|
.sender
|
|
.send(AsyncAction::DeployMods(state.clone().into()))
|
|
.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_DEPLOY) => {
|
|
state.is_deployment_in_progress = false;
|
|
state.dirty = false;
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
|
|
if self
|
|
.sender
|
|
.send(AsyncAction::ResetDeployment(state.clone().into()))
|
|
.is_ok()
|
|
{
|
|
state.is_reset_in_progress = true;
|
|
} else {
|
|
tracing::error!("Failed to queue action to reset mod deployment");
|
|
}
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_FINISH_RESET_DEPLOYMENT) => {
|
|
state.is_reset_in_progress = false;
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_SELECT_MOD) => {
|
|
let index = cmd
|
|
.get(ACTION_SELECT_MOD)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
state.select_mod(*index);
|
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_SELECTED_MOD_UP) => {
|
|
let Some(i) = state.selected_mod_index else {
|
|
return Handled::No;
|
|
};
|
|
|
|
let len = state.mods.len();
|
|
if len == 0 || i == 0 {
|
|
return Handled::No;
|
|
}
|
|
|
|
state.mods.swap(i, i - 1);
|
|
state.selected_mod_index = Some(i - 1);
|
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => {
|
|
let Some(i) = state.selected_mod_index else {
|
|
return Handled::No;
|
|
};
|
|
|
|
let len = state.mods.len();
|
|
if len == 0 || i == usize::MAX || i >= len - 1 {
|
|
return Handled::No;
|
|
}
|
|
|
|
state.mods.swap(i, i + 1);
|
|
state.selected_mod_index = Some(i + 1);
|
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
|
let info = cmd
|
|
.get(ACTION_START_DELETE_SELECTED_MOD)
|
|
.and_then(SingleUse::take)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
if self
|
|
.sender
|
|
.send(AsyncAction::DeleteMod(state.clone().into(), info))
|
|
.is_err()
|
|
{
|
|
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(SingleUse::take)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id);
|
|
let Some((index, _)) = found else {
|
|
return Handled::No;
|
|
};
|
|
|
|
state.mods.remove(index);
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_ADD_MOD) => {
|
|
let info = cmd
|
|
.get(ACTION_ADD_MOD)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
if self
|
|
.sender
|
|
.send(AsyncAction::AddMod(state.clone().into(), info.clone()))
|
|
.is_err()
|
|
{
|
|
tracing::error!("Failed to queue action to add mod");
|
|
}
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_FINISH_ADD_MOD) => {
|
|
let info = cmd
|
|
.get(ACTION_FINISH_ADD_MOD)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
if let Some(info) = info.take() {
|
|
state.add_mod(info);
|
|
}
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_LOG) => {
|
|
let line = cmd
|
|
.get(ACTION_LOG)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
if let Some(line) = line.take() {
|
|
{
|
|
let line = String::from_utf8_lossy(&line);
|
|
state.log.push_back(ansi_to_rich_text(line.trim()));
|
|
}
|
|
|
|
if self
|
|
.sender
|
|
.send(AsyncAction::Log((state.clone().into(), line)))
|
|
.is_err()
|
|
{
|
|
tracing::error!("Failed to queue action to add mod");
|
|
}
|
|
}
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_START_SAVE_SETTINGS) => {
|
|
if state.is_save_in_progress {
|
|
state.is_next_save_pending = true;
|
|
} else if self
|
|
.sender
|
|
.send(AsyncAction::SaveSettings(state.clone().into()))
|
|
.is_ok()
|
|
{
|
|
state.is_save_in_progress = true;
|
|
} else {
|
|
tracing::error!("Failed to queue action to save settings");
|
|
}
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_FINISH_SAVE_SETTINGS) => {
|
|
tracing::trace!(
|
|
in_progress = state.is_save_in_progress,
|
|
next_pending = state.is_next_save_pending,
|
|
"Finished saving settings",
|
|
);
|
|
state.is_save_in_progress = false;
|
|
|
|
if state.is_next_save_pending {
|
|
state.is_next_save_pending = false;
|
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
|
}
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_SET_DIRTY) => {
|
|
state.dirty = true;
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_SHOW_ERROR_DIALOG) => {
|
|
let err = cmd
|
|
.get(ACTION_SHOW_ERROR_DIALOG)
|
|
.and_then(SingleUse::take)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
let window = state
|
|
.windows
|
|
.get(&window::main::WINDOW_ID)
|
|
.expect("root window does not exist");
|
|
|
|
let dialog = window::dialog::error::<State>(err, window.clone());
|
|
ctx.new_window(dialog);
|
|
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_SET_WINDOW_HANDLE) => {
|
|
let (id, handle) = cmd
|
|
.get(ACTION_SET_WINDOW_HANDLE)
|
|
.and_then(SingleUse::take)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
state.windows.insert(id, handle);
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_START_CHECK_UPDATE) => {
|
|
if self
|
|
.sender
|
|
.send(AsyncAction::CheckUpdates(state.clone().into()))
|
|
.is_ok()
|
|
{
|
|
state.is_update_in_progress = true;
|
|
} else {
|
|
tracing::error!("Failed to queue action to check updates");
|
|
}
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_FINISH_CHECK_UPDATE) => {
|
|
let mut updates = cmd
|
|
.get(ACTION_FINISH_CHECK_UPDATE)
|
|
.and_then(SingleUse::take)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
if tracing::enabled!(tracing::Level::DEBUG) {
|
|
let mods: Vec<_> = updates
|
|
.iter()
|
|
.map(|info| {
|
|
format!(
|
|
"{}: {} -> {:?}",
|
|
info.name,
|
|
info.version,
|
|
info.nexus.as_ref().map(|n| &n.version)
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
tracing::info!("Mod updates:\n{}", mods.join("\n"));
|
|
}
|
|
|
|
for mod_info in state.mods.iter_mut() {
|
|
if let Some(index) = updates.iter().position(|i2| i2.id == mod_info.id) {
|
|
let update = updates.swap_remove(index);
|
|
*mod_info = Arc::new(update);
|
|
}
|
|
}
|
|
|
|
state.is_update_in_progress = false;
|
|
Handled::Yes
|
|
}
|
|
cmd if cmd.is(ACTION_FINISH_LOAD_INITIAL) => {
|
|
let data = cmd
|
|
.get(ACTION_FINISH_LOAD_INITIAL)
|
|
.and_then(SingleUse::take)
|
|
.expect("command type matched but didn't contain the expected value");
|
|
|
|
if let Some((config, mods)) = data {
|
|
state.mods = mods;
|
|
state.config_path = Arc::new(config.path);
|
|
state.data_dir = Arc::new(config.data_dir);
|
|
state.game_dir = Arc::new(config.game_dir.unwrap_or_default());
|
|
state.nexus_api_key = Arc::new(config.nexus_api_key.unwrap_or_default());
|
|
state.is_io_enabled = config.unsafe_io;
|
|
}
|
|
|
|
state.loading = false;
|
|
|
|
Handled::Yes
|
|
}
|
|
_ => Handled::No,
|
|
}
|
|
}
|
|
|
|
fn window_added(
|
|
&mut self,
|
|
id: WindowId,
|
|
handle: WindowHandle,
|
|
data: &mut State,
|
|
_: &Env,
|
|
_: &mut DelegateCtx,
|
|
) {
|
|
data.windows.insert(id, handle);
|
|
}
|
|
|
|
fn window_removed(&mut self, id: WindowId, data: &mut State, _: &Env, _: &mut DelegateCtx) {
|
|
data.windows.remove(&id);
|
|
}
|
|
}
|