Implement dialog for critical errors #57

Merged
lucas merged 3 commits from issue/37 into master 2023-03-08 20:41:35 +01:00
12 changed files with 221 additions and 58 deletions
Showing only changes of commit 658d996315 - Show all commits

View file

@ -9,6 +9,7 @@
- dtmm: indicate when a deployment is necessary - dtmm: indicate when a deployment is necessary
- dtmm: check for Steam game update before deployment - dtmm: check for Steam game update before deployment
- dtmm: remove unused bundles from previous deployment - dtmm: remove unused bundles from previous deployment
- dtmm: show dialog for critical errors
=== Fixed === Fixed

1
Cargo.lock generated
View file

@ -705,6 +705,7 @@ dependencies = [
"druid", "druid",
"dtmt-shared", "dtmt-shared",
"futures", "futures",
"lazy_static",
"oodle-sys", "oodle-sys",
"path-slash", "path-slash",
"sdk", "sdk",

View file

@ -26,3 +26,4 @@ tokio-stream = { version = "0.1.12", features = ["fs"] }
path-slash = "0.2.1" path-slash = "0.2.1"
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] } time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }
strip-ansi-escapes = "0.1.1" strip-ansi-escapes = "0.1.1"
lazy_static = "1.4.0"

View file

@ -14,13 +14,13 @@ use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use zip::ZipArchive; use zip::ZipArchive;
use crate::state::{ModInfo, PackageInfo, State}; use crate::state::{ActionState, ModInfo, PackageInfo};
use crate::util::config::{ConfigSerialize, LoadOrderEntry}; use crate::util::config::{ConfigSerialize, LoadOrderEntry};
use super::read_sjson_file; use super::read_sjson_file;
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> { pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<ModInfo> {
let data = fs::read(&info.path) let data = fs::read(&info.path)
.await .await
.wrap_err_with(|| format!("failed to read file {}", info.path.display()))?; .wrap_err_with(|| format!("failed to read file {}", info.path.display()))?;
@ -95,16 +95,16 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
tracing::trace!(?files); tracing::trace!(?files);
let mod_dir = state.get_mod_dir(); let mod_dir = state.mod_dir;
tracing::trace!("Creating mods directory {}", mod_dir.display()); tracing::trace!("Creating mods directory {}", mod_dir.display());
fs::create_dir_all(&mod_dir) fs::create_dir_all(Arc::as_ref(&mod_dir))
.await .await
.wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?; .wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?;
tracing::trace!("Extracting mod archive to {}", mod_dir.display()); tracing::trace!("Extracting mod archive to {}", mod_dir.display());
archive archive
.extract(&mod_dir) .extract(Arc::as_ref(&mod_dir))
.wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; .wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?;
let packages = files let packages = files
@ -117,8 +117,8 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { pub(crate) async fn delete_mod(state: ActionState, info: &ModInfo) -> Result<()> {
let mod_dir = state.get_mod_dir().join(&info.id); let mod_dir = state.mod_dir.join(&info.id);
fs::remove_dir_all(&mod_dir) fs::remove_dir_all(&mod_dir)
.await .await
.wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?;
@ -127,7 +127,7 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn save_settings(state: State) -> Result<()> { pub(crate) async fn save_settings(state: ActionState) -> Result<()> {
let cfg = ConfigSerialize::from(&state); let cfg = ConfigSerialize::from(&state);
tracing::info!("Saving settings to '{}'", state.config_path.display()); tracing::info!("Saving settings to '{}'", state.config_path.display());

View file

@ -22,7 +22,7 @@ use tokio::io::AsyncWriteExt;
use tracing::Instrument; use tracing::Instrument;
use super::read_sjson_file; use super::read_sjson_file;
use crate::state::{PackageInfo, State}; use crate::state::{ActionState, PackageInfo};
const MOD_BUNDLE_NAME: &str = "packages/mods"; const MOD_BUNDLE_NAME: &str = "packages/mods";
const BOOT_BUNDLE_NAME: &str = "packages/boot"; const BOOT_BUNDLE_NAME: &str = "packages/boot";
@ -100,7 +100,7 @@ where
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn patch_game_settings(state: Arc<State>) -> Result<()> { async fn patch_game_settings(state: Arc<ActionState>) -> Result<()> {
let settings_path = state.game_dir.join("bundle").join(SETTINGS_FILE_PATH); let settings_path = state.game_dir.join("bundle").join(SETTINGS_FILE_PATH);
let settings = read_file_with_backup(&settings_path) let settings = read_file_with_backup(&settings_path)
@ -146,7 +146,7 @@ fn make_package(info: &PackageInfo) -> Result<Package> {
Ok(pkg) Ok(pkg)
} }
fn build_mod_data_lua(state: Arc<State>) -> String { fn build_mod_data_lua(state: Arc<ActionState>) -> String {
let mut lua = String::from("return {\n"); let mut lua = String::from("return {\n");
// DMF is handled explicitely by the loading procedures, as it actually drives most of that // DMF is handled explicitely by the loading procedures, as it actually drives most of that
@ -203,7 +203,7 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> { async fn build_bundles(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string()); let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string());
let mut tasks = Vec::new(); let mut tasks = Vec::new();
@ -227,7 +227,7 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
let span = tracing::trace_span!("building mod packages", name = mod_info.name); let span = tracing::trace_span!("building mod packages", name = mod_info.name);
let _enter = span.enter(); let _enter = span.enter();
let mod_dir = state.get_mod_dir().join(&mod_info.id); let mod_dir = state.mod_dir.join(&mod_info.id);
for pkg_info in &mod_info.packages { for pkg_info in &mod_info.packages {
let span = tracing::trace_span!("building package", name = pkg_info.name); let span = tracing::trace_span!("building package", name = pkg_info.name);
let _enter = span.enter(); let _enter = span.enter();
@ -320,7 +320,7 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> { async fn patch_boot_bundle(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let bundle_dir = Arc::new(state.game_dir.join("bundle")); let bundle_dir = Arc::new(state.game_dir.join("bundle"));
let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()))); let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())));
@ -381,7 +381,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
let bundle_name = Murmur64::hash(&pkg_info.name) let bundle_name = Murmur64::hash(&pkg_info.name)
.to_string() .to_string()
.to_ascii_lowercase(); .to_ascii_lowercase();
let src = state.get_mod_dir().join(&mod_info.id).join(&bundle_name); let src = state.mod_dir.join(&mod_info.id).join(&bundle_name);
{ {
let bin = fs::read(&src) let bin = fs::read(&src)
@ -461,7 +461,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
} }
#[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))] #[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))]
async fn patch_bundle_database<B>(state: Arc<State>, bundles: B) -> Result<()> async fn patch_bundle_database<B>(state: Arc<ActionState>, bundles: B) -> Result<()>
where where
B: AsRef<[Bundle]>, B: AsRef<[Bundle]>,
{ {
@ -499,7 +499,7 @@ where
} }
#[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))] #[tracing::instrument(skip_all, fields(bundles = bundles.as_ref().len()))]
async fn write_deployment_data<B>(state: Arc<State>, bundles: B) -> Result<()> async fn write_deployment_data<B>(state: Arc<ActionState>, bundles: B) -> Result<()>
where where
B: AsRef<[Bundle]>, B: AsRef<[Bundle]>,
{ {
@ -525,7 +525,7 @@ where
game_dir = %state.game_dir.display(), game_dir = %state.game_dir.display(),
mods = state.mods.len() mods = state.mods.len()
))] ))]
pub(crate) async fn deploy_mods(state: State) -> Result<()> { pub(crate) async fn deploy_mods(state: ActionState) -> Result<()> {
let state = Arc::new(state); let state = Arc::new(state);
{ {
@ -643,7 +643,7 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { pub(crate) async fn reset_mod_deployment(state: ActionState) -> Result<()> {
let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()));
let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path, SETTINGS_FILE_PATH]; let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path, SETTINGS_FILE_PATH];
let bundle_dir = state.game_dir.join("bundle"); let bundle_dir = state.game_dir.join("bundle");

View file

@ -1,5 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use color_eyre::eyre::Context;
use color_eyre::Help;
use color_eyre::Report;
use color_eyre::Result; use color_eyre::Result;
use druid::{ExtEventSink, SingleUse, Target}; use druid::{ExtEventSink, SingleUse, Target};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
@ -10,11 +13,19 @@ use crate::controller::app::*;
use crate::controller::game::*; use crate::controller::game::*;
use crate::state::AsyncAction; use crate::state::AsyncAction;
use crate::state::ACTION_FINISH_SAVE_SETTINGS; use crate::state::ACTION_FINISH_SAVE_SETTINGS;
use crate::state::ACTION_SHOW_ERROR_DIALOG;
use crate::state::{ use crate::state::{
ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY, ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY,
ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG, ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG,
}; };
async fn send_error(sink: Arc<RwLock<ExtEventSink>>, err: Report) {
sink.write()
.await
.submit_command(ACTION_SHOW_ERROR_DIALOG, SingleUse::new(err), Target::Auto)
.expect("failed to send command");
}
async fn handle_action( async fn handle_action(
event_sink: Arc<RwLock<ExtEventSink>>, event_sink: Arc<RwLock<ExtEventSink>>,
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>, action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
@ -23,8 +34,9 @@ async fn handle_action(
let event_sink = event_sink.clone(); let event_sink = event_sink.clone();
match action { match action {
AsyncAction::DeployMods(state) => tokio::spawn(async move { AsyncAction::DeployMods(state) => tokio::spawn(async move {
if let Err(err) = deploy_mods(state).await { if let Err(err) = deploy_mods(state).await.wrap_err("failed to deploy mods") {
tracing::error!("Failed to deploy mods: {:?}", err); tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink
@ -33,8 +45,11 @@ async fn handle_action(
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
.expect("failed to send command"); .expect("failed to send command");
}), }),
AsyncAction::AddMod((state, info)) => tokio::spawn(async move { AsyncAction::AddMod(state, info) => tokio::spawn(async move {
match import_mod(state, info).await { match import_mod(state, info)
.await
.wrap_err("failed to import mod")
{
Ok(mod_info) => { Ok(mod_info) => {
event_sink event_sink
.write() .write()
@ -47,18 +62,22 @@ async fn handle_action(
.expect("failed to send command"); .expect("failed to send command");
} }
Err(err) => { Err(err) => {
tracing::error!("Failed to import mod: {:?}", err); tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
} }
}), }),
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { AsyncAction::DeleteMod(state, info) => tokio::spawn(async move {
if let Err(err) = delete_mod(state, &info).await { let mod_dir = state.mod_dir.join(&info.id);
tracing::error!( if let Err(err) = delete_mod(state, &info)
"Failed to delete mod files. \ .await
You might want to clean up the data directory manually. \ .wrap_err("failed to delete mod files")
Reason: {:?}", .with_suggestion(|| {
err format!("Clean the folder '{}' manually", mod_dir.display())
); })
{
tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink
@ -72,8 +91,12 @@ async fn handle_action(
.expect("failed to send command"); .expect("failed to send command");
}), }),
AsyncAction::ResetDeployment(state) => tokio::spawn(async move { AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
if let Err(err) = reset_mod_deployment(state).await { if let Err(err) = reset_mod_deployment(state)
tracing::error!("Failed to reset mod deployment: {:?}", err); .await
.wrap_err("failed to reset mod deployment")
{
tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink
@ -83,8 +106,12 @@ async fn handle_action(
.expect("failed to send command"); .expect("failed to send command");
}), }),
AsyncAction::SaveSettings(state) => tokio::spawn(async move { AsyncAction::SaveSettings(state) => tokio::spawn(async move {
if let Err(err) = save_settings(state).await { if let Err(err) = save_settings(state)
tracing::error!("Failed to save settings: {:?}", err); .await
.wrap_err("failed to save settings")
{
tracing::error!("{:?}", err);
send_error(event_sink.clone(), err).await;
} }
event_sink event_sink

View file

@ -1,6 +1,9 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use druid::{im::Vector, Data, Lens}; use druid::{
im::{HashMap, Vector},
Data, Lens, WindowHandle, WindowId,
};
use dtmt_shared::ModConfig; use dtmt_shared::ModConfig;
use super::SelectedModLens; use super::SelectedModLens;
@ -86,6 +89,9 @@ pub(crate) struct State {
pub config_path: Arc<PathBuf>, pub config_path: Arc<PathBuf>,
#[lens(ignore)] #[lens(ignore)]
#[data(ignore)] #[data(ignore)]
pub windows: HashMap<WindowId, WindowHandle>,
#[lens(ignore)]
#[data(ignore)]
pub ctx: Arc<sdk::Context>, pub ctx: Arc<sdk::Context>,
} }
@ -110,6 +116,7 @@ impl State {
game_dir: Arc::new(game_dir), game_dir: Arc::new(game_dir),
data_dir: Arc::new(data_dir), data_dir: Arc::new(data_dir),
log: Arc::new(String::new()), log: Arc::new(String::new()),
windows: HashMap::new(),
} }
} }

View file

@ -1,10 +1,14 @@
use std::sync::Arc; use std::{path::PathBuf, sync::Arc};
use color_eyre::Report;
use druid::{ use druid::{
AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, im::Vector, AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse,
Target, WindowHandle, WindowId,
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::ui::window;
use super::{ModInfo, State}; use super::{ModInfo, State};
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod"); pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
@ -37,12 +41,42 @@ pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty"); 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");
// 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>,
}
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,
}
}
}
pub(crate) enum AsyncAction { pub(crate) enum AsyncAction {
DeployMods(State), DeployMods(ActionState),
ResetDeployment(State), ResetDeployment(ActionState),
AddMod((State, FileInfo)), AddMod(ActionState, FileInfo),
DeleteMod((State, Arc<ModInfo>)), DeleteMod(ActionState, Arc<ModInfo>),
SaveSettings(State), SaveSettings(ActionState),
} }
pub(crate) struct Delegate { pub(crate) struct Delegate {
@ -73,7 +107,7 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_START_DEPLOY) => { cmd if cmd.is(ACTION_START_DEPLOY) => {
if self if self
.sender .sender
.send(AsyncAction::DeployMods(state.clone())) .send(AsyncAction::DeployMods(state.clone().into()))
.is_ok() .is_ok()
{ {
state.is_deployment_in_progress = true; state.is_deployment_in_progress = true;
@ -91,7 +125,7 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
if self if self
.sender .sender
.send(AsyncAction::ResetDeployment(state.clone())) .send(AsyncAction::ResetDeployment(state.clone().into()))
.is_ok() .is_ok()
{ {
state.is_reset_in_progress = true; state.is_reset_in_progress = true;
@ -147,11 +181,12 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
let info = cmd let info = cmd
.get(ACTION_START_DELETE_SELECTED_MOD) .get(ACTION_START_DELETE_SELECTED_MOD)
.and_then(|info| info.take()) .and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if self if self
.sender .sender
.send(AsyncAction::DeleteMod((state.clone(), info))) .send(AsyncAction::DeleteMod(state.clone().into(), info))
.is_err() .is_err()
{ {
tracing::error!("Failed to queue action to deploy mods"); tracing::error!("Failed to queue action to deploy mods");
@ -162,8 +197,9 @@ impl AppDelegate<State> for Delegate {
cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => {
let info = cmd let info = cmd
.get(ACTION_FINISH_DELETE_SELECTED_MOD) .get(ACTION_FINISH_DELETE_SELECTED_MOD)
.and_then(|info| info.take()) .and_then(SingleUse::take)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id); let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id);
let Some((index, _)) = found else { let Some((index, _)) = found else {
return Handled::No; return Handled::No;
@ -177,9 +213,10 @@ impl AppDelegate<State> for Delegate {
let info = cmd let info = cmd
.get(ACTION_ADD_MOD) .get(ACTION_ADD_MOD)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if self if self
.sender .sender
.send(AsyncAction::AddMod((state.clone(), info.clone()))) .send(AsyncAction::AddMod(state.clone().into(), info.clone()))
.is_err() .is_err()
{ {
tracing::error!("Failed to queue action to add mod"); tracing::error!("Failed to queue action to add mod");
@ -190,9 +227,11 @@ impl AppDelegate<State> for Delegate {
let info = cmd let info = cmd
.get(ACTION_FINISH_ADD_MOD) .get(ACTION_FINISH_ADD_MOD)
.expect("command type matched but didn't contain the expected value"); .expect("command type matched but didn't contain the expected value");
if let Some(info) = info.take() { if let Some(info) = info.take() {
state.add_mod(info); state.add_mod(info);
} }
Handled::Yes Handled::Yes
} }
cmd if cmd.is(ACTION_LOG) => { cmd if cmd.is(ACTION_LOG) => {
@ -209,7 +248,7 @@ impl AppDelegate<State> for Delegate {
state.is_next_save_pending = true; state.is_next_save_pending = true;
} else if self } else if self
.sender .sender
.send(AsyncAction::SaveSettings(state.clone())) .send(AsyncAction::SaveSettings(state.clone().into()))
.is_ok() .is_ok()
{ {
state.is_save_in_progress = true; state.is_save_in_progress = true;
@ -233,6 +272,31 @@ impl AppDelegate<State> for Delegate {
state.dirty = true; state.dirty = true;
Handled::Yes 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 => { cmd => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
tracing::warn!("Unknown command: {:?}", cmd); tracing::warn!("Unknown command: {:?}", cmd);
@ -241,4 +305,19 @@ impl AppDelegate<State> for Delegate {
} }
} }
} }
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);
}
} }

View file

@ -1,5 +1,6 @@
pub mod theme; pub mod theme;
pub mod widget; pub mod widget;
pub mod window { pub mod window {
pub mod dialog;
pub mod main; pub mod main;
} }

View file

@ -0,0 +1,36 @@
use color_eyre::Report;
use druid::widget::{Button, CrossAxisAlignment, Flex, Label, LineBreaking, MainAxisAlignment};
use druid::{Data, WidgetExt, WindowDesc, WindowHandle, WindowLevel, WindowSizePolicy};
const ERROR_DIALOG_SIZE: (f64, f64) = (750., 400.);
pub fn error<T: Data>(err: Report, parent: WindowHandle) -> WindowDesc<T> {
let msg = format!("A critical error ocurred: {:?}", err);
let stripped =
strip_ansi_escapes::strip(msg.as_bytes()).expect("failed to strip ANSI in error");
let msg = String::from_utf8_lossy(&stripped);
let text = Label::new(msg.to_string()).with_line_break_mode(LineBreaking::WordWrap);
let button = Button::new("Ok")
.on_click(|ctx, _, _| {
ctx.window().close();
})
.align_right();
let widget = Flex::column()
.main_axis_alignment(MainAxisAlignment::SpaceBetween)
.cross_axis_alignment(CrossAxisAlignment::End)
.with_child(text)
.with_spacer(20.)
.with_child(button)
.padding(10.);
WindowDesc::new(widget)
.title("Error")
.with_min_size(ERROR_DIALOG_SIZE)
.resizable(false)
.window_size_policy(WindowSizePolicy::Content)
.set_always_on_top(true)
.set_level(WindowLevel::Modal(parent))
}

View file

@ -1,25 +1,30 @@
use std::sync::Arc; use std::sync::Arc;
use druid::im::Vector; use druid::im::Vector;
use druid::lens;
use druid::widget::{ use druid::widget::{
Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment,
Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher,
}; };
use druid::{lens, LifeCycleCtx};
use druid::{ use druid::{
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse,
TextAlignment, Widget, WidgetExt, WindowDesc, TextAlignment, Widget, WidgetExt, WindowDesc, WindowId,
}; };
use lazy_static::lazy_static;
use crate::state::{ use crate::state::{
ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP,
ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_DELETE_SELECTED_MOD,
ACTION_START_RESET_DEPLOYMENT, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
}; };
use crate::ui::theme; use crate::ui::theme;
use crate::ui::widget::controller::{AutoScrollController, DirtyStateController}; use crate::ui::widget::controller::{AutoScrollController, DirtyStateController};
use crate::ui::widget::PathBufFormatter; use crate::ui::widget::PathBufFormatter;
lazy_static! {
pub static ref WINDOW_ID: WindowId = WindowId::next();
}
const TITLE: &str = "Darktide Mod Manager"; const TITLE: &str = "Darktide Mod Manager";
const WINDOW_SIZE: (f64, f64) = (1080., 720.); const WINDOW_SIZE: (f64, f64) = (1080., 720.);
const MOD_DETAILS_MIN_WIDTH: f64 = 325.; const MOD_DETAILS_MIN_WIDTH: f64 = 325.;
@ -324,4 +329,9 @@ fn build_window() -> impl Widget<State> {
.with_flex_child(build_main(), 1.0) .with_flex_child(build_main(), 1.0)
.with_child(build_log_view()) .with_child(build_log_view())
.controller(DirtyStateController) .controller(DirtyStateController)
.on_added(|_, ctx: &mut LifeCycleCtx, _, _| {
ctx.submit_command(
ACTION_SET_WINDOW_HANDLE.with(SingleUse::new((*WINDOW_ID, ctx.window().clone()))),
);
})
} }

View file

@ -7,7 +7,7 @@ use clap::{parser::ValueSource, ArgMatches};
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::state::{ModInfo, State}; use crate::state::{ActionState, ModInfo};
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub(crate) struct LoadOrderEntrySerialize<'a> { pub(crate) struct LoadOrderEntrySerialize<'a> {
@ -31,8 +31,8 @@ pub(crate) struct ConfigSerialize<'a> {
mod_order: Vec<LoadOrderEntrySerialize<'a>>, mod_order: Vec<LoadOrderEntrySerialize<'a>>,
} }
impl<'a> From<&'a State> for ConfigSerialize<'a> { impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
fn from(state: &'a State) -> Self { fn from(state: &'a ActionState) -> Self {
Self { Self {
game_dir: &state.game_dir, game_dir: &state.game_dir,
data_dir: &state.data_dir, data_dir: &state.data_dir,