Skip to content

Commit fc933a7

Browse files
jieyouxutrumank
authored andcommitted
Add a basic mod details view
1 parent 561edcc commit fc933a7

File tree

5 files changed

+266
-8
lines changed

5 files changed

+266
-8
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ egui_commonmark = "0.7.4"
1616
egui_dnd = { git = "https://github.com/lucasmerlin/egui_dnd.git", rev = "e9043021e101fb42fc6ce70e508da857cb7ee263" }
1717
futures = "0.3.28"
1818
hex = "0.4.3"
19-
image = { version = "0.24.7", default-features = false, features = ["png"] }
19+
image = { version = "0.24.7", default-features = false, features = ["png", "jpeg"] }
2020
indexmap = { version = "2.0.0", features = ["serde"] }
2121
inventory = "0.3.11"
2222
lazy_static = "1.4.0"

src/gui/message.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub enum Message {
4747
LintMods(LintMods),
4848
SelfUpdate(SelfUpdate),
4949
FetchSelfUpdateProgress(FetchSelfUpdateProgress),
50+
FetchModDetails(FetchModDetails),
5051
}
5152

5253
impl Message {
@@ -60,6 +61,7 @@ impl Message {
6061
Self::LintMods(msg) => msg.receive(app),
6162
Self::SelfUpdate(msg) => msg.receive(app),
6263
Self::FetchSelfUpdateProgress(msg) => msg.receive(app),
64+
Self::FetchModDetails(msg) => msg.receive(app),
6365
}
6466
}
6567
}
@@ -748,3 +750,94 @@ async fn self_update_async(
748750

749751
Ok(original_exe_path)
750752
}
753+
754+
#[derive(Debug)]
755+
pub struct FetchModDetails {
756+
rid: RequestID,
757+
result: Result<ModDetails>,
758+
}
759+
760+
#[derive(Debug)]
761+
pub struct ModDetails {
762+
pub r#mod: modio::mods::Mod,
763+
pub versions: Vec<modio::files::File>,
764+
pub thumbnail: Vec<u8>,
765+
}
766+
767+
impl FetchModDetails {
768+
pub fn send(
769+
rc: &mut RequestCounter,
770+
ctx: &egui::Context,
771+
tx: Sender<Message>,
772+
oauth_token: &str,
773+
modio_id: u32,
774+
) -> MessageHandle<()> {
775+
let rid = rc.next();
776+
let ctx = ctx.clone();
777+
let oauth_token = oauth_token.to_string();
778+
779+
MessageHandle {
780+
rid,
781+
handle: tokio::task::spawn(async move {
782+
let result = fetch_modio_mod_details(oauth_token, modio_id).await;
783+
tx.send(Message::FetchModDetails(FetchModDetails { rid, result }))
784+
.await
785+
.unwrap();
786+
ctx.request_repaint();
787+
}),
788+
state: (),
789+
}
790+
}
791+
792+
fn receive(self, app: &mut App) {
793+
if Some(self.rid) == app.fetch_mod_details_rid.as_ref().map(|r| r.rid) {
794+
match self.result {
795+
Ok(mod_details) => {
796+
info!("fetch mod details successful");
797+
app.mod_details = Some(mod_details);
798+
app.last_action_status =
799+
LastActionStatus::Success("fetch mod details complete".to_string());
800+
}
801+
Err(e) => {
802+
error!("fetch mod details failed");
803+
error!("{:#?}", e);
804+
app.mod_details = None;
805+
app.fetch_mod_details_rid = None;
806+
app.last_action_status =
807+
LastActionStatus::Failure("fetch mod details failed".to_string());
808+
}
809+
}
810+
app.integrate_rid = None;
811+
}
812+
}
813+
}
814+
815+
async fn fetch_modio_mod_details(oauth_token: String, modio_id: u32) -> Result<ModDetails> {
816+
use crate::providers::modio::{LoggingMiddleware, MODIO_DRG_ID};
817+
use modio::{filter::prelude::*, Credentials, Modio};
818+
819+
let credentials = Credentials::with_token("", oauth_token);
820+
let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
821+
.with::<LoggingMiddleware>(Default::default())
822+
.build();
823+
let modio = Modio::new(credentials, client.clone())?;
824+
let mod_ref = modio.mod_(MODIO_DRG_ID, modio_id);
825+
let r#mod = mod_ref.clone().get().await?;
826+
827+
let filter = with_limit(10).order_by(modio::user::filters::files::Version::desc());
828+
let versions = mod_ref.clone().files().search(filter).first_page().await?;
829+
830+
let thumbnail = client
831+
.get(r#mod.logo.thumb_320x180.clone())
832+
.send()
833+
.await?
834+
.bytes()
835+
.await?
836+
.to_vec();
837+
838+
Ok(ModDetails {
839+
r#mod,
840+
versions,
841+
thumbnail,
842+
})
843+
}

src/gui/mod.rs

Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::{
1515
};
1616

1717
use anyhow::{anyhow, Context, Result};
18-
use eframe::egui::{Button, CollapsingHeader, RichText};
18+
use eframe::egui::{Button, CollapsingHeader, Label, RichText};
1919
use eframe::epaint::{Pos2, Vec2};
2020
use eframe::{
2121
egui::{self, FontSelection, Layout, TextFormat, Ui},
@@ -43,6 +43,7 @@ use find_string::FindString;
4343
use message::MessageHandle;
4444
use request_counter::{RequestCounter, RequestID};
4545

46+
use self::message::ModDetails;
4647
use self::toggle_switch::toggle_switch;
4748

4849
pub fn gui(args: Option<Vec<String>>) -> Result<()> {
@@ -98,10 +99,14 @@ pub struct App {
9899
lint_report: Option<LintReport>,
99100
lints_toggle_window: Option<WindowLintsToggle>,
100101
lint_options: LintOptions,
101-
cache: CommonMarkCache,
102+
update_cmark_cache: CommonMarkCache,
102103
needs_restart: bool,
103104
self_update_rid: Option<MessageHandle<SelfUpdateProgress>>,
104105
original_exe_path: Option<PathBuf>,
106+
detailed_mod_info_window: Option<WindowDetailedModInfo>,
107+
mod_details: Option<ModDetails>,
108+
fetch_mod_details_rid: Option<MessageHandle<()>>,
109+
mod_details_thumbnail_texture_handle: Option<egui::TextureHandle>,
105110
}
106111

107112
#[derive(Default)]
@@ -157,10 +162,14 @@ impl App {
157162
lint_report: None,
158163
lints_toggle_window: None,
159164
lint_options: LintOptions::default(),
160-
cache: Default::default(),
165+
update_cmark_cache: Default::default(),
161166
needs_restart: false,
162167
self_update_rid: None,
163168
original_exe_path: None,
169+
detailed_mod_info_window: None,
170+
mod_details: None,
171+
fetch_mod_details_rid: None,
172+
mod_details_thumbnail_texture_handle: None,
164173
})
165174
}
166175

@@ -434,6 +443,25 @@ impl App {
434443
ui.output_mut(|o| o.copied_text = mc.spec.url.to_owned());
435444
}
436445

446+
if let Some(modio_id) = info.modio_id
447+
&& let Some(modio_provider_params) = self.state.config.provider_parameters.get("modio")
448+
&& let Some(oauth_token) = modio_provider_params.get("oauth")
449+
&& ui
450+
.button("ℹ")
451+
.on_hover_text_at_pointer("View details")
452+
.clicked()
453+
{
454+
self.detailed_mod_info_window =
455+
Some(WindowDetailedModInfo { info: info.clone() });
456+
self.fetch_mod_details_rid = Some(message::FetchModDetails::send(
457+
&mut self.request_counter,
458+
ui.ctx(),
459+
self.tx.clone(),
460+
oauth_token,
461+
modio_id
462+
));
463+
}
464+
437465
if mc.enabled {
438466
let is_duplicate = enabled_specs.iter().any(|(i, spec)| {
439467
Some(state.index) != *i && info.spec.satisfies_dependency(spec)
@@ -730,7 +758,7 @@ impl App {
730758
.show(ctx, |ui| {
731759
CommonMarkViewer::new("available-update")
732760
.max_image_width(Some(512))
733-
.show(ui, &mut self.cache, &update.body);
761+
.show(ui, &mut self.update_cmark_cache, &update.body);
734762
ui.with_layout(egui::Layout::right_to_left(Align::TOP), |ui| {
735763
if ui
736764
.add(egui::Button::new("Install update"))
@@ -1400,6 +1428,129 @@ impl App {
14001428
}
14011429
}
14021430
}
1431+
1432+
fn show_detailed_mod_info(&mut self, ctx: &egui::Context) {
1433+
if let Some(WindowDetailedModInfo { info }) = &self.detailed_mod_info_window {
1434+
egui::Area::new("detailed-mod-info-overlay")
1435+
.movable(false)
1436+
.fixed_pos(Pos2::ZERO)
1437+
.order(egui::Order::Background)
1438+
.show(ctx, |ui| {
1439+
egui::Frame::none()
1440+
.fill(Color32::from_rgba_unmultiplied(0, 0, 0, 127))
1441+
.show(ui, |ui| {
1442+
ui.allocate_space(ui.available_size());
1443+
})
1444+
});
1445+
1446+
let mut open = true;
1447+
1448+
egui::Window::new(&info.name)
1449+
.open(&mut open)
1450+
.collapsible(false)
1451+
.anchor(Align2::CENTER_TOP, Vec2::new(0.0, 30.0))
1452+
.resizable(false)
1453+
.show(ctx, |ui| self.show_detailed_mod_info_inner(ui));
1454+
1455+
if !open {
1456+
self.detailed_mod_info_window = None;
1457+
self.mod_details = None;
1458+
self.fetch_mod_details_rid = None;
1459+
self.mod_details_thumbnail_texture_handle = None;
1460+
}
1461+
}
1462+
}
1463+
1464+
fn show_detailed_mod_info_inner(&mut self, ui: &mut egui::Ui) {
1465+
if let Some(mod_details) = &self.mod_details {
1466+
let scroll_area_height = (ui.available_height() - 60.0).clamp(0.0, f32::INFINITY);
1467+
1468+
egui::ScrollArea::vertical()
1469+
.max_height(scroll_area_height)
1470+
.max_width(f32::INFINITY)
1471+
.auto_shrink([false, false])
1472+
.stick_to_right(true)
1473+
.show(ui, |ui| {
1474+
let texture: &egui::TextureHandle = self
1475+
.mod_details_thumbnail_texture_handle
1476+
.get_or_insert_with(|| {
1477+
ui.ctx().load_texture(
1478+
format!("{} image", mod_details.r#mod.name),
1479+
{
1480+
let image =
1481+
image::load_from_memory(&mod_details.thumbnail).unwrap();
1482+
let size = [image.width() as _, image.height() as _];
1483+
let image_buffer = image.to_rgb8();
1484+
let pixels = image_buffer.as_flat_samples();
1485+
egui::ColorImage::from_rgb(size, pixels.as_slice())
1486+
},
1487+
Default::default(),
1488+
)
1489+
});
1490+
ui.vertical_centered(|ui| {
1491+
ui.image(texture, texture.size_vec2());
1492+
});
1493+
1494+
ui.heading("Uploader");
1495+
ui.label(&mod_details.r#mod.submitted_by.username);
1496+
ui.add_space(10.0);
1497+
1498+
ui.heading("Description");
1499+
if let Some(desc) = &mod_details.r#mod.description_plaintext {
1500+
ui.label(desc);
1501+
} else {
1502+
ui.label("No description provided.");
1503+
}
1504+
ui.add_space(10.0);
1505+
1506+
ui.heading("Versions and changelog");
1507+
ui.label(
1508+
RichText::new("Only the 10 most recent versions are shown.")
1509+
.color(Color32::GRAY)
1510+
.italics(),
1511+
);
1512+
egui::Grid::new("mod-details-available-versions")
1513+
.spacing(Vec2::new(3.0, 10.0))
1514+
.striped(true)
1515+
.num_columns(2)
1516+
.show(ui, |ui| {
1517+
mod_details.versions.iter().for_each(|file| {
1518+
if let Some(version) = &file.version {
1519+
ui.label(version);
1520+
} else {
1521+
ui.label("Unknown version");
1522+
}
1523+
if let Some(changelog) = &file.changelog {
1524+
ui.add(Label::new(changelog).wrap(true));
1525+
} else {
1526+
ui.label("N/A");
1527+
}
1528+
ui.end_row();
1529+
});
1530+
});
1531+
ui.add_space(10.0);
1532+
1533+
ui.heading("Files");
1534+
if let Some(file) = &mod_details.r#mod.modfile {
1535+
ui.horizontal(|ui| {
1536+
if let Some(version) = &file.version {
1537+
ui.label(version);
1538+
} else {
1539+
ui.label("Unknown version");
1540+
}
1541+
ui.hyperlink(&file.download.binary_url);
1542+
});
1543+
} else {
1544+
ui.label("No files provided.");
1545+
}
1546+
});
1547+
} else {
1548+
ui.horizontal(|ui| {
1549+
ui.spinner();
1550+
ui.label("Fetching mod details from mod.io...");
1551+
});
1552+
}
1553+
}
14031554
}
14041555

14051556
struct WindowProviderParameters {
@@ -1456,6 +1607,10 @@ struct WindowLintsToggle {
14561607
mods: Vec<ModSpecification>,
14571608
}
14581609

1610+
struct WindowDetailedModInfo {
1611+
info: ModInfo,
1612+
}
1613+
14591614
impl eframe::App for App {
14601615
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
14611616
if self.needs_restart
@@ -1491,6 +1646,7 @@ impl eframe::App for App {
14911646
self.show_settings(ctx);
14921647
self.show_lints_toggle(ctx);
14931648
self.show_lint_report(ctx);
1649+
self.show_detailed_mod_info(ctx);
14941650

14951651
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
14961652
ui.with_layout(egui::Layout::right_to_left(Align::TOP), |ui| {
@@ -1499,6 +1655,8 @@ impl eframe::App for App {
14991655
&& self.update_rid.is_none()
15001656
&& self.lint_rid.is_none()
15011657
&& self.self_update_rid.is_none()
1658+
&& self.detailed_mod_info_window.is_none()
1659+
&& self.fetch_mod_details_rid.is_none()
15021660
&& self.state.config.drg_pak_path.is_some(),
15031661
|ui| {
15041662
if let Some(args) = &self.args {

0 commit comments

Comments
 (0)