diff --git a/assets/locales/en/first_run.ftl b/assets/locales/en/first_run.ftl index 2e719eb..b18632a 100644 --- a/assets/locales/en/first_run.ftl +++ b/assets/locales/en/first_run.ftl @@ -42,6 +42,7 @@ components-index = Components index patch-folder = Patch folder temp-folder = Temp folder +migrate = Migrate select-voice-packages = Select voice packages diff --git a/src/ui/first_run/default_paths.rs b/src/ui/first_run/default_paths.rs index 78cc320..1d09d29 100644 --- a/src/ui/first_run/default_paths.rs +++ b/src/ui/first_run/default_paths.rs @@ -5,14 +5,19 @@ use adw::prelude::*; use anime_launcher_sdk::config; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::*; use crate::i18n::*; use super::main::*; +use crate::ui::components::progress_bar::*; pub struct DefaultPathsApp { + progress_bar: AsyncController, + show_additional: bool, + migrate_installation: bool, + show_progress: bool, launcher: PathBuf, runners: PathBuf, @@ -50,7 +55,8 @@ pub enum DefaultPathsAppMsg { #[relm4::component(async, pub)] impl SimpleAsyncComponent for DefaultPathsApp { - type Init = (); + /// If `true`, then use migrate installation mode + type Init = bool; type Input = DefaultPathsAppMsg; type Output = FirstRunAppMsg; @@ -72,6 +78,9 @@ impl SimpleAsyncComponent for DefaultPathsApp { set_valign: gtk::Align::End, set_vexpand: true, + #[watch] + set_sensitive: !model.show_progress, + adw::ActionRow { set_title: &tr("launcher-folder"), set_icon_name: Some("folder-symbolic"), @@ -107,6 +116,9 @@ impl SimpleAsyncComponent for DefaultPathsApp { #[watch] set_visible: model.show_additional, + #[watch] + set_sensitive: !model.show_progress, + adw::ActionRow { set_title: &tr("runners-folder"), set_icon_name: Some("folder-symbolic"), @@ -210,37 +222,76 @@ impl SimpleAsyncComponent for DefaultPathsApp { add = &adw::PreferencesGroup { set_valign: gtk::Align::Center, set_vexpand: true, - + + #[watch] + set_visible: !model.show_progress, + gtk::Box { set_orientation: gtk::Orientation::Horizontal, set_halign: gtk::Align::Center, set_spacing: 8, - + gtk::Button { - set_label: &tr("continue"), + set_label: &if model.migrate_installation { + tr("migrate") + } else { + tr("continue") + }, + set_css_classes: &["suggested-action", "pill"], connect_clicked => DefaultPathsAppMsg::Continue }, gtk::Button { - set_label: &tr("exit"), + set_label: &if model.migrate_installation { + tr("close") + } else { + tr("exit") + }, + add_css_class: "pill", connect_clicked => DefaultPathsAppMsg::Exit } } + }, + + add = &adw::PreferencesGroup { + set_valign: gtk::Align::Center, + set_vexpand: true, + + #[watch] + set_visible: model.show_progress, + + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_halign: gtk::Align::Center, + + append = model.progress_bar.widget(), + } } } } async fn init( - _init: Self::Init, + init: Self::Init, root: Self::Root, _sender: AsyncComponentSender, ) -> AsyncComponentParts { let model = Self { + progress_bar: ProgressBar::builder() + .launch(ProgressBarInit { + caption: None, + display_progress: true, + display_fraction: false, + visible: false + }) + .detach(), + show_additional: false, + migrate_installation: init, + show_progress: false, launcher: LAUNCHER_FOLDER.to_path_buf(), runners: CONFIG.game.wine.builds.clone(), @@ -256,6 +307,9 @@ impl SimpleAsyncComponent for DefaultPathsApp { temp: CONFIG.launcher.temp.clone().unwrap_or(std::env::temp_dir()) }; + // Set progress bar width + model.progress_bar.widget().set_width_request(400); + let widgets = view_output!(); AsyncComponentParts { model, widgets } @@ -303,17 +357,95 @@ impl SimpleAsyncComponent for DefaultPathsApp { #[allow(unused_must_use)] DefaultPathsAppMsg::Continue => { + let old_config = config::get().unwrap_or_else(|_| CONFIG.clone()); + match self.update_config() { - Ok(_) => sender.output(Self::Output::ScrollToSelectVoiceovers), - - Err(err) => sender.output(Self::Output::Toast { - title: tr("config-update-error"), - description: Some(err.to_string()) - }) - }; + Ok(_) => { + if self.migrate_installation { + self.progress_bar.sender().send(ProgressBarMsg::SetVisible(true)); + + self.show_progress = true; + + let folders = [ + (old_config.game.wine.builds, &self.runners), + (old_config.game.dxvk.builds, &self.dxvks), + (old_config.game.wine.prefix, &self.prefix), + (old_config.game.path.global, &self.game_global), + (old_config.game.path.china, &self.game_china), + (old_config.components.path, &self.components), + (old_config.patch.path, &self.patch), + + (old_config.game.enhancements.fps_unlocker.path, &self.fps_unlocker) + ]; + + fn move_folder(from: &Path, to: &Path) -> std::io::Result<()> { + if !to.exists() { + std::fs::create_dir_all(to); + } + + for entry in from.read_dir()?.flatten() { + let to_path = to.join(entry.file_name()); + + if entry.metadata()?.is_dir() { + move_folder(&entry.path(), &to_path)?; + } + + else if entry.metadata()?.is_file() { + std::fs::copy(entry.path(), &to_path); + } + + // TODO: symlinks? + } + + std::fs::remove_dir_all(from)?; + + Ok(()) + } + + #[allow(clippy::expect_fun_call)] + for (i, (from, to)) in folders.iter().enumerate() { + self.progress_bar.sender().send(ProgressBarMsg::UpdateCaption(Some( + from.to_str().map(|str| str.to_string()).unwrap_or_else(|| format!("{:?}", from)) + ))); + + if &from != to && from.exists() { + move_folder(from, to).expect(&format!("Failed to move folder: {:?} -> {:?}", from, to)); + } + + self.progress_bar.sender().send(ProgressBarMsg::UpdateProgress(i as u64 + 1, folders.len() as u64)); + } + + // Restart the app + + std::process::Command::new(std::env::current_exe().unwrap()).spawn().unwrap(); + + relm4::main_application().quit(); + } + + else { + sender.output(Self::Output::ScrollToSelectVoiceovers); + } + } + + Err(err) => { + sender.output(Self::Output::Toast { + title: tr("config-update-error"), + description: Some(err.to_string()) + }); + } + } } - DefaultPathsAppMsg::Exit => relm4::main_application().quit() + DefaultPathsAppMsg::Exit => { + if self.migrate_installation { + // TODO: this shit should return message to general preferences component somehow to close MigrateInstallation window + todo!(); + } + + else { + relm4::main_application().quit(); + } + } } } } diff --git a/src/ui/first_run/main.rs b/src/ui/first_run/main.rs index f93099d..43b8b9f 100644 --- a/src/ui/first_run/main.rs +++ b/src/ui/first_run/main.rs @@ -145,7 +145,7 @@ impl SimpleComponent for FirstRunApp { .forward(sender.input_sender(), std::convert::identity), default_paths: DefaultPathsApp::builder() - .launch(()) + .launch(false) .forward(sender.input_sender(), std::convert::identity), select_voiceovers: SelectVoiceoversApp::builder() diff --git a/src/ui/migrate_installation.rs b/src/ui/migrate_installation.rs index 76187a2..4b97f94 100644 --- a/src/ui/migrate_installation.rs +++ b/src/ui/migrate_installation.rs @@ -2,9 +2,6 @@ use relm4::prelude::*; use relm4::component::*; use gtk::prelude::*; -use adw::prelude::*; - -use crate::*; use super::first_run::default_paths::DefaultPathsApp; @@ -12,21 +9,17 @@ pub struct MigrateInstallationApp { default_paths: AsyncController, } -#[derive(Debug)] -pub enum MigrateInstallationAppMsg { - Migrate -} - #[relm4::component(pub)] impl SimpleComponent for MigrateInstallationApp { type Init = (); - type Input = MigrateInstallationAppMsg; + type Input = (); type Output = (); view! { adw::Window { set_default_size: (780, 560), set_modal: true, + set_hide_on_close: true, #[watch] set_title: Some("Migrate installation"), @@ -46,13 +39,13 @@ impl SimpleComponent for MigrateInstallationApp { fn init( _init: Self::Init, root: &Self::Root, - sender: ComponentSender, + _sender: ComponentSender, ) -> ComponentParts { tracing::info!("Initializing migration window"); let model = Self { default_paths: DefaultPathsApp::builder() - .launch(()) + .launch(true) .detach() }; @@ -60,12 +53,4 @@ impl SimpleComponent for MigrateInstallationApp { ComponentParts { model, widgets } } - - fn update(&mut self, msg: Self::Input, _sender: ComponentSender) { - match msg { - MigrateInstallationAppMsg::Migrate => { - todo!() - } - } - } }