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

@ -8,8 +8,8 @@ Vzdělávací aplikace k přípravě na maturitu (repo `Graduately`). / An educa
## Česky ## Česky
Rozhraní je v češtině a angličtině. Hotových je pět předmětů; ostatní jsou Rozhraní je v češtině a angličtině. Otevřené předměty jsou v seznamu níže;
zatím zamčené placeholdery. Aplikace se po instalaci aktualizuje sama. ostatní jsou zatím zamčené. Aplikace se po instalaci aktualizuje sama.
| Úvod | Předměty | Německá cesta | | Úvod | Předměty | Německá cesta |
| ---- | -------- | ------------- | | ---- | -------- | ------------- |
@ -78,6 +78,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
- **Občanská nauka** — 1. ročník, 17 lekcí (člověk v lidském společenství); - **Občanská nauka** — 1. ročník, 17 lekcí (člověk v lidském společenství);
2. ročník, 16 lekcí (člověk jako občan); 3. ročník, 16 lekcí (člověk a právo); 2. ročník, 16 lekcí (člověk jako občan); 3. ročník, 16 lekcí (člověk a právo);
4. ročník, 16 lekcí (hospodářství, svět a filozofie) 4. ročník, 16 lekcí (hospodářství, svět a filozofie)
- **Angličtina** — 4 ročníky, 32 lekcí podle osnov gymnázia (A2 až maturita):
poslech, čtení, psaní, doplňování, kvízy a ročníkové testy
- statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina - statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina
- ukončení: `Cmd/Super+Q` nebo `Alt+F4` - ukončení: `Cmd/Super+Q` nebo `Alt+F4`
@ -210,8 +212,8 @@ Licence: [GPL-3.0](LICENSE).
## English ## English
The UI is Czech and English. Five subjects are playable; the rest are locked The UI is Czech and English. Open subjects are listed below; the rest are
placeholders. After install the app updates itself. still locked. After install the app updates itself.
| Welcome | Subjects | German path | | Welcome | Subjects | German path |
| ------- | -------- | ----------- | | ------- | -------- | ----------- |
@ -282,6 +284,8 @@ Intel Mac, another architecture, or hacking on the code →
year 2, 16 lessons (the citizen in a democratic state); year 2, 16 lessons (the citizen in a democratic state);
year 3, 16 lessons (the individual and the law); year 3, 16 lessons (the individual and the law);
year 4, 16 lessons (the economy, the world and philosophy) year 4, 16 lessons (the economy, the world and philosophy)
- **English** — 4 years, 32 lessons from the gymnasium syllabus (A2 through
the maturita): listening, reading, writing, gap-fill, quizzes and year tests
- statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English - statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English
- quit with `Cmd/Super+Q` or `Alt+F4` - quit with `Cmd/Super+Q` or `Alt+F4`

View file

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

View file

@ -49,6 +49,8 @@ import org.maturita.maturita.ui.On3QuizScreen
import org.maturita.maturita.ui.On4QuizScreen import org.maturita.maturita.ui.On4QuizScreen
import org.maturita.maturita.ui.OnQuizScreen import org.maturita.maturita.ui.OnQuizScreen
import org.maturita.maturita.ui.OnYearsScreen 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.PlotScreen
import org.maturita.maturita.ui.RoadmapScreen import org.maturita.maturita.ui.RoadmapScreen
import org.maturita.maturita.ui.SearchScreen import org.maturita.maturita.ui.SearchScreen
@ -238,6 +240,21 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.HwEx -> HwQuizScreen(vm, r.id) 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.OnYears -> OnYearsScreen(vm)
Route.OnMap -> LessonListScreen( Route.OnMap -> LessonListScreen(
vm, vm.tr("on_year1"), vm.tr("on_sub"), 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 object On4Map : Route("on4map")
data class On4Lesson(val id: Int) : Route("on4unit$id") data class On4Lesson(val id: Int) : Route("on4unit$id")
data class On4Ex(val id: Int) : Route("on4ex$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 LitYears : Route("lityears")
data object LitMap : Route("litmap") data object LitMap : Route("litmap")
data class LitLesson(val id: Int) : Route("litunit$id") 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. */ /** Screens with their own bottom action bar; the navigation bar stays hidden there. */
val isExercise: Boolean val isExercise: Boolean
get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx || 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 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 LitLesson || this is LitEx || this is Lit2Lesson || this is Lit2Ex ||
this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex || 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) On4Map -> listOf(Subjects, OnYears, On4Map)
is On4Lesson -> listOf(Subjects, OnYears, On4Map, this) is On4Lesson -> listOf(Subjects, OnYears, On4Map, this)
is On4Ex -> listOf(Subjects, OnYears, On4Map, On4Lesson(id), 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) LitYears -> listOf(Subjects, CzechMap, LitYears)
LitMap -> listOf(Subjects, CzechMap, LitYears, LitMap) LitMap -> listOf(Subjects, CzechMap, LitYears, LitMap)
is LitLesson -> listOf(Subjects, CzechMap, LitYears, LitMap, this) is LitLesson -> listOf(Subjects, CzechMap, LitYears, LitMap, this)
@ -208,6 +216,7 @@ fun routeFromPage(page: String): Route? = when (page) {
"on2map" -> Route.On2Map "on2map" -> Route.On2Map
"on3map" -> Route.On3Map "on3map" -> Route.On3Map
"on4map" -> Route.On4Map "on4map" -> Route.On4Map
"enyears" -> Route.EnYears
"lityears" -> Route.LitYears "lityears" -> Route.LitYears
"litmap" -> Route.LitMap "litmap" -> Route.LitMap
"lit2map" -> Route.Lit2Map "lit2map" -> Route.Lit2Map
@ -348,6 +357,15 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""litex(\d+)""").matchEntire(page)?.let { Regex("""litex(\d+)""").matchEntire(page)?.let {
return Route.LitEx(it.groupValues[1].toInt()) 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 { Regex("""on4unit(\d+)""").matchEntire(page)?.let {
return Route.On4Lesson(it.groupValues[1].toInt()) return Route.On4Lesson(it.groupValues[1].toInt())
} }

View file

@ -33,6 +33,10 @@ class Content(root: JSONObject) {
val on2 = raw.arr("on2") val on2 = raw.arr("on2")
val on3 = raw.arr("on3") val on3 = raw.arr("on3")
val on4 = raw.arr("on4") 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 lit = raw.arr("lit")
val lit2 = raw.arr("lit2") val lit2 = raw.arr("lit2")
val lit3 = raw.arr("lit3") val lit3 = raw.arr("lit3")
@ -59,6 +63,13 @@ class Content(root: JSONObject) {
fun on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id } fun on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id }
fun on3Lesson(id: Int) = on3.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 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 litLesson(id: Int) = lit.firstOrNull { it.int("id") == id }
fun lit2Lesson(id: Int) = lit2.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 } 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 on4Done(id: Int) = prefs.getBoolean("on4.$id", false)
fun markOn4(id: Int) { prefs.edit().putBoolean("on4.$id", true).apply() } 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 litDone(id: Int) = prefs.getBoolean("lit.$id", false)
fun markLit(id: Int) { prefs.edit().putBoolean("lit.$id", true).apply() } fun markLit(id: Int) { prefs.edit().putBoolean("lit.$id", true).apply() }
fun lit2Done(id: Int) = prefs.getBoolean("lit2.$id", false) fun lit2Done(id: Int) = prefs.getBoolean("lit2.$id", false)
@ -146,6 +149,16 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 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 -> content.lit.forEach { l ->
sum.totalEx += 1 sum.totalEx += 1
sum.openUnits += 1 sum.openUnits += 1

View file

@ -38,8 +38,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text 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.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext
import java.util.Locale
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
@ -1370,4 +1377,156 @@ fun PlotScreen(vm: AppViewModel, id: String) {
put("meanings", book.o.optJSONArray("plotMeaning")) put("meanings", book.o.optJSONArray("plotMeaning"))
}) })
DispatchExercise(vm, spec, vm.tr(book.str("plotTitle"))) { vm.markBookPlot(id) } 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 @Composable
fun BookIcon(color: Color, iconSize: Dp = 22.dp) { fun BookIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) { DrawnIcon(iconSize) {

View file

@ -223,6 +223,7 @@ private fun SubjectIcon(icon: String, open: Boolean) {
"chip" -> ChipIcon(tint, 26.dp) "chip" -> ChipIcon(tint, 26.dp)
"people" -> Icon(Icons.Filled.Groups, null, tint = tint, modifier = Modifier.size(26.dp)) "people" -> Icon(Icons.Filled.Groups, null, tint = tint, modifier = Modifier.size(26.dp))
"cz" -> CzechFlag(28.dp) "cz" -> CzechFlag(28.dp)
"uk" -> UkFlag(28.dp)
"flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.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)) "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)) "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.totalEx = vm.content.hw.size
sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } 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" -> { "onyears", "onmap", "on2map", "on3map", "on4map" -> {
sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.size + vm.content.on4.size 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")) } + 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_lesson"), "on4unit${l.int("id")}", extra = "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex${l.int("id")}") 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("Literatura"), vm.tr("search_lesson"), "lityears", extra = "literatura")
add(vm.tr("lit_year1"), vm.tr("search_lesson"), "litmap", extra = "rocnik") add(vm.tr("lit_year1"), vm.tr("search_lesson"), "litmap", extra = "rocnik")
add(vm.tr("lit_year2"), vm.tr("search_lesson"), "lit2map", extra = "rocnik") add(vm.tr("lit_year2"), vm.tr("search_lesson"), "lit2map", extra = "rocnik")
@ -722,6 +736,21 @@ fun HwYearsScreen(vm: AppViewModel) {
} }
@Composable @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) { fun OnYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner -> DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {

View file

@ -6,6 +6,12 @@
# cs: bullet # cs: bullet
# en: bullet # en: bullet
--- 2026-10-07
cs: Angličtina: čtyři ročníky, 32 lekcí podle osnov gymnázia
cs: Poslech, čtení, psaní, doplňování, kvízy a testy od A2 k maturitě
en: English: four years, 32 lessons from the gymnasium syllabus
en: Listening, reading, writing, gap-fill, quizzes and tests from A2 to the exam
--- 2026-10-06 --- 2026-10-06
cs: Matematika: opakování základní školy a čtyři ročníky, 40 lekcí cs: Matematika: opakování základní školy a čtyři ročníky, 40 lekcí
cs: Příklady se píšou a kontrolují, u geometrie se rýsuje na mřížku cs: Příklady se píšou a kontrolují, u geometrie se rýsuje na mřížku

View file

@ -166,6 +166,11 @@ final class AppModel: ObservableObject {
refresh() refresh()
} }
func markEn(_ year: Int, _ id: Int) {
progress.markEn(year, id)
refresh()
}
func markLit(_ id: Int) { func markLit(_ id: Int) {
progress.markLit(id) progress.markLit(id)
refresh() refresh()

View file

@ -35,6 +35,10 @@ final class Content {
let on2: [J] let on2: [J]
let on3: [J] let on3: [J]
let on4: [J] let on4: [J]
let en: [J]
let en2: [J]
let en3: [J]
let en4: [J]
let lit: [J] let lit: [J]
let lit2: [J] let lit2: [J]
let lit3: [J] let lit3: [J]
@ -85,6 +89,10 @@ final class Content {
on2 = raw.arr("on2") on2 = raw.arr("on2")
on3 = raw.arr("on3") on3 = raw.arr("on3")
on4 = raw.arr("on4") on4 = raw.arr("on4")
en = raw.arr("en")
en2 = raw.arr("en2")
en3 = raw.arr("en3")
en4 = raw.arr("en4")
lit = raw.arr("lit") lit = raw.arr("lit")
lit2 = raw.arr("lit2") lit2 = raw.arr("lit2")
lit3 = raw.arr("lit3") lit3 = raw.arr("lit3")
@ -112,6 +120,15 @@ final class Content {
func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } } func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } }
func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } } func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } }
func on4Lesson(_ id: Int) -> J? { on4.first { $0.int("id") == id } } func on4Lesson(_ id: Int) -> J? { on4.first { $0.int("id") == id } }
func enYear(_ year: Int) -> [J] {
switch year {
case 2: return en2
case 3: return en3
case 4: return en4
default: return en
}
}
func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).first { $0.int("id") == id } }
func litLesson(_ id: Int) -> J? { lit.first { $0.int("id") == id } } func litLesson(_ id: Int) -> J? { lit.first { $0.int("id") == id } }
func lit2Lesson(_ id: Int) -> J? { lit2.first { $0.int("id") == id } } func lit2Lesson(_ id: Int) -> J? { lit2.first { $0.int("id") == id } }
func lit3Lesson(_ id: Int) -> J? { lit3.first { $0.int("id") == id } } func lit3Lesson(_ id: Int) -> J? { lit3.first { $0.int("id") == id } }

View file

@ -46,6 +46,9 @@ final class ProgressStore {
func on4Done(_ id: Int) -> Bool { defaults.bool(forKey: "on4.\(id)") } func on4Done(_ id: Int) -> Bool { defaults.bool(forKey: "on4.\(id)") }
func markOn4(_ id: Int) { defaults.set(true, forKey: "on4.\(id)") } func markOn4(_ id: Int) { defaults.set(true, forKey: "on4.\(id)") }
func enDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "en.\(year).\(id)") }
func markEn(_ year: Int, _ id: Int) { defaults.set(true, forKey: "en.\(year).\(id)") }
func litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") } func litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") }
func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") } func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") }
func lit2Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit2.\(id)") } func lit2Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit2.\(id)") }
@ -153,6 +156,16 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
for year in 1...4 {
for l in content.enYear(year) {
sum.totalEx += 1
sum.openUnits += 1
if p.enDone(year, l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
for l in content.lit { for l in content.lit {
sum.totalEx += 1 sum.totalEx += 1
sum.openUnits += 1 sum.openUnits += 1

View file

@ -36,6 +36,10 @@ enum Route: Hashable {
case on4Map case on4Map
case on4Lesson(Int) case on4Lesson(Int)
case on4Ex(Int) case on4Ex(Int)
case enYears
case enMap(Int)
case enLesson(Int, Int)
case enEx(Int, Int)
case litYears case litYears
case litMap case litMap
case litLesson(Int) case litLesson(Int)
@ -123,6 +127,10 @@ enum Route: Hashable {
case .on4Map: return "on4map" case .on4Map: return "on4map"
case .on4Lesson(let id): return "on4unit\(id)" case .on4Lesson(let id): return "on4unit\(id)"
case .on4Ex(let id): return "on4ex\(id)" case .on4Ex(let id): return "on4ex\(id)"
case .enYears: return "enyears"
case .enMap(let year): return "en\(year)map"
case .enLesson(let year, let id): return "en\(year)unit\(id)"
case .enEx(let year, let id): return "en\(year)ex\(id)"
case .litYears: return "lityears" case .litYears: return "lityears"
case .litMap: return "litmap" case .litMap: return "litmap"
case .litLesson(let id): return "litunit\(id)" case .litLesson(let id): return "litunit\(id)"
@ -246,6 +254,14 @@ enum Route: Hashable {
return [.onYears, .on4Map, .on4Lesson(id)] return [.onYears, .on4Map, .on4Lesson(id)]
case .on4Ex(let id): case .on4Ex(let id):
return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)] return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)]
case .enYears:
return [.enYears]
case .enMap(let year):
return [.enYears, .enMap(year)]
case .enLesson(let year, let id):
return [.enYears, .enMap(year), .enLesson(year, id)]
case .enEx(let year, let id):
return [.enYears, .enMap(year), .enLesson(year, id), .enEx(year, id)]
case .litYears: case .litYears:
return [.czechMap, .litYears] return [.czechMap, .litYears]
case .litMap: case .litMap:
@ -377,6 +393,7 @@ func routeFromPage(_ page: String) -> Route? {
case "on2map": return .on2Map case "on2map": return .on2Map
case "on3map": return .on3Map case "on3map": return .on3Map
case "on4map": return .on4Map case "on4map": return .on4Map
case "enyears": return .enYears
case "lityears": return .litYears case "lityears": return .litYears
case "litmap": return .litMap case "litmap": return .litMap
case "lit2map": return .lit2Map case "lit2map": return .lit2Map
@ -529,6 +546,15 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /litex(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /litex(\d+)/), let n = Int(m.1) {
return .litEx(n) return .litEx(n)
} }
if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) {
return .enMap(y)
}
if let m = page.wholeMatch(of: /en(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
return .enLesson(y, n)
}
if let m = page.wholeMatch(of: /en(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
return .enEx(y, n)
}
if let m = page.wholeMatch(of: /on4unit(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /on4unit(\d+)/), let n = Int(m.1) {
return .on4Lesson(n) return .on4Lesson(n)
} }

View file

@ -625,6 +625,7 @@ struct SubjectIcon: View {
switch icon { switch icon {
case "de": GermanFlag(size: size) case "de": GermanFlag(size: size)
case "cz": CzechFlag(size: size) case "cz": CzechFlag(size: size)
case "uk": UkFlag(size: size)
default: default:
Image(systemName: subjectSymbol(icon)) Image(systemName: subjectSymbol(icon))
.font(.system(size: size * 0.72, weight: .semibold)) .font(.system(size: size * 0.72, weight: .semibold))

View file

@ -1,3 +1,4 @@
import AVFoundation
import SwiftUI import SwiftUI
struct DialogEx: View { struct DialogEx: View {
@ -1345,3 +1346,188 @@ struct PlotScreen: View {
} }
} }
private final class EnSpeaker: ObservableObject {
private let synth = AVSpeechSynthesizer()
func speak(_ text: String) {
if synth.isSpeaking { synth.stopSpeaking(at: .immediate) }
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
?? AVSpeechSynthesisVoice(language: "en-US")
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.9
synth.speak(utterance)
}
}
struct EngExScreen: View {
@ObservedObject var vm: AppModel
let year: Int
let id: Int
@StateObject private var speaker: EnSpeaker
@State private var readPicks: [Int]
@State private var listenPicks: [Int]
@State private var quizPicks: [Int]
@State private var gaps: [String]
@State private var essay = ""
@State private var played = false
@State private var showScript = false
@State private var revealed = false
@State private var showModel = false
@State private var fb = ("", "")
init(vm: AppModel, year: Int, id: Int) {
self.vm = vm
self.year = year
self.id = id
let lesson = vm.content.enLesson(year, id)
_readPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("readQuiz").count ?? 0))
_listenPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("listenQuiz").count ?? 0))
_quizPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("quiz").count ?? 0))
_gaps = State(initialValue: Array(repeating: "", count: lesson?.arr("gaps").count ?? 0))
_speaker = StateObject(wrappedValue: EnSpeaker())
}
var body: some View {
if let lesson = vm.content.enLesson(year, id) {
let p = vm.palette
ExerciseScaffold(vm: vm, title: vm.tr(lesson.str("titleKey")), sub: nil, action: vm.tr("check"), onAction: {
check(lesson)
}, feedback: fb.0, kind: fb.1) {
if let reading = lesson.strOrNull("reading") {
section("en_sec_read")
Text(reading).textSelection(.enabled)
choiceBlock(lesson.arr("readQuiz"), picks: $readPicks)
}
if let listening = lesson.strOrNull("listening") {
section("en_sec_listen")
Text(vm.tr("en_listen_note"))
.font(.subheadline)
.foregroundStyle(.secondary)
PillButton(label: vm.tr("en_play")) {
played = true
showScript = false
speaker.speak(listening)
let wait = max(2.5, Double(listening.split(separator: " ").count) * 0.42)
DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
showScript = true
}
}
if showScript {
Text(listening).textSelection(.enabled)
}
choiceBlock(lesson.arr("listenQuiz"), picks: $listenPicks)
}
let gapRows = lesson.arr("gaps")
if !gapRows.isEmpty {
section("en_sec_gap")
ForEach(gapRows.indices, id: \.self) { i in
Text("\(i + 1). \(gapRows[i].str("prompt"))")
.font(.body.weight(.semibold))
WordField(
value: Binding(get: { gaps[i] }, set: { gaps[i] = $0 }),
placeholder: "",
mark: revealed ? answerAccepts(normalizeAnswer(gaps[i]), gapRows[i].str("answers")) : nil,
palette: p
)
if revealed, let hint = gapRows[i].strOrNull("meaning") {
Meaning(text: hint, palette: p, visible: true)
}
}
}
if let writing = lesson.obj("writing") {
section("en_sec_write")
Text(writing.str("prompt"))
TextEditor(text: $essay)
.frame(minHeight: 140)
.padding(8)
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 12))
if showModel {
Text(vm.tr("en_model"))
.font(.subheadline.weight(.semibold))
Text(writing.str("model")).textSelection(.enabled)
}
}
let quiz = lesson.arr("quiz")
if !quiz.isEmpty {
section("en_sec_quiz")
choiceBlock(quiz, picks: $quizPicks)
}
}
}
}
private func section(_ key: String) -> some View {
Text(vm.tr(key))
.font(.title3.weight(.bold))
.padding(.top, 8)
}
private func choiceBlock(_ questions: [J], picks: Binding<[Int]>) -> some View {
let p = vm.palette
return ForEach(questions.indices, id: \.self) { i in
let q = questions[i]
VStack(alignment: .leading, spacing: 6) {
Text("\(i + 1). \(q.str("prompt"))")
.font(.body.weight(.semibold))
ForEach(Array(q.strs("options").enumerated()), id: \.offset) { o, opt in
let selected = picks.wrappedValue[i] == o
let mark: Bool? = revealed && selected ? picks.wrappedValue[i] == q.int("correct") : nil
OptionChip(text: opt, selected: selected, mark: mark) {
picks.wrappedValue[i] = o
}
}
if revealed, let expl = q.strOrNull("expl") {
Meaning(text: expl, palette: p, visible: true)
}
}
.padding(.bottom, 8)
}
}
private func check(_ lesson: J) {
if lesson.strOrNull("listening") != nil && !played {
fb = (vm.tr("en_listen_first"), "err")
return
}
let readOk = score(lesson.arr("readQuiz"), readPicks)
let listenOk = score(lesson.arr("listenQuiz"), listenPicks)
let quizOk = score(lesson.arr("quiz"), quizPicks)
let gapRows = lesson.arr("gaps")
let gapOk = gapRows.indices.filter {
answerAccepts(normalizeAnswer(gaps[$0]), gapRows[$0].str("answers"))
}.count
var writeOk = true
if let writing = lesson.obj("writing") {
let keys = writing.str("keys").split(separator: "|").map(String.init)
let enough = wordCount(essay) >= writing.int("minWords", 20)
writeOk = enough && keys.allSatisfy { hasWord(essay, $0) }
}
revealed = true
let all = readOk && listenOk && quizOk && gapOk == gapRows.count && writeOk
if all {
fb = (vm.tr("feedback_ok"), "ok")
showModel = true
vm.markEn(year, id)
} else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count {
fb = (vm.tr("en_write_short"), "err")
} else {
fb = (vm.tr("feedback_retry_short"), "err")
}
}
private func score(_ questions: [J], _ picks: [Int]) -> Bool {
questions.indices.allSatisfy { picks[$0] == questions[$0].int("correct") }
}
private func wordCount(_ text: String) -> Int {
let n = normalizeAnswer(text)
return n.isEmpty ? 0 : n.split(separator: " ").count
}
private func hasWord(_ text: String, _ word: String) -> Bool {
let n = " \(normalizeAnswer(text)) "
let w = normalizeAnswer(word)
return !w.isEmpty && n.contains(" \(w) ")
}
}

View file

@ -33,3 +33,27 @@ struct CzechFlag: View {
.frame(width: size, height: size) .frame(width: size, height: size)
} }
} }
struct UkFlag: View {
var size: CGFloat = 48
var body: some View {
Canvas { ctx, sz in
let w = sz.width
let h = sz.height
ctx.clip(to: Path(ellipseIn: CGRect(origin: .zero, size: sz)))
ctx.fill(Path(CGRect(origin: .zero, size: sz)), with: .color(Color(red: 0.01, green: 0.14, blue: 0.40)))
var saltire = Path()
saltire.move(to: .zero)
saltire.addLine(to: CGPoint(x: w, y: h))
saltire.move(to: CGPoint(x: w, y: 0))
saltire.addLine(to: CGPoint(x: 0, y: h))
ctx.stroke(saltire, with: .color(.white), lineWidth: w * 0.16)
ctx.stroke(saltire, with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)), lineWidth: w * 0.055)
ctx.fill(Path(CGRect(x: 0, y: h * 0.38, width: w, height: h * 0.24)), with: .color(.white))
ctx.fill(Path(CGRect(x: w * 0.38, y: 0, width: w * 0.24, height: h)), with: .color(.white))
ctx.fill(Path(CGRect(x: 0, y: h * 0.43, width: w, height: h * 0.14)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)))
ctx.fill(Path(CGRect(x: w * 0.43, y: 0, width: w * 0.14, height: h)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)))
}
.frame(width: size, height: size)
}
}

View file

@ -689,6 +689,11 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
case "hwyears", "hwmap": case "hwyears", "hwmap":
sum.totalEx = vm.content.hw.count sum.totalEx = vm.content.hw.count
sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count
case "enyears", "en1map", "en2map", "en3map", "en4map":
sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count }
sum.doneEx = (1...4).reduce(0) { acc, year in
acc + vm.content.enYear(year).filter { vm.progress.enDone(year, $0.int("id")) }.count
}
case "onyears", "onmap", "on2map", "on3map", "on4map": case "onyears", "onmap", "on2map", "on3map", "on4map":
sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + vm.content.on4.count sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + vm.content.on4.count
sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count
@ -905,6 +910,12 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik") add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik")
add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik") add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra: "rocnik") add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra: "rocnik")
for year in 1...4 {
add(vm.tr("en_year\(year)"), vm.tr("search_lesson"), "en\(year)map", extra: "anglictina english rocnik")
for l in vm.content.enYear(year) {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en\(year)unit\(l.int("id"))", extra: "anglictina english")
}
}
for l in vm.content.on { for l in vm.content.on {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex\(l.int("id"))") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex\(l.int("id"))")
@ -1247,6 +1258,40 @@ struct HwYearsScreen: View {
} }
} }
struct EnYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let years: [(String, String, Route)] = [
("en_year1", "en_y1_sub", .enMap(1)),
("en_year2", "en_y2_sub", .enMap(2)),
("en_year3", "en_y3_sub", .enMap(3)),
("en_year4", "en_y4_sub", .enMap(4)),
]
MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
NavigationLink(value: years[i].2) {
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
}
.buttonStyle(.plain)
}
.navigationTitle(vm.tr("English"))
.navigationSubtitle(vm.tr("en_years_sub"))
#else
List {
ForEach(1...4, id: \.self) { year in
NavigationLink(value: Route.enMap(year)) {
ListRowLabel(title: vm.tr("en_year\(year)"), subtitle: vm.tr("en_y\(year)_sub"))
}
}
}
.navigationTitle(vm.tr("English"))
.navigationSubtitle(vm.tr("en_years_sub"))
.appListStyle()
#endif
}
}
struct OnYearsScreen: View { struct OnYearsScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
@ -2183,6 +2228,29 @@ struct RouteDestination: View {
HwQuizScreen(vm: vm, id: id) HwQuizScreen(vm: vm, id: id)
case .onYears: case .onYears:
OnYearsScreen(vm: vm) OnYearsScreen(vm: vm)
case .enYears:
EnYearsScreen(vm: vm)
case .enMap(let year):
LessonListScreen(
vm: vm,
title: vm.tr("en_year\(year)"),
subtitle: vm.tr("en_y\(year)_sub"),
rows: vm.content.enYear(year).map { l in
let id = l.int("id")
return LessonRow(
id: "e\(year)\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.enDone(year, id),
destination: .enLesson(year, id)
)
}
)
case .enLesson(let year, let id):
if let l = vm.content.enLesson(year, id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .enEx(year, id))
}
case .enEx(let year, let id):
EngExScreen(vm: vm, year: year, id: id)
case .onMap: case .onMap:
LessonListScreen( LessonListScreen(
vm: vm, vm: vm,

View file

@ -776,7 +776,10 @@ def main() -> int:
arrays: dict[str, tuple[str, list]] = {} arrays: dict[str, tuple[str, list]] = {}
i18n = {} i18n = {}
for path in sorted(SRC.glob("*.c")): for path in sorted(SRC.glob("*.c")):
text = strip_comments(path.read_text(encoding="utf-8")) raw = path.read_text(encoding="utf-8")
if path.name == "english.c":
raw = expand_local_includes(raw, path.parent)
text = strip_comments(raw)
arrays.update(find_arrays(text)) arrays.update(find_arrays(text))
if path.name == "i18n.c": if path.name == "i18n.c":
i18n = parse_i18n(strip_comments(expand_local_includes(text, path.parent))) i18n = parse_i18n(strip_comments(expand_local_includes(text, path.parent)))
@ -1606,13 +1609,66 @@ def main() -> int:
ex_lines.append("") ex_lines.append("")
(SRC / "cetba_book_exercises.inc").write_text("\n".join(ex_lines) + "\n", encoding="utf-8") (SRC / "cetba_book_exercises.inc").write_text("\n".join(ex_lines) + "\n", encoding="utf-8")
def en_text(name):
if name not in arrays:
return None
raw = A(name)
if isinstance(raw, list):
parts = [x for x in raw if isinstance(x, str)]
return parts[0] if len(parts) == 1 else ("\n".join(parts) if parts else None)
return raw if isinstance(raw, str) else None
def en_choice(name):
if name not in arrays:
return []
quiz = choice_items(A(name))
hint_name = name + "_hints"
hints = str_list(A(hint_name)) if hint_name in arrays else []
for qi, q in enumerate(quiz):
if qi < len(hints) and hints[qi]:
q["expl"] = hints[qi]
return quiz
def en_year(year):
lessons = []
for i in range(1, 9):
prefix = f"en_y{year}_l{i}"
write = str_list(A(f"{prefix}_write")) if f"{prefix}_write" in arrays else []
writing = None
if len(write) >= 3:
writing = {
"prompt": write[0],
"model": write[1],
"keys": write[2],
"minWords": int(write[3]) if len(write) > 3 and str(write[3]).isdigit() else 20,
}
lessons.append({
"id": i,
"titleKey": f"en_y{year}_u{i}",
"subKey": f"en_y{year}_u{i}_sub",
"slides": slides_items(A(f"{prefix}_slides")),
"reading": en_text(f"{prefix}_read"),
"readQuiz": en_choice(f"{prefix}_readq"),
"listening": en_text(f"{prefix}_listen"),
"listenQuiz": en_choice(f"{prefix}_listenq"),
"gaps": typed_items(A(f"{prefix}_gaps")) if f"{prefix}_gaps" in arrays else [],
"writing": writing,
"quiz": en_choice(f"{prefix}_quiz"),
})
return lessons
en_lessons = en_year(1)
en2_lessons = en_year(2)
en3_lessons = en_year(3)
en4_lessons = en_year(4)
subjects = [ subjects = [
{"key": "Deutsch", "open": True, "target": "roadmap", "icon": "de"}, {"key": "Deutsch", "open": True, "target": "roadmap", "icon": "de"},
{"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"}, {"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"},
{"key": "Technické vybavení", "open": True, "target": "hwyears", "icon": "chip"}, {"key": "Technické vybavení", "open": True, "target": "hwyears", "icon": "chip"},
{"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"}, {"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"},
{"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"}, {"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"},
{"key": "English", "open": False, "target": None, "icon": "lock"}, {"key": "English", "open": True, "target": "enyears", "icon": "uk"},
{"key": "Matematika", "open": True, "target": "matyears", "icon": "function"}, {"key": "Matematika", "open": True, "target": "matyears", "icon": "function"},
{"key": "Fyzika", "open": True, "target": "fyzyears", "icon": "bolt"}, {"key": "Fyzika", "open": True, "target": "fyzyears", "icon": "bolt"},
{"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"}, {"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"},
@ -1651,6 +1707,10 @@ def main() -> int:
"on2": on2_lessons, "on2": on2_lessons,
"on3": on3_lessons, "on3": on3_lessons,
"on4": on4_lessons, "on4": on4_lessons,
"en": en_lessons,
"en2": en2_lessons,
"en3": en3_lessons,
"en4": en4_lessons,
"mluvnice": mluvnice, "mluvnice": mluvnice,
"books": books, "books": books,
"changelog": parse_changelog(ROOT / "data" / "changelog.txt"), "changelog": parse_changelog(ROOT / "data" / "changelog.txt"),

2537
scripts/gen-english.py Normal file

File diff suppressed because it is too large Load diff

1331
src/english.c Normal file

File diff suppressed because it is too large Load diff

86
src/english_i18n.inc Normal file
View file

@ -0,0 +1,86 @@
{"en_years_sub", "Čtyři ročníky podle osnov gymnázia, od A2 k maturitě.", "Four years of the gymnasium syllabus, from A2 to the exam."},
{"en_year1", "1. ročník", "Year 1"},
{"en_year2", "2. ročník", "Year 2"},
{"en_year3", "3. ročník", "Year 3"},
{"en_year4", "4. ročník", "Year 4"},
{"en_sec_read", "Čtení", "Reading"},
{"en_sec_listen", "Poslech", "Listening"},
{"en_sec_write", "Psaní", "Writing"},
{"en_sec_gap", "Doplňte", "Complete"},
{"en_sec_quiz", "Kvíz", "Quiz"},
{"en_sec_test", "Jazyková část", "Language"},
{"en_play", "Přehrát", "Play"},
{"en_listen_note", "Nejdřív si přečtěte otázky. Nahrávku přehraje hlas zařízení. Bez něj se věty ukážou postupně.", "Read the questions first. The device voice plays the recording. Without one, the sentences appear one by one."},
{"en_listen_first", "Nejdřív poslech přehrajte.", "Play the recording first."},
{"en_listen_fallback", "Hlas není dostupný, věty se ukazují postupně.", "No voice is available, so the sentences appear one by one."},
{"en_write_short", "Text je krátký nebo v něm chybí požadovaná slova.", "The text is short or it misses the required words."},
{"en_model", "Vzor", "Model"},
{"en_write_ph", "Pište anglicky…", "Write in English…"},
{"en_y1_sub", "Osobní život, škola, domov a minulost. Úroveň A2.", "Personal life, school, home and the past. Level A2."},
{"en_y1_u1", "Já a moje rodina", "Me and my family"},
{"en_y1_u1_sub", "Sloveso be, have got a přivlastňovací zájmena. Doplňování a kvíz.", "The verb be, have got and possessives. Gaps and a quiz."},
{"en_y1_u2", "Školní den", "A school day"},
{"en_y1_u2_sub", "Přítomný čas prostý. Čtení a doplňování.", "The present simple. Reading and gaps."},
{"en_y1_u3", "Víkend ve městě", "A weekend in town"},
{"en_y1_u3_sub", "Přítomný čas průběhový pro domluvené plány. Poslech.", "The present continuous for arrangements. Listening."},
{"en_y1_u4", "Můj pokoj", "My room"},
{"en_y1_u4_sub", "There is / there are a předložky místa. Krátký popis a kvíz.", "There is / there are and prepositions. A short description and a quiz."},
{"en_y1_u5", "Na trhu", "At the market"},
{"en_y1_u5_sub", "Počitatelnost, some a any, how much a how many. Čtení a doplňování.", "Countability, some and any, how much and how many. Reading and gaps."},
{"en_y1_u6", "Minulé léto", "Last summer"},
{"en_y1_u6_sub", "Minulý čas prostý, pravidelná i nepravidelná slovesa. Poslech a kvíz.", "The past simple, regular and irregular verbs. Listening and a quiz."},
{"en_y1_u7", "Zpráva kamarádovi", "A message to a friend"},
{"en_y1_u7_sub", "Will a going to. Krátká neformální zpráva a doplňování.", "Will and going to. A short informal message and gaps."},
{"en_y1_u8", "Test 1. ročníku", "Year 1 test"},
{"en_y1_u8_sub", "Čtení, poslech, gramatika a krátké psaní. Úroveň A2.", "Reading, listening, grammar and a short piece of writing. Level A2."},
{"en_y2_sub", "Cestování, zdraví, práce, média a příroda. Úroveň B1.", "Travel, health, work, the media and nature. Level B1."},
{"en_y2_u1", "Na cestách", "On the road"},
{"en_y2_u1_sub", "Stupňování a minulý čas průběhový. Doplňování a kvíz.", "Comparison and the past continuous. Gaps and a quiz."},
{"en_y2_u2", "U lékaře", "At the clinic"},
{"en_y2_u2_sub", "Should, must a have to. Čtení letáku a kvíz.", "Should, must and have to. A leaflet and a quiz."},
{"en_y2_u3", "První brigáda", "A first job"},
{"en_y2_u3_sub", "Předpřítomný čas, for a since. Poslech pohovoru a doplňování.", "The present perfect, for and since. A job interview and gaps."},
{"en_y2_u4", "Recenze filmu", "A film review"},
{"en_y2_u4_sub", "Vztažné věty who, which a that. Krátká recenze a kvíz.", "Relative clauses with who, which and that. A short review and a quiz."},
{"en_y2_u5", "Počasí a výlet", "Weather and a trip"},
{"en_y2_u5_sub", "Too a enough, will a going to v předpovědi. Čtení a doplňování.", "Too and enough, will and going to in a forecast. Reading and gaps."},
{"en_y2_u6", "Festival", "A festival"},
{"en_y2_u6_sub", "Předpřítomný čas proti minulému. Poslech a kvíz.", "Present perfect versus past simple. Listening and a quiz."},
{"en_y2_u7", "Kdyby", "If I could"},
{"en_y2_u7_sub", "První a druhá podmínková věta. Psaní a doplňování.", "First and second conditionals. Writing and gaps."},
{"en_y2_u8", "Test 2. ročníku", "Year 2 test"},
{"en_y2_u8_sub", "Čtení, poslech, gramatika B1 a krátké psaní.", "Reading, listening, B1 grammar and a short piece of writing."},
{"en_y3_sub", "Společnost, prostředí, média a složitější gramatika. Úroveň B1 až B2.", "Society, the environment, the media and harder grammar. Level B1 to B2."},
{"en_y3_u1", "Jak se to vyrábí", "How things are made"},
{"en_y3_u1_sub", "Trpný rod v přítomnosti a minulosti. Doplňování a kvíz.", "The present and past passive. Gaps and a quiz."},
{"en_y3_u2", "Škola, o které se mluví", "A school people talk about"},
{"en_y3_u2_sub", "Nepřímá řeč oznamovací. Čtení a kvíz.", "Reported statements. Reading and a quiz."},
{"en_y3_u3", "Poslední buk", "The last beech"},
{"en_y3_u3_sub", "Podmínky a slovník prostředí. Poslech podcastu a doplňování.", "Conditionals and environment vocabulary. A podcast and gaps."},
{"en_y3_u4", "Pro a proti", "For and against"},
{"en_y3_u4_sub", "Stavba úvahy a spojovací výrazy. Esej a kvíz.", "Essay structure and linking words. An essay and a quiz."},
{"en_y3_u5", "Kdo to musel být", "It must have been"},
{"en_y3_u5_sub", "Modální slovesa dedukce. Čtení záhady a kvíz.", "Modals of deduction. A short mystery and a quiz."},
{"en_y3_u6", "Zprávy dne", "The news desk"},
{"en_y3_u6_sub", "Nepřímé otázky. Poslech rozhovoru a kvíz.", "Reported questions. An interview and a quiz."},
{"en_y3_u7", "Frázová slovesa v příběhu", "Phrasal verbs in a story"},
{"en_y3_u7_sub", "Běžná frázová slovesa. Doplňování a kvíz.", "Common phrasal verbs. Gaps and a quiz."},
{"en_y3_u8", "Test 3. ročníku", "Year 3 test"},
{"en_y3_u8_sub", "Čtení, poslech, gramatika B1+ a krátká úvaha.", "Reading, listening, B1+ grammar and a short argument."},
{"en_y4_sub", "Maturitní dovednosti: mluvení, formální psaní, čtení, poslech a gramatika. Úroveň B2.", "Exam skills: speaking, formal writing, reading, listening and grammar. Level B2."},
{"en_y4_u1", "Maturitní kartička", "The speaking card"},
{"en_y4_u1_sub", "Strategie ústní zkoušky a kolokace osobních témat. Kvíz a doplňování.", "Speaking-exam strategy and personal-topic collocations. A quiz and gaps."},
{"en_y4_u2", "Formální e-mail", "A formal email"},
{"en_y4_u2_sub", "Žádost a stížnost. Psaní a kvíz rejstříku.", "A request and a complaint. Writing and a register quiz."},
{"en_y4_u3", "Noční vlak", "The night train"},
{"en_y4_u3_sub", "Delší čtení ve stylu didaktického testu. Čtyři otázky k článku.", "A longer reading in the style of the written paper. Four questions."},
{"en_y4_u4", "Tábor ve Walesu", "A camp in Wales"},
{"en_y4_u4_sub", "Delší poslech: pohovor. Pět otázek.", "A longer listening: an interview. Five questions."},
{"en_y4_u5", "Názorová esej", "An opinion essay"},
{"en_y4_u5_sub", "Struktura názoru. Esej a kvíz o odstavcích.", "Opinion-essay structure. An essay and a quiz about paragraphs."},
{"en_y4_u6", "Slovní zásoba okruhů", "Topic vocabulary"},
{"en_y4_u6_sub", "Kolokace k maturitním tématům. Doplňování a kvíz.", "Collocations for the exam topics. Gaps and a quiz."},
{"en_y4_u7", "Gramatická ambulance", "Grammar clinic"},
{"en_y4_u7_sub", "Smíšené jevy k didaktickému testu. Kvíz a doplňování.", "Mixed points for the language paper. A quiz and gaps."},
{"en_y4_u8", "Cvičná maturita", "A practice paper"},
{"en_y4_u8_sub", "Čtení, poslech, jazyková kompetence a krátká písemná práce.", "Reading, listening, language in use and a short piece of writing."},

1907
src/english_lessons.inc Normal file

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@
#define HW_SUBJ 2 /* index of "Technické vybavení" */ #define HW_SUBJ 2 /* index of "Technické vybavení" */
#define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */
#define ON_SUBJ 4 /* index of "Občanská nauka" */ #define ON_SUBJ 4 /* index of "Občanská nauka" */
#define EN_SUBJ 5 /* index of "English" */
#define MATH_SUBJ 6 /* index of "Matematika" */ #define MATH_SUBJ 6 /* index of "Matematika" */
#define PHY_SUBJ 7 /* index of "Fyzika" */ #define PHY_SUBJ 7 /* index of "Fyzika" */
#define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */ #define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */
@ -56,6 +57,7 @@ extern char *app_progress_on;
extern char *app_progress_on2; extern char *app_progress_on2;
extern char *app_progress_on3; extern char *app_progress_on3;
extern char *app_progress_on4; extern char *app_progress_on4;
extern char *app_progress_en;
extern char *app_progress_mluvnice; extern char *app_progress_mluvnice;
extern char *app_progress_cetba; extern char *app_progress_cetba;
extern char *app_progress_lit; extern char *app_progress_lit;
@ -84,6 +86,7 @@ extern char *app_progress_mat4;
#define PROGRESS_ON2 app_progress_on2 #define PROGRESS_ON2 app_progress_on2
#define PROGRESS_ON3 app_progress_on3 #define PROGRESS_ON3 app_progress_on3
#define PROGRESS_ON4 app_progress_on4 #define PROGRESS_ON4 app_progress_on4
#define PROGRESS_EN app_progress_en
#define NODE_SIZE 88.0 #define NODE_SIZE 88.0
#define PATH_SPAC 240.0 #define PATH_SPAC 240.0
#define ROAD_MX 150.0 /* horizontal canvas margin */ #define ROAD_MX 150.0 /* horizontal canvas margin */
@ -903,6 +906,11 @@ void on_lessons_apply_lang(void);
GtkWidget *build_onyears_page(void); GtkWidget *build_onyears_page(void);
GtkWidget *build_onmap_page(void); GtkWidget *build_onmap_page(void);
void add_on_pages(GtkStack *stack); void add_on_pages(GtkStack *stack);
void en_load_progress(void);
void progress_for_en(ProgressSum *out);
void en_rail_theme_reset(void);
GtkWidget *build_enyears_page(void);
void add_en_pages(GtkStack *stack);
void on2_load_progress(void); void on2_load_progress(void);
void on2_save_progress(void); void on2_save_progress(void);
void mark_on2_done(int lesson_id); void mark_on2_done(int lesson_id);
@ -1117,6 +1125,8 @@ void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_uk_flag(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data);
void draw_book_icon(GtkDrawingArea *area, cairo_t *cr, void draw_book_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_book_badge_icon(GtkDrawingArea *area, cairo_t *cr, void draw_book_badge_icon(GtkDrawingArea *area, cairo_t *cr,

View file

@ -271,7 +271,7 @@ const TrEntry tr_ui[] = {
{"Český jazyk a literatura", "Český jazyk a literatura", {"Český jazyk a literatura", "Český jazyk a literatura",
"Czech Language and Literature"}, "Czech Language and Literature"},
{"Občanská nauka", "Občanská nauka", "Civics"}, {"Občanská nauka", "Občanská nauka", "Civics"},
{"English", "English", "English"}, {"English", "Angličtina", "English"},
{"Deutsch", "Deutsch", "Deutsch"}, {"Deutsch", "Deutsch", "Deutsch"},
{"Matematika", "Matematika", "Mathematics"}, {"Matematika", "Matematika", "Mathematics"},
{"Fyzika", "Fyzika", "Physics"}, {"Fyzika", "Fyzika", "Physics"},
@ -1619,6 +1619,7 @@ const TrEntry tr_ui[] = {
{"lit_fuks_plot_title", "Sestavte děj", "Order the plot"}, {"lit_fuks_plot_title", "Sestavte děj", "Order the plot"},
{"lit_fuks_plot_sub", "Přetáhněte části příběhu do správného pořadí.", {"lit_fuks_plot_sub", "Přetáhněte části příběhu do správného pořadí.",
"Drag the story parts into the correct order."}, "Drag the story parts into the correct order."},
#include "english_i18n.inc"
#include "math_i18n.inc" #include "math_i18n.inc"
{NULL, NULL, NULL} {NULL, NULL, NULL}
}; };
@ -2601,6 +2602,7 @@ void on_lessons_apply_lang(void);
void on2_lessons_apply_lang(void); void on2_lessons_apply_lang(void);
void on3_lessons_apply_lang(void); void on3_lessons_apply_lang(void);
void on4_lessons_apply_lang(void); void on4_lessons_apply_lang(void);
void en_lessons_apply_lang(void);
void lit_lessons_apply_lang(void); void lit_lessons_apply_lang(void);
void sci_lessons_apply_lang(void); void sci_lessons_apply_lang(void);
void fyz_lessons_apply_lang(void); void fyz_lessons_apply_lang(void);
@ -2623,6 +2625,7 @@ void apply_language(void) {
on2_lessons_apply_lang(); on2_lessons_apply_lang();
on3_lessons_apply_lang(); on3_lessons_apply_lang();
on4_lessons_apply_lang(); on4_lessons_apply_lang();
en_lessons_apply_lang();
lit_lessons_apply_lang(); lit_lessons_apply_lang();
sci_lessons_apply_lang(); sci_lessons_apply_lang();
fyz_lessons_apply_lang(); fyz_lessons_apply_lang();

View file

@ -416,6 +416,8 @@ static gboolean run_smoke_test(gpointer data) {
"on2unit1", "on2ex1", "on2unit16", "on2ex16", "on2unit1", "on2ex1", "on2unit16", "on2ex16",
"on3unit1", "on3ex1", "on3unit16", "on3ex16", "on3unit1", "on3ex1", "on3unit16", "on3ex16",
"on4unit1", "on4ex1", "on4unit16", "on4ex16", "on4unit1", "on4ex1", "on4unit16", "on4ex16",
"enyears", "en1map", "en4map",
"en1unit1", "en1ex1", "en4unit8", "en4ex8",
"unit1", "unit2", "unit3", "unit1", "unit2", "unit3",
"u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15",
"u1vocab", "u2vocab", "u3vocab", "u1vocab", "u2vocab", "u3vocab",
@ -534,6 +536,8 @@ void activate(GtkApplication *app, gpointer user_data) {
gtk_stack_add_named(main_stack, build_on3map_page(), "on3map"); gtk_stack_add_named(main_stack, build_on3map_page(), "on3map");
add_on3_pages(main_stack); add_on3_pages(main_stack);
gtk_stack_add_named(main_stack, build_on4map_page(), "on4map"); gtk_stack_add_named(main_stack, build_on4map_page(), "on4map");
gtk_stack_add_named(main_stack, build_enyears_page(), "enyears");
add_en_pages(main_stack);
add_on4_pages(main_stack); add_on4_pages(main_stack);
gtk_stack_add_named(main_stack, build_matyears_page(), "matyears"); gtk_stack_add_named(main_stack, build_matyears_page(), "matyears");
gtk_stack_add_named(main_stack, build_mat0map_page(), "mat0map"); gtk_stack_add_named(main_stack, build_mat0map_page(), "mat0map");

View file

@ -58,6 +58,7 @@ void load_progress(void) {
on2_load_progress(); on2_load_progress();
on3_load_progress(); on3_load_progress();
on4_load_progress(); on4_load_progress();
en_load_progress();
lit_load_progress(); lit_load_progress();
sci_load_progress(); sci_load_progress();
fyz_load_progress(); fyz_load_progress();

View file

@ -23,7 +23,8 @@ static GPtrArray *search_items;
static gboolean subject_open(int i) { static gboolean subject_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ
|| i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; || i == ON_SUBJ || i == EN_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ
|| i == SCI_SUBJ;
} }
static const char *subject_page(int i) { static const char *subject_page(int i) {
@ -37,6 +38,8 @@ static const char *subject_page(int i) {
return "czechmap"; return "czechmap";
if (i == ON_SUBJ) if (i == ON_SUBJ)
return "onyears"; return "onyears";
if (i == EN_SUBJ)
return "enyears";
if (i == MATH_SUBJ) if (i == MATH_SUBJ)
return "matyears"; return "matyears";
if (i == PHY_SUBJ) if (i == PHY_SUBJ)
@ -241,6 +244,21 @@ static void catalog_rebuild(void) {
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
} }
catalog_add_key("en_year1", "search_lesson", "en1map", FALSE, 1, "anglictina english");
catalog_add_key("en_year2", "search_lesson", "en2map", FALSE, 1, "anglictina english");
catalog_add_key("en_year3", "search_lesson", "en3map", FALSE, 1, "anglictina english");
catalog_add_key("en_year4", "search_lesson", "en4map", FALSE, 1, "anglictina english");
for (int y = 1; y <= 4; y++) {
for (i = 1; i <= 8; i++) {
g_snprintf(key, sizeof(key), "en_y%d_u%d", y, i);
g_snprintf(page, sizeof(page), "en%dunit%d", y, i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "anglictina english");
g_snprintf(page, sizeof(page), "en%dex%d", y, i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3,
"poslech cteni psani kviz");
}
}
catalog_add_key("Český jazyk a literatura", "search_subject", catalog_add_key("Český jazyk a literatura", "search_subject",
"czechmap", FALSE, 0, "cestina czech"); "czechmap", FALSE, 0, "cestina czech");
catalog_add_key("Literatura", "search_lesson", "lityears", FALSE, 1, catalog_add_key("Literatura", "search_lesson", "lityears", FALSE, 1,

View file

@ -71,6 +71,8 @@ void refresh_stats_ui(void) {
progress_for_hw(&all); progress_for_hw(&all);
else if (s == ON_SUBJ) else if (s == ON_SUBJ)
progress_for_on(&all); progress_for_on(&all);
else if (s == EN_SUBJ)
progress_for_en(&all);
else if (s == CZ_SUBJ) { else if (s == CZ_SUBJ) {
progress_for_mluvnice(&all); progress_for_mluvnice(&all);
progress_for_lit(&all); progress_for_lit(&all);
@ -110,7 +112,8 @@ void refresh_stats_ui(void) {
GtkWidget *bar = stats_ui.subj[s].bar; GtkWidget *bar = stats_ui.subj[s].bar;
ProgressSum sp = {0}; ProgressSum sp = {0};
gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ) gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ)
|| (s == ON_SUBJ) || (s == CZ_SUBJ) || (s == ON_SUBJ) || (s == EN_SUBJ)
|| (s == CZ_SUBJ)
|| (s == SCI_SUBJ) || (s == PHY_SUBJ) || (s == SCI_SUBJ) || (s == PHY_SUBJ)
|| (s == MATH_SUBJ) || (s == MATH_SUBJ)
|| sub_unit_count[s] > 0; || sub_unit_count[s] > 0;
@ -123,6 +126,8 @@ void refresh_stats_ui(void) {
progress_for_hw(&sp); progress_for_hw(&sp);
else if (s == ON_SUBJ) else if (s == ON_SUBJ)
progress_for_on(&sp); progress_for_on(&sp);
else if (s == EN_SUBJ)
progress_for_en(&sp);
else if (s == CZ_SUBJ) { else if (s == CZ_SUBJ) {
progress_for_mluvnice(&sp); progress_for_mluvnice(&sp);
progress_for_lit(&sp); progress_for_lit(&sp);

View file

@ -5,9 +5,8 @@
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ
* (Czech), ON_SUBJ (civics), MATH_SUBJ (mathematics), PHY_SUBJ * (Czech), ON_SUBJ (civics), EN_SUBJ (English), MATH_SUBJ (mathematics),
* (physics) and SCI_SUBJ * PHY_SUBJ (physics) and SCI_SUBJ (natural sciences) are unlocked. */
* (natural sciences) are unlocked. */
const char *sub_keys[NUM_SUBJECTS] = { const char *sub_keys[NUM_SUBJECTS] = {
"Deutsch", "Deutsch",
"Správa počítačových sítí", "Správa počítačových sítí",
@ -156,7 +155,8 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
static gboolean subject_is_open(int i) { static gboolean subject_is_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ
|| i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; || i == ON_SUBJ || i == EN_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ
|| i == SCI_SUBJ;
} }
static GtkWidget *subject_icon(int i) { static GtkWidget *subject_icon(int i) {
@ -179,6 +179,9 @@ static GtkWidget *subject_icon(int i) {
else if (i == ON_SUBJ) else if (i == ON_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_civics_icon, NULL, NULL); draw_civics_icon, NULL, NULL);
else if (i == EN_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_uk_flag, NULL, NULL);
else if (i == MATH_SUBJ) else if (i == MATH_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_math_icon, NULL, NULL); draw_math_icon, NULL, NULL);
@ -204,6 +207,8 @@ static const char *subject_target(int i) {
return "czechmap"; return "czechmap";
if (i == ON_SUBJ) if (i == ON_SUBJ)
return "onyears"; return "onyears";
if (i == EN_SUBJ)
return "enyears";
if (i == MATH_SUBJ) if (i == MATH_SUBJ)
return "matyears"; return "matyears";
if (i == PHY_SUBJ) if (i == PHY_SUBJ)

View file

@ -430,6 +430,7 @@ void apply_theme(void) {
on2_rail_theme_reset(); on2_rail_theme_reset();
on3_rail_theme_reset(); on3_rail_theme_reset();
on4_rail_theme_reset(); on4_rail_theme_reset();
en_rail_theme_reset();
czech_rail_theme_reset(); czech_rail_theme_reset();
lit_rail_theme_reset(); lit_rail_theme_reset();
sci_rail_theme_reset(); sci_rail_theme_reset();

View file

@ -24,6 +24,7 @@ char *app_progress_on;
char *app_progress_on2; char *app_progress_on2;
char *app_progress_on3; char *app_progress_on3;
char *app_progress_on4; char *app_progress_on4;
char *app_progress_en;
char *app_progress_mluvnice; char *app_progress_mluvnice;
char *app_progress_cetba; char *app_progress_cetba;
char *app_progress_lit; char *app_progress_lit;
@ -181,6 +182,7 @@ static void set_progress_paths(const char *write_root) {
g_free(app_progress_on2); g_free(app_progress_on2);
g_free(app_progress_on3); g_free(app_progress_on3);
g_free(app_progress_on4); g_free(app_progress_on4);
g_free(app_progress_en);
g_free(app_progress_mluvnice); g_free(app_progress_mluvnice);
g_free(app_progress_cetba); g_free(app_progress_cetba);
g_free(app_progress_lit); g_free(app_progress_lit);
@ -210,6 +212,7 @@ static void set_progress_paths(const char *write_root) {
app_progress_on2 = g_build_filename(app_progress_dir, "on2.conf", NULL); app_progress_on2 = g_build_filename(app_progress_dir, "on2.conf", NULL);
app_progress_on3 = g_build_filename(app_progress_dir, "on3.conf", NULL); app_progress_on3 = g_build_filename(app_progress_dir, "on3.conf", NULL);
app_progress_on4 = g_build_filename(app_progress_dir, "on4.conf", NULL); app_progress_on4 = g_build_filename(app_progress_dir, "on4.conf", NULL);
app_progress_en = g_build_filename(app_progress_dir, "english.conf", NULL);
app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL); app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL);
app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL); app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL);
app_progress_lit = g_build_filename(app_progress_dir, "lit.conf", NULL); app_progress_lit = g_build_filename(app_progress_dir, "lit.conf", NULL);