use eframe::egui; use std::sync::Arc; use tokio::sync::Mutex; use crate::api::ApiClient; use crate::config::KioskSettings; use crate::models::{UserInfo, LoginResponse}; use crate::session::SessionManager; use crate::ui::app::BeepZoneApp; use super::login::{KioskLoginView, LoginResult}; use super::dashboard::KioskDashboard; pub struct KioskApp { // Session management session_manager: Arc>, api_client: Option, // Kiosk state config: KioskSettings, // UI components login_view: KioskLoginView, dashboard: KioskDashboard, full_ui_app: Option, // State is_initialized: bool, window_setup_done: bool, current_user: Option, // The Kiosk User session_user: Option, // The User currently logged in via Kiosk session_token: Option, // The Token of the User currently logged in via Kiosk session_api_client: Option, // API client for the session user (not kiosk user) error_message: Option, show_full_ui: bool, show_osk: bool, osk_shift_mode: bool, last_focused_id: Option, osk_event_queue: Vec, last_interaction: std::time::Instant, startup_time: std::time::Instant, delayed_fullscreen_done: bool, last_enforce_check: std::time::Instant, } impl KioskApp { pub fn new( cc: &eframe::CreationContext<'_>, session_manager: Arc>, config: KioskSettings, ) -> Self { let login_view = KioskLoginView::new(config.filter.clone(), config.ui.clone()); let mut full_ui_app = BeepZoneApp::new(cc, session_manager.clone()); full_ui_app.is_kiosk_mode = true; full_ui_app.enable_full_osk_button = config.ui.enable_full_osk_button; Self { session_manager, api_client: None, config, login_view, dashboard: KioskDashboard::new(), full_ui_app: Some(full_ui_app), is_initialized: false, window_setup_done: false, current_user: None, session_user: None, session_token: None, session_api_client: None, error_message: None, show_full_ui: false, show_osk: false, osk_shift_mode: false, last_focused_id: None, osk_event_queue: Vec::new(), last_interaction: std::time::Instant::now(), startup_time: std::time::Instant::now(), delayed_fullscreen_done: false, last_enforce_check: std::time::Instant::now(), } } fn initialize_session(&mut self) { if self.is_initialized { return; } log::info!("Initializing Kiosk session for user: {}", self.config.username); // Create API client let mut client = match ApiClient::new(self.config.server_url.clone()) { Ok(c) => c, Err(e) => { self.error_message = Some(format!("Failed to connect to server: {}", e)); return; } }; // Attempt login match client.login_password(&self.config.username, &self.config.password) { Ok(response) => { if response.success { if let (Some(token), Some(user)) = (response.token, response.user) { log::info!("Kiosk login successful"); client.set_token(token); self.api_client = Some(client); self.current_user = Some(user); self.is_initialized = true; self.error_message = None; // Initialize login view with client if let Some(client) = &self.api_client { self.login_view.refresh_users(client, self.current_user.as_ref()); } } else { self.error_message = Some("Login successful but missing token or user data".to_string()); } } else { self.error_message = Some("Login failed: Invalid credentials".to_string()); } } Err(e) => { self.error_message = Some(format!("Login error: {}", e)); } } } } impl eframe::App for KioskApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { // Inject queued OSK events at the start of the frame if !self.osk_event_queue.is_empty() { let events = std::mem::take(&mut self.osk_event_queue); ctx.input_mut(|i| i.events.extend(events)); } // Track focus for OSK if let Some(id) = ctx.memory(|m| m.focused()) { self.last_focused_id = Some(id); } // Ensure window state on first frame if !self.window_setup_done { let want_fullscreen = if self.config.ui.windowed_mode { false } else { self.config.ui.fullscreen }; if want_fullscreen { ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(false)); // On Windows, ensure top-left and ask maximize as fallback #[cfg(target_os = "windows")] { ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0))); ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } } else { // Ensure we are not in fullscreen and re-enable window decorations (borders, title bar) ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(true)); // Ask the window manager to maximize the window ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } // Enforce desired mode again after a short delay (to handle some window managers) if !self.delayed_fullscreen_done && self.startup_time.elapsed().as_secs_f32() > 1.0 { if want_fullscreen { ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(false)); #[cfg(target_os = "windows")] { ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0))); ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } } else { ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } self.delayed_fullscreen_done = true; } self.window_setup_done = true; } // Periodically re-assert desired window mode in case the OS/window manager changed it. if self.last_enforce_check.elapsed().as_secs_f32() > 3.0 { let want_fullscreen = if self.config.ui.windowed_mode { false } else { self.config.ui.fullscreen }; if want_fullscreen { ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(false)); #[cfg(target_os = "windows")] { ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0))); ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } } else { ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } self.last_enforce_check = std::time::Instant::now(); } // Check for interaction (clicks or key presses, ignore mouse moves to prevent drift issues) let has_interaction = ctx.input(|i| { i.pointer.any_pressed() || i.events.iter().any(|e| matches!(e, egui::Event::Key{..} | egui::Event::Text(_))) }); if has_interaction { self.last_interaction = std::time::Instant::now(); } // Check timeout if let Some(timeout) = self.config.ui.timeout_seconds { if self.session_user.is_some() && self.last_interaction.elapsed().as_secs() > timeout { // Timeout! self.session_user = None; self.session_token = None; self.session_api_client = None; self.show_full_ui = false; if let Some(app) = &mut self.full_ui_app { app.handle_logout(); // Ensure app state is cleared app.should_exit_to_kiosk = false; // We handled it } self.login_view.reset(); } } // Handle Full UI Mode if self.show_full_ui { if let Some(app) = &mut self.full_ui_app { app.update(ctx, frame); // Check if we should return to kiosk menu (keep session) if app.should_return_to_kiosk_menu { self.show_full_ui = false; app.should_return_to_kiosk_menu = false; } // Check if we should exit back to kiosk if app.should_exit_to_kiosk { self.show_full_ui = false; app.should_exit_to_kiosk = false; self.session_user = None; // Also sign out of kiosk session self.session_token = None; self.session_api_client = None; self.login_view.reset(); } return; } } // Initialize on first frame if !self.is_initialized && self.error_message.is_none() { self.initialize_session(); } // Full screen container egui::CentralPanel::default().show(ctx, |ui| { // Clone error message to avoid borrow checker issues let error_msg = self.error_message.clone(); if let Some(error) = error_msg { // Error state ui.centered_and_justified(|ui| { ui.vertical_centered(|ui| { ui.heading(egui::RichText::new("Kiosk Initialization Failed").color(egui::Color32::RED)); ui.add_space(10.0); ui.label(error); ui.add_space(20.0); if ui.button("Retry").clicked() { self.error_message = None; self.is_initialized = false; } }); }); } else if !self.is_initialized { // Loading state ui.centered_and_justified(|ui| { ui.spinner(); }); } else { // Main Kiosk UI if let Some(client) = &self.api_client { let session_user = self.session_user.clone(); if let Some(user) = session_user { // Logged In View // Create session API client if not already created if self.session_api_client.is_none() { if let Some(token) = &self.session_token { if let Ok(mut session_client) = ApiClient::new(self.config.server_url.clone()) { session_client.set_token(token.clone()); self.session_api_client = Some(session_client); } } } let mut logout_requested = false; let mut show_full_ui_requested = false; // Use session_api_client for operations, fallback to kiosk client let active_client = self.session_api_client.as_ref().unwrap_or(client); self.dashboard.show( ui, active_client, &user, &self.config.ui, &mut self.show_osk, &mut logout_requested, &mut show_full_ui_requested ); if logout_requested { self.session_user = None; self.session_token = None; self.session_api_client = None; self.login_view.reset(); } if show_full_ui_requested { if let Some(app) = &mut self.full_ui_app { // Construct a LoginResponse to simulate a successful login let login_response = LoginResponse { success: true, token: self.session_token.clone(), user: Some(user.clone()), error: None, }; app.handle_login_success(self.config.server_url.clone(), login_response); self.show_full_ui = true; } } } else { // Login View match self.login_view.show(ui, client) { LoginResult::Success(user, token) => { self.session_user = Some(user); self.session_token = Some(token); // Session API client will be created on next frame } LoginResult::None => {} } } } } }); // Show OSK overlay if enabled and not in full UI mode if !self.show_full_ui { self.show_osk_overlay(ctx); } } } impl KioskApp { fn show_osk_overlay(&mut self, ctx: &egui::Context) { if !self.show_osk { return; } let height = 340.0; egui::TopBottomPanel::bottom("kiosk_osk_panel") .resizable(false) .min_height(height) .show(ctx, |ui| { ui.vertical_centered(|ui| { ui.add_space(10.0); // Styling let btn_size = egui::vec2(50.0, 50.0); let spacing = 6.0; ui.style_mut().spacing.item_spacing = egui::vec2(spacing, spacing); // Layouts let rows_lower = [ vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "="], vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]"], vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "\\"], vec!["z", "x", "c", "v", "b", "n", "m", ",", ".", "/"], ]; let rows_upper = [ vec!["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+"], vec!["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "{", "}"], vec!["A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "|"], vec!["Z", "X", "C", "V", "B", "N", "M", "<", ">", "?"], ]; let rows = if self.osk_shift_mode { rows_upper } else { rows_lower }; for row in rows { ui.horizontal(|ui| { // Center row let width = row.len() as f32 * (btn_size.x + spacing) - spacing; let margin = (ui.available_width() - width) / 2.0; if margin > 0.0 { ui.add_space(margin); } for key in row { if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new(key).size(24.0))).clicked() { // Queue text event for next frame self.osk_event_queue.push(egui::Event::Text(key.to_string())); // Restore focus immediately if let Some(id) = self.last_focused_id { ctx.memory_mut(|m| m.request_focus(id)); } } } }); } // Modifiers and Actions ui.horizontal(|ui| { let shift_width = 100.0; let space_width = 300.0; let back_width = 100.0; let total_width = shift_width + space_width + back_width + (spacing * 2.0); let margin = (ui.available_width() - total_width) / 2.0; if margin > 0.0 { ui.add_space(margin); } // Shift let shift_text = if self.osk_shift_mode { "SHIFT (ON)" } else { "SHIFT" }; let shift_btn = egui::Button::new(egui::RichText::new(shift_text).size(20.0)) .fill(if self.osk_shift_mode { egui::Color32::from_rgb(100, 100, 255) } else { ui.visuals().widgets.inactive.bg_fill }); if ui.add_sized(egui::vec2(shift_width, 50.0), shift_btn).clicked() { self.osk_shift_mode = !self.osk_shift_mode; if let Some(id) = self.last_focused_id { ctx.memory_mut(|m| m.request_focus(id)); } } // Space if ui.add_sized(egui::vec2(space_width, 50.0), egui::Button::new(egui::RichText::new("SPACE").size(20.0))).clicked() { self.osk_event_queue.push(egui::Event::Text(" ".to_string())); if let Some(id) = self.last_focused_id { ctx.memory_mut(|m| m.request_focus(id)); } } // Backspace if ui.add_sized(egui::vec2(back_width, 50.0), egui::Button::new(egui::RichText::new("DEL").size(24.0))).clicked() { self.osk_event_queue.push(egui::Event::Key { key: egui::Key::Backspace, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::default(), }); if let Some(id) = self.last_focused_id { ctx.memory_mut(|m| m.request_focus(id)); } } }); }); }); } }