-
-
-
+
diff --git a/dash-frontend/src/util/cached_fetcher.rs b/dash-frontend/src/util/cached_fetcher.rs
index 531ab38e..2671a748 100644
--- a/dash-frontend/src/util/cached_fetcher.rs
+++ b/dash-frontend/src/util/cached_fetcher.rs
@@ -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::
(&res_utf8)?;
let body = root.get(&app_id).context("invalid body")?;
diff --git a/dash-frontend/src/util/networking/http_client.rs b/dash-frontend/src/util/networking/http_client.rs
index 69a018f2..4778fbed 100644
--- a/dash-frontend/src/util/networking/http_client.rs
+++ b/dash-frontend/src/util/networking/http_client.rs
@@ -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 {
- 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;
+
+pub struct GetParams<'a> {
+ pub executor: &'a AsyncExecutor,
+ pub url: &'a str,
+ pub on_progress: Option,
+}
+
+pub async fn get(params: GetParams<'_>) -> anyhow::Result {
+ 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 anyhow::Result {
+ get(GetParams {
+ executor,
+ url,
+ on_progress: None,
+ })
+ .await
}
async fn fetch(
diff --git a/dash-frontend/src/util/networking/image_fetch.rs b/dash-frontend/src/util/networking/image_fetch.rs
index 68e1499a..38c78348 100644
--- a/dash-frontend/src/util/networking/image_fetch.rs
+++ b/dash-frontend/src/util/networking/image_fetch.rs
@@ -8,7 +8,7 @@ pub async fn fetch_to_glyph_data(
executor: &AsyncExecutor,
url: &str,
) -> anyhow::Result {
- 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)
}
diff --git a/dash-frontend/src/util/networking/skymap_catalog.rs b/dash-frontend/src/util/networking/skymap_catalog.rs
index c3e2e63a..545da274 100644
--- a/dash-frontend/src/util/networking/skymap_catalog.rs
+++ b/dash-frontend/src/util/networking/skymap_catalog.rs
@@ -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 {
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 {
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::()?;
catalog.validate()?;
diff --git a/dash-frontend/src/util/wgui_simple.rs b/dash-frontend/src/util/wgui_simple.rs
index 6dbdcab9..a2bf6629 100644
--- a/dash-frontend/src/util/wgui_simple.rs
+++ b/dash-frontend/src/util/wgui_simple.rs
@@ -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 {
+ 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,
diff --git a/dash-frontend/src/views/download_file.rs b/dash-frontend/src/views/download_file.rs
index b7eea8da..92c3e03c 100644
--- a/dash-frontend/src/views/download_file.rs
+++ b/dash-frontend/src/views/download_file.rs
@@ -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::()?
+ .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, 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(
diff --git a/dash-frontend/src/views/remote_skymap_downloader.rs b/dash-frontend/src/views/remote_skymap_downloader.rs
index 4a2326e0..a495ffed 100644
--- a/dash-frontend/src/views/remote_skymap_downloader.rs
+++ b/dash-frontend/src/views/remote_skymap_downloader.rs
@@ -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::(&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::(&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(())
diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs
index 5ee939b2..0d195e32 100644
--- a/wgui/src/layout.rs
+++ b/wgui/src/layout.rs
@@ -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 {
+ self.get(handle).cloned().context("Failed to fetch widget")
+ }
+
pub fn insert(&mut self, obj: Widget) -> WidgetID {
self
.0