launcher/pages/troubleshoot/
ui.rs1use std::collections::BTreeMap;
2
3use crate::pages::troubleshoot::tasks::{
4 LocalProfileInfo, SnapshotRequest, start_create_snapshot, start_refresh_profiles,
5 start_restore_snapshot, start_upload_snapshot,
6};
7
8use crate::pages::troubleshoot::types::TroubleshootPageState;
9use eframe::egui;
10use supreme_fs::GameStorage;
11
12#[cfg(feature = "local")]
13use crate::pages::troubleshoot::tasks::{run_cleaner_task, start_path_scan};
14
15#[allow(clippy::too_many_lines)]
16pub fn troubleshoot_page(
17 ui: &mut egui::Ui,
18 state: &mut TroubleshootPageState,
19 storage: &GameStorage,
20) {
21 state.update(storage);
22
23 let has_token = std::env::var("SSBZ_API_TOKEN").is_ok();
24
25 ui.horizontal(|ui| {
28 ui.heading("Snapshots");
29 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
30 if ui
31 .add_enabled(
32 !state.is_running && !state.profile_refresh_active,
33 egui::Button::new("๐ Refresh List"),
34 )
35 .clicked()
36 {
37 state.has_attempted_profile_refresh = true;
38 start_refresh_profiles(ui.ctx().clone(), state, storage);
39 }
40
41 if state.profile_refresh_active {
42 ui.add(egui::Spinner::new());
43 }
44
45 if state.available_profiles.is_empty()
47 && !state.is_running
48 && !state.profile_refresh_active
49 && !state.has_attempted_profile_refresh
50 {
51 state.has_attempted_profile_refresh = true;
52 start_refresh_profiles(ui.ctx().clone(), state, storage);
53 }
54 });
55 });
56
57 ui.label("Manage game file snapshots to easily restore or debug configurations.");
58 ui.add_space(10.0);
59
60 let box_height = 140.0;
61
62 ui.columns(2, |cols| {
63 cols[0].vertical(|ui| {
65 ui.label(egui::RichText::new("Available Snapshots:").strong());
66 ui.add_space(2.0);
67 egui::Frame::group(ui.style())
68 .fill(ui.visuals().extreme_bg_color)
69 .show(ui, |ui| {
70 ui.set_min_height(box_height);
71 ui.set_max_height(box_height);
72 ui.set_width(ui.available_width());
73 egui::ScrollArea::vertical()
74 .id_salt("snapshot_list_scroll")
75 .max_height(box_height)
76 .min_scrolled_height(box_height)
77 .auto_shrink([false, true])
78 .show(ui, |ui| {
79 if state.available_profiles.is_empty() {
80 ui.label("No snapshots found.");
81 } else {
82 let mut grouped: BTreeMap<String, Vec<&LocalProfileInfo>> =
83 BTreeMap::new();
84 for profile in &state.available_profiles {
85 let name = profile
86 .name
87 .as_deref()
88 .unwrap_or(&profile.filename)
89 .to_string();
90 grouped.entry(name).or_default().push(profile);
91 }
92
93 for (name, mut profiles) in grouped {
94 profiles.sort_by(|a, b| b.version.cmp(&a.version));
95
96 let active_filename = state
98 .group_versions
99 .get(&name)
100 .cloned()
101 .unwrap_or_else(|| profiles[0].filename.clone());
102
103 let active_profile = profiles
105 .iter()
106 .find(|p| p.filename == active_filename)
107 .copied()
108 .unwrap_or(profiles[0]);
109
110 ui.horizontal(|ui| {
111 let mut label = egui::RichText::new(name.clone());
112 if !active_profile.is_valid {
113 label = label.color(egui::Color32::RED);
114 }
115
116 let is_selected = Some(&active_profile.filename)
117 == state.selected_profile.as_ref();
118 if ui.selectable_label(is_selected, label).clicked() {
119 state.selected_profile =
120 Some(active_profile.filename.clone());
121 state.group_versions.insert(
122 name.clone(),
123 active_profile.filename.clone(),
124 );
125 }
126
127 ui.with_layout(
128 egui::Layout::right_to_left(egui::Align::Center),
129 |ui| {
130 if profiles.len() > 1 {
132 egui::ComboBox::from_id_salt(format!(
133 "combo_{name}"
134 ))
135 .selected_text(
136 active_profile
137 .version
138 .as_deref()
139 .unwrap_or_default(),
140 )
141 .show_ui(ui, |ui| {
142 for p in profiles {
143 let version_text = p
144 .version
145 .as_deref()
146 .unwrap_or_default();
147 if ui
148 .selectable_value(
149 &mut state.selected_profile,
150 Some(p.filename.clone()),
151 version_text,
152 )
153 .clicked()
154 {
155 state.group_versions.insert(
156 name.clone(),
157 p.filename.clone(),
158 );
159 }
160 }
161 });
162 } else if let Some(version) =
163 &active_profile.version
164 {
165 ui.label(egui::RichText::new(version).weak());
166 }
167
168 if active_profile.is_remote {
169 ui.label("โ")
170 .on_hover_text("Available on cloud");
171 }
172
173 if !active_profile.is_valid {
174 ui.label("โ ").on_hover_text(
175 "Invalid or tampered snapshot",
176 );
177 }
178 },
179 );
180 });
181 }
182 }
183 });
184 });
185 });
186
187 cols[1].vertical(|ui| {
189 ui.label(egui::RichText::new("Description:").strong());
190 ui.add_space(2.0);
191 egui::Frame::group(ui.style())
192 .fill(ui.visuals().extreme_bg_color)
193 .show(ui, |ui| {
194 ui.set_min_height(box_height);
195 ui.set_max_height(box_height);
196 ui.set_width(ui.available_width());
197
198 if let Some(filename) = &state.selected_profile {
199 if let Some(profile) = state
200 .available_profiles
201 .iter()
202 .find(|p| &p.filename == filename)
203 {
204 ui.label(
205 profile
206 .description
207 .as_deref()
208 .unwrap_or("No description provided."),
209 );
210 } else {
211 ui.label(egui::RichText::new("No description provided.").weak());
212 }
213 } else {
214 ui.label(egui::RichText::new("Select a snapshot to see details.").weak());
215 }
216 });
217 });
218 });
219
220 ui.columns(2, |cols| {
222 cols[0].vertical(|ui| {
224 ui.add_space(8.0);
225 if ui
226 .add_enabled(
227 !state.is_running,
228 egui::Button::new("โ Create New Snapshot"),
229 )
230 .on_hover_text("Open a dialog to create a new game file snapshot.")
231 .clicked()
232 {
233 state.snapshot_name = "snapshot".to_string();
235 state.snapshot_description.clear();
236 state.snapshot_version.clear();
237 state.snapshot_compression = 0;
238 state.snapshot_directory = None;
239 state.show_create_popup = true;
240 }
241 });
242
243 cols[1].vertical(|ui| {
245 ui.add_space(8.0);
246 ui.horizontal(|ui| {
247 let has_selection = state.selected_profile.is_some();
248
249 let has_game_exe = storage.game_path().join("Supreme.exe").exists();
250 let selected_is_remote = state
251 .selected_profile
252 .as_ref()
253 .and_then(|f| state.available_profiles.iter().find(|p| &p.filename == f))
254 .is_some_and(|p| p.is_remote);
255
256 let restore_enabled =
257 !state.is_running && has_selection && (!selected_is_remote || has_game_exe);
258
259 if ui
260 .add_enabled(restore_enabled, egui::Button::new("โป Restore"))
261 .on_hover_text(if selected_is_remote && !has_game_exe {
262 "Restoration unavailable when the file Supreme.exe is missing."
263 } else if !has_selection {
264 "Select a snapshot to restore."
265 } else if state.is_running {
266 "Cannot restore while an operation is running."
267 } else {
268 "Restore the selected snapshot."
269 })
270 .clicked()
271 {
272 state.show_restore_popup = true;
273 }
274
275 if ui
276 .add_enabled(
277 !state.is_running && has_selection && (has_token || !storage.is_online()),
278 egui::Button::new("โ Upload"),
279 )
280 .on_hover_text(if has_token || !storage.is_online() {
281 "Upload snapshot and required files to the server."
282 } else {
283 "Cloud upload requires SSBZ_API_TOKEN to be set."
284 })
285 .clicked()
286 && let Some(filename) = state.selected_profile.clone()
287 {
288 start_upload_snapshot(ui.ctx().clone(), state, storage, filename);
289 }
290 });
291 });
292 });
293
294 if state.show_create_popup {
296 let mut close_popup = false;
297 egui::Window::new("Create Snapshot")
298 .collapsible(false)
299 .resizable(false)
300 .show(ui.ctx(), |ui| {
301 ui.vertical(|ui| {
302 ui.label("Give your snapshot a name and description.");
303 ui.add_space(8.0);
304
305 egui::Grid::new("popup_creation_grid")
306 .num_columns(2)
307 .spacing([12.0, 8.0])
308 .show(ui, |ui| {
309 ui.label("Name:");
310 ui.add(
311 egui::TextEdit::singleline(&mut state.snapshot_name)
312 .hint_text("Required"),
313 );
314 ui.end_row();
315
316 ui.label("Path:");
317 ui.horizontal(|ui| {
318 let path_str = state
319 .snapshot_directory
320 .as_ref()
321 .map(|p| p.to_string_lossy().to_string())
322 .unwrap_or_else(|| {
323 storage.game_path().to_string_lossy().to_string()
324 });
325 ui.label(egui::RichText::new(path_str).small());
326 if ui.button("๐ Pick...").clicked()
327 && let Some(path) = rfd::FileDialog::new()
328 .set_directory(storage.game_path())
329 .pick_folder()
330 {
331 state.snapshot_directory = Some(path);
332 }
333 });
334 ui.end_row();
335
336 ui.label("Description:");
337 ui.add(
338 egui::TextEdit::multiline(&mut state.snapshot_description)
339 .hint_text("Optional details...")
340 .desired_rows(3)
341 .lock_focus(true),
342 );
343 ui.end_row();
344
345 ui.label("Version:");
346 ui.add(
347 egui::TextEdit::singleline(&mut state.snapshot_version)
348 .hint_text("Optional version string..."),
349 );
350 ui.end_row();
351
352 ui.label("Compression Level:");
353 ui.add(
354 egui::Slider::new(&mut state.snapshot_compression, 0..=8)
355 .text("0=None, 8=Max"),
356 );
357 ui.end_row();
358 });
359
360 ui.add_space(12.0);
361
362 ui.horizontal(|ui| {
363 if ui.button("Cancel").clicked() {
364 close_popup = true;
365 }
366
367 if ui
368 .add_enabled(
369 !state.snapshot_name.trim().is_empty(),
370 egui::Button::new("๐ท Create"),
371 )
372 .clicked()
373 {
374 start_create_snapshot(
375 ui.ctx().clone(),
376 state,
377 storage,
378 SnapshotRequest {
379 name: state.snapshot_name.clone(),
380 description: if state.snapshot_description.is_empty() {
381 None
382 } else {
383 Some(state.snapshot_description.clone())
384 },
385 version: if state.snapshot_version.is_empty() {
386 None
387 } else {
388 Some(state.snapshot_version.clone())
389 },
390 custom_directory: state.snapshot_directory.clone(),
391 compression_level: state.snapshot_compression,
392 },
393 );
394 close_popup = true;
395 }
396 });
397 });
398 });
399
400 if close_popup {
401 state.show_create_popup = false;
402 }
403 }
404
405 if state.show_restore_popup {
407 if let Some(filename) = state.selected_profile.clone() {
408 let snapshot_name = state
409 .available_profiles
410 .iter()
411 .find(|p| p.filename == filename)
412 .map(|p| {
413 format!(
414 "{} {}",
415 p.name.as_deref().unwrap_or(&filename),
416 p.version.as_deref().unwrap_or("")
417 )
418 })
419 .unwrap_or(filename.clone());
420
421 let mut close_popup = false;
422 egui::Window::new("Confirm Restoration")
423 .collapsible(false)
424 .resizable(false)
425 .show(ui.ctx(), |ui| {
426 ui.vertical(|ui| {
427 ui.label(
428 egui::RichText::new("โ Warning: Snapshot restoration can fail.")
429 .color(ui.visuals().warn_fg_color)
430 .strong(),
431 );
432 ui.label("It is highly recommended to create a snapshot of your current game files before restoring.");
433 ui.add_space(4.0);
434 ui.label("Please report any failure along with the error you encountered.");
435 ui.add_space(12.0);
436
437 ui.horizontal(|ui| {
438 if ui.button("Cancel").clicked() {
439 close_popup = true;
440 }
441
442 if ui
443 .button(format!("Restore {snapshot_name}"))
444 .clicked()
445 {
446 start_restore_snapshot(ui.ctx().clone(), state, storage, filename.clone());
447 close_popup = true;
448 }
449 });
450 });
451 });
452
453 if close_popup {
454 state.show_restore_popup = false;
455 }
456 } else {
457 state.show_restore_popup = false;
458 }
459 }
460
461 ui.add_space(10.0);
462
463 #[cfg(feature = "local")]
465 ui.group(|ui| {
466 ui.heading("Path Validation");
467 ui.label("Scan game files for broken path references.");
468
469 if ui.button("๐ Scan for Broken Paths").clicked() && !state.is_running {
470 start_path_scan(ui.ctx().clone(), state, storage);
471 }
472
473 let diagnostics_path = storage.game_path().join(".supreme/path_diagnostics.json");
475 state.path_diagnostics_available = diagnostics_path.exists();
476
477 if state.path_diagnostics_available {
478 ui.label(format!(
479 "๐ Report available at: {}",
480 diagnostics_path.display()
481 ));
482 }
483 });
484
485 ui.add_space(10.0);
486
487 #[cfg(feature = "local")]
489 ui.group(|ui| {
490 ui.heading("Actions");
491 ui.label("Batch operations for game maintenance.");
492
493 if ui
494 .button("๐ง Normalize case")
495 .on_hover_text("Make file and folder names case consistent.")
496 .clicked()
497 && !state.is_running
498 {
499 run_cleaner_task(
500 ui.ctx().clone(),
501 state,
502 storage,
503 "Normalize case",
504 supreme_cleaner::normalize_case,
505 );
506 }
507
508 if ui
509 .button("๐ง Fix object references")
510 .on_hover_text("Repair broken object paths and removal invalid entries.")
511 .clicked()
512 && !state.is_running
513 {
514 run_cleaner_task(
515 ui.ctx().clone(),
516 state,
517 storage,
518 "Fix object references",
519 supreme_cleaner::fix_object_references,
520 );
521 }
522
523 if ui
524 .button("๐งน Free-up textures")
525 .on_hover_text("Keep the best texture for each object.")
526 .clicked()
527 && !state.is_running
528 {
529 run_cleaner_task(
530 ui.ctx().clone(),
531 state,
532 storage,
533 "Free-up textures",
534 supreme_cleaner::free_up_textures,
535 );
536 }
537
538 if ui
539 .button("๐ง Patch game binaries")
540 .on_hover_text("Patch hardcoded paths in DLLs.")
541 .clicked()
542 && !state.is_running
543 {
544 run_cleaner_task(
545 ui.ctx().clone(),
546 state,
547 storage,
548 "Patch game binaries",
549 supreme_cleaner::patch_game_binaries,
550 );
551 }
552
553 if ui
554 .button("๐ฆ Consolidate object resources")
555 .on_hover_text("Ensure all object resources are local to their configuration file.")
556 .clicked()
557 && !state.is_running
558 {
559 run_cleaner_task(
560 ui.ctx().clone(),
561 state,
562 storage,
563 "Consolidate object resources",
564 supreme_cleaner::consolidate_resources,
565 );
566 }
567
568 if ui
569 .button("๐ Clean unused objects")
570 .on_hover_text("Remove object directories not referenced by object_classes.txt.")
571 .clicked()
572 && !state.is_running
573 {
574 run_cleaner_task(
575 ui.ctx().clone(),
576 state,
577 storage,
578 "Clean unused objects",
579 supreme_cleaner::clean_unused_objects,
580 );
581 }
582 });
583}