diff --git a/assets/ui/first_run.blp b/assets/ui/first_run.blp new file mode 100644 index 0000000..7466bdb --- /dev/null +++ b/assets/ui/first_run.blp @@ -0,0 +1,265 @@ +using Gtk 4.0; +using Adw 1; + +Adw.ApplicationWindow window { + default-width: 780; + default-height: 560; + + content: Gtk.Box { + orientation: vertical; + + Adw.HeaderBar { + title-widget: Adw.WindowTitle { + title: "An Anime Game Launcher"; + }; + } + + Adw.Carousel carousel { + allow-mouse-drag: false; + + // First page (welcome message) + + Gtk.Box first_page { + orientation: vertical; + hexpand: true; + + Adw.PreferencesPage { + Adw.PreferencesGroup { + Gtk.Image { + resource: "/org/app/assets/images/icon.png"; + + vexpand: true; + margin-top: 16; + } + + Gtk.Label { + label: "An Anime Game Launcher"; + margin-top: 32; + + styles ["title-1"] + } + + Gtk.Label { + label: "Hi there! Welcome to the An Anime Game Launcher. We need to prepare some stuff and download default components before you could run the game"; + + wrap: true; + justify: center; + margin-top: 32; + } + } + + Adw.PreferencesGroup { + vexpand: true; + valign: center; + + Gtk.Box { + orientation: horizontal; + spacing: 8; + halign: center; + + Gtk.Button first_page_continue { + label: "Continue"; + + styles ["suggested-action"] + } + + Gtk.Button { + label: "Advanced"; + sensitive: false; + } + } + } + } + } + + // Second page (warning message) + + Gtk.Box second_page { + orientation: vertical; + hexpand: true; + + Adw.PreferencesPage { + Adw.PreferencesGroup { + Gtk.Label { + label: "ToS violation warning"; + margin-top: 8; + + styles ["title-1"] + } + + Gtk.Box { + orientation: vertical; + margin-top: 32; + spacing: 12; + + Gtk.Label { + label: "This launcher is an unofficial tool, in no way related to miHoYo nor COGNOSPHERE."; + + wrap: true; + halign: start; + } + + Gtk.Label { + label: "This tool is designed to facilitate playing Genshin Impact on Linux, and was built with the sole purpose of installing and running the game with less hassle."; + + wrap: true; + halign: start; + } + + Gtk.Label { + label: "It does so by using existing components and making the experience simple for the user."; + + wrap: true; + halign: start; + } + + Gtk.Label { + label: "However, some components used here likely break the miHoYo Terms of Service for Genshin Impact."; + + wrap: true; + halign: start; + } + + Gtk.Label { + label: "If you are using this launcher, your player account could become identified as TOS-non-compliant by miHoYo/COGNOSPHERE."; + + wrap: true; + halign: start; + } + + Gtk.Label { + label: "If this happens, as your account would be disobeying TOS, miHoYo/COGNOSPHERE are free to do what they want. Including banning."; + + wrap: true; + halign: start; + } + + Gtk.Label { + label: "If you understand the risk of trying to play the game in an unofficial capacity, press OK and let's go researching the world of Teyvat!"; + + wrap: true; + halign: start; + } + } + } + + Adw.PreferencesGroup { + vexpand: true; + valign: center; + + Gtk.Box { + orientation: horizontal; + spacing: 8; + halign: center; + + Gtk.Button second_page_continue { + label: "Continue"; + + styles ["suggested-action"] + } + + Gtk.Button second_page_exit { + label: "Exit"; + } + } + } + } + } + + // Third page (downloading components) + + Gtk.Box third_page { + orientation: vertical; + hexpand: true; + + Adw.PreferencesPage { + Adw.PreferencesGroup { + Gtk.Label { + label: "Download default components"; + margin-top: 16; + + styles ["title-1"] + } + } + + Adw.PreferencesGroup { + vexpand: true; + valign: center; + + Adw.ComboRow third_page_wine_version { + title: "Wine version"; + + model: Gtk.StringList { + strings [ + "Wine-GE-Proton 7-22" + ] + }; + } + + Adw.ComboRow third_page_dxvk_version { + title: "DXVK version"; + + model: Gtk.StringList { + strings [ + "dxvk-1.10.2" + ] + }; + } + } + + Adw.PreferencesGroup third_page_buttons_group { + vexpand: true; + valign: center; + + Gtk.Box { + orientation: horizontal; + spacing: 8; + halign: center; + + Gtk.Button third_page_download { + label: "Download"; + + styles ["suggested-action"] + } + + Gtk.Button third_page_exit { + label: "Exit"; + } + } + } + + Adw.PreferencesGroup third_page_progress_bar_group { + vexpand: true; + valign: center; + visible: false; + + Gtk.Box { + halign: center; + margin-top: 64; + spacing: 20; + + Gtk.ProgressBar third_page_progress_bar { + text: "Downloading: 37% (3.7 of 10 GB)"; + show-text: true; + + width-request: 260; + fraction: 0.37; + valign: center; + } + + Gtk.Button { + label: "Pause"; + sensitive: false; + } + } + } + } + } + } + + Adw.CarouselIndicatorDots { + carousel: carousel; + height-request: 32; + } + }; +} diff --git a/assets/ui/main.blp b/assets/ui/main.blp index 1b100da..1686e02 100644 --- a/assets/ui/main.blp +++ b/assets/ui/main.blp @@ -102,6 +102,7 @@ Adw.ApplicationWindow window { Gtk.Button { label: "Pause"; + sensitive: false; } } } diff --git a/src/lib/launcher/executors/create_prefix.rs b/src/lib/launcher/executors/create_prefix.rs new file mode 100644 index 0000000..60b5938 --- /dev/null +++ b/src/lib/launcher/executors/create_prefix.rs @@ -0,0 +1,34 @@ +use std::io::Error; + +use wait_not_await::Await; + +use crate::ui::components::progress_bar::ProgressBar; + +use crate::lib::wine_prefix::WinePrefix; +use crate::lib::config::Config; + +pub fn create_prefix(config: Config, progress_bar: ProgressBar) -> Await> { + Await::new(move || { + // Create prefix if needed + let prefix = WinePrefix::new(&config.game.wine.prefix); + + if !prefix.exists() { + progress_bar.update(0.0, Some("Creating prefix...")); + + match config.try_get_selected_wine_info() { + Some(wine_version) => match prefix.update(&config.game.wine.builds, wine_version) { + Ok(_) => Ok(()), + Err(err) => Err((String::from("Failed to create prefix"), err)) + }, + None => { + // TODO: download default wine + todo!() + } + } + } + + else { + Ok(()) + } + }) +} diff --git a/src/lib/launcher/executors/download_diff.rs b/src/lib/launcher/executors/download_diff.rs new file mode 100644 index 0000000..f5a1e00 --- /dev/null +++ b/src/lib/launcher/executors/download_diff.rs @@ -0,0 +1,64 @@ +use std::io::Error; + +use anime_game_core::prelude::*; +use wait_not_await::Await; + +use crate::ui::components::progress_bar::ProgressBar; +use crate::lib::prettify_bytes::prettify_bytes; + +/*pub fn download_diff(diff: &VersionDiff, progress_bar: ProgressBar, suffix: Option) -> Await> { + let (send, recv) = std::sync::mpsc::channel(); + + diff.install(move |state| { + match state { + InstallerUpdate::DownloadingStarted(_) => progress_bar.update(0.0, Some("Downloading...")), + + InstallerUpdate::DownloadingProgress(curr, total) => { + // To reduce amount of action requests + // if curr % 10000 < 200 { + let progress = curr as f64 / total as f64; + + progress_bar.update(progress, Some(&format!( + "Downloading{}: {:.2}% ({} of {})", + if let Some(suffix) = suffix { format!(" {}", suffix) } else { String::new() }, + progress * 100.0, + prettify_bytes(curr), + prettify_bytes(total) + ))); + // } + } + + InstallerUpdate::UnpackingStarted(_) => progress_bar.update(0.0, Some("Unpacking...")), + + InstallerUpdate::UnpackingProgress(curr, total) => { + let progress = curr as f64 / total as f64; + + progress_bar.update(progress, Some(&format!( + "Unpacking{}: {:.2}% ({} of {})", + if let Some(suffix) = suffix { format!(" {}", suffix) } else { String::new() }, + progress * 100.0, + prettify_bytes(curr), + prettify_bytes(total) + ))); + } + + InstallerUpdate::DownloadingFinished => (), + + InstallerUpdate::UnpackingFinished => { + send.send(Ok(())); + }, + + InstallerUpdate::DownloadingError(err) => { + send.send(Err((String::from("Failed to download"), err.into()))); + } + + InstallerUpdate::UnpackingError => { + send.send(Err((String::from("Failed to unpack"), Error::last_os_error()))); + } + } + }); + + Await::new(move || { + recv.recv().unwrap() + }) +}*/ diff --git a/src/lib/launcher/executors/mod.rs b/src/lib/launcher/executors/mod.rs new file mode 100644 index 0000000..b1898f3 --- /dev/null +++ b/src/lib/launcher/executors/mod.rs @@ -0,0 +1,32 @@ +mod create_prefix; +mod download_diff; + +pub use create_prefix::*; +pub use download_diff::*; + +use crate::lib::config; +use crate::lib::wine_prefix::WinePrefix; + +#[derive(Debug, Clone, Copy)] +pub enum Component { + Wine, + DXVK, + Prefix +} + +#[derive(Debug, Clone)] +pub struct ComponentsChain { + pub chain: Vec +} + +impl ComponentsChain { + pub fn get() -> std::io::Result { + let config = config::get()?; + + let wine_prefix = WinePrefix::new(&config.game.wine.prefix); + + + + todo!(); + } +} diff --git a/src/lib/launcher/mod.rs b/src/lib/launcher/mod.rs index 2039478..3cd5d62 100644 --- a/src/lib/launcher/mod.rs +++ b/src/lib/launcher/mod.rs @@ -1 +1,2 @@ pub mod states; +pub mod executors; diff --git a/src/lib/launcher/states.rs b/src/lib/launcher/states.rs index b1b1f42..d2c5276 100644 --- a/src/lib/launcher/states.rs +++ b/src/lib/launcher/states.rs @@ -4,8 +4,11 @@ use libadwaita::{self as adw, prelude::*}; use std::io::{Error, ErrorKind}; use anime_game_core::prelude::*; +use wait_not_await::Await; +use crate::ui::components::progress_bar::ProgressBar; use crate::lib::config; +use crate::lib::prettify_bytes::prettify_bytes; #[derive(Debug, Clone)] pub enum LauncherState { @@ -102,4 +105,171 @@ impl LauncherState { VersionDiff::NotInstalled { .. } => Self::GameNotInstalled(diff) }) } + + /*pub fn execute(&self, progress_bar: &ProgressBar) -> Await> { + match self { + Self::Launch => { + // Display toast message if the game is failed to run + /*if let Err(err) = game::run(false) { + this.toast_error("Failed to run game", err); + }*/ + + todo!(); + }, + + Self::PatchAvailable(_) => todo!(), + + Self::VoiceUpdateAvailable(diff) | + Self::VoiceNotInstalled(diff) | + Self::GameUpdateAvailable(diff) | + Self::GameNotInstalled(diff) => { + // this.update(Actions::DownloadDiff(Rc::new(diff))).unwrap(); + + // Download wine version if not installed + match WineVersion::latest() { + Ok(wine) => match Installer::new(wine.uri) { + Ok(mut installer) => { + let (send, recv) = std::sync::mpsc::channel(); + let wine_title = wine.title.clone(); + + installer.install(&config.game.wine.builds, clone!(@strong this => move |state| { + match state { + InstallerUpdate::UnpackingFinished => { + send.send(true).unwrap(); + } + + InstallerUpdate::DownloadingError(_) | + InstallerUpdate::UnpackingError => { + send.send(false).unwrap(); + } + + _ => () + } + + this.update(Actions::UpdateProgressByState(Rc::new((state, Some(wine_title.clone()))))).unwrap(); + })); + + // Block thread until downloading finished + if recv.recv().unwrap() { + config.game.wine.selected = Some(wine.name); + + config::update(config.clone()); + } + + else { + println!("I'm tired, Boss!"); + + return; + } + }, + Err(err) => { + toast_error(&this, "Failed to init wine version installer", err.into()); + + return; + } + }, + Err(err) => { + toast_error(&this, "Failed to load wine versions list", err.into()); + + return; + } + } + + // Create prefix if needed + let prefix = WinePrefix::new(&config.game.wine.prefix); + + if !prefix.exists() { + this.update(Actions::UpdateProgress { + fraction: Rc::new(0.0), + title: Rc::new(String::from("Creating prefix...")) + }).unwrap(); + + match config.try_get_selected_wine_info() { + Some(wine_version) => { + if let Err(err) = prefix.update(&config.game.wine.builds, wine_version) { + toast_error(&this, "Failed to create wineprefix", err); + + return; + } + }, + None => return + } + } + + // Download and apply DXVK if not installed + match DxvkVersion::latest() { + Ok(dxvk) => match Installer::new(&dxvk.uri) { + Ok(mut installer) => { + let (send, recv) = std::sync::mpsc::channel(); + let dxvk_title = dxvk.name.clone(); + + installer.install(&config.game.dxvk.builds, clone!(@strong this => move |state| { + match state { + InstallerUpdate::UnpackingFinished => { + send.send(true).unwrap(); + } + + InstallerUpdate::DownloadingError(_) | + InstallerUpdate::UnpackingError => { + send.send(false).unwrap(); + } + + _ => () + } + + this.update(Actions::UpdateProgressByState(Rc::new((state, Some(dxvk_title.clone()))))).unwrap(); + })); + + // Block thread until downloading finished + if recv.recv().unwrap() { + config.game.dxvk.selected = Some(dxvk.name.clone()); + + config::update(config.clone()); + } + + else { + return; + } + + // Apply DXVK + this.update(Actions::UpdateProgress { + fraction: Rc::new(100.0), + title: Rc::new(String::from("Applying DXVK...")) + }).unwrap(); + + match dxvk.apply(&config.game.dxvk.builds, &config.game.wine.prefix) { + Ok(_) => { + config.game.dxvk.selected = Some(dxvk.name); + + config::update(config.clone()); + }, + Err(err) => { + toast_error(&this, "Failed to apply DXVK", err); + + return; + } + } + }, + Err(err) => { + toast_error(&this, "Failed to init wine version installer", err.into()); + + return; + } + }, + Err(err) => { + toast_error(&this, "Failed to load wine versions list", err.into()); + + return; + } + } + + todo!(); + }, + + Self::GameOutdated(_) => (), + Self::VoiceOutdated(_) => () + } + + todo!(); + }*/ } diff --git a/src/lib/mod.rs b/src/lib/mod.rs index 4cd0a39..09c7153 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -6,3 +6,4 @@ pub mod dxvk; pub mod wine; pub mod wine_prefix; pub mod launcher; +pub mod prettify_bytes; diff --git a/src/lib/prettify_bytes.rs b/src/lib/prettify_bytes.rs new file mode 100644 index 0000000..516eb71 --- /dev/null +++ b/src/lib/prettify_bytes.rs @@ -0,0 +1,17 @@ +pub fn prettify_bytes(bytes: u64) -> String { + if bytes > 1024 * 1024 * 1024 { + format!("{:.2} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0) + } + + else if bytes > 1024 * 1024 { + format!("{:.2} MB", bytes as f64 / 1024.0 / 1024.0) + } + + else if bytes > 1024 { + format!("{:.2} KB", bytes as f64 / 1024.0) + } + + else { + format!("{:.2} B", bytes) + } +} diff --git a/src/lib/wine_prefix.rs b/src/lib/wine_prefix.rs index 500e116..d03bdb4 100644 --- a/src/lib/wine_prefix.rs +++ b/src/lib/wine_prefix.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::process::Command; +#[derive(Debug, Clone)] pub struct WinePrefix { pub path: String } @@ -17,8 +18,8 @@ impl WinePrefix { fn wineboot(&self, runners_folder: T, runner: super::wine::Version, command: &str) -> std::io::Result<()> { let runners_folder = runners_folder.to_string(); - let wineboot = format!("{}/{}", &runners_folder, runner.files.wineboot); - let wineserver = format!("{}/{}", &runners_folder, runner.files.wineserver); + let wineboot = format!("{}/{}/{}", &runners_folder, runner.name, runner.files.wineboot); + let wineserver = format!("{}/{}/{}", &runners_folder, runner.name, runner.files.wineserver); Command::new(wineboot) .env("WINESERVER", wineserver) diff --git a/src/main.rs b/src/main.rs index cc06dcc..dfe8102 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,9 @@ use gtk::{CssProvider, StyleContext, STYLE_PROVIDER_PRIORITY_APPLICATION}; use gtk::gdk::Display; use gtk::glib::set_application_name; +use std::path::Path; +use std::fs; + pub mod ui; pub mod lib; @@ -27,13 +30,6 @@ async fn main() { // FIXME: doesn't work? set_application_name("An Anime Game Launcher"); - // Create default launcher folder if needed - let launcher_dir = lib::consts::launcher_dir().unwrap(); - - if !std::path::Path::new(&launcher_dir).exists() { - std::fs::create_dir_all(launcher_dir).expect("Failed to create default launcher dir"); - } - // Create app let application = gtk::Application::new( Some(APP_ID), @@ -53,10 +49,24 @@ async fn main() { STYLE_PROVIDER_PRIORITY_APPLICATION ); - // Load main window and show it - let main = MainApp::new(app).expect("Failed to init MainApp"); + // Create default launcher folder if needed + let launcher_dir = lib::consts::launcher_dir().unwrap(); - main.show(); + if !Path::new(&launcher_dir).exists() || Path::new(&format!("{}/.first-run", launcher_dir)).exists() { + fs::create_dir_all(&launcher_dir).expect("Failed to create default launcher dir"); + fs::write(format!("{}/.first-run", launcher_dir), "").expect("Failed to create .first-run file"); + + let first_run = FirstRunApp::new(app).expect("Failed to init FirstRunApp"); + + first_run.show(); + } + + // Load main window and show it + else { + let main = MainApp::new(app).expect("Failed to init MainApp"); + + main.show(); + } }); // Flush config from the memory to the file before closing the app diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index d5db4b8..177841d 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,3 +1,4 @@ pub mod dxvk_row; pub mod wine_group; pub mod wine_row; +pub mod progress_bar; diff --git a/src/ui/components/progress_bar.rs b/src/ui/components/progress_bar.rs new file mode 100644 index 0000000..b345720 --- /dev/null +++ b/src/ui/components/progress_bar.rs @@ -0,0 +1,82 @@ +use gtk4::{self as gtk, prelude::*}; +use libadwaita::{self as adw, prelude::*}; + +use gtk::glib; + +use std::io::Error; + +use anime_game_core::prelude::*; +use wait_not_await::Await; + +#[derive(Debug)] +pub enum ProgressUpdateResult { + Updated, + Error(String, std::io::Error), + Finished +} + +#[derive(Clone, glib::Downgrade)] +pub struct ProgressBar { + pub progress_bar: gtk::ProgressBar, + pub default_group: adw::PreferencesGroup, + pub progress_bar_group: adw::PreferencesGroup +} + +impl ProgressBar { + pub fn new(progress_bar: gtk::ProgressBar, default_group: adw::PreferencesGroup, progress_bar_group: adw::PreferencesGroup) -> Self { + Self { + progress_bar, + default_group, + progress_bar_group + } + } + + pub fn show(&self) { + self.progress_bar.set_text(None); + self.progress_bar.set_fraction(0.0); + + self.default_group.hide(); + self.progress_bar_group.show(); + } + + pub fn hide(&self) { + self.default_group.show(); + self.progress_bar_group.hide(); + } + + pub fn update(&self, fraction: f64, text: Option<&str>) { + self.progress_bar.set_fraction(fraction); + self.progress_bar.set_text(text); + } + + pub fn update_from_state(&self, state: InstallerUpdate) -> ProgressUpdateResult { + match state { + InstallerUpdate::DownloadingStarted(_) => self.show(), + + InstallerUpdate::DownloadingProgress(curr, total) => { + let progress = curr as f64 / total as f64; + + self.update(progress, None); + } + + InstallerUpdate::UnpackingProgress(curr, total) => { + let progress = curr as f64 / total as f64; + + self.update(progress, None); + } + + InstallerUpdate::DownloadingFinished => (), + InstallerUpdate::UnpackingStarted(_) => (), + + InstallerUpdate::DownloadingError(err) => return ProgressUpdateResult::Error(String::from("Failed to download"), err.into()), + InstallerUpdate::UnpackingError => return ProgressUpdateResult::Error(String::from("Failed to unpack"), Error::last_os_error()), + + InstallerUpdate::UnpackingFinished => return ProgressUpdateResult::Finished + } + + ProgressUpdateResult::Updated + } +} + +unsafe impl Send for ProgressBar {} +unsafe impl Sync for ProgressBar {} diff --git a/src/ui/first_run.rs b/src/ui/first_run.rs new file mode 100644 index 0000000..a34b2de --- /dev/null +++ b/src/ui/first_run.rs @@ -0,0 +1,275 @@ +use gtk4::{self as gtk, prelude::*}; +use libadwaita::{self as adw, prelude::*}; + +use gtk::glib; +use gtk::glib::clone; + +use std::rc::Rc; +use std::cell::Cell; + +use anime_game_core::prelude::*; + +use crate::ui::*; +use crate::ui::components::progress_bar::*; + +use crate::lib::wine::Version as WineVersion; +use crate::lib::dxvk::Version as DxvkVersion; +use crate::lib::wine_prefix::WinePrefix; +use crate::lib::config; + +/// This structure is used to describe widgets used in application +/// +/// `AppWidgets::default` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets +/// +/// This function does not implement events +#[derive(Clone)] +pub struct AppWidgets { + pub window: adw::ApplicationWindow, + pub carousel: adw::Carousel, + + // First page + pub first_page: gtk::Box, + pub first_page_continue: gtk::Button, + + // Second page + pub second_page: gtk::Box, + pub second_page_continue: gtk::Button, + pub second_page_exit: gtk::Button, + + // Third page + pub third_page: gtk::Box, + + pub third_page_wine_version: adw::ComboRow, + pub third_page_dxvk_version: adw::ComboRow, + + pub third_page_download: gtk::Button, + pub third_page_exit: gtk::Button, + + pub third_page_progress_bar: ProgressBar +} + +impl AppWidgets { + pub fn try_get() -> Result { + let builder = gtk::Builder::from_string(include_str!("../../assets/ui/.dist/first_run.ui")); + + Ok(Self { + window: get_object(&builder, "window")?, + carousel: get_object(&builder, "carousel")?, + + // First page + first_page: get_object(&builder, "first_page")?, + first_page_continue: get_object(&builder, "first_page_continue")?, + + // Second page + second_page: get_object(&builder, "second_page")?, + second_page_continue: get_object(&builder, "second_page_continue")?, + second_page_exit: get_object(&builder, "second_page_exit")?, + + // Third page + third_page: get_object(&builder, "third_page")?, + + third_page_wine_version: get_object(&builder, "third_page_wine_version")?, + third_page_dxvk_version: get_object(&builder, "third_page_dxvk_version")?, + + third_page_download: get_object(&builder, "third_page_download")?, + third_page_exit: get_object(&builder, "third_page_exit")?, + + third_page_progress_bar: ProgressBar::new( + get_object(&builder, "third_page_progress_bar")?, + get_object(&builder, "third_page_buttons_group")?, + get_object(&builder, "third_page_progress_bar_group")? + ), + }) + } +} + +/// This enum is used to describe an action inside of this application +/// +/// It may be helpful if you want to add the same event for several widgets, or call an action inside of another action +/// +/// Has to implement glib::Downgrade` trait +#[derive(Debug, glib::Downgrade)] +pub enum Actions { + FirstPageContinue, + SecondPageContinue, + ThirdPageDownload, + Exit +} + +impl Actions { + pub fn into_fn>(&self, app: &App) -> Box { + Box::new(clone!(@weak self as action, @strong app => move |_| { + app.update(action).unwrap(); + })) + } +} + +/// This enum is used to store some of this application data +/// +/// In this example we store a counter here to know what should we increment or decrement +/// +/// This must implement `Default` trait +#[derive(Debug, Default)] +pub struct Values; + +/// The main application structure +/// +/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets +/// +/// `Rc>` means this: +/// - `Rc` addeds ability to reference the same value from various clones of the structure. +/// This will guarantee us that inner `Cell` is the same for all the `App::clone()` values +/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference. +/// +/// So we have a shared reference to some value that can be changed without mutable reference. +/// That's what we need and what we use in `App::update` method +#[derive(Clone)] +pub struct App { + widgets: AppWidgets, + values: Rc>, + actions: Rc>>> +} + +impl App { + /// Create new application + pub fn new(app: >k::Application) -> Result { + // Get default widgets from ui file and add events to them + let result = Self { + widgets: AppWidgets::try_get()?, + values: Default::default(), + actions: Default::default() + }.init_events().init_actions(); + + // Bind app to the window + result.widgets.window.set_application(Some(app)); + + Ok(result) + } + + /// Add default events and values to the widgets + fn init_events(self) -> Self { + self.widgets.first_page_continue.connect_clicked(Actions::FirstPageContinue.into_fn(&self)); + self.widgets.second_page_continue.connect_clicked(Actions::SecondPageContinue.into_fn(&self)); + self.widgets.third_page_download.connect_clicked(Actions::ThirdPageDownload.into_fn(&self)); + + self.widgets.second_page_exit.connect_clicked(Actions::Exit.into_fn(&self)); + self.widgets.third_page_exit.connect_clicked(Actions::Exit.into_fn(&self)); + + self + } + + /// Add actions processors + /// + /// Changes will happen in the main thread so you can call `update` method from separate thread + pub fn init_actions(self) -> Self { + let (sender, receiver) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); + + let this = self.clone(); + + receiver.attach(None, move |action| { + // Some debug output + println!("[update] action: {:?}", &action); + + match action { + Actions::FirstPageContinue => { + this.widgets.carousel.scroll_to(&this.widgets.second_page, true); + } + + Actions::SecondPageContinue => { + this.widgets.carousel.scroll_to(&this.widgets.third_page, true); + } + + Actions::ThirdPageDownload => { + this.widgets.third_page_wine_version.set_sensitive(false); + this.widgets.third_page_dxvk_version.set_sensitive(false); + + this.widgets.third_page_progress_bar.show(); + + let this = this.clone(); + + std::thread::spawn(move || { + let config = config::get().unwrap(); + + let wine_version = WineVersion::latest().unwrap(); + let dxvk_version = DxvkVersion::latest().unwrap(); + + let mut wine_version_installer = Installer::new(&wine_version.uri).unwrap(); + + let progress_bar = this.widgets.third_page_progress_bar.clone(); + + wine_version_installer.install(&config.game.wine.builds, move |state| { + match progress_bar.update_from_state(state) { + ProgressUpdateResult::Updated => (), + ProgressUpdateResult::Error(_, _) => todo!(), + + ProgressUpdateResult::Finished => { + let mut config = config::get().unwrap(); + let prefix = WinePrefix::new(&config.game.wine.prefix); + + config.game.wine.selected = Some(wine_version.name.clone()); + + config::update_raw(config.clone()).unwrap(); + + prefix.update(&config.game.wine.builds, wine_version.clone()).unwrap(); + + let mut dxvk_version_installer = Installer::new(&dxvk_version.uri).unwrap(); + + let dxvk_version = dxvk_version.clone(); + let progress_bar = progress_bar.clone(); + + dxvk_version_installer.install(&config.game.dxvk.builds, move |state| { + match progress_bar.update_from_state(state) { + ProgressUpdateResult::Updated => (), + ProgressUpdateResult::Error(_, _) => todo!(), + + ProgressUpdateResult::Finished => { + let mut config = config::get().unwrap(); + + config.game.dxvk.selected = Some(dxvk_version.name.clone()); + + config::update_raw(config.clone()).unwrap(); + + println!("Done!!"); + } + } + }); + } + } + }); + }); + } + + Actions::Exit => { + this.widgets.window.close(); + } + } + + glib::Continue(true) + }); + + self.actions.set(Some(sender)); + + self + } + + /// Update widgets state by calling some action + pub fn update(&self, action: Actions) -> Result<(), std::sync::mpsc::SendError> { + let actions = self.actions.take(); + + let result = match &actions { + Some(sender) => Ok(sender.send(action)?), + None => Ok(()) + }; + + self.actions.set(actions); + + result + } + + /// Show application window + pub fn show(&self) { + self.widgets.window.show(); + } +} + +unsafe impl Send for App {} diff --git a/src/ui/main.rs b/src/ui/main.rs index b1e09e7..00f4b03 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -14,6 +14,7 @@ use crate::ui::*; use super::preferences::PreferencesStack; use super::traits::toast_error::ToastError; +use super::components::progress_bar::ProgressBar; use crate::lib::config; use crate::lib::game; @@ -22,24 +23,7 @@ use crate::lib::launcher::states::LauncherState; use crate::lib::wine_prefix::WinePrefix; use crate::lib::wine::Version as WineVersion; use crate::lib::dxvk::Version as DxvkVersion; - -fn prettify_bytes(bytes: u64) -> String { - if bytes > 1024 * 1024 * 1024 { - format!("{:.2} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0) - } - - else if bytes > 1024 * 1024 { - format!("{:.2} MB", bytes as f64 / 1024.0 / 1024.0) - } - - else if bytes > 1024 { - format!("{:.2} KB", bytes as f64 / 1024.0) - } - - else { - format!("{:.2} B", bytes) - } -} +use crate::lib::prettify_bytes::prettify_bytes; fn toast_error(app: &App, msg: &str, err: Error) { app.update(Actions::ToastError(Rc::new(( @@ -70,15 +54,13 @@ pub struct AppWidgets { pub launch_game: gtk::Button, pub open_preferences: gtk::Button, - pub launch_game_group: adw::PreferencesGroup, - pub progress_bar_group: adw::PreferencesGroup, - pub progress_bar: gtk::ProgressBar, + pub progress_bar: ProgressBar, pub preferences_stack: PreferencesStack } impl AppWidgets { - fn try_get() -> Result { + pub fn try_get() -> Result { let builder = gtk::Builder::from_string(include_str!("../../assets/ui/.dist/main.ui")); let window = get_object::(&builder, "window")?; @@ -98,9 +80,11 @@ impl AppWidgets { launch_game: get_object(&builder, "launch_game")?, open_preferences: get_object(&builder, "open_preferences")?, - launch_game_group: get_object(&builder, "launch_game_group")?, - progress_bar_group: get_object(&builder, "progress_bar_group")?, - progress_bar: get_object(&builder, "progress_bar")?, + progress_bar: ProgressBar::new( + get_object(&builder, "progress_bar")?, + get_object(&builder, "launch_game_group")?, + get_object(&builder, "progress_bar_group")? + ), preferences_stack: PreferencesStack::new(window, toast_overlay)? }; @@ -311,144 +295,6 @@ impl App { this.update(Actions::ShowProgressBar).unwrap(); - // Download wine version if not installed - match WineVersion::latest() { - Ok(wine) => match Installer::new(wine.uri) { - Ok(mut installer) => { - let (send, recv) = std::sync::mpsc::channel(); - let wine_title = wine.title.clone(); - - installer.install(&config.game.wine.builds, clone!(@strong this => move |state| { - match state { - InstallerUpdate::UnpackingFinished => { - send.send(true).unwrap(); - } - - InstallerUpdate::DownloadingError(_) | - InstallerUpdate::UnpackingError => { - send.send(false).unwrap(); - } - - _ => () - } - - this.update(Actions::UpdateProgressByState(Rc::new((state, Some(wine_title.clone()))))).unwrap(); - })); - - // Block thread until downloading finished - if recv.recv().unwrap() { - config.game.wine.selected = Some(wine.name); - - config::update(config.clone()); - } - - else { - println!("I'm tired, Boss!"); - - return; - } - }, - Err(err) => { - toast_error(&this, "Failed to init wine version installer", err.into()); - - return; - } - }, - Err(err) => { - toast_error(&this, "Failed to load wine versions list", err.into()); - - return; - } - } - - // Create prefix if needed - let prefix = WinePrefix::new(&config.game.wine.prefix); - - if !prefix.exists() { - this.update(Actions::UpdateProgress { - fraction: Rc::new(0.0), - title: Rc::new(String::from("Creating prefix...")) - }).unwrap(); - - match config.try_get_selected_wine_info() { - Some(wine_version) => { - if let Err(err) = prefix.update(&config.game.wine.builds, wine_version) { - toast_error(&this, "Failed to create wineprefix", err); - - return; - } - }, - None => return - } - } - - // Download and apply DXVK if not installed - match DxvkVersion::latest() { - Ok(dxvk) => match Installer::new(&dxvk.uri) { - Ok(mut installer) => { - let (send, recv) = std::sync::mpsc::channel(); - let dxvk_title = dxvk.name.clone(); - - installer.install(&config.game.dxvk.builds, clone!(@strong this => move |state| { - match state { - InstallerUpdate::UnpackingFinished => { - send.send(true).unwrap(); - } - - InstallerUpdate::DownloadingError(_) | - InstallerUpdate::UnpackingError => { - send.send(false).unwrap(); - } - - _ => () - } - - this.update(Actions::UpdateProgressByState(Rc::new((state, Some(dxvk_title.clone()))))).unwrap(); - })); - - // Block thread until downloading finished - if recv.recv().unwrap() { - config.game.dxvk.selected = Some(dxvk.name.clone()); - - config::update(config.clone()); - } - - else { - return; - } - - // Apply DXVK - this.update(Actions::UpdateProgress { - fraction: Rc::new(100.0), - title: Rc::new(String::from("Applying DXVK...")) - }).unwrap(); - - match dxvk.apply(&config.game.dxvk.builds, &config.game.wine.prefix) { - Ok(_) => { - config.game.dxvk.selected = Some(dxvk.name); - - config::update(config.clone()); - }, - Err(err) => { - toast_error(&this, "Failed to apply DXVK", err); - - return; - } - } - }, - Err(err) => { - toast_error(&this, "Failed to init wine version installer", err.into()); - - return; - } - }, - Err(err) => { - toast_error(&this, "Failed to load wine versions list", err.into()); - - return; - } - } - // Download diff diff.install_to_by(config.game.path, config.launcher.temp, move |state| { this.update(Actions::UpdateProgressByState(Rc::new((state, None)))).unwrap(); @@ -464,6 +310,7 @@ impl App { } Actions::UpdateProgressByState(state) => { + todo!(); // let (state, suffix) = (&*state).clone(); match &state.0 { @@ -532,21 +379,15 @@ impl App { } Actions::ShowProgressBar => { - this.widgets.progress_bar.set_text(None); - this.widgets.progress_bar.set_fraction(0.0); - - this.widgets.launch_game_group.hide(); - this.widgets.progress_bar_group.show(); + this.widgets.progress_bar.show(); } Actions::UpdateProgress { fraction, title } => { - this.widgets.progress_bar.set_text(Some(title.as_str())); - this.widgets.progress_bar.set_fraction(*fraction); + this.widgets.progress_bar.update(*fraction, Some(title.as_str())); } Actions::HideProgressBar => { - this.widgets.launch_game_group.show(); - this.widgets.progress_bar_group.hide(); + this.widgets.progress_bar.hide(); } Actions::ToastError(toast) => { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bcb80ae..c655869 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,16 +1,15 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; +mod first_run; mod main; mod preferences; mod traits; pub mod components; -pub use main::{ - App as MainApp, - // AppState as MainAppState, -}; +pub use first_run::App as FirstRunApp; +pub use main::App as MainApp; /// This function loads object from builder or panics if it doesn't exist pub fn get_object>(builder: >k::Builder, name: &str) -> Result {