Say it in icons, and put the whole backend behind four tabs
Six things Bryan asked for, which turned out to be one change: the app had no icon set, so every one of them was blocked on having somewhere for icons to come from. That somewhere is dev-updater's arrangement, ported: a Nerd Fonts subset committed as an asset, drawn as text. `Gear.kt`'s hand-drawn canvas gear argued against icon fonts because a system font may not have the glyph and whoever gets the empty box is never the person who wrote it. The objection is right about *relying* on a system font and the answer is to ship the glyph, so the file is gone and its reasoning is restated in `NerdIcons.kt` rather than deleted -- otherwise the next reader re-derives it. `md-cog` and `md-refresh` are dev-updater's own codepoints, because a cog means the same thing in both apps. The root screen's four words under the title are now four tabs, and the two that act on the whole screen -- settings and refresh -- moved up onto the title row as glyphs. That row's old comment recorded that a fifth word would have had nowhere to go; tabs also say something the words did not, which is that sessions, import, models and setups are four views of one backend rather than four errands. Refresh feeds whichever tab is showing. Import, models and setups lose their headings and their Back buttons, since the tab row is now both. Usage is a dialog. It is checked *against* what you were reading -- "can I start this" is asked with the transcript still on screen -- and it had no navigation of its own, so the only thing its Back could mean was "put this away". The button that opens it is a chart glyph coloured by the worst of the machine's windows, so the row says whether the limits are worth opening before anybody opens them. One `quotaColor` now colours every bar that measures a quota: blue, yellow at 75%, red at 95%. The session bar escalates where it used to sit blue at every level, and the dialog's thresholds moved out of it. A download keeps plain blue at every value -- it has no limit to approach, and colouring it like one would say the opposite of what is happening. States that are not measurements take the ordinary control colour, since blue is the low end of this scale and would read as "checked, and fine" about a machine nobody could reach. Send and stop are the filled paper plane and the filled square. Send keeps the word "Queue" while a turn is in flight, because that is what pressing it then does, and an icon that does two things while looking identical would promise something immediate and do something that waits. Looked at on the emulator: all six glyphs render, the tabs and the system back gesture between them, the dialog over a live session, and the bar bands at 82% and 97% forced through a scratch build, since this account is at 72/31/5 and would only ever have shown blue.
This commit is contained in:
1 parent
ba71c798f5
commit
ff39ef5cf9
17 files changed
+627
-310
No files matched your search
@@ -0,0 +1,110 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* The app's root: one title, and four views of the backend behind it.
|
||||
*
|
||||
* These were four screens reached by four words in a row under the title, and the row was already
|
||||
* full -- the comment it replaced recorded that a fifth would have to go somewhere else. Tabs say
|
||||
* the same thing in less space and say one more thing besides: that these are places to be rather
|
||||
* than errands to run. Sessions, the machine's importable history, the models on it and the
|
||||
* machines themselves are all *the same backend*, looked at four ways, and none of them is a step
|
||||
* down from another. Settings still is a step down, which is why it stays a pushed screen and keeps
|
||||
* its own Back.
|
||||
*/
|
||||
private enum class MainTab(val label: String) {
|
||||
Sessions("Sessions"),
|
||||
Import("Import"),
|
||||
Models("Models"),
|
||||
Setups("Setups"),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
settings: ServerSettings,
|
||||
reloadToken: Int,
|
||||
onOpen: (SessionSummary) -> Unit,
|
||||
onSpawn: () -> Unit,
|
||||
onImported: (SessionSummary) -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
) {
|
||||
var tab by remember { mutableStateOf(MainTab.Sessions) }
|
||||
var refreshToken by remember { mutableIntStateOf(0) }
|
||||
|
||||
// A tab the app put over the list has to step back to it rather than fall through to the
|
||||
// system default, which closes the app -- that reads as a crash to somebody who only meant to
|
||||
// get back to their sessions. Nested inside AppRoot's handler, so it wins while it is enabled.
|
||||
BackHandler(enabled = tab != MainTab.Sessions) { tab = MainTab.Sessions }
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
"AI Sessions",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
// Glyphs rather than the words they replaced: neither ever changes, both are read
|
||||
// faster than they are spelled, and together they take the width that let the title
|
||||
// keep its own line. They sit on the title's row because they act on the whole
|
||||
// screen -- everything below this row is one tab's business, and a control belongs
|
||||
// with the thing it acts on.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(GLYPH_BUTTON_GAP)) {
|
||||
GlyphButton(REFRESH_GLYPH, "Refresh", { refreshToken++ })
|
||||
GlyphButton(SETTINGS_GLYPH, "Settings", onSettings)
|
||||
}
|
||||
}
|
||||
// Primary rather than the plain TabRow, which is deprecated in favour of the two that
|
||||
// say where they sit: these are the app's top-level destinations.
|
||||
PrimaryTabRow(selectedTabIndex = tab.ordinal) {
|
||||
MainTab.entries.forEach { entry ->
|
||||
Tab(
|
||||
selected = tab == entry,
|
||||
onClick = { tab = entry },
|
||||
text = { Text(entry.label) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshing means "ask again about what I am looking at", so the button feeds the tab
|
||||
// that is showing. The token from above means something else already changed what these
|
||||
// show; the two are the same instruction to the tab below, so they are summed rather than
|
||||
// tracked apart -- either one moving moves the sum, which is all a tab watches.
|
||||
val token = reloadToken + refreshToken
|
||||
when (tab) {
|
||||
MainTab.Sessions ->
|
||||
SessionListScreen(
|
||||
settings = settings,
|
||||
reloadToken = token,
|
||||
onOpen = onOpen,
|
||||
onSpawn = onSpawn,
|
||||
)
|
||||
MainTab.Import ->
|
||||
ImportScreen(settings = settings, reloadToken = token, onImported = onImported)
|
||||
MainTab.Models -> ModelsScreen(settings = settings, reloadToken = token)
|
||||
MainTab.Setups -> SetupsScreen(settings = settings, reloadToken = token)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user