use freya::prelude::*; use freya::router::RouterContext; use oneclient_core::LauncherState; use oneclient_db::console::{ConsoleQueryResult, run_console_query}; use crate::components::{Button, Icon, IconType, TextInput, toggle}; use crate::hooks::use_dispatch; use crate::notifications::{ClusterUpdateSummary, NotificationAction, NotificationActionKind}; use crate::routes::Route; use crate::theme::colors; use crate::ui::border_all_color; type SqlResult = Option>; #[derive(PartialEq)] pub struct Debug; impl Component for Debug { fn render(&self) -> impl IntoElement { let log_debug_info = use_state(|| false); let show_dev_stuff = use_state(|| false); let seen_onboarding = use_state(|| true); let use_grid_on_mods_list = use_state(|| true); rect() .vertical() .width(Size::fill()) .height(Size::fill()) .padding(40.) .spacing(8.) .child( ScrollView::new() .child( rect() .vertical() .child( label() .text("Debug") .font_size(32.) .font_weight(FontWeight::BOLD) .color(colors::fg_primary()), ) .child( label() .text("The end user won't really be looking at this page.") .font_size(13.) .color(colors::fg_secondary()), ), ) .child(divider()) .child(section( "Settings", vec![ toggle_row("Log Debug Info", log_debug_info), toggle_row("Show Dev stuff", show_dev_stuff), toggle_row("Seen Onboarding", seen_onboarding), toggle_row("Use Grid On Mods List", use_grid_on_mods_list), ], )) .child(divider()) .child(section( "Toast Controller", vec![ToastController.into_element()], )) .child(divider()) .child(section( "Cluster Update Simulator", vec![ClusterUpdateSimulator.into_element()], )) .child(divider()) .child(section("SQL Console", vec![SqlConsole.into_element()])) .child(divider()) .child(section( "Other", vec![action_row(vec![ ("Open Dev Tools", IconType::CodeSnippet02), ("Open Onboarding", IconType::Rocket02), ("Open Launcher Data", IconType::Folder), ("Log Running Processes", IconType::Terminal), ])], )), ) } } #[derive(PartialEq)] struct ToastController; impl Component for ToastController { fn render(&self) -> impl IntoElement { let dispatch = use_dispatch(); let title = use_state(|| "Downloading assets".to_string()); let body = use_state(|| "PolyBlock by Polyfrost".to_string()); let mut progress = use_state(|| 0u64); let info = dispatch.clone(); let error = dispatch.clone(); let prog = dispatch.clone(); let reset = dispatch; rect() .vertical() .width(Size::fill()) .spacing(10.) .child( rect() .horizontal() .width(Size::fill()) .spacing(12.) .child( rect() .width(Size::flex(1.0)) .child(TextInput::new(title).placeholder("Title")), ) .child( rect() .width(Size::flex(1.0)) .child(TextInput::new(body).placeholder("Body")), ), ) .child( rect() .horizontal() .width(Size::fill()) .spacing(12.) .child( Button::new() .primary() .child(Icon::new(IconType::InfoCircle).size(16.)) .text("Send Info") .on_press(move |_| { info.notify(title.read().clone()) .body(body.read().clone()) .info() .send(); }), ) .child( Button::new() .danger() .child(Icon::new(IconType::AlertTriangle).size(16.)) .text("Send Error") .on_press(move |_| { error .notify(title.read().clone()) .body(body.read().clone()) .error() .send(); }), ) .child( Button::new() .secondary() .child(Icon::new(IconType::FolderDownload).size(16.)) .text("Progress +10") .on_press(move |_| { let next = (*progress.read() + 10).min(100); progress.set(next); prog.send_test_progress(next, 100); }), ) .child( Button::new() .secondary() .text("Reset Progress") .on_press(move |_| { progress.set(0); reset.send_test_progress(0, 100); }), ), ) .into_element() } } #[derive(PartialEq)] struct ClusterUpdateSimulator; impl Component for ClusterUpdateSimulator { fn render(&self) -> impl IntoElement { let dispatch = use_dispatch(); let cluster_id = use_state(|| "1".to_string()); let cluster_name = use_state(|| "PolyBlock".to_string()); let updated = use_state(|| "Sodium 0.5 → 0.6, Iris 1.7 → 1.8".to_string()); let added = use_state(|| "Lithium".to_string()); let removed = use_state(|| "OptiFine".to_string()); let simulate = dispatch; rect() .vertical() .width(Size::fill()) .spacing(10.) .child( rect() .horizontal() .width(Size::fill()) .spacing(12.) .child( rect() .width(Size::px(120.)) .child(TextInput::new(cluster_id).placeholder("Cluster ID")), ) .child( rect() .width(Size::flex(1.0)) .child(TextInput::new(cluster_name).placeholder("Cluster name")), ), ) .child( rect() .horizontal() .width(Size::fill()) .spacing(12.) .child( rect().width(Size::flex(1.0)).child( TextInput::new(updated).placeholder("Updated (comma separated)"), ), ) .child( rect() .width(Size::flex(1.0)) .child(TextInput::new(added).placeholder("Added (comma separated)")), ) .child( rect().width(Size::flex(1.0)).child( TextInput::new(removed).placeholder("Removed (comma separated)"), ), ), ) .child( Button::new() .primary() .child(Icon::new(IconType::DownloadCloud02).size(16.)) .text("Simulate Cluster Update") .on_press(move |_| { let name = cluster_name.read().clone(); let summary = ClusterUpdateSummary { cluster_id: cluster_id.read().trim().parse().unwrap_or(1), cluster_name: name.clone(), updated: split_csv(&updated.read()), added: split_csv(&added.read()), removed: split_csv(&removed.read()), }; let total = summary.total(); simulate .notify("Cluster updated") .body(format!( "{total} package{} changed in {name}", if total == 1 { "" } else { "s" } )) .icon(IconType::DownloadCloud02) .action(NotificationAction { label: "View changes".to_string(), kind: NotificationActionKind::OpenClusterUpdate(summary), }) .send(); }), ) .into_element() } } #[derive(PartialEq)] struct SqlConsole; impl Component for SqlConsole { fn render(&self) -> impl IntoElement { let query = use_state(|| "SELECT * FROM clusters;".to_string()); let result = use_state(|| None::>); let running = use_state(|| false); rect() .vertical() .width(Size::fill()) .spacing(10.) .child( rect() .horizontal() .width(Size::fill()) .cross_align(Alignment::Center) .spacing(12.) .child( rect().width(Size::flex(1.0)).child( TextInput::new(query) .placeholder("SELECT * FROM …") .on_submit(move |_| run_sql(query, result, running)), ), ) .child( Button::new() .primary() .child(Icon::new(IconType::Terminal).size(16.)) .text(if *running.read() { "Running…" } else { "Run" }) .on_press(move |_| run_sql(query, result, running)), ), ) .child(sql_result(&result.read())) .into_element() } } fn run_sql(query: State, mut result: State, mut running: State) { if *running.read() { return; } let sql = query.read().clone(); if sql.trim().is_empty() { return; } running.set(true); spawn(async move { let res = match LauncherState::get() { Ok(state) => run_console_query(&state.services.db, &sql) .await .map_err(|e| e.to_string()), Err(e) => Err(e.to_string()), }; result.set(Some(res)); running.set(false); }); } fn sql_result(state: &SqlResult) -> Element { match state { None => label() .text("No query run yet.") .font_size(13.) .color(colors::fg_secondary()) .into_element(), Some(Err(err)) => rect() .width(Size::fill()) .padding(Gaps::new_symmetric(10., 14.)) .corner_radius(CornerRadius::new_all(8.)) .background(colors::component_bg()) .border(border_all_color(1., colors::danger())) .child( label() .text(err.clone()) .font_size(13.) .color(colors::danger()), ) .into_element(), Some(Ok(res)) if !res.is_select => label() .text(format!("OK — {} row(s) affected.", res.rows_affected)) .font_size(13.) .color(colors::success()) .into_element(), Some(Ok(res)) => sql_table(res), } } fn sql_table(res: &ConsoleQueryResult) -> Element { if res.columns.is_empty() { return label() .text("0 rows returned.") .font_size(13.) .color(colors::fg_secondary()) .into_element(); } let header = table_row(&res.columns, true); let mut table = rect() .vertical() .width(Size::fill()) .corner_radius(CornerRadius::new_all(8.)) .border(border_all_color(1., colors::component_border())) .child(header); for row in &res.rows { table = table.child(table_row(row, false)); } rect() .vertical() .width(Size::fill()) .spacing(6.) .child( label() .text(format!("{} row(s)", res.rows.len())) .font_size(12.) .color(colors::fg_secondary()), ) .child(table) .into_element() } fn table_row(cells: &[String], header: bool) -> Element { let mut row = rect() .horizontal() .width(Size::fill()) .content(Content::Flex) .background(if header { colors::page_elevated() } else { colors::component_bg() }); for cell in cells { row = row.child( rect() .width(Size::flex(1.0)) .padding(Gaps::new_symmetric(6., 10.)) .border(border_all_color(0.5, colors::component_border())) .child( label() .text(cell.clone()) .font_size(12.) .max_lines(1) .font_weight(if header { FontWeight::SEMI_BOLD } else { FontWeight::NORMAL }) .color(if header { colors::fg_primary() } else { colors::fg_secondary() }), ), ); } row.into_element() } fn split_csv(raw: &str) -> Vec { raw.split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() } fn divider() -> impl IntoElement { rect() .width(Size::fill()) .height(Size::px(1.)) .margin(Gaps::new_symmetric(8., 0.)) .corner_radius(CornerRadius::new_all(1.)) .background(colors::component_border()) .into_element() } fn section(title: &'static str, rows: Vec) -> impl IntoElement { rect() .vertical() .width(Size::fill()) .spacing(10.) .child( label() .text(title) .font_size(22.) .font_weight(FontWeight::SEMI_BOLD) .color(colors::fg_primary()), ) .children(rows) .into_element() } // fn field_row(label_text: &'static str, value: &'static str) -> impl IntoElement { // rect() // .horizontal() // .width(Size::fill()) // .cross_align(Alignment::Center) // .spacing(12.) // .child( // label() // .text(label_text) // .width(Size::px(80.)) // .font_size(13.) // .color(colors::fg_secondary()), // ) // .child( // rect() // .width(Size::flex(1.0)) // .padding(Gaps::new_symmetric(7., 12.)) // .corner_radius(CornerRadius::new_all(8.)) // .background(colors::component_bg()) // .border(border_all_color(1., colors::component_border())) // .child( // label() // .text(value) // .font_size(12.) // .color(colors::fg_primary()), // ), // ) // .into_element() // } fn toggle_row(title: &'static str, on: State) -> Element { rect() .horizontal() .width(Size::fill()) .cross_align(Alignment::Center) .main_align(Alignment::SpaceBetween) .padding(Gaps::new_symmetric(10., 14.)) .corner_radius(CornerRadius::new_all(10.)) .background(colors::page_elevated()) .child( label() .text(title) .font_size(14.) .color(colors::fg_primary()), ) .child(toggle(on)) .into_element() } fn action_row(buttons: Vec<(&'static str, IconType)>) -> Element { let mut row = rect().horizontal().width(Size::fill()).spacing(12.); for (text, icon) in buttons { let mut button = Button::new() .secondary() .child(Icon::new(icon).size(16.)) .text(text); if text == "Open Onboarding" { button = button.on_press(|_| { let _ = RouterContext::get().replace(Route::OnboardingWelcome {}); }); } row = row.child(button); } row.into_element() }