Krolyxon
Commit 8ec1f82

Branches

Refs →

budctlcratesbudctl-guisrcearbuds.rs

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