1use super::LauncherApp;
2use crate::pages::trainer::TrainerPageAction;
3use crate::pages::{
4 audio, display_page, language_page, mods_page, music_page, replays_page, riders_page,
5 snowboards_page, tournament_play_selection, trainer_page, troubleshoot_page,
6};
7use crate::state::{OperationType, Page};
8use crate::updater::UpdateStatus;
9use crate::widgets::nav_button::nav_button;
10use crate::widgets::nav_group_button::{NavGroupItem, nav_group_button};
11use crate::widgets::play_quit_button::{FooterAction, play_quit_button};
12use eframe::egui;
13
14impl LauncherApp {
15 pub fn update_unified_operations_ui(&mut self) {
16 self.update_unified_operations();
17 }
18
19 pub fn show_sidebar(&mut self, ui: &mut egui::Ui, update_status: &UpdateStatus) {
20 ui.add_space(4.0);
21
22 let logo_resp = ui
23 .add(
24 egui::Image::new(egui::include_image!("../../../icon.svg"))
25 .max_width(ui.available_width())
26 .sense(egui::Sense::click()),
27 )
28 .on_hover_cursor(egui::CursorIcon::PointingHand);
29
30 let badge_count = self.news_manager.badge_count();
31 if badge_count > 0 {
32 let badge_radius = 8.0;
33 let badge_center = egui::pos2(logo_resp.rect.right() - 6.0, logo_resp.rect.top() + 6.0);
34
35 ui.painter()
36 .circle_filled(badge_center, badge_radius, ui.visuals().warn_fg_color);
37
38 let text_color = egui::Color32::WHITE;
39 let text_galley = ui.painter().layout_no_wrap(
40 badge_count.to_string(),
41 egui::FontId::proportional(11.0),
42 text_color,
43 );
44 let text_pos = egui::pos2(
45 badge_center.x - text_galley.size().x / 2.0,
46 badge_center.y - text_galley.size().y / 2.0,
47 );
48 ui.painter().galley(text_pos, text_galley, text_color);
49 }
50
51 if logo_resp.clicked() {
52 self.show_news = true;
53 self.current_article_index = 0;
54 self.news_manager.clear_badge(&mut self.launcher_config);
55 }
56
57 ui.horizontal(|ui| {
58 ui.label(
60 egui::RichText::new(format!("v{}", env!("CARGO_PKG_VERSION")))
61 .weak()
62 .size(10.0),
63 );
64
65 let (color, tooltip) = match update_status {
67 UpdateStatus::UpdateAvailable(_) => {
68 (egui::Color32::YELLOW, "Downloading update...".to_string())
69 }
70 _ => (egui::Color32::TRANSPARENT, String::new()),
71 };
72
73 if color != egui::Color32::TRANSPARENT {
74 ui.label(egui::RichText::new("ยฐ").color(color))
75 .on_hover_text(tooltip);
76 }
77 });
78
79 ui.add_space(20.0);
80
81 ui.vertical(|ui| {
82 ui.spacing_mut().item_spacing.y = 8.0;
83
84 let display_warning = self.config_manager.detail_config.ground_detail >= 4
86 || self.config_manager.detail_config.visibility_cube_width > 400;
87
88 if nav_button(
89 ui,
90 "๐ฅ",
91 "Display Config",
92 self.current_page == Page::Display,
93 display_warning,
94 )
95 .clicked()
96 {
97 self.current_page = Page::Display;
98 self.error_message = None;
99 }
100
101 if nav_button(
102 ui,
103 "๐",
104 "Language",
105 self.current_page == Page::Language,
106 false,
107 )
108 .clicked()
109 {
110 self.current_page = Page::Language;
111 self.error_message = None;
112 }
113
114 let music_warning =
115 match audio::SoundMode::from_config(&self.config_manager.sound_config) {
116 audio::SoundMode::Disabled | audio::SoundMode::WithoutMusic => true,
117 audio::SoundMode::Full => false,
118 };
119
120 if nav_button(
121 ui,
122 "๐",
123 "Audio",
124 self.current_page == Page::Music,
125 music_warning,
126 )
127 .clicked()
128 {
129 self.current_page = Page::Music;
130 self.load_music_configs();
131 self.error_message = None;
132 }
133
134 let tweaks_items = [
135 NavGroupItem {
136 page: Page::Mods,
137 icon: "๐ง",
138 label: "Mods",
139 },
140 NavGroupItem {
141 page: Page::Trainer,
142 icon: "โ",
143 label: "Trainer",
144 },
145 ];
146
147 nav_group_button(ui, "๐", "Tweaks", &tweaks_items, &mut self.current_page);
148
149 let snowboarder_items = [
150 NavGroupItem {
151 page: Page::Snowboards,
152 icon: "๐",
153 label: "Boards",
154 },
155 NavGroupItem {
156 page: Page::Riders,
157 icon: "๐จ",
158 label: "Riders",
159 },
160 ];
161
162 nav_group_button(
163 ui,
164 "๐",
165 "Snowboarders",
166 &snowboarder_items,
167 &mut self.current_page,
168 );
169
170 if nav_button(
171 ui,
172 "๐ฌ",
173 "Troubleshoot",
174 self.current_page == Page::Troubleshoot,
175 false,
176 )
177 .clicked()
178 {
179 self.current_page = Page::Troubleshoot;
180 self.error_message = None;
181 }
182
183 if nav_button(
184 ui,
185 "๐ฌ",
186 "Replays",
187 self.current_page == Page::Replays,
188 false,
189 )
190 .clicked()
191 {
192 self.current_page = Page::Replays;
193 self.error_message = None;
194 }
195 });
196 }
197
198 pub fn show_footer(&mut self, ui: &mut egui::Ui) {
199 ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
200 ui.add_space(10.0);
201
202 let play_enabled = self.is_supreme_initiator;
203
204 let tournament_selection = tournament_play_selection(&self.storage, &self.mods_state);
206 let now = chrono::Utc::now().timestamp();
207 let (play_label, tournament_tooltip, tournament_ok) = match &tournament_selection {
208 Some(selection) => {
209 let room_desc = if selection.name.is_empty() {
210 format!("{} / {}", selection.env, selection.difficulty)
211 } else {
212 format!(
213 "{} ({} / {})",
214 selection.name, selection.env, selection.difficulty
215 )
216 };
217 match selection.play_blocker(now) {
218 None => (
219 "๐ Online",
220 Some(format!(
221 "Online Tournament ยท room {room_desc}\n\nCreate or join another room on the Mods page to switch."
222 )),
223 true,
224 ),
225 Some(blocker) => (
226 "๐ Online",
227 Some(format!(
228 "Online Tournament ยท room {room_desc}:\n{blocker}.\n\nJoin a live room on the Mods page."
229 )),
230 false,
231 ),
232 }
233 }
234 None => ("๐ฎ Play!", None, true),
235 };
236 let play_enabled = play_enabled && tournament_ok;
237
238 let play_tooltip = if !play_enabled && !self.is_supreme_initiator {
239 Some("Supreme.exe could not be found.".to_string())
240 } else {
241 tournament_tooltip
242 };
243
244 match play_quit_button(ui, !self.has_focused_play, play_enabled, play_tooltip, play_label) {
245 FooterAction::Quit => {
246 self.save_all_configs();
247 std::process::exit(1);
248 }
249 FooterAction::Play => {
250 if self.error_message.is_some() {
251 self.save_all_configs();
253 std::process::exit(0);
254 }
255
256 self.save_all_configs();
257
258 if self.error_message.is_none() {
259 self.run_injector();
260 }
261 }
262 FooterAction::None => {}
263 }
264
265 if !self.has_focused_play {
266 self.has_focused_play = true;
267 }
268
269 ui.add_space(5.0);
270
271 if let Some(error) = &self.error_message {
272 let err = crate::theme::palette(ui.ctx()).err;
273 ui.colored_label(err, format!("โ Error: {error}"));
274 ui.separator();
275 }
276
277 #[cfg(feature = "local")]
278 if ui.button("๐ค Export Config").clicked() {
279 self.export_configs();
280 }
281 });
282 }
283
284 pub fn render_page_content(&mut self, ui: &mut egui::Ui) {
285 self.update_unified_operations();
287
288 match self.current_page {
289 Page::Display => {
290 display_page(
291 ui,
292 &self.storage,
293 &mut self.config_manager.display_config,
294 &mut self.config_manager.detail_config,
295 &mut self.config_manager.trainer_config,
296 );
297 }
298 Page::Language => language_page(ui, &mut self.config_manager.language_config),
299 Page::Music => music_page(
300 ui,
301 &mut self.music_state,
302 &mut self.config_manager.sound_config,
303 &mut self.config_manager.music_configs,
304 &self.storage,
305 ),
306 Page::Mods => {
307 if mods_page(
308 ui,
309 &self.storage,
310 &mut self.mods_state,
311 &mut self.launcher_config,
312 ) {
313 crate::utils::refresh_hook_resolver(&mut self.storage);
314 }
315 }
316 Page::Trainer => {
317 let action = trainer_page(
318 ui,
319 &mut self.trainer_state,
320 &mut self.config_manager.trainer_config,
321 &self.storage,
322 );
323
324 match action {
325 TrainerPageAction::InstallDependencies(handle) => {
326 self.dependency_receiver = Some(handle);
327 self.unified_operation.operation_type = OperationType::Dependency;
328 self.unified_operation.is_running = true;
329 self.unified_operation.total_items = 0;
330 self.unified_operation.processed_items = 0;
331 self.unified_operation.status = "Downloading dependencies...".to_string();
332 self.unified_operation.logs.clear();
333 self.unified_operation
334 .logs
335 .push("Starting dependency installation...".to_string());
336 }
337 TrainerPageAction::InstallHdFonts(handle) => {
338 self.dependency_receiver = Some(handle);
339 self.unified_operation.operation_type = OperationType::Dependency;
340 self.unified_operation.is_running = true;
341 self.unified_operation.total_items = 0;
342 self.unified_operation.processed_items = 0;
343 self.unified_operation.status = "Downloading HD Fonts...".to_string();
344 self.unified_operation.logs.clear();
345 self.unified_operation
346 .logs
347 .push("Starting HD Font installation...".to_string());
348 }
349 TrainerPageAction::CheckStatus(handle) => {
350 self.trainer_receiver = Some(handle);
351 }
352 TrainerPageAction::None => {}
353 }
354 }
355 Page::Snowboards => snowboards_page(ui, &self.storage, &mut self.snowboards_state),
356 Page::Riders => riders_page(ui, &self.storage, &mut self.riders_state),
357 Page::Troubleshoot => {
358 troubleshoot_page(ui, &mut self.troubleshoot_state, &self.storage);
359 }
360 Page::Replays => replays_page(ui, &mut self.replays_state, &self.storage),
361 }
362 }
363
364 pub fn render_operation_progress(&mut self, ui: &mut egui::Ui) {
365 let show_progress = self.unified_operation.is_running
366 || !self.unified_operation.logs.is_empty()
367 || self.troubleshoot_state.is_running
368 || !self.troubleshoot_state.logs.is_empty();
369
370 if show_progress {
371 egui::Panel::bottom("operation_progress")
372 .resizable(false)
373 .min_size(100.0)
374 .max_size(300.0)
375 .show(ui, |ui| {
376 ui.add_space(10.0);
377 ui.horizontal_wrapped(|ui| {
378 ui.vertical(|ui| {
379 ui.horizontal(|ui| {
380 ui.heading("Operation Progress");
381 ui.with_layout(
382 egui::Layout::right_to_left(egui::Align::Center),
383 |ui| {
384 if ui.button("โ Close").clicked() {
385 self.unified_operation.logs.clear();
386 self.troubleshoot_state.logs.clear();
387 }
388 },
389 );
390 });
391
392 let (status, total_items, processed_items, is_running) =
393 if self.troubleshoot_state.is_running {
394 (
395 self.troubleshoot_state.status.clone(),
396 self.troubleshoot_state.total_items,
397 self.troubleshoot_state.processed_items,
398 true,
399 )
400 } else if self.unified_operation.is_running {
401 (
402 self.unified_operation.status.clone(),
403 self.unified_operation.total_items,
404 self.unified_operation.processed_items,
405 true,
406 )
407 } else if !self.troubleshoot_state.logs.is_empty() {
408 (
409 self.troubleshoot_state.status.clone(),
410 self.troubleshoot_state.total_items,
411 self.troubleshoot_state.processed_items,
412 false,
413 )
414 } else {
415 (
416 self.unified_operation.status.clone(),
417 self.unified_operation.total_items,
418 self.unified_operation.processed_items,
419 false,
420 )
421 };
422
423 if total_items > 0 {
424 ui.label(format!("{status} ({processed_items}/{total_items})"));
425 let progress = processed_items as f32 / total_items as f32;
426 ui.add(egui::ProgressBar::new(progress).show_percentage());
427 } else if is_running {
428 ui.label(&status);
429 ui.spinner();
430 } else {
431 ui.label(&status);
432 }
433 });
434
435 ui.separator();
436
437 let logs_to_show = if self.troubleshoot_state.is_running {
438 self.troubleshoot_state.logs.clone()
439 } else if self.unified_operation.is_running {
440 self.unified_operation.logs.clone()
441 } else if !self.troubleshoot_state.logs.is_empty() {
442 self.troubleshoot_state.logs.clone()
443 } else {
444 self.unified_operation.logs.clone()
445 };
446
447 if !logs_to_show.is_empty() {
448 ui.vertical(|ui| {
449 ui.label("Operation Logs:");
450 egui::ScrollArea::vertical()
451 .max_height(180.0)
452 .stick_to_bottom(true)
453 .auto_shrink([false, true])
454 .show(ui, |ui| {
455 for log in &logs_to_show {
456 ui.label(log);
457 }
458 });
459 });
460 }
461 });
462 });
463 }
464 }
465
466 pub fn show_news_window(&mut self, ctx: &egui::Context) {
467 let mut open = self.show_news;
468
469 egui::Window::new("News")
470 .open(&mut open)
471 .resizable(true)
472 .default_width(500.0)
473 .default_height(450.0)
474 .show(ctx, |ui| {
475 let articles = self.news_manager.articles();
476 let article_count = articles.len();
477
478 if self.current_article_index >= article_count && article_count > 0 {
480 self.current_article_index = article_count - 1;
481 }
482
483 if article_count == 0 {
484 ui.vertical_centered(|ui| {
485 ui.add_space(20.0);
486 if self.news_manager.is_checking() {
487 ui.spinner();
488 ui.label("Checking for news...");
489 } else {
490 ui.label("No news articles available.");
491 if ui.button("Check Now").clicked() {
492 self.news_manager.start_check(&self.launcher_config.news.last_read);
493 }
494 }
495 ui.add_space(20.0);
496 });
497 } else {
498 let article = &self.news_manager.articles()[self.current_article_index];
499
500 let (title, content) = if let Some(pos) = article.content.find('\n') {
501 let first = &article.content[..pos];
502
503 if let Some(rest) = first.strip_prefix("# ") {
504 (Some(rest.to_string()), article.content[pos + 1..].to_string())
505 } else {
506 (None, article.content.clone())
507 }
508 } else {
509 (article.content.strip_prefix("# ").map(|t| t.to_string()), String::new())
511 };
512
513 let title_text = title.unwrap_or_else(|| article.title.clone());
514 let date_text = article.date.clone();
515
516 ui.horizontal(|ui| {
517 let prev_enabled = self.current_article_index + 1 < article_count;
518 if ui.add_enabled(prev_enabled, egui::Button::new("โ")).clicked() {
519 self.current_article_index += 1;
520 }
521
522 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
523 let next_enabled = self.current_article_index > 0;
524 if ui.add_enabled(next_enabled, egui::Button::new("โถ")).clicked() {
525 self.current_article_index -= 1;
526 }
527
528 ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
529 ui.add_space(2.0);
530 ui.label(egui::RichText::new(&title_text).strong().size(15.0));
531 ui.label(egui::RichText::new(&date_text).weak().size(12.0));
532 });
533 });
534 });
535
536 ui.separator();
537
538 egui::ScrollArea::vertical()
539 .auto_shrink([false, false])
540 .max_height(350.0)
541 .show(ui, |ui| {
542 egui_commonmark::CommonMarkViewer::new()
543 .show(ui, &mut self.markdown_cache, &content);
544 });
545 }
546
547 ui.separator();
548
549 ui.vertical_centered(|ui| {
550 ui.hyperlink_to(
551 "Supreme Snowboarding Community Launcher homepage",
552 "https://codeberg.org/SupremeSnowboardingCommunity/SupremeSnowboardingLauncher/",
553 );
554 });
555 });
556
557 self.show_news = open;
558 }
559}