Skip to main content

iris_hub/ui/dialogs/
app_modal.rs

1//! # App Modal Dialog
2//! 
3//! Modal para adicionar/editar aplicações.
4
5use eframe::egui::{self, RichText};
6use crate::core::{AppConfig, IconInfo};
7use crate::services::IconCache;
8
9/// Estado do modal de aplicação
10pub struct AppModalState {
11    /// Aplicação sendo editada
12    pub app: AppConfig,
13    /// Novo comando sendo digitado
14    pub new_command: String,
15    /// Se o picker de ícones está aberto
16    pub show_icon_picker: bool,
17    /// Filtro de busca de ícones
18    pub icon_search_filter: String,
19    /// Índice da app sendo editada (None = nova app)
20    pub edit_index: Option<usize>,
21}
22
23impl Default for AppModalState {
24    fn default() -> Self {
25        Self {
26            app: AppConfig::default(),
27            new_command: String::new(),
28            show_icon_picker: false,
29            icon_search_filter: String::new(),
30            edit_index: None,
31        }
32    }
33}
34
35impl AppModalState {
36    /// Cria um novo estado para adicionar uma aplicação
37    pub fn new_app() -> Self {
38        Self {
39            app: AppConfig {
40                id: crate::utils::uuid_simple(),
41                ..Default::default()
42            },
43            ..Default::default()
44        }
45    }
46    
47    /// Cria um estado para editar uma aplicação existente
48    pub fn edit_app(app: AppConfig, index: usize) -> Self {
49        Self {
50            app,
51            edit_index: Some(index),
52            ..Default::default()
53        }
54    }
55    
56    /// Reseta o estado
57    pub fn reset(&mut self) {
58        *self = Self::default();
59    }
60}
61
62/// Resultado das ações do modal
63pub enum AppModalResult {
64    /// Modal fechado, nenhuma ação
65    None,
66    /// Salvar aplicação
67    Save(AppConfig, Option<usize>),
68    /// Cancelado
69    Cancelled,
70}
71
72/// Renderiza o modal de adicionar/editar aplicação.
73/// 
74/// # Argumentos
75/// * `ctx` - Contexto do egui
76/// * `state` - Estado do modal
77/// * `is_editing` - Se está editando ou adicionando
78/// * `available_icons` - Lista de ícones disponíveis
79/// * `icon_cache` - Cache de ícones
80/// 
81/// # Retorno
82/// `AppModalResult` indicando a ação tomada
83pub fn render_app_modal(
84    ctx: &egui::Context,
85    state: &mut AppModalState,
86    is_editing: bool,
87    available_icons: &[IconInfo],
88    icon_cache: &mut IconCache,
89) -> AppModalResult {
90    let mut result = AppModalResult::None;
91    
92    let title = if is_editing {
93        "✏ Editar Aplicação"
94    } else {
95        "➕ Nova Aplicação"
96    };
97
98    egui::Window::new(title)
99        .collapsible(false)
100        .resizable(true)
101        .default_width(500.0)
102        .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
103        .show(ctx, |ui| {
104            ui.add_space(10.0);
105
106            // Nome e Ícone
107            render_name_and_icon(ui, state, icon_cache);
108            
109            // Icon picker
110            if state.show_icon_picker {
111                render_icon_picker(ui, state, available_icons, icon_cache);
112            }
113
114            ui.add_space(15.0);
115
116            // Diretório de trabalho
117            render_working_dir(ui, state);
118
119            ui.add_space(15.0);
120
121            // Lista de comandos
122            render_commands_list(ui, state);
123
124            ui.add_space(8.0);
125
126            // Comandos sugeridos
127            render_suggested_commands(ui, state);
128
129            ui.add_space(20.0);
130            ui.separator();
131            ui.add_space(10.0);
132
133            // Botões de ação
134            result = render_modal_actions(ui, state, is_editing);
135        });
136
137    result
138}
139
140fn render_name_and_icon(ui: &mut egui::Ui, state: &mut AppModalState, icon_cache: &mut IconCache) {
141    ui.horizontal(|ui| {
142        ui.vertical(|ui| {
143            ui.label("Ícone:");
144            ui.horizontal(|ui| {
145                let icon_size = egui::vec2(40.0, 40.0);
146                
147                if !state.app.icon_emoji.is_empty() {
148                    if let Some(texture) = icon_cache.get_or_load(ui.ctx(), &state.app.icon_emoji) {
149                        let btn = egui::ImageButton::new(egui::load::SizedTexture::new(
150                            texture.id(),
151                            icon_size,
152                        ));
153                        if ui.add(btn).on_hover_text("Clique para trocar").clicked() {
154                            state.show_icon_picker = !state.show_icon_picker;
155                        }
156                    } else {
157                        let btn = egui::Button::new(
158                            RichText::new(&state.app.icon_emoji).size(10.0)
159                        ).min_size(icon_size);
160                        if ui.add(btn).on_hover_text("Clique para trocar").clicked() {
161                            state.show_icon_picker = !state.show_icon_picker;
162                        }
163                    }
164                } else {
165                    let btn = egui::Button::new(
166                        RichText::new("🚀").size(24.0)
167                    ).min_size(icon_size);
168                    if ui.add(btn).on_hover_text("Clique para escolher").clicked() {
169                        state.show_icon_picker = !state.show_icon_picker;
170                    }
171                }
172                
173                if !state.app.icon_emoji.is_empty() {
174                    ui.label(
175                        RichText::new(&state.app.icon_emoji)
176                            .size(11.0)
177                            .color(egui::Color32::GRAY)
178                    );
179                }
180            });
181        });
182
183        ui.add_space(10.0);
184
185        ui.vertical(|ui| {
186            ui.label("Nome da Aplicação:");
187            ui.add(
188                egui::TextEdit::singleline(&mut state.app.name)
189                    .desired_width(300.0)
190                    .hint_text("Minha Aplicação"),
191            );
192        });
193    });
194}
195
196fn render_icon_picker(
197    ui: &mut egui::Ui,
198    state: &mut AppModalState,
199    available_icons: &[IconInfo],
200    icon_cache: &mut IconCache,
201) {
202    ui.add_space(10.0);
203    ui.group(|ui| {
204        ui.horizontal(|ui| {
205            ui.label("🎨 Escolha um ícone:");
206            ui.add_space(10.0);
207            ui.add(
208                egui::TextEdit::singleline(&mut state.icon_search_filter)
209                    .desired_width(200.0)
210                    .hint_text("🔍 Filtrar (ex: react, python, docker...)")
211            );
212            
213            if ui.small_button("❌").clicked() {
214                state.show_icon_picker = false;
215                state.icon_search_filter.clear();
216            }
217        });
218        
219        ui.add_space(5.0);
220        
221        let filter = state.icon_search_filter.to_lowercase();
222        let filtered_icons: Vec<&IconInfo> = available_icons
223            .iter()
224            .filter(|icon| filter.is_empty() || icon.name.to_lowercase().contains(&filter))
225            .take(50)
226            .collect();
227        
228        ui.label(
229            RichText::new(format!("{} ícones encontrados", filtered_icons.len()))
230                .size(11.0)
231                .color(egui::Color32::GRAY)
232        );
233        
234        egui::ScrollArea::vertical()
235            .max_height(200.0)
236            .show(ui, |ui| {
237                ui.horizontal_wrapped(|ui| {
238                    for icon in filtered_icons {
239                        let icon_name = icon.name.clone();
240                        
241                        if let Some(texture) = icon_cache.get_or_load(ui.ctx(), &icon_name) {
242                            let btn = egui::ImageButton::new(egui::load::SizedTexture::new(
243                                texture.id(),
244                                egui::vec2(32.0, 32.0),
245                            ));
246                            if ui.add(btn).on_hover_text(&icon_name).clicked() {
247                                state.app.icon_emoji = icon_name;
248                                state.show_icon_picker = false;
249                                state.icon_search_filter.clear();
250                            }
251                        } else {
252                            let short_name: String = icon_name.chars().take(3).collect();
253                            let btn = egui::Button::new(
254                                RichText::new(&short_name).size(10.0)
255                            ).min_size(egui::vec2(32.0, 32.0));
256                            if ui.add(btn).on_hover_text(&icon_name).clicked() {
257                                state.app.icon_emoji = icon_name;
258                                state.show_icon_picker = false;
259                                state.icon_search_filter.clear();
260                            }
261                        }
262                    }
263                });
264            });
265    });
266}
267
268fn render_working_dir(ui: &mut egui::Ui, state: &mut AppModalState) {
269    ui.label("Pasta Inicial (Working Directory):");
270    ui.horizontal(|ui| {
271        ui.add(
272            egui::TextEdit::singleline(&mut state.app.working_dir)
273                .desired_width(380.0)
274                .hint_text("C:\\meus-projetos\\minha-app"),
275        );
276        if ui.button("📁 Selecionar").clicked() {
277            if let Some(path) = rfd::FileDialog::new().pick_folder() {
278                state.app.working_dir = path.display().to_string();
279            }
280        }
281    });
282}
283
284fn render_commands_list(ui: &mut egui::Ui, state: &mut AppModalState) {
285    ui.label("Comandos (serão executados em sequência):");
286    
287    ui.add_space(5.0);
288
289    let mut to_remove: Option<usize> = None;
290    let mut to_move_up: Option<usize> = None;
291    let mut to_move_down: Option<usize> = None;
292    let commands_len = state.app.commands.len();
293
294    egui::ScrollArea::vertical()
295        .max_height(200.0)
296        .show(ui, |ui| {
297            for i in 0..commands_len {
298                ui.horizontal(|ui| {
299                    ui.label(format!("{}.", i + 1));
300                    
301                    ui.add_enabled_ui(i > 0, |ui| {
302                        if ui.small_button("⬆").on_hover_text("Mover para cima").clicked() {
303                            to_move_up = Some(i);
304                        }
305                    });
306                    ui.add_enabled_ui(i < commands_len - 1, |ui| {
307                        if ui.small_button("⬇").on_hover_text("Mover para baixo").clicked() {
308                            to_move_down = Some(i);
309                        }
310                    });
311
312                    ui.add(
313                        egui::TextEdit::singleline(&mut state.app.commands[i])
314                            .desired_width(320.0)
315                            .font(egui::TextStyle::Monospace),
316                    );
317
318                    if ui.button("❌").on_hover_text("Remover comando").clicked() {
319                        to_remove = Some(i);
320                    }
321                });
322            }
323        });
324
325    if let Some(i) = to_remove {
326        state.app.commands.remove(i);
327    }
328    if let Some(i) = to_move_up {
329        state.app.commands.swap(i, i - 1);
330    }
331    if let Some(i) = to_move_down {
332        state.app.commands.swap(i, i + 1);
333    }
334
335    ui.add_space(10.0);
336
337    // Adicionar novo comando
338    ui.horizontal(|ui| {
339        let response = ui.add(
340            egui::TextEdit::singleline(&mut state.new_command)
341                .desired_width(380.0)
342                .hint_text("Digite um comando (ex: npm run dev)")
343                .font(egui::TextStyle::Monospace),
344        );
345
346        let add_clicked = ui.button("➕ Adicionar").clicked();
347        let enter_pressed = response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
348
349        if (add_clicked || enter_pressed) && !state.new_command.trim().is_empty() {
350            state.app.commands.push(state.new_command.trim().to_string());
351            state.new_command.clear();
352        }
353    });
354}
355
356fn render_suggested_commands(ui: &mut egui::Ui, state: &mut AppModalState) {
357    ui.collapsing("💡 Comandos Comuns", |ui| {
358        ui.horizontal_wrapped(|ui| {
359            let suggestions = [
360                "npm install",
361                "npm run dev",
362                "npm start",
363                "yarn dev",
364                "pnpm dev",
365                "cargo run",
366                "python main.py",
367                "dotnet run",
368                "go run .",
369                "docker-compose up",
370            ];
371            for suggestion in suggestions {
372                if ui.small_button(suggestion).clicked() {
373                    state.app.commands.push(suggestion.to_string());
374                }
375            }
376        });
377    });
378}
379
380fn render_modal_actions(
381    ui: &mut egui::Ui,
382    state: &mut AppModalState,
383    is_editing: bool,
384) -> AppModalResult {
385    let mut result = AppModalResult::None;
386    
387    ui.horizontal(|ui| {
388        if ui.button(RichText::new("❌ Cancelar").size(14.0)).clicked() {
389            result = AppModalResult::Cancelled;
390        }
391
392        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
393            let save_enabled = !state.app.name.trim().is_empty();
394            
395            ui.add_enabled_ui(save_enabled, |ui| {
396                let save_text = if is_editing { "💾 Salvar" } else { "✅ Criar" };
397                if ui.button(
398                    RichText::new(save_text)
399                        .size(14.0)
400                        .color(egui::Color32::WHITE),
401                ).clicked() {
402                    let mut app = state.app.clone();
403                    app.name = app.name.trim().to_string();
404                    app.working_dir = app.working_dir.trim().to_string();
405                    result = AppModalResult::Save(app, state.edit_index);
406                }
407            });
408
409            if !save_enabled {
410                ui.label(
411                    RichText::new("⚠ Nome é obrigatório")
412                        .size(12.0)
413                        .color(egui::Color32::from_rgb(255, 200, 100)),
414                );
415            }
416        });
417    });
418    
419    result
420}