diff --git a/Cargo.toml b/Cargo.toml index ef4f345..bd2e814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,10 @@ build = "build.rs" [dependencies] gtk4 = "0.4" libadwaita = "0.1" + anime-game-core = { path = "anime-game-core", features = ["all"] } + +serde = { version = "1.0", features = ["derive"] } +toml = "0.5.9" + +dirs = "4.0.0" diff --git a/assets/ui/main.blp b/assets/ui/main.blp index 5727e95..f9927d3 100644 --- a/assets/ui/main.blp +++ b/assets/ui/main.blp @@ -18,8 +18,6 @@ Adw.ApplicationWindow window { orientation: vertical; hexpand: true; - name: "main"; - Adw.HeaderBar { title-widget: Adw.WindowTitle { title: "An Anime Game Launcher"; @@ -72,6 +70,8 @@ Adw.ApplicationWindow window { } } } + + Adw.ToastOverlay toast_overlay {} } } }; diff --git a/assets/ui/preferences_general.blp b/assets/ui/preferences_general.blp index 29c9822..9d297de 100644 --- a/assets/ui/preferences_general.blp +++ b/assets/ui/preferences_general.blp @@ -91,6 +91,7 @@ Adw.PreferencesPage general_page { Adw.ExpanderRow { title: "Proton-GE"; + subtitle: "This version includes its own DXVK builds and you can use DXVK_ASYNC variable"; Adw.ActionRow { title: "7-16"; diff --git a/src/lib/config.rs b/src/lib/config.rs new file mode 100644 index 0000000..3049723 --- /dev/null +++ b/src/lib/config.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; +use std::{fs::File, io::Read}; +use std::path::Path; +use std::io::{Error, ErrorKind, Write}; + +use serde::{Serialize, Deserialize}; + +use super::consts::config_file; + +pub fn get() -> Result { + match config_file() { + Some(path) => { + // Try to read config if the file exists + if Path::new(&path).exists() { + let mut file = File::open(path)?; + let mut toml = String::new(); + + file.read_to_string(&mut toml)?; + + match toml::from_str::(&toml) { + Ok(toml) => Ok(toml), + Err(err) => Err(Error::new(ErrorKind::InvalidData, format!("Failed to decode data from toml format: {}", err.to_string()))) + } + } + + // Otherwise create default config file + else { + update(Config::default())?; + + Ok(Config::default()) + } + }, + None => Err(Error::new(ErrorKind::NotFound, format!("Failed to get config file path"))) + } +} + +pub fn update(config: Config) -> Result<(), Error> { + match config_file() { + Some(path) => { + let mut file = File::create(&path)?; + + match toml::to_string(&config) { + Ok(toml) => { + file.write_all(&mut toml.as_bytes())?; + + Ok(()) + }, + Err(err) => Err(Error::new(ErrorKind::InvalidData, format!("Failed to encode data into toml format: {}", err.to_string()))) + } + }, + None => Err(Error::new(ErrorKind::NotFound, format!("Failed to get config file path"))) + } +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Config { + pub paths: Paths, + pub patch: Patch, + pub wine: Wine +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Paths { + pub game: String, + pub patch: String +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Patch { + pub hosts: Vec +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Wine { + pub prefix: String, + pub executable: String, + pub environment: HashMap +} + +impl Default for Wine { + fn default() -> Self { + Self { + prefix: String::new(), + executable: String::new(), + environment: HashMap::new() + } + } +} diff --git a/src/lib/consts.rs b/src/lib/consts.rs new file mode 100644 index 0000000..f2d9895 --- /dev/null +++ b/src/lib/consts.rs @@ -0,0 +1,38 @@ +static mut LAUNCHER_DIR: Option> = None; +static mut CONFIG_FILE: Option> = None; + +pub fn launcher_dir() -> Option { + unsafe { + match &LAUNCHER_DIR { + Some(value) => value.clone(), + None => { + let value = match dirs::data_dir() { + Some(dir) => Some(format!("{}/anime-game-launcher", dir.to_string_lossy())), + None => None + }; + + LAUNCHER_DIR = Some(value.clone()); + + value + } + } + } +} + +pub fn config_file() -> Option { + unsafe { + match &CONFIG_FILE { + Some(value) => value.clone(), + None => { + let value = match launcher_dir() { + Some(dir) => Some(format!("{}/config.toml", dir)), + None => None + }; + + CONFIG_FILE = Some(value.clone()); + + value + } + } + } +} diff --git a/src/lib/game.rs b/src/lib/game.rs new file mode 100644 index 0000000..0ce154d --- /dev/null +++ b/src/lib/game.rs @@ -0,0 +1,22 @@ +use std::io::{Error, ErrorKind}; +use std::path::Path; +use std::process::Command; + +use super::config; + +pub fn run() -> Result<(), Error> { + let config = config::get()?; + + if Path::new(&config.paths.game).exists() { + return Err(Error::new(ErrorKind::Other, "Game is not installed")); + } + + Command::new(config.wine.executable) + .env("WINEPREFIX", &config.wine.prefix) + .envs(config.wine.environment) + .current_dir(config.paths.game) + .arg("launcher.bat") + .output()?; + + Ok(()) +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs new file mode 100644 index 0000000..2608f2c --- /dev/null +++ b/src/lib/mod.rs @@ -0,0 +1,3 @@ +pub mod consts; +pub mod config; +pub mod game; diff --git a/src/main.rs b/src/main.rs index 0abda41..6118e29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; pub mod ui; +pub mod lib; use ui::MainApp; diff --git a/src/ui/main.rs b/src/ui/main.rs index ef8c749..b5a4cd3 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -1,14 +1,18 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; -use super::get_object; +use super::{get_object, add_action}; use super::preferences::PreferencesStack; +use crate::lib::game; + +#[derive(Clone)] pub struct App { pub window: adw::ApplicationWindow, pub leaflet: adw::Leaflet, pub launch_game: adw::SplitButton, - pub open_preferences: gtk::Button + pub open_preferences: gtk::Button, + pub toast_overlay: adw::ToastOverlay } impl App { @@ -21,7 +25,8 @@ impl App { window: get_object(&builder, "window")?, leaflet: get_object(&builder, "leaflet")?, launch_game: get_object(&builder, "launch_game")?, - open_preferences: get_object(&builder, "open_preferences")? + open_preferences: get_object(&builder, "open_preferences")?, + toast_overlay: get_object(&builder, "toast_overlay")? }; // Add preferences page to the leaflet @@ -35,9 +40,50 @@ impl App { leaflet.navigate(adw::NavigationDirection::Back); }); + // Launch game + let app_copy = result.clone(); + + result.launch_game.connect_clicked(move |_| { + // Display toast message if the game is failed to run + if let Err(err) = game::run() { + app_copy.toast_error("Failed to run game", err); + } + }); + // Bind app to the window result.window.set_application(Some(app)); Ok(result) } + + /// Show toast with `toast` title and `See message` button + /// + /// This button will show message dialog with error message + pub fn toast_error(&self, toast: &str, err: std::io::Error) { + let toast = adw::Toast::new(toast); + + toast.set_button_label(Some("See message")); + toast.set_action_name(Some("see-message.see-message")); + + let window_copy = self.window.clone(); + + // Show error message in a dialog window + add_action(&self.toast_overlay, "see-message", move || { + let dialog = gtk::MessageDialog::new( + Some(&window_copy), + gtk::DialogFlags::all(), + gtk::MessageType::Info, + gtk::ButtonsType::Close, + &err.to_string() + ); + + dialog.connect_response(move |dialog, _| { + dialog.close(); + }); + + dialog.show(); + }); + + self.toast_overlay.add_toast(&toast); + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 83dba12..a67d792 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,5 @@ use gtk4::{self as gtk, prelude::*}; +use libadwaita::{self as adw, prelude::*}; mod main; mod preferences; @@ -12,3 +13,33 @@ pub fn get_object>(builder: >k::Builder, name: &str) None => Err(format!("Failed to parse object '{}'", name)) } } + +/// Add action to widget +/// +/// All the actions needs to be in some group. This function creates new group with the name of the action. +/// This means that to add action to some widget you need to speify `name.name` as its name +/// +/// ## Example: +/// +/// ``` +/// let toast = libadwaita::Toast::new("Example toast"); +/// +/// toast.set_button_label(Some("Example button")); +/// toast.set_action_name(Some("example-button.example-button")); +/// +/// add_action(&toast, "example-button", || { +/// println!("Hello, World!"); +/// }); +/// ``` +pub fn add_action, F: Fn() + 'static>(obj: &T, name: &str, closure: F) { + let action_group = adw::gio::SimpleActionGroup::new(); + let action = adw::gio::SimpleAction::new(name, None); + + obj.insert_action_group(name, Some(&action_group)); + + action.connect_activate(move |_, _| { + closure(); + }); + + action_group.add_action(&action); +}