|
|
@@ -8,6 +8,7 @@ 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
|
|
|
@@ -19,6 +20,7 @@ pub struct KioskApp {
|
|
|
|
|
|
// UI components
|
|
|
login_view: KioskLoginView,
|
|
|
+ dashboard: KioskDashboard,
|
|
|
full_ui_app: Option<BeepZoneApp>,
|
|
|
|
|
|
// State
|
|
|
@@ -27,11 +29,17 @@ pub struct KioskApp {
|
|
|
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 {
|
|
|
@@ -50,17 +58,24 @@ impl KioskApp {
|
|
|
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(),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -112,10 +127,23 @@ impl KioskApp {
|
|
|
|
|
|
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 {
|
|
|
- if self.config.ui.fullscreen {
|
|
|
+ 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));
|
|
|
+<<<<<<< HEAD
|
|
|
<<<<<<< Updated upstream
|
|
|
}
|
|
|
|
|
|
@@ -133,22 +161,51 @@ impl eframe::App for KioskApp {
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(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));
|
|
|
+ }
|
|
|
+
|
|
|
+>>>>>>> 18c79d3cb731cabd4618de1c87e3cf53a4c82777
|
|
|
// 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));
|
|
|
+<<<<<<< HEAD
|
|
|
+=======
|
|
|
+ #[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));
|
|
|
+ }
|
|
|
+>>>>>>> 18c79d3cb731cabd4618de1c87e3cf53a4c82777
|
|
|
} 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;
|
|
|
+<<<<<<< HEAD
|
|
|
>>>>>>> Stashed changes
|
|
|
}
|
|
|
self.delayed_fullscreen_done = true;
|
|
|
}
|
|
|
<<<<<<< Updated upstream
|
|
|
+=======
|
|
|
+ }
|
|
|
+>>>>>>> 18c79d3cb731cabd4618de1c87e3cf53a4c82777
|
|
|
self.window_setup_done = true;
|
|
|
=======
|
|
|
|
|
|
@@ -167,6 +224,25 @@ impl eframe::App for KioskApp {
|
|
|
>>>>>>> Stashed changes
|
|
|
}
|
|
|
|
|
|
+ // 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() ||
|
|
|
@@ -183,6 +259,7 @@ impl eframe::App for KioskApp {
|
|
|
// 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
|
|
|
@@ -197,12 +274,19 @@ impl eframe::App for KioskApp {
|
|
|
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;
|
|
|
@@ -244,44 +328,60 @@ impl eframe::App for KioskApp {
|
|
|
let session_user = self.session_user.clone();
|
|
|
if let Some(user) = session_user {
|
|
|
// Logged In View
|
|
|
- ui.centered_and_justified(|ui| {
|
|
|
- ui.vertical_centered(|ui| {
|
|
|
- ui.heading(egui::RichText::new("Yay it works").size(48.0).color(egui::Color32::GREEN));
|
|
|
- ui.add_space(20.0);
|
|
|
- ui.label(egui::RichText::new(format!("Welcome, {}", user.name)).size(32.0));
|
|
|
- ui.add_space(40.0);
|
|
|
-
|
|
|
- if ui.add_sized(egui::vec2(200.0, 80.0), egui::Button::new(egui::RichText::new("Sign Out").size(24.0))).clicked() {
|
|
|
- self.session_user = None;
|
|
|
- self.session_token = None;
|
|
|
- self.login_view.reset();
|
|
|
+ // 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);
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if user.power >= self.config.ui.minimum_power_level_for_full_ui {
|
|
|
- ui.add_space(20.0);
|
|
|
- if ui.add_sized(egui::vec2(200.0, 60.0), egui::Button::new(egui::RichText::new("Show Full UI").size(20.0))).clicked() {
|
|
|
- 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;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
+ 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 => {}
|
|
|
}
|
|
|
@@ -289,5 +389,113 @@ impl eframe::App for KioskApp {
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
+
|
|
|
+ // 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));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
}
|
|
|
}
|