#![windows_subsystem = "windows"] mod utils; mod components; 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 }; 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 std::{ collections::HashMap, env, fs::{self, create_dir_all, read_to_string}, io::{Cursor, Write}, path::PathBuf }; trait InterfaceImage { fn into_handle(self) -> image::Handle; } impl InterfaceImage for DynamicImage { fn into_handle(self) -> image::Handle { image::Handle::from_rgba(self.width(), self.height(), self.to_rgba8().into_raw()) } } pub fn main() -> iced::Result { let segoe_assets = include_bytes!("../resources/segoe-mdl2-assets.ttf"); let main_font = include_bytes!("../resources/QuodlibetSans-Regular.ttf"); let icon_file = include_bytes!("../resources/icon.png"); let icon_image = ImageReader::new(Cursor::new(icon_file)) .with_guessed_format() .unwrap() .decode() .unwrap(); let rgba_vec = icon_image.as_rgba8().unwrap().to_vec(); let settings = Settings { decorations: false, icon: Some(icon::from_rgba(rgba_vec, icon_image.width(), icon_image.height()).unwrap()), size: Size::new(2000.0, 1000.0), maximized: false, fullscreen: false, position: window::Position::Centered, max_size: None, min_size: None, visible: true, resizable: false, transparent: false, level: window::Level::Normal, exit_on_close_request: false, ..Settings::default() }; iced::application(Launcher::boot, Launcher::update, Launcher::view) .subscription(Launcher::subscription) .title(Launcher::title) .window(settings) .font(segoe_assets) .font(main_font) .window_size((1280.0, 760.0)) .run() } #[derive(Debug, PartialEq, Eq, Hash, Clone, EnumIter, Default, Serialize, Deserialize)] enum PossibleGames { #[default] WutheringWaves, HonkaiStarRail, ZenlessZoneZero, GenshinImpact, } #[derive(Debug)] enum Launcher { Loaded(Box), } #[derive(Debug)] enum LauncherBackground { Video(Video), Image(DynamicImage), } #[derive(Debug, Default)] struct State { selected_game: PossibleGames, installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, background: Option, 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_game_icon_row(&self) -> Element { container(row![ self.create_game_icon(&PossibleGames::WutheringWaves), self.create_game_icon(&PossibleGames::HonkaiStarRail), self.create_game_icon(&PossibleGames::ZenlessZoneZero), self.create_game_icon(&PossibleGames::GenshinImpact), ] .spacing(10) ).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)) } 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() } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] struct SavedState { selected_game: PossibleGames, installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, } impl From for Box { fn from(val: SavedState) -> Self { Box::new(State { selected_game: val.selected_game, installed_games: val.installed_games, installed_game_servers: val.installed_game_servers, db_software_installed: val.db_software_installed, ..State::default() }) } } #[derive(Debug, Clone)] enum LoadError { File, Format, } #[derive(Debug, Clone)] enum SaveError { Write, Format, } #[derive(Debug, Clone)] enum Message { EventOccurred(Event), DragStarted(), // GameSelected(PossibleGames), Close, Minimize } impl State { fn path() -> PathBuf { let project_dirs = ProjectDirs::from("com", "RabbyDevs", "rr-launcher").unwrap(); let path = project_dirs.config_dir(); path.join("launcher-state.json").to_path_buf() } fn load(self) -> Result { let contents = read_to_string(Self::path()).map_err(|_| LoadError::File)?; 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 }) } fn save(&mut self) -> Result<(), SaveError> { let saved_state = SavedState { selected_game: self.selected_game.clone(), installed_games: self.installed_games.clone(), installed_game_servers: self.installed_game_servers.clone(), db_software_installed: self.db_software_installed, }; let json = serde_json::to_string_pretty(&saved_state).map_err(|_| SaveError::Format)?; let path = Self::path(); if let Some(dir) = path.parent() { create_dir_all(dir).map_err(|_| SaveError::Write)?; } { let mut file = fs::File::open(path).unwrap(); file.write_all(json.as_bytes()).map_err(|_| SaveError::Write)?; file.flush().map_err(|_| SaveError::Write)?; } Ok(()) } } 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()) } fn title(&self) -> String { format!("RR Launcher v{}", env!("CARGO_PKG_VERSION")) } fn update(&mut self, message: Message) -> Task { match self { Launcher::Loaded(state) => { match message { Message::DragStarted() => { window::get_latest().and_then(move |id: window::Id| { window::drag(id) }) } Message::Close => { window::get_latest().and_then(move |id: window::Id| { window::close(id) }) }, Message::Minimize => { window::get_latest().and_then(move |id: window::Id| { window::minimize(id, true) }) }, Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { state.save().unwrap(); window::get_latest().and_then(window::close) } else { Task::none() } } _ => Task::none() } } } } fn subscription(&self) -> Subscription { event::listen_with(|event, _status, _| { match event { Event::Window(window::Event::CloseRequested) => Some(Message::EventOccurred(event)), _ => None, } }) } fn view(&self) -> Element { let mut font = Font::with_name("Quodlibet Sans"); font.weight = Weight::Normal; font.stretch = Stretch::Normal; let mut bolded_font = Font::with_name("Quodlibet Sans"); bolded_font.weight = Weight::Bold; bolded_font.stretch = Stretch::Normal; println!("Rerender triggered!"); match self { Launcher::Loaded(state) => { let top_bar = stack![ mouse_area(container( stack![ row![ text("Reversed Rooms").size(35).font(font).color(Color::from_rgb8(255, 255, 255)), Space::new(Length::Fill, Length::Fixed(0.0)), row![ opaque(mouse_area(text("\u{E949}").font(Font::with_name("Segoe MDL2 Assets")).size(25)).on_release(Message::Minimize).interaction(Interaction::Pointer)), opaque(mouse_area(text("\u{E106}").font(Font::with_name("Segoe MDL2 Assets")).size(25)).on_release(Message::Close).interaction(Interaction::Pointer)) ].spacing(20) ].align_y(Center).padding(4), state.get_game_icon_row(), ] ) .padding(Padding { top: 5.0, right: 10.0, bottom: 5.0, left: 5.0 }) .align_y(Top) .align_x(Center) .width(Length::Fill) .style(move |_| container::Style { text_color: Color::from_rgba8(255, 255, 255, 1.0).into(), background: Some(Color::from_rgba8(0, 0, 0, 0.75).into()), ..container::Style::default() })) .on_press(Message::DragStarted()) .interaction(Interaction::Grab) ]; let bottom_bar = container(row![ 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 }) .style(move |_, _| { button::Style { text_color: Color::from_rgba8(0, 0, 0, 1.0), background: Some(Color::from_rgba8(255, 255, 255, 1.0).into()), border: border::rounded(5), ..button::Style::default() } })) .interaction(Interaction::Pointer) ) ]) .align_y(Bottom) .width(Length::Fill) .style(move |_theme| style_container(180.0, true)) .padding(20); let user_area = column![top_bar, Space::new(Length::Fill, Length::Fill), bottom_bar] .width(Length::Fill) .height(Length::Fill); let bg_element: Element = state.get_background_element(); stack![bg_element, user_area].into() } } } }