1use 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
17pub struct AppHub {
21 state: AppState,
23 config_manager: ConfigManager,
24
25 process_manager: ProcessManager,
27 icon_cache: IconCache,
28 available_icons: Vec<IconInfo>,
29
30 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 pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
41 let config_manager = ConfigManager::new();
42 let mut state = config_manager.load();
43
44 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 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 fn start_add_app(&mut self) {
74 self.modal_state = AppModalState::new_app();
75 self.show_add_modal = true;
76 }
77
78 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 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 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 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 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 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 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 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 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 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 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 theme::apply_theme(ctx);
276
277 self.process_manager.cleanup_dead_processes();
279
280 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 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 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 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 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 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}