initial commit

This commit is contained in:
vandechat96
2023-07-03 19:26:17 +02:00
commit 72e2e9f182
20 changed files with 6180 additions and 0 deletions

2
.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
/target
.idea

4006
Cargo.lock generated Executable file

File diff suppressed because it is too large Load Diff

34
Cargo.toml Executable file
View File

@@ -0,0 +1,34 @@
[package]
name = "aw-mod-installer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lazy_static = "1.4"
anyhow = "1.0"
thiserror = "1.0"
log = "0.4"
env_logger = "0.9"
tokio = { version = "1", features = ["fs","macros"] }
# utils
regex = "1"
dotenv = "0.15"
dirs-next = "2"
strum = { version = "0.24", features = ["derive"] }
# file and net
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
scraper = "0.13"
zip = "0.6"
sevenz-rust = "0.1"
# ui
iced = {git ="https://github.com/iced-rs/iced", features =["tokio"]}
native-dialog = "0.6"
open = "3"

132
src/api.rs Executable file
View File

@@ -0,0 +1,132 @@
pub mod forum {
// this will be ugly until access to forum API
use crate::util::{read_cache_file, write_to_cache_json};
use anyhow::Result;
use log::info;
use scraper::Node::Text;
use scraper::{Html, Selector};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Post {
pub url: String,
pub author: String,
pub title: String,
pub content: String,
}
pub async fn get_post_info(url: &str) -> Result<Post> {
let response = reqwest::get(url).await?;
let txt = response.text().await?;
let doc = Html::parse_document(&txt);
let info_selector = Selector::parse(
r#"div[class="ipsPhotoPanel ipsPhotoPanel_small ipsPhotoPanel_notPhone ipsClearfix"]"#,
)
.unwrap();
let info = doc.select(&info_selector).next().unwrap();
let author_selector = Selector::parse(r#"a[class="ipsType_break"]"#).unwrap();
let author_elem = info.select(&author_selector).next().unwrap();
let selector = Selector::parse("h1").unwrap();
let h1 = info.select(&selector).next().unwrap();
let title_selector =
Selector::parse(r#"span[class="ipsType_break ipsContained"]"#).unwrap();
let title_elem = h1.select(&title_selector).next().unwrap();
let selector = Selector::parse("span").unwrap();
let span = title_elem.select(&selector).next().unwrap();
let title = span.inner_html();
let mut author = author_elem.inner_html();
let parsed = Html::parse_fragment(&author);
let selector = Selector::parse("span").unwrap();
if let Some(span) = parsed.select(&selector).next() {
let mut children = span.children();
while let Some(child) = children.next() {
if let Text(name) = child.value() {
author = name.to_string();
break;
}
children = child.children();
}
}
let article_selector = Selector::parse("article").unwrap();
let article = doc.select(&article_selector).next().unwrap();
let selector = Selector::parse(r#"div[class="cPost_contentWrap ipsPad"]"#).unwrap();
let div = article.select(&selector).next().unwrap();
let p_selector = Selector::parse("p").unwrap();
let mut content = String::new();
for p in div.select(&p_selector) {
content.push_str(&p.inner_html());
content.push('\n');
}
Ok(Post {
url: url.to_string(),
title,
author,
content,
})
}
pub async fn get_posts() -> Result<Vec<Post>> {
let url = "https://armoredlabs.net/index.php?/forum/45-game-mods-add-ons";
let mut posts_url = Vec::new();
for i in 1..=5 {
info!("Parsing page {}", i);
let url = format!("{}/page/{}", url, i);
info!("{}", url);
if let Ok(response) = reqwest::get(url).await {
let txt = response.text().await?;
let doc = Html::parse_document(&txt);
let main_selector = Selector::parse(r#"div[id="ipsLayout_mainArea"]"#).unwrap();
let main = doc.select(&main_selector).next().unwrap();
let post_selector =
Selector::parse(r#"span[class="ipsType_break ipsContained"]"#).unwrap();
for element in main.select(&post_selector) {
let selector = Selector::parse("a").unwrap();
let a = element.select(&selector).next().unwrap();
let post_url = a.value().attr("href").unwrap();
posts_url.push(post_url.to_owned());
}
} else {
info!("Error while fetching page : {}", i);
}
}
let mut posts = Vec::new();
for url in posts_url {
for i in 1..=4 {
if let Ok(post) = get_post_info(&url).await {
info!("Found post : {} - {}", post.title, post.author);
posts.push(post);
break;
} else {
info!("Error while fetching mod : {}", url);
info!("Retrying {} of 4...", i);
}
}
}
Ok(posts)
}
pub async fn write_posts_to_disk() -> Result<()> {
let posts = get_posts().await?;
write_to_cache_json(&posts, "posts").await
}
pub async fn fetch_posts_from_disk() -> Result<Vec<Post>> {
let content = read_cache_file("posts.json").await?;
let posts = serde_json::from_slice(&content)?;
Ok(posts)
}
}

55
src/config.rs Normal file
View File

@@ -0,0 +1,55 @@
use crate::util::{find_aw_path, get_app_config_path, read_file, write_to_json, LoadError};
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
pub aw_path: PathBuf,
pub installed_mod: Vec<InstalledMod>,
}
impl Default for Config {
fn default() -> Self {
Self {
aw_path: find_aw_path(),
installed_mod: vec![],
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct InstalledMod {
pub download: String,
pub title: String,
pub path: PathBuf,
}
impl InstalledMod {
pub fn new(download: String, title: String, path: PathBuf) -> Self {
Self { download, title, path }
}
}
pub async fn get_config() -> Result<Config, LoadError> {
let path = get_app_config_path().map_err(|_| LoadError::File)?;
if !path.exists() {
File::create(&path).map_err(|_| LoadError::File)?;
write_to_json(&Config::default(), &path)
.await
.map_err(|_| LoadError::File)?;
}
let content = read_file(path).await.map_err(|_| LoadError::File)?;
serde_json::from_slice(&content).map_err(|_| LoadError::Format)
}
pub async fn update_config(cfg: Config) -> Result<(), LoadError> {
let path = get_app_config_path().map_err(|_| LoadError::File)?;
write_to_json(&cfg, &path)
.await
.map_err(|_| LoadError::File)
}

84
src/downloader_api.rs Executable file
View File

@@ -0,0 +1,84 @@
use crate::util::get_app_download_path;
use anyhow::{Context, Result};
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
lazy_static! {
static ref DRIVE_API_KEY: Result<String, InfoError> =
std::env::var("DRIVE_API_KEY").map_err(|_| InfoError::APIKey("Drive".to_string()));
static ref DRIVE_RE: Regex =
Regex::new(r#"https://drive.google.com/file/d/([-_\w]+)/"#).unwrap();
static ref FORUM_RE: Regex = Regex::new(
r#"https://armoredlabs.net/applications/core/interface/file/attachment.php?id=(\d+)"#
)
.unwrap();
}
#[derive(Debug, Clone)]
pub enum InfoError {
File,
Connection,
FormatUrl(String),
FormatResponse,
APIKey(String),
}
pub type ResponseInfo = Result<(String, PathBuf), InfoError>;
#[derive(Debug, Clone)]
pub struct DownloadInfo {
pub(crate) title: String,
pub(crate) info: ResponseInfo,
}
pub async fn get_download_info(url: String) -> ResponseInfo {
if let Some(capt) = DRIVE_RE.captures(&url) {
let id = capt
.get(1)
.context("Id not found in url")
.map_err(|_| InfoError::FormatUrl(url.clone()))?
.as_str();
get_drive_download_info(id).await
} else if let Some(capt) = DRIVE_RE.captures(&url) {
let id = capt
.get(1)
.context("Id not found in url")
.map_err(|_| InfoError::FormatUrl(url.clone()))?
.as_str();
let mut path = get_app_download_path().map_err(|_| InfoError::File)?;
path.push(id);
Ok((url, path))
} else {
Err(InfoError::FormatUrl(url))
}
}
#[derive(Deserialize)]
pub struct DriveResponse {
name: String,
}
async fn get_drive_download_info(id: &str) -> ResponseInfo {
let key = &*DRIVE_API_KEY.clone()?;
let base_url = "https://www.googleapis.com/drive/v3/files";
let mut file_url = format!("{base_url}/{id}?key={key}");
let response = reqwest::get(&file_url)
.await
.map_err(|_| InfoError::Connection)?;
match response.error_for_status() {
Ok(res) => {
let info: DriveResponse = res.json().await.map_err(|_| InfoError::FormatResponse)?;
let file_name = info.name;
let mut path = get_app_download_path().map_err(|_| InfoError::File)?;
path.push(file_name);
file_url.push_str("&alt=media");
Ok((file_url, path))
}
Err(_) => Err(InfoError::Connection),
}
}

65
src/explore_fs.rs Executable file
View File

@@ -0,0 +1,65 @@
use std::path::{Path, PathBuf};
pub async fn _explore() {
/*
let gs_path = PathBuf::from("/run/media/jika/Windows/MyGames/Armored Warfare MyCom/gamesdk/");
let el_path = PathBuf::from(
"/run/media/jika/Windows/MyGames/Armored Warfare MyCom/localization/english/",
);
let mut list = list_files(&gs_path);
list.extend(list_files(&el_path));
list = list
.into_iter()
.map(|x| {
x.strip_prefix("/run/media/jika/Windows/MyGames/Armored Warfare MyCom/")
.unwrap()
.to_path_buf()
})
.collect();
write_to_cache_json(&list, "files").await.unwrap();
let mut file = File::create("./list.txt").unwrap();
file.write_all(format!("{:#?}", list).as_bytes()).unwrap();
println!("a");
let mut unique_name = vec![];
for file in &list {
if let Some(name) = file.file_name() {
let name = name.to_os_string();
if !unique_name.contains(&name) {
unique_name.push(name);
}
}
}
let mut file = File::create("./unique_name.txt").unwrap();
file.write_all(format!("{:#?}", unique_name).as_bytes())
.unwrap();
println!("b");
let mut file = File::create("./unique_list.txt").unwrap();
for name in unique_name {
let list: Vec<&PathBuf> = list
.iter()
.filter(|path| path.file_name().unwrap() == name)
.collect();
file.write_all(format!("=== {} ==={}", name.to_str().unwrap(), list.len()).as_bytes())
.unwrap();
file.write_all(format!("{:#?}", list).as_bytes()).unwrap();
}
*/
}
fn _list_files(path: &Path) -> Vec<PathBuf> {
if let Ok(read_dir) = path.read_dir() {
let mut list = vec![];
for entry in read_dir.flatten() {
list.extend(_list_files(&entry.path()));
}
list
} else {
vec![path.to_path_buf()]
}
}

110
src/extractor.rs Executable file
View File

@@ -0,0 +1,110 @@
use anyhow::{bail, Result};
use log::debug;
use std::fs::File;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fs, io};
#[derive(Debug, Clone)]
pub struct ModArchive {
pub title: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub enum ExtractError {
File,
}
pub async fn extract_all(
files: Vec<ModArchive>,
) -> std::result::Result<Vec<ModArchive>, ExtractError> {
ExtractAll { files, current: 0 }.await
}
#[derive(Debug)]
pub struct ExtractAll {
pub files: Vec<ModArchive>,
current: usize,
}
impl Future for ExtractAll {
type Output = std::result::Result<Vec<ModArchive>, ExtractError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.current >= self.files.len() {
Poll::Ready(Ok(self.files.to_owned()))
} else {
self.current += 1;
let file = &self.files[self.current - 1];
match extract_archive(&file.path) {
Ok(_) => {}
Err(_) => {
return Poll::Ready(Err(ExtractError::File));
}
};
// Schedule future for another poll()
cx.waker().clone().wake();
Poll::Pending
}
}
}
pub fn extract_archive(path: &PathBuf) -> Result<()> {
if let Some(ext) = path.extension() {
match ext.to_str().unwrap() {
"7z" => extract_7zip(path.to_owned())?,
"zip" => extract_zip(path.to_owned())?,
_ => {}
}
} else {
bail!("not file")
}
Ok(())
}
pub fn extract_zip(path: PathBuf) -> Result<()> {
let dest_path = path.with_extension("d");
fs::create_dir_all(&dest_path)?;
let file = File::open(&path)?;
let mut zip = zip::ZipArchive::new(file)?;
for i in 0..zip.len() {
let mut file = zip.by_index(i)?;
debug!("Filename: {}", file.name());
let mut outpath = dest_path.clone();
match file.enclosed_name() {
Some(path) => outpath.push(path),
None => continue,
};
if (*file.name()).ends_with('/') {
debug!("File {} extracted to \"{}\"", i, outpath.display());
fs::create_dir_all(&outpath).unwrap();
} else {
debug!(
"File {} extracted to \"{}\" ({} bytes)",
i,
outpath.display(),
file.size()
);
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(&p).unwrap();
}
}
let mut outfile = File::create(&outpath).unwrap();
io::copy(&mut file, &mut outfile).unwrap();
}
}
Ok(())
}
pub fn extract_7zip(path: PathBuf) -> Result<()> {
let des_path = path.with_extension("d");
fs::create_dir_all(&des_path)?;
sevenz_rust::decompress_file(path, des_path)?;
Ok(())
}

181
src/install.rs Executable file
View File

@@ -0,0 +1,181 @@
use crate::util::{get_app_download_path, read_cache_file_sync, LoadError};
use lazy_static::lazy_static;
use log::debug;
use std::ffi::OsStr;
use std::fs;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use thiserror::Error;
lazy_static! {
static ref GAME_FILES: Result<Vec<String>, LoadError> = fetch_file_list_from_disk();
}
#[derive(Debug, Clone)]
pub struct State {
pub(crate) show_children: bool,
pub(crate) selected: bool,
pub(crate) id: usize,
}
#[derive(Debug, Clone)]
pub enum FSElement {
Dir(String, PathBuf, Vec<FSElement>, State),
File(String, PathBuf, State),
}
impl FSElement {
pub fn is_dir(&self) -> bool {
match self {
FSElement::Dir(..) => true,
FSElement::File(..) => false,
}
}
}
pub fn crawl(id: usize, path: &Path) -> (usize, FSElement) {
let name = path.file_name().unwrap().to_str().unwrap().to_string();
let state = State {
show_children: false,
selected: true,
id,
};
if let Ok(read_dir) = path.read_dir() {
let mut list = vec![];
let mut id = id;
for entry in read_dir.flatten() {
let (sub_id, elem) = crawl(id + 1, &entry.path());
id = sub_id;
list.push(elem)
}
(id, FSElement::Dir(name, path.to_path_buf(), list, state))
} else {
(id, FSElement::File(name, path.to_path_buf(), state))
}
}
pub fn list_mod() -> Vec<FSElement> {
let path = get_app_download_path().unwrap();
let mut list = vec![];
if let Ok(read_dir) = path.read_dir() {
for entry in read_dir.flatten() {
if entry.metadata().unwrap().is_dir() {
let (_, elem) = crawl(0, &entry.path());
list.push(elem)
}
}
}
list
}
#[derive(Error, Clone, Debug)]
pub enum InstallError {
#[error("Path is not a downloaded mod path")]
Path,
#[error("Failed to install this mod type")]
ModType,
#[error("Failed to copy the mod to the target location")]
Copy,
#[error("Game file list could not be loaded")]
Files,
}
fn is_known_name(name: &str) -> Option<&str> {
match name {
"localization" => Some(""),
"english" | "french" | "german" | "polish" | "russian" => Some("localization"),
"animations" | "Levels" | "libs" | "materials" | "movies" | "music" | "objects"
| "sounds" | "textures" => Some("localization/english"),
_ => None,
}
}
fn dir_copy(from: &Path, to: &Path, lang: &str) -> anyhow::Result<()> {
debug!("Create dir {:?} ", to);
create_dir_all(to)?;
let mut to_path = to.to_path_buf();
let name = match from.file_name().unwrap() {
a if a == "english" => OsStr::new(lang),
a => a,
};
to_path.push(name);
if let Ok(read_dir) = from.read_dir() {
for entry in read_dir.flatten() {
debug!("Process {:?} to {:?}", entry.path(), to_path);
dir_copy(&entry.path(), &to_path, lang)?
}
} else {
debug!("copy file {:?}", to_path);
fs::copy(from, to_path)?;
}
Ok(())
}
pub fn install(mod_: &FSElement, game_path: &Path, lang: &str) -> Result<(), InstallError> {
match mod_ {
FSElement::Dir(name, path, children, state) => {
if !state.selected {
return Ok(());
}
// if folder name is known copy it to game folder
if let Some(install_path) = is_known_name(name) {
let mut game_path = game_path.to_path_buf();
game_path.push(install_path.replace("english", lang));
dir_copy(path, &game_path, lang).map_err(|_| InstallError::Copy)?;
} else {
for child in children {
install(child, game_path, lang)?;
}
}
Ok(())
}
FSElement::File(name, from, state) => {
if !state.selected {
return Ok(());
}
let files = (*GAME_FILES).as_ref().map_err(|_| InstallError::Files)?;
let mut iter = files.iter();
let test = |x: &&String| {
let v: Vec<&str> = x.split('/').collect();
let n = v.last().unwrap();
*n == name
};
if let Some(path) = iter.find(test) {
if iter.find(test).is_some() {
return Err(InstallError::ModType);
}
let mut found_path = path.clone();
if path.starts_with("gamesdk") {
let replace = format!("localization/{}", lang);
found_path = found_path.replace("gamesdk", &replace);
}
let mut to = game_path.to_path_buf();
to.push(found_path);
create_dir_all(to.parent().unwrap()).map_err(|_| InstallError::Copy)?;
fs::copy(from, to).map_err(|_| InstallError::Copy)?;
}
Ok(())
}
}
}
fn fetch_file_list_from_disk() -> Result<Vec<String>, LoadError> {
let content = read_cache_file_sync("files.json").map_err(|_| LoadError::File)?;
serde_json::from_slice(&content).map_err(|_| LoadError::Format)
}
pub fn remove_mods(aw_path: &Path) -> anyhow::Result<()> {
if let Ok(read_dir) = aw_path.read_dir() {
for entry in read_dir.flatten() {
if entry.file_name() != "movies" {
fs::remove_dir_all(entry.path())?;
}
}
}
Ok(())
}

11
src/lib.rs Executable file
View File

@@ -0,0 +1,11 @@
extern crate core;
pub mod api;
pub mod config;
pub mod downloader_api;
pub mod explore_fs;
pub mod extractor;
pub mod install;
pub mod mod_parser;
pub mod ui;
pub mod util;

19
src/main.rs Executable file
View File

@@ -0,0 +1,19 @@
use anyhow::{Context, Result};
use aw_mod_installer::ui::app::App;
use iced::{Application, Settings};
use std::env;
//#[tokio::main]
fn main() -> Result<()> {
env_logger::init();
dotenv::dotenv().context("Error while reading .env file")?;
if cfg!(windows) && env::var("WGPU_BACKEND").is_err() {
env::set_var("WGPU_BACKEND", "DX12")
}
App::run(Settings::default()).context("App could not be started")?;
Ok(())
}

83
src/mod_parser.rs Executable file
View File

@@ -0,0 +1,83 @@
use crate::api::forum::Post;
use crate::util::{read_cache_file, write_to_cache_json, LoadError};
use anyhow::Result;
use log::info;
use regex::Regex;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone, EnumIter, Display)]
pub enum Tag {
All,
Sound,
Skin,
Model,
Camo,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Mod {
pub url: String,
pub author: String,
pub title: String,
pub content: String,
pub downloads: Vec<String>,
pub tags: Vec<Tag>,
}
impl From<Post> for Mod {
fn from(post: Post) -> Self {
Self {
url: post.url,
author: post.author,
title: post.title,
content: post.content,
downloads: vec![],
tags: vec![],
}
}
}
pub fn filter_mods(posts: Vec<Post>) -> Vec<Mod> {
let title_regex = Regex::new(r"\?|guide|tuto|tutorial|request").unwrap();
let download_regex =
Regex::new(r##""((https://cloud.mail.ru/|https://mega.nz/|https://drive.google.com/|https://disk.yandex.ru/|https://armoredlabs.net/applications/core/interface/file/attachment.php?)[\w/=?\-#]+)""##)
.unwrap();
let mut mods = Vec::new();
for post in posts {
let mut urls = Vec::new();
info!("== {} ==", post.title);
match post.title {
x if title_regex.is_match(&x) => continue,
_ => {
for captures in download_regex.captures_iter(&post.content) {
if let Some(match_) = captures.get(1) {
let url = match_.as_str();
urls.push(url.to_string());
info!("Found {:?}", url);
}
}
}
}
if !urls.is_empty() {
let mut mod_: Mod = post.into();
mod_.downloads = urls;
mods.push(mod_);
}
}
mods
}
pub async fn write_mod_list(posts: Vec<Post>) -> Result<()> {
let mods = filter_mods(posts);
write_to_cache_json(&mods, "mods").await
}
pub async fn fetch_mods_from_disk() -> std::result::Result<Vec<Mod>, LoadError> {
let content = read_cache_file("mods.json")
.await
.map_err(|_| LoadError::File)?;
serde_json::from_slice(&content).map_err(|_| LoadError::Format)
}

769
src/ui/app.rs Executable file
View File

@@ -0,0 +1,769 @@
use crate::config::{get_config, update_config, Config, InstalledMod};
use crate::downloader_api::{get_download_info, DownloadInfo, InfoError};
use crate::extractor::{extract_all, ExtractError, ModArchive};
use crate::install::{install, remove_mods, FSElement};
use crate::mod_parser::{fetch_mods_from_disk, Mod, Tag};
use crate::ui;
use crate::ui::download::Progress;
use crate::ui::download_bar::DownloadBar;
use crate::ui::installcheckbox::InstallCheckbox;
use crate::ui::modcheckbox::ModCheckbox;
use crate::ui::util::{open_url, show_error_dialog, show_question_dialog};
use crate::util::{find_aw_path, get_app_cache_path, get_app_config_path, is_aw_dir, LoadError};
use iced::alignment::Horizontal;
use iced::widget::{
button, checkbox, column, container, horizontal_space, pick_list, row, scrollable, text,
text_input, vertical_space, Button, Column,
};
use iced::{theme, Alignment, Application, Command, Element, Length, Subscription, Theme};
use lazy_static::lazy_static;
use native_dialog::FileDialog;
use std::path::{Path, PathBuf};
use std::string::ToString;
use strum::IntoEnumIterator;
lazy_static! {
static ref LANGUAGE_LIST: Vec<String> = vec![
String::from("english"),
String::from("french"),
String::from("german"),
String::from("polish"),
String::from("russian"),
];
}
pub struct App {
steps: Steps,
config: Config,
}
#[derive(Debug, Clone)]
pub enum Message {
BackPressed,
NextPressed,
StepMessage(StepMessage),
ModsLoaded(Result<Vec<Mod>, LoadError>),
ConfigLoaded(Result<Config, LoadError>),
UpdateConfig(Result<(), LoadError>),
}
impl Application for App {
type Executor = iced::executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Self {
steps: Steps::new(),
config: Config::default(),
},
Command::perform(get_config(), Message::ConfigLoaded),
)
}
fn title(&self) -> String {
format!("{} - AW mod installer", self.steps.title())
}
fn update(&mut self, event: Message) -> Command<Message> {
match event {
Message::BackPressed => {
self.steps.go_back();
if let Step::Installation(extracting, ..) =
&mut self.steps.steps[self.steps.current + 1]
{
*extracting = false;
}
Command::none()
}
Message::NextPressed => {
let cmd = match &self.steps.steps[self.steps.current] {
Step::Welcome(path) => {
self.config.aw_path = path.to_owned();
Command::none()
}
Step::Selection(state) => {
let urls: Vec<_> = state
.mod_list
.iter()
.filter(|x| x.selected)
.map(|mod_| {
let url = mod_.get_download_url();
let title = mod_.m.title.clone();
if !self.config.installed_mod.iter().any(|m| m.download == url) {
self.config.installed_mod.push(InstalledMod::new(
url.clone(),
mod_.m.title.clone(),
PathBuf::new(),
));
}
Command::perform(get_download_info(url), |x| {
Message::StepMessage(StepMessage::DownloadInfoFound(
DownloadInfo { title, info: x },
))
})
})
.collect();
if !urls.is_empty() {
if let Step::Download(bool, _) =
&mut self.steps.steps[self.steps.current + 1]
{
*bool = false;
}
}
Command::batch(urls)
}
Step::Download(_, downloads) => {
let mut mods = vec![];
if let Step::Installation(_, extracted, ..) =
&self.steps.steps[self.steps.current + 1]
{
for download in downloads {
if let Some(found) = self
.config
.installed_mod
.iter_mut()
.find(|m| m.title == download.title)
{
found.path = download.path.clone();
}
if !extracted.iter().any(|x| x.path == download.path) {
mods.push(ModArchive {
title: download.title.clone(),
path: download.path.clone(),
})
}
}
}
Command::perform(extract_all(mods), |x| {
Message::StepMessage(StepMessage::Extracted(x))
})
}
Step::Installation(_, install_list, lang, clean) => {
if *clean {
if install_list.is_empty() {
self.config.installed_mod = vec![];
}
let mut path = self.config.aw_path.clone();
path.push("localization");
path.push(lang);
remove_mods(&path).unwrap_or_else(|_| {
show_error_dialog(
"Error removing mods",
"An error occurred while removing installed mods.\
\nSome mod could have not been removed.\
\nYou can remove them manually and restart the process.",
)
});
}
for mod_ in install_list {
if let FSElement::Dir(_, _, _, state) = &mod_.file_list {
if !state.selected {
if let Some(id) = self
.config
.installed_mod
.iter()
.position(|m| mod_.title == m.title)
{
self.config.installed_mod.remove(id);
}
}
};
install(&mod_.file_list, &self.config.aw_path, lang).unwrap_or_else(
|e| {
let content = format!(
"The mod {} could not be correctly installed.\
\n Please report the issue if possible.\n Error :{:?}",
mod_.title, e
);
show_error_dialog("Error installing Mod", &content);
},
)
}
Command::none()
}
_ => Command::none(),
};
self.steps.advance();
let update_cmd =
Command::perform(update_config(self.config.clone()), Message::UpdateConfig);
Command::batch([cmd, update_cmd])
}
Message::ConfigLoaded(result) => {
match result {
Ok(cfg) => self.config = cfg,
Err(_) => show_error_dialog(
"Error loading config",
"The configuration file could not be loaded using default settings.",
),
}
Command::perform(fetch_mods_from_disk(), Message::ModsLoaded)
}
Message::ModsLoaded(loaded) => {
if let Ok(mods) = loaded {
let cfg = &self.config;
let mut mod_list: Vec<ModCheckbox> = vec![];
let mut installed_list = vec![];
let mut extracted_list = vec![];
for m in mods {
let mut selected = false;
let mut version = None;
if let Some(found) = cfg.installed_mod.iter().find(|c| c.title == m.title) {
if let Some(i) = m.downloads.iter().position(|x| *x == found.download) {
selected = true;
if m.downloads.len() > 1 {
version = Some(i);
}
installed_list.push(DownloadBar::from_installed(i, found.clone()));
extracted_list.push(InstallCheckbox::from(found.clone()));
}
}
mod_list.push(ModCheckbox::new(m, selected, version));
}
let new_steps = Steps::get_next_steps(mod_list, installed_list, extracted_list);
self.steps.steps.extend(new_steps);
} else {
let content = format!(
"The mod file could not be loaded!\
\nPlease restart the app and try again.\
\nIf the error persist check the files in {}",
get_app_cache_path().unwrap_or_default().to_str().unwrap()
);
show_error_dialog("Error loading mods", &content)
}
Command::none()
}
Message::StepMessage(step_msg) => {
self.steps.update(step_msg);
Command::none()
}
Message::UpdateConfig(res) => {
match res {
Ok(_) => {}
Err(_) => {
let path = get_app_config_path().unwrap_or_default();
let content = format!("Configuration could not be saved ! Please restart the app and/or check the file at {:?}.",path);
show_error_dialog("Error saving config", &content);
}
};
Command::none()
}
}
}
fn view(&self) -> Element<Message> {
let Self { steps, .. } = self;
let mut controls = row![];
if steps.can_go_back() {
controls = controls.push(
button_c("Back")
.on_press(Message::BackPressed)
.style(theme::Button::Secondary),
);
}
controls = controls.push(horizontal_space(Length::Fill));
if steps.can_continue() {
controls = controls.push(
button_c("Next")
.on_press(Message::NextPressed)
.style(theme::Button::Primary),
);
}
let content = column![steps.view().map(Message::StepMessage), controls,]
.spacing(20)
.padding(20)
.max_width(1000);
match self.steps.steps[self.steps.current] {
Step::Selection(_) => container(content)
.center_x()
.center_y()
.height(Length::Fill)
.width(Length::Fill)
.into(),
_ => {
let scrollable = scrollable(container(content).width(Length::Fill).center_x());
container(scrollable).height(Length::Fill).center_y().into()
}
}
}
fn theme(&self) -> Self::Theme {
Theme::Dark
}
fn subscription(&self) -> Subscription<Message> {
self.steps.subscription().map(Message::StepMessage)
}
}
struct Steps {
steps: Vec<Step>,
current: usize,
}
impl Steps {
fn new() -> Steps {
Steps {
steps: vec![Step::Welcome(find_aw_path())],
current: 0,
}
}
fn get_next_steps(
mod_list: Vec<ModCheckbox>,
installed_list: Vec<DownloadBar>,
extracted_list: Vec<InstallCheckbox>,
) -> Vec<Step> {
vec![
Step::Selection(SelectionStruct::new(mod_list)),
Step::Download(true, installed_list),
Step::Installation(false, extracted_list, "english".to_string(), false),
Step::End,
]
}
fn update(&mut self, msg: StepMessage) {
self.steps[self.current].update(msg);
}
fn subscription(&self) -> Subscription<StepMessage> {
Subscription::batch(self.steps.iter().map(Step::subscription))
}
fn view(&self) -> Element<StepMessage> {
self.steps[self.current].view()
}
fn advance(&mut self) {
if self.can_continue() {
self.current += 1;
}
}
fn go_back(&mut self) {
if self.can_go_back() {
self.current -= 1;
}
}
fn can_go_back(&self) -> bool {
self.current > 0 && self.steps[self.current].can_go_back()
}
fn can_continue(&self) -> bool {
self.steps.len() > 1
&& self.current + 1 < self.steps.len()
&& self.steps[self.current].can_continue()
}
fn title(&self) -> &str {
self.steps[self.current].title()
}
}
struct SelectionStruct {
input: String,
mod_list: Vec<ModCheckbox>,
filter: Tag,
}
impl SelectionStruct {
pub fn new(mod_list: Vec<ModCheckbox>) -> Self {
Self {
input: String::new(),
mod_list,
filter: Tag::All,
}
}
}
enum Step {
Welcome(PathBuf),
Selection(SelectionStruct),
Download(bool, Vec<DownloadBar>),
Installation(bool, Vec<InstallCheckbox>, String, bool),
End,
}
#[derive(Debug, Clone)]
pub enum StepMessage {
ModChecked(usize, bool),
VersionSelected(usize, usize),
OpenExt(String),
AWPathInputChanged(String),
SelectAWPath,
FilterInputChanged(String),
ChangeFilter(Tag),
DownloadInfoFound(DownloadInfo),
Download(usize),
DownloadProgressed((usize, Progress)),
Extracted(Result<Vec<ModArchive>, ExtractError>),
InstallModChecked(usize, usize),
InstallModShow(usize, usize),
LangSelected(String),
ToggleCleanInstall(bool),
}
impl<'a> Step {
fn subscription(&self) -> Subscription<StepMessage> {
match self {
Step::Download(_, downloads) => {
Subscription::batch(downloads.iter().map(DownloadBar::subscription))
}
_ => Subscription::none(),
}
}
fn update(&mut self, msg: StepMessage) {
match msg {
StepMessage::ModChecked(i, checked) => {
if let Step::Selection(state) = self {
let m = state.mod_list.get_mut(i).unwrap();
if m.m.downloads.len() <= 1 || m.version.is_some() {
m.selected = checked;
}
}
}
StepMessage::VersionSelected(i, version) => {
if let Step::Selection(state) = self {
let m = state.mod_list.get_mut(i).unwrap();
m.version = Some(version);
}
}
StepMessage::OpenExt(url) => {
open_url(url);
}
StepMessage::AWPathInputChanged(path) => {
if let Step::Welcome(old_path) = self {
*old_path = PathBuf::from(path);
}
}
StepMessage::SelectAWPath => {
if let Step::Welcome(old_path) = self {
let path = FileDialog::new()
.add_filter("exe", &["exe"])
.show_open_single_file()
.unwrap();
let path = match path {
Some(path) => path,
None => return,
};
if let Some(path) = path.parent() {
if let Some(path) = path.parent() {
if is_aw_dir(path.to_path_buf()) {
*old_path = path.to_path_buf();
}
}
}
}
}
StepMessage::FilterInputChanged(input) => {
if let Step::Selection(state) = self {
state.input = input;
}
}
StepMessage::ChangeFilter(new_filter) => {
if let Step::Selection(state) = self {
state.filter = new_filter;
}
}
StepMessage::DownloadInfoFound(res) => {
if let Step::Download(bool, downloads) = self {
*bool = true;
match res.info {
Ok((url, path)) => {
if !downloads
.iter()
.any(|d| d.url == url || (d.path == path && d.url == "skip"))
{
let id = downloads.len();
let mut download = DownloadBar::new(id, url, path, res.title);
download.start();
downloads.push(download)
}
}
Err(err) => match err {
InfoError::FormatUrl(url) => {
let response = show_question_dialog("Mod could not be downloaded","A mod could not be automatically downloaded would you like to open the download link in a browser ?");
if response {
open_url(url)
}
}
InfoError::APIKey(api) => {
let txt = format!(
"The key for the {} API was not found int the .env file.",
api
);
show_error_dialog("API key not found !", txt.as_str())
}
_ => {}
},
}
}
}
StepMessage::Download(index) => {
if let Step::Download(_, downloads) = self {
if let Some(download) = downloads.get_mut(index) {
download.start();
}
}
}
StepMessage::DownloadProgressed((id, progress)) => {
if let Step::Download(_, downloads) = self {
if let Some(download) = downloads.iter_mut().find(|download| download.id == id)
{
download.progress(progress);
}
}
}
StepMessage::Extracted(r) => {
if let Step::Installation(extracting, extracted, ..) = self {
if let Ok(extracted_) = r {
for arch in extracted_ {
extracted.push(arch.into());
}
}
*extracting = true;
}
}
StepMessage::InstallModChecked(box_id, fs_id) => {
if let Step::Installation(_, extracted, ..) = self {
let box_ = &mut extracted[box_id];
box_.toggle_checked(fs_id)
}
}
StepMessage::InstallModShow(box_id, fs_id) => {
if let Step::Installation(_, extracted, ..) = self {
let box_ = &mut extracted[box_id];
box_.toggle_show(fs_id)
}
}
StepMessage::LangSelected(lang) => {
if let Step::Installation(.., current_lang, _) = self {
*current_lang = lang;
}
}
StepMessage::ToggleCleanInstall(new) => {
if let Step::Installation(.., clean) = self {
*clean = new;
}
}
}
}
fn title(&self) -> &str {
match self {
Step::Welcome(_) => "Welcome",
Step::Selection(_) => "Selection",
Step::Download(..) => "Download",
Step::Installation(..) => "Installation",
Step::End => "End",
}
}
fn can_download_continue(downloads: &Vec<DownloadBar>) -> bool {
for d in downloads {
match d.state {
ui::download_bar::State::Finished { .. } => {}
_ => return false,
}
}
true
}
pub fn can_go_back(&self) -> bool {
match self {
Step::Download(bool, downloads) => *bool && Self::can_download_continue(downloads),
Step::Installation(bool, ..) => *bool,
_ => true,
}
}
fn can_continue(&self) -> bool {
match self {
Step::End => false,
Step::Welcome(path) => is_aw_dir(path.clone()),
Step::Download(bool, downloads) => *bool && Self::can_download_continue(downloads),
Step::Installation(bool, ..) => *bool,
_ => true,
}
}
fn view(&self) -> Element<StepMessage> {
match self {
Step::Welcome(path) => Self::welcome(path),
Step::Selection(state) => Self::selection(state),
Step::Download(fetched, downloads) => Self::download(fetched, downloads),
Step::Installation(extracting, extracted, lang, clean) => {
Self::installation(extracting, extracted, lang.clone(), clean)
}
Step::End => Self::end(),
}
.into()
}
fn container(title: &str) -> Column<'a, StepMessage> {
column![text(title).size(50)].spacing(20)
}
fn welcome(path: &Path) -> Column<'a, StepMessage> {
let msg = Self::container("Welcome!")
.push(
"This is a simple utility to help you chose and install Armored Warfare mods to your client..",
)
.push(
"It will guide you trough the different steps. Press \"next\" to continue",
).push("Before continuing please verify that this is the correct path to your Armored Warfare installation");
let text_input = text_input(
"Path to AW directory",
path.to_str().unwrap(),
StepMessage::AWPathInputChanged,
)
.padding(10);
let btn_select_path = button_c("...").on_press(StepMessage::SelectAWPath);
let path_input = row![text_input, btn_select_path].align_items(Alignment::Center);
let mut col = column![
msg,
path_input,
text("*Select the exe file in 'bin64'").size(15)
];
if !is_aw_dir(path.to_path_buf()) {
col = col.push(text("Not an Armored Warfare directory"))
};
col.spacing(20)
}
fn selection(state: &'a SelectionStruct) -> Column<'a, StepMessage> {
let mods = &state.mod_list;
let input = &state.input;
let filter = &state.filter;
let title = Self::container("Selection").width(Length::Fill);
let mod_list = column(
mods.iter()
.enumerate()
.filter(|(_, m)| m.m.title.to_lowercase().contains(&input.to_lowercase()))
.map(|(i, x)| x.view(i))
.collect(),
);
let scroll = scrollable(mod_list);
let input = text_input(
"Filter mods by name...",
input,
StepMessage::FilterInputChanged,
)
.padding(15)
.size(30);
let filter_butons = row(Tag::iter()
.map(|tag| {
let txt = text(&tag.to_string()).size(25);
button(txt)
.style(if tag == *filter {
theme::Button::Primary
} else {
theme::Button::Text
})
.padding(5)
.on_press(StepMessage::ChangeFilter(tag))
.into()
})
.collect());
let filter_row =
row![input, filter_butons.width(Length::Shrink)].align_items(Alignment::Center);
column![title, filter_row, scroll]
.spacing(20)
.padding(20)
.height(Length::Fill)
}
fn download(fetched: &'a bool, downloads: &'a [DownloadBar]) -> Column<'a, StepMessage> {
let title = Self::container("Download");
let downloads = if !downloads.is_empty() {
Column::with_children(downloads.iter().map(DownloadBar::view).collect())
.spacing(20)
.align_items(Alignment::End)
} else {
column![
vertical_space(Length::Units(30)),
text(if !*fetched {
"Fetching mod information"
} else {
"No mod selected to download"
}),
vertical_space(Length::Units(100))
]
};
column![title, downloads].padding(20)
}
fn installation(
extracted: &bool,
mods: &'a [InstallCheckbox],
lang: String,
clean: &bool,
) -> Column<'a, StepMessage> {
let mut content = Self::container("Installation");
let pickup = pick_list(&*LANGUAGE_LIST, Some(lang), move |x| {
StepMessage::LangSelected(x)
});
content = content.push(
row![text("Select your game language : "), pickup].align_items(Alignment::Center),
);
let check = checkbox("", *clean, StepMessage::ToggleCleanInstall);
content = content.push(
row![text("Clean install (will delete installed mods) : "), check]
.align_items(Alignment::Center),
);
if !*extracted {
content = content.push(text("extracting..."));
} else {
let mod_list = column(mods.iter().enumerate().map(|(i, x)| x.view(i)).collect());
content = content.push(mod_list);
}
content
}
fn end() -> Column<'a, StepMessage> {
Self::container("You reached the end!")
.push("The selected mods have now been installed. Enjoy!")
.push("You can go back and make changes if you want to.")
}
}
fn button_c<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
iced::widget::button(text(label).horizontal_alignment(Horizontal::Center))
.padding(12)
.width(Length::Units(100))
}

108
src/ui/download.rs Executable file
View File

@@ -0,0 +1,108 @@
use anyhow::Context;
use iced::subscription;
use std::hash::Hash;
use std::path::PathBuf;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
// Just a little utility function
pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
id: I,
url: T,
path: PathBuf,
) -> iced::Subscription<(I, Progress)> {
subscription::unfold(id, State::Ready(url.to_string(), path), move |state| {
download(id, state)
})
}
#[derive(Debug, Hash, Clone)]
pub struct Download<I> {
id: I,
url: String,
}
async fn download<I: Copy>(id: I, state: State) -> (Option<(I, Progress)>, State) {
match state {
State::Ready(url, path) => {
let response = reqwest::get(&url).await;
match response {
Ok(response) => {
if let Some(total) = response.content_length() {
let file = File::create(&path)
.await
.context("Cloud not create file")
.unwrap();
(
Some((id, Progress::Started)),
State::Downloading {
response: Box::new(response),
total,
downloaded: 0,
file,
},
)
} else {
(Some((id, Progress::Errored)), State::Finished)
}
}
Err(_) => (Some((id, Progress::Errored)), State::Finished),
}
}
State::Downloading {
mut response,
total,
downloaded,
mut file,
} => match response.chunk().await {
Ok(Some(chunk)) => {
file.write_all(&chunk)
.await
.context("Could not write to file")
.unwrap();
let downloaded = downloaded + chunk.len() as u64;
let percentage = (downloaded as f32 / total as f32) * 100.0;
(
Some((id, Progress::Advanced(percentage))),
State::Downloading {
response,
total,
downloaded,
file,
},
)
}
Ok(None) => (Some((id, Progress::Finished)), State::Finished),
Err(_) => (Some((id, Progress::Errored)), State::Finished),
},
State::Finished => {
// We do not let the stream die, as it would start a
// new download repeatedly if the user is not careful
// in case of errors.
iced::futures::future::pending().await
}
}
}
#[derive(Debug, Clone)]
pub enum Progress {
Started,
Advanced(f32),
Finished,
Errored,
}
pub enum State {
Ready(String, PathBuf),
Downloading {
response: Box<reqwest::Response>,
total: u64,
downloaded: u64,
file: File,
},
Finished,
}

120
src/ui/download_bar.rs Executable file
View File

@@ -0,0 +1,120 @@
use crate::config::InstalledMod;
use crate::ui::app::StepMessage;
use crate::ui::download;
use iced::widget::{button, column, progress_bar, text, Column};
use iced::{Alignment, Element, Subscription};
use std::path::PathBuf;
#[derive(Debug)]
pub struct DownloadBar {
pub id: usize,
pub state: State,
pub url: String,
pub path: PathBuf,
pub title: String,
}
#[derive(Debug)]
pub enum State {
Idle,
Downloading { progress: f32 },
Finished,
Errored,
}
impl DownloadBar {
pub fn new(id: usize, url: String, path: PathBuf, title: String) -> Self {
DownloadBar {
id,
state: State::Idle,
url,
path,
title,
}
}
pub fn from_installed(id: usize, installed: InstalledMod) -> Self {
DownloadBar {
id,
state: State::Finished,
url: "skip".to_string(),
path: installed.path,
title: installed.title,
}
}
pub fn start(&mut self) {
match self.state {
State::Idle { .. } | State::Finished { .. } | State::Errored { .. } => {
self.state = State::Downloading { progress: 0.0 };
}
_ => {}
}
}
pub fn progress(&mut self, new_progress: download::Progress) {
if let State::Downloading { progress } = &mut self.state {
match new_progress {
download::Progress::Started => {
*progress = 0.0;
}
download::Progress::Advanced(percentage) => {
*progress = percentage;
}
download::Progress::Finished => {
self.state = State::Finished;
}
download::Progress::Errored => {
self.state = State::Errored;
}
}
}
}
pub fn subscription(&self) -> Subscription<StepMessage> {
match self.state {
State::Downloading { .. } => download::file(self.id, &self.url, self.path.to_owned())
.map(StepMessage::DownloadProgressed),
_ => Subscription::none(),
}
}
pub fn view(&self) -> Element<StepMessage> {
let current_progress = match &self.state {
State::Idle { .. } => 0.0,
State::Downloading { progress } => *progress,
State::Finished { .. } => 100.0,
State::Errored { .. } => 0.0,
};
let progress_bar = progress_bar(0.0..=100.0, current_progress);
let control: Element<_> = match &self.state {
State::Idle => text("Download starting...").into(),
State::Finished => text("Download finished!").into(),
State::Downloading { .. } => text(format!(
"Downloading {}... {:.2}%",
self.title, current_progress
))
.into(),
State::Errored => column![
text(format!(
"Something went wrong downloading {} :(",
self.title
)),
button("Try again").on_press(StepMessage::Download(self.id)),
]
.spacing(10)
.align_items(Alignment::Center)
.into(),
};
Column::new()
.spacing(10)
.padding(10)
.align_items(Alignment::Center)
.push(progress_bar)
.push(control)
.into()
}
}

164
src/ui/installcheckbox.rs Executable file
View File

@@ -0,0 +1,164 @@
use crate::config::InstalledMod;
use crate::extractor::ModArchive;
use crate::install::{crawl, FSElement, State};
use crate::ui::app::StepMessage;
use iced::widget::{button, checkbox, column, horizontal_space, row, text};
use iced::{Alignment, Element, Length};
use std::path::{PathBuf, MAIN_SEPARATOR};
#[derive(Clone)]
pub struct InstallCheckbox {
pub title: String,
pub path: PathBuf,
pub file_list: FSElement,
}
impl From<ModArchive> for InstallCheckbox {
fn from(m: ModArchive) -> Self {
let (_, file_list) = crawl(0, &m.path.with_extension("d"));
Self {
title: m.title,
path: m.path,
file_list,
}
}
}
impl From<InstalledMod> for InstallCheckbox {
fn from(m: InstalledMod) -> Self {
let (_, file_list) = crawl(0, &m.path.with_extension("d"));
Self {
title: m.title,
path: m.path,
file_list,
}
}
}
impl InstallCheckbox {
pub fn view(&self, id: usize) -> Element<StepMessage> {
if let FSElement::Dir(_, _, children, state) = &self.file_list {
view_dir(id, 0, &self.title, children, state)
} else {
column![].into()
}
}
pub fn toggle_show(&mut self, id: usize) {
apply_to_elem(&mut self.file_list, id, |element| match element {
FSElement::Dir(_, _, _, state) | FSElement::File(_, _, state) => {
state.show_children = !state.show_children
}
});
}
pub fn toggle_checked(&mut self, id: usize) {
apply_to_elem(&mut self.file_list, id, |x| {
let selected = match x {
FSElement::Dir(_, _, _, state) | FSElement::File(_, _, state) => !state.selected,
};
toggle_recursive(x, selected)
});
}
}
fn view_dir<'a>(
id: usize,
space: u16,
name: &'a str,
children: &'a Vec<FSElement>,
state: &'a State,
) -> Element<'a, StepMessage> {
let expand = button(text("+").size(20))
.on_press(StepMessage::InstallModShow(id, state.id))
.padding(0);
let mut name_ = name.to_string();
let mut children_ = children;
while space != 0 && children_.len() == 1 && children_[0].is_dir() {
if let FSElement::Dir(new_name, _, new_children, _) = &children_[0] {
name_ = format!("{}{}{}", name_, MAIN_SEPARATOR, new_name);
children_ = new_children;
}
}
let c = checkbox(name_, state.selected, move |_| {
StepMessage::InstallModChecked(id, state.id)
});
let row = row![expand, c].align_items(Alignment::Center);
let mut list = column!().padding(5);
if state.show_children {
for child in children_ {
list = list.push(view_children(id, space + 1, child))
}
}
column![row, list].into()
}
fn view_children(id: usize, space: u16, elem: &FSElement) -> Element<StepMessage> {
let card: Element<StepMessage> = match elem {
FSElement::Dir(name, _, children, state) => view_dir(id, space, name, children, state),
FSElement::File(name, _, state) => checkbox(name, state.selected, move |_| {
StepMessage::InstallModChecked(id, state.id)
})
.into(),
};
let space = horizontal_space(Length::Units(space * 5));
row![space, card].into()
}
fn toggle_recursive(element: &mut FSElement, selected: bool) {
match element {
FSElement::Dir(_, _, children, state) => {
state.selected = selected;
for child in children {
toggle_recursive(child, selected);
}
}
FSElement::File(_, _, state) => {
state.selected = selected;
}
}
}
fn apply_to_elem<F>(element: &mut FSElement, id: usize, mut apply: F) -> bool
where
F: FnMut(&mut FSElement),
{
match element {
FSElement::Dir(_, _, children, state) => {
if state.id == id {
apply(element);
true
} else {
for i in 0..children.len() {
if let Some(elem) = children.get(i + 1) {
if let FSElement::Dir(_, _, _, state) = elem {
if state.id > id {
return apply_to_elem(&mut children[i], id, apply);
}
}
} else {
return apply_to_elem(&mut children[i], id, apply);
}
if let FSElement::File(_, _, state) = &mut children[i] {
if state.id == id {
apply(&mut children[i]);
return true;
}
}
}
false
}
}
FSElement::File(_, _, state) => {
if state.id == id {
apply(element);
true
} else {
false
}
}
}
}

6
src/ui/mod.rs Executable file
View File

@@ -0,0 +1,6 @@
pub mod app;
pub mod download;
mod download_bar;
mod installcheckbox;
mod modcheckbox;
mod util;

57
src/ui/modcheckbox.rs Executable file
View File

@@ -0,0 +1,57 @@
use crate::mod_parser::Mod;
use crate::ui::app::StepMessage;
use iced::alignment::Horizontal;
use iced::widget::{button, checkbox, pick_list, row, text};
use iced::{Alignment, Element, Length};
#[derive(Clone)]
pub struct ModCheckbox {
pub(crate) m: Mod,
pub(crate) selected: bool,
pub(crate) version: Option<usize>,
}
impl ModCheckbox {
pub fn new(m: Mod, selected: bool, version: Option<usize>) -> Self {
Self {
m,
selected,
version,
}
}
pub fn get_download_url(&self) -> String {
match self.version {
Some(n) => self.m.downloads[n].clone(),
None => self.m.downloads[0].clone(),
}
}
pub fn view(&self, i: usize) -> Element<StepMessage> {
let title = format!("{} by ", self.m.title);
let author = text(&self.m.author).size(24);
let checkbox = checkbox(title, self.selected, move |x| StepMessage::ModChecked(i, x));
let txt = text("open").horizontal_alignment(Horizontal::Center);
let ext_button = button(txt)
.padding(10)
.width(Length::Units(65))
.on_press(StepMessage::OpenExt(self.m.url.clone()));
let check_mod = if self.m.downloads.len() > 1 {
let list: Vec<usize> = (1..=self.m.downloads.len()).collect();
let pickup = pick_list(list, self.version.map(|n| n + 1), move |x| {
StepMessage::VersionSelected(i, x - 1)
})
.placeholder("...");
row![checkbox, author, pickup]
} else {
row![checkbox, author,]
}
.align_items(Alignment::Center)
.width(Length::Fill);
row![check_mod, ext_button]
.align_items(Alignment::Center)
.padding(5)
.into()
}
}

27
src/ui/util.rs Executable file
View File

@@ -0,0 +1,27 @@
use log::error;
use std::thread;
pub fn show_error_dialog(title: &str, content: &str) {
native_dialog::MessageDialog::new()
.set_type(native_dialog::MessageType::Error)
.set_title(title)
.set_text(content)
.show_alert()
.unwrap_or_default();
}
pub fn show_question_dialog(title: &str, content: &str) -> bool {
native_dialog::MessageDialog::new()
.set_type(native_dialog::MessageType::Warning)
.set_title(title)
.set_text(content)
.show_confirm()
.unwrap_or_default()
}
pub fn open_url(url: String) {
thread::spawn(|| {
if open::that(url).is_err() {
error!("Could not open external link")
};
});
}

147
src/util.rs Executable file
View File

@@ -0,0 +1,147 @@
use crate::util::Error::ReadCache;
use anyhow::Result;
use serde::Serialize;
use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::fs;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
#[derive(Error, Debug)]
pub enum Error {
#[error("Error reading cache dir")]
ReadCache,
}
#[derive(Debug, Clone)]
pub enum LoadError {
File,
Format,
}
pub(crate) fn get_app_cache_path() -> Result<PathBuf> {
let mut path = dirs_next::cache_dir().ok_or(ReadCache)?;
path.push("aw-mod-installer");
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
Ok(path)
}
pub(crate) fn get_app_config_path() -> Result<PathBuf> {
let mut path = dirs_next::config_dir().ok_or(ReadCache)?;
path.push("aw-mod-installer");
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
path.push("config.json");
Ok(path)
}
pub(crate) fn get_app_download_path() -> Result<PathBuf> {
let mut path = dirs_next::download_dir().ok_or(ReadCache)?;
path.push("aw-mod-installer");
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
Ok(path)
}
pub(crate) async fn write_to_json<T>(val: &T, path: &Path) -> Result<()>
where
T: Serialize,
{
let j = serde_json::to_string(val)?;
fs::write(path, &j).await?;
Ok(())
}
pub(crate) async fn write_to_cache_json<T>(val: &T, file_name: &str) -> Result<()>
where
T: Serialize,
{
let mut file = get_app_cache_path()?;
file.push(format!("{file_name}.json"));
write_to_json(val, &file).await
}
pub(crate) async fn read_file(path: PathBuf) -> Result<Vec<u8>> {
let mut file = File::open(path).await?;
let mut contents = vec![];
file.read_to_end(&mut contents).await?;
Ok(contents)
}
pub(crate) async fn read_cache_file(file_name: &str) -> Result<Vec<u8>> {
let mut path = get_app_cache_path()?;
path.push(file_name);
read_file(path).await
}
pub(crate) fn read_cache_file_sync(file_name: &str) -> Result<Vec<u8>> {
use std::io::Read;
let mut path = get_app_cache_path()?;
path.push(file_name);
let mut file = std::fs::File::open(path)?;
let mut contents = vec![];
file.read_to_end(&mut contents)?;
Ok(contents)
}
pub fn find_aw_path() -> PathBuf {
let launcher_dirs = vec!["MyGames", "MGLauncher"];
let steam_path;
let root;
if cfg!(windows) {
root = "C:\\";
steam_path = format!(
"{root}Program Files (x86)\\Steam\\steamapps\\common\\Armored Warfare\\13_2000009"
);
} else {
root = "/";
let home = dirs_next::home_dir().unwrap();
steam_path = format!(
"{}/.steam/steam/steamapps/common/Armored Warfare/13_2000009/",
home.to_str().unwrap()
);
};
for ld in launcher_dirs {
let root_path = format!("{root}{ld}");
let path = Path::new(&root_path);
if let Ok(p) = path.read_dir() {
for entry in p.flatten() {
if let Ok(t) = entry.file_type() {
if t.is_dir() && is_aw_dir(entry.path()) {
return entry.path();
}
}
}
}
}
let steam_path = PathBuf::from(&steam_path);
if steam_path.exists() && is_aw_dir(steam_path.clone()) {
return steam_path;
}
PathBuf::new()
}
pub fn is_aw_dir(path: PathBuf) -> bool {
let exe_path = ("bin64", "ArmoredWarfare.exe");
let mut p = path;
p.push(exe_path.0);
p.push(exe_path.1);
p.exists()
}