Add four years of English from the gymnasium syllabus.

Each year mixes listening, reading, writing, gap-fill, quizzes, and a year test so the subject is usable through the maturita.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-07 10:42:28 +02:00
commit bb93ec76ec
32 changed files with 6607 additions and 14 deletions

View file

@ -146,6 +146,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
refresh()
}
fun markEn(year: Int, id: Int) {
progress.markEn(year, id)
refresh()
}
fun markLit(id: Int) {
progress.markLit(id)
refresh()

View file

@ -49,6 +49,8 @@ import org.maturita.maturita.ui.On3QuizScreen
import org.maturita.maturita.ui.On4QuizScreen
import org.maturita.maturita.ui.OnQuizScreen
import org.maturita.maturita.ui.OnYearsScreen
import org.maturita.maturita.ui.EnYearsScreen
import org.maturita.maturita.ui.EngExScreen
import org.maturita.maturita.ui.PlotScreen
import org.maturita.maturita.ui.RoadmapScreen
import org.maturita.maturita.ui.SearchScreen
@ -238,6 +240,21 @@ private fun MaturitaAppBody(vm: AppViewModel) {
}
}
is Route.HwEx -> HwQuizScreen(vm, r.id)
Route.EnYears -> EnYearsScreen(vm)
is Route.EnMap -> LessonListScreen(
vm, vm.tr("en_year${r.year}"), vm.tr("en_y${r.year}_sub"),
vm.content.enYear(r.year).map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.enDone(r.year, id), Route.EnLesson(r.year, id))
},
)
is Route.EnLesson -> {
val l = vm.content.enLesson(r.year, r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.EnEx(r.year, r.id))
}
}
is Route.EnEx -> EngExScreen(vm, r.year, r.id)
Route.OnYears -> OnYearsScreen(vm)
Route.OnMap -> LessonListScreen(
vm, vm.tr("on_year1"), vm.tr("on_sub"),

View file

@ -30,6 +30,10 @@ sealed class Route(val page: String) {
data object On4Map : Route("on4map")
data class On4Lesson(val id: Int) : Route("on4unit$id")
data class On4Ex(val id: Int) : Route("on4ex$id")
data object EnYears : Route("enyears")
data class EnMap(val year: Int) : Route("en${year}map")
data class EnLesson(val year: Int, val id: Int) : Route("en${year}unit$id")
data class EnEx(val year: Int, val id: Int) : Route("en${year}ex$id")
data object LitYears : Route("lityears")
data object LitMap : Route("litmap")
data class LitLesson(val id: Int) : Route("litunit$id")
@ -93,7 +97,7 @@ sealed class Route(val page: String) {
/** Screens with their own bottom action bar; the navigation bar stays hidden there. */
val isExercise: Boolean
get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx ||
this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is MluvEx || this is BookQuiz || this is BookPlot ||
this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is EnLesson || this is EnEx || this is MluvEx || this is BookQuiz || this is BookPlot ||
this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson || this is On4Lesson ||
this is LitLesson || this is LitEx || this is Lit2Lesson || this is Lit2Ex ||
this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex ||
@ -134,6 +138,10 @@ sealed class Route(val page: String) {
On4Map -> listOf(Subjects, OnYears, On4Map)
is On4Lesson -> listOf(Subjects, OnYears, On4Map, this)
is On4Ex -> listOf(Subjects, OnYears, On4Map, On4Lesson(id), this)
EnYears -> listOf(Subjects, EnYears)
is EnMap -> listOf(Subjects, EnYears, this)
is EnLesson -> listOf(Subjects, EnYears, EnMap(year), this)
is EnEx -> listOf(Subjects, EnYears, EnMap(year), EnLesson(year, id), this)
LitYears -> listOf(Subjects, CzechMap, LitYears)
LitMap -> listOf(Subjects, CzechMap, LitYears, LitMap)
is LitLesson -> listOf(Subjects, CzechMap, LitYears, LitMap, this)
@ -208,6 +216,7 @@ fun routeFromPage(page: String): Route? = when (page) {
"on2map" -> Route.On2Map
"on3map" -> Route.On3Map
"on4map" -> Route.On4Map
"enyears" -> Route.EnYears
"lityears" -> Route.LitYears
"litmap" -> Route.LitMap
"lit2map" -> Route.Lit2Map
@ -348,6 +357,15 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""litex(\d+)""").matchEntire(page)?.let {
return Route.LitEx(it.groupValues[1].toInt())
}
Regex("""en(\d)map""").matchEntire(page)?.let {
return Route.EnMap(it.groupValues[1].toInt())
}
Regex("""en(\d)unit(\d+)""").matchEntire(page)?.let {
return Route.EnLesson(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""en(\d)ex(\d+)""").matchEntire(page)?.let {
return Route.EnEx(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""on4unit(\d+)""").matchEntire(page)?.let {
return Route.On4Lesson(it.groupValues[1].toInt())
}

View file

@ -33,6 +33,10 @@ class Content(root: JSONObject) {
val on2 = raw.arr("on2")
val on3 = raw.arr("on3")
val on4 = raw.arr("on4")
val en = raw.arr("en")
val en2 = raw.arr("en2")
val en3 = raw.arr("en3")
val en4 = raw.arr("en4")
val lit = raw.arr("lit")
val lit2 = raw.arr("lit2")
val lit3 = raw.arr("lit3")
@ -59,6 +63,13 @@ class Content(root: JSONObject) {
fun on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id }
fun on3Lesson(id: Int) = on3.firstOrNull { it.int("id") == id }
fun on4Lesson(id: Int) = on4.firstOrNull { it.int("id") == id }
fun enYear(year: Int) = when (year) {
2 -> en2
3 -> en3
4 -> en4
else -> en
}
fun enLesson(year: Int, id: Int) = enYear(year).firstOrNull { it.int("id") == id }
fun litLesson(id: Int) = lit.firstOrNull { it.int("id") == id }
fun lit2Lesson(id: Int) = lit2.firstOrNull { it.int("id") == id }
fun lit3Lesson(id: Int) = lit3.firstOrNull { it.int("id") == id }

View file

@ -44,6 +44,9 @@ class ProgressStore(context: Context) {
fun on4Done(id: Int) = prefs.getBoolean("on4.$id", false)
fun markOn4(id: Int) { prefs.edit().putBoolean("on4.$id", true).apply() }
fun enDone(year: Int, id: Int) = prefs.getBoolean("en.$year.$id", false)
fun markEn(year: Int, id: Int) { prefs.edit().putBoolean("en.$year.$id", true).apply() }
fun litDone(id: Int) = prefs.getBoolean("lit.$id", false)
fun markLit(id: Int) { prefs.edit().putBoolean("lit.$id", true).apply() }
fun lit2Done(id: Int) = prefs.getBoolean("lit2.$id", false)
@ -146,6 +149,16 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1
}
}
for (year in 1..4) {
content.enYear(year).forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.enDone(year, l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
content.lit.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1

View file

@ -38,8 +38,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import android.os.Handler
import android.os.Looper
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext
import java.util.Locale
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
@ -1370,4 +1377,156 @@ fun PlotScreen(vm: AppViewModel, id: String) {
put("meanings", book.o.optJSONArray("plotMeaning"))
})
DispatchExercise(vm, spec, vm.tr(book.str("plotTitle"))) { vm.markBookPlot(id) }
}
@Composable
fun EngExScreen(vm: AppViewModel, year: Int, id: Int) {
val lesson = vm.content.enLesson(year, id) ?: return
val readQ = lesson.arr("readQuiz")
val listenQ = lesson.arr("listenQuiz")
val quiz = lesson.arr("quiz")
val gapRows = lesson.arr("gaps")
val writing = lesson.obj("writing")
val listening = lesson.strOrNull("listening")
val readPicks = remember(year, id) { mutableStateListOf(*Array(readQ.size) { -1 }) }
val listenPicks = remember(year, id) { mutableStateListOf(*Array(listenQ.size) { -1 }) }
val quizPicks = remember(year, id) { mutableStateListOf(*Array(quiz.size) { -1 }) }
val gapText = remember(year, id) { mutableStateListOf(*Array(gapRows.size) { "" }) }
var essay by remember(year, id) { mutableStateOf("") }
var played by remember(year, id) { mutableStateOf(false) }
var showScript by remember(year, id) { mutableStateOf(false) }
var revealed by remember(year, id) { mutableStateOf(false) }
var showModel by remember(year, id) { mutableStateOf(false) }
var fb by remember(year, id) { mutableStateOf("" to "") }
val context = LocalContext.current
var tts by remember { mutableStateOf<TextToSpeech?>(null) }
DisposableEffect(context) {
var engine: TextToSpeech? = null
engine = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) engine?.language = Locale.UK
}
tts = engine
onDispose {
engine?.stop()
engine?.shutdown()
}
}
fun picksOk(questions: List<J>, picks: List<Int>) =
questions.indices.all { picks[it] == questions[it].int("correct") }
ExerciseScaffold(vm, vm.tr(lesson.str("titleKey")), null, vm.tr("check"), {
if (listening != null && !played) {
fb = vm.tr("en_listen_first") to "err"
} else {
val readOk = picksOk(readQ, readPicks)
val listenOk = picksOk(listenQ, listenPicks)
val quizOk = picksOk(quiz, quizPicks)
val gapOk = gapRows.indices.count {
answerAccepts(normalizeAnswer(gapText[it]), gapRows[it].str("answers"))
}
val writeOk = if (writing == null) {
true
} else {
val norm = " ${normalizeAnswer(essay)} "
val keysOk = writing.str("keys").split("|").all { key ->
val w = normalizeAnswer(key)
w.isEmpty() || norm.contains(" $w ")
}
keysOk && normalizeAnswer(essay).split(" ").count { it.isNotEmpty() } >= writing.int("minWords", 20)
}
revealed = true
val all = readOk && listenOk && quizOk && gapOk == gapRows.size && writeOk
fb = when {
all -> {
showModel = true
vm.markEn(year, id)
vm.tr("feedback_ok") to "ok"
}
writing != null && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.size ->
vm.tr("en_write_short") to "err"
else -> vm.tr("feedback_retry_short") to "err"
}
}
}, fb.first, fb.second) {
lesson.strOrNull("reading")?.let { reading ->
Text(vm.tr("en_sec_read"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
Text(reading)
EngChoices(readQ, readPicks, revealed) { index, option -> readPicks[index] = option }
}
if (listening != null) {
Text(vm.tr("en_sec_listen"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
Text(vm.tr("en_listen_note"), color = MaterialTheme.colorScheme.onSurfaceVariant)
OutlinedButton(onClick = {
played = true
showScript = false
val engine = tts
if (engine == null) {
showScript = true
} else {
engine.language = Locale.UK
engine.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {}
override fun onDone(utteranceId: String?) {
Handler(Looper.getMainLooper()).post { showScript = true }
}
@Deprecated("Deprecated in Java")
override fun onError(utteranceId: String?) {
Handler(Looper.getMainLooper()).post { showScript = true }
}
})
engine.speak(listening, TextToSpeech.QUEUE_FLUSH, null, "en-lesson")
val wait = (listening.split(" ").size * 420L).coerceAtLeast(2500L)
Handler(Looper.getMainLooper()).postDelayed({ showScript = true }, wait)
}
}) { Text(vm.tr("en_play")) }
if (showScript) Text(listening)
EngChoices(listenQ, listenPicks, revealed) { index, option -> listenPicks[index] = option }
}
if (gapRows.isNotEmpty()) {
Text(vm.tr("en_sec_gap"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
gapRows.forEachIndexed { i, row ->
Text("${i + 1}. ${row.str("prompt")}", style = MaterialTheme.typography.titleSmall)
val mark = if (!revealed) null else answerAccepts(normalizeAnswer(gapText[i]), row.str("answers"))
WordField(gapText[i], { gapText[i] = it }, "", mark)
if (revealed) row.strOrNull("meaning")?.let { Meaning(it, true) }
}
}
if (writing != null) {
Text(vm.tr("en_sec_write"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
Text(writing.str("prompt"))
OutlinedTextField(
value = essay,
onValueChange = { essay = it },
modifier = Modifier.fillMaxWidth().height(160.dp),
minLines = 5,
)
if (showModel) {
Text(vm.tr("en_model"), style = MaterialTheme.typography.titleSmall)
Text(writing.str("model"))
}
}
if (quiz.isNotEmpty()) {
Text(vm.tr("en_sec_quiz"), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
EngChoices(quiz, quizPicks, revealed) { index, option -> quizPicks[index] = option }
}
}
}
@Composable
private fun EngChoices(
questions: List<J>,
picks: List<Int>,
revealed: Boolean,
onPick: (Int, Int) -> Unit,
) {
questions.forEachIndexed { i, q ->
Text("${i + 1}. ${q.str("prompt")}", style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(top = 8.dp))
q.strs("options").forEachIndexed { o, opt ->
val selected = picks.getOrElse(i) { -1 } == o
val mark = if (revealed && selected) picks[i] == q.int("correct") else null
OptionRow(opt, selected, mark) { onPick(i, o) }
}
if (revealed) q.strOrNull("expl")?.let { Meaning(it, true) }
}
}

View file

@ -195,6 +195,26 @@ fun CzechFlag(iconSize: Dp = 48.dp) {
}
}
@Composable
fun UkFlag(iconSize: Dp = 48.dp) {
DrawnIcon(iconSize) {
val w = this.size.width
val h = this.size.height
val clip = Path().apply { addOval(androidx.compose.ui.geometry.Rect(0f, 0f, w, h)) }
clipPath(clip) {
drawRect(Color(0xFF012466), size = Size(w, h))
drawLine(Color.White, Offset(0f, 0f), Offset(w, h), strokeWidth = w * 0.16f)
drawLine(Color.White, Offset(w, 0f), Offset(0f, h), strokeWidth = w * 0.16f)
drawLine(Color(0xFFCC0D26), Offset(0f, 0f), Offset(w, h), strokeWidth = w * 0.055f)
drawLine(Color(0xFFCC0D26), Offset(w, 0f), Offset(0f, h), strokeWidth = w * 0.055f)
drawRect(Color.White, topLeft = Offset(0f, h * 0.38f), size = Size(w, h * 0.24f))
drawRect(Color.White, topLeft = Offset(w * 0.38f, 0f), size = Size(w * 0.24f, h))
drawRect(Color(0xFFCC0D26), topLeft = Offset(0f, h * 0.43f), size = Size(w, h * 0.14f))
drawRect(Color(0xFFCC0D26), topLeft = Offset(w * 0.43f, 0f), size = Size(w * 0.14f, h))
}
}
}
@Composable
fun BookIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {

View file

@ -223,6 +223,7 @@ private fun SubjectIcon(icon: String, open: Boolean) {
"chip" -> ChipIcon(tint, 26.dp)
"people" -> Icon(Icons.Filled.Groups, null, tint = tint, modifier = Modifier.size(26.dp))
"cz" -> CzechFlag(28.dp)
"uk" -> UkFlag(28.dp)
"flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp))
"bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp))
"function" -> Icon(Icons.Filled.Functions, null, tint = tint, modifier = Modifier.size(26.dp))
@ -306,6 +307,12 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
sum.totalEx = vm.content.hw.size
sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) }
}
"enyears", "en1map", "en2map", "en3map", "en4map" -> {
sum.totalEx = (1..4).sumOf { vm.content.enYear(it).size }
sum.doneEx = (1..4).sumOf { year ->
vm.content.enYear(year).count { vm.progress.enDone(year, it.int("id")) }
}
}
"onyears", "onmap", "on2map", "on3map", "on4map" -> {
sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.size + vm.content.on4.size
sum.doneEx = vm.content.on.count { vm.progress.onDone(it.int("id")) } +
@ -545,6 +552,13 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on4unit${l.int("id")}", extra = "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex${l.int("id")}")
}
for (year in 1..4) {
add(vm.tr("en_year$year"), vm.tr("search_lesson"), "en${year}map", extra = "anglictina english rocnik")
vm.content.enYear(year).forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en${year}unit${l.int("id")}", extra = "anglictina english")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "en${year}ex${l.int("id")}")
}
}
add(vm.tr("Literatura"), vm.tr("search_lesson"), "lityears", extra = "literatura")
add(vm.tr("lit_year1"), vm.tr("search_lesson"), "litmap", extra = "rocnik")
add(vm.tr("lit_year2"), vm.tr("search_lesson"), "lit2map", extra = "rocnik")
@ -722,6 +736,21 @@ fun HwYearsScreen(vm: AppViewModel) {
}
@Composable
fun EnYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("English"), vm.tr("en_years_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
items(4) { index ->
val year = index + 1
NavRow(
title = vm.tr("en_year$year"),
subtitle = vm.tr("en_y${year}_sub"),
leading = { Text("$year.", style = MaterialTheme.typography.titleMedium) },
) { vm.go(Route.EnMap(year)) }
}
}
}
}
fun OnYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {