From 96d9726174296f81cffbe7ad078c68487c78cdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=9D=B0=E5=8F=8B=20Jieyou=20Xu=20=28Joe=29?= Date: Mon, 21 Aug 2023 10:29:52 +0800 Subject: [PATCH] Add a basic mod details view --- Cargo.lock | 31 +++++--- Cargo.toml | 2 +- src/gui/message.rs | 93 +++++++++++++++++++++++ src/gui/mod.rs | 166 ++++++++++++++++++++++++++++++++++++++++- src/providers/modio.rs | 6 +- 5 files changed, 278 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a16f0f70..abd87c51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,14 +307,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.5.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232344f51ca91edec473432f677c0f5ddf8deaa72f165d253ee19fb196f7d6f2" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand 2.0.0", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -665,9 +665,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", @@ -1275,7 +1275,7 @@ dependencies = [ [[package]] name = "egui_animation" version = "0.1.0" -source = "git+https://github.com/lucasmerlin/egui_dnd.git#e18402b8ff990bd5938bed2a5e57a3a98774259a" +source = "git+https://github.com/lucasmerlin/egui_dnd.git#5fa7342d8d781ede57344ee1086a388e005759ee" dependencies = [ "egui", "hello_egui_utils", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "egui_dnd" version = "0.5.0" -source = "git+https://github.com/lucasmerlin/egui_dnd.git#e18402b8ff990bd5938bed2a5e57a3a98774259a" +source = "git+https://github.com/lucasmerlin/egui_dnd.git#5fa7342d8d781ede57344ee1086a388e005759ee" dependencies = [ "egui", "egui_animation", @@ -1877,7 +1877,7 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hello_egui_utils" version = "0.1.0" -source = "git+https://github.com/lucasmerlin/egui_dnd.git#e18402b8ff990bd5938bed2a5e57a3a98774259a" +source = "git+https://github.com/lucasmerlin/egui_dnd.git#5fa7342d8d781ede57344ee1086a388e005759ee" dependencies = [ "egui", ] @@ -2062,6 +2062,7 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "jpeg-decoder", "num-rational", "num-traits", "png", @@ -2194,6 +2195,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" @@ -3614,18 +3621,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2497539e..2e3ca7b1 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" } 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 abc6e6a9..a5e6954b 100644 --- a/src/gui/message.rs +++ b/src/gui/message.rs @@ -46,6 +46,7 @@ pub enum Message { LintMods(LintMods), SelfUpdate(SelfUpdate), FetchSelfUpdateProgress(FetchSelfUpdateProgress), + FetchModDetails(FetchModDetails), } impl Message { @@ -59,6 +60,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), } } } @@ -712,3 +714,94 @@ async fn self_update_async( Ok(original_exe_path) } + +#[derive(Debug)] +pub struct FetchModDetails { + rid: RequestID, + 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 })) + .await + .unwrap(); + ctx.request_repaint(); + }), + state: (), + } + } + + fn receive(self, app: &mut App) { + if Some(self.rid) == app.fetch_mod_details_rid.as_ref().map(|r| r.rid) { + match self.result { + Ok(mod_details) => { + info!("fetch mod details successful"); + app.mod_details = Some(mod_details); + app.last_action_status = + LastActionStatus::Success("fetch mod details complete".to_string()); + } + Err(e) => { + error!("fetch mod details failed"); + error!("{:#?}", e); + app.mod_details = None; + app.fetch_mod_details_rid = None; + app.last_action_status = + LastActionStatus::Failure("fetch mod details failed".to_string()); + } + } + app.integrate_rid = None; + } + } +} + +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 b708c757..0d93be58 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::{CollapsingHeader, RichText}; +use eframe::egui::{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<()> { @@ -88,10 +89,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_window: Option, + mod_details: Option, + fetch_mod_details_rid: Option>, + mod_details_thumbnail_texture_handle: Option, } #[derive(Default)] @@ -147,10 +152,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_window: None, + mod_details: None, + fetch_mod_details_rid: None, + mod_details_thumbnail_texture_handle: None, }) } @@ -424,6 +433,25 @@ 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_window = + Some(WindowDetailedModInfo { info: info.clone() }); + self.fetch_mod_details_rid = Some(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) @@ -712,7 +740,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")) @@ -1382,6 +1410,129 @@ impl App { } } } + + fn show_detailed_mod_info(&mut self, ctx: &egui::Context) { + if let Some(WindowDetailedModInfo { info }) = &self.detailed_mod_info_window { + egui::Area::new("detailed-mod-info-overlay") + .movable(false) + .fixed_pos(Pos2::ZERO) + .order(egui::Order::Background) + .show(ctx, |ui| { + egui::Frame::none() + .fill(Color32::from_rgba_unmultiplied(0, 0, 0, 127)) + .show(ui, |ui| { + ui.allocate_space(ui.available_size()); + }) + }); + + let mut open = true; + + egui::Window::new(&info.name) + .open(&mut open) + .collapsible(false) + .anchor(Align2::CENTER_TOP, Vec2::new(0.0, 30.0)) + .resizable(false) + .show(ctx, |ui| self.show_detailed_mod_info_inner(ui)); + + if !open { + self.detailed_mod_info_window = None; + self.mod_details = None; + self.fetch_mod_details_rid = None; + self.mod_details_thumbnail_texture_handle = None; + } + } + } + + fn show_detailed_mod_info_inner(&mut self, ui: &mut egui::Ui) { + if let Some(mod_details) = &self.mod_details { + 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 + .get_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 { @@ -1438,6 +1589,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 @@ -1473,6 +1628,7 @@ impl eframe::App for App { self.show_settings(ctx); self.show_lints_toggle(ctx); self.show_lint_report(ctx); + self.show_detailed_mod_info(ctx); egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { ui.with_layout(egui::Layout::right_to_left(Align::TOP), |ui| { @@ -1481,6 +1637,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_window.is_none() + && self.fetch_mod_details_rid.is_none() && 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 db3d087c..823be45f 100644 --- a/src/providers/modio.rs +++ b/src/providers/modio.rs @@ -19,7 +19,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! { @@ -164,8 +164,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]