1use anyhow::Result;
2use gtk::Align;
3use gtk::GestureClick;
4use gtk::prelude::*;
5use gtk::{Box, Image, Label, ListBox, ListBoxRow, Orientation};
6use nmrs::models::WifiSecurity;
7use nmrs::{NetworkManager, models};
8use std::rc::Rc;
9
10use crate::ui::connect;
11use crate::ui::network_page::NetworkPage;
12
13pub struct NetworkRowController {
14 pub row: gtk::ListBoxRow,
15 pub arrow: gtk::Image,
16 pub ctx: Rc<NetworksContext>,
17 pub net: models::Network,
18 pub details_page: Rc<NetworkPage>,
19}
20
21pub struct NetworksContext {
22 pub nm: Rc<NetworkManager>,
23 pub on_success: Rc<dyn Fn()>,
24 pub status: Label,
25 pub stack: gtk::Stack,
26 pub parent_window: gtk::ApplicationWindow,
27 pub details_page: Rc<NetworkPage>,
28 pub wired_details_page: Rc<crate::ui::wired_page::WiredPage>,
29}
30
31impl NetworksContext {
32 pub async fn new(
33 on_success: Rc<dyn Fn()>,
34 status: &Label,
35 stack: >k::Stack,
36 parent_window: >k::ApplicationWindow,
37 details_page: Rc<NetworkPage>,
38 wired_details_page: Rc<crate::ui::wired_page::WiredPage>,
39 ) -> Result<Self> {
40 let nm = Rc::new(NetworkManager::new().await?);
41
42 Ok(Self {
43 nm,
44 on_success,
45 status: status.clone(),
46 stack: stack.clone(),
47 parent_window: parent_window.clone(),
48 details_page,
49 wired_details_page,
50 })
51 }
52}
53
54impl NetworkRowController {
55 pub fn new(
56 row: gtk::ListBoxRow,
57 arrow: gtk::Image,
58 ctx: Rc<NetworksContext>,
59 net: models::Network,
60 details_page: Rc<NetworkPage>,
61 ) -> Self {
62 Self {
63 row,
64 arrow,
65 ctx,
66 net,
67 details_page,
68 }
69 }
70
71 pub fn attach(&self) {
72 self.attach_arrow();
73 self.attach_row_double();
74 }
75
76 fn attach_arrow(&self) {
77 let click = GestureClick::new();
78
79 let ctx = self.ctx.clone();
80 let net = self.net.clone();
81 let stack = self.ctx.stack.clone();
82 let page = self.details_page.clone();
83
84 click.connect_pressed(move |_, _, _, _| {
85 let ctx_c = ctx.clone();
86 let net_c = net.clone();
87 let stack_c = stack.clone();
88 let page_c = page.clone();
89
90 glib::MainContext::default().spawn_local(async move {
91 if let Ok(info) = ctx_c.nm.show_details(&net_c).await {
92 page_c.update(&info);
93 stack_c.set_visible_child_name("details");
94 }
95 });
96 });
97
98 self.arrow.add_controller(click);
99 }
100
101 fn attach_row_double(&self) {
102 let click = GestureClick::new();
103
104 let ctx = self.ctx.clone();
105 let net = self.net.clone();
106 let ssid = net.ssid.clone();
107 let secured = net.secured;
108 let is_eap = net.is_eap;
109
110 let status = ctx.status.clone();
111 let window = ctx.parent_window.clone();
112 let on_success = ctx.on_success.clone();
113
114 click.connect_pressed(move |_, n, _, _| {
115 if n != 2 {
116 return;
117 }
118
119 status.set_text(&format!("Connecting to {ssid}..."));
120
121 let ssid_c = ssid.clone();
122 let nm_c = ctx.nm.clone();
123 let status_c = status.clone();
124 let window_c = window.clone();
125 let on_success_c = on_success.clone();
126
127 glib::MainContext::default().spawn_local(async move {
128 if secured {
129 let have = nm_c.has_saved_connection(&ssid_c).await.unwrap_or(false);
130
131 if have {
132 status_c.set_text(&format!("Connecting to {}...", ssid_c));
133 window_c.set_sensitive(false);
134 let creds = WifiSecurity::WpaPsk { psk: "".into() };
135 match nm_c.connect(&ssid_c, creds).await {
136 Ok(_) => {
137 status_c.set_text("");
138 on_success_c();
139 }
140 Err(e) => status_c.set_text(&format!("Failed to connect: {e}")),
141 }
142 window_c.set_sensitive(true);
143 } else {
144 connect::connect_modal(
145 nm_c.clone(),
146 &window_c,
147 &ssid_c,
148 is_eap,
149 on_success_c.clone(),
150 );
151 }
152 } else {
153 status_c.set_text(&format!("Connecting to {}...", ssid_c));
154 window_c.set_sensitive(false);
155 let creds = WifiSecurity::Open;
156 match nm_c.connect(&ssid_c, creds).await {
157 Ok(_) => {
158 status_c.set_text("");
159 on_success_c();
160 }
161 Err(e) => status_c.set_text(&format!("Failed to connect: {e}")),
162 }
163 window_c.set_sensitive(true);
164 }
165
166 status_c.set_text("");
167 });
168 });
169
170 self.row.add_controller(click);
171 }
172}
173
174pub fn networks_view(
175 ctx: Rc<NetworksContext>,
176 networks: &[models::Network],
177 current_ssid: Option<&str>,
178 current_band: Option<&str>,
179) -> ListBox {
180 let conn_threshold = 75;
181 let list = ListBox::new();
182
183 let mut sorted_networks: Vec<_> = networks
184 .iter()
185 .filter(|net| !net.ssid.trim().is_empty())
186 .cloned()
187 .collect();
188
189 sorted_networks.sort_by(|a, b| {
190 let a_connected = is_current_network(a, current_ssid, current_band);
191 let b_connected = is_current_network(b, current_ssid, current_band);
192
193 match (a_connected, b_connected) {
194 (true, false) => std::cmp::Ordering::Less,
195 (false, true) => std::cmp::Ordering::Greater,
196 _ => b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)),
197 }
198 });
199
200 for net in sorted_networks {
201 let row = ListBoxRow::new();
202 let hbox = Box::new(Orientation::Horizontal, 6);
203
204 row.add_css_class("network-selection");
205
206 if is_current_network(&net, current_ssid, current_band) {
207 row.add_css_class("connected");
208 }
209
210 let display_name = match net.frequency.and_then(crate::ui::freq_to_band) {
211 Some(band) => format!("{} ({band})", net.ssid),
212 None => net.ssid.clone(),
213 };
214
215 hbox.append(&Label::new(Some(&display_name)));
216
217 if is_current_network(&net, current_ssid, current_band) {
218 let connected_label = Label::new(Some("Connected"));
219 connected_label.add_css_class("connected-label");
220 hbox.append(&connected_label);
221 }
222
223 let spacer = Box::new(Orientation::Horizontal, 0);
224 spacer.set_hexpand(true);
225 hbox.append(&spacer);
226
227 if let Some(s) = net.strength {
228 let icon_name = if net.secured {
229 "network-wireless-encrypted-symbolic"
230 } else {
231 "network-wireless-signal-excellent-symbolic"
232 };
233
234 let image = Image::from_icon_name(icon_name);
235 if net.secured {
236 image.add_css_class("wifi-secure");
237 } else {
238 image.add_css_class("wifi-open");
239 }
240
241 let strength_label = Label::new(Some(&format!("{s}%")));
242 hbox.append(&image);
243 hbox.append(&strength_label);
244
245 if s >= conn_threshold {
246 strength_label.add_css_class("network-good");
247 } else if s > 65 {
248 strength_label.add_css_class("network-okay");
249 } else {
250 strength_label.add_css_class("network-poor");
251 }
252 }
253
254 let arrow = Image::from_icon_name("go-next-symbolic");
255 arrow.set_halign(Align::End);
256 arrow.add_css_class("network-arrow");
257 arrow.set_cursor_from_name(Some("pointer"));
258 hbox.append(&arrow);
259
260 row.set_child(Some(&hbox));
261
262 let controller = NetworkRowController::new(
263 row.clone(),
264 arrow.clone(),
265 ctx.clone(),
266 net.clone(),
267 ctx.details_page.clone(),
268 );
269
270 controller.attach();
271
272 list.append(&row);
273 }
274 list
275}
276
277fn is_current_network(
278 net: &models::Network,
279 current_ssid: Option<&str>,
280 current_band: Option<&str>,
281) -> bool {
282 let ssid = match current_ssid {
283 Some(s) => s,
284 None => return false,
285 };
286
287 if net.ssid != ssid {
288 return false;
289 }
290
291 if let Some(band) = current_band {
292 let net_band = net.frequency.and_then(crate::ui::freq_to_band);
293
294 return net_band == Some(band);
295 }
296
297 true
298}