initial commit
This commit is contained in:
2
.gitignore
vendored
Executable file
2
.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.idea
|
||||
4006
Cargo.lock
generated
Executable file
4006
Cargo.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
Executable file
34
Cargo.toml
Executable 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
132
src/api.rs
Executable 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
55
src/config.rs
Normal 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
84
src/downloader_api.rs
Executable 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
65
src/explore_fs.rs
Executable 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
110
src/extractor.rs
Executable 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
181
src/install.rs
Executable 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
11
src/lib.rs
Executable 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
19
src/main.rs
Executable 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
83
src/mod_parser.rs
Executable 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
769
src/ui/app.rs
Executable 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
108
src/ui/download.rs
Executable 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
120
src/ui/download_bar.rs
Executable 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
164
src/ui/installcheckbox.rs
Executable 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
6
src/ui/mod.rs
Executable 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
57
src/ui/modcheckbox.rs
Executable 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
27
src/ui/util.rs
Executable 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
147
src/util.rs
Executable 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()
|
||||
}
|
||||
Reference in New Issue
Block a user