dash-frontend: Skymap catalog fetch

This commit is contained in:
Aleksander 2026-04-04 21:50:35 +02:00 committed by galister
parent 1d8b22d992
commit 7f822b6787
12 changed files with 273 additions and 11 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="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -0,0 +1,11 @@
<layout>
<elements>
<div flex_direction="column">
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
</div>
<div flex_direction="row" gap="4">
<Button id="btn_refresh" tooltip="REFRESH" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
<Button id="btn_download_skymaps" height="32" translation="APP_SETTINGS.DOWNLOAD_SKYMAPS" sprite_src_builtin="dashboard/download.svg"/>
</div>
</elements>
</layout>

View File

@ -53,6 +53,7 @@
"DELETE_ALL_CONFIGS_HELP": "Remove all configuration files from conf.d",
"DOUBLE_CURSOR_FIX": "Double cursor fix",
"DOUBLE_CURSOR_FIX_HELP": "Enable this if you see 2 cursors",
"DOWNLOAD_SKYMAPS": "Download skymaps",
"FEATURES": "Features",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch",
"HANDSFREE_POINTER": "Handsfree mode",
@ -72,6 +73,7 @@
"LONG_PRESS_DURATION": "Long press duration",
"LOOK_AND_FEEL": "Look & Feel",
"MISC": "Miscellaneous",
"NO_SKYMAPS_FOUND": "No skymaps found",
"NOTIFICATIONS_ENABLED": "Enable notifications",
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"OPAQUE_BACKGROUND": "Opaque background",

View File

@ -20,7 +20,9 @@ use wgui::{
},
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
};
use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode};
use wlx_common::{
async_executor::AsyncExecutor, config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode,
};
use crate::{
frontend::{Frontend, FrontendTask},
@ -78,12 +80,31 @@ enum Task {
SetTab(TabNameEnum),
}
struct SettingsMountParams<'a> {
mp: &'a mut MacroParams<'a>,
globals: &'a WguiGlobals,
parent_id: WidgetID,
}
struct SettingsUpdateParams<'a> {
layout: &'a mut Layout,
executor: &'a AsyncExecutor,
}
trait SettingsTab {
fn update(&mut self, _par: SettingsUpdateParams) -> anyhow::Result<()> {
Ok(())
}
}
pub struct TabSettings<T> {
pub state: ParserState,
app_button_ids: Vec<Rc<str>>,
context_menu: ContextMenu,
current_tab: Option<Box<dyn SettingsTab>>,
tasks: Tasks<Task>,
marker: PhantomData<T>,
}
@ -94,6 +115,13 @@ impl<T> Tab<T> for TabSettings<T> {
}
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
if let Some(tab) = &mut self.current_tab {
tab.update(SettingsUpdateParams {
layout: &mut frontend.layout,
executor: &frontend.executor,
})?;
}
let mut changed = false;
for task in self.tasks.drain() {
match task {
@ -507,6 +535,7 @@ impl<T> TabSettings<T> {
let root = self.state.get_widget_id("settings_root")?;
frontend.layout.remove_children(root);
let globals = frontend.layout.state.globals.clone();
self.current_tab = None;
let mut mp = MacroParams {
layout: &mut frontend.layout,
@ -537,7 +566,11 @@ impl<T> TabSettings<T> {
tab_troubleshooting::mount(&mut mp, root)?;
}
TabNameEnum::Skybox => {
tab_skybox::mount(&mut mp, root)?;
self.current_tab = Some(Box::new(tab_skybox::State::mount(SettingsMountParams {
mp: &mut mp,
globals: &globals,
parent_id: root,
})?));
}
}
@ -572,6 +605,7 @@ impl<T> TabSettings<T> {
state: parser_state,
marker: PhantomData,
context_menu: ContextMenu::default(),
current_tab: None,
})
}
}

View File

@ -1,12 +1,34 @@
use crate::tab::settings::{
SettingType,
macros::{MacroParams, options_category, options_checkbox},
use crate::{
tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox},
},
views::skymap_list,
};
use wgui::layout::WidgetID;
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
let c = options_category(mp, parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
options_checkbox(mp, c, SettingType::UseSkybox)?;
options_checkbox(mp, c, SettingType::OpaqueBackground)?;
Ok(())
pub struct State {
skymap_list: skymap_list::View,
}
impl SettingsTab for State {
fn update(&mut self, par: super::SettingsUpdateParams) -> anyhow::Result<()> {
self.skymap_list.update(par.layout, par.executor)?;
Ok(())
}
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
let c = options_category(par.mp, par.parent_id, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
options_checkbox(par.mp, c, SettingType::UseSkybox)?;
options_checkbox(par.mp, c, SettingType::OpaqueBackground)?;
let skymap_list = skymap_list::View::new(skymap_list::Params {
globals: par.globals.clone(),
layout: par.mp.layout,
parent_id: c,
})?;
Ok(Self { skymap_list })
}
}

View File

@ -18,6 +18,16 @@ pub struct HttpClientResponse {
pub data: Vec<u8>,
}
impl HttpClientResponse {
pub fn as_json<T>(self) -> anyhow::Result<T>
where
T: for<'a> serde::Deserialize<'a>,
{
let utf8 = str::from_utf8(&self.data)?;
Ok(serde_json::from_str::<T>(utf8)?)
}
}
pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
log::info!("fetching URL \"{}\"", url);

View File

@ -1,5 +1,6 @@
pub mod cached_fetcher;
pub mod http_client;
pub mod networking;
pub mod pactl_wrapper;
pub mod popup_manager;
pub mod steam_utils;

View File

@ -0,0 +1,4 @@
pub mod skymap_catalog;
// pub const WAYVR_ROOT_URL: &'static str = "https://wayvr.org";
pub const WAYVR_SKYMAPS_ROOT: &'static str = "https://wayvr.org/skymaps";

View File

@ -0,0 +1,42 @@
#![allow(dead_code)] // TODO: Remove later
use serde::Deserialize;
use wlx_common::async_executor::AsyncExecutor;
use crate::util::{http_client, networking};
#[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 preview: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SkymapCatalogEntry {
pub uuid: String,
pub created_at: String,
pub modified_at: String,
pub entry_version: u32,
pub name: String,
pub description: String,
pub author: String,
pub files: SkymapCatalogEntryFiles,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SkymapCatalog {
pub version: u32,
pub r#type: String,
pub entries: Vec<SkymapCatalogEntry>,
}
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 catalog = res.as_json::<SkymapCatalog>()?;
Ok(catalog)
}

View File

@ -4,3 +4,4 @@ pub mod game_cover;
pub mod game_launcher;
pub mod game_list;
pub mod running_games_list;
pub mod skymap_list;

View File

@ -0,0 +1,117 @@
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::{async_executor::AsyncExecutor, config_io};
use crate::util::{networking, wgui_simple};
#[derive(Clone)]
enum Task {
DownloadSkymaps,
SetSkymapCatalog(Result<networking::skymap_catalog::SkymapCatalog, String>),
Refresh,
}
pub struct Params<'a> {
pub globals: WguiGlobals,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
}
pub struct View {
#[allow(dead_code)]
parser_state: ParserState,
tasks: Tasks<Task>,
list_parent: WidgetID,
}
impl View {
pub fn new(params: Params) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/skymap_list.xml"),
extra: Default::default(),
};
let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?.id;
let tasks = Tasks::new();
tasks.push(Task::Refresh);
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_download_skymaps")?,
Task::DownloadSkymaps,
);
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_refresh")?,
Task::Refresh,
);
Ok(Self {
parser_state,
tasks,
list_parent,
})
}
pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::DownloadSkymaps => {
self.download_skymaps(executor)?;
}
Task::Refresh => {
self.refresh(layout)?;
}
Task::SetSkymapCatalog(skymap_catalog) => {
log::info!("{:?}", skymap_catalog);
}
}
}
}
Ok(())
}
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.map_err(|e| format!("{}", e))));
}
fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> {
let fut = View::skymap_catalog_request_wrapper(self.tasks.clone(), executor.clone());
executor.spawn(fut).detach();
Ok(())
}
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
let skymaps_uuids = config_io::get_skymaps_uuids().unwrap_or_default();
log::info!("skymap uuids {:?}", skymaps_uuids);
layout.remove_children(self.list_parent);
if skymaps_uuids.is_empty() {
wgui_simple::create_label(
layout,
self.list_parent,
Translation::from_translation_key("APP_SETTINGS.NO_SKYMAPS_FOUND"),
)?;
return Ok(());
}
Ok(())
}
}

View File

@ -21,6 +21,23 @@ pub fn get_config_root() -> PathBuf {
CONFIG_ROOT_PATH.clone()
}
pub fn get_skymaps_root() -> PathBuf {
get_config_root().join("skymaps")
}
pub fn get_skymaps_uuids() -> anyhow::Result<Vec<String>> {
let data = std::fs::read_to_string(get_skymaps_root().join("skymaps.txt"))?;
Ok(data.lines().filter(|line| !line.is_empty()).map(String::from).collect())
}
pub fn set_skymaps_uuids(uuids: &[String]) -> anyhow::Result<()> {
let skymaps_root = get_skymaps_root();
let _ = std::fs::create_dir_all(&skymaps_root);
let data = String::from_iter(uuids.iter().map(|uuid| format!("{}\n", uuid)));
std::fs::write(skymaps_root.join("skymaps.txt"), data)?;
Ok(())
}
impl ConfigRoot {
pub fn get_conf_d_path(&self) -> PathBuf {
get_config_root().join(match self {