Several changes

- added `add_action` function to add actions to widgets
- added `MainApp::toast_error` method to show toast with some error
- added `lib::game` mod with `run` function to run the game
- added `lib::consts` mod with constant values like config file path
- added `lib::config` mod to control config file
- added warning subtitle to `Proton-GE` wine version selector
This commit is contained in:
Observer KRypt0n_ 2022-06-30 21:40:25 +02:00
parent 48d354cc4c
commit 1eed56e53a
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
10 changed files with 241 additions and 5 deletions

View file

@ -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"

View file

@ -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 {}
}
}
};

View file

@ -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";

88
src/lib/config.rs Normal file
View file

@ -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<Config, Error> {
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::<Config>(&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<String>
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Wine {
pub prefix: String,
pub executable: String,
pub environment: HashMap<String, String>
}
impl Default for Wine {
fn default() -> Self {
Self {
prefix: String::new(),
executable: String::new(),
environment: HashMap::new()
}
}
}

38
src/lib/consts.rs Normal file
View file

@ -0,0 +1,38 @@
static mut LAUNCHER_DIR: Option<Option<String>> = None;
static mut CONFIG_FILE: Option<Option<String>> = None;
pub fn launcher_dir() -> Option<String> {
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<String> {
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
}
}
}
}

22
src/lib/game.rs Normal file
View file

@ -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(())
}

3
src/lib/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod consts;
pub mod config;
pub mod game;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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<T: IsA<gtk::glib::Object>>(builder: &gtk::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<T: IsA<gtk::Widget>, 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);
}