rooms-launcher/src/main.rs
2025-05-02 21:46:13 +03:00

344 lines
12 KiB
Rust

// #![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<State>),
}
#[derive(Debug)]
enum LauncherBackground {
Video(Video),
Image(DynamicImage),
}
#[derive(Debug, Default)]
struct State {
selected_game: PossibleGames,
installed_games: Vec<PossibleGames>,
installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool,
background: Option<LauncherBackground>,
icon_images: HashMap<PossibleGames, DynamicImage>
}
impl State {
fn get_background_element(&self) -> Element<Message> {
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<Message> {
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<Message> {
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<PossibleGames>,
installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool,
}
impl From<SavedState> for Box<State> {
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<SavedState, LoadError>),
DragStarted(),
GameSelected(PossibleGames),
Close,
Minimize
}
impl State {
// fn path() -> PathBuf {
// path.push("launcher-state.json");
// path
// }
// fn load() -> Result<State, LoadError> {
// 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<Message>) {
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<Message> {
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<Message> {
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<Message> = state.get_background_element();
stack![bg_element, user_area].into()
}
}
}
}