// #![windows_subsystem = "windows"] mod utils; use file_format::FileFormat; use ::image::{DynamicImage, ImageReader}; use iced::{ alignment::Vertical::{Bottom, Top}, border, font::{self, Family, Stretch, Weight}, gradient, mouse::{self, Interaction}, wgpu::naga::back, widget::{button, center, column, container, image, mouse_area, opaque, row, stack, text, Column, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Font, Length::{self, Fill}, Padding, Point, Renderer, Size, Subscription, Task, Theme }; use iced_video_player::{Video, VideoPlayer}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; use utils::{img_utils::round_image, 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, Read, Write}, path::PathBuf, sync::Arc }; 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()) } } #[derive(rust_embed::Embed)] #[folder = "resources"] struct Assets; pub fn main() -> iced::Result { let segoe_assets = Assets::get("segoe-mdl2-assets.ttf").unwrap(); let main_font = Assets::get("QuodlibetSans-Regular.ttf").unwrap(); let icon_file = Assets::get("icon.png").unwrap(); let icon_image = ImageReader::new(Cursor::new(icon_file.data)) .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: true, ..Settings::default() }; iced::application(Launcher::boot, Launcher::update, Launcher::view) .title(Launcher::title) .window(settings) .font(segoe_assets.data) .font(main_font.data) .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 { Loading, 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.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 { installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, } impl From for Box { fn from(val: SavedState) -> Self { Box::new(State { 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 { Loaded(Result), DragStarted(), GameSelected(PossibleGames), Close, Minimize } impl State { // fn path() -> PathBuf { // path.push("launcher-state.json"); // path // } // fn load() -> 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: PossibleGames::WutheringWaves, // installed_games: saved_state.installed_games, // installed_game_servers: saved_state.installed_game_servers, // db_software_installed: saved_state.db_software_installed, // }) // } // async fn save(self) -> Result<(), SaveError> { // let saved_state = SavedState { // installed_games: self.installed_games, // installed_game_servers: self.installed_game_servers, // 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)?; // } // { // fs::write(path, json.as_bytes()).map_err(|_| SaveError::Write)?; // } // sleep(std::time::Duration::from_secs(2)); // Ok(()) // } } impl Launcher { fn boot() -> (Self, Task) { let launcher_bg = get_game_background(&State::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::Loading => match message { Message::Loaded(Ok(save_state)) => { *self = Launcher::Loaded(save_state.into()); Task::none() }, _ => Task::none(), }, Launcher::Loaded(_) => { 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) }) } _ => Task::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::Loading => center(text("Loading...").size(50)).into(), 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: 10.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() } } } }