1use eframe::egui::{self, RichText};
6use crate::core::{AppConfig, IconInfo};
7use crate::services::IconCache;
8
9pub struct AppModalState {
11 pub app: AppConfig,
13 pub new_command: String,
15 pub show_icon_picker: bool,
17 pub icon_search_filter: String,
19 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 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 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 pub fn reset(&mut self) {
58 *self = Self::default();
59 }
60}
61
62pub enum AppModalResult {
64 None,
66 Save(AppConfig, Option<usize>),
68 Cancelled,
70}
71
72pub 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 render_name_and_icon(ui, state, icon_cache);
108
109 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 render_working_dir(ui, state);
118
119 ui.add_space(15.0);
120
121 render_commands_list(ui, state);
123
124 ui.add_space(8.0);
125
126 render_suggested_commands(ui, state);
128
129 ui.add_space(20.0);
130 ui.separator();
131 ui.add_space(10.0);
132
133 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 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}