Krolyxon
Commit b835c9b

Branches

Refs →

budctlcratesbudctl-guisrcearbuds.rs

159 lines · 5.3 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, text};
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 p = look.p;
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 ui::pill(look, if ringing { "Stop" } else { "Ring" }, ringing, Message::Find),
59 None,
60 marks.focus == Some(Item::Find),
61 );
62 let value = |value: Option<&'a str>| -> Element<'a, Message> {
63 text(value.unwrap_or("Unknown")).size(14).color(p.dim).into()
64 };
65 let status = &buds.status;
66 let facts = vec![
67 ui::setting(look, Icon::Earbud, "Name", None, value(Some(&buds.name)), None, false),
68 ui::setting(
69 look,
70 Icon::Chip,
71 "Firmware",
72 None,
73 value(status.firmware.as_deref()),
74 None,
75 false,
76 ),
77 ui::setting(
78 look,
79 Icon::Info,
80 "Audio codec",
81 None,
82 value(status.codec.as_deref()),
83 None,
84 false,
85 ),
86 ];
87 column![ui::title(look, "Earbud settings")]
88 .push(dual)
89 .push(buds.dual.as_deref().map(|list| devices(look, list)))
90 .push(column![ui::heading(look, "Find my earbuds"), ui::card(look, vec![find])].spacing(10))
91 .push(column![ui::heading(look, "About these buds"), ui::card(look, facts)].spacing(10))
92 .spacing(18)
93 .into()
94}
95
96/// The devices dual connection keeps, as the buds list them. Nothing here can be changed: the
97/// buds take no command to connect, disconnect or forget one.
98fn devices<'a>(look: Look, list: &'a [DualDevice]) -> Element<'a, Message> {
99 let p = look.p;
100 let rows: Vec<Element<'a, Message>> = list
101 .iter()
102 .map(|device| {
103 let icon = if device.this_computer {
104 Icon::Laptop
105 } else {
106 Icon::Phone
107 };
108 let name = if device.name.is_empty() {
109 "Unnamed device"
110 } else {
111 device.name.as_str()
112 };
113 let address = text(device.address.to_string()).size(13).color(p.dim);
114 ui::setting(look, icon, name, Some(state(device)), address.into(), None, false)
115 })
116 .collect();
117 let list: Element<'a, Message> = if rows.is_empty() {
118 text("The buds list no devices.").size(13).color(p.dim).into()
119 } else {
120 ui::card(look, rows)
121 };
122 let note = text("These buds cannot connect, disconnect or forget a device from here.")
123 .size(13)
124 .color(p.dim);
125 column![ui::heading(look, "Devices"), list, note].spacing(10).into()
126}
127
128/// "Connected · this computer", "Not connected"…
129fn state(device: &DualDevice) -> String {
130 let mut words = vec![if device.connected { "Connected" } else { "Not connected" }];
131 for (on, what) in [
132 (device.this_computer, "this computer"),
133 (device.main_audio, "main audio"),
134 (device.playing, "playing"),
135 ] {
136 if on {
137 words.push(what);
138 }
139 }
140 words.join(" · ")
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::buds;
147
148 #[test]
149 fn device_states_in_words() {
150 let dual = buds::demo().dual.unwrap();
151 let words: Vec<String> = dual.iter().map(state).collect();
152 assert_eq!(words, ["Connected · this computer", "Connected · main audio · playing"]);
153 let mut gone = dual[1].clone();
154 gone.connected = false;
155 gone.main_audio = false;
156 gone.playing = false;
157 assert_eq!(state(&gone), "Not connected");
158 }
159}