use crate::azure::models::Application; use crate::state::AppState; use crate::ui::components::{ show_error_message, show_loading_spinner, show_section_header, show_success_message, show_text, }; use egui::{Context, ScrollArea, Ui}; pub fn show_app_list_view(ctx: &Context, state: &mut AppState, config: &crate::config::Config) -> Option { let mut action = None; // Handle keyboard navigation for app list // Check early, before text edits might consume the input let any_text_focused = ctx.memory(|m| m.focused().is_some()); if !any_text_focused { let filtered_apps = state.filtered_applications(); if !filtered_apps.is_empty() { let current_idx = state.selected_app.as_ref() .and_then(|sel| filtered_apps.iter().position(|a| a.id == sel.id)); ctx.input(|i| { let mut target_idx = None; if i.key_pressed(egui::Key::ArrowDown) { target_idx = Some(current_idx.map_or(0, |idx| (idx + 1) % filtered_apps.len())); } if i.key_pressed(egui::Key::ArrowUp) { target_idx = Some(current_idx.map_or( filtered_apps.len() - 1, |idx| if idx > 0 { idx - 1 } else { filtered_apps.len() - 1 } )); } if let Some(idx) = target_idx { if let Some(app) = filtered_apps.get(idx) { action = Some(AppListAction::SelectApp(app.clone())); } } }); } } egui::TopBottomPanel::top("top_panel") .frame(egui::Frame::none() .fill(egui::Color32::TRANSPARENT) .stroke(egui::Stroke::NONE) .inner_margin(egui::Margin::symmetric(20.0, 10.0))) .show_separator_line(false) .show(ctx, |ui| { ui.horizontal(|ui| { show_section_header(ui, "App Registrations"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("Sign Out").clicked() { action = Some(AppListAction::SignOut); } ui.add_space(10.0); // Replace separator with space if ui.button("🔄 Refresh").clicked() { action = Some(AppListAction::Refresh); } }); }); if let Some(user) = &state.user_info { show_text(ui, &format!("Logged in as: {}", user.display_name)); } }); egui::CentralPanel::default() .frame(egui::Frame::none() .fill(egui::Color32::TRANSPARENT) .inner_margin(egui::Margin { left: 20.0, right: 20.0, top: 20.0, bottom: 80.0, // Extra margin so cards don't show behind bottom panel })) .show(ctx, |ui| { // Show messages if let Some(error) = &state.error_message { show_error_message(ui, error); ui.add_space(10.0); } if let Some(success) = &state.success_message { show_success_message(ui, success); ui.add_space(10.0); } // Show loading spinner if loading if state.operations.load_applications.is_some() { show_loading_spinner(ui, "Loading applications..."); return; } // Search box ui.horizontal(|ui| { show_text(ui, "Search:"); ui.text_edit_singleline(&mut state.app_search_filter); }); ui.add_space(10.0); let filtered_apps = state.filtered_applications(); if filtered_apps.is_empty() { ui.centered_and_justified(|ui| { if state.applications.is_empty() { show_text(ui, "No app registrations found. Click Refresh to load."); } else { show_text(ui, "No matching applications found."); } }); } else { show_text(ui, &format!("Found {} applications", filtered_apps.len())); ui.add_space(5.0); ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| { ui.set_width(ui.available_width() - 20.0); // Add margin for scrollbar for app in filtered_apps { show_app_card(ui, &app, &state.selected_app, &mut action, config); } }); } }); egui::TopBottomPanel::bottom("bottom_panel") .frame(egui::Frame::none() .fill(egui::Color32::TRANSPARENT) .stroke(egui::Stroke::NONE) .inner_margin(egui::Margin::symmetric(20.0, 15.0))) .show_separator_line(false) .show(ctx, |ui| { ui.horizontal(|ui| { let is_app_selected = state.selected_app.is_some(); if ui .add_enabled(is_app_selected, egui::Button::new("Create Secret").min_size(egui::vec2(120.0, 32.0))) .clicked() { action = Some(AppListAction::CreateSecret); } if !is_app_selected { show_text(ui, "Select an app registration to create a secret"); } }); }); action } fn show_app_card( ui: &mut Ui, app: &Application, selected_app: &Option, action: &mut Option, config: &crate::config::Config, ) { let is_selected = selected_app .as_ref() .map(|selected| selected.id == app.id) .unwrap_or(false); // Get card colors from config with fallback to defaults let card_colors = config.colors.as_ref() .map(|c| &c.cards.app_list); let frame = if is_selected { let bg = card_colors.map(|c| c.selected_bg).unwrap_or([60, 60, 100, 180]); let border = card_colors.map(|c| c.selected_border).unwrap_or([100, 150, 255]); egui::Frame::none() .fill(egui::Color32::from_rgba_unmultiplied(bg[0], bg[1], bg[2], bg[3])) .inner_margin(10.0) .rounding(5.0) .stroke(egui::Stroke::new(2.0, egui::Color32::from_rgb(border[0], border[1], border[2]))) } else { let bg = card_colors.map(|c| c.unselected_bg).unwrap_or([40, 40, 60, 150]); egui::Frame::none() .fill(egui::Color32::from_rgba_unmultiplied(bg[0], bg[1], bg[2], bg[3])) .inner_margin(10.0) .rounding(5.0) }; let response = frame.show(ui, |ui| { ui.vertical(|ui| { ui.set_width(ui.available_width()); ui.strong(&app.display_name); ui.label(format!("App ID: {}", app.app_id)); if let Some(created) = &app.created_date_time { ui.label(format!("Created: {}", created)); } }); }); // Make the entire card clickable if response.response.interact(egui::Sense::click()).clicked() && !is_selected { *action = Some(AppListAction::SelectApp(app.clone())); } ui.add_space(5.0); } pub enum AppListAction { Refresh, SelectApp(Application), CreateSecret, SignOut, }