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"
minSdk = 26
targetSdk = 35
versionCode = 3
versionName = "android-3"
versionCode = 4
versionName = "android-4"
buildConfigField("String", "COMMIT", "\"$commit\"")
buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"")
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.SettingsSheet
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.StatsScreen
import org.maturita.maturita.ui.SubjectsScreen
@ -153,7 +155,7 @@ fun MaturitaApp(vm: AppViewModel) {
error = p.error,
)
}
MaterialTheme(colorScheme = scheme) {
MaterialTheme(colorScheme = scheme, typography = appTypography()) {
MaturitaAppBody(vm)
}
}
@ -183,7 +185,7 @@ private fun MaturitaAppBody(vm: AppViewModel) {
.padding(horizontal = 12.dp, vertical = 6.dp),
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.go(Route.Stats) }) { StatsIcon(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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.material3.Typography
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
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.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.maturita.maturita.AppViewModel
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 Card = RoundedCornerShape(20.dp)
@ -42,7 +80,7 @@ fun PrimaryButton(label: String, palette: Palette, modifier: Modifier = Modifier
.padding(horizontal = 22.dp, vertical = 12.dp),
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)
.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
fun PageTop(vm: AppViewModel, back: () -> Unit, title: String, subtitle: String? = null) {
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.fillMaxWidth(), contentAlignment = Alignment.Center) {
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.unit.dp
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.data.J
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))
d.arr("rows").forEach { row ->
val idx = i++
ComboLine(vm, pool, picks, expected, idx, row.str("answer"),
prefix = "${row.str("speaker")} ${row.str("before")}",
ComboLine(
vm, pool, picks, expected, idx, row.str("answer"),
prefix = listOf(row.str("speaker"), row.str("before")).filter { it.isNotBlank() }.joinToString(" "),
suffix = row.str("after"),
meaning = meanings.getOrNull(idx),
revealed = revealed,
@ -644,6 +647,7 @@ private fun ComboRows(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ComboLine(
vm: AppViewModel,
@ -663,58 +667,87 @@ private fun ComboLine(
}
val p = vm.palette
var open by remember { mutableStateOf(false) }
val box = @Composable {
Column {
val pick = picks.getOrNull(idx).orEmpty()
val mark = if (revealed && pick.isNotEmpty()) {
answerAccepts(normalizeAnswer(pick), answer)
} else null
val chip = @Composable {
Box(
Modifier
.clip(RoundedCornerShape(12.dp))
.background(p.mantle)
.border(1.dp, if (revealed && picks.getOrNull(idx).orEmpty().isNotEmpty()) {
if (answerAccepts(normalizeAnswer(picks.getOrNull(idx).orEmpty()), answer)) p.success else p.error
} else p.text.copy(0.1f), RoundedCornerShape(12.dp))
.clickable { open = !open }
.border(
1.dp,
when (mark) {
true -> p.success
false -> p.error
null -> p.text.copy(0.12f)
},
RoundedCornerShape(12.dp),
)
.clickable { open = true }
.padding(horizontal = 12.dp, vertical = 8.dp),
) {
val pick = picks.getOrNull(idx).orEmpty()
Text(pick.ifEmpty { "—" }, color = if (pick.isEmpty()) p.overlay else p.text)
Text(
pick.ifEmpty { "—" },
style = appTextStyle(15.sp, color = if (pick.isEmpty()) p.overlay else p.text),
)
}
}
if (inline) {
chip()
} 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))
}
}
}
if (open) {
WordPicker(p, pool) { word ->
if (word != null && idx in picks.indices) picks[idx] = word
open = false
}
}
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
.padding(top = 4.dp)
.heightIn(max = 220.dp)
.verticalScroll(rememberScrollState())
.clip(RoundedCornerShape(12.dp))
.background(p.surface0)
.border(1.dp, p.text.copy(0.08f), RoundedCornerShape(12.dp)),
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
.background(p.base)
.padding(10.dp)
.heightIn(max = 420.dp)
.verticalScroll(rememberScrollState()),
) {
pool.forEach { w ->
Text(
w,
color = p.text,
style = appTextStyle(16.sp, color = p.text),
modifier = Modifier
.fillMaxWidth()
.clickable {
if (idx in picks.indices) picks[idx] = w
open = false
}
.padding(horizontal = 12.dp, vertical = 8.dp),
.clip(RoundedCornerShape(12.dp))
.clickable { onPick(w) }
.padding(horizontal = 14.dp, vertical = 12.dp),
)
}
}
}
}
}
if (inline) {
box()
return
}
Row(Modifier.fillMaxWidth().padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
if (prefix.isNotBlank()) Text(prefix, color = p.text, modifier = Modifier.padding(end = 6.dp))
box()
if (suffix.isNotBlank()) Text(suffix, color = p.text, modifier = Modifier.padding(start = 6.dp))
}
if (revealed && !meaning.isNullOrBlank()) Meaning(vm.tr(meaning), p, true)
}
@OptIn(ExperimentalLayoutApi::class)
@ -968,29 +1001,20 @@ private fun VlsmEx(vm: AppViewModel, @Suppress("UNUSED_PARAMETER") lesson: J) {
@Composable
private fun PrefixMenu(value: String, p: Palette, onPick: (String) -> Unit) {
var open by remember { mutableStateOf(false) }
Column(Modifier.padding(vertical = 4.dp)) {
Box(
Modifier.clip(RoundedCornerShape(12.dp)).background(p.mantle).clickable { open = !open }.padding(10.dp),
) { Text(value.ifEmpty { "/…" }, color = p.text) }
if (open) {
Column(
Modifier
.heightIn(max = 220.dp)
.verticalScroll(rememberScrollState())
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(12.dp))
.background(p.surface0),
.background(p.mantle)
.clickable { open = true }
.padding(10.dp),
) {
(8..30).forEach { n ->
Text(
"/$n",
color = p.text,
modifier = Modifier
.fillMaxWidth()
.clickable { onPick("/$n"); open = false }
.padding(horizontal = 12.dp, vertical = 8.dp),
)
}
Text(value.ifEmpty { "/…" }, style = appTextStyle(15.sp, color = p.text))
}
if (open) {
WordPicker(p, (8..30).map { "/$it" }) { word ->
if (word != null) onPick(word)
open = false
}
}
}

View file

@ -55,16 +55,15 @@ fun WelcomeScreen(vm: AppViewModel) {
.verticalScroll(rememberScrollState())
.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(
"sestavení ${org.maturita.maturita.BuildConfig.VERSION_NAME} · ${org.maturita.maturita.BuildConfig.COMMIT.take(7)}",
color = p.overlay,
fontSize = 12.sp,
style = appTextStyle(12.sp, color = p.overlay),
)
Spacer(Modifier.height(12.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
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))
Text(
vm.tr("welcome_body_android"),