parent
65c0974de2
commit
978701bed8
12 changed files with 289 additions and 59 deletions
60
Cargo.lock
generated
60
Cargo.lock
generated
|
@ -178,6 +178,12 @@ version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
|
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
|
@ -399,6 +405,12 @@ dependencies = [
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color_quant"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "confy"
|
name = "confy"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -725,6 +737,7 @@ dependencies = [
|
||||||
name = "dtmt"
|
name = "dtmt"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-recursion",
|
||||||
"clap",
|
"clap",
|
||||||
"cli-table",
|
"cli-table",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
@ -1314,6 +1327,21 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.24.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder",
|
||||||
|
"color_quant",
|
||||||
|
"jpeg-decoder",
|
||||||
|
"num-rational 0.4.1",
|
||||||
|
"num-traits 0.2.15",
|
||||||
|
"png",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -1398,6 +1426,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jpeg-decoder"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.61"
|
version = "0.3.61"
|
||||||
|
@ -1635,7 +1669,7 @@ dependencies = [
|
||||||
"num-complex",
|
"num-complex",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-rational",
|
"num-rational 0.3.2",
|
||||||
"num-traits 0.2.15",
|
"num-traits 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1692,6 +1726,17 @@ dependencies = [
|
||||||
"num-traits 0.2.15",
|
"num-traits 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-rational"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits 0.2.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.1.43"
|
version = "0.1.43"
|
||||||
|
@ -1924,6 +1969,7 @@ version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e381186490a3e2017a506d62b759ea8eaf4be14666b13ed53973e8ae193451b1"
|
checksum = "e381186490a3e2017a506d62b759ea8eaf4be14666b13ed53973e8ae193451b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"image",
|
||||||
"kurbo",
|
"kurbo",
|
||||||
"unic-bidi",
|
"unic-bidi",
|
||||||
]
|
]
|
||||||
|
@ -2022,6 +2068,18 @@ version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.17.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
|
@ -10,7 +10,7 @@ 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", "serde"] }
|
druid = { git = "https://github.com/linebender/druid.git", features = ["im", "serde", "image", "png", "jpeg", "bmp", "webp"] }
|
||||||
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 = "*" }
|
||||||
|
|
|
@ -4,9 +4,9 @@ use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
use color_eyre::{Help, Result};
|
use color_eyre::{Help, Report, Result};
|
||||||
use druid::im::Vector;
|
use druid::im::Vector;
|
||||||
use druid::FileInfo;
|
use druid::{FileInfo, ImageBuf};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
use tokio::fs::{self, DirEntry};
|
use tokio::fs::{self, DirEntry};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
@ -95,6 +95,36 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
|
|
||||||
tracing::trace!(?files);
|
tracing::trace!(?files);
|
||||||
|
|
||||||
|
let image = if let Some(path) = &mod_cfg.image {
|
||||||
|
let name = names
|
||||||
|
.iter()
|
||||||
|
.find(|name| name.ends_with(&path.display().to_string()))
|
||||||
|
.ok_or_else(|| eyre::eyre!("archive does not contain configured image file"))?;
|
||||||
|
|
||||||
|
let mut f = archive
|
||||||
|
.by_name(name)
|
||||||
|
.wrap_err("Failed to read image file from archive")?;
|
||||||
|
let mut buf = Vec::with_capacity(f.size() as usize);
|
||||||
|
f.read_to_end(&mut buf)
|
||||||
|
.wrap_err("Failed to read file index from archive")?;
|
||||||
|
|
||||||
|
// Druid somehow doesn't return an error compatible with eyre, here.
|
||||||
|
// So we have to wrap through `Display` manually.
|
||||||
|
let img = match ImageBuf::from_data(&buf) {
|
||||||
|
Ok(img) => img,
|
||||||
|
Err(err) => {
|
||||||
|
let err = Report::msg(err.to_string()).wrap_err("Invalid image data");
|
||||||
|
return Err(err).with_suggestion(|| {
|
||||||
|
"Supported formats are: PNG, JPEG, Bitmap and WebP".to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(img)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mod_dir = state.mod_dir;
|
let mod_dir = state.mod_dir;
|
||||||
|
|
||||||
tracing::trace!("Creating mods directory {}", mod_dir.display());
|
tracing::trace!("Creating mods directory {}", mod_dir.display());
|
||||||
|
@ -111,7 +141,7 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(mod_cfg, packages);
|
let info = ModInfo::new(mod_cfg, packages, image);
|
||||||
|
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
@ -161,11 +191,38 @@ async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
|
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
|
||||||
|
|
||||||
|
let image = if let Some(path) = &cfg.image {
|
||||||
|
let path = entry.path().join(path);
|
||||||
|
if let Ok(data) = fs::read(&path).await {
|
||||||
|
// Druid somehow doesn't return an error compatible with eyre, here.
|
||||||
|
// So we have to wrap through `Display` manually.
|
||||||
|
let img = match ImageBuf::from_data(&data) {
|
||||||
|
Ok(img) => img,
|
||||||
|
Err(err) => {
|
||||||
|
let err = Report::msg(err.to_string());
|
||||||
|
return Err(err)
|
||||||
|
.wrap_err_with(|| {
|
||||||
|
format!("Failed to import image file '{}'", path.display())
|
||||||
|
})
|
||||||
|
.with_suggestion(|| {
|
||||||
|
"Supported formats are: PNG, JPEG, Bitmap and WebP".to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(img)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let packages = files
|
let packages = files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(cfg, packages);
|
let info = ModInfo::new(cfg, packages, image);
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use druid::{
|
use druid::{
|
||||||
im::{HashMap, Vector},
|
im::{HashMap, Vector},
|
||||||
Data, Lens, WindowHandle, WindowId,
|
Data, ImageBuf, Lens, WindowHandle, WindowId,
|
||||||
};
|
};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ impl From<dtmt_shared::ModDependency> for ModDependency {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Data, Debug, Lens, PartialEq)]
|
#[derive(Clone, Data, Debug, Lens)]
|
||||||
pub(crate) struct ModInfo {
|
pub(crate) struct ModInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -77,6 +77,7 @@ pub(crate) struct ModInfo {
|
||||||
pub description: Option<Arc<String>>,
|
pub description: Option<Arc<String>>,
|
||||||
pub categories: Vector<String>,
|
pub categories: Vector<String>,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
pub image: Option<ImageBuf>,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
#[lens(ignore)]
|
#[lens(ignore)]
|
||||||
|
@ -89,7 +90,11 @@ pub(crate) struct ModInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
pub fn new(cfg: ModConfig, packages: Vector<Arc<PackageInfo>>) -> Self {
|
pub fn new(
|
||||||
|
cfg: ModConfig,
|
||||||
|
packages: Vector<Arc<PackageInfo>>,
|
||||||
|
image: Option<ImageBuf>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: cfg.id,
|
id: cfg.id,
|
||||||
name: cfg.name,
|
name: cfg.name,
|
||||||
|
@ -99,6 +104,7 @@ impl ModInfo {
|
||||||
version: cfg.version,
|
version: cfg.version,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
packages,
|
packages,
|
||||||
|
image,
|
||||||
categories: cfg.categories.into_iter().collect(),
|
categories: cfg.categories.into_iter().collect(),
|
||||||
resources: ModResourceInfo {
|
resources: ModResourceInfo {
|
||||||
init: cfg.resources.init,
|
init: cfg.resources.init,
|
||||||
|
|
|
@ -259,6 +259,11 @@ impl AppDelegate<State> for Delegate {
|
||||||
Handled::Yes
|
Handled::Yes
|
||||||
}
|
}
|
||||||
cmd if cmd.is(ACTION_FINISH_SAVE_SETTINGS) => {
|
cmd if cmd.is(ACTION_FINISH_SAVE_SETTINGS) => {
|
||||||
|
tracing::trace!(
|
||||||
|
in_progress = state.is_save_in_progress,
|
||||||
|
next_pending = state.is_next_save_pending,
|
||||||
|
"Finished saving settings",
|
||||||
|
);
|
||||||
state.is_save_in_progress = false;
|
state.is_save_in_progress = false;
|
||||||
|
|
||||||
if state.is_next_save_pending {
|
if state.is_next_save_pending {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use druid::widget::{Button, Controller, Scroll};
|
use druid::widget::{Button, Controller, Image, Scroll};
|
||||||
use druid::{Data, Env, Event, EventCtx, Rect, UpdateCtx, Widget};
|
use druid::{
|
||||||
|
Data, Env, Event, EventCtx, ImageBuf, LifeCycle, LifeCycleCtx, Rect, UpdateCtx, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::state::{State, ACTION_SET_DIRTY, ACTION_START_SAVE_SETTINGS};
|
use crate::state::{State, ACTION_SET_DIRTY, ACTION_START_SAVE_SETTINGS};
|
||||||
|
|
||||||
|
@ -48,18 +50,19 @@ impl<T: Data, W: Widget<T>> Controller<T, Scroll<T, W>> for AutoScrollController
|
||||||
data: &T,
|
data: &T,
|
||||||
env: &Env,
|
env: &Env,
|
||||||
) {
|
) {
|
||||||
|
child.update(ctx, old_data, data, env);
|
||||||
|
|
||||||
if !ctx.is_disabled() {
|
if !ctx.is_disabled() {
|
||||||
let size = child.child_size();
|
let size = child.child_size();
|
||||||
let end_region = Rect::new(size.width - 1., size.height - 1., size.width, size.height);
|
let end_region = Rect::new(size.width - 1., size.height - 1., size.width, size.height);
|
||||||
child.scroll_to(ctx, end_region);
|
child.scroll_to(ctx, end_region);
|
||||||
}
|
}
|
||||||
child.update(ctx, old_data, data, env)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! compare_state_fields {
|
macro_rules! compare_state_fields {
|
||||||
($old:ident, $new:ident, $($field:ident),+) => {
|
($old:ident, $new:ident, $($field:ident),+) => {
|
||||||
$($old.$field != $new.$field) || +
|
$(!Data::same(&$old.$field, &$new.$field)) || +
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,3 +89,37 @@ impl<W: Widget<State>> Controller<State, W> for DirtyStateController {
|
||||||
child.update(ctx, old_data, data, env)
|
child.update(ctx, old_data, data, env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ImageLensController;
|
||||||
|
|
||||||
|
impl Controller<ImageBuf, Image> for ImageLensController {
|
||||||
|
fn lifecycle(
|
||||||
|
&mut self,
|
||||||
|
widget: &mut Image,
|
||||||
|
ctx: &mut LifeCycleCtx,
|
||||||
|
event: &LifeCycle,
|
||||||
|
data: &ImageBuf,
|
||||||
|
env: &Env,
|
||||||
|
) {
|
||||||
|
if let LifeCycle::WidgetAdded = event {
|
||||||
|
widget.set_image_data(data.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.lifecycle(ctx, event, data, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
widget: &mut Image,
|
||||||
|
ctx: &mut UpdateCtx,
|
||||||
|
old_data: &ImageBuf,
|
||||||
|
data: &ImageBuf,
|
||||||
|
env: &Env,
|
||||||
|
) {
|
||||||
|
if !Data::same(old_data, data) {
|
||||||
|
widget.set_image_data(data.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.update(ctx, old_data, data, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use druid::im::Vector;
|
use druid::im::Vector;
|
||||||
use druid::widget::{
|
use druid::widget::{
|
||||||
Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment,
|
Button, Checkbox, CrossAxisAlignment, Flex, Image, Label, LineBreaking, List,
|
||||||
Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher,
|
MainAxisAlignment, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher,
|
||||||
};
|
};
|
||||||
use druid::{lens, LifeCycleCtx};
|
use druid::{lens, Data, ImageBuf, LifeCycleCtx};
|
||||||
use druid::{
|
use druid::{
|
||||||
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse,
|
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse,
|
||||||
TextAlignment, Widget, WidgetExt, WindowDesc, WindowId,
|
TextAlignment, Widget, WidgetExt, WindowDesc, WindowId,
|
||||||
|
@ -18,7 +18,9 @@ use crate::state::{
|
||||||
ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
|
ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
|
||||||
};
|
};
|
||||||
use crate::ui::theme;
|
use crate::ui::theme;
|
||||||
use crate::ui::widget::controller::{AutoScrollController, DirtyStateController};
|
use crate::ui::widget::controller::{
|
||||||
|
AutoScrollController, DirtyStateController, ImageLensController,
|
||||||
|
};
|
||||||
use crate::ui::widget::PathBufFormatter;
|
use crate::ui::widget::PathBufFormatter;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -124,7 +126,7 @@ fn build_mod_list() -> impl Widget<State> {
|
||||||
},
|
},
|
||||||
|state, infos| {
|
|state, infos| {
|
||||||
infos.into_iter().for_each(|(i, new, _)| {
|
infos.into_iter().for_each(|(i, new, _)| {
|
||||||
if state.mods.get(i).cloned() != Some(new.clone()) {
|
if Data::same(&state.mods.get(i).cloned(), &Some(new.clone())) {
|
||||||
state.mods.set(i, new);
|
state.mods.set(i, new);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -251,7 +253,7 @@ fn build_mod_details_info() -> impl Widget<State> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Flex::column()
|
let details = Flex::column()
|
||||||
.cross_axis_alignment(CrossAxisAlignment::Start)
|
.cross_axis_alignment(CrossAxisAlignment::Start)
|
||||||
.main_axis_alignment(MainAxisAlignment::Start)
|
.main_axis_alignment(MainAxisAlignment::Start)
|
||||||
.with_child(name)
|
.with_child(name)
|
||||||
|
@ -261,10 +263,23 @@ fn build_mod_details_info() -> impl Widget<State> {
|
||||||
.with_child(version_line)
|
.with_child(version_line)
|
||||||
.with_spacer(4.)
|
.with_spacer(4.)
|
||||||
.with_child(categories)
|
.with_child(categories)
|
||||||
|
.padding((4., 4.));
|
||||||
|
|
||||||
|
let image =
|
||||||
|
Maybe::or_empty(|| Image::new(ImageBuf::empty()).controller(ImageLensController))
|
||||||
|
.lens(ModInfo::image.in_arc());
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.main_axis_alignment(MainAxisAlignment::Start)
|
||||||
|
.must_fill_main_axis(true)
|
||||||
|
.cross_axis_alignment(CrossAxisAlignment::Start)
|
||||||
|
.with_child(image)
|
||||||
|
// .with_spacer(4.)
|
||||||
|
// .with_flex_child(details, 1.)
|
||||||
|
.with_child(details)
|
||||||
},
|
},
|
||||||
Flex::column,
|
Flex::column,
|
||||||
)
|
)
|
||||||
.padding((4., 4.))
|
|
||||||
.lens(State::selected_mod)
|
.lens(State::selected_mod)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ tracing = { version = "0.1.37", features = ["async-await"] }
|
||||||
zip = "0.6.3"
|
zip = "0.6.3"
|
||||||
path-clean = "1.0.1"
|
path-clean = "1.0.1"
|
||||||
path-slash = "0.2.1"
|
path-slash = "0.2.1"
|
||||||
|
async-recursion = "1.0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
|
|
|
@ -201,6 +201,17 @@ fn normalize_file_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
||||||
pub(crate) async fn read_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
|
pub(crate) async fn read_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
|
||||||
let mut cfg = find_project_config(dir).await?;
|
let mut cfg = find_project_config(dir).await?;
|
||||||
|
|
||||||
|
if let Some(path) = cfg.image {
|
||||||
|
let path = normalize_file_path(path)
|
||||||
|
.wrap_err("Invalid config field 'image'")
|
||||||
|
.with_suggestion(|| {
|
||||||
|
"Specify a file path relative to and child path of the \
|
||||||
|
directory where 'dtmt.cfg' is."
|
||||||
|
.to_string()
|
||||||
|
})?;
|
||||||
|
cfg.image = Some(path);
|
||||||
|
}
|
||||||
|
|
||||||
cfg.resources.init = normalize_file_path(cfg.resources.init)
|
cfg.resources.init = normalize_file_path(cfg.resources.init)
|
||||||
.wrap_err("Invalid config field 'resources.init'")
|
.wrap_err("Invalid config field 'resources.init'")
|
||||||
.with_suggestion(|| {
|
.with_suggestion(|| {
|
||||||
|
@ -349,14 +360,36 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
|
||||||
.wrap_err("Failed to build mod bundles")?;
|
.wrap_err("Failed to build mod bundles")?;
|
||||||
|
|
||||||
{
|
{
|
||||||
|
let path = out_path.join("files.sjson");
|
||||||
|
tracing::trace!(path = %path.display(), "Writing file index");
|
||||||
let file_map = file_map.lock().await;
|
let file_map = file_map.lock().await;
|
||||||
let data = serde_sjson::to_string(file_map.deref())?;
|
let data = serde_sjson::to_string(file_map.deref())?;
|
||||||
let path = out_path.join("files.sjson");
|
|
||||||
fs::write(&path, data)
|
fs::write(&path, data)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to write file index to '{}'", path.display()))?;
|
.wrap_err_with(|| format!("Failed to write file index to '{}'", path.display()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(img_path) = &cfg.image {
|
||||||
|
let path = cfg.dir.join(img_path);
|
||||||
|
let dest = out_path.join(img_path);
|
||||||
|
|
||||||
|
tracing::trace!(src = %path.display(), dest = %dest.display(), "Copying image file");
|
||||||
|
|
||||||
|
if let Some(parent) = dest.parent() {
|
||||||
|
fs::create_dir_all(&parent)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("Failed to create directory '{}'", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::copy(&path, &dest).await.wrap_err_with(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to copy image from '{}' to '{}'",
|
||||||
|
path.display(),
|
||||||
|
dest.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
tracing::info!("Compiled bundles written to '{}'", out_path.display());
|
tracing::info!("Compiled bundles written to '{}'", out_path.display());
|
||||||
|
|
||||||
if let Some(game_dir) = game_dir.as_ref() {
|
if let Some(game_dir) = game_dir.as_ref() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ version = "0.1.0"
|
||||||
// A one- or two-line short description.
|
// A one- or two-line short description.
|
||||||
summary = "This is my new mod '{{name}}'!"
|
summary = "This is my new mod '{{name}}'!"
|
||||||
// description = ""
|
// description = ""
|
||||||
|
// image = "assets/logo.png"
|
||||||
|
|
||||||
// Can contain arbitrary strings. But to keep things consistent and useful,
|
// Can contain arbitrary strings. But to keep things consistent and useful,
|
||||||
// capitalize names and check existing mods for matching categories.
|
// capitalize names and check existing mods for matching categories.
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::io::{Cursor, Write};
|
use std::io::{Cursor, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{value_parser, Arg, ArgMatches, Command};
|
use clap::{value_parser, Arg, ArgMatches, Command};
|
||||||
use color_eyre::eyre::{Context, Result};
|
use color_eyre::eyre::{Context, Result};
|
||||||
use color_eyre::Help;
|
use color_eyre::Help;
|
||||||
use path_slash::PathBufExt;
|
use path_slash::PathBufExt;
|
||||||
use tokio::fs::{self, DirEntry};
|
use tokio::fs;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use zip::ZipWriter;
|
use zip::ZipWriter;
|
||||||
|
@ -46,15 +47,45 @@ pub(crate) fn command_definition() -> Command {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_dir_entry(res: Result<DirEntry>) -> Result<(OsString, Vec<u8>)> {
|
#[async_recursion::async_recursion]
|
||||||
let entry = res?;
|
async fn process_directory<W: std::io::Write + std::io::Seek + std::marker::Send>(
|
||||||
let path = entry.path();
|
zip: Arc<Mutex<ZipWriter<W>>>,
|
||||||
|
path: PathBuf,
|
||||||
let data = fs::read(&path)
|
prefix: PathBuf,
|
||||||
|
) -> Result<()> {
|
||||||
|
zip.lock()
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to read '{}'", path.display()))?;
|
.add_directory(prefix.to_slash_lossy(), Default::default())?;
|
||||||
|
|
||||||
Ok((entry.file_name(), data))
|
let read_dir = fs::read_dir(&path)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("Failed to read directory '{}'", path.display()))?;
|
||||||
|
|
||||||
|
let stream = ReadDirStream::new(read_dir).map(|res| res.wrap_err("Failed to read dir entry"));
|
||||||
|
tokio::pin!(stream);
|
||||||
|
|
||||||
|
while let Some(res) = stream.next().await {
|
||||||
|
let entry = res?;
|
||||||
|
let in_path = entry.path();
|
||||||
|
let out_path = prefix.join(entry.file_name());
|
||||||
|
|
||||||
|
let t = entry.file_type().await?;
|
||||||
|
|
||||||
|
if t.is_file() || t.is_symlink() {
|
||||||
|
let data = fs::read(&in_path)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("Failed to read '{}'", in_path.display()))?;
|
||||||
|
{
|
||||||
|
let mut zip = zip.lock().await;
|
||||||
|
zip.start_file(out_path.to_slash_lossy(), Default::default())?;
|
||||||
|
zip.write_all(&data)?;
|
||||||
|
}
|
||||||
|
} else if t.is_dir() {
|
||||||
|
process_directory(zip.clone(), in_path, out_path).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -75,14 +106,23 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
|
||||||
};
|
};
|
||||||
|
|
||||||
let data = Cursor::new(Vec::new());
|
let data = Cursor::new(Vec::new());
|
||||||
let mut zip = ZipWriter::new(data);
|
let zip = ZipWriter::new(data);
|
||||||
|
let zip = Arc::new(Mutex::new(zip));
|
||||||
|
|
||||||
zip.add_directory(&cfg.id, Default::default())?;
|
let path = cfg.dir.join(
|
||||||
|
matches
|
||||||
|
.get_one::<PathBuf>("directory")
|
||||||
|
.expect("parameter has default value"),
|
||||||
|
);
|
||||||
|
|
||||||
let base_path = PathBuf::from(cfg.id);
|
process_directory(zip.clone(), path, PathBuf::from(&cfg.id))
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to add directory to archive")?;
|
||||||
|
|
||||||
|
let mut zip = zip.lock().await;
|
||||||
|
|
||||||
{
|
{
|
||||||
let name = base_path.join("dtmt.cfg");
|
let name = PathBuf::from(&cfg.id).join("dtmt.cfg");
|
||||||
let path = cfg.dir.join("dtmt.cfg");
|
let path = cfg.dir.join("dtmt.cfg");
|
||||||
|
|
||||||
let data = fs::read(&path)
|
let data = fs::read(&path)
|
||||||
|
@ -93,30 +133,6 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
|
||||||
zip.write_all(&data)?;
|
zip.write_all(&data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let path = cfg.dir.join(
|
|
||||||
matches
|
|
||||||
.get_one::<PathBuf>("directory")
|
|
||||||
.expect("parameter has default value"),
|
|
||||||
);
|
|
||||||
let read_dir = fs::read_dir(&path)
|
|
||||||
.await
|
|
||||||
.wrap_err_with(|| format!("Failed to read directory '{}'", path.display()))?;
|
|
||||||
|
|
||||||
let stream = ReadDirStream::new(read_dir)
|
|
||||||
.map(|res| res.wrap_err("Failed to read dir entry"))
|
|
||||||
.then(process_dir_entry);
|
|
||||||
tokio::pin!(stream);
|
|
||||||
|
|
||||||
while let Some(res) = stream.next().await {
|
|
||||||
let (name, data) = res?;
|
|
||||||
|
|
||||||
let name = base_path.join(name);
|
|
||||||
zip.start_file(name.to_slash_lossy(), Default::default())?;
|
|
||||||
zip.write_all(&data)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = zip.finish()?;
|
let data = zip.finish()?;
|
||||||
|
|
||||||
fs::write(&dest, data.into_inner())
|
fs::write(&dest, data.into_inner())
|
||||||
|
|
|
@ -33,13 +33,14 @@ pub enum ModDependency {
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct ModConfig {
|
pub struct ModConfig {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub dir: std::path::PathBuf,
|
pub dir: PathBuf,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
pub image: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
pub packages: Vec<PathBuf>,
|
pub packages: Vec<PathBuf>,
|
||||||
|
|
Loading…
Add table
Reference in a new issue