| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- 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<Mutex<SessionManager>>,
- api_client: Option<ApiClient>,
-
- // Kiosk state
- config: KioskSettings,
-
- // UI components
- login_view: KioskLoginView,
- dashboard: KioskDashboard,
- full_ui_app: Option<BeepZoneApp>,
-
- // State
- is_initialized: bool,
- window_setup_done: bool,
- current_user: Option<UserInfo>, // The Kiosk User
- session_user: Option<UserInfo>, // The User currently logged in via Kiosk
- session_token: Option<String>, // The Token of the User currently logged in via Kiosk
- session_api_client: Option<ApiClient>, // API client for the session user (not kiosk user)
- error_message: Option<String>,
- show_full_ui: bool,
- show_osk: bool,
- osk_shift_mode: bool,
- last_focused_id: Option<egui::Id>,
- osk_event_queue: Vec<egui::Event>,
- 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<Mutex<SessionManager>>,
- 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));
- }
- }
- });
- });
- });
- }
- }
|