dash-frontend: skymap downloader dialog

This commit is contained in:
Aleksander 2026-04-07 23:43:01 +02:00 committed by galister
parent e5e798b599
commit 47bb5ab7a2
9 changed files with 362 additions and 22 deletions

View File

@ -0,0 +1,35 @@
<layout>
<!--
parameters:
"text"
ids:
"button"
-->
<template name="ResolutionButton">
<Button id="button" sprite_src_builtin="dashboard/download.svg" text="${text}"/>
</template>
<include src="../t_separator.xml"/>
<elements>
<div gap="8" flex_direction="column" min_width="100%" overflow_y="scroll">
<label id="label_author" weight="bold"/>
<label id="label_description" wrap="1"/>
<Separator/>
<div gap="24" justify_content="center">
<label id="label_creation_date"/>
<label id="label_modification_date"/>
<label id="label_version"/>
</div>
<div gap="8" flex_direction="column" align_items="center">
<image id="image" width="500" height="250" round="8" border="2" border_color="~color_accent"/>
<div id="resolution_buttons" gap="8" flex_direction="row">
<!-- filled-in at runtime -->
</div>
</div>
</div>
</elements>
</layout>

View File

@ -2,12 +2,14 @@
<!--
ids:
"button"
"image_preview"
"label_title"
"label_desc"
-->
<template name="Cell">
<Button
id="button"
padding="8"
round="8"
flex_direction="column"

View File

@ -139,6 +139,7 @@
"VOLUME": "Volume"
},
"CLOSE_WINDOW": "Close window",
"CREATION_DATE": "Creation date",
"DEBUG_INFO": "Debug info",
"DISPLAY_BRIGHTNESS": "Display brightness",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
@ -154,6 +155,7 @@
"HELLO_USER": "Hello, {USER}!",
"HIDE": "Hide",
"HOME_SCREEN": "Home",
"MODIFICATION_DATE": "Modification date",
"MONADO_RUNTIME": "Monado runtime",
"LOADING": "Loading...",
"POPUP_ADD_DISPLAY": {
@ -169,5 +171,6 @@
"SETTINGS": "Settings",
"SHOW": "Show",
"TERMINATE_PROCESS": "Terminate process",
"VERSION": "Version",
"WIDTH": "Width"
}

View File

@ -6,19 +6,28 @@ use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client};
pub type SkymapUuid = uuid::Uuid;
#[derive(Copy, Clone)]
pub enum SkymapResolution {
Res2k,
Res4k,
Res8k,
Res16k,
}
impl SkymapResolution {
pub const fn get_display_str(&self) -> &'static str {
match self {
SkymapResolution::Res2k => "2K (6 MiB VRAM)",
SkymapResolution::Res4k => "4K (24 MiB VRAM)",
SkymapResolution::Res8k => "8K (96 MiB VRAM)",
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct SkymapCatalogEntryFiles {
pub size_16k: Option<String>, // "my_skymap_16k.png"
pub size_8k: Option<String>, // "my_skymap_8k.png"
pub size_4k: Option<String>, // "my_skymap_4k.png"
pub size_2k: String, // we should have *at least* this
pub size_8k: Option<String>, // "my_skymap_8k.png"
pub size_4k: Option<String>, // "my_skymap_4k.png"
pub size_2k: String, // we should have *at least* this
pub preview: String,
}
@ -32,7 +41,6 @@ impl SkymapCatalogEntryFiles {
SkymapResolution::Res2k => Some(&self.size_2k),
SkymapResolution::Res4k => self.size_4k.as_ref(),
SkymapResolution::Res8k => self.size_8k.as_ref(),
SkymapResolution::Res16k => self.size_16k.as_ref(),
}
}

View File

@ -3,6 +3,7 @@ pub mod audio_settings;
pub mod game_cover;
pub mod game_launcher;
pub mod game_list;
pub mod remote_skymap_downloader;
pub mod remote_skymap_list;
pub mod running_games_list;
pub mod skymap_list;

View File

@ -0,0 +1,199 @@
use std::{collections::HashMap, rc::Rc};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
networking::{self, skymap_catalog::SkymapResolution},
popup_manager::{MountPopupOnceParams, PopupHolder},
},
};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
event::EventAlterables,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::custom_glyph::CustomGlyphData,
task::Tasks,
widget::{image::WidgetImage, label::WidgetLabel},
};
use wlx_common::async_executor::AsyncExecutor;
pub struct Params<'a> {
pub globals: &'a WguiGlobals,
pub layout: &'a mut Layout,
pub executor: &'a AsyncExecutor,
pub parent_id: WidgetID,
pub entry: networking::skymap_catalog::SkymapCatalogEntry,
pub on_close_request: Box<dyn Fn()>,
pub preview_image: Option<CustomGlyphData>,
}
#[derive(Clone)]
enum Task {
ResolutionClicked(networking::skymap_catalog::SkymapResolution),
}
pub struct View {
id_parent: WidgetID,
entry: networking::skymap_catalog::SkymapCatalogEntry,
globals: WguiGlobals,
tasks: Tasks<Task>,
executor: AsyncExecutor,
#[allow(dead_code)]
parser_state: ParserState,
}
fn mount_resolution_button(
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
layout: &mut Layout,
parent_id: WidgetID,
res: SkymapResolution,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
let mut t = HashMap::<Rc<str>, Rc<str>>::new();
t.insert(Rc::from("text"), Rc::from(res.get_display_str()));
let data = parser_state.realize_template(doc_params, "ResolutionButton", layout, parent_id, t)?;
let button = data.fetch_component_as::<ComponentButton>("button")?;
tasks.handle_button(&button, Task::ResolutionClicked(res));
Ok(())
}
impl View {
pub fn new(par: Params) -> anyhow::Result<Self> {
let tasks = Tasks::<Task>::new();
let doc_params = ParseDocumentParams {
globals: par.globals.clone(),
path: AssetPath::BuiltIn("gui/view/remote_skymap_downloader.xml"),
extra: Default::default(),
};
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?;
let id_resolution_buttons = parser_state.get_widget_id("resolution_buttons")?;
let str_version = par.globals.i18n().translate("VERSION");
let str_creation_date = par.globals.i18n().translate("CREATION_DATE");
let str_modification_date = par.globals.i18n().translate("MODIFICATION_DATE");
{
let mut alterables = EventAlterables::default();
let image = parser_state.fetch_widget(&par.layout.state, "image")?.widget;
let mut image = image.cast::<WidgetImage>()?;
image.set_content(&mut alterables, par.preview_image);
par.layout.process_alterables(alterables)?;
}
// Set author label
parser_state
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_author")?
.set_text_simple(
&mut par.globals.get(),
Translation::from_raw_text_string(format!("by {}", par.entry.author)),
);
// Set description label
parser_state
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_description")?
.set_text_simple(
&mut par.globals.get(),
Translation::from_raw_text(&par.entry.description),
);
// Set version label
parser_state
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_version")?
.set_text_simple(
&mut par.globals.get(),
Translation::from_raw_text_string(format!("{}: {}", str_version, par.entry.version)),
);
// Set creation date label
parser_state
.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,)),
);
// Set modification date label
parser_state
.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,)),
);
let files = &par.entry.files;
let mut mount_res = |res: SkymapResolution| -> anyhow::Result<()> {
mount_resolution_button(
&mut parser_state,
&doc_params,
par.layout,
id_resolution_buttons,
res,
&tasks,
)
};
mount_res(SkymapResolution::Res2k)?;
if files.size_4k.is_some() {
mount_res(SkymapResolution::Res4k)?;
}
if files.size_8k.is_some() {
mount_res(SkymapResolution::Res8k)?;
}
Ok(Self {
id_parent: par.parent_id,
tasks,
globals: par.globals.clone(),
executor: par.executor.clone(),
entry: par.entry,
parser_state,
})
}
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::ResolutionClicked(skymap_resolution) => todo!(),
}
}
Ok(())
}
}
pub fn mount_popup(
frontend_tasks: FrontendTasks,
executor: AsyncExecutor,
globals: WguiGlobals,
entry: networking::skymap_catalog::SkymapCatalogEntry,
preview_image: Option<CustomGlyphData>,
on_close_request: Box<dyn Fn()>,
set_holder: Box<dyn FnOnce(PopupHolder<View>)>,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text(&entry.name),
Box::new(move |data| {
let view = View::new(Params {
globals: &globals,
layout: data.layout,
executor: &executor,
parent_id: data.id_content,
entry,
on_close_request,
preview_image,
})?;
set_holder((data.handle, view));
Ok(())
}),
)));
}

View File

@ -1,3 +1,5 @@
use std::{cell::RefCell, rc::Rc};
use wgui::{
assets::AssetPath,
globals::WguiGlobals,
@ -28,11 +30,15 @@ pub struct Params<'a> {
pub executor: &'a AsyncExecutor,
pub parent_id: WidgetID,
pub on_close_request: Box<dyn Fn()>,
pub frontend_tasks: FrontendTasks,
}
#[derive(Clone)]
enum Task {
SetSkymapCatalog(anyhow::Result<networking::skymap_catalog::SkymapCatalog>),
SetSkymapCatalog(Rc<anyhow::Result<networking::skymap_catalog::SkymapCatalog>>),
SetSkymapPreview((SkymapUuid, Option<CustomGlyphData>)),
ShowRemoteSkymapDownloader(SkymapUuid),
CloseRemoteSkymapDownloader,
}
struct MountedCell {
@ -40,6 +46,10 @@ struct MountedCell {
view: views::skymap_list_cell::View,
}
pub struct State {
popup_remote_skymap_downloader: Option<PopupHolder<views::remote_skymap_downloader::View>>,
}
pub struct View {
id_parent: WidgetID,
id_list: WidgetID,
@ -48,12 +58,15 @@ pub struct View {
tasks: Tasks<Task>,
mounted_cells: Vec<MountedCell>,
executor: AsyncExecutor,
frontend_tasks: FrontendTasks,
catalog: Option<networking::skymap_catalog::SkymapCatalog>,
state: Rc<RefCell<State>>,
}
impl View {
async fn skymap_catalog_request_wrapper(tasks: Tasks<Task>, executor: AsyncExecutor) {
let res = networking::skymap_catalog::request_catalog(&executor).await;
tasks.push(Task::SetSkymapCatalog(res))
tasks.push(Task::SetSkymapCatalog(Rc::new(res)))
}
pub fn new(par: Params) -> anyhow::Result<Self> {
@ -73,6 +86,11 @@ impl View {
globals: par.globals.clone(),
mounted_cells: Vec::new(),
executor: par.executor.clone(),
frontend_tasks: par.frontend_tasks,
catalog: None,
state: Rc::new(RefCell::new(State {
popup_remote_skymap_downloader: None,
})),
})
}
@ -91,7 +109,7 @@ impl View {
fn mount_catalog(
&mut self,
layout: &mut Layout,
catalog: networking::skymap_catalog::SkymapCatalog,
catalog: &networking::skymap_catalog::SkymapCatalog,
) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams {
globals: self.globals.clone(),
@ -103,7 +121,7 @@ impl View {
let id_list = parser_state.fetch_widget(&layout.state, "list")?.id;
for entry in catalog.entries {
for entry in &catalog.entries {
let task = View::request_skymap_preview(
self.globals.clone(),
self.executor.clone(),
@ -111,27 +129,78 @@ impl View {
self.tasks.clone(),
);
let skymap_uuid = entry.uuid.clone();
self.mounted_cells.push(MountedCell {
skymap_uuid: entry.uuid.clone(),
view: views::skymap_list_cell::View::new(views::skymap_list_cell::Params {
id_parent: id_list,
layout,
entry,
entry: entry.clone(),
on_click: self
.tasks
.get_button_click_callback(Task::ShowRemoteSkymapDownloader(skymap_uuid)),
})?,
});
self.executor.spawn(task).detach();
}
self.catalog = Some(catalog.clone());
Ok(())
}
fn show_remote_skymap_downloader(
&mut self,
uuid: SkymapUuid,
preview_image: Option<CustomGlyphData>,
) -> anyhow::Result<()> {
let Some(catalog) = &self.catalog else {
debug_assert!(false); // impossible
return Ok(());
};
let Some(entry) = catalog.entries.iter().find(|entry| entry.uuid == uuid) else {
debug_assert!(false); // impossible
return Ok(());
};
views::remote_skymap_downloader::mount_popup(
self.frontend_tasks.clone(),
self.executor.clone(),
self.globals.clone(),
entry.clone(),
preview_image,
self.tasks.make_callback_box(Task::CloseRemoteSkymapDownloader),
Box::new({
let state = self.state.clone();
move |popup| state.borrow_mut().popup_remote_skymap_downloader = Some(popup)
}),
);
Ok(())
}
fn get_image_preview(&self, skymap_uuid: SkymapUuid) -> Option<CustomGlyphData> {
if let Some(cell) = &self.mounted_cells.iter().find(|mc| mc.skymap_uuid == skymap_uuid) {
return cell.view.get_image();
}
None
}
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
{
let mut state = self.state.borrow_mut();
if let Some(view) = &mut state.popup_remote_skymap_downloader {
view.1.update(layout)?;
}
}
for task in self.tasks.drain() {
match task {
Task::SetSkymapCatalog(skymap_catalog) => {
layout.remove_widget(self.id_loading);
match skymap_catalog {
match &*skymap_catalog {
Ok(skymap_catalog) => {
self.mount_catalog(layout, skymap_catalog)?;
}
@ -151,6 +220,13 @@ impl View {
cell.view.set_image(layout, glyph_data)?;
}
}
Task::ShowRemoteSkymapDownloader(skymap_uuid) => {
let preview_image = self.get_image_preview(skymap_uuid);
self.show_remote_skymap_downloader(skymap_uuid, preview_image)?;
}
Task::CloseRemoteSkymapDownloader => {
self.state.borrow_mut().popup_remote_skymap_downloader = None;
}
}
}
Ok(())
@ -175,6 +251,7 @@ pub fn mount_popup(
executor: &executor,
parent_id: data.id_content,
on_close_request,
frontend_tasks,
})?;
set_holder((data.handle, view));

View File

@ -1,5 +1,6 @@
use wgui::{
assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton},
event::EventAlterables,
i18n::Translation,
layout::{Layout, WidgetID},
@ -14,6 +15,7 @@ pub struct Params<'a> {
pub id_parent: WidgetID,
pub layout: &'a mut Layout,
pub entry: networking::skymap_catalog::SkymapCatalogEntry,
pub on_click: ButtonClickCallback,
}
pub struct View {
@ -21,6 +23,7 @@ pub struct View {
parser_state: ParserState,
id_loading: WidgetID,
id_image_preview: WidgetID,
image: Option<CustomGlyphData>,
}
impl View {
@ -38,6 +41,10 @@ impl View {
let id_image_preview = data.get_widget_id("image_preview")?;
data
.fetch_component_as::<ComponentButton>("button")?
.on_click(par.on_click);
{
let mut label_title = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_title")?;
let mut label_author = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_author")?;
@ -59,6 +66,7 @@ impl View {
parser_state,
id_loading,
id_image_preview,
image: None,
})
}
@ -67,10 +75,14 @@ impl View {
let mut alt = EventAlterables::default();
{
let mut image_preview = layout.state.widgets.cast_as::<WidgetImage>(self.id_image_preview)?;
image_preview.set_content(&mut alt, content);
image_preview.set_content(&mut alt, content.clone());
}
layout.process_alterables(alt)?;
self.image = content;
Ok(())
}
pub fn get_image(&self) -> Option<CustomGlyphData> {
return self.image.clone();
}
}

View File

@ -1,4 +1,4 @@
use crate::components::button::ComponentButton;
use crate::components::button::{ButtonClickCallback, ComponentButton};
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
pub struct Tasks<TaskType>(Rc<RefCell<VecDeque<TaskType>>>);
@ -30,15 +30,18 @@ impl<TaskType: 'static> Default for Tasks<TaskType> {
}
}
// copyable tasks only!
impl<TaskType: Clone + 'static> Tasks<TaskType> {
pub fn get_button_click_callback(&self, task: TaskType) -> ButtonClickCallback {
let this = self.clone();
Rc::new(move |_, _| {
this.push(task.clone());
Ok(())
})
}
pub fn handle_button(&self, button: &Rc<ComponentButton>, task: TaskType) {
button.on_click({
let this = self.clone();
Rc::new(move |_, _| {
this.push(task.clone());
Ok(())
})
});
button.on_click(self.get_button_click_callback(task));
}
pub fn make_callback_rc(&self, task: TaskType) -> Rc<dyn Fn()> {