parent
55335c0fdc
commit
e6c9fe834c
7 changed files with 134 additions and 28 deletions
|
@ -9,6 +9,7 @@ use tokio::fs;
|
|||
use zip::ZipArchive;
|
||||
|
||||
use crate::state::{ModInfo, PackageInfo, State};
|
||||
use crate::util::config::Config;
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
||||
|
@ -103,3 +104,23 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn save_settings(state: State) -> Result<()> {
|
||||
// TODO: Avoid allocations, especially once the config grows, by
|
||||
// creating a separate struct with only borrowed data to serialize from.
|
||||
let cfg = Config {
|
||||
path: state.config_path.as_ref().clone(),
|
||||
game_dir: Some(state.game_dir.as_ref().clone()),
|
||||
data_dir: Some(state.data_dir.as_ref().clone()),
|
||||
};
|
||||
|
||||
tracing::info!("Saving settings to '{}'", state.config_path.display());
|
||||
tracing::debug!(?cfg);
|
||||
|
||||
let data = serde_sjson::to_string(&cfg).wrap_err("failed to serialize config")?;
|
||||
|
||||
fs::write(&cfg.path, &data)
|
||||
.await
|
||||
.wrap_err_with(|| format!("failed to write config to '{}'", cfg.path.display()))
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use tokio::sync::RwLock;
|
|||
use crate::controller::app::*;
|
||||
use crate::controller::game::*;
|
||||
use crate::state::AsyncAction;
|
||||
use crate::state::ACTION_FINISH_SAVE_SETTINGS;
|
||||
use crate::state::{
|
||||
ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY,
|
||||
ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG,
|
||||
|
@ -81,6 +82,17 @@ async fn handle_action(
|
|||
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::SaveSettings(state) => tokio::spawn(async move {
|
||||
if let Err(err) = save_settings(state).await {
|
||||
tracing::error!("Failed to save settings: {:?}", err);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(ACTION_FINISH_SAVE_SETTINGS, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,10 +82,18 @@ pub(crate) struct State {
|
|||
pub selected_mod_index: Option<usize>,
|
||||
pub is_deployment_in_progress: bool,
|
||||
pub is_reset_in_progress: bool,
|
||||
pub is_save_in_progress: bool,
|
||||
pub is_next_save_pending: bool,
|
||||
pub game_dir: Arc<PathBuf>,
|
||||
pub data_dir: Arc<PathBuf>,
|
||||
pub ctx: Arc<sdk::Context>,
|
||||
pub log: Arc<String>,
|
||||
|
||||
#[lens(ignore)]
|
||||
#[data(ignore)]
|
||||
pub config_path: Arc<PathBuf>,
|
||||
#[lens(ignore)]
|
||||
#[data(ignore)]
|
||||
pub ctx: Arc<sdk::Context>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
@ -102,8 +110,11 @@ impl State {
|
|||
selected_mod_index: None,
|
||||
is_deployment_in_progress: false,
|
||||
is_reset_in_progress: false,
|
||||
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
|
||||
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
|
||||
is_save_in_progress: false,
|
||||
is_next_save_pending: false,
|
||||
config_path: Arc::new(config.path),
|
||||
game_dir: Arc::new(config.game_dir.unwrap_or_default()),
|
||||
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
|
||||
log: Arc::new(String::new()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,11 +28,17 @@ pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
|
|||
|
||||
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = 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) enum AsyncAction {
|
||||
DeployMods(State),
|
||||
ResetDeployment(State),
|
||||
AddMod((State, FileInfo)),
|
||||
DeleteMod((State, ModInfo)),
|
||||
SaveSettings(State),
|
||||
}
|
||||
|
||||
pub(crate) struct Delegate {
|
||||
|
@ -49,12 +55,16 @@ impl AppDelegate<State> for Delegate {
|
|||
#[tracing::instrument(name = "Delegate", skip_all)]
|
||||
fn command(
|
||||
&mut self,
|
||||
_ctx: &mut DelegateCtx,
|
||||
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
|
||||
|
@ -152,6 +162,8 @@ impl AppDelegate<State> for Delegate {
|
|||
};
|
||||
|
||||
state.mods.remove(index);
|
||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_ADD_MOD) => {
|
||||
|
@ -173,6 +185,7 @@ impl AppDelegate<State> for Delegate {
|
|||
.expect("command type matched but didn't contain the expected value");
|
||||
if let Some(info) = info.take() {
|
||||
state.add_mod(info);
|
||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||
}
|
||||
Handled::Yes
|
||||
}
|
||||
|
@ -185,6 +198,31 @@ impl AppDelegate<State> for Delegate {
|
|||
}
|
||||
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()))
|
||||
.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) => {
|
||||
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 cfg!(debug_assertions) {
|
||||
tracing::warn!("Unknown command: {:?}", cmd);
|
||||
|
|
|
@ -2,6 +2,8 @@ use std::path::PathBuf;
|
|||
use std::sync::Arc;
|
||||
|
||||
use druid::text::Formatter;
|
||||
use druid::widget::{TextBoxEvent, ValidationDelegate};
|
||||
use druid::EventCtx;
|
||||
|
||||
pub(crate) struct PathBufFormatter;
|
||||
|
||||
|
@ -29,3 +31,19 @@ impl Formatter<Arc<PathBuf>> for PathBufFormatter {
|
|||
Ok(Arc::new(p))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextBoxOnChanged<F: Fn(&mut EventCtx, &str)>(F);
|
||||
|
||||
impl<F: Fn(&mut EventCtx, &str)> TextBoxOnChanged<F> {
|
||||
pub fn new(f: F) -> Self {
|
||||
Self(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(&mut EventCtx, &str)> ValidationDelegate for TextBoxOnChanged<F> {
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str) {
|
||||
if let TextBoxEvent::Complete = event {
|
||||
(self.0)(ctx, current_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,10 @@ use druid::{
|
|||
TextAlignment, Widget, WidgetExt, WindowDesc,
|
||||
};
|
||||
|
||||
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
|
||||
use crate::state::{
|
||||
ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_START_RESET_DEPLOYMENT,
|
||||
ACTION_START_SAVE_SETTINGS,
|
||||
};
|
||||
use crate::state::{
|
||||
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
||||
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
||||
|
@ -246,6 +249,9 @@ fn build_view_settings() -> impl Widget<State> {
|
|||
.with_flex_child(
|
||||
TextBox::new()
|
||||
.with_formatter(PathBufFormatter::new())
|
||||
.delegate(TextBoxOnChanged::new(|ctx, _| {
|
||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS)
|
||||
}))
|
||||
.expand_width()
|
||||
.lens(State::data_dir),
|
||||
1.,
|
||||
|
@ -260,6 +266,9 @@ fn build_view_settings() -> impl Widget<State> {
|
|||
.with_flex_child(
|
||||
TextBox::new()
|
||||
.with_formatter(PathBufFormatter::new())
|
||||
.delegate(TextBoxOnChanged::new(|ctx, _| {
|
||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS)
|
||||
}))
|
||||
.expand_width()
|
||||
.lens(State::game_dir),
|
||||
1.,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{parser::ValueSource, ArgMatches};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
|
@ -8,18 +8,10 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Config {
|
||||
data_dir: Option<PathBuf>,
|
||||
game_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn game_dir(&self) -> Option<&PathBuf> {
|
||||
self.game_dir.as_ref()
|
||||
}
|
||||
|
||||
pub fn data_dir(&self) -> Option<&PathBuf> {
|
||||
self.data_dir.as_ref()
|
||||
}
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub data_dir: Option<PathBuf>,
|
||||
pub game_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[cfg(not(arget_os = "windows"))]
|
||||
|
@ -60,22 +52,26 @@ pub fn get_default_data_dir() -> PathBuf {
|
|||
PathBuf::from(data_dir).join("dtmm")
|
||||
}
|
||||
|
||||
pub(crate) fn read_config<P: AsRef<Path>>(
|
||||
default_config_path: P,
|
||||
matches: &ArgMatches,
|
||||
) -> Result<Config> {
|
||||
#[tracing::instrument(skip(matches),fields(path = ?matches.get_one::<PathBuf>("config")))]
|
||||
pub(crate) fn read_config<P>(default: P, matches: &ArgMatches) -> Result<Config>
|
||||
where
|
||||
P: Into<PathBuf> + std::fmt::Debug,
|
||||
{
|
||||
let path = matches
|
||||
.get_one::<PathBuf>("config")
|
||||
.expect("argument missing despite default");
|
||||
let default_config_path = default_config_path.as_ref();
|
||||
let default_path = default.into();
|
||||
|
||||
match fs::read(path) {
|
||||
Ok(data) => {
|
||||
let data = String::from_utf8(data).wrap_err_with(|| {
|
||||
format!("config file {} contains invalid UTF-8", path.display())
|
||||
})?;
|
||||
serde_sjson::from_str(&data)
|
||||
.wrap_err_with(|| format!("invalid config file {}", path.display()))
|
||||
let mut cfg: Config = serde_sjson::from_str(&data)
|
||||
.wrap_err_with(|| format!("invalid config file {}", path.display()))?;
|
||||
|
||||
cfg.path = path.clone();
|
||||
Ok(cfg)
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
||||
|
@ -84,7 +80,7 @@ pub(crate) fn read_config<P: AsRef<Path>>(
|
|||
}
|
||||
|
||||
{
|
||||
let parent = default_config_path
|
||||
let parent = default_path
|
||||
.parent()
|
||||
.expect("a file path always has a parent directory");
|
||||
fs::create_dir_all(parent).wrap_err_with(|| {
|
||||
|
@ -93,6 +89,7 @@ pub(crate) fn read_config<P: AsRef<Path>>(
|
|||
}
|
||||
|
||||
let config = Config {
|
||||
path: default_path,
|
||||
data_dir: Some(get_default_data_dir()),
|
||||
game_dir: None,
|
||||
};
|
||||
|
@ -100,10 +97,10 @@ pub(crate) fn read_config<P: AsRef<Path>>(
|
|||
{
|
||||
let data = serde_sjson::to_string(&config)
|
||||
.wrap_err("failed to serialize default config value")?;
|
||||
fs::write(default_config_path, data).wrap_err_with(|| {
|
||||
fs::write(&config.path, data).wrap_err_with(|| {
|
||||
format!(
|
||||
"failed to write default config to {}",
|
||||
default_config_path.display()
|
||||
config.path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue