mirror of https://github.com/wayvr-org/wayvr.git
dash-frontend: http_client: Progress callback, display download status
This commit is contained in:
parent
2035d9ba76
commit
1bee41aea9
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#33FF99" d="m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 295 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#FF4455" d="M12 17q.425 0 .713-.288T13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17m-1-4h2V7h-2zm1 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>
|
||||
|
After Width: | Height: | Size: 574 B |
|
|
@ -1,10 +1,17 @@
|
|||
<layout>
|
||||
<include src="../t_separator.xml"/>
|
||||
|
||||
<elements>
|
||||
<div align_items="center" justify_content="center">
|
||||
<div flex_direction="column" gap="8">
|
||||
<label translation="DOWNLOADING_FILE" size="14"/>
|
||||
<label id="label_target_path" />
|
||||
<label id="label_progress"/>
|
||||
<div align_items="center" justify_content="center" width="100%">
|
||||
<div flex_direction="column" gap="8" width="100%">
|
||||
<label translation="DOWNLOADING_FILE" size="24" weight="bold"/>
|
||||
<Separator/>
|
||||
<label id="label_target_path" color="~color_text_translucent" />
|
||||
|
||||
<div gap="8" align_items="center">
|
||||
<div id="loading_parent"/>
|
||||
<label id="label_status"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Re
|
|||
app_id
|
||||
);
|
||||
|
||||
match http_client::get(&executor, &url).await {
|
||||
match http_client::get_simple(&executor, &url).await {
|
||||
Ok(response) => {
|
||||
log::info!("Success");
|
||||
cache_dir::set_data(&cache_file_path, &response.data).await?;
|
||||
|
|
@ -68,7 +68,7 @@ async fn get_app_details_json_internal(
|
|||
// Fetch from Steam API
|
||||
log::info!("Fetching app detail ID {}", app_id);
|
||||
let url = format!("https://store.steampowered.com/api/appdetails?appids={}", app_id);
|
||||
let response = http_client::get(&executor, &url).await?;
|
||||
let response = http_client::get_simple(&executor, &url).await?;
|
||||
let res_utf8 = String::from_utf8(response.data)?;
|
||||
let root = serde_json::from_str::<serde_json::Value>(&res_utf8)?;
|
||||
let body = root.get(&app_id).context("invalid body")?;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use http_body_util::{BodyStream, Empty};
|
|||
use hyper::Request;
|
||||
use smol::{net::TcpStream, prelude::*};
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Debug;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
|
|
@ -28,10 +29,23 @@ impl HttpClientResponse {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
|
||||
log::info!("fetching URL \"{}\"", url);
|
||||
pub struct ProgressFuncData {
|
||||
pub bytes_downloaded: u64,
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
let url: hyper::Uri = url.try_into()?;
|
||||
pub type ProgressFunc = Box<dyn Fn(ProgressFuncData)>;
|
||||
|
||||
pub struct GetParams<'a> {
|
||||
pub executor: &'a AsyncExecutor,
|
||||
pub url: &'a str,
|
||||
pub on_progress: Option<ProgressFunc>,
|
||||
}
|
||||
|
||||
pub async fn get(params: GetParams<'_>) -> anyhow::Result<HttpClientResponse> {
|
||||
log::info!("fetching URL \"{}\"", params.url);
|
||||
|
||||
let url: hyper::Uri = params.url.try_into()?;
|
||||
let req = Request::builder()
|
||||
.header(
|
||||
hyper::header::HOST,
|
||||
|
|
@ -40,23 +54,56 @@ pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClie
|
|||
.uri(url)
|
||||
.body(Empty::new())?;
|
||||
|
||||
let resp = fetch(executor, req).await?;
|
||||
let resp = fetch(params.executor, req).await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
// non-200 HTTP response
|
||||
anyhow::bail!("non-200 HTTP response: {}", resp.status().as_str());
|
||||
}
|
||||
|
||||
let body = BodyStream::new(resp.into_body())
|
||||
let mut bytes_downloaded: u64 = 0;
|
||||
let mut file_size: u64 = 1;
|
||||
|
||||
let (parts, body) = resp.into_parts();
|
||||
|
||||
// that's a pretty interesting way to get file size :]
|
||||
if let Some(val) = parts.headers.get("Content-Length") {
|
||||
if let Ok(str) = val.to_str() {
|
||||
if let Ok(s) = str.parse() {
|
||||
file_size = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut on_progress = params.on_progress;
|
||||
|
||||
let data = BodyStream::new(body)
|
||||
.try_fold(Vec::new(), |mut body, chunk| {
|
||||
if let Some(chunk) = chunk.data_ref() {
|
||||
bytes_downloaded += chunk.len() as u64;
|
||||
body.extend_from_slice(chunk);
|
||||
|
||||
if let Some(on_progress) = &mut on_progress {
|
||||
on_progress(ProgressFuncData {
|
||||
bytes_downloaded,
|
||||
file_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(body)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(HttpClientResponse { data: body })
|
||||
Ok(HttpClientResponse { data })
|
||||
}
|
||||
|
||||
pub async fn get_simple(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
|
||||
get(GetParams {
|
||||
executor,
|
||||
url,
|
||||
on_progress: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub async fn fetch_to_glyph_data(
|
|||
executor: &AsyncExecutor,
|
||||
url: &str,
|
||||
) -> anyhow::Result<CustomGlyphData> {
|
||||
let res = http_client::get(executor, url).await?;
|
||||
let res = http_client::get_simple(executor, url).await?;
|
||||
let glyph_data = CustomGlyphData::from_bytes_raster(globals, url, &res.data)?;
|
||||
Ok(glyph_data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
#![allow(dead_code)] // TODO: Remove later
|
||||
#![allow(dead_code)]
|
||||
use std::path::PathBuf;
|
||||
|
||||
// TODO: Remove later
|
||||
use serde::Deserialize;
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
|
||||
|
|
@ -36,12 +39,18 @@ impl SkymapCatalogEntryFiles {
|
|||
format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, self.preview)
|
||||
}
|
||||
|
||||
pub fn get_filename_from_res(&self, res: SkymapResolution) -> Option<&String> {
|
||||
pub fn get_filename_from_res(&self, res: SkymapResolution) -> Option<String> {
|
||||
match res {
|
||||
SkymapResolution::Res2k => Some(&self.size_2k),
|
||||
SkymapResolution::Res4k => self.size_4k.as_ref(),
|
||||
SkymapResolution::Res8k => self.size_8k.as_ref(),
|
||||
}
|
||||
.map(|raw_filename| {
|
||||
// sanitize filename, do not allow "../" just in case
|
||||
PathBuf::from(raw_filename)
|
||||
.file_name()
|
||||
.map(|s| String::from(s.to_string_lossy()))
|
||||
})?
|
||||
}
|
||||
|
||||
// example result: "https://wayvr.org/skymaps/files/my_skymap_8k.png"
|
||||
|
|
@ -89,7 +98,7 @@ impl SkymapCatalog {
|
|||
pub async fn request_catalog(executor: &AsyncExecutor) -> anyhow::Result<SkymapCatalog> {
|
||||
log::info!("Fetching skymap list");
|
||||
|
||||
let res = http_client::get(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?;
|
||||
let res = http_client::get_simple(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?;
|
||||
let catalog = res.as_json::<SkymapCatalog>()?;
|
||||
catalog.validate()?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use glam::Mat4;
|
||||
use glam::{Mat4, Vec2};
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
assets::AssetPath,
|
||||
|
|
@ -7,10 +7,15 @@ use wgui::{
|
|||
layout::{Layout, LayoutTask, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
renderer_vk::{
|
||||
text::{FontWeight, TextStyle},
|
||||
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
|
||||
util::centered_matrix,
|
||||
},
|
||||
widget::label::{WidgetLabel, WidgetLabelParams},
|
||||
taffy::{self, prelude::length},
|
||||
widget::{
|
||||
self,
|
||||
label::{WidgetLabel, WidgetLabelParams},
|
||||
sprite::{WidgetSprite, WidgetSpriteParams},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
|
||||
|
|
@ -49,6 +54,31 @@ pub fn create_label_error(layout: &mut Layout, parent: WidgetID, content: String
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_icon(layout: &mut Layout, id_parent: WidgetID, size: Vec2, path: AssetPath) -> anyhow::Result<WidgetID> {
|
||||
let widget_sprite = WidgetSprite::create(WidgetSpriteParams {
|
||||
color: None,
|
||||
glyph_data: Some(CustomGlyphData::from_assets(&layout.state.globals, path)?),
|
||||
});
|
||||
|
||||
let size = taffy::Size {
|
||||
width: length(size.x),
|
||||
height: length(size.y),
|
||||
};
|
||||
|
||||
let (widget, _) = layout.add_child(
|
||||
id_parent,
|
||||
widget_sprite,
|
||||
taffy::Style {
|
||||
min_size: size.clone(),
|
||||
max_size: size.clone(),
|
||||
size: size.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(widget.id)
|
||||
}
|
||||
|
||||
pub struct CreateLoadingParams<'a> {
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
util::{
|
||||
networking::http_client::{self, ProgressFuncData},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
wgui_simple,
|
||||
},
|
||||
views::{ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
use glam::Vec2;
|
||||
use std::path::PathBuf;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
|
|
@ -26,7 +31,12 @@ pub struct Params<'a> {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {}
|
||||
enum Task {
|
||||
StartDownload(/*url*/ String),
|
||||
SetStatusText(String),
|
||||
ShowIconSuccess,
|
||||
ShowIconError,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
id_parent: WidgetID,
|
||||
|
|
@ -36,12 +46,47 @@ pub struct View {
|
|||
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
|
||||
id_label_status: WidgetID,
|
||||
id_loading_parent: WidgetID,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
for task in self.tasks.drain() {
|
||||
match task {}
|
||||
match task {
|
||||
Task::StartDownload(url) => {
|
||||
self
|
||||
.executor
|
||||
.spawn(View::download(self.tasks.clone(), self.executor.clone(), url))
|
||||
.detach();
|
||||
}
|
||||
Task::SetStatusText(text) => {
|
||||
let widgets = &mut par.layout.state.widgets;
|
||||
widgets
|
||||
.fetch(self.id_label_status)?
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut par.layout.common(), Translation::from_raw_text_string(text));
|
||||
}
|
||||
Task::ShowIconSuccess => {
|
||||
par.layout.remove_children(self.id_loading_parent);
|
||||
wgui_simple::create_icon(
|
||||
par.layout,
|
||||
self.id_loading_parent,
|
||||
Vec2::splat(32.0),
|
||||
AssetPath::BuiltIn("dashboard/check.svg"),
|
||||
)?;
|
||||
}
|
||||
Task::ShowIconError => {
|
||||
par.layout.remove_children(self.id_loading_parent);
|
||||
wgui_simple::create_icon(
|
||||
par.layout,
|
||||
self.id_loading_parent,
|
||||
Vec2::splat(32.0),
|
||||
AssetPath::BuiltIn("dashboard/error.svg"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -57,7 +102,15 @@ impl View {
|
|||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?;
|
||||
let parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?;
|
||||
let id_label_status = parser_state.get_widget_id("label_status")?;
|
||||
let id_loading_parent = parser_state.get_widget_id("loading_parent")?;
|
||||
|
||||
wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
|
||||
parent_id: id_loading_parent,
|
||||
layout: par.layout,
|
||||
with_text: false,
|
||||
})?;
|
||||
|
||||
let str_target_path = par.globals.i18n().translate("TARGET_PATH");
|
||||
|
||||
|
|
@ -71,14 +124,50 @@ impl View {
|
|||
);
|
||||
}
|
||||
|
||||
tasks.push(Task::StartDownload(par.url.clone()));
|
||||
|
||||
Ok(Self {
|
||||
id_parent: par.parent_id,
|
||||
tasks,
|
||||
globals: par.globals.clone(),
|
||||
executor: par.executor.clone(),
|
||||
parser_state,
|
||||
id_label_status,
|
||||
id_loading_parent,
|
||||
})
|
||||
}
|
||||
|
||||
async fn download(tasks: Tasks<Task>, executor: AsyncExecutor, url: String) {
|
||||
tasks.push(Task::SetStatusText(String::from("Connecting to the server...")));
|
||||
|
||||
let res = http_client::get(http_client::GetParams {
|
||||
executor: &executor,
|
||||
url: &url,
|
||||
on_progress: Some(Box::new({
|
||||
let tasks = tasks.clone();
|
||||
move |data: ProgressFuncData| {
|
||||
tasks.push(Task::SetStatusText(format!(
|
||||
"{}/{} KiB ({}%)",
|
||||
data.bytes_downloaded / 1024,
|
||||
data.file_size / 1024,
|
||||
(data.bytes_downloaded as f32 / data.file_size as f32 * 100.0).round()
|
||||
)))
|
||||
}
|
||||
})),
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_response) => {
|
||||
tasks.push(Task::SetStatusText(String::from("Download finished")));
|
||||
tasks.push(Task::ShowIconSuccess);
|
||||
}
|
||||
Err(e) => {
|
||||
tasks.push(Task::ShowIconError);
|
||||
tasks.push(Task::SetStatusText(format!("Download failed: {:?}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use wgui::{
|
|||
task::Tasks,
|
||||
widget::{image::WidgetImage, label::WidgetLabel},
|
||||
};
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
use wlx_common::{async_executor::AsyncExecutor, config_io};
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: &'a WguiGlobals,
|
||||
|
|
@ -72,10 +72,12 @@ impl ViewTrait for View {
|
|||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::ResolutionClicked(skymap_resolution) => {
|
||||
self.run_download(&mut par.layout, skymap_resolution)?;
|
||||
self.run_download(skymap_resolution)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.popup_download.update(par)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +132,7 @@ impl View {
|
|||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_creation_date")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_creation_date, par.entry.created_at,)),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_creation_date, par.entry.created_at)),
|
||||
);
|
||||
|
||||
// Set modification date label
|
||||
|
|
@ -138,7 +140,7 @@ impl View {
|
|||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_modification_date")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_modification_date, par.entry.created_at,)),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_modification_date, par.entry.created_at)),
|
||||
);
|
||||
|
||||
let files = &par.entry.files;
|
||||
|
|
@ -173,18 +175,24 @@ impl View {
|
|||
})
|
||||
}
|
||||
|
||||
fn run_download(&mut self, layout: &mut Layout, resolution: SkymapResolution) -> anyhow::Result<()> {
|
||||
let target_path = PathBuf::from(format!(""));
|
||||
fn run_download(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
|
||||
let Some(url) = self.entry.files.get_url_from_res(resolution) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(filename) = self.entry.files.get_filename_from_res(resolution) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut path = config_io::get_skymaps_root();
|
||||
path = path.join(filename);
|
||||
|
||||
views::download_file::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
self.executor.clone(),
|
||||
self.globals.clone(),
|
||||
self.popup_download.clone(),
|
||||
target_path,
|
||||
path,
|
||||
url,
|
||||
);
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -86,6 +86,11 @@ impl WidgetMap {
|
|||
self.0.get(handle)
|
||||
}
|
||||
|
||||
// same as get(), but with error message
|
||||
pub fn fetch(&self, handle: WidgetID) -> anyhow::Result<Widget> {
|
||||
self.get(handle).cloned().context("Failed to fetch widget")
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, obj: Widget) -> WidgetID {
|
||||
self
|
||||
.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue