Skip to main content

iris_hub/ui/
app_hub.rs

1//! # App Hub - Aplicação Principal
2//! 
3//! Este módulo contém a estrutura principal da aplicação e a
4//! implementação do trait `eframe::App`.
5
6use std::sync::Arc;
7use std::time::Duration;
8use eframe::egui;
9
10use crate::core::{AppConfig, AppState, ConfigManager, IconInfo};
11use crate::services::{IconCache, ProcessManager, load_available_icons};
12use crate::ui::components::{render_app_card, render_header, render_footer, render_empty_state, render_no_results};
13use crate::ui::dialogs::{AppModalState, AppModalResult, DeleteConfirmResult, render_app_modal, render_delete_confirm};
14use crate::ui::theme;
15use crate::utils::uuid_simple;
16
17/// Aplicação principal do Hub Iris.
18/// 
19/// Gerencia o estado da UI e coordena todos os microserviços.
20pub struct AppHub {
21    // Dados
22    state: AppState,
23    config_manager: ConfigManager,
24    
25    // Serviços
26    process_manager: ProcessManager,
27    icon_cache: IconCache,
28    available_icons: Vec<IconInfo>,
29    
30    // Estado da UI
31    search_filter: String,
32    show_add_modal: bool,
33    show_edit_modal: bool,
34    modal_state: AppModalState,
35    show_delete_confirm: Option<usize>,
36}
37
38impl AppHub {
39    /// Cria uma nova instância da aplicação
40    pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
41        let config_manager = ConfigManager::new();
42        let mut state = config_manager.load();
43        
44        // Garantir que todos os apps tenham ID
45        for app in &mut state.apps {
46            if app.id.is_empty() {
47                app.id = uuid_simple();
48            }
49        }
50
51        Self {
52            state,
53            config_manager,
54            process_manager: ProcessManager::new(),
55            icon_cache: IconCache::new(),
56            available_icons: load_available_icons(),
57            search_filter: String::new(),
58            show_add_modal: false,
59            show_edit_modal: false,
60            modal_state: AppModalState::default(),
61            show_delete_confirm: None,
62        }
63    }
64
65    /// Salva o estado atual em disco
66    fn save_state(&self) {
67        if let Err(e) = self.config_manager.save(&self.state) {
68            eprintln!("Erro ao salvar: {}", e);
69        }
70    }
71
72    /// Inicia o modal para adicionar nova aplicação
73    fn start_add_app(&mut self) {
74        self.modal_state = AppModalState::new_app();
75        self.show_add_modal = true;
76    }
77
78    /// Inicia o modal para editar uma aplicação
79    fn start_edit_app(&mut self, index: usize) {
80        if let Some(app) = self.state.apps.get(index) {
81            self.modal_state = AppModalState::edit_app(app.clone(), index);
82            self.show_edit_modal = true;
83        }
84    }
85
86    /// Fecha o modal e limpa o estado
87    fn close_modal(&mut self) {
88        self.show_add_modal = false;
89        self.show_edit_modal = false;
90        self.modal_state.reset();
91    }
92
93    /// Processa o resultado do modal de aplicação
94    fn handle_modal_result(&mut self, result: AppModalResult) {
95        match result {
96            AppModalResult::Save(app, edit_index) => {
97                if let Some(index) = edit_index {
98                    self.state.apps[index] = app;
99                } else {
100                    self.state.add_app(app);
101                }
102                self.save_state();
103                self.close_modal();
104            }
105            AppModalResult::Cancelled => {
106                self.close_modal();
107            }
108            AppModalResult::None => {}
109        }
110    }
111
112    /// Processa o resultado do diálogo de confirmação de exclusão
113    fn handle_delete_result(&mut self, result: DeleteConfirmResult) {
114        match result {
115            DeleteConfirmResult::Confirmed(index) => {
116                self.state.remove_app(index);
117                self.save_state();
118                self.show_delete_confirm = None;
119            }
120            DeleteConfirmResult::Cancelled => {
121                self.show_delete_confirm = None;
122            }
123            DeleteConfirmResult::None => {}
124        }
125    }
126
127    /// Exporta as configurações para um arquivo
128    fn export_config(&self) {
129        if let Some(path) = rfd::FileDialog::new()
130            .set_title("Exportar configurações do Iris")
131            .set_file_name("iris-config.json")
132            .add_filter("JSON", &["json"])
133            .save_file()
134        {
135            if let Err(e) = self.config_manager.export(&self.state, &path) {
136                eprintln!("{}", e);
137            }
138        }
139    }
140
141    /// Importa configurações de um arquivo
142    fn import_config(&mut self) {
143        if let Some(path) = rfd::FileDialog::new()
144            .set_title("Importar configurações do Iris")
145            .add_filter("JSON", &["json"])
146            .pick_file()
147        {
148            match self.config_manager.import(&path) {
149                Ok(imported_state) => {
150                    self.state.apps.extend(imported_state.apps);
151                    self.save_state();
152                }
153                Err(e) => {
154                    eprintln!("{}", e);
155                }
156            }
157        }
158    }
159
160    /// Renderiza a área central com os cards das aplicações
161    fn render_central_panel(&mut self, ui: &mut egui::Ui) {
162        if self.state.apps.is_empty() {
163            if render_empty_state(ui) {
164                self.start_add_app();
165            }
166        } else {
167            self.render_apps_grid(ui);
168        }
169    }
170
171    /// Renderiza o grid de aplicações
172    fn render_apps_grid(&mut self, ui: &mut egui::Ui) {
173        egui::ScrollArea::vertical().show(ui, |ui| {
174            ui.add_space(20.0);
175
176            // Filtrar apps pela busca
177            let filtered_indices: Vec<usize> = self.state.apps
178                .iter()
179                .enumerate()
180                .filter(|(_, app)| {
181                    self.search_filter.is_empty()
182                        || app.name.to_lowercase().contains(&self.search_filter.to_lowercase())
183                        || app.working_dir.to_lowercase().contains(&self.search_filter.to_lowercase())
184                })
185                .map(|(i, _)| i)
186                .collect();
187
188            if filtered_indices.is_empty() {
189                render_no_results(ui, &self.search_filter);
190            } else {
191                self.render_filtered_apps(ui, &filtered_indices);
192            }
193
194            ui.add_space(20.0);
195        });
196    }
197
198    /// Renderiza as aplicações filtradas em um grid
199    fn render_filtered_apps(&mut self, ui: &mut egui::Ui, filtered_indices: &[usize]) {
200        let available_width = ui.available_width();
201        let card_width = 260.0;
202        let spacing = 16.0;
203        let cards_per_row = ((available_width + spacing) / (card_width + spacing)).floor() as usize;
204        let cards_per_row = cards_per_row.max(1);
205
206        let mut app_to_launch: Option<usize> = None;
207        let mut app_to_stop: Option<usize> = None;
208        let mut app_to_restart: Option<usize> = None;
209        let mut app_to_edit: Option<usize> = None;
210        let mut app_to_delete: Option<usize> = None;
211
212        egui::Grid::new("apps_grid")
213            .spacing([spacing, spacing])
214            .show(ui, |ui| {
215                for (col, &index) in filtered_indices.iter().enumerate() {
216                    let app = &self.state.apps[index];
217                    let is_running = self.process_manager.is_running(&app.id);
218                    let is_loading = self.process_manager.is_loading(&app.id);
219                    
220                    let actions = render_app_card(
221                        ui,
222                        app,
223                        is_running,
224                        is_loading,
225                        &mut self.icon_cache,
226                    );
227                    
228                    if actions.start_clicked {
229                        app_to_launch = Some(index);
230                    }
231                    if actions.stop_clicked {
232                        app_to_stop = Some(index);
233                    }
234                    if actions.restart_clicked {
235                        app_to_restart = Some(index);
236                    }
237                    if actions.edit_clicked {
238                        app_to_edit = Some(index);
239                    }
240                    if actions.delete_clicked {
241                        app_to_delete = Some(index);
242                    }
243
244                    if (col + 1) % cards_per_row == 0 {
245                        ui.end_row();
246                    }
247                }
248            });
249
250        // Executar ações
251        if let Some(index) = app_to_launch {
252            let app = self.state.apps[index].clone();
253            self.process_manager.launch_app(&app);
254        }
255        if let Some(index) = app_to_stop {
256            let app = &self.state.apps[index];
257            self.process_manager.stop_app(&app.id, Some(&app.name), Some(&app.commands));
258        }
259        if let Some(index) = app_to_restart {
260            let app = self.state.apps[index].clone();
261            self.process_manager.restart_app(&app);
262        }
263        if let Some(index) = app_to_edit {
264            self.start_edit_app(index);
265        }
266        if let Some(index) = app_to_delete {
267            self.show_delete_confirm = Some(index);
268        }
269    }
270}
271
272impl eframe::App for AppHub {
273    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
274        // Aplicar tema
275        theme::apply_theme(ctx);
276        
277        // Limpar processos mortos
278        self.process_manager.cleanup_dead_processes();
279        
280        // Configurar repaint
281        let needs_fast_repaint = self.process_manager.has_loading() || self.process_manager.has_running();
282        if needs_fast_repaint {
283            ctx.request_repaint_after(Duration::from_millis(250));
284        } else {
285            ctx.request_repaint_after(Duration::from_secs(2));
286        }
287
288        // Header
289        egui::TopBottomPanel::top("header")
290            .frame(egui::Frame::none()
291                .fill(egui::Color32::from_rgb(22, 22, 26))
292                .inner_margin(egui::Margin::symmetric(20.0, 16.0))
293            )
294            .show(ctx, |ui| {
295                let header_actions = render_header(ui, &mut self.search_filter);
296                
297                if header_actions.add_app_clicked {
298                    self.start_add_app();
299                }
300                if header_actions.export_clicked {
301                    self.export_config();
302                }
303                if header_actions.import_clicked {
304                    self.import_config();
305                }
306            });
307
308        // Footer
309        egui::TopBottomPanel::bottom("footer")
310            .frame(egui::Frame::none()
311                .fill(egui::Color32::from_rgb(22, 22, 26))
312                .inner_margin(egui::Margin::symmetric(20.0, 12.0))
313            )
314            .show(ctx, |ui| {
315                render_footer(ui, self.state.app_count(), self.process_manager.running_count());
316            });
317
318        // Área central
319        egui::CentralPanel::default()
320            .frame(egui::Frame::none()
321                .fill(egui::Color32::from_rgb(18, 18, 22))
322                .inner_margin(egui::Margin::same(24.0))
323            )
324            .show(ctx, |ui| {
325                self.render_central_panel(ui);
326            });
327
328        // Modais
329        if self.show_add_modal || self.show_edit_modal {
330            let result = render_app_modal(
331                ctx,
332                &mut self.modal_state,
333                self.show_edit_modal,
334                &self.available_icons,
335                &mut self.icon_cache,
336            );
337            self.handle_modal_result(result);
338        }
339
340        // Diálogo de confirmação de exclusão
341        if let Some(index) = self.show_delete_confirm {
342            let app_name = self.state.apps.get(index)
343                .map(|a| a.name.clone())
344                .unwrap_or_default();
345            let result = render_delete_confirm(ctx, &app_name, index);
346            self.handle_delete_result(result);
347        }
348    }
349}