epik/gui/
app.rs

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    // Shared download progress updates from background threads
65    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        // Apply theme from config
74        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        // Initialize localizer with configured language
82        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        // Check if already authenticated
95        let is_authenticated = auth.is_authenticated();
96
97        // Load settings from config
98        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; // Not in config yet
110        settings_view.wine_prefix_per_game = false; // Not in config yet
111        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        // Inject localizer into views
153        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        // Load library and installed games if already authenticated
161        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        // Use GameManager to benefit from token auto-refresh
184        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                // Reset update tracking when installed set changes
207                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            // Avoid spawning duplicate promises for the same app
223            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        // Empty path represents auto-detect
256        options.push(("Auto-detect".to_string(), String::new()));
257        // Special sentinel to trigger file picker
258        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            // Skip if already cached or already loading
275            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            // Stop loading if cache is getting large
285            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        // Enrich game details in background for games with catalog info
330        for game in &self.library_games {
331            // Only enrich if we have catalog info and title looks like UUID
332            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        // Runs installation in background and updates UI
368        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        // Add to download view
382        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            // Create progress callback
397            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        // Record launch stats
427        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            // Use legendary CLI for updates
453            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            // Create progress callback
524            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            // Update config with settings from UI
592            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            // Wine settings not fully mapped yet
603            config.enable_dxvk = self.settings_view.dxvk_enabled;
604            config.enable_esync = self.settings_view.esync_enabled;
605            config.enable_fsync = false; // Not in SettingsView yet
606            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            // Apply theme change immediately
614            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            // Apply language change immediately
622            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            // Update localizer in all views
632            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            // Save to disk
639            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        // Check for library loading completion
654        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                        // Download images for games that have image URLs
662                        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        // Sidebar panel
718        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(), // Other items not yet implemented
735                        };
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                    // Update download view with current progress
785                    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            // Status bar at bottom using StatusBar component
863            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        // Handle installation completion without mutating self during iteration
871        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        // Process completed installations
882        for (idx, app_name, maybe_err) in completed.into_iter().rev() {
883            let _ = self.install_promises.remove(idx);
884            // Clean up cancellation flag
885            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                    // Remove from download view
893                    self.download_view.remove_download(&app_name);
894                    // Clean up progress tracking
895                    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                    // Remove from download view even on error
904                    self.download_view.remove_download(&app_name);
905                    // Clean up progress tracking
906                    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        // Handle verification completion
917        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        // Handle game enrichment completion
955        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                // Update the game in library_games with enriched data
971                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        // Handle image loading completion
986        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        // Handle update checks completion
1010        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        // Request repaint for animations/updates
1040        ctx.request_repaint_after(std::time::Duration::from_millis(100));
1041    }
1042}