From 5239d2cd98d94540be168368897b1cb21fb07e8a Mon Sep 17 00:00:00 2001 From: RabbyDevs <67389402+RabbyDevs@users.noreply.github.com> Date: Sun, 4 May 2025 17:29:00 +0300 Subject: [PATCH] 0.4.0: Add game switching, implement proper error handling thru out installer, and splash screen, probably more. --- Cargo.lock | 2 +- Cargo.toml | 4 +- iced_video_player/src/video.rs | 4 +- src/components/installer.rs | 79 ++++++++----- src/main.rs | 203 +++++++++++++++++++++++---------- src/utils/visual_helper.rs | 43 ++++++- 6 files changed, 238 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16589c9..b135862 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3155,7 +3155,7 @@ dependencies = [ [[package]] name = "reversed-rooms-launcher" -version = "0.3.0" +version = "0.4.0" dependencies = [ "directories", "file-format", diff --git a/Cargo.toml b/Cargo.toml index bf3f6de..7a30cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ #![feature(let_chains)] [package] name = "reversed-rooms-launcher" -version = "0.3.0" +version = "0.4.0" edition = "2024" [dependencies] @@ -22,4 +22,4 @@ reqwest = { version = "0.12.15", features = ["blocking", "gzip", "json"] } strip = true # Automatically strip symbols from the binary. lto = true # Link-time optimization. opt-level = 3 # Optimize for speed. -codegen-units = 1 # Maximum size reduction optimizations. \ No newline at end of file +codegen-units = 1 # Maximum size reduction optimizations. diff --git a/iced_video_player/src/video.rs b/iced_video_player/src/video.rs index f9a6071..dc349d0 100644 --- a/iced_video_player/src/video.rs +++ b/iced_video_player/src/video.rs @@ -469,8 +469,8 @@ impl Video { } /// Get the size/resolution of the video as `(width, height)`. - pub fn size(&self) -> (i32, i32) { - (self.read().width, self.read().height) + pub fn size(&self) -> (u32, u32) { + (self.read().width.try_into().unwrap(), self.read().height.try_into().unwrap()) } /// Get the framerate of the video as frames per second. diff --git a/src/components/installer.rs b/src/components/installer.rs index baac838..2b79c2a 100644 --- a/src/components/installer.rs +++ b/src/components/installer.rs @@ -1,5 +1,6 @@ +use std::path::PathBuf; use std::{fs::create_dir_all, io::Write, path::Path}; -use std::fs::OpenOptions; +use std::fs::{DirEntry, OpenOptions}; use directories::ProjectDirs; use reqwest::blocking::Client; @@ -150,42 +151,61 @@ fn refresh_kuro_install(proj_dirs: &ProjectDirs, client: &Client) -> Result<(), } fn update_file_if_needed(dir: &Path, client: &Client, file_url: &str, file_type: &str) -> Result<(), String> { + let current_file: Option = { + let mut current_file = None; + for path in dir.read_dir().unwrap() { + let path = path.unwrap(); + if path.file_name().into_string().unwrap().starts_with(format!("{}_", file_type).as_str()) { + current_file = Some(path); + } + } + current_file + }; + let filename = extract_filename_from_url(file_url); let expected_file = format!("{}_{}", file_type, filename); let file_path = dir.join(&expected_file); - - let file_exists = file_path.exists(); - - if !file_exists { - eprintln!("Downloading {} file from: {}", file_type, file_url); - let file_bytes = client - .get(file_url) - .send() - .map_err(|e| format!("Failed to send request for {} file: {}", file_type, e))? - .bytes() - .map_err(|e| format!("Failed to get bytes for {} file: {}", file_type, e))?; - - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&file_path) - .map_err(|e| format!("Failed to create {} file ({}): {}", file_type, file_path.display(), e))?; - - file.write_all(&file_bytes) - .map_err(|e| format!("Failed to write {} file: {}", file_type, e))?; - - file.flush() - .map_err(|e| format!("Failed to flush {} file: {}", file_type, e))?; - - eprintln!("Successfully downloaded {} file to: {}", file_type, file_path.display()); + + if let Some(file) = current_file { + if filename != file.file_name().into_string().unwrap().strip_prefix(format!("{}_", file_type).as_str()).unwrap() { + update_file(client, file_path, file_type, file_url)?; + } else { + eprintln!("{} file already exists at: {}", file_type, file.path().display()); + } } else { - eprintln!("{} file already exists at: {}", file_type, file_path.display()); + update_file(client, file_path, file_type, file_url)?; } Ok(()) } +fn update_file(client: &Client, file_path: PathBuf, file_type: &str, file_url: &str) -> Result<(), String> { + eprintln!("Downloading {} file from: {}", file_type, file_url); + let file_bytes = client + .get(file_url) + .send() + .map_err(|e| format!("Failed to send request for {} file: {}", file_type, e))? + .bytes() + .map_err(|e| format!("Failed to get bytes for {} file: {}", file_type, e))?; + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&file_path) + .map_err(|e| format!("Failed to create {} file ({}): {}", file_type, file_path.display(), e))?; + + file.write_all(&file_bytes) + .map_err(|e| format!("Failed to write {} file: {}", file_type, e))?; + + file.flush() + .map_err(|e| format!("Failed to flush {} file: {}", file_type, e))?; + + eprintln!("Successfully downloaded {} file to: {}", file_type, file_path.display()); + + Ok(()) +} + fn refresh_hoyo_install(proj_dirs: &ProjectDirs, client: &Client) -> Result<(), String> { let hoyo_url = "https://sg-hyp-api.hoyoverse.com/hyp/hyp-connect/api/getGames?launcher_id=VYTpXlbWo8&language=en-us"; @@ -227,6 +247,9 @@ fn refresh_hoyo_install(proj_dirs: &ProjectDirs, client: &Client) -> Result<(), let icon_url = &game.display.icon.url; update_file_if_needed(data_dir, client, icon_url, "icon")?; + + let bg_url = &game.display.background.url; + update_file_if_needed(data_dir, client, bg_url, "background")?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index 8c22ee0..01ca103 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -// TODO: fix loading the save data lol -#![windows_subsystem = "windows"] +// #![windows_subsystem = "windows"] mod utils; mod components; @@ -7,13 +6,13 @@ use components::installer::refresh_install; use directories::ProjectDirs; use ::image::{DynamicImage, ImageReader}; use iced::{ - alignment::Vertical::{Bottom, Top}, border, event, font::{Stretch, Weight}, mouse::Interaction, widget::{button, column, container, image, mouse_area, opaque, row, stack, text, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Event, Font, Length, Padding, Size, Subscription, Task + alignment::Vertical::{Bottom, Top}, border, event, font::{Stretch, Weight}, mouse::Interaction, widget::{button, center, column, container, image, mouse_area, opaque, row, stack, text, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Event, Font, Length, Padding, Size, Subscription, Task }; use iced_video_player::{Video, VideoPlayer}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; -use utils::visual_helper::{get_game_background, get_game_icon_dynamic_image, style_container}; +use utils::visual_helper::{get_game_background, get_game_icon_dynamic_image, get_game_splash_dynamic_image, style_container}; use std::{ collections::HashMap, env, fs::{self, create_dir_all, read_to_string}, io::{Cursor, Write}, path::PathBuf }; @@ -77,6 +76,21 @@ enum PossibleGames { GenshinImpact, } +trait IdentifibleGameType { + fn get_game_preferred_size(&self) -> (u32, u32); +} + +impl IdentifibleGameType for PossibleGames { + fn get_game_preferred_size(&self) -> (u32, u32) { + match self { + PossibleGames::WutheringWaves => (1280, 760), + PossibleGames::HonkaiStarRail => (1280, 720), + PossibleGames::ZenlessZoneZero => (1280, 720), + PossibleGames::GenshinImpact => (1280, 720), + } + } +} + #[derive(Debug)] enum Launcher { Loaded(Box), @@ -95,17 +109,21 @@ struct State { installed_game_servers: Vec, db_software_installed: bool, background: Option, + splash_images: HashMap, icon_images: HashMap } impl State { - fn get_background_element(&self) -> Element { - match self.background.as_ref().unwrap() { - LauncherBackground::Video(video) => VideoPlayer::new(video).into(), - LauncherBackground::Image(img) => image(image::Handle::from_rgba(img.width(), img.height(), img.to_rgba8().into_raw())).into(), + fn get_background_element(&self) -> Option> { + if let Some(background) = self.background.as_ref() { + match background { + LauncherBackground::Video(video) => Some(VideoPlayer::new(video).into()), + LauncherBackground::Image(img) => Some(image(image::Handle::from_rgba(img.width(), img.height(), img.to_rgba8().into_raw())).into()), + } + } else { + None } } - fn get_game_icon_row(&self) -> Element { container(row![ self.create_game_icon(&PossibleGames::WutheringWaves), @@ -117,23 +135,36 @@ impl State { ).align_x(Center).width(Length::Fill).into() } fn create_game_icon(&self, game: &PossibleGames) -> Element { - let icon = self.icon_images.get(game).unwrap(); - let img = if &self.selected_game == game { - image(icon.clone().into_handle()) - .width(Length::Fixed(52.0)) - .height(Length::Fixed(68.0)) + if let Some(icon) = self.icon_images.get(game) { + let img = if &self.selected_game == game { + image(icon.clone().into_handle()) + .width(Length::Fixed(52.0)) + .height(Length::Fixed(68.0)) + } else { + image(icon.brighten(-85).blur(2.5).adjust_contrast(-15.0).into_handle()) + .width(Length::Fixed(48.0)) + .height(Length::Fixed(64.0)) + }; + + mouse_area( + img + ) + .on_press(Message::GameSelected(game.clone())) + .interaction(Interaction::Pointer) + .into() } else { - image(icon.brighten(-85).blur(2.5).adjust_contrast(-15.0).into_handle()) - .width(Length::Fixed(48.0)) - .height(Length::Fixed(64.0)) - }; - - mouse_area( - img - ) - // .on_press(Message::GameSelected(game.clone())) - .interaction(Interaction::Pointer) - .into() + text("loading").size(10).into() + } + } + fn get_splash(&self) -> Option> { + if let Some(splash) = self.splash_images.get(&self.selected_game) { + let preferred_size = self.selected_game.get_game_preferred_size(); + Some(image(splash.clone().into_handle()) + .width(Length::Fixed(preferred_size.0 as f32)) + .height(Length::Fixed(preferred_size.1 as f32)).into()) + } else { + None + } } } @@ -153,7 +184,6 @@ impl From for Box { #[derive(Debug, Clone)] enum LoadError { - File, Format, } @@ -166,8 +196,12 @@ enum SaveError { #[derive(Debug, Clone)] enum Message { EventOccurred(Event), - DragStarted(), - // GameSelected(PossibleGames), + DragWindow, + LoadIcons(HashMap), + LoadSplashes(HashMap), + LoadBackground(()), + RefreshInstall(()), + GameSelected(PossibleGames), Close, Minimize } @@ -181,19 +215,21 @@ impl State { } fn load(self) -> Result { - let contents = read_to_string(Self::path()).map_err(|_| LoadError::File)?; + if let Ok(contents) = read_to_string(Self::path()) { + let saved_state: SavedState = serde_json::from_str(&contents).map_err(|_| LoadError::Format)?; - let saved_state: SavedState = - serde_json::from_str(&contents).map_err(|_| LoadError::Format)?; - - Ok(State { - selected_game: saved_state.selected_game, - installed_games: saved_state.installed_games, - installed_game_servers: saved_state.installed_game_servers, - db_software_installed: saved_state.db_software_installed, - background: self.background, - icon_images: self.icon_images - }) + Ok(State { + selected_game: saved_state.selected_game, + installed_games: saved_state.installed_games, + installed_game_servers: saved_state.installed_game_servers, + db_software_installed: saved_state.db_software_installed, + background: self.background, + splash_images: self.splash_images, + icon_images: self.icon_images, + }) + } else { + Ok(self) + } } fn save(&mut self) -> Result<(), SaveError> { @@ -222,22 +258,29 @@ impl State { } } +async fn get_icons() -> HashMap { + let mut icons: HashMap = HashMap::new(); + for game in PossibleGames::iter() { + let icon = get_game_icon_dynamic_image(&game); + icons.insert(game, icon); + }; + icons +} + +async fn get_splashes() -> HashMap { + let mut splashes: HashMap = HashMap::new(); + for game in PossibleGames::iter() { + let splash = get_game_splash_dynamic_image(&game); + if let Some(splash) = splash {splashes.insert(game, splash)} else {continue}; + }; + splashes +} + +async fn empty() {} + impl Launcher { fn boot() -> (Self, Task) { - refresh_install().unwrap(); - let launcher_bg = get_game_background(&PossibleGames::default()); - let mut icons = HashMap::new(); - for game in PossibleGames::iter() { - let icon = get_game_icon_dynamic_image(&game); - icons.insert(game, icon); - } - - let final_state = State { - background: Some(launcher_bg), - icon_images: icons, - ..State::default() - }; - (Self::Loaded(Box::new(final_state)), Task::none()) + (Self::Loaded(Box::new(State::default().load().unwrap())), Task::batch([Task::perform(empty(), Message::LoadBackground), Task::perform(get_icons(), Message::LoadIcons), Task::perform(get_splashes(), Message::LoadSplashes), Task::perform(empty(), Message::RefreshInstall)])) } fn title(&self) -> String { @@ -248,7 +291,7 @@ impl Launcher { match self { Launcher::Loaded(state) => { match message { - Message::DragStarted() => { + Message::DragWindow => { window::get_latest().and_then(move |id: window::Id| { window::drag(id) }) @@ -270,8 +313,34 @@ impl Launcher { } else { Task::none() } - } - _ => Task::none() + }, + Message::LoadSplashes(splashes) => { + state.splash_images = splashes; + Task::none() + }, + Message::LoadBackground(()) => { + state.background = Some(get_game_background(&PossibleGames::default())); + let (width, height) = state.selected_game.get_game_preferred_size(); + window::get_latest().and_then(move |id: window::Id| { + window::resize(id, Size { width: width as f32, height: height as f32 }) + }) + }, + Message::LoadIcons(icons) => { + state.icon_images = icons; + Task::none() + }, + Message::RefreshInstall(_result) => { + refresh_install().unwrap(); + Task::none() + }, + Message::GameSelected(game) => { + state.background = Some(get_game_background(&game)); + state.selected_game = game; + let (width, height) = state.selected_game.get_game_preferred_size(); + window::get_latest().and_then(move |id: window::Id| { + window::resize(id, Size { width: width as f32, height: height as f32 }) + }) + }, } } } @@ -321,7 +390,7 @@ impl Launcher { background: Some(Color::from_rgba8(0, 0, 0, 0.75).into()), ..container::Style::default() })) - .on_press(Message::DragStarted()) + .on_press(Message::DragWindow) .interaction(Interaction::Grab) ]; @@ -329,7 +398,7 @@ impl Launcher { text("The quick brown fox jumped over the lazy dog.").size(25).font(font), Space::new(Length::Fill, Length::Fixed(0.0)), opaque(mouse_area(button(text("Start").size(25).font(bolded_font).align_x(Center)) - .padding(Padding { top: 10.0, right: 70.0, bottom: 10.0, left: 70.0 }) + .padding(Padding { top: 10.0, right: 70.0, bottom: 10.0, left: 70.0 }) .style(move |_, _| { button::Style { text_color: Color::from_rgba8(0, 0, 0, 1.0), @@ -350,9 +419,21 @@ impl Launcher { .width(Length::Fill) .height(Length::Fill); - let bg_element: Element = state.get_background_element(); + let bg_element = if let Some(bg) = state.get_background_element() { + bg + } else { + center(text("Loading...").size(50)).style(|_| {container::Style { text_color: Some(Color::from_rgba8(255, 255, 255, 1.0)), background: Some(Color::from_rgba8(0, 0, 0, 1.0).into()), ..container::Style::default() }}).into() + }; - stack![bg_element, user_area].into() + let mut final_stack = stack![bg_element]; + + if let Some(splash) = state.get_splash() { + final_stack = final_stack.push(splash); + } + + final_stack = final_stack.push(user_area); + + final_stack.into() } } } diff --git a/src/utils/visual_helper.rs b/src/utils/visual_helper.rs index 0cec164..d34c660 100644 --- a/src/utils/visual_helper.rs +++ b/src/utils/visual_helper.rs @@ -44,9 +44,9 @@ pub fn get_game_background(game: &PossibleGames) -> LauncherBackground { fn get_background_file(proj_dirs: &ProjectDirs, game: &PossibleGames) -> Result, std::io::Error> { let game_dir = match game { PossibleGames::WutheringWaves => proj_dirs.data_dir().join("kuro/wuwa"), - PossibleGames::ZenlessZoneZero => proj_dirs.data_dir().join("zzz"), - PossibleGames::HonkaiStarRail => proj_dirs.data_dir().join("hsr"), - PossibleGames::GenshinImpact => proj_dirs.data_dir().join("gi"), + PossibleGames::ZenlessZoneZero => proj_dirs.data_dir().join("hoyoverse/zzz"), + PossibleGames::HonkaiStarRail => proj_dirs.data_dir().join("hoyoverse/hsr"), + PossibleGames::GenshinImpact => proj_dirs.data_dir().join("hoyoverse/gi"), }; if !game_dir.exists() { @@ -73,6 +73,20 @@ fn get_background_file(proj_dirs: &ProjectDirs, game: &PossibleGames) -> Result< )) } +pub fn get_game_splash_dynamic_image(game: &PossibleGames) -> Option { + let proj_dirs = ProjectDirs::from("com", "RabbyDevs", "rr-launcher").unwrap(); + let file_data = get_splash_file(&proj_dirs, game); + if let Some(data) = file_data { + let data_cursor = Cursor::new(data); + + Some(ImageReader::new(data_cursor) + .with_guessed_format() + .unwrap() + .decode() + .unwrap()) + } else {None} +} + pub fn get_game_icon_dynamic_image(game: &PossibleGames) -> DynamicImage { let proj_dirs = ProjectDirs::from("com", "RabbyDevs", "rr-launcher").unwrap(); let file_data: &[u8] = match game { @@ -118,6 +132,29 @@ fn get_hoyo_game_icon_file(proj_dirs: &ProjectDirs, game: &PossibleGames) -> Vec std::fs::read(icon.expect("installation went wrong.").path()).unwrap() } +fn get_splash_file(proj_dirs: &ProjectDirs, game: &PossibleGames) -> Option> { + let game_path = match game { + PossibleGames::ZenlessZoneZero => "hoyoverse/zzz", + PossibleGames::HonkaiStarRail => "hoyoverse/hsr", + PossibleGames::GenshinImpact => "hoyoverse/gi", + PossibleGames::WutheringWaves => "kuro/wuwa" + }; + + let data_dir = proj_dirs.data_dir().join(game_path); + let icon: Option = { + let mut icon = None; + for path in data_dir.read_dir().unwrap() { + let path = path.unwrap(); + if path.file_name().into_string().unwrap().starts_with("splash_") { + icon = Some(path); + } + } + icon + }; + + icon.map(|icon| std::fs::read(icon.path()).unwrap()) +} + fn rad(deg: f32) -> f32 { deg * std::f32::consts::PI / 180.0 }