From bef92e71fae9cc9c661c9408d80cf2529f488e6e Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Wed, 19 Aug 2020 21:18:32 +0200 Subject: [PATCH 01/20] chore: anchor regex to the start --- src/toc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toc.rs b/src/toc.rs index f2dc2eb4..44a1430f 100644 --- a/src/toc.rs +++ b/src/toc.rs @@ -109,7 +109,7 @@ async fn parse_toc_entry(toc_entry: DirEntry) -> Option { // which is why they are created here. // // https://docs.rs/regex/1.3.9/regex/#example-avoid-compiling-the-same-regex-in-a-loop - let re_toc = Regex::new(r"##\s(?P.*?):\s?(?P.*)").unwrap(); + let re_toc = Regex::new(r"^##\s(?P.*?):\s?(?P.*)").unwrap(); let re_title = Regex::new(r"\|[a-fA-F\d]{9}([^|]+)\|r?").unwrap(); for line in reader.lines() { From 906fa7d8e98521a01caacb920409746a75572fc7 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Wed, 19 Aug 2020 21:49:24 +0200 Subject: [PATCH 02/20] fix: sometimes zip failed to unpack --- src/fs.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 2edbb4e0..f00a2a0f 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -17,26 +17,23 @@ pub async fn install_addon( to_directory: &PathBuf, ) -> Result<()> { let zip_path = from_directory.join(addon.id.clone()); - // TODO: This sometimes fails: No such file or directory (os error 2). let mut zip_file = std::fs::File::open(&zip_path)?; let mut archive = zip::ZipArchive::new(&mut zip_file)?; // TODO: Maybe remove old addon here, so we don't replace. - - for i in 1..archive.len() { + for i in 0..archive.len() { let mut file = archive.by_index(i)?; let path = to_directory.join(file.sanitized_name()); - - if file.is_dir() { - std::fs::create_dir_all(path)?; + if (&*file.name()).ends_with('/') { + std::fs::create_dir_all(&path).unwrap(); } else { - let mut target = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path)?; - - std::io::copy(&mut file, &mut target)?; + if let Some(p) = path.parent() { + if !p.exists() { + std::fs::create_dir_all(&p).unwrap(); + } + } + let mut outfile = std::fs::File::create(&path).unwrap(); + std::io::copy(&mut file, &mut outfile).unwrap(); } } From 8d70fb042d090443b29d984b3aa99b02df718153 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Wed, 19 Aug 2020 22:17:22 +0200 Subject: [PATCH 03/20] fix: handle case with multiple toc files --- src/toc.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/toc.rs b/src/toc.rs index 44a1430f..aef51392 100644 --- a/src/toc.rs +++ b/src/toc.rs @@ -30,9 +30,16 @@ pub async fn read_addon_directory>(path: P) -> Result> let file_name = entry.file_name(); let file_extension = get_extension(file_name).await; if file_extension == Some("toc") { - let addon = parse_toc_entry(entry).await; - if let Some(addon) = addon { - addons.push(addon) + // We only look at the .toc file if it has the same name as the current folder. + // This is because an Addon can have multiple toc files, for development purpose. + let parent_path = entry.path().parent().expect("Expected to have parent."); + let parent_path_folder_name = parent_path.file_name().expect("Expected folder to have name."); + let toc_file_stem = entry.path().file_stem().expect("Expected .toc file to have name."); + if parent_path_folder_name == toc_file_stem { + let addon = parse_toc_entry(entry).await; + if let Some(addon) = addon { + addons.push(addon) + } } } } From 68a96388c1be13ce813690c30734b3b4e5f19536 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Thu, 20 Aug 2020 11:41:10 +0200 Subject: [PATCH 04/20] feat: parse single addon after download --- src/addon.rs | 11 +++++++++++ src/gui/mod.rs | 3 ++- src/gui/update.rs | 25 ++++++++++++++++++++----- src/toc.rs | 9 +++++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/addon.rs b/src/addon.rs index 38dc12c2..d27c4644 100644 --- a/src/addon.rs +++ b/src/addon.rs @@ -201,6 +201,17 @@ impl Addon { dependencies } + /// Takes a `Addon` and updates self. + /// Used when we reparse a single `Addon`. + pub fn update_addon(&mut self, other: &Addon) { + self.title = other.title.clone(); + self.version = other.version.clone(); + self.dependencies = other.dependencies.clone(); + self.wowi_id = other.wowi_id.clone(); + self.tukui_id = other.tukui_id.clone(); + self.curse_id = other.curse_id.clone(); + } + /// Check if the `Addon` is updatable. /// We strip both version for non digits, and then /// checks if `remote_version` is a sub_slice of `local_version`. diff --git a/src/gui/mod.rs b/src/gui/mod.rs index bbd2e31d..870a4b2b 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -29,7 +29,8 @@ pub enum Interaction { #[derive(Debug)] pub enum Message { Parse(Config), - PatchAddons(Result>), + ParsedAddons(Result>), + PartialParsedAddons(Result>), DownloadedAddon((String, Result<()>)), UnpackedAddon((String, Result<()>)), CursePackage((String, Result)), diff --git a/src/gui/update.rs b/src/gui/update.rs index cc7c5f3c..5345608b 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -26,7 +26,7 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { return Ok(Command::perform( read_addon_directory(dir), - Message::PatchAddons, + Message::ParsedAddons, )) } None => { @@ -61,7 +61,7 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { @@ -98,7 +98,19 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { + Message::PartialParsedAddons(Ok(addons)) => { + if let Some(updated_addon) = addons.first() { + let addon = ajour + .addons + .iter_mut() + .find(|a| a.id == updated_addon.id) + .expect("Expected addon for id to exist."); + + // Update the addon with the newly parsed information. + addon.update_addon(updated_addon); + } + } + Message::ParsedAddons(Ok(addons)) => { ajour.addons = addons; ajour.addons.sort(); @@ -235,7 +247,8 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { addon.state = AddonState::Ajour(Some("Completed".to_owned())); - addon.version = addon.remote_version.clone(); + // Re-parse the single addon. + return Ok(Command::perform(read_addon_directory(addon.path.clone()), Message::PartialParsedAddons)) } Err(err) => { ajour.state = AjourState::Error(err); @@ -243,7 +256,9 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { + Message::Error(error) + | Message::ParsedAddons(Err(error)) + | Message::PartialParsedAddons(Err(error)) => { ajour.state = AjourState::Error(error); } } diff --git a/src/toc.rs b/src/toc.rs index aef51392..cee9bf2b 100644 --- a/src/toc.rs +++ b/src/toc.rs @@ -33,8 +33,13 @@ pub async fn read_addon_directory>(path: P) -> Result> // We only look at the .toc file if it has the same name as the current folder. // This is because an Addon can have multiple toc files, for development purpose. let parent_path = entry.path().parent().expect("Expected to have parent."); - let parent_path_folder_name = parent_path.file_name().expect("Expected folder to have name."); - let toc_file_stem = entry.path().file_stem().expect("Expected .toc file to have name."); + let parent_path_folder_name = parent_path + .file_name() + .expect("Expected folder to have name."); + let toc_file_stem = entry + .path() + .file_stem() + .expect("Expected .toc file to have name."); if parent_path_folder_name == toc_file_stem { let addon = parse_toc_entry(entry).await; if let Some(addon) = addon { From 136dfa16d9524905c5b838b57e5076a707697c8f Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Thu, 20 Aug 2020 11:45:57 +0200 Subject: [PATCH 05/20] chore: cargo fmt --- src/gui/update.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gui/update.rs b/src/gui/update.rs index 5345608b..e2adc9bd 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -248,7 +248,10 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { addon.state = AddonState::Ajour(Some("Completed".to_owned())); // Re-parse the single addon. - return Ok(Command::perform(read_addon_directory(addon.path.clone()), Message::PartialParsedAddons)) + return Ok(Command::perform( + read_addon_directory(addon.path.clone()), + Message::PartialParsedAddons, + )); } Err(err) => { ajour.state = AjourState::Error(err); From f6644442ef2412c9fa08407e7051f5267c531cca Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Thu, 20 Aug 2020 14:22:58 +0200 Subject: [PATCH 06/20] chore: clippy --- src/addon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon.rs b/src/addon.rs index d27c4644..05831071 100644 --- a/src/addon.rs +++ b/src/addon.rs @@ -209,7 +209,7 @@ impl Addon { self.dependencies = other.dependencies.clone(); self.wowi_id = other.wowi_id.clone(); self.tukui_id = other.tukui_id.clone(); - self.curse_id = other.curse_id.clone(); + self.curse_id = other.curse_id; } /// Check if the `Addon` is updatable. From 22d8ed7de43f5c9853571268fd5b4405513cdfe3 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Thu, 20 Aug 2020 15:16:46 +0200 Subject: [PATCH 07/20] chore: refactor --- src/gui/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 870a4b2b..a30c162c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -176,15 +176,10 @@ impl Application for Ajour { let mut addons_scrollable = Scrollable::new(&mut self.addons_scrollable_state).spacing(1); // Loops addons for GUI. - for addon in &mut self.addons { + for addon in &mut self.addons.iter_mut().filter(|a| a.is_parent()) { // Default element height let default_height = Length::Units(35); - // We filter away addons which isn't parent. - if !addon.is_parent() { - continue; - } - let title = addon.title.clone(); let version = addon.version.clone().unwrap_or_else(|| String::from("-")); let remote_version = addon From 5ff66503cacbc6cd2f203fd45a41ccd25499a55a Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm <2248455+casperstorm@users.noreply.github.com> Date: Thu, 20 Aug 2020 20:19:00 +0200 Subject: [PATCH 08/20] chore: fixed warning on windows --- src/config/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/mod.rs b/src/config/mod.rs index 8e344f31..a9229a85 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,5 @@ use serde_derive::Deserialize; +#[cfg(not(windows))] use std::env; use std::fs; use std::path::PathBuf; From 6fc1681a9fd89daaaa33b05dd8a7f656b9a4416a Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm <2248455+casperstorm@users.noreply.github.com> Date: Thu, 20 Aug 2020 20:19:18 +0200 Subject: [PATCH 09/20] chore: show loaded addons count --- src/gui/mod.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index a30c162c..4405938d 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -119,11 +119,23 @@ impl Application for Ajour { let refresh_button: Element = refresh_button.into(); // Displays text depending on the state of the app. + let parent_addons_count = self.addons.clone().iter().filter(|a| a.is_parent()).count(); let status_text = match &self.state { - AjourState::Idle => Text::new(env!("CARGO_PKG_VERSION")).size(default_font_size), + AjourState::Idle => { + Text::new(format!("{} addons loaded", parent_addons_count)).size(default_font_size) + } AjourState::Error(e) => Text::new(e.to_string()).size(default_font_size), }; let status_container = Container::new(status_text) + .center_y() + .padding(5) + .width(Length::FillPortion(1)) + .style(style::StatusTextContainer); + + let version_text = Text::new(env!("CARGO_PKG_VERSION")) + .size(default_font_size) + .horizontal_alignment(HorizontalAlignment::Right); + let version_container = Container::new(version_text) .center_y() .padding(5) .style(style::StatusTextContainer); @@ -134,7 +146,8 @@ impl Application for Ajour { .push(update_all_button.map(Message::Interaction)) .push(spacer) .push(refresh_button.map(Message::Interaction)) - .push(status_container); + .push(status_container) + .push(version_container); // A row containing titles above the addon rows. let mut row_titles = Row::new().spacing(1).height(Length::Units(20)); From 777f628c8544f34c0ab11c9e2311cc74ae5aafd9 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm <2248455+casperstorm@users.noreply.github.com> Date: Thu, 20 Aug 2020 22:11:58 +0200 Subject: [PATCH 10/20] feat: partial widows icon --- Cargo.lock | 42 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 ++++- build.rs | 6 ++++++ resources/windows/res.rc | 3 +++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 build.rs create mode 100644 resources/windows/res.rc diff --git a/Cargo.lock b/Cargo.lock index 3aee522f..6c9d72bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,10 +17,11 @@ dependencies = [ [[package]] name = "ajour" -version = "0.1.0-beta2" +version = "0.1.0-beta3" dependencies = [ "async-std", "dirs 3.0.1", + "embed-resource", "iced", "isahc", "percent-encoding", @@ -640,6 +641,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "embed-resource" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6b0b4403da80c2fd32333937dd468292c001d778c587ae759b75432772715d" +dependencies = [ + "vswhom", + "winreg", +] + [[package]] name = "encoding_rs" version = "0.8.23" @@ -2505,6 +2516,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f5402d3d0e79a069714f7b48e3ecc60be7775a2c049cb839457457a239532" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "waker-fn" version = "1.0.0" @@ -2822,6 +2853,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "wio" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 9fdd6062..21675d40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ajour" description = "A World of Warcraft addon manager" -version = "0.1.0-beta2" +version = "0.1.0-beta3" authors = ["Casper Rogild Storm"] license = "MIT" homepage = "https://github.com/casperstorm/ajour" @@ -20,3 +20,6 @@ serde_json = "1.0.57" isahc = { version = "0.9.6", features = ["json"] } zip = "0.5.6" percent-encoding = "2.1.0" + +[build-dependencies] +embed-resource = "1.3.3" diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..545dda43 --- /dev/null +++ b/build.rs @@ -0,0 +1,6 @@ +extern crate embed_resource; + +fn main() { + #[cfg(windows)] + embed_resource::compile("resources/windows/res.rc"); +} diff --git a/resources/windows/res.rc b/resources/windows/res.rc new file mode 100644 index 00000000..184cc425 --- /dev/null +++ b/resources/windows/res.rc @@ -0,0 +1,3 @@ +#define IDI_ICON 0x101 + +IDI_ICON ICON "ajour.ico" \ No newline at end of file From 01c8306392ec53809d1ba1d6c21ffcb57baea58e Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 14:08:12 +0200 Subject: [PATCH 11/20] feat: windows config location can now be next to exe --- README.md | 4 +++- src/config/mod.rs | 20 ++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 63409cab..a83530a8 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,11 @@ Ajour doesn't create the config file for you, but it looks for one in the follow ## Windows -On Windows, the config file should be located at: +On Windows, it looks for a config file in the following locations: - `%APPDATA%\ajour\ajour.yml` +- `In the same directory as the executable` + # Screenshots diff --git a/src/config/mod.rs b/src/config/mod.rs index a9229a85..b3d32a6a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -90,11 +90,27 @@ fn installed_config() -> Option { /// according to the following order: /// /// 1. %APPDATA%\ajour\ajour.yml +/// 2. In the same directory as the executable #[cfg(windows)] fn installed_config() -> Option { - dirs::config_dir() + let fallback = dirs::config_dir() .map(|path| path.join("ajour\\ajour.yml")) - .filter(|new| new.exists()) + .filter(|new| new.exists()); + if let Some(fallback) = fallback { + return Some(fallback); + } + + let fallback = std::env::current_exe(); + if let Ok(fallback) = fallback.as_ref().map(|p| p.parent()) { + if let Some(fallback) = fallback { + let fallback = fallback.join("ajour.yml"); + if fallback.exists() { + return Some(fallback); + } + } + } + + None } /// Returns the config after the content of the file From fac892d6eab1670b9a49f0ea01702fad8a4ff6e5 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 14:13:12 +0200 Subject: [PATCH 12/20] chore: optimization --- src/config/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index b3d32a6a..91110dc3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -102,8 +102,7 @@ fn installed_config() -> Option { let fallback = std::env::current_exe(); if let Ok(fallback) = fallback.as_ref().map(|p| p.parent()) { - if let Some(fallback) = fallback { - let fallback = fallback.join("ajour.yml"); + if let Some(fallback) = fallback.map(|f| f.join("ajour.yml")) { if fallback.exists() { return Some(fallback); } From cc6a09933632770f1562f582acc4215e4d06654c Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 14:36:10 +0200 Subject: [PATCH 13/20] chore: removed wix dependency from the project for now --- README.md | 2 +- resources/windows/wix/main.wxs | 83 ---------------------------------- 2 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 resources/windows/wix/main.wxs diff --git a/README.md b/README.md index a83530a8..de05e3b1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ make app make dmg # Windows -cargo wix -I .\resources\windows\wix\main.wxs +cargo build --release ``` # Configuration diff --git a/resources/windows/wix/main.wxs b/resources/windows/wix/main.wxs deleted file mode 100644 index 6c781182..00000000 --- a/resources/windows/wix/main.wxs +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 1 - - - From d843b22789bf70e759d8d723d30f97726c1ccc9c Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 16:02:09 +0200 Subject: [PATCH 14/20] feat: separated scrollbar from scrollview --- src/gui/mod.rs | 47 ++++++++++++++++++++++++++++++++++++++--------- src/gui/style.rs | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 4405938d..4b0500c0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -9,7 +9,7 @@ use crate::{ }; use iced::{ button, scrollable, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Scrollable, Settings, Text, VerticalAlignment, + HorizontalAlignment, Length, Row, Scrollable, Settings, Space, Text, VerticalAlignment, }; #[derive(Debug)] @@ -88,6 +88,7 @@ impl Application for Ajour { fn view(&mut self) -> Element { let default_font_size = 14; + let default_padding = 15; // A row contain general controls. let mut controls = Row::new().spacing(1).height(Length::Units(35)); @@ -140,18 +141,30 @@ impl Application for Ajour { .padding(5) .style(style::StatusTextContainer); - let spacer = Container::new(Text::new("")).width(Length::Units(2)); + let spacer = Space::new(Length::Units(2), Length::Units(0)); + // Not using default padding, just to make it look prettier UI wise + let top_spacer = Space::new(Length::Units(0), Length::Units(5)); + let left_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); + let right_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); controls = controls + .push(left_spacer) .push(update_all_button.map(Message::Interaction)) .push(spacer) .push(refresh_button.map(Message::Interaction)) .push(status_container) - .push(version_container); + .push(version_container) + .push(right_spacer); + + let controls_column = Column::new().push(top_spacer).push(controls); + let controls_container = Container::new(controls_column); // A row containing titles above the addon rows. let mut row_titles = Row::new().spacing(1).height(Length::Units(20)); + let left_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); + let right_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); + let addon_row_text = Text::new("Addon").size(default_font_size); let addon_row_container = Container::new(addon_row_text) .width(Length::FillPortion(1)) @@ -178,15 +191,19 @@ impl Application for Ajour { .style(style::StatusTextContainer); row_titles = row_titles + .push(left_spacer) .push(addon_row_container) .push(local_version_container) .push(remote_version_container) .push(status_row_container) - .push(delete_row_container); + .push(delete_row_container) + .push(right_spacer); // A scrollable list containing rows. // Each row holds information about a single addon. - let mut addons_scrollable = Scrollable::new(&mut self.addons_scrollable_state).spacing(1); + let mut addons_scrollable = Scrollable::new(&mut self.addons_scrollable_state) + .spacing(1) + .style(style::Scrollable); // Loops addons for GUI. for addon in &mut self.addons.iter_mut().filter(|a| a.is_parent()) { @@ -292,33 +309,45 @@ impl Application for Ajour { .width(Length::Units(70)) .center_y() .center_x() - .padding(5) .style(style::AddonDescriptionContainer); + let left_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); + let right_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); + let row = Row::new() + .push(left_spacer) .push(text_container) .push(installed_version_container) .push(remote_version_container) .push(update_button_container) .push(delete_button_container) + .push(right_spacer) .spacing(1); let cell = Container::new(row).width(Length::Fill).style(style::Cell); addons_scrollable = addons_scrollable.push(cell); } + // We add a final cell to act as "bottom padding" + let row = Row::new().push(Space::new( + Length::FillPortion(1), + Length::Units(default_padding), + )); + let cell = Container::new(row).width(Length::Fill).style(style::Cell); + addons_scrollable = addons_scrollable.push(cell); + // This column gathers all the other elements together. let content = Column::new() - .push(controls) + .push(controls_container) .push(row_titles) - .push(addons_scrollable); + .push(addons_scrollable) + .padding(3); // small padding to make scrollbar fit better. // This container wraps the whole content. Container::new(content) .width(Length::Fill) .height(Length::Fill) .style(style::Content) - .padding(10) .into() } } diff --git a/src/gui/style.rs b/src/gui/style.rs index 6fb9f680..e7516751 100644 --- a/src/gui/style.rs +++ b/src/gui/style.rs @@ -1,4 +1,4 @@ -use iced::{button, container, Background, Color}; +use iced::{button, container, scrollable, Background, Color}; enum ColorPalette { Primary, @@ -148,3 +148,38 @@ impl container::StyleSheet for Cell { } } } + +pub struct Scrollable; +impl scrollable::StyleSheet for Scrollable { + fn active(&self) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Some(Background::Color(ColorPalette::Background.rgb())), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: ColorPalette::Surface.rgb(), + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> scrollable::Scrollbar { + let active = self.active(); + + scrollable::Scrollbar { + scroller: scrollable::Scroller { ..active.scroller }, + ..active + } + } + + fn dragging(&self) -> scrollable::Scrollbar { + let hovered = self.hovered(); + scrollable::Scrollbar { + scroller: scrollable::Scroller { ..hovered.scroller }, + ..hovered + } + } +} From 8e0fbf0a523a67f2a6f88c1e4b790fe493197a54 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 21:04:40 +0200 Subject: [PATCH 15/20] chore: guard against empty dependencies --- src/toc.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/toc.rs b/src/toc.rs index cee9bf2b..93c29b94 100644 --- a/src/toc.rs +++ b/src/toc.rs @@ -178,6 +178,10 @@ async fn parse_toc_entry(toc_entry: DirEntry) -> Option { /// Helper function to split a comma separated string into `Vec`. fn split_dependencies_into_vec(value: &str) -> Vec { + if value == "" { + return vec![]; + } + value .split([','].as_ref()) .map(|s| s.trim().to_string()) From 88ea9f8c3f956c8d299feaea215b9c367e11b2a2 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 21:36:57 +0200 Subject: [PATCH 16/20] chore: gui enhancements --- src/gui/mod.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 4b0500c0..4896f5f9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -88,7 +88,7 @@ impl Application for Ajour { fn view(&mut self) -> Element { let default_font_size = 14; - let default_padding = 15; + let default_padding = 10; // A row contain general controls. let mut controls = Row::new().spacing(1).height(Length::Units(35)); @@ -141,7 +141,7 @@ impl Application for Ajour { .padding(5) .style(style::StatusTextContainer); - let spacer = Space::new(Length::Units(2), Length::Units(0)); + let spacer = Space::new(Length::Units(7), Length::Units(0)); // Not using default padding, just to make it look prettier UI wise let top_spacer = Space::new(Length::Units(0), Length::Units(5)); let left_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); @@ -203,6 +203,7 @@ impl Application for Ajour { // Each row holds information about a single addon. let mut addons_scrollable = Scrollable::new(&mut self.addons_scrollable_state) .spacing(1) + .height(Length::FillPortion(1)) .style(style::Scrollable); // Loops addons for GUI. @@ -312,7 +313,7 @@ impl Application for Ajour { .style(style::AddonDescriptionContainer); let left_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); - let right_spacer = Space::new(Length::Units(default_padding), Length::Units(0)); + let right_spacer = Space::new(Length::Units(default_padding + 5), Length::Units(0)); let row = Row::new() .push(left_spacer) @@ -328,19 +329,13 @@ impl Application for Ajour { addons_scrollable = addons_scrollable.push(cell); } - // We add a final cell to act as "bottom padding" - let row = Row::new().push(Space::new( - Length::FillPortion(1), - Length::Units(default_padding), - )); - let cell = Container::new(row).width(Length::Fill).style(style::Cell); - addons_scrollable = addons_scrollable.push(cell); - + let bottom_space = Space::new(Length::FillPortion(1), Length::Units(default_padding)); // This column gathers all the other elements together. let content = Column::new() .push(controls_container) .push(row_titles) .push(addons_scrollable) + .push(bottom_space) .padding(3); // small padding to make scrollbar fit better. // This container wraps the whole content. From 29b4028e6abe4174ae114476b41cc6d37fb63bc2 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm <2248455+casperstorm@users.noreply.github.com> Date: Fri, 21 Aug 2020 21:42:47 +0200 Subject: [PATCH 17/20] feat: new gh action --- .github/workflows/commit_to_pr_body.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/commit_to_pr_body.yml diff --git a/.github/workflows/commit_to_pr_body.yml b/.github/workflows/commit_to_pr_body.yml new file mode 100644 index 00000000..5a8a38bb --- /dev/null +++ b/.github/workflows/commit_to_pr_body.yml @@ -0,0 +1,14 @@ +on: + pull_request: + types: [opened, synchronize] + +name: Pull Request updated + +jobs: + history: + name: Pull Request Body + runs-on: ubuntu-latest + if: startsWith(github.event.pull_request.head.ref, 'release/') + steps: + - name: Pull Request Body + uses: technote-space/pr-commit-body-action@v1 From 4e3d32cd6779c9399a93d2576983fc3410f592d2 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm <2248455+casperstorm@users.noreply.github.com> Date: Fri, 21 Aug 2020 21:47:36 +0200 Subject: [PATCH 18/20] feat: added pull_request_template.md --- .github/workflows/pull_request_template.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/workflows/pull_request_template.md diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md new file mode 100644 index 00000000..0902aba6 --- /dev/null +++ b/.github/workflows/pull_request_template.md @@ -0,0 +1,4 @@ +# Description + + + From 092efbc4324df3a8dd97373a7b727d43731dd443 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 22:53:12 +0200 Subject: [PATCH 19/20] fix: guard against race condition in refreshing addon list --- src/gui/update.rs | 150 ++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 86 deletions(-) diff --git a/src/gui/update.rs b/src/gui/update.rs index e2adc9bd..c5ac34a7 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -84,30 +84,24 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result>::new(); for addon in &mut ajour.addons { if addon.state == AddonState::Updatable { - let to_directory = ajour - .config - .get_temporary_addon_directory() - .expect("Expected a valid path"); - addon.state = AddonState::Downloading; - let addon = addon.clone(); - commands.push(Command::perform( - perform_download_addon(addon, to_directory), - Message::DownloadedAddon, - )) + if let Some(to_directory) = ajour.config.get_temporary_addon_directory() { + addon.state = AddonState::Downloading; + let addon = addon.clone(); + commands.push(Command::perform( + perform_download_addon(addon, to_directory), + Message::DownloadedAddon, + )) + } } } return Ok(Command::batch(commands)); } Message::PartialParsedAddons(Ok(addons)) => { if let Some(updated_addon) = addons.first() { - let addon = ajour - .addons - .iter_mut() - .find(|a| a.id == updated_addon.id) - .expect("Expected addon for id to exist."); - - // Update the addon with the newly parsed information. - addon.update_addon(updated_addon); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == updated_addon.id) { + // Update the addon with the newly parsed information. + addon.update_addon(updated_addon); + } } } Message::ParsedAddons(Ok(addons)) => { @@ -116,33 +110,30 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result>::new(); let addons = ajour.addons.clone(); - for addon in addons { - // TODO: filter this instead of this if. - if addon.is_parent() { - if let (Some(_), Some(token)) = - (&addon.wowi_id, &ajour.config.tokens.wowinterface) - { - commands.push(Command::perform( - fetch_wowinterface_packages(addon, token.to_string()), - Message::WowinterfacePackages, - )) - } else if addon.tukui_id.is_some() { - commands.push(Command::perform( - fetch_tukui_package(addon), - Message::TukuiPackage, - )) - } else if addon.curse_id.is_some() { - commands.push(Command::perform( - fetch_curse_package(addon), - Message::CursePackage, - )) - } else { - let retries = 4; - commands.push(Command::perform( - fetch_curse_packages(addon, retries), - Message::CursePackages, - )) - } + for addon in addons.iter().filter(|a| a.is_parent()) { + let addon = addon.to_owned(); + if let (Some(_), Some(token)) = (&addon.wowi_id, &ajour.config.tokens.wowinterface) + { + commands.push(Command::perform( + fetch_wowinterface_packages(addon, token.to_string()), + Message::WowinterfacePackages, + )) + } else if addon.tukui_id.is_some() { + commands.push(Command::perform( + fetch_tukui_package(addon), + Message::TukuiPackage, + )) + } else if addon.curse_id.is_some() { + commands.push(Command::perform( + fetch_curse_package(addon), + Message::CursePackage, + )) + } else { + let retries = 4; + commands.push(Command::perform( + fetch_curse_packages(addon, retries), + Message::CursePackages, + )) } } @@ -150,59 +141,46 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { if let Ok(package) = result { - let addon = ajour - .addons - .iter_mut() - .find(|a| a.id == id) - .expect("Expected addon for id to exist."); - addon.apply_curse_package(&package, &ajour.config.wow.flavor); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == id) { + addon.apply_curse_package(&package, &ajour.config.wow.flavor); + } } } Message::CursePackages((id, retries, result)) => { - let addon = ajour - .addons - .iter_mut() - .find(|a| a.id == id) - .expect("Expected addon for id to exist."); - - if let Ok(packages) = result { - addon.apply_curse_packages(&packages, &ajour.config.wow.flavor); - } else { - // FIXME: This could be improved quite a lot. - // Idea is that Curse API returns `NetworkError(CouldntResolveHost)` quite often, - // if called to quickly. So i've implemented a very basic retry functionallity - // which solves the problem for now. - let error = result.err().unwrap(); - if matches!( - error, - ClientError::NetworkError(isahc::Error::CouldntResolveHost) - ) && retries > 0 - { - return Ok(Command::perform( - fetch_curse_packages(addon.clone(), retries), - Message::CursePackages, - )); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == id) { + if let Ok(packages) = result { + addon.apply_curse_packages(&packages, &ajour.config.wow.flavor); + } else { + // FIXME: This could be improved quite a lot. + // Idea is that Curse API returns `NetworkError(CouldntResolveHost)` quite often, + // if called to quickly. So i've implemented a very basic retry functionallity + // which solves the problem for now. + let error = result.err().unwrap(); + if matches!( + error, + ClientError::NetworkError(isahc::Error::CouldntResolveHost) + ) && retries > 0 + { + return Ok(Command::perform( + fetch_curse_packages(addon.clone(), retries), + Message::CursePackages, + )); + } } } } Message::TukuiPackage((id, result)) => { if let Ok(package) = result { - let addon = ajour - .addons - .iter_mut() - .find(|a| a.id == id) - .expect("Expected addon for id to exist."); - addon.apply_tukui_package(&package); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == id) { + addon.apply_tukui_package(&package); + } } } Message::WowinterfacePackages((id, result)) => { if let Ok(packages) = result { - let addon = ajour - .addons - .iter_mut() - .find(|a| a.id == id) - .expect("Expected addon for id to exist."); - addon.apply_wowi_packages(&packages); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == id) { + addon.apply_wowi_packages(&packages); + } } } Message::DownloadedAddon((id, result)) => { From 29b4b0c5aa60495a9c68847d66a3fb149d2b7ea0 Mon Sep 17 00:00:00 2001 From: Casper Storm Date: Fri, 21 Aug 2020 23:00:13 +0200 Subject: [PATCH 20/20] fix: guard against race condition when downloading and unpacking --- src/gui/update.rs | 62 +++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/gui/update.rs b/src/gui/update.rs index c5ac34a7..a71dbd0b 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -195,45 +195,39 @@ pub fn handle_message(ajour: &mut Ajour, message: Message) -> Result { - if addon.state == AddonState::Downloading { - addon.state = AddonState::Unpacking; - let addon = addon.clone(); - return Ok(Command::perform( - perform_unpack_addon(addon, from_directory, to_directory), - Message::UnpackedAddon, - )); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == id) { + match result { + Ok(_) => { + if addon.state == AddonState::Downloading { + addon.state = AddonState::Unpacking; + let addon = addon.clone(); + return Ok(Command::perform( + perform_unpack_addon(addon, from_directory, to_directory), + Message::UnpackedAddon, + )); + } + } + Err(err) => { + ajour.state = AjourState::Error(err); } - } - Err(err) => { - ajour.state = AjourState::Error(err); } } } Message::UnpackedAddon((id, result)) => { - let addon = ajour - .addons - .iter_mut() - .find(|a| a.id == id) - .expect("Expected addon for id to exist."); - match result { - Ok(_) => { - addon.state = AddonState::Ajour(Some("Completed".to_owned())); - // Re-parse the single addon. - return Ok(Command::perform( - read_addon_directory(addon.path.clone()), - Message::PartialParsedAddons, - )); - } - Err(err) => { - ajour.state = AjourState::Error(err); - addon.state = AddonState::Ajour(Some("Error!".to_owned())); + if let Some(addon) = ajour.addons.iter_mut().find(|a| a.id == id) { + match result { + Ok(_) => { + addon.state = AddonState::Ajour(Some("Completed".to_owned())); + // Re-parse the single addon. + return Ok(Command::perform( + read_addon_directory(addon.path.clone()), + Message::PartialParsedAddons, + )); + } + Err(err) => { + ajour.state = AjourState::Error(err); + addon.state = AddonState::Ajour(Some("Error!".to_owned())); + } } } }