app.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. use eframe::egui;
  2. use std::sync::Arc;
  3. use tokio::sync::Mutex;
  4. use crate::api::ApiClient;
  5. use crate::config::KioskSettings;
  6. use crate::models::{UserInfo, LoginResponse};
  7. use crate::session::SessionManager;
  8. use crate::ui::app::BeepZoneApp;
  9. use super::login::{KioskLoginView, LoginResult};
  10. use super::dashboard::KioskDashboard;
  11. pub struct KioskApp {
  12. // Session management
  13. session_manager: Arc<Mutex<SessionManager>>,
  14. api_client: Option<ApiClient>,
  15. // Kiosk state
  16. config: KioskSettings,
  17. // UI components
  18. login_view: KioskLoginView,
  19. dashboard: KioskDashboard,
  20. full_ui_app: Option<BeepZoneApp>,
  21. // State
  22. is_initialized: bool,
  23. window_setup_done: bool,
  24. current_user: Option<UserInfo>, // The Kiosk User
  25. session_user: Option<UserInfo>, // The User currently logged in via Kiosk
  26. session_token: Option<String>, // The Token of the User currently logged in via Kiosk
  27. session_api_client: Option<ApiClient>, // API client for the session user (not kiosk user)
  28. error_message: Option<String>,
  29. show_full_ui: bool,
  30. show_osk: bool,
  31. osk_shift_mode: bool,
  32. last_focused_id: Option<egui::Id>,
  33. osk_event_queue: Vec<egui::Event>,
  34. last_interaction: std::time::Instant,
  35. startup_time: std::time::Instant,
  36. delayed_fullscreen_done: bool,
  37. last_enforce_check: std::time::Instant,
  38. }
  39. impl KioskApp {
  40. pub fn new(
  41. cc: &eframe::CreationContext<'_>,
  42. session_manager: Arc<Mutex<SessionManager>>,
  43. config: KioskSettings,
  44. ) -> Self {
  45. let login_view = KioskLoginView::new(config.filter.clone(), config.ui.clone());
  46. let mut full_ui_app = BeepZoneApp::new(cc, session_manager.clone());
  47. full_ui_app.is_kiosk_mode = true;
  48. full_ui_app.enable_full_osk_button = config.ui.enable_full_osk_button;
  49. Self {
  50. session_manager,
  51. api_client: None,
  52. config,
  53. login_view,
  54. dashboard: KioskDashboard::new(),
  55. full_ui_app: Some(full_ui_app),
  56. is_initialized: false,
  57. window_setup_done: false,
  58. current_user: None,
  59. session_user: None,
  60. session_token: None,
  61. session_api_client: None,
  62. error_message: None,
  63. show_full_ui: false,
  64. show_osk: false,
  65. osk_shift_mode: false,
  66. last_focused_id: None,
  67. osk_event_queue: Vec::new(),
  68. last_interaction: std::time::Instant::now(),
  69. startup_time: std::time::Instant::now(),
  70. delayed_fullscreen_done: false,
  71. last_enforce_check: std::time::Instant::now(),
  72. }
  73. }
  74. fn initialize_session(&mut self) {
  75. if self.is_initialized {
  76. return;
  77. }
  78. log::info!("Initializing Kiosk session for user: {}", self.config.username);
  79. // Create API client
  80. let mut client = match ApiClient::new(self.config.server_url.clone()) {
  81. Ok(c) => c,
  82. Err(e) => {
  83. self.error_message = Some(format!("Failed to connect to server: {}", e));
  84. return;
  85. }
  86. };
  87. // Attempt login
  88. match client.login_password(&self.config.username, &self.config.password) {
  89. Ok(response) => {
  90. if response.success {
  91. if let (Some(token), Some(user)) = (response.token, response.user) {
  92. log::info!("Kiosk login successful");
  93. client.set_token(token);
  94. self.api_client = Some(client);
  95. self.current_user = Some(user);
  96. self.is_initialized = true;
  97. self.error_message = None;
  98. // Initialize login view with client
  99. if let Some(client) = &self.api_client {
  100. self.login_view.refresh_users(client, self.current_user.as_ref());
  101. }
  102. } else {
  103. self.error_message = Some("Login successful but missing token or user data".to_string());
  104. }
  105. } else {
  106. self.error_message = Some("Login failed: Invalid credentials".to_string());
  107. }
  108. }
  109. Err(e) => {
  110. self.error_message = Some(format!("Login error: {}", e));
  111. }
  112. }
  113. }
  114. }
  115. impl eframe::App for KioskApp {
  116. fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
  117. // Inject queued OSK events at the start of the frame
  118. if !self.osk_event_queue.is_empty() {
  119. let events = std::mem::take(&mut self.osk_event_queue);
  120. ctx.input_mut(|i| i.events.extend(events));
  121. }
  122. // Track focus for OSK
  123. if let Some(id) = ctx.memory(|m| m.focused()) {
  124. self.last_focused_id = Some(id);
  125. }
  126. // Ensure window state on first frame
  127. if !self.window_setup_done {
  128. let want_fullscreen = if self.config.ui.windowed_mode { false } else { self.config.ui.fullscreen };
  129. if want_fullscreen {
  130. ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
  131. ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(false));
  132. // On Windows, ensure top-left and ask maximize as fallback
  133. #[cfg(target_os = "windows")]
  134. {
  135. ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)));
  136. ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
  137. }
  138. } else {
  139. // Ensure we are not in fullscreen and re-enable window decorations (borders, title bar)
  140. ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
  141. ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(true));
  142. // Ask the window manager to maximize the window
  143. ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
  144. }
  145. // Enforce desired mode again after a short delay (to handle some window managers)
  146. if !self.delayed_fullscreen_done && self.startup_time.elapsed().as_secs_f32() > 1.0 {
  147. if want_fullscreen {
  148. ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
  149. ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(false));
  150. #[cfg(target_os = "windows")]
  151. {
  152. ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)));
  153. ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
  154. }
  155. } else {
  156. ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
  157. ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(true));
  158. ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
  159. }
  160. self.delayed_fullscreen_done = true;
  161. }
  162. self.window_setup_done = true;
  163. }
  164. // Periodically re-assert desired window mode in case the OS/window manager changed it.
  165. if self.last_enforce_check.elapsed().as_secs_f32() > 3.0 {
  166. let want_fullscreen = if self.config.ui.windowed_mode { false } else { self.config.ui.fullscreen };
  167. if want_fullscreen {
  168. ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
  169. ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(false));
  170. #[cfg(target_os = "windows")]
  171. {
  172. ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)));
  173. ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
  174. }
  175. } else {
  176. ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
  177. ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(true));
  178. ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
  179. }
  180. self.last_enforce_check = std::time::Instant::now();
  181. }
  182. // Check for interaction (clicks or key presses, ignore mouse moves to prevent drift issues)
  183. let has_interaction = ctx.input(|i| {
  184. i.pointer.any_pressed() ||
  185. i.events.iter().any(|e| matches!(e, egui::Event::Key{..} | egui::Event::Text(_)))
  186. });
  187. if has_interaction {
  188. self.last_interaction = std::time::Instant::now();
  189. }
  190. // Check timeout
  191. if let Some(timeout) = self.config.ui.timeout_seconds {
  192. if self.session_user.is_some() && self.last_interaction.elapsed().as_secs() > timeout {
  193. // Timeout!
  194. self.session_user = None;
  195. self.session_token = None;
  196. self.session_api_client = None;
  197. self.show_full_ui = false;
  198. if let Some(app) = &mut self.full_ui_app {
  199. app.handle_logout(); // Ensure app state is cleared
  200. app.should_exit_to_kiosk = false; // We handled it
  201. }
  202. self.login_view.reset();
  203. }
  204. }
  205. // Handle Full UI Mode
  206. if self.show_full_ui {
  207. if let Some(app) = &mut self.full_ui_app {
  208. app.update(ctx, frame);
  209. // Check if we should return to kiosk menu (keep session)
  210. if app.should_return_to_kiosk_menu {
  211. self.show_full_ui = false;
  212. app.should_return_to_kiosk_menu = false;
  213. }
  214. // Check if we should exit back to kiosk
  215. if app.should_exit_to_kiosk {
  216. self.show_full_ui = false;
  217. app.should_exit_to_kiosk = false;
  218. self.session_user = None; // Also sign out of kiosk session
  219. self.session_token = None;
  220. self.session_api_client = None;
  221. self.login_view.reset();
  222. }
  223. return;
  224. }
  225. }
  226. // Initialize on first frame
  227. if !self.is_initialized && self.error_message.is_none() {
  228. self.initialize_session();
  229. }
  230. // Full screen container
  231. egui::CentralPanel::default().show(ctx, |ui| {
  232. // Clone error message to avoid borrow checker issues
  233. let error_msg = self.error_message.clone();
  234. if let Some(error) = error_msg {
  235. // Error state
  236. ui.centered_and_justified(|ui| {
  237. ui.vertical_centered(|ui| {
  238. ui.heading(egui::RichText::new("Kiosk Initialization Failed").color(egui::Color32::RED));
  239. ui.add_space(10.0);
  240. ui.label(error);
  241. ui.add_space(20.0);
  242. if ui.button("Retry").clicked() {
  243. self.error_message = None;
  244. self.is_initialized = false;
  245. }
  246. });
  247. });
  248. } else if !self.is_initialized {
  249. // Loading state
  250. ui.centered_and_justified(|ui| {
  251. ui.spinner();
  252. });
  253. } else {
  254. // Main Kiosk UI
  255. if let Some(client) = &self.api_client {
  256. let session_user = self.session_user.clone();
  257. if let Some(user) = session_user {
  258. // Logged In View
  259. // Create session API client if not already created
  260. if self.session_api_client.is_none() {
  261. if let Some(token) = &self.session_token {
  262. if let Ok(mut session_client) = ApiClient::new(self.config.server_url.clone()) {
  263. session_client.set_token(token.clone());
  264. self.session_api_client = Some(session_client);
  265. }
  266. }
  267. }
  268. let mut logout_requested = false;
  269. let mut show_full_ui_requested = false;
  270. // Use session_api_client for operations, fallback to kiosk client
  271. let active_client = self.session_api_client.as_ref().unwrap_or(client);
  272. self.dashboard.show(
  273. ui,
  274. active_client,
  275. &user,
  276. &self.config.ui,
  277. &mut self.show_osk,
  278. &mut logout_requested,
  279. &mut show_full_ui_requested
  280. );
  281. if logout_requested {
  282. self.session_user = None;
  283. self.session_token = None;
  284. self.session_api_client = None;
  285. self.login_view.reset();
  286. }
  287. if show_full_ui_requested {
  288. if let Some(app) = &mut self.full_ui_app {
  289. // Construct a LoginResponse to simulate a successful login
  290. let login_response = LoginResponse {
  291. success: true,
  292. token: self.session_token.clone(),
  293. user: Some(user.clone()),
  294. error: None,
  295. };
  296. app.handle_login_success(self.config.server_url.clone(), login_response);
  297. self.show_full_ui = true;
  298. }
  299. }
  300. } else {
  301. // Login View
  302. match self.login_view.show(ui, client) {
  303. LoginResult::Success(user, token) => {
  304. self.session_user = Some(user);
  305. self.session_token = Some(token);
  306. // Session API client will be created on next frame
  307. }
  308. LoginResult::None => {}
  309. }
  310. }
  311. }
  312. }
  313. });
  314. // Show OSK overlay if enabled and not in full UI mode
  315. if !self.show_full_ui {
  316. self.show_osk_overlay(ctx);
  317. }
  318. }
  319. }
  320. impl KioskApp {
  321. fn show_osk_overlay(&mut self, ctx: &egui::Context) {
  322. if !self.show_osk { return; }
  323. let height = 340.0;
  324. egui::TopBottomPanel::bottom("kiosk_osk_panel")
  325. .resizable(false)
  326. .min_height(height)
  327. .show(ctx, |ui| {
  328. ui.vertical_centered(|ui| {
  329. ui.add_space(10.0);
  330. // Styling
  331. let btn_size = egui::vec2(50.0, 50.0);
  332. let spacing = 6.0;
  333. ui.style_mut().spacing.item_spacing = egui::vec2(spacing, spacing);
  334. // Layouts
  335. let rows_lower = [
  336. vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "="],
  337. vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]"],
  338. vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "\\"],
  339. vec!["z", "x", "c", "v", "b", "n", "m", ",", ".", "/"],
  340. ];
  341. let rows_upper = [
  342. vec!["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+"],
  343. vec!["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "{", "}"],
  344. vec!["A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "|"],
  345. vec!["Z", "X", "C", "V", "B", "N", "M", "<", ">", "?"],
  346. ];
  347. let rows = if self.osk_shift_mode { rows_upper } else { rows_lower };
  348. for row in rows {
  349. ui.horizontal(|ui| {
  350. // Center row
  351. let width = row.len() as f32 * (btn_size.x + spacing) - spacing;
  352. let margin = (ui.available_width() - width) / 2.0;
  353. if margin > 0.0 { ui.add_space(margin); }
  354. for key in row {
  355. if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new(key).size(24.0))).clicked() {
  356. // Queue text event for next frame
  357. self.osk_event_queue.push(egui::Event::Text(key.to_string()));
  358. // Restore focus immediately
  359. if let Some(id) = self.last_focused_id {
  360. ctx.memory_mut(|m| m.request_focus(id));
  361. }
  362. }
  363. }
  364. });
  365. }
  366. // Modifiers and Actions
  367. ui.horizontal(|ui| {
  368. let shift_width = 100.0;
  369. let space_width = 300.0;
  370. let back_width = 100.0;
  371. let total_width = shift_width + space_width + back_width + (spacing * 2.0);
  372. let margin = (ui.available_width() - total_width) / 2.0;
  373. if margin > 0.0 { ui.add_space(margin); }
  374. // Shift
  375. let shift_text = if self.osk_shift_mode { "SHIFT (ON)" } else { "SHIFT" };
  376. let shift_btn = egui::Button::new(egui::RichText::new(shift_text).size(20.0))
  377. .fill(if self.osk_shift_mode { egui::Color32::from_rgb(100, 100, 255) } else { ui.visuals().widgets.inactive.bg_fill });
  378. if ui.add_sized(egui::vec2(shift_width, 50.0), shift_btn).clicked() {
  379. self.osk_shift_mode = !self.osk_shift_mode;
  380. if let Some(id) = self.last_focused_id {
  381. ctx.memory_mut(|m| m.request_focus(id));
  382. }
  383. }
  384. // Space
  385. if ui.add_sized(egui::vec2(space_width, 50.0), egui::Button::new(egui::RichText::new("SPACE").size(20.0))).clicked() {
  386. self.osk_event_queue.push(egui::Event::Text(" ".to_string()));
  387. if let Some(id) = self.last_focused_id {
  388. ctx.memory_mut(|m| m.request_focus(id));
  389. }
  390. }
  391. // Backspace
  392. if ui.add_sized(egui::vec2(back_width, 50.0), egui::Button::new(egui::RichText::new("DEL").size(24.0))).clicked() {
  393. self.osk_event_queue.push(egui::Event::Key {
  394. key: egui::Key::Backspace,
  395. physical_key: None,
  396. pressed: true,
  397. repeat: false,
  398. modifiers: egui::Modifiers::default(),
  399. });
  400. if let Some(id) = self.last_focused_id {
  401. ctx.memory_mut(|m| m.request_focus(id));
  402. }
  403. }
  404. });
  405. });
  406. });
  407. }
  408. }