diff --git a/Cargo.lock b/Cargo.lock index a86f1553..b8ea0879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2042,6 +2042,7 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "jpeg-decoder", "num-rational", "num-traits", "png", @@ -2174,6 +2175,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.64" diff --git a/Cargo.toml b/Cargo.toml index 5bfa7a1b..7da7b3ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ egui_commonmark = "0.7.4" egui_dnd = { git = "https://github.com/lucasmerlin/egui_dnd.git", rev = "e9043021e101fb42fc6ce70e508da857cb7ee263" } futures = "0.3.28" hex = "0.4.3" -image = { version = "0.24.7", default-features = false, features = ["png"] } +image = { version = "0.24.7", default-features = false, features = ["png", "jpeg"] } indexmap = { version = "2.0.0", features = ["serde"] } inventory = "0.3.11" lazy_static = "1.4.0" diff --git a/src/gui/message.rs b/src/gui/message.rs index 7ee1b371..542e270d 100644 --- a/src/gui/message.rs +++ b/src/gui/message.rs @@ -47,6 +47,7 @@ pub enum Message { LintMods(LintMods), SelfUpdate(SelfUpdate), FetchSelfUpdateProgress(FetchSelfUpdateProgress), + FetchModDetails(FetchModDetails), } impl Message { @@ -60,6 +61,7 @@ impl Message { Self::LintMods(msg) => msg.receive(app), Self::SelfUpdate(msg) => msg.receive(app), Self::FetchSelfUpdateProgress(msg) => msg.receive(app), + Self::FetchModDetails(msg) => msg.receive(app), } } } @@ -748,3 +750,105 @@ async fn self_update_async( Ok(original_exe_path) } + +#[derive(Debug)] +pub struct FetchModDetails { + rid: RequestID, + modio_id: u32, + result: Result, +} + +#[derive(Debug)] +pub struct ModDetails { + pub r#mod: modio::mods::Mod, + pub versions: Vec, + pub thumbnail: Vec, +} + +impl FetchModDetails { + pub fn send( + rc: &mut RequestCounter, + ctx: &egui::Context, + tx: Sender, + oauth_token: &str, + modio_id: u32, + ) -> MessageHandle<()> { + let rid = rc.next(); + let ctx = ctx.clone(); + let oauth_token = oauth_token.to_string(); + + MessageHandle { + rid, + handle: tokio::task::spawn(async move { + let result = fetch_modio_mod_details(oauth_token, modio_id).await; + tx.send(Message::FetchModDetails(FetchModDetails { + rid, + result, + modio_id, + })) + .await + .unwrap(); + ctx.request_repaint(); + }), + state: (), + } + } + + fn receive(self, app: &mut App) { + let mut to_remove = None; + + if let Some(req) = app.fetch_mod_details_rid.get(&self.modio_id) + && req.rid == self.rid + { + match self.result { + Ok(mod_details) => { + info!("fetch mod details successful"); + app.mod_details.insert(mod_details.r#mod.id, mod_details); + app.last_action_status = + LastActionStatus::Success("fetch mod details complete".to_string()); + } + Err(e) => { + error!("fetch mod details failed"); + error!("{:#?}", e); + to_remove = Some(self.modio_id); + app.last_action_status = + LastActionStatus::Failure("fetch mod details failed".to_string()); + } + } + } + + if let Some(id) = to_remove { + app.fetch_mod_details_rid.remove(&id); + } + } +} + +async fn fetch_modio_mod_details(oauth_token: String, modio_id: u32) -> Result { + use crate::providers::modio::{LoggingMiddleware, MODIO_DRG_ID}; + use modio::{filter::prelude::*, Credentials, Modio}; + + let credentials = Credentials::with_token("", oauth_token); + let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()) + .with::(Default::default()) + .build(); + let modio = Modio::new(credentials, client.clone())?; + let mod_ref = modio.mod_(MODIO_DRG_ID, modio_id); + let r#mod = mod_ref.clone().get().await?; + + let filter = with_limit(10).order_by(modio::user::filters::files::Version::desc()); + let versions = mod_ref.clone().files().search(filter).first_page().await?; + + let thumbnail = client + .get(r#mod.logo.thumb_320x180.clone()) + .send() + .await? + .bytes() + .await? + .to_vec(); + + Ok(ModDetails { + r#mod, + versions, + thumbnail, + }) +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index afc617f9..b2e6f9b0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -15,7 +15,7 @@ use std::{ }; use anyhow::{anyhow, Context, Result}; -use eframe::egui::{Button, CollapsingHeader, RichText}; +use eframe::egui::{Button, CollapsingHeader, Label, RichText}; use eframe::epaint::{Pos2, Vec2}; use eframe::{ egui::{self, FontSelection, Layout, TextFormat, Ui}, @@ -43,6 +43,7 @@ use find_string::FindString; use message::MessageHandle; use request_counter::{RequestCounter, RequestID}; +use self::message::ModDetails; use self::toggle_switch::toggle_switch; pub fn gui(args: Option>) -> Result<()> { @@ -98,10 +99,14 @@ pub struct App { lint_report: Option, lints_toggle_window: Option, lint_options: LintOptions, - cache: CommonMarkCache, + update_cmark_cache: CommonMarkCache, needs_restart: bool, self_update_rid: Option>, original_exe_path: Option, + detailed_mod_info_windows: HashMap, + mod_details: HashMap, + fetch_mod_details_rid: HashMap>, + mod_details_thumbnail_texture_handle: HashMap, } #[derive(Default)] @@ -157,10 +162,14 @@ impl App { lint_report: None, lints_toggle_window: None, lint_options: LintOptions::default(), - cache: Default::default(), + update_cmark_cache: Default::default(), needs_restart: false, self_update_rid: None, original_exe_path: None, + detailed_mod_info_windows: HashMap::default(), + mod_details: HashMap::default(), + fetch_mod_details_rid: HashMap::default(), + mod_details_thumbnail_texture_handle: HashMap::default(), }) } @@ -434,6 +443,24 @@ impl App { ui.output_mut(|o| o.copied_text = mc.spec.url.to_owned()); } + if let Some(modio_id) = info.modio_id + && let Some(modio_provider_params) = self.state.config.provider_parameters.get("modio") + && let Some(oauth_token) = modio_provider_params.get("oauth") + && ui + .button("ℹ") + .on_hover_text_at_pointer("View details") + .clicked() + { + self.detailed_mod_info_windows.insert(modio_id, WindowDetailedModInfo { info: info.clone() }); + self.fetch_mod_details_rid.insert(modio_id, message::FetchModDetails::send( + &mut self.request_counter, + ui.ctx(), + self.tx.clone(), + oauth_token, + modio_id + )); + } + if mc.enabled { let is_duplicate = enabled_specs.iter().any(|(i, spec)| { Some(state.index) != *i && info.spec.satisfies_dependency(spec) @@ -730,7 +757,7 @@ impl App { .show(ctx, |ui| { CommonMarkViewer::new("available-update") .max_image_width(Some(512)) - .show(ui, &mut self.cache, &update.body); + .show(ui, &mut self.update_cmark_cache, &update.body); ui.with_layout(egui::Layout::right_to_left(Align::TOP), |ui| { if ui .add(egui::Button::new("Install update")) @@ -1400,6 +1427,125 @@ impl App { } } } + + fn show_detailed_mod_info(&mut self, ctx: &egui::Context, modio_id: u32) { + let mut to_remove = Vec::new(); + + if let Some(WindowDetailedModInfo { info }) = self.detailed_mod_info_windows.get(&modio_id) + { + let mut open = true; + + egui::Window::new(&info.name) + .open(&mut open) + .collapsible(false) + .movable(true) + .resizable(true) + .show(ctx, |ui| self.show_detailed_mod_info_inner(ui, modio_id)); + + if !open { + to_remove.push(modio_id); + } + } + + for id in to_remove { + self.detailed_mod_info_windows.remove(&id); + self.mod_details.remove(&id); + self.fetch_mod_details_rid.remove(&id); + self.mod_details_thumbnail_texture_handle.remove(&id); + } + } + + fn show_detailed_mod_info_inner(&mut self, ui: &mut egui::Ui, modio_id: u32) { + if let Some(mod_details) = &self.mod_details.get(&modio_id) { + let scroll_area_height = (ui.available_height() - 60.0).clamp(0.0, f32::INFINITY); + + egui::ScrollArea::vertical() + .max_height(scroll_area_height) + .max_width(f32::INFINITY) + .auto_shrink([false, false]) + .stick_to_right(true) + .show(ui, |ui| { + let texture: &egui::TextureHandle = self + .mod_details_thumbnail_texture_handle + .entry(modio_id) + .or_insert_with(|| { + ui.ctx().load_texture( + format!("{} image", mod_details.r#mod.name), + { + let image = + image::load_from_memory(&mod_details.thumbnail).unwrap(); + let size = [image.width() as _, image.height() as _]; + let image_buffer = image.to_rgb8(); + let pixels = image_buffer.as_flat_samples(); + egui::ColorImage::from_rgb(size, pixels.as_slice()) + }, + Default::default(), + ) + }); + ui.vertical_centered(|ui| { + ui.image(texture, texture.size_vec2()); + }); + + ui.heading("Uploader"); + ui.label(&mod_details.r#mod.submitted_by.username); + ui.add_space(10.0); + + ui.heading("Description"); + if let Some(desc) = &mod_details.r#mod.description_plaintext { + ui.label(desc); + } else { + ui.label("No description provided."); + } + ui.add_space(10.0); + + ui.heading("Versions and changelog"); + ui.label( + RichText::new("Only the 10 most recent versions are shown.") + .color(Color32::GRAY) + .italics(), + ); + egui::Grid::new("mod-details-available-versions") + .spacing(Vec2::new(3.0, 10.0)) + .striped(true) + .num_columns(2) + .show(ui, |ui| { + mod_details.versions.iter().for_each(|file| { + if let Some(version) = &file.version { + ui.label(version); + } else { + ui.label("Unknown version"); + } + if let Some(changelog) = &file.changelog { + ui.add(Label::new(changelog).wrap(true)); + } else { + ui.label("N/A"); + } + ui.end_row(); + }); + }); + ui.add_space(10.0); + + ui.heading("Files"); + if let Some(file) = &mod_details.r#mod.modfile { + ui.horizontal(|ui| { + if let Some(version) = &file.version { + ui.label(version); + } else { + ui.label("Unknown version"); + } + ui.hyperlink(&file.download.binary_url); + }); + } else { + ui.label("No files provided."); + } + }); + } else { + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Fetching mod details from mod.io..."); + }); + } + } } struct WindowProviderParameters { @@ -1456,6 +1602,10 @@ struct WindowLintsToggle { mods: Vec, } +struct WindowDetailedModInfo { + info: ModInfo, +} + impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { if self.needs_restart @@ -1492,6 +1642,16 @@ impl eframe::App for App { self.show_lints_toggle(ctx); self.show_lint_report(ctx); + let modio_ids = self + .detailed_mod_info_windows + .keys() + .copied() + .collect::>(); + + for modio_id in modio_ids { + self.show_detailed_mod_info(ctx, modio_id); + } + egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { ui.with_layout(egui::Layout::right_to_left(Align::TOP), |ui| { ui.add_enabled_ui( @@ -1499,6 +1659,8 @@ impl eframe::App for App { && self.update_rid.is_none() && self.lint_rid.is_none() && self.self_update_rid.is_none() + && self.detailed_mod_info_windows.is_empty() + && self.fetch_mod_details_rid.is_empty() && self.state.config.drg_pak_path.is_some(), |ui| { if let Some(args) = &self.args { diff --git a/src/providers/modio.rs b/src/providers/modio.rs index 5ae3b5fe..61ebeb92 100644 --- a/src/providers/modio.rs +++ b/src/providers/modio.rs @@ -20,7 +20,7 @@ lazy_static::lazy_static! { static ref RE_MOD: regex::Regex = regex::Regex::new("^https://mod.io/g/drg/m/(?P[^/#]+)(:?#(?P\\d+)(:?/(?P\\d+))?)?$").unwrap(); } -const MODIO_DRG_ID: u32 = 2475; +pub(crate) const MODIO_DRG_ID: u32 = 2475; const MODIO_PROVIDER_ID: &str = "modio"; inventory::submit! { @@ -177,8 +177,8 @@ impl ModioFile { } #[derive(Default)] -struct LoggingMiddleware { - requests: std::sync::Arc, +pub(crate) struct LoggingMiddleware { + pub(crate) requests: std::sync::Arc, } #[async_trait::async_trait]