diff --git a/dash-frontend/assets/dashboard/trash.svg b/dash-frontend/assets/dashboard/trash.svg new file mode 100644 index 00000000..914fd402 --- /dev/null +++ b/dash-frontend/assets/dashboard/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash-frontend/assets/gui/view/dialog_box.xml b/dash-frontend/assets/gui/view/dialog_box.xml new file mode 100644 index 00000000..0c3f87cf --- /dev/null +++ b/dash-frontend/assets/gui/view/dialog_box.xml @@ -0,0 +1,15 @@ + + + + +
+
+
+
diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index 5ab10456..1bff5c4f 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -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", diff --git a/dash-frontend/src/util/networking/skymap_catalog.rs b/dash-frontend/src/util/networking/skymap_catalog.rs index 5591349b..db1feee6 100644 --- a/dash-frontend/src/util/networking/skymap_catalog.rs +++ b/dash-frontend/src/util/networking/skymap_catalog.rs @@ -32,6 +32,15 @@ impl SkymapResolution { SkymapResolution::Res8k => "8K", } } + + pub fn from_display_str_simple(text: &str) -> Option { + 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)] diff --git a/dash-frontend/src/views/dialog_box.rs b/dash-frontend/src/views/dialog_box.rs new file mode 100644 index 00000000..eba712af --- /dev/null +++ b/dash-frontend/src/views/dialog_box.rs @@ -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, + pub message: Translation, + pub on_action_click: Box, +} + +#[derive(Clone)] +enum Task { + ActionClicked(&'static str), +} + +pub struct View { + tasks: Tasks, + + #[allow(dead_code)] + parser_state: ParserState, + + on_action_click: Option>, + on_close_request: Option>, +} + +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, + par: Params, + ) -> anyhow::Result { + let tasks = Tasks::::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::()? + .set_text(&mut layout.common(), par.message); + } + + for entry in par.entries { + let mut t_par = HashMap::, Rc>::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::("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, 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)) + }), + ))); +} diff --git a/dash-frontend/src/views/mod.rs b/dash-frontend/src/views/mod.rs index e916e5bf..91cdee11 100644 --- a/dash-frontend/src/views/mod.rs +++ b/dash-frontend/src/views/mod.rs @@ -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; diff --git a/dash-frontend/src/views/remote_skymap_downloader.rs b/dash-frontend/src/views/remote_skymap_downloader.rs index 61573c62..84e03350 100644 --- a/dash-frontend/src/views/remote_skymap_downloader.rs +++ b/dash-frontend/src/views/remote_skymap_downloader.rs @@ -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, + popup_dialog_box: PopupHolder, + preview_image_compressed: Rc>, on_updated_library: Rc, } @@ -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( diff --git a/dash-frontend/src/views/skymap_list.rs b/dash-frontend/src/views/skymap_list.rs index cc2bd6ce..66a58635 100644 --- a/dash-frontend/src/views/skymap_list.rs +++ b/dash-frontend/src/views/skymap_list.rs @@ -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, + popup_dialog_box: PopupHolder, cells: Vec, } 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::::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 diff --git a/dash-frontend/src/views/skymap_list_cell.rs b/dash-frontend/src/views/skymap_list_cell.rs index 540a6d39..26bfcce3 100644 --- a/dash-frontend/src/views/skymap_list_cell.rs +++ b/dash-frontend/src/views/skymap_list_cell.rs @@ -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") },