Darktide Mod Manager #39
19 changed files with 539 additions and 603 deletions
|
@ -93,7 +93,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<State>) -> Result<()> {
|
||||||
let settings_path = state
|
let settings_path = state
|
||||||
.get_game_dir()
|
.game_dir
|
||||||
.join("bundle/application_settings/settings_common.ini");
|
.join("bundle/application_settings/settings_common.ini");
|
||||||
|
|
||||||
let settings = read_file_with_backup(&settings_path)
|
let settings = read_file_with_backup(&settings_path)
|
||||||
|
@ -121,11 +121,11 @@ async fn patch_game_settings(state: Arc<State>) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(package = info.get_name()))]
|
#[tracing::instrument(skip_all, fields(package = info.name))]
|
||||||
fn make_package(info: &PackageInfo) -> Result<Package> {
|
fn make_package(info: &PackageInfo) -> Result<Package> {
|
||||||
let mut pkg = Package::new(info.get_name().clone(), PathBuf::new());
|
let mut pkg = Package::new(info.name.clone(), PathBuf::new());
|
||||||
|
|
||||||
for f in info.get_files().iter() {
|
for f in &info.files {
|
||||||
let mut it = f.rsplit('.');
|
let mut it = f.rsplit('.');
|
||||||
let file_type = it
|
let file_type = it
|
||||||
.next()
|
.next()
|
||||||
|
@ -144,32 +144,28 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
|
||||||
|
|
||||||
// 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
|
||||||
// and should therefore not show up in the load order.
|
// and should therefore not show up in the load order.
|
||||||
for mod_info in state
|
for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) {
|
||||||
.get_mods()
|
|
||||||
.iter()
|
|
||||||
.filter(|m| m.get_id() != "dml" && m.get_enabled())
|
|
||||||
{
|
|
||||||
lua.push_str(" {\n name = \"");
|
lua.push_str(" {\n name = \"");
|
||||||
lua.push_str(mod_info.get_name());
|
lua.push_str(&mod_info.name);
|
||||||
|
|
||||||
lua.push_str("\",\n id = \"");
|
lua.push_str("\",\n id = \"");
|
||||||
lua.push_str(mod_info.get_id());
|
lua.push_str(&mod_info.id);
|
||||||
|
|
||||||
lua.push_str("\",\n run = function()\n");
|
lua.push_str("\",\n run = function()\n");
|
||||||
|
|
||||||
let resources = mod_info.get_resources();
|
let resources = &mod_info.resources;
|
||||||
if resources.get_data().is_some() || resources.get_localization().is_some() {
|
if resources.data.is_some() || resources.localization.is_some() {
|
||||||
lua.push_str(" new_mod(\"");
|
lua.push_str(" new_mod(\"");
|
||||||
lua.push_str(mod_info.get_id());
|
lua.push_str(&mod_info.id);
|
||||||
lua.push_str("\", {\n init = \"");
|
lua.push_str("\", {\n init = \"");
|
||||||
lua.push_str(&resources.get_init().to_string_lossy());
|
lua.push_str(&resources.init.to_string_lossy());
|
||||||
|
|
||||||
if let Some(data) = resources.get_data() {
|
if let Some(data) = resources.data.as_ref() {
|
||||||
lua.push_str("\",\n data = \"");
|
lua.push_str("\",\n data = \"");
|
||||||
lua.push_str(&data.to_string_lossy());
|
lua.push_str(&data.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(localization) = resources.get_localization() {
|
if let Some(localization) = &resources.localization {
|
||||||
lua.push_str("\",\n localization = \"");
|
lua.push_str("\",\n localization = \"");
|
||||||
lua.push_str(&localization.to_string_lossy());
|
lua.push_str(&localization.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
@ -177,15 +173,15 @@ fn build_mod_data_lua(state: Arc<State>) -> String {
|
||||||
lua.push_str("\",\n })\n");
|
lua.push_str("\",\n })\n");
|
||||||
} else {
|
} else {
|
||||||
lua.push_str(" return dofile(\"");
|
lua.push_str(" return dofile(\"");
|
||||||
lua.push_str(&resources.get_init().to_string_lossy());
|
lua.push_str(&resources.init.to_string_lossy());
|
||||||
lua.push_str("\")\n");
|
lua.push_str("\")\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
lua.push_str(" end,\n packages = {\n");
|
lua.push_str(" end,\n packages = {\n");
|
||||||
|
|
||||||
for pkg_info in mod_info.get_packages() {
|
for pkg_info in &mod_info.packages {
|
||||||
lua.push_str(" \"");
|
lua.push_str(" \"");
|
||||||
lua.push_str(pkg_info.get_name());
|
lua.push_str(&pkg_info.name);
|
||||||
lua.push_str("\",\n");
|
lua.push_str("\",\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,10 +197,10 @@ 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<State>) -> Result<Vec<Bundle>> {
|
||||||
let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME);
|
let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string());
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
let bundle_dir = Arc::new(state.get_game_dir().join("bundle"));
|
let bundle_dir = Arc::new(state.game_dir.join("bundle"));
|
||||||
|
|
||||||
let mut bundles = Vec::new();
|
let mut bundles = Vec::new();
|
||||||
|
|
||||||
|
@ -220,17 +216,13 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
mod_bundle.add_file(file);
|
mod_bundle.add_file(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
for mod_info in state
|
for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) {
|
||||||
.get_mods()
|
let span = tracing::trace_span!("building mod packages", name = mod_info.name);
|
||||||
.iter()
|
|
||||||
.filter(|m| m.get_id() != "dml" && m.get_enabled())
|
|
||||||
{
|
|
||||||
let span = tracing::trace_span!("building mod packages", name = mod_info.get_name());
|
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
|
|
||||||
let mod_dir = state.get_mod_dir().join(mod_info.get_id());
|
let mod_dir = state.get_mod_dir().join(&mod_info.id);
|
||||||
for pkg_info in mod_info.get_packages() {
|
for pkg_info in &mod_info.packages {
|
||||||
let span = tracing::trace_span!("building package", name = pkg_info.get_name());
|
let span = tracing::trace_span!("building package", name = pkg_info.name);
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
|
|
||||||
let pkg = make_package(pkg_info).wrap_err("failed to make package")?;
|
let pkg = make_package(pkg_info).wrap_err("failed to make package")?;
|
||||||
|
@ -239,24 +231,24 @@ async fn build_bundles(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
.to_binary()
|
.to_binary()
|
||||||
.wrap_err("failed to serialize package to binary")?;
|
.wrap_err("failed to serialize package to binary")?;
|
||||||
variant.set_data(bin);
|
variant.set_data(bin);
|
||||||
let mut file = BundleFile::new(pkg_info.get_name().clone(), BundleFileType::Package);
|
let mut file = BundleFile::new(pkg_info.name.clone(), BundleFileType::Package);
|
||||||
file.add_variant(variant);
|
file.add_variant(variant);
|
||||||
|
|
||||||
mod_bundle.add_file(file);
|
mod_bundle.add_file(file);
|
||||||
|
|
||||||
let bundle_name = Murmur64::hash(pkg_info.get_name())
|
let bundle_name = Murmur64::hash(&pkg_info.name)
|
||||||
.to_string()
|
.to_string()
|
||||||
.to_ascii_lowercase();
|
.to_ascii_lowercase();
|
||||||
let src = mod_dir.join(&bundle_name);
|
let src = mod_dir.join(&bundle_name);
|
||||||
let dest = bundle_dir.join(&bundle_name);
|
let dest = bundle_dir.join(&bundle_name);
|
||||||
let pkg_name = pkg_info.get_name().clone();
|
let pkg_name = pkg_info.name.clone();
|
||||||
let mod_name = mod_info.get_name().clone();
|
let mod_name = mod_info.name.clone();
|
||||||
|
|
||||||
// Explicitely drop the guard, so that we can move the span
|
// Explicitely drop the guard, so that we can move the span
|
||||||
// into the async operation
|
// into the async operation
|
||||||
drop(_enter);
|
drop(_enter);
|
||||||
|
|
||||||
let ctx = state.get_ctx().clone();
|
let ctx = state.ctx.clone();
|
||||||
|
|
||||||
let task = async move {
|
let task = async move {
|
||||||
let bundle = {
|
let bundle = {
|
||||||
|
@ -322,7 +314,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<State>) -> Result<Vec<Bundle>> {
|
||||||
let bundle_dir = Arc::new(state.get_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())));
|
||||||
|
|
||||||
let mut bundles = Vec::with_capacity(2);
|
let mut bundles = Vec::with_capacity(2);
|
||||||
|
@ -332,7 +324,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
.await
|
.await
|
||||||
.wrap_err("failed to read boot bundle")?;
|
.wrap_err("failed to read boot bundle")?;
|
||||||
|
|
||||||
Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin)
|
Bundle::from_binary(&state.ctx, BOOT_BUNDLE_NAME.to_string(), bin)
|
||||||
.wrap_err("failed to parse boot bundle")
|
.wrap_err("failed to parse boot bundle")
|
||||||
}
|
}
|
||||||
.instrument(tracing::trace_span!("read boot bundle"))
|
.instrument(tracing::trace_span!("read boot bundle"))
|
||||||
|
@ -346,9 +338,9 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
|
|
||||||
let mut pkg = Package::new(MOD_BUNDLE_NAME.to_string(), PathBuf::new());
|
let mut pkg = Package::new(MOD_BUNDLE_NAME.to_string(), PathBuf::new());
|
||||||
|
|
||||||
for mod_info in state.get_mods() {
|
for mod_info in &state.mods {
|
||||||
for pkg_info in mod_info.get_packages() {
|
for pkg_info in &mod_info.packages {
|
||||||
pkg.add_file(BundleFileType::Package, pkg_info.get_name());
|
pkg.add_file(BundleFileType::Package, &pkg_info.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,32 +361,28 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
|
|
||||||
let mut variant = BundleFileVariant::new();
|
let mut variant = BundleFileVariant::new();
|
||||||
|
|
||||||
let mods = state.get_mods();
|
let mod_info = state
|
||||||
let mod_info = mods
|
.mods
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| m.get_id() == "dml")
|
.find(|m| m.id == "dml")
|
||||||
.ok_or_else(|| eyre::eyre!("DML not found in mod list"))?;
|
.ok_or_else(|| eyre::eyre!("DML not found in mod list"))?;
|
||||||
let pkg_info = mod_info
|
let pkg_info = mod_info
|
||||||
.get_packages()
|
.packages
|
||||||
.get(0)
|
.get(0)
|
||||||
.ok_or_else(|| eyre::eyre!("invalid mod package for DML"))
|
.ok_or_else(|| eyre::eyre!("invalid mod package for DML"))
|
||||||
.with_suggestion(|| "Re-download and import the newest version.".to_string())?;
|
.with_suggestion(|| "Re-download and import the newest version.".to_string())?;
|
||||||
let bundle_name = Murmur64::hash(pkg_info.get_name())
|
let bundle_name = Murmur64::hash(&pkg_info.name)
|
||||||
.to_string()
|
.to_string()
|
||||||
.to_ascii_lowercase();
|
.to_ascii_lowercase();
|
||||||
let src = state
|
let src = state.get_mod_dir().join(&mod_info.id).join(&bundle_name);
|
||||||
.get_mod_dir()
|
|
||||||
.join(mod_info.get_id())
|
|
||||||
.join(&bundle_name);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let ctx = state.get_ctx();
|
|
||||||
let bin = fs::read(&src)
|
let bin = fs::read(&src)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("failed to read bundle file '{}'", src.display()))?;
|
.wrap_err_with(|| format!("failed to read bundle file '{}'", src.display()))?;
|
||||||
let name = Bundle::get_name_from_path(&ctx, &src);
|
let name = Bundle::get_name_from_path(&state.ctx, &src);
|
||||||
|
|
||||||
let dml_bundle = Bundle::from_binary(&ctx, name, bin)
|
let dml_bundle = Bundle::from_binary(&state.ctx, name, bin)
|
||||||
.wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))?;
|
.wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))?;
|
||||||
|
|
||||||
bundles.push(dml_bundle);
|
bundles.push(dml_bundle);
|
||||||
|
@ -402,8 +390,8 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
|
|
||||||
{
|
{
|
||||||
let dest = bundle_dir.join(&bundle_name);
|
let dest = bundle_dir.join(&bundle_name);
|
||||||
let pkg_name = pkg_info.get_name().clone();
|
let pkg_name = pkg_info.name.clone();
|
||||||
let mod_name = mod_info.get_name().clone();
|
let mod_name = mod_info.name.clone();
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Copying bundle {} for mod {}: {} -> {}",
|
"Copying bundle {} for mod {}: {} -> {}",
|
||||||
|
@ -441,7 +429,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
let span = tracing::debug_span!("Importing mod main script");
|
let span = tracing::debug_span!("Importing mod main script");
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
|
|
||||||
let lua = include_str!("../assets/mod_main.lua");
|
let lua = include_str!("../../assets/mod_main.lua");
|
||||||
let lua = CString::new(lua).wrap_err("failed to build CString from mod main Lua string")?;
|
let lua = CString::new(lua).wrap_err("failed to build CString from mod main Lua string")?;
|
||||||
let file =
|
let file =
|
||||||
lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?;
|
lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?;
|
||||||
|
@ -467,7 +455,7 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<Vec<Bundle>> {
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(bundles = bundles.len()))]
|
#[tracing::instrument(skip_all, fields(bundles = bundles.len()))]
|
||||||
async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Result<()> {
|
async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Result<()> {
|
||||||
let bundle_dir = Arc::new(state.get_game_dir().join("bundle"));
|
let bundle_dir = Arc::new(state.game_dir.join("bundle"));
|
||||||
let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME);
|
let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME);
|
||||||
|
|
||||||
let mut db = {
|
let mut db = {
|
||||||
|
@ -501,16 +489,15 @@ async fn patch_bundle_database(state: Arc<State>, bundles: Vec<Bundle>) -> Resul
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(
|
#[tracing::instrument(skip_all, fields(
|
||||||
game_dir = %state.get_game_dir().display(),
|
game_dir = %state.game_dir.display(),
|
||||||
mods = state.get_mods().len()
|
mods = state.mods.len()
|
||||||
))]
|
))]
|
||||||
pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
||||||
let state = Arc::new(state);
|
let state = Arc::new(state);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mods = state.get_mods();
|
let first = state.mods.get(0);
|
||||||
let first = mods.get(0);
|
if first.is_none() || !(first.unwrap().id == "dml" && first.unwrap().enabled) {
|
||||||
if first.is_none() || !(first.unwrap().get_id() == "dml" && first.unwrap().get_enabled()) {
|
|
||||||
// TODO: Add a suggestion where to get it, once that's published
|
// TODO: Add a suggestion where to get it, once that's published
|
||||||
eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order");
|
eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order");
|
||||||
}
|
}
|
||||||
|
@ -518,8 +505,8 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Deploying {} mods to {}",
|
"Deploying {} mods to {}",
|
||||||
state.get_mods().len(),
|
state.mods.len(),
|
||||||
state.get_game_dir().join("bundle").display()
|
state.game_dir.join("bundle").display()
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!("Build mod bundles");
|
tracing::info!("Build mod bundles");
|
||||||
|
@ -550,7 +537,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: State) -> Result<()> {
|
||||||
let paths = [BUNDLE_DATABASE_NAME, BOOT_BUNDLE_NAME];
|
let paths = [BUNDLE_DATABASE_NAME, BOOT_BUNDLE_NAME];
|
||||||
let bundle_dir = state.get_game_dir().join("bundle");
|
let bundle_dir = state.game_dir.join("bundle");
|
||||||
|
|
||||||
tracing::info!("Resetting mod deployment in {}", bundle_dir.display());
|
tracing::info!("Resetting mod deployment in {}", bundle_dir.display());
|
||||||
|
|
||||||
|
@ -664,7 +651,7 @@ 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: State, info: &ModInfo) -> Result<()> {
|
||||||
let mod_dir = state.get_mod_dir().join(info.get_id());
|
let mod_dir = state.get_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()))?;
|
|
@ -6,7 +6,7 @@ use tokio::runtime::Runtime;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::engine::*;
|
use crate::controller::engine::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
async fn handle_action(
|
async fn handle_action(
|
|
@ -12,24 +12,25 @@ use color_eyre::{Report, Result};
|
||||||
use druid::AppLauncher;
|
use druid::AppLauncher;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::controller::worker::work_thread;
|
||||||
use crate::state::{Delegate, State};
|
use crate::state::{Delegate, State};
|
||||||
use crate::worker::work_thread;
|
|
||||||
|
|
||||||
mod controller;
|
mod controller {
|
||||||
mod engine;
|
pub mod engine;
|
||||||
mod log;
|
pub mod worker;
|
||||||
mod main_window;
|
}
|
||||||
mod state;
|
mod state;
|
||||||
mod theme;
|
mod util {
|
||||||
mod util;
|
pub mod config;
|
||||||
mod widget;
|
pub mod log;
|
||||||
mod worker;
|
}
|
||||||
|
mod ui;
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let default_config_path = util::get_default_config_path();
|
let default_config_path = util::config::get_default_config_path();
|
||||||
|
|
||||||
tracing::trace!(default_config_path = %default_config_path.display());
|
tracing::trace!(default_config_path = %default_config_path.display());
|
||||||
|
|
||||||
|
@ -51,21 +52,21 @@ fn main() -> Result<()> {
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
log::create_tracing_subscriber(log_tx);
|
util::log::create_tracing_subscriber(log_tx);
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
oodle_sys::init(matches.get_one::<String>("oodle"));
|
oodle_sys::init(matches.get_one::<String>("oodle"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let config =
|
let config = util::config::read_config(&default_config_path, &matches)
|
||||||
util::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 = State::new(config);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
|
let launcher = AppLauncher::with_window(ui::window::main::new()).delegate(delegate);
|
||||||
|
|
||||||
let event_sink = launcher.get_external_handle();
|
let event_sink = launcher.get_external_handle();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|
|
@ -1,510 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use druid::im::Vector;
|
|
||||||
use druid::text::Formatter;
|
|
||||||
use druid::{
|
|
||||||
AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse,
|
|
||||||
Target,
|
|
||||||
};
|
|
||||||
use dtmt_shared::ModConfig;
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
|
|
||||||
use crate::util::Config;
|
|
||||||
|
|
||||||
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<ModInfo>> =
|
|
||||||
Selector::new("dtmm.action.srart-delete-selected-mod");
|
|
||||||
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<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<ModInfo>> =
|
|
||||||
Selector::new("dtmm.action.finish-add-mod");
|
|
||||||
|
|
||||||
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
|
||||||
pub(crate) enum View {
|
|
||||||
Mods,
|
|
||||||
Settings,
|
|
||||||
About,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for View {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Mods
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Data, Debug)]
|
|
||||||
pub struct PackageInfo {
|
|
||||||
name: String,
|
|
||||||
files: Vector<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PackageInfo {
|
|
||||||
pub fn new(name: String, files: Vector<String>) -> Self {
|
|
||||||
Self { name, files }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(&self) -> &String {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_files(&self) -> &Vector<String> {
|
|
||||||
&self.files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct ModResourceInfo {
|
|
||||||
init: PathBuf,
|
|
||||||
data: Option<PathBuf>,
|
|
||||||
localization: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModResourceInfo {
|
|
||||||
pub(crate) fn get_init(&self) -> &PathBuf {
|
|
||||||
&self.init
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_data(&self) -> Option<&PathBuf> {
|
|
||||||
self.data.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_localization(&self) -> Option<&PathBuf> {
|
|
||||||
self.localization.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Data, Debug, Lens)]
|
|
||||||
pub(crate) struct ModInfo {
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
description: Arc<String>,
|
|
||||||
enabled: bool,
|
|
||||||
#[lens(ignore)]
|
|
||||||
#[data(ignore)]
|
|
||||||
packages: Vector<PackageInfo>,
|
|
||||||
#[lens(ignore)]
|
|
||||||
#[data(ignore)]
|
|
||||||
resources: ModResourceInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModInfo {
|
|
||||||
pub fn new(cfg: ModConfig, packages: Vector<PackageInfo>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: cfg.id,
|
|
||||||
name: cfg.name,
|
|
||||||
description: Arc::new(cfg.description),
|
|
||||||
enabled: false,
|
|
||||||
packages,
|
|
||||||
resources: ModResourceInfo {
|
|
||||||
init: cfg.resources.init,
|
|
||||||
data: cfg.resources.data,
|
|
||||||
localization: cfg.resources.localization,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_packages(&self) -> &Vector<PackageInfo> {
|
|
||||||
&self.packages
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_name(&self) -> &String {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_id(&self) -> &String {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_enabled(&self) -> bool {
|
|
||||||
self.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_resources(&self) -> &ModResourceInfo {
|
|
||||||
&self.resources
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for ModInfo {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.name.eq(&other.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Data, Lens)]
|
|
||||||
pub(crate) struct State {
|
|
||||||
current_view: View,
|
|
||||||
mods: Vector<ModInfo>,
|
|
||||||
selected_mod_index: Option<usize>,
|
|
||||||
is_deployment_in_progress: bool,
|
|
||||||
is_reset_in_progress: bool,
|
|
||||||
game_dir: Arc<PathBuf>,
|
|
||||||
data_dir: Arc<PathBuf>,
|
|
||||||
ctx: Arc<sdk::Context>,
|
|
||||||
log: Arc<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
#[allow(non_upper_case_globals)]
|
|
||||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
|
||||||
|
|
||||||
pub fn new(config: Config) -> Self {
|
|
||||||
let ctx = sdk::Context::new();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
ctx: Arc::new(ctx),
|
|
||||||
current_view: View::default(),
|
|
||||||
mods: Vector::new(),
|
|
||||||
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()),
|
|
||||||
log: Arc::new(String::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_current_view(&self) -> View {
|
|
||||||
self.current_view
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_view(&mut self, view: View) {
|
|
||||||
self.current_view = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mods(&self) -> Vector<ModInfo> {
|
|
||||||
self.mods.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_mod(&mut self, index: usize) {
|
|
||||||
self.selected_mod_index = Some(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_mod(&mut self, info: ModInfo) {
|
|
||||||
if let Some(pos) = self.mods.index_of(&info) {
|
|
||||||
self.mods.set(pos, info);
|
|
||||||
self.selected_mod_index = Some(pos);
|
|
||||||
} else {
|
|
||||||
self.mods.push_back(info);
|
|
||||||
self.selected_mod_index = Some(self.mods.len() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_move_mod_down(&self) -> bool {
|
|
||||||
self.selected_mod_index
|
|
||||||
.map(|i| i < (self.mods.len().saturating_sub(1)))
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_move_mod_up(&self) -> bool {
|
|
||||||
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_deploy_mods(&self) -> bool {
|
|
||||||
!self.is_deployment_in_progress
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_reset_deployment(&self) -> bool {
|
|
||||||
!self.is_reset_in_progress
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_game_dir(&self) -> &PathBuf {
|
|
||||||
&self.game_dir
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_mod_dir(&self) -> PathBuf {
|
|
||||||
self.data_dir.join("mods")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
|
|
||||||
self.ctx.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_log_line(&mut self, line: String) {
|
|
||||||
let log = Arc::make_mut(&mut self.log);
|
|
||||||
log.push_str(&line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct SelectedModLens;
|
|
||||||
|
|
||||||
impl Lens<State, Option<ModInfo>> for SelectedModLens {
|
|
||||||
#[tracing::instrument(name = "SelectedModLens::with", skip_all)]
|
|
||||||
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
|
|
||||||
let info = data
|
|
||||||
.selected_mod_index
|
|
||||||
.and_then(|i| data.mods.get(i).cloned());
|
|
||||||
|
|
||||||
f(&info)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)]
|
|
||||||
fn with_mut<V, F: FnOnce(&mut Option<ModInfo>) -> V>(&self, data: &mut State, f: F) -> V {
|
|
||||||
match data.selected_mod_index {
|
|
||||||
Some(i) => {
|
|
||||||
let mut info = data.mods.get_mut(i).cloned();
|
|
||||||
let ret = f(&mut info);
|
|
||||||
|
|
||||||
if let Some(info) = info {
|
|
||||||
// TODO: Figure out a way to check for equality and
|
|
||||||
// only update when needed
|
|
||||||
data.mods.set(i, info);
|
|
||||||
} else {
|
|
||||||
data.selected_mod_index = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
None => f(&mut None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Lens that maps an `im::Vector<T>` to `im::Vector<(usize, T)>`,
|
|
||||||
/// where each element in the destination vector includes its index in the
|
|
||||||
/// source vector.
|
|
||||||
pub(crate) struct IndexedVectorLens;
|
|
||||||
|
|
||||||
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
|
|
||||||
#[tracing::instrument(name = "IndexedVectorLens::with", skip_all)]
|
|
||||||
fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, values: &Vector<T>, f: F) -> V {
|
|
||||||
let indexed = values
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, val)| (i, val.clone()))
|
|
||||||
.collect();
|
|
||||||
f(&indexed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)]
|
|
||||||
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
|
|
||||||
&self,
|
|
||||||
values: &mut Vector<T>,
|
|
||||||
f: F,
|
|
||||||
) -> V {
|
|
||||||
let mut indexed = values
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, val)| (i, val.clone()))
|
|
||||||
.collect();
|
|
||||||
let ret = f(&mut indexed);
|
|
||||||
|
|
||||||
*values = indexed.into_iter().map(|(_i, val)| val).collect();
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) enum AsyncAction {
|
|
||||||
DeployMods(State),
|
|
||||||
ResetDeployment(State),
|
|
||||||
AddMod((State, FileInfo)),
|
|
||||||
DeleteMod((State, ModInfo)),
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
match cmd {
|
|
||||||
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
|
||||||
if self
|
|
||||||
.sender
|
|
||||||
.send(AsyncAction::DeployMods(state.clone()))
|
|
||||||
.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;
|
|
||||||
Handled::Yes
|
|
||||||
}
|
|
||||||
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
|
|
||||||
if self
|
|
||||||
.sender
|
|
||||||
.send(AsyncAction::ResetDeployment(state.clone()))
|
|
||||||
.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);
|
|
||||||
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);
|
|
||||||
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);
|
|
||||||
Handled::Yes
|
|
||||||
}
|
|
||||||
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
|
||||||
let info = cmd
|
|
||||||
.get(ACTION_START_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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 let Err(err) = self
|
|
||||||
.sender
|
|
||||||
.send(AsyncAction::AddMod((state.clone(), info.clone())))
|
|
||||||
{
|
|
||||||
tracing::error!("Failed to add mod: {}", err);
|
|
||||||
}
|
|
||||||
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() {
|
|
||||||
state.add_log_line(line);
|
|
||||||
}
|
|
||||||
Handled::Yes
|
|
||||||
}
|
|
||||||
cmd => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
tracing::warn!("Unknown command: {:?}", cmd);
|
|
||||||
}
|
|
||||||
Handled::No
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
144
crates/dtmm/src/state/data.rs
Normal file
144
crates/dtmm/src/state/data.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use druid::{im::Vector, Data, Lens};
|
||||||
|
use dtmt_shared::ModConfig;
|
||||||
|
|
||||||
|
use crate::util::config::Config;
|
||||||
|
|
||||||
|
use super::SelectedModLens;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
||||||
|
pub(crate) enum View {
|
||||||
|
Mods,
|
||||||
|
Settings,
|
||||||
|
About,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for View {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Mods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Data, Debug)]
|
||||||
|
pub struct PackageInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub files: Vector<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageInfo {
|
||||||
|
pub fn new(name: String, files: Vector<String>) -> Self {
|
||||||
|
Self { name, files }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct ModResourceInfo {
|
||||||
|
pub init: PathBuf,
|
||||||
|
pub data: Option<PathBuf>,
|
||||||
|
pub localization: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Data, Debug, Lens)]
|
||||||
|
pub(crate) struct ModInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Arc<String>,
|
||||||
|
pub enabled: bool,
|
||||||
|
#[lens(ignore)]
|
||||||
|
#[data(ignore)]
|
||||||
|
pub packages: Vector<PackageInfo>,
|
||||||
|
#[lens(ignore)]
|
||||||
|
#[data(ignore)]
|
||||||
|
pub resources: ModResourceInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModInfo {
|
||||||
|
pub fn new(cfg: ModConfig, packages: Vector<PackageInfo>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: cfg.id,
|
||||||
|
name: cfg.name,
|
||||||
|
description: Arc::new(cfg.description),
|
||||||
|
enabled: false,
|
||||||
|
packages,
|
||||||
|
resources: ModResourceInfo {
|
||||||
|
init: cfg.resources.init,
|
||||||
|
data: cfg.resources.data,
|
||||||
|
localization: cfg.resources.localization,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ModInfo {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name.eq(&other.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Data, Lens)]
|
||||||
|
pub(crate) struct State {
|
||||||
|
pub current_view: View,
|
||||||
|
pub mods: Vector<ModInfo>,
|
||||||
|
pub selected_mod_index: Option<usize>,
|
||||||
|
pub is_deployment_in_progress: bool,
|
||||||
|
pub is_reset_in_progress: bool,
|
||||||
|
pub game_dir: Arc<PathBuf>,
|
||||||
|
pub data_dir: Arc<PathBuf>,
|
||||||
|
pub ctx: Arc<sdk::Context>,
|
||||||
|
pub log: Arc<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
pub const selected_mod: SelectedModLens = SelectedModLens;
|
||||||
|
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let ctx = sdk::Context::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ctx: Arc::new(ctx),
|
||||||
|
current_view: View::default(),
|
||||||
|
mods: Vector::new(),
|
||||||
|
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()),
|
||||||
|
log: Arc::new(String::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_mod(&mut self, index: usize) {
|
||||||
|
self.selected_mod_index = Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_mod(&mut self, info: ModInfo) {
|
||||||
|
if let Some(pos) = self.mods.index_of(&info) {
|
||||||
|
self.mods.set(pos, info);
|
||||||
|
self.selected_mod_index = Some(pos);
|
||||||
|
} else {
|
||||||
|
self.mods.push_back(info);
|
||||||
|
self.selected_mod_index = Some(self.mods.len() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_move_mod_down(&self) -> bool {
|
||||||
|
self.selected_mod_index
|
||||||
|
.map(|i| i < (self.mods.len().saturating_sub(1)))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_move_mod_up(&self) -> bool {
|
||||||
|
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_mod_dir(&self) -> PathBuf {
|
||||||
|
self.data_dir.join("mods")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_log_line(&mut self, line: String) {
|
||||||
|
let log = Arc::make_mut(&mut self.log);
|
||||||
|
log.push_str(&line);
|
||||||
|
}
|
||||||
|
}
|
197
crates/dtmm/src/state/delegate.rs
Normal file
197
crates/dtmm/src/state/delegate.rs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
use druid::{
|
||||||
|
AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target,
|
||||||
|
};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
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<ModInfo>> =
|
||||||
|
Selector::new("dtmm.action.srart-delete-selected-mod");
|
||||||
|
pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector<SingleUse<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<ModInfo>> =
|
||||||
|
Selector::new("dtmm.action.finish-add-mod");
|
||||||
|
|
||||||
|
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
||||||
|
|
||||||
|
pub(crate) enum AsyncAction {
|
||||||
|
DeployMods(State),
|
||||||
|
ResetDeployment(State),
|
||||||
|
AddMod((State, FileInfo)),
|
||||||
|
DeleteMod((State, ModInfo)),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
match cmd {
|
||||||
|
cmd if cmd.is(ACTION_START_DEPLOY) => {
|
||||||
|
if self
|
||||||
|
.sender
|
||||||
|
.send(AsyncAction::DeployMods(state.clone()))
|
||||||
|
.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;
|
||||||
|
Handled::Yes
|
||||||
|
}
|
||||||
|
cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => {
|
||||||
|
if self
|
||||||
|
.sender
|
||||||
|
.send(AsyncAction::ResetDeployment(state.clone()))
|
||||||
|
.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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
Handled::Yes
|
||||||
|
}
|
||||||
|
cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => {
|
||||||
|
let info = cmd
|
||||||
|
.get(ACTION_START_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 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 let Err(err) = self
|
||||||
|
.sender
|
||||||
|
.send(AsyncAction::AddMod((state.clone(), info.clone())))
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to add mod: {}", err);
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
state.add_log_line(line);
|
||||||
|
}
|
||||||
|
Handled::Yes
|
||||||
|
}
|
||||||
|
cmd => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
tracing::warn!("Unknown command: {:?}", cmd);
|
||||||
|
}
|
||||||
|
Handled::No
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
crates/dtmm/src/state/lens.rs
Normal file
73
crates/dtmm/src/state/lens.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use druid::im::Vector;
|
||||||
|
use druid::{Data, Lens};
|
||||||
|
|
||||||
|
use super::{ModInfo, State};
|
||||||
|
|
||||||
|
pub(crate) struct SelectedModLens;
|
||||||
|
|
||||||
|
impl Lens<State, Option<ModInfo>> for SelectedModLens {
|
||||||
|
#[tracing::instrument(name = "SelectedModLens::with", skip_all)]
|
||||||
|
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
|
||||||
|
let info = data
|
||||||
|
.selected_mod_index
|
||||||
|
.and_then(|i| data.mods.get(i).cloned());
|
||||||
|
|
||||||
|
f(&info)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)]
|
||||||
|
fn with_mut<V, F: FnOnce(&mut Option<ModInfo>) -> V>(&self, data: &mut State, f: F) -> V {
|
||||||
|
match data.selected_mod_index {
|
||||||
|
Some(i) => {
|
||||||
|
let mut info = data.mods.get_mut(i).cloned();
|
||||||
|
let ret = f(&mut info);
|
||||||
|
|
||||||
|
if let Some(info) = info {
|
||||||
|
// TODO: Figure out a way to check for equality and
|
||||||
|
// only update when needed
|
||||||
|
data.mods.set(i, info);
|
||||||
|
} else {
|
||||||
|
data.selected_mod_index = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
None => f(&mut None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Lens that maps an `im::Vector<T>` to `im::Vector<(usize, T)>`,
|
||||||
|
/// where each element in the destination vector includes its index in the
|
||||||
|
/// source vector.
|
||||||
|
pub(crate) struct IndexedVectorLens;
|
||||||
|
|
||||||
|
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
|
||||||
|
#[tracing::instrument(name = "IndexedVectorLens::with", skip_all)]
|
||||||
|
fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, values: &Vector<T>, f: F) -> V {
|
||||||
|
let indexed = values
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, val)| (i, val.clone()))
|
||||||
|
.collect();
|
||||||
|
f(&indexed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)]
|
||||||
|
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
|
||||||
|
&self,
|
||||||
|
values: &mut Vector<T>,
|
||||||
|
f: F,
|
||||||
|
) -> V {
|
||||||
|
let mut indexed = values
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, val)| (i, val.clone()))
|
||||||
|
.collect();
|
||||||
|
let ret = f(&mut indexed);
|
||||||
|
|
||||||
|
*values = indexed.into_iter().map(|(_i, val)| val).collect();
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
9
crates/dtmm/src/state/mod.rs
Normal file
9
crates/dtmm/src/state/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
mod data;
|
||||||
|
mod delegate;
|
||||||
|
mod lens;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub(crate) use data::*;
|
||||||
|
pub(crate) use delegate::*;
|
||||||
|
pub(crate) use lens::*;
|
||||||
|
pub(crate) use util::*;
|
31
crates/dtmm/src/state/util.rs
Normal file
31
crates/dtmm/src/state/util.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use druid::text::Formatter;
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
5
crates/dtmm/src/ui/mod.rs
Normal file
5
crates/dtmm/src/ui/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod theme;
|
||||||
|
pub mod widget;
|
||||||
|
pub mod window {
|
||||||
|
pub mod main;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use druid::{Data, Widget};
|
||||||
use self::fill_container::FillContainer;
|
use self::fill_container::FillContainer;
|
||||||
|
|
||||||
pub mod container;
|
pub mod container;
|
||||||
|
pub mod controller;
|
||||||
pub mod fill_container;
|
pub mod fill_container;
|
||||||
|
|
||||||
pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static {
|
pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static {
|
|
@ -13,8 +13,8 @@ use crate::state::{
|
||||||
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
||||||
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
||||||
};
|
};
|
||||||
use crate::theme;
|
use crate::ui::theme;
|
||||||
use crate::widget::ExtraWidgetExt;
|
use crate::ui::widget::ExtraWidgetExt;
|
||||||
|
|
||||||
const TITLE: &str = "Darktide Mod Manager";
|
const TITLE: &str = "Darktide Mod Manager";
|
||||||
const WINDOW_SIZE: (f64, f64) = (800.0, 600.0);
|
const WINDOW_SIZE: (f64, f64) = (800.0, 600.0);
|
||||||
|
@ -33,21 +33,19 @@ fn build_top_bar() -> impl Widget<State> {
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Button::new("Mods").on_click(|_ctx, state: &mut State, _env| {
|
Button::new("Mods")
|
||||||
state.set_current_view(View::Mods)
|
.on_click(|_ctx, state: &mut State, _env| state.current_view = View::Mods),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.with_default_spacer()
|
.with_default_spacer()
|
||||||
.with_child(
|
.with_child(
|
||||||
Button::new("Settings").on_click(|_ctx, state: &mut State, _env| {
|
Button::new("Settings").on_click(|_ctx, state: &mut State, _env| {
|
||||||
state.set_current_view(View::Settings)
|
state.current_view = View::Settings;
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.with_default_spacer()
|
.with_default_spacer()
|
||||||
.with_child(
|
.with_child(
|
||||||
Button::new("About").on_click(|_ctx, state: &mut State, _env| {
|
Button::new("About")
|
||||||
state.set_current_view(View::About)
|
.on_click(|_ctx, state: &mut State, _env| state.current_view = View::About),
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -57,7 +55,7 @@ fn build_top_bar() -> impl Widget<State> {
|
||||||
.on_click(|ctx, _state: &mut State, _env| {
|
.on_click(|ctx, _state: &mut State, _env| {
|
||||||
ctx.submit_command(ACTION_START_DEPLOY);
|
ctx.submit_command(ACTION_START_DEPLOY);
|
||||||
})
|
})
|
||||||
.disabled_if(|data, _| !data.can_deploy_mods()),
|
.disabled_if(|data, _| !data.is_deployment_in_progress),
|
||||||
)
|
)
|
||||||
.with_default_spacer()
|
.with_default_spacer()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -65,7 +63,7 @@ fn build_top_bar() -> impl Widget<State> {
|
||||||
.on_click(|ctx, _state: &mut State, _env| {
|
.on_click(|ctx, _state: &mut State, _env| {
|
||||||
ctx.submit_command(ACTION_START_RESET_DEPLOYMENT);
|
ctx.submit_command(ACTION_START_RESET_DEPLOYMENT);
|
||||||
})
|
})
|
||||||
.disabled_if(|data, _| !data.can_reset_deployment()),
|
.disabled_if(|data, _| !data.is_reset_in_progress),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.padding(theme::TOP_BAR_INSETS)
|
.padding(theme::TOP_BAR_INSETS)
|
||||||
|
@ -253,7 +251,7 @@ fn build_view_about() -> impl Widget<State> {
|
||||||
|
|
||||||
fn build_main() -> impl Widget<State> {
|
fn build_main() -> impl Widget<State> {
|
||||||
ViewSwitcher::new(
|
ViewSwitcher::new(
|
||||||
|state: &State, _env| state.get_current_view(),
|
|state: &State, _env| state.current_view,
|
||||||
|selector, _state, _env| match selector {
|
|selector, _state, _env| match selector {
|
||||||
View::Mods => Box::new(build_view_mods()),
|
View::Mods => Box::new(build_view_mods()),
|
||||||
View::Settings => Box::new(build_view_settings()),
|
View::Settings => Box::new(build_view_settings()),
|
Loading…
Add table
Reference in a new issue