Skip to content

Commit 6d77bbf

Browse files
committed
skin fetching is asynchronous
1 parent 51b49be commit 6d77bbf

2 files changed

Lines changed: 109 additions & 43 deletions

File tree

resources/ui/skin-loader-popover.ui

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
<class name="linked"/>
1515
</style>
1616
<child>
17-
<object class="GtkSearchEntry" id="search_skin_entry">
17+
<object class="GtkSearchEntry" id="nickname_entry">
1818
<property name="placeholder-text">nickname</property>
1919
</object>
2020
</child>
2121
<child>
22-
<object class="GtkButton" id="search_skin_button">
22+
<object class="GtkButton" id="search_button">
2323
<property name="label">Search</property>
2424
</object>
2525
</child>

src/skin_loader_popover.rs

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::error::Error;
22
use std::io::{Read, Write};
3-
use std::sync::OnceLock;
3+
use std::ops::{Add, Deref, DerefMut};
4+
use std::sync::{Arc, OnceLock};
5+
use std::time::{Duration, Instant};
46

57
use bytes::BufMut;
68
use gtk::{gio, glib, Orientation};
@@ -12,31 +14,35 @@ use gtk::subclass::prelude::ObjectSubclassIsExt;
1214
use image::{DynamicImage, EncodableLayout, GenericImage, GenericImageView};
1315
use tokio::runtime::Runtime;
1416
use tokio::sync::mpsc::channel;
17+
use tokio::sync::Mutex;
1518

1619
use crate::glium_area::skin_parser::TextureType;
1720
use crate::utils::guess_model_type;
1821
use crate::window::Window;
1922

2023
mod imp {
21-
use gtk::{glib, TemplateChild};
22-
use gtk::CompositeTemplate;
24+
use std::cell::Cell;
25+
use std::sync::Arc;
26+
27+
use gtk::{CompositeTemplate, glib, TemplateChild};
2328
use gtk::subclass::popover::PopoverImpl;
2429
use gtk::subclass::prelude::{CompositeTemplate, CompositeTemplateInitializingExt, ObjectImpl, ObjectSubclass, WidgetImpl};
2530
use gtk::subclass::widget::WidgetClassExt;
2631

27-
use crate::skin_loader_popover::SkinApiClient;
32+
use crate::skin_loader_popover::SkinClient;
2833

2934
#[derive(CompositeTemplate, Default)]
3035
#[template(file = "../resources/ui/skin-loader-popover.ui")]
3136
pub struct SkinLoaderPopover {
3237
#[template_child]
33-
pub search_skin_button: TemplateChild<gtk::Button>,
38+
pub nickname_entry: TemplateChild<gtk::SearchEntry>,
3439
#[template_child]
35-
pub search_skin_entry: TemplateChild<gtk::SearchEntry>,
40+
pub search_button: TemplateChild<gtk::Button>,
3641
#[template_child]
3742
pub popover_content: TemplateChild<gtk::Box>,
3843

39-
pub skin_loader_api_client: SkinApiClient,
44+
pub client: Arc<SkinClient>,
45+
pub is_searching: Cell<bool>,
4046
}
4147

4248
#[glib::object_subclass]
@@ -66,26 +72,46 @@ glib::wrapper! {
6672

6773
fn runtime() -> &'static Runtime {
6874
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
69-
// RUNTIME.get_or_init(|| tokio::runtime::Builder::new_current_thread()
70-
// .enable_all()
71-
// .build()
72-
// .expect("Setting up tokio runtime needs to succeed."))
73-
RUNTIME.get_or_init(|| Runtime::new().expect("Setting up tokio runtime needs to succeed."))
75+
RUNTIME.get_or_init(|| Runtime::new()
76+
.expect("Setting up tokio runtime needs to succeed."))
7477
}
7578

7679
#[derive(Default)]
77-
struct SkinApiClient;
78-
impl SkinApiClient {
79-
const URI: &'static str = "https://mc-heads.net/skin";
80+
struct SkinClient {
81+
last_request_time: Arc<Mutex<Option<Instant>>>,
82+
cooldown_duration: Arc<Mutex<Duration>>,
83+
}
84+
impl SkinClient {
85+
const BASE_URL: &'static str = "https://mc-heads.net/skin";
86+
87+
pub async fn set_cooldown(&self, secs: f32) {
88+
*self.cooldown_duration.lock().await = Duration::from_secs_f32(secs);
89+
}
90+
91+
pub async fn cooldown(&self) {
92+
let mut last_request_time = self.last_request_time.lock().await;
93+
let cooldown_duration = self.cooldown_duration.lock().await.clone();
94+
95+
if let Some(last_time) = *last_request_time {
96+
let elapsed = last_time.elapsed();
97+
if elapsed < cooldown_duration {
98+
let remained_cooldown = cooldown_duration - elapsed;
99+
tokio::time::sleep(remained_cooldown).await;
100+
}
101+
}
80102

81-
pub fn new() -> SkinApiClient {
82-
SkinApiClient
103+
// Update last request time
104+
*last_request_time = Some(Instant::now());
83105
}
84-
106+
85107
pub async fn get_skin(&self, nickname: &str) -> Result<DynamicImage, Box<dyn Error>> {
86-
let uri = format!("{}/{}", Self::URI, nickname);
108+
self.cooldown().await;
109+
let uri = format!("{}/{}", Self::BASE_URL, nickname);
87110
let url = reqwest::Url::parse(uri.as_str()).unwrap();
88-
let mut skin = reqwest::get(url).await?.bytes().await?;
111+
let client = reqwest::Client::builder()
112+
.timeout(Duration::from_secs(5))
113+
.build()?;
114+
let mut skin = client.get(url).send().await?.bytes().await?;
89115
let image = image::load_from_memory(skin.as_bytes())?;
90116
Ok(image)
91117
}
@@ -94,21 +120,22 @@ impl SkinApiClient {
94120
impl SkinLoaderPopover {
95121
pub fn new(win: &Window) -> Self {
96122
let popover: SkinLoaderPopover = glib::Object::new();
97-
123+
runtime().block_on(async {
124+
popover.imp().client.set_cooldown(1.5).await
125+
});
98126
popover.connect_signals(win);
99-
100127
popover
101128
}
102129

103130
pub fn connect_signals(&self, win: &Window) {
104-
self.imp().search_skin_button.connect_clicked(self.get_search_skin_button_handler(win.clone()));
131+
self.imp().search_button.connect_clicked(self.get_search_skin_button_handler(win.clone()));
105132
}
106133

107134
fn create_texture_button(win: Window, texture: DynamicImage, title: &str) -> gtk::Button {
108135
let texture_button = gtk::Button::new();
109-
let temporary_file = "temporary_file.png";
110-
texture.save(temporary_file).unwrap();
111136
let paintable = {
137+
let temporary_file = "temporary_file.png";
138+
texture.save(temporary_file).unwrap();
112139
let f = gio::File::for_path(temporary_file);
113140
Texture::from_file(&f).unwrap()
114141
};
@@ -147,36 +174,75 @@ impl SkinLoaderPopover {
147174

148175
texture_button
149176
}
150-
177+
151178
fn get_search_skin_button_handler(&self, win: Window) -> impl Fn(&gtk::Button) {
152179
let popover = self.clone();
153180
move |btn| {
154-
let popover = popover.clone();
155-
let win = win.clone();
181+
if popover.searching() {
182+
return
183+
}
184+
let nickname = match popover.get_nickname() {
185+
Some(nickname) => nickname,
186+
None => return
187+
};
188+
popover.set_searching(true);
156189

190+
popover.clear();
191+
popover.add_spinner();
157192
let (sender, mut receiver) = channel::<Result<DynamicImage, ()>>(10000);
158-
let nickname = popover.imp().search_skin_entry.text();
159193

160-
runtime().spawn(clone!(@strong nickname, @strong sender => async move {
161-
let client = SkinApiClient::new();
162-
let texture = client.get_skin(nickname.as_str()).await.map_err(|_| ());
163-
sender.send(texture).await.expect("The channel needs to be open");
194+
// Spawn a task to fetch the skin
195+
let client = popover.imp().client.clone();
196+
runtime().spawn(clone!(@strong nickname => async move {
197+
println!("Fetching the skin...");
198+
let response = client.get_skin(nickname.as_str()).await.map_err(|_| ());
199+
sender.send(response).await.expect("The channel needs to be open");
164200
}));
165201

166-
glib::spawn_future_local(async move {
167-
while let Some(texture_result) = receiver.recv().await {
168-
if texture_result.is_err() {
202+
glib::spawn_future_local(clone!(@strong win, @strong popover => async move {
203+
while let Some(response) = receiver.recv().await {
204+
popover.set_searching(false);
205+
if response.is_err() {
169206
println!("Bad request");
207+
popover.clear();
170208
return
171209
}
172-
let texture = texture_result.unwrap();
210+
let texture = response.unwrap();
173211
let texture_button = SkinLoaderPopover::create_texture_button(win.clone(), texture, nickname.as_str());
174-
if let Some(child) = popover.imp().popover_content.last_child() {
175-
popover.imp().popover_content.remove(&child);
176-
}
212+
popover.clear();
177213
popover.imp().popover_content.append(&texture_button);
178214
}
179-
});
215+
}));
216+
}
217+
}
218+
219+
fn get_nickname(&self) -> Option<String> {
220+
let text = self.imp().nickname_entry.text();
221+
if text.is_empty() {
222+
return None
180223
}
224+
Some(text.to_string())
225+
}
226+
227+
fn searching(&self) -> bool {
228+
self.imp().is_searching.get()
229+
}
230+
231+
fn set_searching(&self, searching: bool) {
232+
self.imp().is_searching.replace(searching);
233+
}
234+
235+
fn clear(&self) {
236+
while let Some(spinner) = self.imp().popover_content.last_child() {
237+
self.imp().popover_content.remove(&spinner);
238+
}
239+
}
240+
241+
fn add_spinner(&self) {
242+
let spinner = gtk::Spinner::new();
243+
spinner.set_height_request(40);
244+
spinner.set_width_request(40);
245+
spinner.set_spinning(true);
246+
self.imp().popover_content.append(&spinner);
181247
}
182248
}

0 commit comments

Comments
 (0)