| 1 | package io.github.krolyxon.budctl |
| 2 | |
| 3 | import androidx.compose.foundation.layout.Arrangement |
| 4 | import androidx.compose.foundation.layout.Column |
| 5 | import androidx.compose.foundation.layout.Row |
| 6 | import androidx.compose.foundation.layout.fillMaxWidth |
| 7 | import androidx.compose.foundation.layout.padding |
| 8 | import androidx.compose.foundation.layout.width |
| 9 | import androidx.compose.runtime.Composable |
| 10 | import androidx.compose.ui.Alignment |
| 11 | import androidx.compose.ui.Modifier |
| 12 | import androidx.compose.ui.unit.dp |
| 13 | |
| 14 | /** The Overview page (crates/budctl-gui/src/overview.rs): battery, noise control, the switches. */ |
| 15 | @Composable |
| 16 | fun 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 |
| 31 | private 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 |
| 42 | private 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 |
| 65 | private 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. */ |
| 93 | private 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 |
| 102 | private 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. */ |
| 120 | private 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 |
| 124 | private 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". */ |
| 144 | fun gain(value: Int) = if (value == 0) "0" else "%+d".format(value) |