357 lines
No EOL
12 KiB
Rust
357 lines
No EOL
12 KiB
Rust
#![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<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.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<PossibleGames>,
|
|
installed_game_servers: Vec<PossibleGames>,
|
|
db_software_installed: bool,
|
|
}
|
|
|
|
impl From<SavedState> for Box<State> {
|
|
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<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: 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<Message>) {
|
|
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<Message> {
|
|
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<Message> {
|
|
event::listen_with(|event, _status, _| {
|
|
match event {
|
|
Event::Window(window::Event::CloseRequested) => Some(Message::EventOccurred(event)),
|
|
_ => 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::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<Message> = state.get_background_element();
|
|
|
|
stack![bg_element, user_area].into()
|
|
}
|
|
}
|
|
}
|
|
} |