1use glib::clone;
2use gtk::prelude::*;
3use gtk::STYLE_PROVIDER_PRIORITY_USER;
4use gtk::{glib, Align, Box as GtkBox, HeaderBar, Label, ListBox, Orientation, Switch};
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 if let Some(idx) = THEMES.iter().position(|t| t.key == saved.as_str()) {
70 dropdown.set_selected(idx as u32);
71 }
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 });
102
103 wifi_box.append(&wifi_label);
104 wifi_box.append(&dropdown);
105 header.pack_start(&wifi_box);
106
107 let refresh_btn = gtk::Button::from_icon_name("view-refresh-symbolic");
108 refresh_btn.add_css_class("refresh-btn");
109 refresh_btn.set_tooltip_text(Some("Refresh networks and devices"));
110 header.pack_end(&refresh_btn);
111 refresh_btn.connect_clicked(clone!(
112 #[weak]
113 list_container,
114 #[strong]
115 ctx,
116 #[strong]
117 is_scanning,
118 move |_| {
119 let ctx = ctx.clone();
120 let list_container = list_container.clone();
121 let is_scanning = is_scanning.clone();
122
123 glib::MainContext::default().spawn_local(async move {
124 refresh_networks(ctx, &list_container, &is_scanning).await;
125 });
126 }
127 ));
128
129 let theme_btn = gtk::Button::new();
130 theme_btn.add_css_class("theme-toggle-btn");
131 theme_btn.set_valign(gtk::Align::Center);
132 theme_btn.set_has_frame(false);
133
134 let is_light = window.has_css_class("light-theme");
135 let initial_icon = if is_light {
136 "weather-clear-night-symbolic"
137 } else {
138 "weather-clear-symbolic"
139 };
140 theme_btn.set_icon_name(initial_icon);
141
142 let window_weak = window.downgrade();
143 theme_btn.connect_clicked(move |btn| {
144 if let Some(window) = window_weak.upgrade() {
145 let is_light = window.has_css_class("light-theme");
146
147 if is_light {
148 window.remove_css_class("light-theme");
149 window.add_css_class("dark-theme");
150 btn.set_icon_name("weather-clear-symbolic");
151 crate::theme_config::save_theme("light");
152 } else {
153 window.remove_css_class("dark-theme");
154 window.add_css_class("light-theme");
155 btn.set_icon_name("weather-clear-night-symbolic");
156 crate::theme_config::save_theme("dark");
157 }
158 }
159 });
160
161 header.pack_end(&theme_btn);
162
163 let wifi_switch = Switch::new();
164 wifi_switch.set_valign(gtk::Align::Center);
165 header.pack_end(&wifi_switch);
166 wifi_switch.set_size_request(24, 24);
167
168 header.pack_end(&ctx.status);
169
170 {
171 let list_container = list_container.clone();
172 let wifi_switch = wifi_switch.clone();
173 let ctx = ctx.clone();
174 let is_scanning = is_scanning.clone();
175
176 glib::MainContext::default().spawn_local(async move {
177 ctx.stack.set_visible_child_name("loading");
178 clear_children(&list_container);
179
180 match ctx.nm.wifi_enabled().await {
181 Ok(enabled) => {
182 wifi_switch.set_active(enabled);
183 if enabled {
184 refresh_networks(ctx, &list_container, &is_scanning).await;
185 }
186 }
187 Err(err) => {
188 ctx.status
189 .set_text(&format!("Error fetching networks: {err}"));
190 }
191 }
192 })
193 };
194
195 {
196 let ctx = ctx.clone();
197
198 wifi_switch.connect_active_notify(move |sw| {
199 let ctx = ctx.clone();
200 let list_container = list_container.clone();
201 let sw = sw.clone();
202 let is_scanning = is_scanning.clone();
203
204 glib::MainContext::default().spawn_local(async move {
205 clear_children(&list_container);
206
207 if let Err(err) = ctx.nm.set_wifi_enabled(sw.is_active()).await {
208 ctx.status.set_text(&format!("Error setting Wi-Fi: {err}"));
209 return;
210 }
211
212 if sw.is_active() {
213 if ctx.nm.wait_for_wifi_ready().await.is_ok() {
214 refresh_networks(ctx, &list_container, &is_scanning).await;
215 } else {
216 ctx.status.set_text("Wi-Fi failed to initialize");
217 }
218 }
219 });
220 });
221 }
222
223 header
224}
225
226pub async fn refresh_networks(
227 ctx: Rc<NetworksContext>,
228 list_container: &GtkBox,
229 is_scanning: &Rc<Cell<bool>>,
230) {
231 if is_scanning.get() {
232 ctx.status.set_text("Scan already in progress");
233 return;
234 }
235 is_scanning.set(true);
236
237 clear_children(list_container);
238 ctx.status.set_text("Scanning...");
239
240 match ctx.nm.list_wired_devices().await {
242 Ok(wired_devices) => {
243 let available_devices: Vec<_> = wired_devices
246 .into_iter()
247 .filter(|dev| {
248 let show = matches!(
249 dev.state,
250 models::DeviceState::Activated
251 | models::DeviceState::Disconnected
252 | models::DeviceState::Prepare
253 | models::DeviceState::Config
254 );
255 show
263 })
264 .collect();
265
266 if !available_devices.is_empty() {
272 let wired_header = Label::new(Some("Wired"));
273 wired_header.add_css_class("section-header");
274 wired_header.add_css_class("wired-section-header");
275 wired_header.set_halign(Align::Start);
276 wired_header.set_margin_top(8);
277 wired_header.set_margin_bottom(4);
278 wired_header.set_margin_start(12);
279 list_container.append(&wired_header);
280
281 let wired_ctx = wired_devices::WiredDevicesContext {
283 nm: ctx.nm.clone(),
284 on_success: ctx.on_success.clone(),
285 status: ctx.status.clone(),
286 stack: ctx.stack.clone(),
287 parent_window: ctx.parent_window.clone(),
288 };
289 let wired_ctx = Rc::new(wired_ctx);
290
291 let wired_list = wired_devices::wired_devices_view(
292 wired_ctx,
293 &available_devices,
294 ctx.wired_details_page.clone(),
295 );
296 wired_list.add_css_class("wired-devices-list");
297 list_container.append(&wired_list);
298
299 let separator = gtk::Separator::new(Orientation::Horizontal);
300 separator.add_css_class("device-separator");
301 separator.set_margin_top(12);
302 separator.set_margin_bottom(12);
303 list_container.append(&separator);
304 }
305 }
306 Err(e) => {
307 eprintln!("Failed to list wired devices: {}", e);
308 }
309 }
310
311 let wireless_header = Label::new(Some("Wireless"));
312 wireless_header.add_css_class("section-header");
313 wireless_header.add_css_class("wireless-section-header");
314 wireless_header.set_halign(Align::Start);
315 wireless_header.set_margin_top(8);
316 wireless_header.set_margin_bottom(4);
317 wireless_header.set_margin_start(12);
318 list_container.append(&wireless_header);
319
320 if let Err(err) = ctx.nm.scan_networks().await {
321 ctx.status.set_text(&format!("Scan failed: {err}"));
322 is_scanning.set(false);
323 return;
324 }
325
326 let mut last_len = 0;
327 for _ in 0..5 {
328 let nets = ctx.nm.list_networks().await.unwrap_or_default();
329 if nets.len() == last_len && last_len > 0 {
330 break;
331 }
332 last_len = nets.len();
333 glib::timeout_future_seconds(1).await;
334 }
335
336 match ctx.nm.list_networks().await {
337 Ok(mut nets) => {
338 let current_conn = ctx.nm.current_connection_info().await;
339 let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
340 let ssid_str = ssid.clone();
341 let band: Option<String> = freq
342 .and_then(crate::ui::freq_to_band)
343 .map(|s| s.to_string());
344 (Some(ssid_str), band)
345 } else {
346 (None, None)
347 };
348
349 nets.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)));
350
351 let mut seen_combinations = HashSet::new();
352 nets.retain(|net| {
353 let band = net.frequency.and_then(crate::ui::freq_to_band);
354 let key = (net.ssid.clone(), band);
355 seen_combinations.insert(key)
356 });
357
358 ctx.status.set_text("");
359
360 let list: ListBox = networks::networks_view(
361 ctx.clone(),
362 &nets,
363 current_ssid.as_deref(),
364 current_band.as_deref(),
365 );
366 list_container.append(&list);
367 ctx.stack.set_visible_child_name("networks");
368 }
369 Err(err) => ctx
370 .status
371 .set_text(&format!("Error fetching networks: {err}")),
372 }
373
374 is_scanning.set(false);
375}
376
377pub fn clear_children(container: >k::Box) {
378 let mut child = container.first_child();
379 while let Some(widget) = child {
380 child = widget.next_sibling();
381 container.remove(&widget);
382 }
383}
384
385pub async fn refresh_networks_no_scan(
389 ctx: Rc<NetworksContext>,
390 list_container: &GtkBox,
391 is_scanning: &Rc<Cell<bool>>,
392) {
393 if is_scanning.get() {
394 return;
396 }
397
398 is_scanning.set(true);
400
401 clear_children(list_container);
402
403 if let Ok(wired_devices) = ctx.nm.list_wired_devices().await {
405 let available_devices: Vec<_> = wired_devices
409 .into_iter()
410 .filter(|dev| {
411 let show = matches!(
412 dev.state,
413 models::DeviceState::Activated
414 | models::DeviceState::Disconnected
415 | models::DeviceState::Prepare
416 | models::DeviceState::Config
417 | models::DeviceState::Unmanaged
418 );
419 show
427 })
428 .collect();
429
430 if !available_devices.is_empty() {
436 let wired_header = Label::new(Some("Wired"));
437 wired_header.add_css_class("section-header");
438 wired_header.add_css_class("wired-section-header");
439 wired_header.set_halign(Align::Start);
440 wired_header.set_margin_top(8);
441 wired_header.set_margin_bottom(4);
442 wired_header.set_margin_start(12);
443 list_container.append(&wired_header);
444
445 let wired_ctx = wired_devices::WiredDevicesContext {
446 nm: ctx.nm.clone(),
447 on_success: ctx.on_success.clone(),
448 status: ctx.status.clone(),
449 stack: ctx.stack.clone(),
450 parent_window: ctx.parent_window.clone(),
451 };
452 let wired_ctx = Rc::new(wired_ctx);
453
454 let wired_list = wired_devices::wired_devices_view(
455 wired_ctx,
456 &available_devices,
457 ctx.wired_details_page.clone(),
458 );
459 wired_list.add_css_class("wired-devices-list");
460 list_container.append(&wired_list);
461
462 let separator = gtk::Separator::new(Orientation::Horizontal);
463 separator.add_css_class("device-separator");
464 separator.set_margin_top(12);
465 separator.set_margin_bottom(12);
466 list_container.append(&separator);
467 }
468 }
469
470 let wireless_header = Label::new(Some("Wireless"));
471 wireless_header.add_css_class("section-header");
472 wireless_header.add_css_class("wireless-section-header");
473 wireless_header.set_halign(Align::Start);
474 wireless_header.set_margin_top(8);
475 wireless_header.set_margin_bottom(4);
476 wireless_header.set_margin_start(12);
477 list_container.append(&wireless_header);
478
479 match ctx.nm.list_networks().await {
480 Ok(mut nets) => {
481 let current_conn = ctx.nm.current_connection_info().await;
482 let (current_ssid, current_band) = if let Some((ssid, freq)) = current_conn {
483 let ssid_str = ssid.clone();
484 let band: Option<String> = freq
485 .and_then(crate::ui::freq_to_band)
486 .map(|s| s.to_string());
487 (Some(ssid_str), band)
488 } else {
489 (None, None)
490 };
491
492 nets.sort_by(|a, b| b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)));
493
494 let mut seen_combinations = HashSet::new();
495 nets.retain(|net| {
496 let band = net.frequency.and_then(crate::ui::freq_to_band);
497 let key = (net.ssid.clone(), band);
498 seen_combinations.insert(key)
499 });
500
501 let list: ListBox = networks::networks_view(
502 ctx.clone(),
503 &nets,
504 current_ssid.as_deref(),
505 current_band.as_deref(),
506 );
507 list_container.append(&list);
508 ctx.stack.set_visible_child_name("networks");
509 }
510 Err(err) => {
511 ctx.status
512 .set_text(&format!("Error fetching networks: {err}"));
513 }
514 }
515
516 is_scanning.set(false);
518}