can now download proton from the new gtk launcher

This commit is contained in:
ALEZ-DEV 2025-02-26 21:46:56 +01:00
parent 846b4291c6
commit fa242c1cca
10 changed files with 1273 additions and 74 deletions

782
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@ relm4-components = "0.9.1"
wincompatlib = "0.7.5"
relm4 = { version = "0.9.1", features = ["libadwaita"] }
libadwaita = { version = "0.7.1", features = ["v1_4"] }
downloader = { git = "https://github.com/ALEZ-DEV/downloader" } # version = "0.2.7",
rfd = "0.15.2"
[build-dependencies]
glib-build-tools = "0.20.0"

View File

@ -1,6 +1,8 @@
use std::{ops::Deref, sync::Arc};
use babylonia_terminal_sdk::{
components::proton_component::ProtonComponent, game_config::GameConfig,
game_manager::GameManager,
game_manager::GameManager, utils::github_requester::GithubRelease,
};
use log::error;
use relm4::{
@ -9,7 +11,10 @@ use relm4::{
};
use wincompatlib::prelude::Proton;
use crate::ui;
use crate::ui::{
self,
pages::steps::{self, download_components},
};
static PROTON: OnceCell<Proton> = OnceCell::const_new();
@ -71,3 +76,75 @@ impl Worker for HandleGameProcess {
}
}
}
#[derive(Debug)]
pub enum HandleComponentInstallationMsg {
StartInstallation(
(
usize,
usize,
Arc<download_components::DownloadComponentProgressBarReporter>,
),
), // proton release and dxvk release
}
#[derive(Debug)]
pub struct HandleComponentInstallation;
impl Worker for HandleComponentInstallation {
type Init = ();
type Input = HandleComponentInstallationMsg;
type Output = download_components::DownloadComponentsMsg;
fn init(_init: Self::Init, _sender: relm4::ComponentSender<Self>) -> Self {
Self
}
fn update(&mut self, message: Self::Input, sender: relm4::ComponentSender<Self>) {
match message {
HandleComponentInstallationMsg::StartInstallation((
proton_release,
dxvk_release,
progress_bar,
)) => {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let _ = sender.output(
download_components::DownloadComponentsMsg::UpdateProgressBarMsg(
String::from("Starting download for proton"),
),
);
let _ = sender.output(
download_components::DownloadComponentsMsg::UpdateDownloadedComponentName(
String::from("proton"),
),
);
let game_dir = if let Some(dir) = GameConfig::get_config().await.game_dir {
dir
} else {
GameConfig::get_config_directory().await
};
GameManager::install_wine(game_dir, proton_release, Some(progress_bar))
.await;
let _ = sender.output(
download_components::DownloadComponentsMsg::UpdateProgressBarMsg(
String::from("Unpacking proton"),
),
);
let _ = sender
.output(download_components::DownloadComponentsMsg::UpdateGameState);
});
}
}
}
}

View File

View File

@ -17,7 +17,7 @@ use libadwaita as adw;
use crate::APP_RESOURCE_PATH;
mod pages;
pub mod pages;
pub fn run(app: RelmApp<MainWindowMsg>) {
app.run_async::<MainWindow>(None);
@ -28,6 +28,7 @@ pub enum MainWindowMsg {
ToggleMenuVisibility,
SelectPage,
SetIsGameRunning(bool),
UpdateGameState,
}
struct MainWindow {
@ -103,7 +104,7 @@ impl SimpleAsyncComponent for MainWindow {
set_valign: gtk::Align::Center,
#[watch]
set_visible: model.game_state == GameState::GameInstalled,
set_visible: model.game_state.is_environment_ready(),
adw::PreferencesPage {
add = &adw::PreferencesGroup {
@ -142,7 +143,7 @@ impl SimpleAsyncComponent for MainWindow {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_visible: model.game_state != GameState::GameInstalled,
set_visible: !model.game_state.is_environment_ready(),
model.setup_page.widget(),
}
@ -236,6 +237,9 @@ impl SimpleAsyncComponent for MainWindow {
MainWindowMsg::ToggleMenuVisibility => self.is_menu_visible = !self.is_menu_visible,
MainWindowMsg::SelectPage => println!("Tried to select a new page"),
MainWindowMsg::SetIsGameRunning(value) => self.is_game_running = value,
MainWindowMsg::UpdateGameState => {
self.game_state = GameState::get_current_state().await.unwrap();
}
}
}
}

View File

@ -0,0 +1,122 @@
use std::{path::PathBuf, str::FromStr};
use babylonia_terminal_sdk::{game_config::GameConfig, game_manager::GameManager};
use log::info;
use relm4::{
gtk,
prelude::{AsyncComponentParts, SimpleAsyncComponent},
view, RelmWidgetExt,
};
use libadwaita::{self as adw, prelude::*};
use super::SetupPageMsg;
#[derive(Debug)]
pub enum ChooseGameDirectoryMsg {
ChoosePath,
Next,
}
pub struct ChooseGameDirectoryPage {
path: PathBuf,
}
#[relm4::component(async, pub)]
impl SimpleAsyncComponent for ChooseGameDirectoryPage {
type Input = ChooseGameDirectoryMsg;
type Output = SetupPageMsg;
type Init = ();
view! {
#[root]
gtk::Box {
adw::PreferencesPage {
set_hexpand: true,
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
gtk::Label {
set_label: "Game directory",
add_css_class: "title-1"
},
},
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
adw::ActionRow {
set_title: "Game directory",
set_icon_name: Some("folder-symbolic"),
set_activatable: true,
#[watch]
set_subtitle: model.path.to_str().unwrap(),
connect_activated => ChooseGameDirectoryMsg::ChoosePath,
},
},
add = &adw::PreferencesGroup {
set_margin_vertical: 48,
gtk::Button {
set_css_classes: &["suggested-action", "pill"],
set_label: "Next",
set_hexpand: false,
set_width_request: 200,
connect_clicked => ChooseGameDirectoryMsg::Next,
},
},
},
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: relm4::AsyncComponentSender<Self>,
) -> relm4::prelude::AsyncComponentParts<Self> {
let path = if let Some(dir) = GameConfig::get_config().await.game_dir {
dir
} else {
GameConfig::get_config_directory().await
};
let model = ChooseGameDirectoryPage { path };
let widgets = view_output!();
AsyncComponentParts { widgets, model }
}
async fn update(&mut self, message: Self::Input, sender: relm4::AsyncComponentSender<Self>) {
match message {
ChooseGameDirectoryMsg::ChoosePath => {
info!("choose path");
let result = rfd::AsyncFileDialog::new()
.set_directory(self.path.clone())
.pick_folder()
.await;
if let Some(result) = result {
self.path = result.path().to_path_buf();
}
GameConfig::set_game_dir(Some(self.path.clone()))
.await
.unwrap(); // TODO: remove unwrap
}
ChooseGameDirectoryMsg::Next => {
let _ = sender.output(SetupPageMsg::GoToDownloadComponentPage);
}
}
}
}

View File

@ -1,10 +1,14 @@
use std::{convert::identity, usize};
use babylonia_terminal_sdk::{
components::{
dxvk_component::{self, DXVKComponent},
proton_component::{self, ProtonComponent},
},
game_state::GameState,
utils::github_requester::{GithubRelease, GithubRequester},
};
use log::{error, info};
use relm4::{
self,
gtk::{self, prelude::*},
@ -15,18 +19,44 @@ use relm4::{
use adw::prelude::*;
use libadwaita as adw;
use crate::ui::MainWindowMsg;
use crate::{manager, ui::MainWindowMsg};
use super::SetupPageMsg;
#[derive(Debug)]
pub enum DownloadComponentsMsg {
Next,
UpdateGameState,
UpdateProgressBar((u64, u64)), // current and max_progress
UpdateProgressBarMsg(String),
UpdateDownloadedComponentName(String),
}
#[derive(Debug)]
pub struct DownloadComponentsPage {
//state
game_state: GameState,
// widgets
proton_combo: adw::ComboRow,
dxvk_combo: adw::ComboRow,
// values
proton_versions: Vec<GithubRelease>,
dxvk_versions: Vec<GithubRelease>,
selected_proton_version: Option<GithubRelease>,
selected_dxvk_version: Option<GithubRelease>,
//progress_bar
progress_bar_reporter: std::sync::Arc<DownloadComponentProgressBarReporter>,
progress_bar_message: String,
fraction: f64,
show_progress_bar: bool,
// download part
is_installing: bool,
installation_handler: WorkerController<manager::HandleComponentInstallation>,
downloaded_component_name: String,
}
#[relm4::component(async, pub)]
@ -35,68 +65,128 @@ impl SimpleAsyncComponent for DownloadComponentsPage {
type Output = SetupPageMsg;
type Init = ();
type Init = GameState;
view! {
#[root]
adw::PreferencesPage {
set_hexpand: true,
gtk::Box {
adw::PreferencesPage {
set_hexpand: true,
#[watch]
set_visible: !model.is_installing,
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
gtk::Label {
set_label: "Install components",
add_css_class: "title-1"
gtk::Label {
set_label: "Install components",
add_css_class: "title-1"
},
},
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
#[local_ref]
proton_combo -> adw::ComboRow {
set_title: "proton version",
set_model: Some(&gtk::StringList::new(model
.proton_versions
.iter()
.map(|r| r.tag_name.as_str())
.collect::<Vec<&str>>()
.as_slice())),
},
#[local_ref]
dxvk_combo -> adw::ComboRow {
set_title: "dxvk version",
set_model: Some(&gtk::StringList::new(model
.dxvk_versions
.iter()
.map(|r| r.tag_name.as_str())
.collect::<Vec<&str>>()
.as_slice())),
},
},
add = &adw::PreferencesGroup {
set_margin_vertical: 48,
gtk::Button {
set_css_classes: &["suggested-action", "pill"],
set_label: "Next",
set_hexpand: false,
set_width_request: 200,
connect_clicked => DownloadComponentsMsg::Next,
},
},
},
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
adw::PreferencesPage {
set_hexpand: true,
#[watch]
set_visible: model.is_installing,
adw::ComboRow {
set_title: "Proton version",
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
set_model: Some(&gtk::StringList::new(model
.proton_versions
.iter()
.map(|r| r.tag_name.as_str())
.collect::<Vec<&str>>()
.as_slice())),
gtk::Label {
set_label: "Downloading and installing components",
add_css_class: "title-1"
},
},
adw::ComboRow {
set_title: "DXVK version",
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
set_model: Some(&gtk::StringList::new(model
.dxvk_versions
.iter()
.map(|r| r.tag_name.as_str())
.collect::<Vec<&str>>()
.as_slice())),
adw::ActionRow {
#[watch]
set_title: match &model.selected_proton_version {
Some(release) => &release.tag_name,
None => "WTF??!! there's no proton version found ????",
},
set_subtitle: "Proton version",
#[watch]
set_icon_name: if model.game_state == GameState::ProtonNotInstalled { Some("emblem-ok-symbolic") } else { Some("process-working") },
add_prefix = &gtk::Spinner {
set_spinning: true,
#[watch]
set_visible: model.game_state == GameState::ProtonNotInstalled,
}
}
},
add = &adw::PreferencesGroup {
set_valign: gtk::Align::Center,
set_vexpand: true,
gtk::ProgressBar {
#[watch]
set_fraction: model.fraction,
#[watch]
set_text: Some(&model.progress_bar_message),
set_show_text: true,
}
}
},
add = &adw::PreferencesGroup {
set_margin_vertical: 48,
gtk::Button {
set_css_classes: &["suggested-action", "pill"],
set_label: "Next",
set_hexpand: false,
set_width_request: 200,
connect_clicked => DownloadComponentsMsg::Next,
},
},
},
}
}
async fn init(
init: Self::Init,
game_state: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
@ -113,16 +203,129 @@ impl SimpleAsyncComponent for DownloadComponentsPage {
.unwrap(); //TODO: remove unwrap()
let model = DownloadComponentsPage {
game_state,
proton_combo: adw::ComboRow::new(),
dxvk_combo: adw::ComboRow::new(),
proton_versions: proton_releases,
dxvk_versions: dxvk_releases,
selected_proton_version: None,
selected_dxvk_version: None,
progress_bar_reporter: DownloadComponentProgressBarReporter::create(sender.clone()),
progress_bar_message: String::new(),
fraction: 0.0,
show_progress_bar: false,
is_installing: false,
installation_handler: manager::HandleComponentInstallation::builder()
.detach_worker(())
.forward(sender.input_sender(), identity),
downloaded_component_name: String::new(),
};
let proton_combo = &model.proton_combo;
let dxvk_combo = &model.dxvk_combo;
let widgets = view_output!();
AsyncComponentParts { widgets, model }
}
async fn update(&mut self, message: Self::Input, sender: AsyncComponentSender<Self>) -> () {
todo!();
match message {
DownloadComponentsMsg::Next => {
if !self.is_installing {
self.is_installing = true;
let proton_index = self.proton_combo.selected() as usize;
let dxvk_index = self.dxvk_combo.selected() as usize;
let proton_release = self.proton_versions[proton_index].clone();
let dxvk_release = self.dxvk_versions[dxvk_index].clone();
self.selected_proton_version = Some(proton_release);
self.selected_dxvk_version = Some(dxvk_release);
let _ = self.installation_handler.sender().send(
manager::HandleComponentInstallationMsg::StartInstallation((
proton_index,
dxvk_index,
self.progress_bar_reporter.clone(),
)),
);
} else {
let _ = sender.output(SetupPageMsg::Finish);
}
}
DownloadComponentsMsg::UpdateDownloadedComponentName(name) => {
self.downloaded_component_name = name;
}
DownloadComponentsMsg::UpdateGameState => {
self.game_state = GameState::get_current_state().await.unwrap();
}
DownloadComponentsMsg::UpdateProgressBar((current, max_progress)) => {
self.fraction = if current == 0 {
0.0
} else {
current as f64 / max_progress as f64
};
self.progress_bar_message = format!(
"Downloading {} : {:.2}%",
self.downloaded_component_name,
self.fraction * 100.0
);
}
DownloadComponentsMsg::UpdateProgressBarMsg(message) => {
self.progress_bar_message = message;
}
}
}
}
#[derive(Debug)]
struct ProgressBarReporterPrivate {
max_progress: Option<u64>,
}
#[derive(Debug)]
pub struct DownloadComponentProgressBarReporter {
private: std::sync::Mutex<Option<ProgressBarReporterPrivate>>,
sender: relm4::AsyncComponentSender<DownloadComponentsPage>,
}
impl DownloadComponentProgressBarReporter {
fn create(page: relm4::AsyncComponentSender<DownloadComponentsPage>) -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
private: std::sync::Mutex::new(None),
sender: page,
})
}
}
impl downloader::progress::Reporter for DownloadComponentProgressBarReporter {
fn setup(&self, max_progress: Option<u64>, message: &str) {
let private = ProgressBarReporterPrivate { max_progress };
let mut guard = self.private.lock().unwrap();
*guard = Some(private);
}
fn progress(&self, current: u64) {
if let Some(p) = self.private.lock().unwrap().as_mut() {
self.sender.input(DownloadComponentsMsg::UpdateProgressBar((
current,
p.max_progress.unwrap(),
)));
}
}
fn set_message(&self, message: &str) {}
fn done(&self) {
self.sender.input(DownloadComponentsMsg::Next);
let mut guard = self.private.lock().unwrap();
*guard = None;
}
}

View File

@ -1,6 +1,7 @@
use std::convert::identity;
use babylonia_terminal_sdk::game_state::GameState;
use babylonia_terminal_sdk::{game_config::GameConfig, game_state::GameState};
use choose_game_directory::ChooseGameDirectoryPage;
use download_components::DownloadComponentsPage;
use libadwaita::prelude::{OrientableExt, WidgetExt};
use relm4::{
@ -14,17 +15,21 @@ use welcome::WelcomePage;
use crate::ui::MainWindowMsg;
mod download_components;
mod choose_game_directory;
pub mod download_components;
mod welcome;
#[derive(Debug)]
pub enum SetupPageMsg {
UpdateGameState,
GoToChooseGameDirectoryPage,
GoToDownloadComponentPage,
Finish,
}
pub struct SetupPage {
game_state: GameState,
welcome_page: Controller<welcome::WelcomePage>,
choose_game_directory_page: AsyncController<choose_game_directory::ChooseGameDirectoryPage>,
download_components_page: AsyncController<download_components::DownloadComponentsPage>,
carousel: adw::Carousel,
@ -42,6 +47,8 @@ impl SimpleAsyncComponent for SetupPage {
#[root]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: true,
#[local_ref]
carousel -> adw::Carousel {
@ -50,6 +57,7 @@ impl SimpleAsyncComponent for SetupPage {
set_allow_scroll_wheel: false,
append = model.welcome_page.widget(),
append = model.choose_game_directory_page.widget(),
append = model.download_components_page.widget(),
},
@ -68,9 +76,12 @@ impl SimpleAsyncComponent for SetupPage {
let welcome_page = WelcomePage::builder()
.launch(())
.forward(sender.input_sender(), identity);
let download_components_page = DownloadComponentsPage::builder()
let choose_game_directory_page = ChooseGameDirectoryPage::builder()
.launch(())
.forward(sender.input_sender(), identity);
let download_components_page = DownloadComponentsPage::builder()
.launch(game_state.clone())
.forward(sender.input_sender(), identity);
let carousel = adw::Carousel::new();
@ -78,6 +89,7 @@ impl SimpleAsyncComponent for SetupPage {
let model = SetupPage {
welcome_page,
choose_game_directory_page,
download_components_page,
game_state,
carousel: carousel.clone(),
@ -88,24 +100,20 @@ impl SimpleAsyncComponent for SetupPage {
}
async fn update(&mut self, message: Self::Input, sender: AsyncComponentSender<Self>) {
match message {
SetupPageMsg::UpdateGameState => {
self.game_state = GameState::get_current_state().await.unwrap()
} // TODO: delete this unwrap()
}
self.game_state = GameState::get_current_state().await.unwrap(); // TODO: delete this unwrap()
match self.game_state {
GameState::ProtonNotInstalled => {
match message {
SetupPageMsg::GoToChooseGameDirectoryPage => {
self.carousel
.scroll_to(self.choose_game_directory_page.widget(), true);
}
SetupPageMsg::GoToDownloadComponentPage => {
self.carousel
.scroll_to(self.download_components_page.widget(), true);
}
GameState::DXVKNotInstalled => todo!(),
GameState::FontNotInstalled => todo!(),
GameState::DependecieNotInstalled => todo!(),
GameState::GameNotInstalled => todo!(),
GameState::GameNeedUpdate => todo!(),
GameState::GameNotPatched => todo!(),
GameState::GameInstalled => todo!(),
SetupPageMsg::Finish => {
sender.output(MainWindowMsg::UpdateGameState);
}
}
}
}

View File

@ -78,7 +78,9 @@ impl SimpleComponent for WelcomePage {
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
match message {
WelcomePageMsg::Next => sender.output(SetupPageMsg::UpdateGameState).unwrap(),
WelcomePageMsg::Next => sender
.output(SetupPageMsg::GoToChooseGameDirectoryPage)
.unwrap(),
}
}
}

View File

@ -46,4 +46,11 @@ impl GameState {
Ok(GameState::GameInstalled)
}
pub fn is_environment_ready(&self) -> bool {
self == &GameState::GameNotInstalled
|| self == &GameState::GameNeedUpdate
|| self == &GameState::GameNotPatched
|| self == &GameState::GameInstalled
}
}