1use eframe::egui;
2use poll_promise::Promise;
3use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex};
6
7use crate::api::Game;
8use crate::auth::AuthManager;
9use crate::config::Config;
10use crate::downloader::DownloadProgress as DlProgress;
11use crate::games::{DownloadProgressCallback, GameManager, InstalledGame};
12use crate::i18n::{Language, Localizer};
13use crate::launcher::discover_wine_installations;
14use crate::legendary::cli::LegendaryCLI;
15use crate::Result;
16
17use super::auth_view::AuthView;
18use super::components::{
19 AchievementsView, Header, SidebarItem, SidebarState, StatsView, StatusBar, StoreView,
20};
21use super::download_view::DownloadView;
22
23use super::library_view::{LibraryAction, LibraryView};
24use super::settings_view::{SettingsAction, SettingsView};
25use super::styles;
26
27type ImageData = (usize, usize, Vec<u8>);
28
29#[derive(Clone, Copy)]
30enum AppState {
31 Login,
32 Library,
33 Store,
34 Downloads,
35 Settings,
36}
37
38pub struct LauncherApp {
39 state: AppState,
40 auth: Arc<Mutex<AuthManager>>,
41 config: Arc<Mutex<Config>>,
42 localizer: Localizer,
43 auth_view: AuthView,
44 library_view: LibraryView,
45 download_view: DownloadView,
46 store_view: StoreView,
47
48 sidebar_state: SidebarState,
49
50 settings_view: SettingsView,
51 library_games: Vec<Game>,
52 installed_games: Vec<InstalledGame>,
53 status_message: String,
54 loading_library: bool,
55 library_promise: Option<Promise<Result<Vec<Game>>>>,
56 install_promises: Vec<(String, Promise<Result<()>>)>,
57 image_promises: Vec<(String, Promise<Option<ImageData>>)>,
58 verify_promises: Vec<(String, Promise<Result<Vec<String>>>)>,
59 enrich_promises: Vec<(String, Promise<Result<Game>>)>,
60 update_promises: Vec<(String, Promise<Result<Option<String>>>)>,
61 updates_available: HashMap<String, String>,
62 updates_checked: HashSet<String>,
63 wine_options: Vec<(String, String)>,
64 download_progress: Arc<Mutex<std::collections::HashMap<String, DlProgress>>>,
66 install_cancel_flags: Arc<Mutex<HashMap<String, Arc<AtomicBool>>>>,
67}
68
69impl LauncherApp {
70 pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
71 let config = Config::load().unwrap_or_default();
72
73 let theme = match config.theme.as_str() {
75 "Dark" => styles::Theme::Dark,
76 "Light" => styles::Theme::Light,
77 _ => styles::Theme::Epic,
78 };
79 theme.apply(&cc.egui_ctx);
80
81 let mut localizer = Localizer::new();
83 let language = match config.language.as_str() {
84 "it" => Language::Italian,
85 "fr" => Language::French,
86 "de" => Language::German,
87 "es" => Language::Spanish,
88 _ => Language::English,
89 };
90 localizer.set_language(language);
91
92 let auth = AuthManager::new().unwrap_or_default();
93
94 let is_authenticated = auth.is_authenticated();
96
97 let mut settings_view = SettingsView::default();
99 settings_view.auto_update = config.auto_update;
100 settings_view.start_minimized = config.minimize_to_tray;
101 settings_view.close_to_tray = config.close_to_tray;
102 settings_view.language = config.language.clone();
103 settings_view.theme = config.theme.clone();
104 settings_view.max_concurrent_downloads = config.max_concurrent_downloads as usize;
105 settings_view.download_threads = config.download_threads as usize;
106 settings_view.bandwidth_limit_enabled = config.enable_bandwidth_limit;
107 settings_view.bandwidth_limit_mbps = config.bandwidth_limit_mbps as f32;
108 settings_view.cdn_region = config.cdn_region.clone();
109 settings_view.auto_detect_wine = true; settings_view.wine_prefix_per_game = false; settings_view.dxvk_enabled = config.enable_dxvk;
112 settings_view.esync_enabled = config.enable_esync;
113 settings_view.log_level = config.log_level.clone();
114 settings_view.enable_telemetry = !config.disable_telemetry;
115 settings_view.log_to_file = config.log_to_file;
116 settings_view.crash_reporting = config.enable_crash_reporting;
117 settings_view.privacy_mode = config.privacy_mode;
118
119 let mut app = Self {
120 state: if is_authenticated {
121 AppState::Library
122 } else {
123 AppState::Login
124 },
125 auth: Arc::new(Mutex::new(auth)),
126 config: Arc::new(Mutex::new(config)),
127 localizer: localizer.clone(),
128 auth_view: AuthView::default(),
129 library_view: LibraryView::default(),
130 download_view: DownloadView::default(),
131 store_view: StoreView::default(),
132 sidebar_state: SidebarState::default(),
133
134 settings_view,
135 library_games: Vec::new(),
136 installed_games: Vec::new(),
137 status_message: String::new(),
138 loading_library: false,
139 library_promise: None,
140 install_promises: Vec::new(),
141 image_promises: Vec::new(),
142 verify_promises: Vec::new(),
143 enrich_promises: Vec::new(),
144 update_promises: Vec::new(),
145 updates_available: HashMap::new(),
146 updates_checked: HashSet::new(),
147 wine_options: Self::load_wine_options(),
148 download_progress: Arc::new(Mutex::new(std::collections::HashMap::new())),
149 install_cancel_flags: Arc::new(Mutex::new(HashMap::new())),
150 };
151
152 let localizer_arc = Arc::new(localizer);
154 app.library_view.set_localizer(Arc::clone(&localizer_arc));
155 app.download_view.set_localizer(Arc::clone(&localizer_arc));
156 app.settings_view.set_localizer(Arc::clone(&localizer_arc));
157 app.library_view
158 .set_privacy_mode(app.config.lock().unwrap().privacy_mode);
159
160 if is_authenticated {
162 app.load_library();
163 app.load_installed_games();
164 }
165
166 app
167 }
168
169 fn handle_login(&mut self) {
170 self.state = AppState::Library;
171 self.load_library();
172 self.load_installed_games();
173 }
174
175 fn load_library(&mut self) {
176 if self.loading_library {
177 return;
178 }
179
180 self.loading_library = true;
181 self.status_message = "Loading library...".to_string();
182
183 let config = (*self.config.lock().unwrap()).clone();
185 let auth = (*self.auth.lock().unwrap()).clone();
186
187 self.library_promise = Some(Promise::spawn_thread("load_library", move || {
188 let rt = tokio::runtime::Runtime::new()
189 .expect("Failed to create Tokio runtime for library load");
190 rt.block_on(async move {
191 match GameManager::new(config, auth) {
192 Ok(mut manager) => manager.list_library().await,
193 Err(e) => Err(e),
194 }
195 })
196 }));
197 }
198
199 fn load_installed_games(&mut self) {
200 let config = (*self.config.lock().unwrap()).clone();
201 let auth = (*self.auth.lock().unwrap()).clone();
202
203 if let Ok(manager) = GameManager::new(config, auth) {
204 if let Ok(games) = manager.list_installed() {
205 self.installed_games = games;
206 self.updates_available
208 .retain(|name, _| self.installed_games.iter().any(|g| &g.app_name == name));
209 self.updates_checked.clear();
210 self.update_promises.clear();
211 self.start_update_checks();
212 }
213 }
214 }
215
216 fn start_update_checks(&mut self) {
217 for game in &self.installed_games {
218 if self.updates_checked.contains(&game.app_name) {
219 continue;
220 }
221
222 if self
224 .update_promises
225 .iter()
226 .any(|(name, _)| name == &game.app_name)
227 {
228 continue;
229 }
230
231 let app_name = game.app_name.clone();
232 let app_name_for_rt = app_name.clone();
233 let config = Arc::clone(&self.config);
234 let auth = Arc::clone(&self.auth);
235 let promise = Promise::spawn_thread("check_updates", move || {
236 let rt = tokio::runtime::Runtime::new()
237 .expect("Failed to create Tokio runtime for update check");
238 let config = (*config.lock().unwrap()).clone();
239 let auth = (*auth.lock().unwrap()).clone();
240
241 rt.block_on(async move {
242 match GameManager::new(config, auth) {
243 Ok(manager) => manager.check_for_updates(&app_name_for_rt).await,
244 Err(e) => Err(e),
245 }
246 })
247 });
248
249 self.update_promises.push((app_name, promise));
250 }
251 }
252
253 fn load_wine_options() -> Vec<(String, String)> {
254 let mut options = Vec::new();
255 options.push(("Auto-detect".to_string(), String::new()));
257 options.push((
259 "Browse custom path...".to_string(),
260 "__custom__".to_string(),
261 ));
262
263 for entry in discover_wine_installations() {
264 options.push((entry.name, entry.path.to_string_lossy().to_string()));
265 }
266
267 options
268 }
269
270 fn load_game_images(&mut self, _ctx: &egui::Context) {
271 const MAX_CACHED_IMAGES: usize = 100;
272
273 for game in &self.library_games {
274 if self.library_view.image_cache.contains_key(&game.app_name)
276 || self
277 .image_promises
278 .iter()
279 .any(|(name, _)| name == &game.app_name)
280 {
281 continue;
282 }
283
284 if self.library_view.image_cache.len() >= MAX_CACHED_IMAGES {
286 break;
287 }
288
289 if let Some(image_url) = &game.image_url {
290 log::info!("Loading image for {}: {}", game.app_name, image_url);
291 let app_name = game.app_name.clone();
292 let app_name_for_log = app_name.clone();
293 let url = image_url.clone();
294
295 let promise = Promise::spawn_thread("load_image", move || {
296 let name = app_name_for_log.clone();
297 match reqwest::blocking::get(&url) {
298 Ok(response) => match response.bytes() {
299 Ok(bytes) => match image::load_from_memory(&bytes) {
300 Ok(img) => {
301 let rgba = img.to_rgba8();
302 let size = rgba.dimensions();
303 Some((size.0 as usize, size.1 as usize, rgba.into_raw()))
304 }
305 Err(e) => {
306 log::error!("Failed to decode image for {}: {}", name, e);
307 None
308 }
309 },
310 Err(e) => {
311 log::error!("Failed to download image for {}: {}", name, e);
312 None
313 }
314 },
315 Err(e) => {
316 log::error!("Failed to fetch image for {}: {}", name, e);
317 None
318 }
319 }
320 });
321
322 self.image_promises.push((app_name, promise));
323 }
324 }
325 }
326
327 #[allow(dead_code)]
328 fn enrich_game_details_background(&mut self) {
329 for game in &self.library_games {
331 if let (Some(namespace), Some(catalog_id)) = (&game.namespace, &game.catalog_item_id) {
333 if game.app_name.len() == 32 && game.app_name.chars().all(|c| c.is_alphanumeric()) {
334 log::info!("Starting enrichment for UUID game: {}", game.app_name);
335 let app_name = game.app_name.clone();
336 let ns = namespace.clone();
337 let cat_id = catalog_id.clone();
338 let config = Arc::clone(&self.config);
339 let auth = Arc::clone(&self.auth);
340
341 let app_name_for_promise = app_name.clone();
342 let promise = Promise::spawn_thread("enrich_game", move || {
343 let rt = tokio::runtime::Runtime::new()
344 .expect("Failed to create Tokio runtime for enrich");
345 let config = (*config.lock().unwrap()).clone();
346 let auth = (*auth.lock().unwrap()).clone();
347
348 rt.block_on(async move {
349 match GameManager::new(config, auth) {
350 Ok(mut manager) => {
351 manager
352 .enrich_game(&app_name_for_promise, &ns, &cat_id)
353 .await
354 }
355 Err(e) => Err(e),
356 }
357 })
358 });
359
360 self.enrich_promises.push((app_name, promise));
361 }
362 }
363 }
364 }
365
366 fn handle_install(&mut self, app_name: String) {
367 let config = Arc::clone(&self.config);
369 let auth = Arc::clone(&self.auth);
370 let download_progress = Arc::clone(&self.download_progress);
371 let cancel_flag = Arc::new(AtomicBool::new(false));
372 self.install_cancel_flags
373 .lock()
374 .unwrap()
375 .insert(app_name.clone(), cancel_flag.clone());
376 let mut library_view = self.library_view.clone();
377 library_view.mark_installation_started(&app_name);
378 self.library_view = library_view.clone();
379 self.status_message = format!("Installation started for {}...", app_name);
380
381 self.download_view.add_download(app_name.clone());
383
384 let app_name_clone = app_name.clone();
385 let app_name_for_callback = app_name.clone();
386 let cancel_flag_thread = cancel_flag.clone();
387
388 let promise = Promise::spawn_thread("install_game", move || {
389 let rt =
390 tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime for install");
391 let config = (*config.lock().unwrap()).clone();
392 let auth = (*auth.lock().unwrap()).clone();
393 let dl_progress = Arc::clone(&download_progress);
394 let game_name = app_name_for_callback.clone();
395
396 let callback: DownloadProgressCallback = Arc::new(move |progress: DlProgress| {
398 if let Ok(mut map) = dl_progress.lock() {
399 map.insert(game_name.clone(), progress);
400 }
401 });
402
403 rt.block_on(async move {
404 match GameManager::new(config, auth) {
405 Ok(mut manager) => {
406 manager
407 .install_game_with_callback(
408 &app_name_clone,
409 Some(callback),
410 Some(cancel_flag_thread),
411 )
412 .await
413 }
414 Err(e) => Err(e),
415 }
416 })
417 });
418
419 self.install_promises.push((app_name, promise));
420 }
421
422 fn handle_launch(&mut self, app_name: String) {
423 let config = (*self.config.lock().unwrap()).clone();
424 let auth = (*self.auth.lock().unwrap()).clone();
425
426 if let Ok(mut stats) = crate::stats::GameStats::load(&config, &app_name) {
428 stats.record_launch();
429 let _ = stats.save(&config);
430 }
431
432 match GameManager::new(config, auth) {
433 Ok(manager) => match manager.launch_game(&app_name) {
434 Ok(()) => {
435 self.status_message = format!("Launched {}", app_name);
436 }
437 Err(e) => {
438 self.status_message = format!("Failed to launch {}: {}", app_name, e);
439 }
440 },
441 Err(e) => {
442 self.status_message = format!("Error: {}", e);
443 }
444 }
445 }
446
447 fn handle_update(&mut self, app_name: String) {
448 self.status_message = format!("Updating {}...", app_name);
449
450 let app_name_clone = app_name.clone();
451 let promise = Promise::spawn_thread("update_game", move || {
452 let legendary = LegendaryCLI::new();
454 legendary.update_game(&app_name_clone)
455 });
456
457 self.install_promises.push((app_name, promise));
458 }
459
460 fn handle_uninstall(&mut self, app_name: String) {
461 let config = (*self.config.lock().unwrap()).clone();
462 let auth = (*self.auth.lock().unwrap()).clone();
463
464 match GameManager::new(config, auth) {
465 Ok(manager) => match manager.uninstall_game(&app_name) {
466 Ok(()) => {
467 self.status_message = format!("Uninstalled {}", app_name);
468 self.load_installed_games();
469 }
470 Err(e) => {
471 self.status_message = format!("Failed to uninstall {}: {}", app_name, e);
472 }
473 },
474 Err(e) => {
475 self.status_message = format!("Error: {}", e);
476 }
477 }
478 }
479
480 fn handle_verify(&mut self, app_name: String) {
481 let config = Arc::clone(&self.config);
482 let auth = Arc::clone(&self.auth);
483
484 self.status_message = format!("Verifying {}...", app_name);
485
486 let app_name_clone = app_name.clone();
487 let promise = Promise::spawn_thread("verify_game", move || {
488 let rt =
489 tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime for verify");
490 let config = (*config.lock().unwrap()).clone();
491 let auth = (*auth.lock().unwrap()).clone();
492
493 rt.block_on(async move {
494 match GameManager::new(config, auth) {
495 Ok(mut manager) => manager.verify_game(&app_name_clone).await,
496 Err(e) => Err(e),
497 }
498 })
499 });
500
501 self.verify_promises.push((app_name, promise));
502 }
503
504 fn handle_repair(&mut self, app_name: String) {
505 let config = Arc::clone(&self.config);
506 let auth = Arc::clone(&self.auth);
507 let download_progress = Arc::clone(&self.download_progress);
508
509 self.status_message = format!("Repairing {}...", app_name);
510 self.download_view.add_download(app_name.clone());
511
512 let app_name_clone = app_name.clone();
513 let app_name_for_callback = app_name.clone();
514
515 let promise = Promise::spawn_thread("repair_game", move || {
516 let rt =
517 tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime for repair");
518 let config = (*config.lock().unwrap()).clone();
519 let auth = (*auth.lock().unwrap()).clone();
520 let dl_progress = Arc::clone(&download_progress);
521 let game_name = app_name_for_callback.clone();
522
523 let callback = Arc::new(move |progress: DlProgress| {
525 if let Ok(mut map) = dl_progress.lock() {
526 map.insert(game_name.clone(), progress);
527 }
528 });
529
530 rt.block_on(async move {
531 match GameManager::new(config, auth) {
532 Ok(mut manager) => manager.repair_game(&app_name_clone, Some(callback)).await,
533 Err(e) => Err(e),
534 }
535 })
536 });
537
538 self.install_promises.push((app_name, promise));
539 }
540
541 fn handle_set_wine(&mut self, app_name: String, wine_path: Option<String>) {
542 let config = (*self.config.lock().unwrap()).clone();
543 let auth = (*self.auth.lock().unwrap()).clone();
544
545 match GameManager::new(config, auth) {
546 Ok(manager) => match manager.set_game_wine(&app_name, wine_path.clone()) {
547 Ok(()) => {
548 self.status_message = if let Some(path) = wine_path {
549 format!("Wine/Proton set for {} -> {}", app_name, path)
550 } else {
551 format!("Wine/Proton reset to auto for {}", app_name)
552 };
553 self.load_installed_games();
554 }
555 Err(e) => {
556 self.status_message = format!("Failed to set Wine/Proton: {}", e);
557 }
558 },
559 Err(e) => {
560 self.status_message = format!("Error: {}", e);
561 }
562 }
563 }
564
565 fn handle_toggle_cloud_save(&mut self, app_name: String, enabled: bool) {
566 let config = (*self.config.lock().unwrap()).clone();
567 let auth = (*self.auth.lock().unwrap()).clone();
568
569 match GameManager::new(config, auth) {
570 Ok(manager) => match manager.set_cloud_save_enabled(&app_name, enabled) {
571 Ok(()) => {
572 self.status_message = format!(
573 "Cloud saves {} for {}",
574 if enabled { "enabled" } else { "disabled" },
575 app_name
576 );
577 self.load_installed_games();
578 }
579 Err(e) => {
580 self.status_message = format!("Failed to update cloud saves: {}", e);
581 }
582 },
583 Err(e) => {
584 self.status_message = format!("Error: {}", e);
585 }
586 }
587 }
588
589 fn save_settings(&mut self, ctx: &egui::Context) {
590 if let Ok(mut config) = self.config.lock() {
591 config.auto_update = self.settings_view.auto_update;
593 config.minimize_to_tray = self.settings_view.start_minimized;
594 config.close_to_tray = self.settings_view.close_to_tray;
595 config.language = self.settings_view.language.clone();
596 config.theme = self.settings_view.theme.clone();
597 config.max_concurrent_downloads = self.settings_view.max_concurrent_downloads as u32;
598 config.download_threads = self.settings_view.download_threads as u32;
599 config.enable_bandwidth_limit = self.settings_view.bandwidth_limit_enabled;
600 config.bandwidth_limit_mbps = self.settings_view.bandwidth_limit_mbps as u32;
601 config.cdn_region = self.settings_view.cdn_region.clone();
602 config.enable_dxvk = self.settings_view.dxvk_enabled;
604 config.enable_esync = self.settings_view.esync_enabled;
605 config.enable_fsync = false; config.log_level = self.settings_view.log_level.to_lowercase();
607 config.log_to_file = self.settings_view.log_to_file;
608 config.enable_crash_reporting = self.settings_view.crash_reporting;
609 config.privacy_mode = self.settings_view.privacy_mode;
610 config.telemetry_opt_in = self.settings_view.enable_telemetry;
611 config.disable_telemetry = !self.settings_view.enable_telemetry;
612
613 let theme = match config.theme.as_str() {
615 "Dark" => styles::Theme::Dark,
616 "Light" => styles::Theme::Light,
617 _ => styles::Theme::Epic,
618 };
619 theme.apply(ctx);
620
621 let language = match config.language.as_str() {
623 "it" => Language::Italian,
624 "fr" => Language::French,
625 "de" => Language::German,
626 "es" => Language::Spanish,
627 _ => Language::English,
628 };
629 self.localizer.set_language(language);
630
631 let localizer_arc = Arc::new(self.localizer.clone());
633 self.library_view.set_localizer(Arc::clone(&localizer_arc));
634 self.download_view.set_localizer(Arc::clone(&localizer_arc));
635 self.settings_view.set_localizer(Arc::clone(&localizer_arc));
636 self.library_view.set_privacy_mode(config.privacy_mode);
637
638 match config.save() {
640 Ok(()) => {
641 self.status_message = format!("Settings saved successfully");
642 }
643 Err(e) => {
644 self.status_message = format!("Failed to save settings: {}", e);
645 }
646 }
647 }
648 }
649}
650
651impl eframe::App for LauncherApp {
652 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
653 if let Some(promise) = &self.library_promise {
655 if let Some(result) = promise.ready() {
656 match result {
657 Ok(games) => {
658 self.library_games = games.clone();
659 self.status_message = "Library loaded successfully".to_string();
660
661 self.load_game_images(ctx);
663 }
664 Err(e) => {
665 self.status_message = format!("Failed to load library: {}", e);
666 }
667 }
668 self.loading_library = false;
669 self.library_promise = None;
670 }
671 }
672
673 egui::TopBottomPanel::top("top_panel")
674 .frame(
675 egui::Frame::NONE
676 .fill(egui::Color32::from_rgb(22, 24, 28))
677 .inner_margin(egui::Margin::symmetric(20, 15)),
678 )
679 .show(ctx, |ui| {
680 let mut logout_requested = false;
681 let mut nav_button = None;
682 let is_authenticated = !matches!(self.state, AppState::Login);
683 let current_view = match self.state {
684 AppState::Library => super::components::header::NavButton::Library,
685 AppState::Store => super::components::header::NavButton::Library,
686 AppState::Downloads => super::components::header::NavButton::Downloads,
687 AppState::Settings => super::components::header::NavButton::Settings,
688 AppState::Login => super::components::header::NavButton::Library,
689 };
690 Header::show(
691 ui,
692 is_authenticated,
693 current_view,
694 &mut nav_button,
695 &mut logout_requested,
696 self.download_progress.lock().map(|m| m.len()).unwrap_or(0),
697 );
698
699 if logout_requested {
700 if let Ok(mut auth) = self.auth.lock() {
701 let _ = auth.logout();
702 }
703 self.state = AppState::Login;
704 self.library_games.clear();
705 self.installed_games.clear();
706 }
707
708 if let Some(nav) = nav_button {
709 self.state = match nav {
710 super::components::header::NavButton::Library => AppState::Library,
711 super::components::header::NavButton::Downloads => AppState::Downloads,
712 super::components::header::NavButton::Settings => AppState::Settings,
713 };
714 }
715 });
716
717 if !matches!(self.state, AppState::Login) {
719 egui::SidePanel::left("sidebar")
720 .resizable(false)
721 .default_width(if self.sidebar_state.collapsed { 60.0 } else { 240.0 })
722 .width_range(60.0..=240.0)
723 .show(ctx, |ui| {
724 if let Some(selected_item) = super::components::Sidebar::show(
725 ui,
726 &mut self.sidebar_state,
727 ui.available_height(),
728 ) {
729 self.state = match selected_item {
730 super::components::SidebarItem::Library => AppState::Library,
731 super::components::SidebarItem::Store => AppState::Store,
732 super::components::SidebarItem::Downloads => AppState::Downloads,
733 super::components::SidebarItem::Settings => AppState::Settings,
734 _ => self.state.clone(), };
736 }
737 });
738 }
739
740 egui::CentralPanel::default().show(ctx, |ui| {
741 match self.state {
742 AppState::Login => {
743 if self.auth_view.ui(ui, &mut self.auth.lock().unwrap()) {
744 self.handle_login();
745 }
746 }
747 AppState::Library => {
748 if let Some(action) = self.library_view.ui(
749 ui,
750 &self.library_games,
751 &self.installed_games,
752 &self.updates_available,
753 &self.wine_options,
754 ) {
755 match action {
756 LibraryAction::Install(app_name) => {
757 self.handle_install(app_name);
758 }
759 LibraryAction::Launch(app_name) => {
760 self.handle_launch(app_name);
761 }
762 LibraryAction::Update(app_name) => {
763 self.handle_update(app_name);
764 }
765 LibraryAction::Uninstall(app_name) => {
766 self.handle_uninstall(app_name);
767 }
768 LibraryAction::Verify(app_name) => {
769 self.handle_verify(app_name);
770 }
771 LibraryAction::Repair(app_name) => {
772 self.handle_repair(app_name);
773 }
774 LibraryAction::SetWine(app_name, wine_path) => {
775 self.handle_set_wine(app_name, wine_path);
776 }
777 LibraryAction::ToggleCloudSave(app_name, enabled) => {
778 self.handle_toggle_cloud_save(app_name, enabled);
779 }
780 }
781 }
782 }
783 AppState::Downloads => {
784 if let Ok(progress_map) = self.download_progress.lock() {
786 for (game_name, progress) in progress_map.iter() {
787 self.download_view
788 .update_progress(game_name, progress.clone());
789 }
790 }
791
792 if let Some(action) = self.download_view.show(ui) {
793 match action {
794 super::download_view::DownloadAction::Cancel(app_name) => {
795 if let Some(flag) = self
796 .install_cancel_flags
797 .lock()
798 .unwrap()
799 .get(&app_name)
800 .cloned()
801 {
802 flag.store(true, Ordering::Relaxed);
803 self.status_message =
804 format!("Cancelling download for {}...", app_name);
805 } else {
806 self.status_message = format!(
807 "No active download flag found for {}, unable to cancel",
808 app_name
809 );
810 }
811 }
812 }
813 }
814 }
815 AppState::Settings => {
816 if let Some(action) = self.settings_view.show(ui) {
817 match action {
818 SettingsAction::Save => {
819 self.save_settings(ctx);
820 }
821 SettingsAction::ResetDefaults => {
822 let default_config = Config::default();
823 self.settings_view.auto_update = default_config.auto_update;
824 self.settings_view.start_minimized =
825 default_config.minimize_to_tray;
826 self.settings_view.close_to_tray = default_config.close_to_tray;
827 self.settings_view.language = default_config.language;
828 self.settings_view.theme = default_config.theme;
829 self.settings_view.max_concurrent_downloads =
830 default_config.max_concurrent_downloads as usize;
831 self.settings_view.download_threads =
832 default_config.download_threads as usize;
833 self.settings_view.bandwidth_limit_enabled =
834 default_config.enable_bandwidth_limit;
835 self.settings_view.bandwidth_limit_mbps =
836 default_config.bandwidth_limit_mbps as f32;
837 self.settings_view.cdn_region = default_config.cdn_region;
838 self.settings_view.dxvk_enabled = default_config.enable_dxvk;
839 self.settings_view.esync_enabled = default_config.enable_esync;
840 self.settings_view.log_level = default_config.log_level;
841 self.settings_view.enable_telemetry =
842 !default_config.disable_telemetry;
843 self.settings_view.log_to_file = default_config.log_to_file;
844 self.settings_view.crash_reporting =
845 default_config.enable_crash_reporting;
846 self.settings_view.privacy_mode = default_config.privacy_mode;
847 self.status_message = "Settings reset to defaults".to_string();
848 }
849 SettingsAction::Close => {
850 self.state = AppState::Library;
851 }
852 }
853 }
854 }
855 AppState::Store => {
856 if let Some(app_name) = self.store_view.show(ui, &self.library_games) {
857 self.handle_install(app_name);
858 }
859 }
860 }
861
862 let mut clear_status = false;
864 StatusBar::show(ui, &self.status_message, &mut clear_status);
865 if clear_status {
866 self.status_message.clear();
867 }
868 });
869
870 let mut completed: Vec<(usize, String, Option<String>)> = Vec::new();
872 for (idx, (app_name, p)) in self.install_promises.iter().enumerate() {
873 if let Some(result) = p.ready() {
874 match result {
875 Ok(()) => completed.push((idx, app_name.clone(), None)),
876 Err(e) => completed.push((idx, app_name.clone(), Some(format!("{}", e)))),
877 }
878 }
879 }
880 let mut need_reload_installed = false;
881 for (idx, app_name, maybe_err) in completed.into_iter().rev() {
883 let _ = self.install_promises.remove(idx);
884 if let Ok(mut flags) = self.install_cancel_flags.lock() {
886 flags.remove(&app_name);
887 }
888 match maybe_err {
889 None => {
890 self.status_message = format!("Installation completed for {}", app_name);
891 self.library_view.mark_installation_complete(&app_name);
892 self.download_view.remove_download(&app_name);
894 if let Ok(mut map) = self.download_progress.lock() {
896 map.remove(&app_name);
897 }
898 need_reload_installed = true;
899 }
900 Some(err) => {
901 self.status_message = format!("Installation failed for {}: {}", app_name, err);
902 self.library_view.mark_installation_complete(&app_name);
903 self.download_view.remove_download(&app_name);
905 if let Ok(mut map) = self.download_progress.lock() {
907 map.remove(&app_name);
908 }
909 }
910 }
911 }
912 if need_reload_installed {
913 self.load_installed_games();
914 }
915
916 let mut verified: Vec<(usize, String, Option<String>)> = Vec::new();
918 for (idx, (app_name, p)) in self.verify_promises.iter().enumerate() {
919 if let Some(result) = p.ready() {
920 match result {
921 Ok(corrupted_files) => {
922 if corrupted_files.is_empty() {
923 verified.push((idx, app_name.clone(), None));
924 } else {
925 let msg = format!(
926 "Found {} corrupted file(s): {}",
927 corrupted_files.len(),
928 corrupted_files.join(", ")
929 );
930 verified.push((idx, app_name.clone(), Some(msg)));
931 }
932 }
933 Err(e) => verified.push((
934 idx,
935 app_name.clone(),
936 Some(format!("Verification failed: {}", e)),
937 )),
938 }
939 }
940 }
941 for (idx, app_name, maybe_msg) in verified.into_iter().rev() {
942 let _ = self.verify_promises.remove(idx);
943 match maybe_msg {
944 None => {
945 self.status_message =
946 format!("{} - All files verified successfully ✓", app_name);
947 }
948 Some(msg) => {
949 self.status_message = format!("{} - {}", app_name, msg);
950 }
951 }
952 }
953
954 let mut enriched: Vec<(usize, String, Option<Game>)> = Vec::new();
956 for (idx, (app_name, p)) in self.enrich_promises.iter().enumerate() {
957 if let Some(result) = p.ready() {
958 match result {
959 Ok(game) => enriched.push((idx, app_name.clone(), Some(game.clone()))),
960 Err(e) => {
961 log::debug!("Failed to enrich {}: {}", app_name, e);
962 enriched.push((idx, app_name.clone(), None));
963 }
964 }
965 }
966 }
967 for (idx, app_name, maybe_game) in enriched.into_iter().rev() {
968 let _ = self.enrich_promises.remove(idx);
969 if let Some(enriched_game) = maybe_game {
970 if let Some(game) = self
972 .library_games
973 .iter_mut()
974 .find(|g| g.app_name == app_name)
975 {
976 game.app_title = enriched_game.app_title;
977 if enriched_game.image_url.is_some() {
978 game.image_url = enriched_game.image_url;
979 }
980 log::info!("Enriched game: {} -> {}", app_name, game.app_title);
981 }
982 }
983 }
984
985 let mut loaded_images: Vec<(usize, String, Option<ImageData>)> = Vec::new();
987 for (idx, (app_name, promise)) in self.image_promises.iter().enumerate() {
988 if let Some(image_data) = promise.ready() {
989 loaded_images.push((idx, app_name.clone(), image_data.clone()));
990 }
991 }
992 for (idx, app_name, image_data) in loaded_images.into_iter().rev() {
993 let _ = self.image_promises.remove(idx);
994 if let Some((width, height, pixels)) = image_data {
995 log::info!("Image loaded for {}: {}x{}", app_name, width, height);
996 let color_image =
997 egui::ColorImage::from_rgba_unmultiplied([width, height], &pixels);
998 let texture =
999 ctx.load_texture(&app_name, color_image, egui::TextureOptions::default());
1000 self.library_view
1001 .image_cache
1002 .insert(app_name.clone(), texture);
1003 log::info!("Texture cached for {}", app_name);
1004 } else {
1005 log::warn!("No image data received for {}", app_name);
1006 }
1007 }
1008
1009 let mut update_done: Vec<(usize, String, std::result::Result<Option<String>, String>)> =
1011 Vec::new();
1012 for (idx, (app_name, p)) in self.update_promises.iter().enumerate() {
1013 if let Some(result) = p.ready() {
1014 let owned: std::result::Result<Option<String>, String> = match result {
1015 Ok(opt) => Ok(opt.clone()),
1016 Err(e) => Err(e.to_string()),
1017 };
1018 update_done.push((idx, app_name.clone(), owned));
1019 }
1020 }
1021 for (idx, app_name, result) in update_done.into_iter().rev() {
1022 let _ = self.update_promises.remove(idx);
1023 self.updates_checked.insert(app_name.clone());
1024 match result {
1025 Ok(Some(version)) => {
1026 self.updates_available
1027 .insert(app_name.clone(), version.clone());
1028 self.status_message = format!("Update available for {}: {}", app_name, version);
1029 }
1030 Ok(None) => {
1031 self.updates_available.remove(&app_name);
1032 }
1033 Err(err_msg) => {
1034 log::warn!("Update check failed for {}: {}", app_name, err_msg);
1035 }
1036 }
1037 }
1038
1039 ctx.request_repaint_after(std::time::Duration::from_millis(100));
1041 }
1042}