dash-frontend: Dialog boxes, Skymap removal dialog, Skymap resolution selector dialog

This commit is contained in:
Aleksander 2026-04-16 15:34:20 +02:00 committed by galister
parent c59e195d94
commit 0d8937ff80
9 changed files with 334 additions and 14 deletions

View File

@ -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="currentColor" d="M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zM17 6H7v13h10zM9 17h2V8H9zm4 0h2V8h-2zM7 6v13z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -0,0 +1,15 @@
<layout>
<template name="DialogBoxButton">
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
</template>
<elements>
<div flex_direction="column" align_items="center" justify_content="center" width="100%" gap="32">
<label id="label_message" size="18" weight="bold"/>
<div id="buttons" gap="8">
<!-- filled-in at runtime -->
</div>
</div>
</elements>
</layout>

View File

@ -101,8 +101,10 @@
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
"SCROLL_SPEED": "Scroll speed",
"SELECT_VARIANT": "Select variant",
"SETS_ON_WATCH": "Sets on watch",
"SKYBOX": "Skybox",
"SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.",
"SPACE_DRAG_MULTIPLIER": "Space drag multiplier",
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
@ -144,6 +146,7 @@
"DEBUG_INFO": "Debug info",
"DISPLAY_BRIGHTNESS": "Display brightness",
"DOWNLOADER": "Downloader",
"DOWNLOAD_AGAIN": "Download again",
"DOWNLOADING_FILE": "Downloading file...",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",

View File

@ -32,6 +32,15 @@ impl SkymapResolution {
SkymapResolution::Res8k => "8K",
}
}
pub fn from_display_str_simple(text: &str) -> Option<SkymapResolution> {
match text {
"2K" => Some(SkymapResolution::Res2k),
"4K" => Some(SkymapResolution::Res4k),
"8K" => Some(SkymapResolution::Res8k),
_ => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -69,6 +78,19 @@ impl SkymapCatalogEntryFiles {
Some(format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, filename))
}
pub fn get_preview_path(&self) -> PathBuf {
config_io::get_skymaps_root().join(&self.preview)
}
pub fn save_preview_to_file(&self, data: &[u8]) -> anyhow::Result<()> {
std::fs::write(self.get_preview_path(), data)?;
Ok(())
}
pub fn remove_preview_file(&self) {
let _dont_care = std::fs::remove_file(self.get_preview_path());
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -104,11 +126,29 @@ impl SkymapCatalogEntry {
Ok(std::fs::exists(full_path)?)
}
pub fn has_any_downloaded(&self) -> bool {
self.is_downloaded(SkymapResolution::Res2k).unwrap_or(false)
|| self.is_downloaded(SkymapResolution::Res4k).unwrap_or(false)
|| self.is_downloaded(SkymapResolution::Res8k).unwrap_or(false)
}
pub fn remove_file(&self, resolution: SkymapResolution) {
let Some(full_path) = self.get_destination_path(resolution) else {
return;
};
let _dont_care = std::fs::remove_file(full_path);
}
pub fn save_metadata(&self) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(self.get_destination_metadata_path(), json)?;
Ok(())
}
pub fn remove_metadata(&self) {
let _dont_care = std::fs::remove_file(self.get_destination_metadata_path());
}
}
#[derive(Clone, Debug, Deserialize)]

View File

@ -0,0 +1,127 @@
use std::{collections::HashMap, rc::Rc};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::popup_manager::{MountPopupOnceParams, PopupHolder},
views::{ViewTrait, ViewUpdateParams},
};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
};
pub struct ButtonEntry {
pub content: Translation, // button text
pub icon: &'static str, // sprite_src_builtin
pub action: &'static str, // action name (will be passed into on_action_click)
}
pub struct Params {
pub globals: WguiGlobals,
pub entries: Vec<ButtonEntry>,
pub message: Translation,
pub on_action_click: Box<dyn FnOnce(&'static str)>,
}
#[derive(Clone)]
enum Task {
ActionClicked(&'static str),
}
pub struct View {
tasks: Tasks<Task>,
#[allow(dead_code)]
parser_state: ParserState,
on_action_click: Option<Box<dyn FnOnce(&'static str)>>,
on_close_request: Option<Box<dyn FnOnce()>>,
}
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/dialog_box.xml"),
extra: Default::default(),
}
}
impl ViewTrait for View {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::ActionClicked(action) => {
if let Some(func) = self.on_action_click.take() {
func(action);
}
if let Some(on_close) = self.on_close_request.take() {
on_close();
}
}
}
}
Ok(())
}
}
impl View {
pub fn new(
layout: &mut Layout,
id_parent: WidgetID,
on_close_request: Box<dyn FnOnce()>,
par: Params,
) -> anyhow::Result<Self> {
let tasks = Tasks::<Task>::new();
let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
let id_buttons = parser_state.get_widget_id("buttons")?;
{
let label_message = parser_state.fetch_widget(&layout.state, "label_message")?.widget;
label_message
.cast::<WidgetLabel>()?
.set_text(&mut layout.common(), par.message);
}
for entry in par.entries {
let mut t_par = HashMap::<Rc<str>, Rc<str>>::new();
t_par.insert(Rc::from("icon"), Rc::from(entry.icon));
let data =
parser_state.realize_template(&doc_params(&par.globals), "DialogBoxButton", layout, id_buttons, t_par)?;
let button = data.fetch_component_as::<ComponentButton>("btn")?;
button.set_text(&mut layout.common(), entry.content.clone());
button.on_click(tasks.get_button_click_callback(Task::ActionClicked(entry.action)));
}
Ok(Self {
tasks,
parser_state,
on_action_click: Some(par.on_action_click),
on_close_request: Some(on_close_request),
})
}
}
pub fn mount_popup(popup: PopupHolder<View>, frontend_tasks: FrontendTasks, params: Params) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text("Info"),
Box::new(move |data| {
let on_close_request = popup.get_close_callback(data.layout);
let view = View::new(data.layout, data.id_content, on_close_request, params)?;
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
)));
}

View File

@ -2,6 +2,7 @@ use wlx_common::async_executor::AsyncExecutor;
pub mod app_launcher;
pub mod audio_settings;
pub mod dialog_box;
pub mod download_file;
pub mod game_cover;
pub mod game_launcher;

View File

@ -37,8 +37,10 @@ pub struct Params<'a> {
#[derive(Clone)]
enum Task {
Refresh,
ResolutionClicked(networking::skymap_catalog::SkymapResolution),
ResolutionClicked(SkymapResolution),
DownloadFinished,
RunDownload(SkymapResolution),
RemoveFile(SkymapResolution),
}
pub struct View {
@ -54,6 +56,8 @@ pub struct View {
parser_state: ParserState,
popup_download: PopupHolder<views::download_file::View>,
popup_dialog_box: PopupHolder<views::dialog_box::View>,
preview_image_compressed: Rc<Vec<u8>>,
on_updated_library: Rc<dyn Fn()>,
}
@ -90,8 +94,8 @@ impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::ResolutionClicked(skymap_resolution) => {
self.run_download(skymap_resolution)?;
Task::ResolutionClicked(resolution) => {
self.resolution_clicked(resolution)?;
}
Task::Refresh => {
self.refresh(par.layout)?;
@ -99,10 +103,17 @@ impl ViewTrait for View {
Task::DownloadFinished => {
self.download_finished()?;
}
Task::RunDownload(resolution) => {
self.run_download(resolution)?;
}
Task::RemoveFile(resolution) => {
self.remove_file(resolution)?;
}
}
}
self.popup_download.update(par)?;
self.popup_dialog_box.update(par)?;
Ok(())
}
}
@ -180,6 +191,7 @@ impl View {
parser_state,
frontend_tasks: par.frontend_tasks,
popup_download: Default::default(),
popup_dialog_box: Default::default(),
id_resolution_buttons,
preview_image_compressed: par.preview_image_compressed,
on_updated_library: par.on_updated_library,
@ -212,6 +224,57 @@ impl View {
Ok(())
}
fn resolution_clicked(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
let is_downloaded = self.entry.is_downloaded(resolution).unwrap_or(false);
if !is_downloaded {
self.tasks.push(Task::RunDownload(resolution));
} else {
self.show_dialog_box_action(resolution)?;
}
Ok(())
}
fn show_dialog_box_action(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
const ACTION_REMOVE: &'static str = "remove";
const ACTION_DOWNLOAD_AGAIN: &'static str = "download_again";
let tasks = self.tasks.clone();
views::dialog_box::mount_popup(
self.popup_dialog_box.clone(),
self.frontend_tasks.clone(),
views::dialog_box::Params {
globals: self.globals.clone(),
message: Translation::from_translation_key("APP_SETTINGS.SKYMAP_ALREADY_DOWNLOADED"),
entries: vec![
views::dialog_box::ButtonEntry {
content: Translation::from_translation_key("REMOVE"),
icon: "dashboard/trash.svg",
action: ACTION_REMOVE,
},
views::dialog_box::ButtonEntry {
content: Translation::from_translation_key("DOWNLOAD_AGAIN"),
icon: "dashboard/download.svg",
action: ACTION_DOWNLOAD_AGAIN,
},
],
on_action_click: Box::new(move |action| match action {
ACTION_REMOVE => {
tasks.push(Task::RemoveFile(resolution));
tasks.push(Task::Refresh);
}
ACTION_DOWNLOAD_AGAIN => {
tasks.push(Task::RunDownload(resolution));
tasks.push(Task::Refresh);
}
_ => unreachable!(),
}),
},
);
Ok(())
}
fn download_finished(&mut self) -> anyhow::Result<()> {
self.entry.save_metadata()?;
let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default();
@ -222,8 +285,7 @@ impl View {
config_io::set_skymaps_uuids(&uuids)?;
// Save preview image
let preview_path = config_io::get_skymaps_root().join(&self.entry.files.preview);
std::fs::write(preview_path, self.preview_image_compressed.as_ref())?;
self.entry.files.save_preview_to_file(&self.preview_image_compressed)?;
(*self.on_updated_library)();
@ -253,6 +315,27 @@ impl View {
);
Ok(())
}
fn remove_file(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
self.entry.remove_file(resolution);
if !self.entry.has_any_downloaded() {
// all skymaps of this uuid are removed, clean-up files
self.entry.remove_metadata();
// remove uuid of this entry from downloaded skymaps uuid and save the file again
let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default();
uuids.retain(|uuid| *uuid != self.entry.uuid.to_string());
config_io::set_skymaps_uuids(&uuids)?;
// remove "_preview.dds" files from the disk too
self.entry.files.remove_preview_file();
}
(*self.on_updated_library)();
Ok(())
}
}
pub fn mount_popup(

View File

@ -25,7 +25,8 @@ use crate::{
enum Task {
DownloadSkymaps,
Refresh,
SetSkymap(SkymapCatalogEntry),
ShowSkymapResolutionSelector(SkymapCatalogEntry),
SetSkymap(SkymapCatalogEntry, SkymapResolution),
}
pub struct Params<'a> {
@ -48,12 +49,14 @@ pub struct View {
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
popup_remote_skymap_list: PopupHolder<views::remote_skymap_list::View>,
popup_dialog_box: PopupHolder<views::dialog_box::View>,
cells: Vec<Cell>,
}
impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.popup_remote_skymap_list.update(par)?;
self.popup_dialog_box.update(par)?;
loop {
let tasks = self.tasks.drain();
@ -68,8 +71,11 @@ impl ViewTrait for View {
Task::Refresh => {
self.refresh(&mut par.layout)?;
}
Task::SetSkymap(entry) => {
self.set_skymap(entry)?;
Task::ShowSkymapResolutionSelector(entry) => {
self.show_skymap_resolution_selector(entry);
}
Task::SetSkymap(entry, resolution) => {
self.set_skymap(entry, resolution)?;
}
}
}
@ -110,6 +116,7 @@ impl View {
frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(),
popup_remote_skymap_list: Default::default(),
popup_dialog_box: Default::default(),
cells: Vec::new(),
})
}
@ -125,19 +132,60 @@ impl View {
Ok(())
}
fn set_skymap(&mut self, entry: SkymapCatalogEntry) -> anyhow::Result<()> {
fn set_skymap(&mut self, entry: SkymapCatalogEntry, resolution: SkymapResolution) -> anyhow::Result<()> {
let skymap_file_path = entry
.get_destination_path(SkymapResolution::Res2k /* todo: resolution selector */)
.get_destination_path(resolution)
.context("Skymap not found" /* you shouldn't really see this, like ever. */)?;
log::error!(
"not implemented (skymap path to be loaded: {})",
skymap_file_path.to_string_lossy()
"not implemented (skymap path to be loaded: {} with resolution {:?})",
skymap_file_path.to_string_lossy(),
resolution
);
Ok(())
}
fn show_skymap_resolution_selector(&mut self, entry: SkymapCatalogEntry) {
let tasks = self.tasks.clone();
let mut entries = Vec::<views::dialog_box::ButtonEntry>::new();
let mut add_entry_resolution = |resolution: SkymapResolution| {
if entry.is_downloaded(resolution).unwrap_or(false) {
entries.push(views::dialog_box::ButtonEntry {
content: Translation::from_raw_text(resolution.get_display_str()),
icon: "dashboard/globe.svg",
action: resolution.get_display_str_simple(),
});
}
};
add_entry_resolution(SkymapResolution::Res2k);
add_entry_resolution(SkymapResolution::Res4k);
add_entry_resolution(SkymapResolution::Res8k);
if entries.is_empty() {
// if the skymap is not downloaded, how did we get there?!
debug_assert!(false);
return;
}
views::dialog_box::mount_popup(
self.popup_dialog_box.clone(),
self.frontend_tasks.clone(),
views::dialog_box::Params {
globals: self.globals.clone(),
entries,
message: Translation::from_translation_key("APP_SETTINGS.SELECT_VARIANT"),
on_action_click: Box::new(move |action| {
let resolution = SkymapResolution::from_display_str_simple(action).unwrap(); // safety: never fails.
tasks.push(Task::SetSkymap(entry, resolution))
}),
},
);
}
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
let entries = match skymap_catalog::get_entries_from_disk() {
Ok(entries) => entries,
@ -166,7 +214,9 @@ impl View {
id_parent: self.list_parent,
layout,
entry: entry.clone(),
on_click: self.tasks.get_button_click_callback(Task::SetSkymap(entry.clone())),
on_click: self
.tasks
.get_button_click_callback(Task::ShowSkymapResolutionSelector(entry.clone())),
})?;
// load preview image

View File

@ -59,7 +59,7 @@ fn populate_res_pips(
tpar.insert(
Rc::from("color"),
if downloaded {
Rc::from("#116625")
Rc::from("#11aa40")
} else {
Rc::from("#444444")
},