Merge pull request 'Improve mod details' (#64) from feat/mod-details into master

Reviewed-on: #64
This commit is contained in:
Lucas Schwiderski 2023-03-09 20:13:07 +01:00
commit 0c63a8b046
12 changed files with 361 additions and 73 deletions

60
Cargo.lock generated
View file

@ -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"

View file

@ -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 = "*" }

View file

@ -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)
} }

View file

@ -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,11 +69,16 @@ 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,
pub description: Arc<String>, pub summary: Arc<String>,
pub description: Option<Arc<String>>,
pub categories: Vector<String>,
pub author: Option<String>,
pub image: Option<ImageBuf>,
pub version: String,
pub enabled: bool, pub enabled: bool,
#[lens(ignore)] #[lens(ignore)]
#[data(ignore)] #[data(ignore)]
@ -85,13 +90,22 @@ 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,
description: Arc::new(cfg.description), summary: Arc::new(cfg.summary),
description: cfg.description.map(Arc::new),
author: cfg.author,
version: cfg.version,
enabled: false, enabled: false,
packages, packages,
image,
categories: cfg.categories.into_iter().collect(),
resources: ModResourceInfo { resources: ModResourceInfo {
init: cfg.resources.init, init: cfg.resources.init,
data: cfg.resources.data, data: cfg.resources.data,

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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);
} }
}); });
@ -220,20 +222,64 @@ fn build_mod_details_info() -> impl Widget<State> {
// so that we can center-align it. // so that we can center-align it.
.expand_width() .expand_width()
.lens(ModInfo::name.in_arc()); .lens(ModInfo::name.in_arc());
let description = Label::raw() let summary = Label::raw()
.with_line_break_mode(LineBreaking::WordWrap) .with_line_break_mode(LineBreaking::WordWrap)
.lens(ModInfo::description.in_arc()); .lens(ModInfo::summary.in_arc());
Flex::column() // TODO: Image/icon?
let version_line = Label::dynamic(|info: &Arc<ModInfo>, _| {
if let Some(author) = &info.author {
format!("Version: {}, by {author}", info.version)
} else {
format!("Version: {}", info.version)
}
});
let categories = Label::dynamic(|info: &Arc<ModInfo>, _| {
if info.categories.is_empty() {
String::from("Uncategorized")
} else {
info.categories.iter().enumerate().fold(
String::from("Category: "),
|mut s, (i, category)| {
if i > 0 {
s.push_str(", ");
}
s.push_str(category);
s
},
)
}
});
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)
.with_spacer(4.) .with_spacer(4.)
.with_child(description) .with_child(summary)
.with_spacer(4.)
.with_child(version_line)
.with_spacer(4.)
.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)
} }

View file

@ -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"

View file

@ -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() {

View file

@ -13,8 +13,28 @@ const TEMPLATES: [(&str, &str); 5] = [
"dtmt.cfg", "dtmt.cfg",
r#"id = "{{id}}" r#"id = "{{id}}"
name = "{{name}}" name = "{{name}}"
description = "This is my new mod '{{name}}'!"
version = "0.1.0" version = "0.1.0"
// author = ""
// A one- or two-line short description.
summary = "This is my new mod '{{name}}'!"
// description = ""
// image = "assets/logo.png"
// Can contain arbitrary strings. But to keep things consistent and useful,
// capitalize names and check existing mods for matching categories.
categories = [
Misc
// UI
// QoL
// Tools
]
// A list of mod IDs that this mod depends on. You can find
// those IDs by downloading the mod and extracting their `dtmt.cfg`.
depends = [
DMF
]
resources = { resources = {
init = "scripts/mods/{{id}}/init" init = "scripts/mods/{{id}}/init"
@ -23,16 +43,12 @@ resources = {
} }
packages = [ packages = [
"packages/{{id}}" "packages/mods/{{id}}"
]
depends = [
"dmf"
] ]
"#, "#,
), ),
( (
"packages/{{id}}.package", "packages/mods/{{id}}.package",
r#"lua = [ r#"lua = [
"scripts/mods/{{id}}/*" "scripts/mods/{{id}}/*"
] ]

View file

@ -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())

View file

@ -33,12 +33,17 @@ 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 description: String, pub summary: String,
pub description: Option<String>,
pub author: Option<String>,
pub version: String, pub version: String,
pub packages: Vec<std::path::PathBuf>, pub image: Option<PathBuf>,
#[serde(default)]
pub categories: Vec<String>,
pub packages: Vec<PathBuf>,
pub resources: ModConfigResources, pub resources: ModConfigResources,
#[serde(default)] #[serde(default)]
pub depends: Vec<ModDependency>, pub depends: Vec<ModDependency>,