Keep Android dialogue text readable and move word picks out of the sentence.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-18 13:22:43 +02:00
commit 9b9693fe4f
5 changed files with 160 additions and 89 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId = "org.maturita.maturita" applicationId = "org.maturita.maturita"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 3 versionCode = 4
versionName = "android-3" versionName = "android-4"
buildConfigField("String", "COMMIT", "\"$commit\"") buildConfigField("String", "COMMIT", "\"$commit\"")
buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"") buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"")
buildConfigField("String", "UPDATE_BRANCH", "\"builds\"") buildConfigField("String", "UPDATE_BRANCH", "\"builds\"")

View file

@ -53,6 +53,8 @@ import org.maturita.maturita.ui.SearchOverlay
import org.maturita.maturita.ui.SettingsIcon import org.maturita.maturita.ui.SettingsIcon
import org.maturita.maturita.ui.SettingsSheet import org.maturita.maturita.ui.SettingsSheet
import org.maturita.maturita.ui.SlidesScreen import org.maturita.maturita.ui.SlidesScreen
import org.maturita.maturita.ui.appTextStyle
import org.maturita.maturita.ui.appTypography
import org.maturita.maturita.ui.StatsIcon import org.maturita.maturita.ui.StatsIcon
import org.maturita.maturita.ui.StatsScreen import org.maturita.maturita.ui.StatsScreen
import org.maturita.maturita.ui.SubjectsScreen import org.maturita.maturita.ui.SubjectsScreen
@ -153,7 +155,7 @@ fun MaturitaApp(vm: AppViewModel) {
error = p.error, error = p.error,
) )
} }
MaterialTheme(colorScheme = scheme) { MaterialTheme(colorScheme = scheme, typography = appTypography()) {
MaturitaAppBody(vm) MaturitaAppBody(vm)
} }
} }
@ -183,7 +185,7 @@ private fun MaturitaAppBody(vm: AppViewModel) {
.padding(horizontal = 12.dp, vertical = 6.dp), .padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text("maturita.c", color = p.text, fontWeight = FontWeight.Bold, fontSize = 15.sp, modifier = Modifier.weight(1f)) Text("maturita.c", style = appTextStyle(15.sp, FontWeight.Medium, color = p.text), modifier = Modifier.weight(1f))
IconHit({ vm.searchOpen = true }) { SearchIcon(p.text) } IconHit({ vm.searchOpen = true }) { SearchIcon(p.text) }
IconHit({ vm.go(Route.Stats) }) { StatsIcon(p.text) } IconHit({ vm.go(Route.Stats) }) { StatsIcon(p.text) }
IconHit({ vm.settingsOpen = true }) { SettingsIcon(p.text) } IconHit({ vm.settingsOpen = true }) { SettingsIcon(p.text) }

View file

@ -19,16 +19,54 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.material3.Typography
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.maturita.maturita.AppViewModel import org.maturita.maturita.AppViewModel
import org.maturita.maturita.data.Palette import org.maturita.maturita.data.Palette
fun appTextStyle(
fontSize: TextUnit,
fontWeight: FontWeight = FontWeight.Normal,
textAlign: TextAlign = TextAlign.Unspecified,
color: Color = Color.Unspecified,
): TextStyle {
val weight = if (fontWeight >= FontWeight.Bold) FontWeight.Medium else fontWeight
return TextStyle(
color = color,
fontSize = fontSize,
fontWeight = weight,
fontFamily = FontFamily.SansSerif,
fontSynthesis = FontSynthesis.None,
lineHeight = fontSize * 1.4f,
letterSpacing = 0.sp,
textAlign = textAlign,
)
}
fun appTypography(): Typography {
val base = appTextStyle(16.sp)
return Typography(
bodyLarge = base,
bodyMedium = appTextStyle(14.sp),
bodySmall = appTextStyle(12.sp),
titleLarge = appTextStyle(22.sp, FontWeight.Medium),
titleMedium = appTextStyle(18.sp, FontWeight.Medium),
titleSmall = appTextStyle(16.sp, FontWeight.Medium),
labelLarge = appTextStyle(14.sp, FontWeight.Medium),
labelMedium = appTextStyle(12.sp, FontWeight.Medium),
headlineMedium = appTextStyle(24.sp, FontWeight.Medium),
)
}
private val Pill = RoundedCornerShape(999.dp) private val Pill = RoundedCornerShape(999.dp)
private val Card = RoundedCornerShape(20.dp) private val Card = RoundedCornerShape(20.dp)
@ -42,7 +80,7 @@ fun PrimaryButton(label: String, palette: Palette, modifier: Modifier = Modifier
.padding(horizontal = 22.dp, vertical = 12.dp), .padding(horizontal = 22.dp, vertical = 12.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text(label, color = palette.onAccent, fontWeight = FontWeight.SemiBold, fontSize = 15.sp) Text(label, style = appTextStyle(15.sp, FontWeight.Medium, color = palette.onAccent))
} }
} }
@ -55,7 +93,7 @@ fun PillButton(label: String, palette: Palette, modifier: Modifier = Modifier, o
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
) { ) {
Text(label, color = palette.text, fontWeight = FontWeight.SemiBold, fontSize = 13.sp) Text(label, style = appTextStyle(13.sp, FontWeight.Medium, color = palette.text))
} }
} }
@ -73,15 +111,23 @@ fun IconHit(onClick: () -> Unit, content: @Composable () -> Unit) {
@Composable @Composable
fun PageTop(vm: AppViewModel, back: () -> Unit, title: String, subtitle: String? = null) { fun PageTop(vm: AppViewModel, back: () -> Unit, title: String, subtitle: String? = null) {
val p = vm.palette val p = vm.palette
Box(Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 14.dp)) { Column(Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 14.dp)) {
Box(Modifier.align(Alignment.CenterStart)) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconHit(back) { BackIcon(p.text) } Box(Modifier.align(Alignment.CenterStart)) {
} IconHit(back) { BackIcon(p.text) }
Column(Modifier.align(Alignment.Center).padding(horizontal = 48.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text(title, color = p.text, fontSize = 24.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
if (!subtitle.isNullOrBlank()) {
Text(subtitle, color = p.subtext, fontSize = 14.sp, textAlign = TextAlign.Center, modifier = Modifier.padding(top = 4.dp))
} }
Text(
title,
modifier = Modifier.padding(horizontal = 48.dp),
style = appTextStyle(22.sp, FontWeight.Medium, TextAlign.Center, p.text),
)
}
if (!subtitle.isNullOrBlank()) {
Text(
subtitle,
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
style = appTextStyle(14.sp, textAlign = TextAlign.Center, color = p.subtext),
)
} }
} }
} }

View file

@ -37,6 +37,8 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.maturita.maturita.AppViewModel import org.maturita.maturita.AppViewModel
import org.maturita.maturita.data.J import org.maturita.maturita.data.J
import org.maturita.maturita.data.Palette import org.maturita.maturita.data.Palette
@ -519,8 +521,9 @@ private fun DialogEx(vm: AppViewModel, title: String, sub: String?, spec: J, onD
Text(d.str("name"), color = vm.palette.subtext, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(vertical = 6.dp)) Text(d.str("name"), color = vm.palette.subtext, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(vertical = 6.dp))
d.arr("rows").forEach { row -> d.arr("rows").forEach { row ->
val idx = i++ val idx = i++
ComboLine(vm, pool, picks, expected, idx, row.str("answer"), ComboLine(
prefix = "${row.str("speaker")} ${row.str("before")}", vm, pool, picks, expected, idx, row.str("answer"),
prefix = listOf(row.str("speaker"), row.str("before")).filter { it.isNotBlank() }.joinToString(" "),
suffix = row.str("after"), suffix = row.str("after"),
meaning = meanings.getOrNull(idx), meaning = meanings.getOrNull(idx),
revealed = revealed, revealed = revealed,
@ -644,6 +647,7 @@ private fun ComboRows(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun ComboLine( private fun ComboLine(
vm: AppViewModel, vm: AppViewModel,
@ -663,60 +667,89 @@ private fun ComboLine(
} }
val p = vm.palette val p = vm.palette
var open by remember { mutableStateOf(false) } var open by remember { mutableStateOf(false) }
val box = @Composable { val pick = picks.getOrNull(idx).orEmpty()
Column { val mark = if (revealed && pick.isNotEmpty()) {
Box( answerAccepts(normalizeAnswer(pick), answer)
Modifier } else null
.clip(RoundedCornerShape(12.dp)) val chip = @Composable {
.background(p.mantle) Box(
.border(1.dp, if (revealed && picks.getOrNull(idx).orEmpty().isNotEmpty()) { Modifier
if (answerAccepts(normalizeAnswer(picks.getOrNull(idx).orEmpty()), answer)) p.success else p.error .clip(RoundedCornerShape(12.dp))
} else p.text.copy(0.1f), RoundedCornerShape(12.dp)) .background(p.mantle)
.clickable { open = !open } .border(
.padding(horizontal = 12.dp, vertical = 8.dp), 1.dp,
) { when (mark) {
val pick = picks.getOrNull(idx).orEmpty() true -> p.success
Text(pick.ifEmpty { "—" }, color = if (pick.isEmpty()) p.overlay else p.text) false -> p.error
} null -> p.text.copy(0.12f)
if (open) { },
Column( RoundedCornerShape(12.dp),
Modifier )
.padding(top = 4.dp) .clickable { open = true }
.heightIn(max = 220.dp) .padding(horizontal = 12.dp, vertical = 8.dp),
.verticalScroll(rememberScrollState()) ) {
.clip(RoundedCornerShape(12.dp)) Text(
.background(p.surface0) pick.ifEmpty { "—" },
.border(1.dp, p.text.copy(0.08f), RoundedCornerShape(12.dp)), style = appTextStyle(15.sp, color = if (pick.isEmpty()) p.overlay else p.text),
) { )
pool.forEach { w ->
Text(
w,
color = p.text,
modifier = Modifier
.fillMaxWidth()
.clickable {
if (idx in picks.indices) picks[idx] = w
open = false
}
.padding(horizontal = 12.dp, vertical = 8.dp),
)
}
}
}
} }
} }
if (inline) { if (inline) {
box() chip()
return } else {
FlowRow(
Modifier.fillMaxWidth().padding(bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.Center,
) {
if (prefix.isNotBlank()) {
Text(prefix, style = appTextStyle(16.sp, color = p.text))
}
chip()
if (suffix.isNotBlank()) {
Text(suffix, style = appTextStyle(16.sp, color = p.text))
}
}
} }
Row(Modifier.fillMaxWidth().padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) { if (open) {
if (prefix.isNotBlank()) Text(prefix, color = p.text, modifier = Modifier.padding(end = 6.dp)) WordPicker(p, pool) { word ->
box() if (word != null && idx in picks.indices) picks[idx] = word
if (suffix.isNotBlank()) Text(suffix, color = p.text, modifier = Modifier.padding(start = 6.dp)) open = false
}
} }
if (revealed && !meaning.isNullOrBlank()) Meaning(vm.tr(meaning), p, true) if (revealed && !meaning.isNullOrBlank()) Meaning(vm.tr(meaning), p, true)
} }
@Composable
private fun WordPicker(p: Palette, pool: List<String>, onPick: (String?) -> Unit) {
Dialog(
onDismissRequest = { onPick(null) },
properties = DialogProperties(usePlatformDefaultWidth = true),
) {
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
.background(p.base)
.padding(10.dp)
.heightIn(max = 420.dp)
.verticalScroll(rememberScrollState()),
) {
pool.forEach { w ->
Text(
w,
style = appTextStyle(16.sp, color = p.text),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clickable { onPick(w) }
.padding(horizontal = 14.dp, vertical = 12.dp),
)
}
}
}
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun Ex2Ex(vm: AppViewModel, title: String, sub: String?, spec: J, onDone: () -> Unit) { private fun Ex2Ex(vm: AppViewModel, title: String, sub: String?, spec: J, onDone: () -> Unit) {
@ -968,29 +1001,20 @@ private fun VlsmEx(vm: AppViewModel, @Suppress("UNUSED_PARAMETER") lesson: J) {
@Composable @Composable
private fun PrefixMenu(value: String, p: Palette, onPick: (String) -> Unit) { private fun PrefixMenu(value: String, p: Palette, onPick: (String) -> Unit) {
var open by remember { mutableStateOf(false) } var open by remember { mutableStateOf(false) }
Column(Modifier.padding(vertical = 4.dp)) { Box(
Box( Modifier
Modifier.clip(RoundedCornerShape(12.dp)).background(p.mantle).clickable { open = !open }.padding(10.dp), .padding(vertical = 4.dp)
) { Text(value.ifEmpty { "/…" }, color = p.text) } .clip(RoundedCornerShape(12.dp))
if (open) { .background(p.mantle)
Column( .clickable { open = true }
Modifier .padding(10.dp),
.heightIn(max = 220.dp) ) {
.verticalScroll(rememberScrollState()) Text(value.ifEmpty { "/…" }, style = appTextStyle(15.sp, color = p.text))
.clip(RoundedCornerShape(12.dp)) }
.background(p.surface0), if (open) {
) { WordPicker(p, (8..30).map { "/$it" }) { word ->
(8..30).forEach { n -> if (word != null) onPick(word)
Text( open = false
"/$n",
color = p.text,
modifier = Modifier
.fillMaxWidth()
.clickable { onPick("/$n"); open = false }
.padding(horizontal = 12.dp, vertical = 8.dp),
)
}
}
} }
} }
} }

View file

@ -55,16 +55,15 @@ fun WelcomeScreen(vm: AppViewModel) {
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp, vertical = 16.dp), .padding(horizontal = 24.dp, vertical = 16.dp),
) { ) {
Text("maturita.c", color = p.text, fontWeight = FontWeight.Bold, fontSize = 15.sp) Text("maturita.c", style = appTextStyle(15.sp, FontWeight.Medium, color = p.text))
Text( Text(
"sestavení ${org.maturita.maturita.BuildConfig.VERSION_NAME} · ${org.maturita.maturita.BuildConfig.COMMIT.take(7)}", "sestavení ${org.maturita.maturita.BuildConfig.VERSION_NAME} · ${org.maturita.maturita.BuildConfig.COMMIT.take(7)}",
color = p.overlay, style = appTextStyle(12.sp, color = p.overlay),
fontSize = 12.sp,
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(vm.tr("welcome_title"), color = p.text, fontSize = 36.sp, fontWeight = FontWeight.Bold, lineHeight = 40.sp) Text(vm.tr("welcome_title"), style = appTextStyle(32.sp, FontWeight.Medium, color = p.text))
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
vm.tr("welcome_body_android"), vm.tr("welcome_body_android"),