Krolyxon
Commit c98214a

Branches

Refs →

budctlandroidappsrcmainkotliniogithubkrolyxonbudctlOverview.kt

144 lines · 6.1 KB · Kotlin

1package io.github.krolyxon.budctl
2
3import androidx.compose.foundation.layout.Arrangement
4import androidx.compose.foundation.layout.Column
5import androidx.compose.foundation.layout.Row
6import androidx.compose.foundation.layout.fillMaxWidth
7import androidx.compose.foundation.layout.padding
8import androidx.compose.foundation.layout.width
9import androidx.compose.runtime.Composable
10import androidx.compose.ui.Alignment
11import androidx.compose.ui.Modifier
12import androidx.compose.ui.unit.dp
13
14/** The Overview page (crates/budctl-gui/src/overview.rs): battery, noise control, the switches. */
15@Composable
16fun Overview(app: App, askPermission: () -> Unit, open: (Page) -> Unit) {
17 if (!app.permitted) {
18 Note("budctl needs the Nearby devices permission to reach the buds.")
19 Row(Modifier.padding(horizontal = 18.dp)) { Pill("Allow", filled = true, onClick = askPermission) }
20 return
21 }
22 val buds = app.buds
23 Battery(buds)
24 buds.noise?.let { NoiseChoice(app, it) }
25 if (buds.switches.isNotEmpty()) Switches(app)
26 More(app, open)
27}
28
29/** The rows that open the other pages. */
30@Composable
31private fun More(app: App, open: (Page) -> Unit) {
32 Card {
33 SettingRow(Icon.Equalizer, "Equalizer", app.buds.eqName(), onClick = { open(Page.Equalizer) }) { Chevron() }
34 SettingRow(Icon.Gestures, "Gestures", "Taps and holds on each bud", onClick = { open(Page.Gestures) }) { Chevron() }
35 SettingRow(Icon.Earbud, "Earbud settings", "Dual connection, find my earbuds, firmware", onClick = { open(Page.Earbuds) }) {
36 Chevron()
37 }
38 }
39}
40
41@Composable
42private fun Battery(buds: Buds) {
43 val p = LocalLook.current.p
44 Card {
45 Row(Modifier.padding(horizontal = 4.dp, vertical = 20.dp)) {
46 for ((side, charge) in listOf("left" to buds.left, "case" to buds.case, "right" to buds.right)) {
47 Column(
48 Modifier.weight(1f),
49 horizontalAlignment = Alignment.CenterHorizontally,
50 verticalArrangement = Arrangement.spacedBy(12.dp),
51 ) {
52 Ring(charge?.level, Modifier.padding(horizontal = 8.dp).fillMaxWidth())
53 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
54 // Charging: the bolt, and the side in the live color.
55 if (charge?.charging == true) Glyph(Icon.Bolt, 13.dp, p.live)
56 Label(side, color = if (charge?.charging == true) p.live else p.grey)
57 }
58 }
59 }
60 }
61 }
62}
63
64@Composable
65private fun NoiseChoice(app: App, now: NoiseControl) {
66 val p = LocalLook.current.p
67 val modes = listOf(
68 Triple(Icon.NoiseOff, "Off", "off"),
69 Triple(Icon.Anc, "Noise cancelling", "anc"),
70 Triple(Icon.Transparency, "Transparency", "transparency"),
71 )
72 Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
73 Segmented(modes.map { (icon, label, mode) -> Segment(label, now.mode == mode, icon) { app.noise(mode) } }, large = true)
74 if (app.saving(Item.Noise)) {
75 Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Label(SAVING, color = p.live) }
76 }
77 // The strength, when noise cancelling is on and the buds report its level.
78 if (now.mode == "anc" && now.level != null) {
79 val levels = listOf("light" to "Light", "medium" to "Medium", "deep" to "Deep")
80 Row(Modifier.padding(start = 18.dp), verticalAlignment = Alignment.CenterVertically) {
81 Label("strength", modifier = Modifier.weight(1f))
82 Segmented(
83 levels.map { (level, label) -> Segment(label, now.level == level) { app.noise("anc", level) } },
84 large = false,
85 Modifier.width(240.dp),
86 )
87 }
88 }
89 }
90}
91
92/** How the page shows the switches it knows, in this order; others come last, plain. */
93private val SWITCHES = listOf(
94 Triple("game", Icon.Game, "Lower latency, for games and video"),
95 Triple("spatial", Icon.Spatial, "Wider, surround-like sound"),
96 Triple("bass-boost", Icon.Bass, "Stronger low end"),
97 Triple("wind", Icon.Wind, "Less wind noise outdoors"),
98 Triple("dual", Icon.Dual, "Stay connected to two devices at once"),
99)
100
101@Composable
102private fun Switches(app: App) {
103 val buds = app.buds
104 val rank = { s: Switch -> SWITCHES.indexOfFirst { it.first == s.key }.let { if (it < 0) SWITCHES.size else it } }
105 Card {
106 for (s in buds.switches.sortedBy(rank)) {
107 val known = SWITCHES.find { it.first == s.key }
108 SettingRow(
109 known?.second ?: Icon.Settings,
110 s.label,
111 if (app.saving(Item.Switch(s.key))) SAVING else known?.third,
112 onClick = { app.switch(s.key, !s.on) },
113 ) { Toggle(s.on) }
114 if (s.key == "bass-boost") buds.bassRange?.let { range -> for ((band, title) in BASS_BANDS) BassLevel(app, range, band, title) }
115 }
116 }
117}
118
119/** The bands of bass boost, as the buds and the rows name them. */
120private val BASS_BANDS = listOf("low" to "Low frequencies", "medium" to "Medium frequencies", "high" to "High frequencies")
121
122/** One band of bass boost: the buds take its level but do not report it, so the row shows the last level set here. */
123@Composable
124private fun BassLevel(app: App, range: IntRange, band: String, title: String) {
125 val level = app.bassDraft[band] ?: app.buds.bass[band]
126 SettingRow(
127 Icon.Bass,
128 title,
129 if (app.saving(Item.Bass(band))) SAVING else "Bass boost, last set here",
130 below = {
131 DotSlider(
132 range.first,
133 range.last,
134 level ?: 0,
135 change = { app.moveBass(band, it) },
136 release = { app.saveBass(band) },
137 modifier = Modifier.padding(start = 52.dp, top = 6.dp),
138 )
139 },
140 ) { ValueChip(level?.let(::gain) ?: "not set") }
141}
142
143/** A gain or level with its sign: "+3", "0", "-1". */
144fun gain(value: Int) = if (value == 0) "0" else "%+d".format(value)