1use glib::clone;
2use gtk::STYLE_PROVIDER_PRIORITY_USER;
3use gtk::prelude::*;
4use gtk::{Align, Box as GtkBox, HeaderBar, Label, ListBox, Orientation, Switch, glib};
5use std::cell::Cell;
6use std::collections::HashSet;
7use std::rc::Rc;
8
9use nmrs::models;
10
11use crate::ui::networks;
12use crate::ui::networks::NetworksContext;
13use crate::ui::wired_devices;
14
15pub struct ThemeDef {
16 pub key: &'static str,
17 pub name: &'static str,
18 pub css: &'static str,
19}
20
21pub static THEMES: &[ThemeDef] = &[
22 ThemeDef {
23 key: "gruvbox",
24 name: "Gruvbox",
25 css: include_str!("../themes/gruvbox.css"),
26 },
27 ThemeDef {
28 key: "nord",
29 name: "Nord",
30 css: include_str!("../themes/nord.css"),
31 },
32 ThemeDef {
33 key: "dracula",
34 name: "Dracula",
35 css: include_str!("../themes/dracula.css"),
36 },
37 ThemeDef {
38 key: "catppuccin",
39 name: "Catppuccin",
40 css: include_str!("../themes/catppuccin.css"),
41 },
42 ThemeDef {
43 key: "tokyo",
44 name: "Tokyo Night",
45 css: include_str!("../themes/tokyo.css"),
46 },
47];
48
49pub fn build_header(
50 ctx: Rc<NetworksContext>,
51 list_container: &GtkBox,
52 is_scanning: Rc<Cell<bool>>,
53 window: >k::ApplicationWindow,
54) -> HeaderBar {
55 let header = HeaderBar::new();
56 header.set_show_title_buttons(false);
57
58 let list_container = list_container.clone();
59
60 let wifi_box = GtkBox::new(Orientation::Horizontal, 6);
61 let wifi_label = Label::new(Some("Wi-Fi"));
62 wifi_label.set_halign(gtk::Align::Start);
63 wifi_label.add_css_class("wifi-label");
64
65 let names: Vec<&str> = THEMES.iter().map(|t| t.name).collect();
66 let dropdown = gtk::DropDown::from_strings(&names);
67
68 if let Some(saved) = crate::theme_config::load_theme()
69 && let Some(idx) = THEMES.iter().position(|t| t.key == saved.as_str())
70 {
71 dropdown.set_selected(idx as u32);
72 }
73
74 dropdown.set_valign(gtk::Align::Center);
75 dropdown.add_css_class("dropdown");
76
77 let window_weak = window.downgrade();
78
79 dropdown.connect_selected_notify(move |dd| {
80 let idx = dd.selected() as usize;
81 if idx >= THEMES.len() {
82 return;
83 }
84
85 let theme = &THEMES[idx];
86
87 if let Some(window) = window_weak.upgrade() {
88 let provider = gtk::CssProvider::new();
89 provider.load_from_data(theme.css);
90
91 let display = gtk::prelude::RootExt::display(&window);
92
93 gtk::style_context_add_provider_for_display(
94 &display,
95 &provider,
96 STYLE_PROVIDER_PRIORITY_USER,
97 );
98
99 crate::theme_config::save_theme(theme.key);
100
101 crate::style::load_user_css();
103 }
104 });
105
106 wifi_box.append(&wifi_label);
107 wifi_box.append(&dropdown);
108 header.pack_start(&wifi_box);
109
110 let refresh_btn = gtk::Button::from_icon_name("view-refresh-symbolic");
111 refresh_btn.add_css_class("refresh-btn");
112 refresh_btn.set_tooltip_text(Some("Refresh networks and devices"));
113 header.pack_end(&refresh_btn);
114 refresh_btn.connect_clicked(clone!(
115 #[weak]
116 list_container,
117 #[strong]
118 ctx,
119 #[strong]
120 is_scanning,
121 move |_| {
122 let ctx = ctx.clone();
123 let list_container = list_container.clone();
124 let is_scanning = is_scanning.clone();
125
126 glib::MainContext::default().spawn_local(async move {
127 refresh_networks(ctx, &list_container, &is_scanning).await;
128 });
129 }
130 ));
131
132 let theme_btn = gtk::Button::new();
133 theme_btn.add_css_class("theme-toggle-btn");
134 theme_btn.set_valign(gtk::Align::Center);
135 theme_btn.set_has_frame(false);
136
137 let is_light = window.has_css_class("light-theme");
138 let initial_icon = if is_light {
139 "weather-clear-night-symbolic"
140 } else {
141 "weather-clear-symbolic"
142 };
143 theme_btn.set_icon_name(initial_icon);
144
145 let window_weak = window.downgrade();
146 theme_btn.connect_clicked(move |btn| {
147 if let Some(window) = window_weak.upgrade() {
148 let is_light = window.has_css_class("light-theme");
149
150 if is_light {
151 window.remove_css_class("light-theme");
152 window.add_css_class("dark-theme");
153 btn.set_icon_name("weather-clear-symbolic");
154 crate::theme_config::save_theme("light");
155 } else {
156 window.remove_css_class("dark-theme");
157 window.add_css_class("light-theme");
158 btn.set_icon_name("weather-clear-night-symbolic");
159 crate::theme_config::save_theme("dark");
160 }
161 }
162 });
163
164 header.pack_end(&theme_btn);
165
166 let wifi_switch = Switch::new();
167 wifi_switch.set_valign(gtk::Align::Center);
168 header.pack_end(&wifi_switch);
169 wifi_switch.set_size_request(24, 24);
170
171 header.pack_end(&ctx.status);
172
173 {
174 let list_container = list_container.clone();
175 let wifi_switch = wifi_switch.clone();
176 let ctx = ctx.clone();
177 let is_scanning = is_scanning.clone();
178
179 glib::MainContext::default().spawn_local(async move {
180 ctx.stack.set_visible_child_name("loading");
181 clear_children(&list_container);
182
183 match ctx.nm.wifi_enabled().await {
184 Ok(enabled) => {
185 wifi_switch.set_active(enabled);
186 if enabled {
187 refresh_networks(ctx, &list_container, &is_scanning).await;
188 }
189 }
190 Err(err) => {
191 ctx.status
192 .set_text(&format!("Error fetching networks: {err}"));
193 }
194 }
195 })
196 };
197
198 {
199 let ctx = ctx.clone();
200
201 wifi_switch.connect_active_notify(move |sw| {
202 let ctx = ctx.clone();
203 let list_container = list_container.clone();
204 let sw = sw.clone();
205 let is_scanning = is_scanning.clone();
206
207 glib::MainContext::default().spawn_local(async move {
208 clear_children(&list_container);
209
210 if let Err(err) = ctx.nm.set_wifi_enabled(sw.is_active()).await {
211 ctx.status.set_text(&format!("Error setting Wi-Fi: {err}"));
212 return;
213 }
214
215 if sw.is_active() {
216 if ctx.nm.wait_for_wifi_ready().await.is_ok() {
217 refresh_networks(ctx, &list_container, &is_scanning).await;
218 } else {
219 ctx.status.set_text("Wi-Fi failed to initialize");
220 }
221 }
222 });
223 });
224 }
225
226 header
227}
228
229pub async fn refresh_networks(
230 ctx: Rc<NetworksContext>,
231 list_container: &GtkBox,
232 is_scanning: &Rc<Cell<bool>>,
233) {
234 if is_scanning.get() {
235 ctx.status.set_text("Scan already in progress");
236 return;
237 }
238 is_scanning.set(true);
239
240 clear_children(list_container);
241 ctx.status.set_text("Scanning...");
242
243 match ctx.nm.list_wired_devices().await {
245 Ok(wired_devices) => {
246 let available_devices: Vec<_> = wired_devices
249 .into_iter()
250 .filter(|dev| {
251 let show = matches!(
252 dev.state,
253 models::DeviceState::Activated
254 | models::DeviceState::Disconnected
255 | models::DeviceState::Prepare
256 | models::DeviceState::Config
257 );
258 show
266 })
267 .collect();
268
269 if !available_devices.is_empty() {
275 let wired_header = Label::new(Some("Wired"));
276 wired_header.add_css_class("section-header");
277 wired_header.add_css_class("wired-section-header");
278 wired_header.set_halign(Align::Start);
279 wired_header.set_margin_top(8);
280 wired_header.set_margin_bottom(4);
281 wired_header.set_margin_start(12);
282 list_container.append(&wired_header);
283
284 let wired_list = wired_devices::wired_devices_view(
285 ctx.clone(),
286 &available_devices,
287 ctx.wired_details_page.clone(),
288 );
289 wired_list.add_css_class("wired-devices-list");
290 list_container.append(&wired_list);
291
292 let separator = gtk::Separator::new(Orientation::Horizontal);
293 separator.add_css_class("device-separator");
294 separator.set_margin_top(12);
295 separator.set_margin_bottom(12);
296 list_container.append(&separator);
297 }
298 }
299 Err(e) => {
300 eprintln!("Failed to list wired devices: {}", e);
301 }
302 }
303
304 let wireless_header = Label::new(Some("Wireless"));
305 wireless_header.add_css_class("section-header");
306 wireless_header.add_css_class("wireless-section-header");
307 wireless_header.set_halign(Align::Start);
308 wireless_header.set_margin_top(8);
309 wireless_header.set_margin_bottom(4);
310 wireless_header.set_margin_start(12);
311 list_container.append(&wireless_header);
312
313 if let Err(err) = ctx.nm.scan_networks().await {
314 ctx.status.set_text(&format!("Scan failed: {err}"));
315 is_scanning.set(false);
316 return;
317 }
318
319 let mut last_len = 0;
320 for _ in 0..5 {
321 let nets = ctx.nm.list_networks().await.unwrap_or_default();
322 if nets.len() == last_len && last_len > 0 {
323 break;
324 }
325 last_len = nets.len();
326 glib::timeout_future_seconds(1).await;
327 }
328
329 match ctx.nm.list_networks().await {
330 Ok(mut nets) => {
331 let current_conn = ctx.nm.current_connection_info().await;
332 let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
333 let ssid_str = ssid.clone();
334 let band: Option<String> = freq
335 .and_then(crate::ui::freq_to_band)
336 .map(|s| s.to_string());
337 (Some(ssid_str), band)
338 } else {
339 (None, None)
340 };
341
342 nets.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)));
343
344 let mut seen_combinations = HashSet::new();
345 nets.retain(|net| {
346 let band = net.frequency.and_then(crate::ui::freq_to_band);
347 let key = (net.ssid.clone(), band);
348 seen_combinations.insert(key)
349 });
350
351 ctx.status.set_text("");
352
353 let list: ListBox = networks::networks_view(
354 ctx.clone(),
355 &nets,
356 current_ssid.as_deref(),
357 current_band.as_deref(),
358 );
359 list_container.append(&list);
360 ctx.stack.set_visible_child_name("networks");
361 }
362 Err(err) => ctx
363 .status
364 .set_text(&format!("Error fetching networks: {err}")),
365 }
366
367 is_scanning.set(false);
368}
369
370pub fn clear_children(container: >k::Box) {
371 let mut child = container.first_child();
372 while let Some(widget) = child {
373 child = widget.next_sibling();
374 container.remove(&widget);
375 }
376}
377
378pub async fn refresh_networks_no_scan(
382 ctx: Rc<NetworksContext>,
383 list_container: &GtkBox,
384 is_scanning: &Rc<Cell<bool>>,
385) {
386 if is_scanning.get() {
387 return;
389 }
390
391 is_scanning.set(true);
393
394 clear_children(list_container);
395
396 if let Ok(wired_devices) = ctx.nm.list_wired_devices().await {
398 let available_devices: Vec<_> = wired_devices
402 .into_iter()
403 .filter(|dev| {
404 let show = matches!(
405 dev.state,
406 models::DeviceState::Activated
407 | models::DeviceState::Disconnected
408 | models::DeviceState::Prepare
409 | models::DeviceState::Config
410 | models::DeviceState::Unmanaged
411 );
412 show
420 })
421 .collect();
422
423 if !available_devices.is_empty() {
429 let wired_header = Label::new(Some("Wired"));
430 wired_header.add_css_class("section-header");
431 wired_header.add_css_class("wired-section-header");
432 wired_header.set_halign(Align::Start);
433 wired_header.set_margin_top(8);
434 wired_header.set_margin_bottom(4);
435 wired_header.set_margin_start(12);
436 list_container.append(&wired_header);
437
438 let wired_list = wired_devices::wired_devices_view(
439 ctx.clone(),
440 &available_devices,
441 ctx.wired_details_page.clone(),
442 );
443 wired_list.add_css_class("wired-devices-list");
444 list_container.append(&wired_list);
445
446 let separator = gtk::Separator::new(Orientation::Horizontal);
447 separator.add_css_class("device-separator");
448 separator.set_margin_top(12);
449 separator.set_margin_bottom(12);
450 list_container.append(&separator);
451 }
452 }
453
454 let wireless_header = Label::new(Some("Wireless"));
455 wireless_header.add_css_class("section-header");
456 wireless_header.add_css_class("wireless-section-header");
457 wireless_header.set_halign(Align::Start);
458 wireless_header.set_margin_top(8);
459 wireless_header.set_margin_bottom(4);
460 wireless_header.set_margin_start(12);
461 list_container.append(&wireless_header);
462
463 match ctx.nm.list_networks().await {
464 Ok(mut nets) => {
465 let current_conn = ctx.nm.current_connection_info().await;
466 let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
467 let ssid_str = ssid.clone();
468 let band: Option<String> = freq
469 .and_then(crate::ui::freq_to_band)
470 .map(|s| s.to_string());
471 (Some(ssid_str), band)
472 } else {
473 (None, None)
474 };
475
476 nets.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)));
477
478 let mut seen_combinations = HashSet::new();
479 nets.retain(|net| {
480 let band = net.frequency.and_then(crate::ui::freq_to_band);
481 let key = (net.ssid.clone(), band);
482 seen_combinations.insert(key)
483 });
484
485 let list: ListBox = networks::networks_view(
486 ctx.clone(),
487 &nets,
488 current_ssid.as_deref(),
489 current_band.as_deref(),
490 );
491 list_container.append(&list);
492 ctx.stack.set_visible_child_name("networks");
493 }
494 Err(err) => {
495 ctx.status
496 .set_text(&format!("Error fetching networks: {err}"));
497 }
498 }
499
500 is_scanning.set(false);
502}