Krolyxon
Commit 93bfdf6

Branches

Refs →

budctlcratesbudctl-guisrcearbuds.rs

169 lines · 5.5 KB · Rust

1//! The Earbud settings page: dual connection and its devices, find the buds, and what they run.
2
3use budctl_core::status::DualDevice;
4use iced::Element;
5use iced::widget::{column, container};
6
7use crate::buds::Buds;
8use crate::icons::Icon;
9use crate::theme::Look;
10use crate::ui;
11use crate::{Item, Marks, Message};
12
13/// The switch for dual connection, by its key.
14pub const DUAL: &str = "dual";
15
16/// What the keyboard cursor can rest on, top to bottom.
17pub fn items(buds: &Buds) -> Vec<Item> {
18 let dual = buds.status.switches.iter().any(|s| s.key == DUAL);
19 dual.then_some(Item::Switch(DUAL))
20 .into_iter()
21 .chain([Item::Find])
22 .collect()
23}
24
25/// `ringing`: the buds were told to ring and not yet to stop.
26pub fn view<'a>(look: Look, buds: &'a Buds, ringing: bool, marks: Marks) -> Element<'a, Message> {
27 let dual = buds.status.switches.iter().find(|s| s.key == DUAL).map(|dual| {
28 let about = if marks.pending.contains(&Item::Switch(DUAL)) {
29 "Saving…"
30 } else if dual.on {
31 "Your buds stay connected to two devices at once, like your phone and this computer"
32 } else {
33 "Your buds connect to one device at a time"
34 };
35 let row = ui::setting(
36 look,
37 Icon::Dual,
38 "Dual connection",
39 Some(about.to_owned()),
40 ui::switch(look, dual.on),
41 Some(Message::Switch(DUAL, !dual.on)),
42 marks.focus == Some(Item::Switch(DUAL)),
43 );
44 column![ui::heading(look, "Connection"), ui::card(look, vec![row])].spacing(10)
45 });
46 let about = match (marks.pending.contains(&Item::Find), ringing) {
47 (true, false) => "Starting…",
48 (true, true) => "Stopping…",
49 (false, true) => "Ringing: stop it once you have found them",
50 (false, false) => "Plays a loud sound from both buds",
51 };
52 let find = ui::setting(
53 look,
54 Icon::Find,
55 "Find my earbuds",
56 Some(about.to_owned()),
57 ui::pill(look, if ringing { "Stop" } else { "Ring" }, ringing, Message::Find),
58 None,
59 marks.focus == Some(Item::Find),
60 );
61 let value = |value: Option<&'a str>| -> Element<'a, Message> { ui::value(look, value.unwrap_or("unknown")) };
62 let status = &buds.status;
63 let facts = vec![
64 ui::setting(look, Icon::Earbud, "Name", None, value(Some(&buds.name)), None, false),
65 ui::setting(
66 look,
67 Icon::Chip,
68 "Firmware",
69 None,
70 value(status.firmware.as_deref()),
71 None,
72 false,
73 ),
74 ui::setting(
75 look,
76 Icon::Info,
77 "Audio codec",
78 None,
79 value(status.codec.as_deref()),
80 None,
81 false,
82 ),
83 ];
84 column![ui::title(look, "earbud settings · connection, find, firmware")]
85 .push(dual)
86 .push(buds.dual.as_deref().map(|list| devices(look, list)))
87 .push(column![ui::heading(look, "Find my earbuds"), ui::card(look, vec![find])].spacing(10))
88 .push(column![ui::heading(look, "About these buds"), ui::card(look, facts)].spacing(10))
89 .spacing(18)
90 .into()
91}
92
93/// The devices dual connection keeps, as the buds list them. Nothing here can be changed: the
94/// buds take no command to connect, disconnect or forget one.
95fn devices<'a>(look: Look, list: &'a [DualDevice]) -> Element<'a, Message> {
96 let p = look.p;
97 let rows: Vec<Element<'a, Message>> = list
98 .iter()
99 .map(|device| {
100 let icon = if device.this_computer {
101 Icon::Laptop
102 } else {
103 Icon::Phone
104 };
105 let name = if device.name.is_empty() {
106 "Unnamed device"
107 } else {
108 device.name.as_str()
109 };
110 let address = ui::label(look, &device.address.to_string(), 11.0, p.grey);
111 ui::list_row(
112 look,
113 icon,
114 device.connected,
115 name,
116 Some(state(device)),
117 address,
118 None,
119 false,
120 )
121 })
122 .collect();
123 let list: Element<'a, Message> = if rows.is_empty() {
124 container(ui::note(look, "The buds list no devices."))
125 .padding([0, 18])
126 .into()
127 } else {
128 ui::card(look, rows)
129 };
130 let note = container(ui::note(
131 look,
132 "These buds cannot connect, disconnect or forget a device from here.",
133 ))
134 .padding([0, 18]);
135 column![ui::heading(look, "Devices"), list, note].spacing(10).into()
136}
137
138/// "Connected · this computer", "Not connected"…
139fn state(device: &DualDevice) -> String {
140 let mut words = vec![if device.connected { "Connected" } else { "Not connected" }];
141 for (on, what) in [
142 (device.this_computer, "this computer"),
143 (device.main_audio, "main audio"),
144 (device.playing, "playing"),
145 ] {
146 if on {
147 words.push(what);
148 }
149 }
150 words.join(" · ")
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::buds;
157
158 #[test]
159 fn device_states_in_words() {
160 let dual = buds::demo().dual.unwrap();
161 let words: Vec<String> = dual.iter().map(state).collect();
162 assert_eq!(words, ["Connected · this computer", "Connected · main audio · playing"]);
163 let mut gone = dual[1].clone();
164 gone.connected = false;
165 gone.main_audio = false;
166 gone.playing = false;
167 assert_eq!(state(&gone), "Not connected");
168 }
169}