Darktide Mod Manager #39
12 changed files with 247 additions and 104 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -680,6 +680,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_sjson",
|
"serde_sjson",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
@ -1261,6 +1262,7 @@ dependencies = [
|
||||||
"bitmaps",
|
"bitmaps",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"rand_xoshiro",
|
"rand_xoshiro",
|
||||||
|
"serde",
|
||||||
"sized-chunks",
|
"sized-chunks",
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
@ -1375,6 +1377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e119590a03caff1f7a582e8ee8c2164ddcc975791701188132fd1d1b518d3871"
|
checksum = "e119590a03caff1f7a582e8ee8c2164ddcc975791701188132fd1d1b518d3871"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -10,15 +10,16 @@ bitflags = "1.3.2"
|
||||||
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
|
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
confy = "0.5.1"
|
confy = "0.5.1"
|
||||||
druid = { git = "https://github.com/linebender/druid.git", features = ["im"] }
|
druid = { git = "https://github.com/linebender/druid.git", features = ["im", "serde"] }
|
||||||
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
||||||
sdk = { path = "../../lib/sdk", version = "0.2.0" }
|
sdk = { path = "../../lib/sdk", version = "0.2.0" }
|
||||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||||
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
zip = "0.6.4"
|
zip = "0.6.4"
|
||||||
|
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{Cursor, Read};
|
use std::io::{Cursor, ErrorKind, Read};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
use color_eyre::{Help, Result};
|
use color_eyre::{Help, Result};
|
||||||
|
use druid::im::Vector;
|
||||||
use druid::FileInfo;
|
use druid::FileInfo;
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
use tokio::fs;
|
use serde::Deserialize;
|
||||||
|
use tokio::fs::{self, DirEntry};
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use crate::state::{ModInfo, PackageInfo, State};
|
use crate::state::{ModInfo, PackageInfo, State};
|
||||||
|
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
||||||
|
|
||||||
#[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: State, info: FileInfo) -> Result<ModInfo> {
|
||||||
|
@ -105,24 +111,9 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
struct Config<'a> {
|
|
||||||
game_dir: &'a Path,
|
|
||||||
data_dir: &'a Path,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a State> for Config<'a> {
|
|
||||||
fn from(value: &'a State) -> Self {
|
|
||||||
Self {
|
|
||||||
game_dir: &value.game_dir,
|
|
||||||
data_dir: &value.data_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub(crate) async fn save_settings(state: State) -> Result<()> {
|
pub(crate) async fn save_settings(state: State) -> Result<()> {
|
||||||
let cfg = Config::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());
|
||||||
tracing::debug!(?cfg);
|
tracing::debug!(?cfg);
|
||||||
|
@ -138,3 +129,85 @@ pub(crate) async fn save_settings(state: State) -> Result<()> {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_sjson_file<P, T>(path: P) -> Result<T>
|
||||||
|
where
|
||||||
|
T: for<'a> Deserialize<'a>,
|
||||||
|
P: AsRef<Path> + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let buf = fs::read(path).await.wrap_err("failed to read file")?;
|
||||||
|
let data = String::from_utf8(buf).wrap_err("invalid UTF8")?;
|
||||||
|
serde_sjson::from_str(&data).wrap_err("failed to deserialize")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all,fields(
|
||||||
|
name = ?res.as_ref().map(|entry| entry.file_name())
|
||||||
|
))]
|
||||||
|
async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
||||||
|
let entry = res?;
|
||||||
|
let config_path = entry.path().join("dtmt.cfg");
|
||||||
|
let index_path = entry.path().join("files.sjson");
|
||||||
|
|
||||||
|
let cfg: ModConfig = read_sjson_file(&config_path)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to read mod config '{}'", config_path.display()))?;
|
||||||
|
|
||||||
|
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to read file index '{}'", index_path.display()))?;
|
||||||
|
|
||||||
|
let packages = files
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, files)| PackageInfo::new(name, files.into_iter().collect()))
|
||||||
|
.collect();
|
||||||
|
let info = ModInfo::new(cfg, packages);
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(mod_order))]
|
||||||
|
pub(crate) fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result<Vector<ModInfo>>
|
||||||
|
where
|
||||||
|
S: Iterator<Item = &'a LoadOrderEntry>,
|
||||||
|
P: AsRef<Path> + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let rt = Runtime::new()?;
|
||||||
|
|
||||||
|
rt.block_on(async move {
|
||||||
|
let mod_dir = mod_dir.as_ref();
|
||||||
|
let read_dir = match fs::read_dir(mod_dir).await {
|
||||||
|
Ok(read_dir) => read_dir,
|
||||||
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||||
|
return Ok(Vector::new());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err)
|
||||||
|
.wrap_err_with(|| format!("failed to open directory '{}'", mod_dir.display()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = ReadDirStream::new(read_dir)
|
||||||
|
.map(|res| res.wrap_err("failed to read dir entry"))
|
||||||
|
.then(read_mod_dir_entry);
|
||||||
|
tokio::pin!(stream);
|
||||||
|
|
||||||
|
let mut mods: HashMap<String, ModInfo> = HashMap::new();
|
||||||
|
|
||||||
|
while let Some(res) = stream.next().await {
|
||||||
|
let info = res?;
|
||||||
|
mods.insert(info.id.clone(), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mods = mod_order
|
||||||
|
.filter_map(|entry| {
|
||||||
|
if let Some(mut info) = mods.remove(&entry.id) {
|
||||||
|
info.enabled = entry.enabled;
|
||||||
|
Some(info)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok::<_, color_eyre::Report>(mods)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use color_eyre::{Report, Result};
|
||||||
use druid::AppLauncher;
|
use druid::AppLauncher;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::controller::app::load_mods;
|
||||||
use crate::controller::worker::work_thread;
|
use crate::controller::worker::work_thread;
|
||||||
use crate::state::{Delegate, State};
|
use crate::state::{Delegate, State};
|
||||||
|
|
||||||
|
@ -62,7 +63,16 @@ fn main() -> Result<()> {
|
||||||
let config = util::config::read_config(&default_config_path, &matches)
|
let config = util::config::read_config(&default_config_path, &matches)
|
||||||
.wrap_err("failed to read config file")?;
|
.wrap_err("failed to read config file")?;
|
||||||
|
|
||||||
let initial_state = State::new(config);
|
let initial_state = {
|
||||||
|
let mut state = State::new(
|
||||||
|
config.path,
|
||||||
|
config.game_dir.unwrap_or_default(),
|
||||||
|
config.data_dir.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter())
|
||||||
|
.wrap_err("failed to load mods")?;
|
||||||
|
state
|
||||||
|
};
|
||||||
|
|
||||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let delegate = Delegate::new(action_tx);
|
let delegate = Delegate::new(action_tx);
|
||||||
|
|
|
@ -3,8 +3,6 @@ use std::{path::PathBuf, sync::Arc};
|
||||||
use druid::{im::Vector, Data, Lens};
|
use druid::{im::Vector, Data, Lens};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
|
|
||||||
use crate::util::config::Config;
|
|
||||||
|
|
||||||
use super::SelectedModLens;
|
use super::SelectedModLens;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
||||||
|
@ -100,7 +98,7 @@ impl State {
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
pub const selected_mod: SelectedModLens = SelectedModLens;
|
||||||
|
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config_path: PathBuf, game_dir: PathBuf, data_dir: PathBuf) -> Self {
|
||||||
let ctx = sdk::Context::new();
|
let ctx = sdk::Context::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
@ -112,9 +110,9 @@ impl State {
|
||||||
is_reset_in_progress: false,
|
is_reset_in_progress: false,
|
||||||
is_save_in_progress: false,
|
is_save_in_progress: false,
|
||||||
is_next_save_pending: false,
|
is_next_save_pending: false,
|
||||||
config_path: Arc::new(config.path),
|
config_path: Arc::new(config_path),
|
||||||
game_dir: Arc::new(config.game_dir.unwrap_or_default()),
|
game_dir: Arc::new(game_dir),
|
||||||
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
|
data_dir: Arc::new(data_dir),
|
||||||
log: Arc::new(String::new()),
|
log: Arc::new(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
.expect("command type matched but didn't contain the expected value");
|
.expect("command type matched but didn't contain the expected value");
|
||||||
|
|
||||||
state.select_mod(*index);
|
state.select_mod(*index);
|
||||||
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
cmd if cmd.is(ACTION_SELECTED_MOD_UP) => {
|
cmd if cmd.is(ACTION_SELECTED_MOD_UP) => {
|
||||||
|
@ -120,6 +121,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
|
|
||||||
state.mods.swap(i, i - 1);
|
state.mods.swap(i, i - 1);
|
||||||
state.selected_mod_index = Some(i - 1);
|
state.selected_mod_index = Some(i - 1);
|
||||||
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => {
|
cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => {
|
||||||
|
@ -134,6 +136,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
|
|
||||||
state.mods.swap(i, i + 1);
|
state.mods.swap(i, i + 1);
|
||||||
state.selected_mod_index = Some(i + 1);
|
state.selected_mod_index = Some(i + 1);
|
||||||
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
||||||
|
@ -162,7 +165,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
};
|
};
|
||||||
|
|
||||||
state.mods.remove(index);
|
state.mods.remove(index);
|
||||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
|
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
|
@ -185,7 +188,7 @@ impl AppDelegate<State> for Delegate {
|
||||||
.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);
|
||||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
// ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
}
|
}
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
mod data;
|
mod data;
|
||||||
mod delegate;
|
mod delegate;
|
||||||
mod lens;
|
mod lens;
|
||||||
mod util;
|
|
||||||
|
|
||||||
pub(crate) use data::*;
|
pub(crate) use data::*;
|
||||||
pub(crate) use delegate::*;
|
pub(crate) use delegate::*;
|
||||||
pub(crate) use lens::*;
|
pub(crate) use lens::*;
|
||||||
pub(crate) use util::*;
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use druid::text::Formatter;
|
|
||||||
use druid::widget::{TextBoxEvent, ValidationDelegate};
|
|
||||||
use druid::EventCtx;
|
|
||||||
|
|
||||||
pub(crate) struct PathBufFormatter;
|
|
||||||
|
|
||||||
impl PathBufFormatter {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Formatter<Arc<PathBuf>> for PathBufFormatter {
|
|
||||||
fn format(&self, value: &Arc<PathBuf>) -> String {
|
|
||||||
value.display().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_partial_input(
|
|
||||||
&self,
|
|
||||||
_input: &str,
|
|
||||||
_sel: &druid::text::Selection,
|
|
||||||
) -> druid::text::Validation {
|
|
||||||
druid::text::Validation::success()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn value(&self, input: &str) -> Result<Arc<PathBuf>, druid::text::ValidationError> {
|
|
||||||
let p = PathBuf::from(input);
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
use druid::widget::{Button, Controller, Scroll};
|
use druid::widget::{Button, Controller, Scroll};
|
||||||
use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, Rect, UpdateCtx, Widget};
|
use druid::{Data, Env, Event, EventCtx, Rect, UpdateCtx, Widget};
|
||||||
|
|
||||||
|
use crate::state::{State, ACTION_START_SAVE_SETTINGS};
|
||||||
|
|
||||||
pub struct DisabledButtonController;
|
pub struct DisabledButtonController;
|
||||||
|
|
||||||
|
@ -19,17 +21,6 @@ impl<T: Data> Controller<T, Button<T>> for DisabledButtonController {
|
||||||
child.event(ctx, event, data, env)
|
child.event(ctx, event, data, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lifecycle(
|
|
||||||
&mut self,
|
|
||||||
child: &mut Button<T>,
|
|
||||||
ctx: &mut LifeCycleCtx,
|
|
||||||
event: &LifeCycle,
|
|
||||||
data: &T,
|
|
||||||
env: &Env,
|
|
||||||
) {
|
|
||||||
child.lifecycle(ctx, event, data, env)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
child: &mut Button<T>,
|
child: &mut Button<T>,
|
||||||
|
@ -65,3 +56,27 @@ impl<T: Data, W: Widget<T>> Controller<T, Scroll<T, W>> for AutoScrollController
|
||||||
child.update(ctx, old_data, data, env)
|
child.update(ctx, old_data, data, env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A controller that submits the command to save settings every time its widget's
|
||||||
|
/// data changes.
|
||||||
|
pub struct SaveSettingsController;
|
||||||
|
|
||||||
|
impl<W: Widget<State>> Controller<State, W> for SaveSettingsController {
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
child: &mut W,
|
||||||
|
ctx: &mut UpdateCtx,
|
||||||
|
old_data: &State,
|
||||||
|
data: &State,
|
||||||
|
env: &Env,
|
||||||
|
) {
|
||||||
|
// Only filter for the values that actually go into the settings file.
|
||||||
|
if old_data.mods != data.mods
|
||||||
|
|| old_data.game_dir != data.game_dir
|
||||||
|
|| old_data.data_dir != data.data_dir
|
||||||
|
{
|
||||||
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
|
}
|
||||||
|
child.update(ctx, old_data, data, env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use druid::text::Formatter;
|
||||||
|
use druid::widget::{TextBoxEvent, ValidationDelegate};
|
||||||
|
use druid::EventCtx;
|
||||||
use druid::{Data, Widget};
|
use druid::{Data, Widget};
|
||||||
|
|
||||||
pub mod container;
|
pub mod container;
|
||||||
|
@ -6,3 +12,46 @@ pub mod controller;
|
||||||
pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static {}
|
pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static {}
|
||||||
|
|
||||||
impl<T: Data, W: Widget<T> + 'static> ExtraWidgetExt<T> for W {}
|
impl<T: Data, W: Widget<T> + 'static> ExtraWidgetExt<T> for W {}
|
||||||
|
|
||||||
|
pub(crate) struct PathBufFormatter;
|
||||||
|
|
||||||
|
impl PathBufFormatter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Formatter<Arc<PathBuf>> for PathBufFormatter {
|
||||||
|
fn format(&self, value: &Arc<PathBuf>) -> String {
|
||||||
|
value.display().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_partial_input(
|
||||||
|
&self,
|
||||||
|
_input: &str,
|
||||||
|
_sel: &druid::text::Selection,
|
||||||
|
) -> druid::text::Validation {
|
||||||
|
druid::text::Validation::success()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(&self, input: &str) -> Result<Arc<PathBuf>, druid::text::ValidationError> {
|
||||||
|
let p = PathBuf::from(input);
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,13 +10,13 @@ use druid::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_ADD_MOD,
|
ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP,
|
||||||
ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
||||||
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
|
ACTION_START_RESET_DEPLOYMENT,
|
||||||
ACTION_START_SAVE_SETTINGS,
|
|
||||||
};
|
};
|
||||||
use crate::ui::theme;
|
use crate::ui::theme;
|
||||||
use crate::ui::widget::controller::AutoScrollController;
|
use crate::ui::widget::controller::{AutoScrollController, SaveSettingsController};
|
||||||
|
use crate::ui::widget::PathBufFormatter;
|
||||||
|
|
||||||
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.);
|
||||||
|
@ -248,9 +248,6 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
TextBox::new()
|
TextBox::new()
|
||||||
.with_formatter(PathBufFormatter::new())
|
.with_formatter(PathBufFormatter::new())
|
||||||
.delegate(TextBoxOnChanged::new(|ctx, _| {
|
|
||||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS)
|
|
||||||
}))
|
|
||||||
.expand_width()
|
.expand_width()
|
||||||
.lens(State::data_dir),
|
.lens(State::data_dir),
|
||||||
1.,
|
1.,
|
||||||
|
@ -265,9 +262,6 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
TextBox::new()
|
TextBox::new()
|
||||||
.with_formatter(PathBufFormatter::new())
|
.with_formatter(PathBufFormatter::new())
|
||||||
.delegate(TextBoxOnChanged::new(|ctx, _| {
|
|
||||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS)
|
|
||||||
}))
|
|
||||||
.expand_width()
|
.expand_width()
|
||||||
.lens(State::game_dir),
|
.lens(State::game_dir),
|
||||||
1.,
|
1.,
|
||||||
|
@ -318,4 +312,5 @@ fn build_window() -> impl Widget<State> {
|
||||||
.with_child(build_top_bar())
|
.with_child(build_top_bar())
|
||||||
.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(SaveSettingsController)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,63 @@
|
||||||
use std::fs;
|
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
use clap::{parser::ValueSource, ArgMatches};
|
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};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub(crate) struct LoadOrderEntrySerialize<'a> {
|
||||||
|
pub id: &'a String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ModInfo> for LoadOrderEntrySerialize<'a> {
|
||||||
|
fn from(info: &'a ModInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: &info.id,
|
||||||
|
enabled: info.enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub(crate) struct ConfigSerialize<'a> {
|
||||||
|
game_dir: &'a Path,
|
||||||
|
data_dir: &'a Path,
|
||||||
|
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a State> for ConfigSerialize<'a> {
|
||||||
|
fn from(state: &'a State) -> Self {
|
||||||
|
Self {
|
||||||
|
game_dir: &state.game_dir,
|
||||||
|
data_dir: &state.data_dir,
|
||||||
|
mod_order: state
|
||||||
|
.mods
|
||||||
|
.iter()
|
||||||
|
.map(LoadOrderEntrySerialize::from)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct LoadOrderEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub data_dir: Option<PathBuf>,
|
pub data_dir: Option<PathBuf>,
|
||||||
pub game_dir: Option<PathBuf>,
|
pub game_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mod_order: Vec<LoadOrderEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(arget_os = "windows"))]
|
#[cfg(not(arget_os = "windows"))]
|
||||||
|
@ -92,6 +138,7 @@ where
|
||||||
path: default_path,
|
path: default_path,
|
||||||
data_dir: Some(get_default_data_dir()),
|
data_dir: Some(get_default_data_dir()),
|
||||||
game_dir: None,
|
game_dir: None,
|
||||||
|
mod_order: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue