Add four years of German listening, reading and writing beside the textbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-07 19:30:00 +02:00
commit b822c57875
31 changed files with 3589 additions and 102 deletions

View file

@ -61,8 +61,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
### Co v aplikaci je ### Co v aplikaci je
- **Deutsch** — 3 otevřené jednotky (Neue Freunde, Aus aller Welt, Bei uns zu - **Deutsch** — učebnice (Neue Freunde, Aus aller Welt, Bei uns zu Hause)
Hause): cvičení, nápovědy, vokabeltraining a 4 ročníky, 24 lekcí od A1 k B1: poslech, čtení, psaní, doplňování a kvízy
- **Správa počítačových sítí** — 1. ročník, 27 lekcí (VLSM + teorie a kvízy); - **Správa počítačových sítí** — 1. ročník, 27 lekcí (VLSM + teorie a kvízy);
ročníky 2–4 jsou zamčené ročníky 2–4 jsou zamčené
- **Technické vybavení** — 1. ročník, 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení); ročníky 2–4 jsou zamčené - **Technické vybavení** — 1. ročník, 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení); ročníky 2–4 jsou zamčené
@ -266,8 +266,8 @@ Intel Mac, another architecture, or hacking on the code →
### What’s inside ### What’s inside
- **Deutsch** — 3 open units (Neue Freunde, Aus aller Welt, Bei uns zu Hause): - **Deutsch** — the textbook (Neue Freunde, Aus aller Welt, Bei uns zu Hause)
exercises, hints, vocabulary training plus 4 years, 24 lessons from A1 to B1: listening, reading, writing, gap-fill and quizzes
- **Computer networks** — year 1, 27 lessons (VLSM + theory/quizzes); - **Computer networks** — year 1, 27 lessons (VLSM + theory/quizzes);
years 2–4 are locked years 2–4 are locked
- **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked - **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked

View file

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

View file

@ -1,6 +1,7 @@
package org.maturita.maturita package org.maturita.maturita
import android.os.Bundle import android.os.Bundle
import java.util.Locale
import android.util.TypedValue import android.util.TypedValue
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
@ -50,6 +51,7 @@ 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.EnYearsScreen
import org.maturita.maturita.ui.DeHomeScreen
import org.maturita.maturita.ui.EngExScreen 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
@ -206,6 +208,23 @@ private fun MaturitaAppBody(vm: AppViewModel) {
Route.Welcome, Route.Subjects -> HomeScreen(vm) Route.Welcome, Route.Subjects -> HomeScreen(vm)
Route.Stats -> StatsScreen(vm) Route.Stats -> StatsScreen(vm)
Route.Settings -> SettingsScreen(vm) Route.Settings -> SettingsScreen(vm)
Route.DeHome -> DeHomeScreen(vm)
is Route.DeMap -> LessonListScreen(
vm, vm.tr("de_year${r.year}"), vm.tr("de_y${r.year}_sub"),
vm.content.deYear(r.year).map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.deDone(r.year, id), Route.DeLesson(r.year, id))
},
)
is Route.DeLesson -> {
val l = vm.content.deLesson(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.DeEx(r.year, r.id))
}
}
is Route.DeEx -> EngExScreen(
vm, r.year, r.id, Locale.GERMANY, vm.content.deLesson(r.year, r.id),
) { vm.markDe(r.year, r.id) }
Route.Roadmap -> RoadmapScreen(vm) Route.Roadmap -> RoadmapScreen(vm)
is Route.UnitMap -> UnitMapScreen(vm, r.unitId) is Route.UnitMap -> UnitMapScreen(vm, r.unitId)
is Route.GermanEx -> GermanExercise(vm, r.unitId, r.ex) is Route.GermanEx -> GermanExercise(vm, r.unitId, r.ex)

View file

@ -6,6 +6,10 @@ sealed class Route(val page: String) {
data object Stats : Route("stats") data object Stats : Route("stats")
data object Settings : Route("settings") data object Settings : Route("settings")
data object Roadmap : Route("roadmap") data object Roadmap : Route("roadmap")
data object DeHome : Route("dehome")
data class DeMap(val year: Int) : Route("de${year}map")
data class DeLesson(val year: Int, val id: Int) : Route("de${year}unit$id")
data class DeEx(val year: Int, val id: Int) : Route("de${year}ex$id")
data class UnitMap(val unitId: Int) : Route("unit${unitId + 1}") data class UnitMap(val unitId: Int) : Route("unit${unitId + 1}")
data class GermanEx(val unitId: Int, val ex: Int) : Route("u${unitId + 1}e$ex") data class GermanEx(val unitId: Int, val ex: Int) : Route("u${unitId + 1}e$ex")
data class Vocab(val unitId: Int) : Route("u${unitId + 1}vocab") data class Vocab(val unitId: Int) : Route("u${unitId + 1}vocab")
@ -97,7 +101,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 EnLesson || this is EnEx || 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 DeLesson || this is DeEx || 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 ||
@ -113,10 +117,14 @@ sealed class Route(val page: String) {
Welcome, Subjects -> listOf(Subjects) Welcome, Subjects -> listOf(Subjects)
Stats -> listOf(Stats) Stats -> listOf(Stats)
Settings -> listOf(Settings) Settings -> listOf(Settings)
Roadmap -> listOf(Subjects, Roadmap) DeHome -> listOf(Subjects, DeHome)
is UnitMap -> listOf(Subjects, Roadmap, this) is DeMap -> listOf(Subjects, DeHome, this)
is GermanEx -> listOf(Subjects, Roadmap, UnitMap(unitId), this) is DeLesson -> listOf(Subjects, DeHome, DeMap(year), this)
is Vocab -> listOf(Subjects, Roadmap, UnitMap(unitId), this) is DeEx -> listOf(Subjects, DeHome, DeMap(year), DeLesson(year, id), this)
Roadmap -> listOf(Subjects, DeHome, Roadmap)
is UnitMap -> listOf(Subjects, DeHome, Roadmap, this)
is GermanEx -> listOf(Subjects, DeHome, Roadmap, UnitMap(unitId), this)
is Vocab -> listOf(Subjects, DeHome, Roadmap, UnitMap(unitId), this)
NetYears -> listOf(Subjects, NetYears) NetYears -> listOf(Subjects, NetYears)
NetMap -> listOf(Subjects, NetYears, NetMap) NetMap -> listOf(Subjects, NetYears, NetMap)
is NetLesson -> listOf(Subjects, NetYears, NetMap, this) is NetLesson -> listOf(Subjects, NetYears, NetMap, this)
@ -207,6 +215,7 @@ fun routeFromPage(page: String): Route? = when (page) {
"stats" -> Route.Stats "stats" -> Route.Stats
"settings" -> Route.Settings "settings" -> Route.Settings
"roadmap" -> Route.Roadmap "roadmap" -> Route.Roadmap
"dehome" -> Route.DeHome
"netyears" -> Route.NetYears "netyears" -> Route.NetYears
"netmap" -> Route.NetMap "netmap" -> Route.NetMap
"hwyears" -> Route.HwYears "hwyears" -> Route.HwYears
@ -357,6 +366,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("""de(\d)map""").matchEntire(page)?.let {
return Route.DeMap(it.groupValues[1].toInt())
}
Regex("""de(\d)unit(\d+)""").matchEntire(page)?.let {
return Route.DeLesson(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""de(\d)ex(\d+)""").matchEntire(page)?.let {
return Route.DeEx(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""en(\d)map""").matchEntire(page)?.let { Regex("""en(\d)map""").matchEntire(page)?.let {
return Route.EnMap(it.groupValues[1].toInt()) return Route.EnMap(it.groupValues[1].toInt())
} }

View file

@ -37,6 +37,10 @@ class Content(root: JSONObject) {
val en2 = raw.arr("en2") val en2 = raw.arr("en2")
val en3 = raw.arr("en3") val en3 = raw.arr("en3")
val en4 = raw.arr("en4") val en4 = raw.arr("en4")
val de = raw.arr("de")
val de2 = raw.arr("de2")
val de3 = raw.arr("de3")
val de4 = raw.arr("de4")
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")
@ -70,6 +74,13 @@ class Content(root: JSONObject) {
else -> en else -> en
} }
fun enLesson(year: Int, id: Int) = enYear(year).firstOrNull { it.int("id") == id } fun enLesson(year: Int, id: Int) = enYear(year).firstOrNull { it.int("id") == id }
fun deYear(year: Int) = when (year) {
2 -> de2
3 -> de3
4 -> de4
else -> de
}
fun deLesson(year: Int, id: Int) = deYear(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

@ -46,6 +46,8 @@ class ProgressStore(context: Context) {
fun enDone(year: Int, id: Int) = prefs.getBoolean("en.$year.$id", false) 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 markEn(year: Int, id: Int) { prefs.edit().putBoolean("en.$year.$id", true).apply() }
fun deDone(year: Int, id: Int) = prefs.getBoolean("de.$year.$id", false)
fun markDe(year: Int, id: Int) { prefs.edit().putBoolean("de.$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() }
@ -158,6 +160,14 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
content.deYear(year).forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.deDone(year, l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
} }
content.lit.forEach { l -> content.lit.forEach { l ->
sum.totalEx += 1 sum.totalEx += 1

View file

@ -1382,8 +1382,15 @@ fun PlotScreen(vm: AppViewModel, id: String) {
} }
@Composable @Composable
fun EngExScreen(vm: AppViewModel, year: Int, id: Int) { fun EngExScreen(
val lesson = vm.content.enLesson(year, id) ?: return vm: AppViewModel,
year: Int,
id: Int,
locale: Locale = Locale.UK,
spec: J? = vm.content.enLesson(year, id),
onDone: () -> Unit = { vm.markEn(year, id) },
) {
val lesson = spec ?: return
val readQ = lesson.arr("readQuiz") val readQ = lesson.arr("readQuiz")
val listenQ = lesson.arr("listenQuiz") val listenQ = lesson.arr("listenQuiz")
val quiz = lesson.arr("quiz") val quiz = lesson.arr("quiz")
@ -1410,7 +1417,7 @@ fun EngExScreen(vm: AppViewModel, year: Int, id: Int) {
DisposableEffect(context) { DisposableEffect(context) {
var engine: TextToSpeech? = null var engine: TextToSpeech? = null
engine = TextToSpeech(context) { status -> engine = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) engine?.language = Locale.UK if (status == TextToSpeech.SUCCESS) engine?.language = locale
} }
tts = engine tts = engine
onDispose { onDispose {
@ -1447,7 +1454,7 @@ fun EngExScreen(vm: AppViewModel, year: Int, id: Int) {
fb = when { fb = when {
all -> { all -> {
showModel = true showModel = true
vm.markEn(year, id) onDone()
vm.tr("feedback_ok") to "ok" vm.tr("feedback_ok") to "ok"
} }
writing != null && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.size -> writing != null && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.size ->
@ -1472,7 +1479,7 @@ fun EngExScreen(vm: AppViewModel, year: Int, id: Int) {
val start = index.coerceIn(0, listening.length) val start = index.coerceIn(0, listening.length)
val id = "en-${System.nanoTime()}" val id = "en-${System.nanoTime()}"
utterance = id utterance = id
engine.language = Locale.UK engine.language = locale
engine.setSpeechRate(speed) engine.setSpeechRate(speed)
engine.setOnUtteranceProgressListener(object : UtteranceProgressListener() { engine.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {} override fun onStart(utteranceId: String?) {}

View file

@ -294,10 +294,16 @@ private fun Metric(label: String, value: String, modifier: Modifier) {
private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
val sum = ProgressSum() val sum = ProgressSum()
when (s.str("target")) { when (s.str("target")) {
"roadmap" -> vm.content.german.filter { it.bool("unlocked") }.forEach { u -> "dehome" -> {
val n = u.strs("names").size vm.content.german.filter { it.bool("unlocked") }.forEach { u ->
sum.totalEx += n val n = u.strs("names").size
sum.doneEx += (1..n).count { vm.progress.germanDone(u.int("id"), it) } sum.totalEx += n
sum.doneEx += (1..n).count { vm.progress.germanDone(u.int("id"), it) }
}
sum.totalEx += (1..4).sumOf { vm.content.deYear(it).size }
sum.doneEx += (1..4).sumOf { year ->
vm.content.deYear(year).count { vm.progress.deDone(year, it.int("id")) }
}
} }
"netyears" -> { "netyears" -> {
sum.totalEx = vm.content.netLessons.size sum.totalEx = vm.content.netLessons.size
@ -558,6 +564,11 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
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_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(l.str("titleKey")), vm.tr("search_exercise"), "en${year}ex${l.int("id")}")
} }
add(vm.tr("de_year$year"), vm.tr("search_lesson"), "de${year}map", extra = "nemcina deutsch rocnik")
vm.content.deYear(year).forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "de${year}unit${l.int("id")}", extra = "nemcina deutsch")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "de${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")
@ -735,6 +746,28 @@ fun HwYearsScreen(vm: AppViewModel) {
} }
} }
@Composable
@Composable
fun DeHomeScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Deutsch"), vm.tr("de_home_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
item {
NavRow(title = vm.tr("de_book_title"), subtitle = vm.tr("de_book_sub"), leading = { Text("U", style = MaterialTheme.typography.titleMedium) }) {
vm.go(Route.Roadmap)
}
}
items(4) { index ->
val year = index + 1
NavRow(
title = vm.tr("de_year$year"),
subtitle = vm.tr("de_y${year}_sub"),
leading = { Text("$year.", style = MaterialTheme.typography.titleMedium) },
) { vm.go(Route.DeMap(year)) }
}
}
}
}
@Composable @Composable
fun EnYearsScreen(vm: AppViewModel) { fun EnYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("English"), vm.tr("en_years_sub")) { inner -> DetailScaffold(vm, vm.tr("English"), vm.tr("en_years_sub")) { inner ->

View file

@ -6,6 +6,12 @@
# cs: bullet # cs: bullet
# en: bullet # en: bullet
--- 2026-10-07
cs: Němčina: čtyři ročníky, 24 lekcí s poslechem, čtením a psaním
cs: Učebnice Neue Freunde zůstává, k ní přibyl kurz od A1 k B1
en: German: four years, 24 lessons with listening, reading and writing
en: The Neue Freunde textbook stays, and a course from A1 to B1 sits beside it
--- 2026-10-07 --- 2026-10-07
cs: Angličtina: čtyři ročníky, 32 lekcí podle osnov gymnázia 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ě cs: Poslech, čtení, psaní, doplňování, kvízy a testy od A2 k maturitě

View file

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

View file

@ -39,6 +39,10 @@ final class Content {
let en2: [J] let en2: [J]
let en3: [J] let en3: [J]
let en4: [J] let en4: [J]
let de: [J]
let de2: [J]
let de3: [J]
let de4: [J]
let lit: [J] let lit: [J]
let lit2: [J] let lit2: [J]
let lit3: [J] let lit3: [J]
@ -93,6 +97,10 @@ final class Content {
en2 = raw.arr("en2") en2 = raw.arr("en2")
en3 = raw.arr("en3") en3 = raw.arr("en3")
en4 = raw.arr("en4") en4 = raw.arr("en4")
de = raw.arr("de")
de2 = raw.arr("de2")
de3 = raw.arr("de3")
de4 = raw.arr("de4")
lit = raw.arr("lit") lit = raw.arr("lit")
lit2 = raw.arr("lit2") lit2 = raw.arr("lit2")
lit3 = raw.arr("lit3") lit3 = raw.arr("lit3")
@ -129,6 +137,15 @@ final class Content {
} }
} }
func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).first { $0.int("id") == id } } func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).first { $0.int("id") == id } }
func deYear(_ year: Int) -> [J] {
switch year {
case 2: return de2
case 3: return de3
case 4: return de4
default: return de
}
}
func deLesson(_ year: Int, _ id: Int) -> J? { deYear(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

@ -48,6 +48,8 @@ final class ProgressStore {
func enDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "en.\(year).\(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 markEn(_ year: Int, _ id: Int) { defaults.set(true, forKey: "en.\(year).\(id)") }
func deDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "de.\(year).\(id)") }
func markDe(_ year: Int, _ id: Int) { defaults.set(true, forKey: "de.\(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)") }
@ -165,6 +167,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
for l in content.deYear(year) {
sum.totalEx += 1
sum.openUnits += 1
if p.deDone(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

View file

@ -12,6 +12,10 @@ enum Route: Hashable {
case subjects case subjects
case stats case stats
case roadmap case roadmap
case deHome
case deMap(Int)
case deLesson(Int, Int)
case deEx(Int, Int)
case unitMap(Int) case unitMap(Int)
case germanEx(Int, Int) case germanEx(Int, Int)
case vocab(Int) case vocab(Int)
@ -103,6 +107,10 @@ enum Route: Hashable {
case .subjects: return "subjects" case .subjects: return "subjects"
case .stats: return "stats" case .stats: return "stats"
case .roadmap: return "roadmap" case .roadmap: return "roadmap"
case .deHome: return "dehome"
case .deMap(let year): return "de\(year)map"
case .deLesson(let year, let id): return "de\(year)unit\(id)"
case .deEx(let year, let id): return "de\(year)ex\(id)"
case .unitMap(let unitId): return "unit\(unitId + 1)" case .unitMap(let unitId): return "unit\(unitId + 1)"
case .germanEx(let unitId, let ex): return "u\(unitId + 1)e\(ex)" case .germanEx(let unitId, let ex): return "u\(unitId + 1)e\(ex)"
case .vocab(let unitId): return "u\(unitId + 1)vocab" case .vocab(let unitId): return "u\(unitId + 1)vocab"
@ -204,14 +212,22 @@ enum Route: Hashable {
switch self { switch self {
case .welcome, .subjects, .stats: case .welcome, .subjects, .stats:
return [] return []
case .deHome:
return [.deHome]
case .deMap(let year):
return [.deHome, .deMap(year)]
case .deLesson(let year, let id):
return [.deHome, .deMap(year), .deLesson(year, id)]
case .deEx(let year, let id):
return [.deHome, .deMap(year), .deLesson(year, id), .deEx(year, id)]
case .roadmap: case .roadmap:
return [.roadmap] return [.deHome, .roadmap]
case .unitMap(let id): case .unitMap(let id):
return [.roadmap, .unitMap(id)] return [.deHome, .roadmap, .unitMap(id)]
case .germanEx(let unit, let ex): case .germanEx(let unit, let ex):
return [.roadmap, .unitMap(unit), .germanEx(unit, ex)] return [.deHome, .roadmap, .unitMap(unit), .germanEx(unit, ex)]
case .vocab(let unit): case .vocab(let unit):
return [.roadmap, .unitMap(unit), .vocab(unit)] return [.deHome, .roadmap, .unitMap(unit), .vocab(unit)]
case .netYears: case .netYears:
return [.netYears] return [.netYears]
case .netMap: case .netMap:
@ -384,6 +400,7 @@ func routeFromPage(_ page: String) -> Route? {
case "subjects": return .subjects case "subjects": return .subjects
case "stats": return .stats case "stats": return .stats
case "roadmap": return .roadmap case "roadmap": return .roadmap
case "dehome": return .deHome
case "netyears": return .netYears case "netyears": return .netYears
case "netmap": return .netMap case "netmap": return .netMap
case "hwyears": return .hwYears case "hwyears": return .hwYears
@ -546,6 +563,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: /de(\d)map/), let y = Int(m.1) {
return .deMap(y)
}
if let m = page.wholeMatch(of: /de(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
return .deLesson(y, n)
}
if let m = page.wholeMatch(of: /de(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
return .deEx(y, n)
}
if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) { if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) {
return .enMap(y) return .enMap(y)
} }

View file

@ -1359,6 +1359,7 @@ private final class EnSpeaker: NSObject, ObservableObject, AVSpeechSynthesizerDe
@Published var rate: Float = 0.9 @Published var rate: Float = 0.9
@Published var volume: Float = 1 @Published var volume: Float = 1
@Published var phase: Phase = .idle @Published var phase: Phase = .idle
var language = "en-GB"
var onFinish: (() -> Void)? var onFinish: (() -> Void)?
override init() { override init() {
@ -1408,8 +1409,8 @@ private final class EnSpeaker: NSObject, ObservableObject, AVSpeechSynthesizerDe
let rest = ns.substring(from: start) let rest = ns.substring(from: start)
let utterance = AVSpeechUtterance(string: rest.isEmpty ? text : rest) let utterance = AVSpeechUtterance(string: rest.isEmpty ? text : rest)
if rest.isEmpty { base = 0 } if rest.isEmpty { base = 0 }
utterance.voice = AVSpeechSynthesisVoice(language: "en-GB") utterance.voice = AVSpeechSynthesisVoice(language: language)
?? AVSpeechSynthesisVoice(language: "en-US") ?? AVSpeechSynthesisVoice(language: language.hasPrefix("de") ? "de-DE" : "en-US")
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * rate utterance.rate = AVSpeechUtteranceDefaultSpeechRate * rate
utterance.volume = volume utterance.volume = volume
synth.speak(utterance) synth.speak(utterance)
@ -1443,6 +1444,9 @@ struct EngExScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
let year: Int let year: Int
let id: Int let id: Int
let voice: String
let lessonOf: (AppModel, Int, Int) -> J?
let mark: (AppModel, Int, Int) -> Void
@StateObject private var speaker: EnSpeaker @StateObject private var speaker: EnSpeaker
@State private var readPicks: [Int] @State private var readPicks: [Int]
@State private var listenPicks: [Int] @State private var listenPicks: [Int]
@ -1455,20 +1459,27 @@ struct EngExScreen: View {
@State private var showModel = false @State private var showModel = false
@State private var fb = ("", "") @State private var fb = ("", "")
init(vm: AppModel, year: Int, id: Int) { init(vm: AppModel, year: Int, id: Int, voice: String = "en-GB",
lesson: @escaping (AppModel, Int, Int) -> J? = { $0.content.enLesson($1, $2) },
mark: @escaping (AppModel, Int, Int) -> Void = { $0.markEn($1, $2) }) {
self.vm = vm self.vm = vm
self.year = year self.year = year
self.id = id self.id = id
let lesson = vm.content.enLesson(year, id) self.voice = voice
_readPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("readQuiz").count ?? 0)) self.lessonOf = lesson
_listenPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("listenQuiz").count ?? 0)) self.mark = mark
_quizPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("quiz").count ?? 0)) let row = lesson(vm, year, id)
_gaps = State(initialValue: Array(repeating: "", count: lesson?.arr("gaps").count ?? 0)) _readPicks = State(initialValue: Array(repeating: -1, count: row?.arr("readQuiz").count ?? 0))
_speaker = StateObject(wrappedValue: EnSpeaker()) _listenPicks = State(initialValue: Array(repeating: -1, count: row?.arr("listenQuiz").count ?? 0))
_quizPicks = State(initialValue: Array(repeating: -1, count: row?.arr("quiz").count ?? 0))
_gaps = State(initialValue: Array(repeating: "", count: row?.arr("gaps").count ?? 0))
let speaker = EnSpeaker()
speaker.language = voice
_speaker = StateObject(wrappedValue: speaker)
} }
var body: some View { var body: some View {
if let lesson = vm.content.enLesson(year, id) { if let lesson = lessonOf(vm, year, id) {
let p = vm.palette let p = vm.palette
ExerciseScaffold(vm: vm, title: vm.tr(lesson.str("titleKey")), sub: nil, action: vm.tr("check"), onAction: { ExerciseScaffold(vm: vm, title: vm.tr(lesson.str("titleKey")), sub: nil, action: vm.tr("check"), onAction: {
check(lesson) check(lesson)
@ -1619,7 +1630,7 @@ struct EngExScreen: View {
if all { if all {
fb = (vm.tr("feedback_ok"), "ok") fb = (vm.tr("feedback_ok"), "ok")
showModel = true showModel = true
vm.markEn(year, id) mark(vm, year, id)
} else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count { } else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count {
fb = (vm.tr("en_write_short"), "err") fb = (vm.tr("en_write_short"), "err")
} else { } else {

View file

@ -662,12 +662,16 @@ struct StatsScreen: View {
private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
var sum = ProgressSum() var sum = ProgressSum()
switch s.str("target") { switch s.str("target") {
case "roadmap": case "dehome":
for u in vm.content.german where u.bool("unlocked") { for u in vm.content.german where u.bool("unlocked") {
let n = u.strs("names").count let n = u.strs("names").count
sum.totalEx += n sum.totalEx += n
sum.doneEx += (1...n).filter { vm.progress.germanDone(u.int("id"), $0) }.count sum.doneEx += (1...n).filter { vm.progress.germanDone(u.int("id"), $0) }.count
} }
sum.totalEx += (1...4).reduce(0) { $0 + vm.content.deYear($1).count }
sum.doneEx += (1...4).reduce(0) { acc, year in
acc + vm.content.deYear(year).filter { vm.progress.deDone(year, $0.int("id")) }.count
}
case "netyears": case "netyears":
sum.totalEx = vm.content.netLessons.count sum.totalEx = vm.content.netLessons.count
sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count
@ -900,6 +904,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
for l in vm.content.enYear(year) { 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") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en\(year)unit\(l.int("id"))", extra: "anglictina english")
} }
add(vm.tr("de_year\(year)"), vm.tr("search_lesson"), "de\(year)map", extra: "nemcina deutsch rocnik")
for l in vm.content.deYear(year) {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "de\(year)unit\(l.int("id"))", extra: "nemcina deutsch")
}
} }
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")
@ -1243,6 +1251,43 @@ struct HwYearsScreen: View {
} }
} }
struct DeHomeScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let rows: [(String, String, String, Route)] = [
("U", "de_book_title", "de_book_sub", .roadmap),
("1", "de_year1", "de_y1_sub", .deMap(1)),
("2", "de_year2", "de_y2_sub", .deMap(2)),
("3", "de_year3", "de_y3_sub", .deMap(3)),
("4", "de_year4", "de_y4_sub", .deMap(4)),
]
MacFillGrid(count: rows.count, minWidth: 260, minHeight: 180) { i in
NavigationLink(value: rows[i].3) {
MacLinkCard(title: vm.tr(rows[i].1), subtitle: vm.tr(rows[i].2), badge: rows[i].0)
}
.buttonStyle(.plain)
}
.navigationTitle(vm.tr("Deutsch"))
.navigationSubtitle(vm.tr("de_home_sub"))
#else
List {
NavigationLink(value: Route.roadmap) {
ListRowLabel(title: vm.tr("de_book_title"), subtitle: vm.tr("de_book_sub"))
}
ForEach(1...4, id: \.self) { year in
NavigationLink(value: Route.deMap(year)) {
ListRowLabel(title: vm.tr("de_year\(year)"), subtitle: vm.tr("de_y\(year)_sub"))
}
}
}
.navigationTitle(vm.tr("Deutsch"))
.navigationSubtitle(vm.tr("de_home_sub"))
#endif
}
}
struct EnYearsScreen: View { struct EnYearsScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
@ -2157,6 +2202,29 @@ struct RouteDestination: View {
PracticeHome(vm: vm) PracticeHome(vm: vm)
case .stats: case .stats:
StatsScreen(vm: vm) StatsScreen(vm: vm)
case .deHome:
DeHomeScreen(vm: vm)
case .deMap(let year):
LessonListScreen(
vm: vm,
title: vm.tr("de_year\(year)"),
subtitle: vm.tr("de_y\(year)_sub"),
rows: vm.content.deYear(year).map { l in
let id = l.int("id")
return LessonRow(
id: "d\(year)\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.deDone(year, id),
destination: .deLesson(year, id)
)
}
)
case .deLesson(let year, let id):
if let l = vm.content.deLesson(year, id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .deEx(year, id))
}
case .deEx(let year, let id):
EngExScreen(vm: vm, year: year, id: id, voice: "de-DE", lesson: { $0.content.deLesson($1, $2) }, mark: { $0.markDe($1, $2) })
case .roadmap: case .roadmap:
RoadmapScreen(vm: vm) RoadmapScreen(vm: vm)
case .unitMap(let id): case .unitMap(let id):

View file

@ -777,7 +777,7 @@ def main() -> int:
i18n = {} i18n = {}
for path in sorted(SRC.glob("*.c")): for path in sorted(SRC.glob("*.c")):
raw = path.read_text(encoding="utf-8") raw = path.read_text(encoding="utf-8")
if path.name == "english.c": if path.name in ("english.c", "german_course.c"):
raw = expand_local_includes(raw, path.parent) raw = expand_local_includes(raw, path.parent)
text = strip_comments(raw) text = strip_comments(raw)
arrays.update(find_arrays(text)) arrays.update(find_arrays(text))
@ -1629,10 +1629,10 @@ def main() -> int:
q["expl"] = hints[qi] q["expl"] = hints[qi]
return quiz return quiz
def en_year(year): def skill_year(tag, year, count):
lessons = [] lessons = []
for i in range(1, 9): for i in range(1, count + 1):
prefix = f"en_y{year}_l{i}" prefix = f"{tag}_y{year}_l{i}"
write = str_list(A(f"{prefix}_write")) if f"{prefix}_write" in arrays else [] write = str_list(A(f"{prefix}_write")) if f"{prefix}_write" in arrays else []
writing = None writing = None
if len(write) >= 3: if len(write) >= 3:
@ -1644,8 +1644,8 @@ def main() -> int:
} }
lessons.append({ lessons.append({
"id": i, "id": i,
"titleKey": f"en_y{year}_u{i}", "titleKey": f"{tag}_y{year}_u{i}",
"subKey": f"en_y{year}_u{i}_sub", "subKey": f"{tag}_y{year}_u{i}_sub",
"slides": slides_items(A(f"{prefix}_slides")), "slides": slides_items(A(f"{prefix}_slides")),
"reading": en_text(f"{prefix}_read"), "reading": en_text(f"{prefix}_read"),
"readQuiz": en_choice(f"{prefix}_readq"), "readQuiz": en_choice(f"{prefix}_readq"),
@ -1657,13 +1657,17 @@ def main() -> int:
}) })
return lessons return lessons
en_lessons = en_year(1) en_lessons = skill_year("en", 1, 8)
en2_lessons = en_year(2) en2_lessons = skill_year("en", 2, 8)
en3_lessons = en_year(3) en3_lessons = skill_year("en", 3, 8)
en4_lessons = en_year(4) en4_lessons = skill_year("en", 4, 8)
de_lessons = skill_year("de", 1, 6)
de2_lessons = skill_year("de", 2, 6)
de3_lessons = skill_year("de", 3, 6)
de4_lessons = skill_year("de", 4, 6)
subjects = [ subjects = [
{"key": "Deutsch", "open": True, "target": "roadmap", "icon": "de"}, {"key": "Deutsch", "open": True, "target": "dehome", "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"},
@ -1711,6 +1715,10 @@ def main() -> int:
"en2": en2_lessons, "en2": en2_lessons,
"en3": en3_lessons, "en3": en3_lessons,
"en4": en4_lessons, "en4": en4_lessons,
"de": de_lessons,
"de2": de2_lessons,
"de3": de3_lessons,
"de4": de4_lessons,
"mluvnice": mluvnice, "mluvnice": mluvnice,
"books": books, "books": books,
"changelog": parse_changelog(ROOT / "data" / "changelog.txt"), "changelog": parse_changelog(ROOT / "data" / "changelog.txt"),

View file

@ -2401,8 +2401,8 @@ def emit_questions(name, items, lines):
lines.append("") lines.append("")
def emit_lesson(year, index, lesson, lines, inits): def emit_lesson(year, index, lesson, lines, inits, prefix="en"):
p = f"en_y{year}_l{index}" p = f"{prefix}_y{year}_l{index}"
lines.append(f"static const NetSlide {p}_slides[] = {{") lines.append(f"static const NetSlide {p}_slides[] = {{")
for kicker, title, tip, bullets in lesson["slides"]: for kicker, title, tip, bullets in lesson["slides"]:
lines.append(" {") lines.append(" {")

1387
scripts/gen-german.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,28 +17,6 @@
#define EN_YEARS 4 #define EN_YEARS 4
#define EN_N 8 #define EN_N 8
typedef struct {
const NetSlide *slides;
int n_slides;
const char *reading;
const ChoiceQ *readq;
const char **read_hints;
int n_read;
const char *listening;
const ChoiceQ *listenq;
const char **listen_hints;
int n_listen;
const TypedQ *gaps;
int n_gaps;
const char *write_prompt;
const char *write_model;
const char *write_keys;
int write_min;
const ChoiceQ *quiz;
const char **quiz_hints;
int n_quiz;
} EnLesson;
#include "english_lessons.inc" #include "english_lessons.inc"
typedef struct { typedef struct {
@ -51,6 +29,7 @@ typedef struct {
int year; int year;
int index; int index;
const EnLesson *lesson; const EnLesson *lesson;
void (*on_done)(int year, int index);
GtkWidget *feedback; GtkWidget *feedback;
GtkWidget *model; GtkWidget *model;
GArray *picks; GArray *picks;
@ -69,6 +48,7 @@ typedef struct {
gboolean heard; gboolean heard;
double speed; double speed;
double gain; double gain;
const char *voice;
GPid pid; GPid pid;
guint timer; guint timer;
guint tick_ms; guint tick_ms;
@ -646,7 +626,7 @@ GtkWidget *build_enmap_page(int year) {
/* Slides */ /* Slides */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
static void en_slide_apply(NetLesson *L) { void skill_slide_apply(NetLesson *L) {
char name[16]; char name[16];
if (!L || !L->stack) if (!L || !L->stack)
@ -705,7 +685,7 @@ static void en_slide_prev(GtkButton *button, gpointer data) {
(void)button; (void)button;
if (L && L->idx > 0) { if (L && L->idx > 0) {
L->idx--; L->idx--;
en_slide_apply(L); skill_slide_apply(L);
} }
} }
@ -717,7 +697,7 @@ static void en_slide_next(GtkButton *button, gpointer data) {
return; return;
if (L->idx + 1 < L->n_slides) { if (L->idx + 1 < L->n_slides) {
L->idx++; L->idx++;
en_slide_apply(L); skill_slide_apply(L);
} else { } else {
gtk_stack_set_visible_child_name(main_stack, L->ex_page); gtk_stack_set_visible_child_name(main_stack, L->ex_page);
} }
@ -726,12 +706,12 @@ static void en_slide_next(GtkButton *button, gpointer data) {
void en_lessons_apply_lang(void) { void en_lessons_apply_lang(void) {
for (int y = 0; y < EN_YEARS; y++) for (int y = 0; y < EN_YEARS; y++)
for (int i = 0; i < EN_N; i++) for (int i = 0; i < EN_N; i++)
en_slide_apply(&en_rt[y][i]); skill_slide_apply(&en_rt[y][i]);
} }
static GtkWidget *build_en_unit_page(int year, int index) { GtkWidget *skill_unit_page(NetLesson *L, const EnLesson *lesson,
NetLesson *L = &en_rt[year][index]; const char *back, const char *title_key,
const EnLesson *lesson = &en_lessons[year][index]; const char *sub_key) {
GtkWidget *page; GtkWidget *page;
GtkWidget *scroll; GtkWidget *scroll;
GtkWidget *nav; GtkWidget *nav;
@ -746,8 +726,7 @@ static GtkWidget *build_en_unit_page(int year, int index) {
gtk_widget_set_margin_top(page, 20); gtk_widget_set_margin_top(page, 20);
gtk_widget_set_margin_bottom(page, 20); gtk_widget_set_margin_bottom(page, 20);
gtk_box_append(GTK_BOX(page), gtk_box_append(GTK_BOX(page),
top_bar(en_map_page[year], en_title_key[year][index], top_bar(back, title_key, sub_key));
en_sub_key[year][index]));
scroll = gtk_scrolled_window_new(); scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
@ -795,7 +774,7 @@ static GtkWidget *build_en_unit_page(int year, int index) {
} }
en_rescale_notes(0); en_rescale_notes(0);
net_notes_target = NULL; net_notes_target = NULL;
en_slide_apply(L); skill_slide_apply(L);
return page; return page;
} }
@ -1054,14 +1033,58 @@ static void en_mac_levels(EnEx *ctx) {
ctx->synth, sel_registerName("setVolume:"), vol); ctx->synth, sel_registerName("setVolume:"), vol);
} }
static gboolean en_mac_ensure(EnEx *ctx) { static gboolean en_mac_try_voice(id synth, const char *name) {
static const char *voices[] = { return ((BOOL (*)(id, SEL, id))objc_msgSend)(
synth, sel_registerName("setVoice:"), en_ns(name));
}
static void en_mac_choose(id synth, const char *lang) {
static const char *en_voices[] = {
"com.apple.voice.compact.en-GB.Daniel", "com.apple.voice.compact.en-GB.Daniel",
"com.apple.speech.synthesis.voice.daniel", "com.apple.speech.synthesis.voice.daniel",
"com.apple.voice.compact.en-US.Samantha", "com.apple.voice.compact.en-US.Samantha",
"com.apple.speech.synthesis.voice.samantha", "com.apple.speech.synthesis.voice.samantha",
NULL NULL
}; };
static const char *de_voices[] = {
"com.apple.voice.compact.de-DE.Anna",
"com.apple.speech.synthesis.voice.anna",
"com.apple.voice.compact.de-DE.Helena",
"com.apple.speech.synthesis.voice.petra",
NULL
};
gboolean german = lang && g_str_has_prefix(lang, "de");
const char **list = german ? de_voices : en_voices;
const char *needle = german ? "de-DE" : "en-GB";
Class cls;
id voices;
unsigned long n;
for (int i = 0; list[i]; i++) {
if (en_mac_try_voice(synth, list[i]))
return;
}
cls = objc_getClass("NSSpeechSynthesizer");
if (!cls)
return;
voices = ((id (*)(Class, SEL))objc_msgSend)(
cls, sel_registerName("availableVoices"));
if (!voices)
return;
n = ((unsigned long (*)(id, SEL))objc_msgSend)(
voices, sel_registerName("count"));
for (unsigned long i = 0; i < n; i++) {
id name = ((id (*)(id, SEL, unsigned long))objc_msgSend)(
voices, sel_registerName("objectAtIndex:"), i);
const char *utf = ((const char *(*)(id, SEL))objc_msgSend)(
name, sel_registerName("UTF8String"));
if (utf && strstr(utf, needle) && en_mac_try_voice(synth, utf))
return;
}
}
static gboolean en_mac_ensure(EnEx *ctx) {
Class cls; Class cls;
id obj; id obj;
@ -1075,11 +1098,7 @@ static gboolean en_mac_ensure(EnEx *ctx) {
if (!obj) if (!obj)
return FALSE; return FALSE;
ctx->synth = obj; ctx->synth = obj;
for (int i = 0; voices[i]; i++) { en_mac_choose(obj, ctx->voice);
if (((BOOL (*)(id, SEL, id))objc_msgSend)(
obj, sel_registerName("setVoice:"), en_ns(voices[i])))
break;
}
return TRUE; return TRUE;
} }
@ -1163,14 +1182,16 @@ static gboolean en_launch(EnEx *ctx) {
g_snprintf(rate, sizeof rate, "%d", (int)(160.0 * speed)); g_snprintf(rate, sizeof rate, "%d", (int)(160.0 * speed));
g_snprintf(amp, sizeof amp, "%d", (int)(ctx->gain * 200.0)); g_snprintf(amp, sizeof amp, "%d", (int)(ctx->gain * 200.0));
{ {
char *a[] = {"espeak-ng", "-v", "en", "-s", rate, "-a", amp, text, NULL}; const char *voice = ctx->voice && ctx->voice[0] ? ctx->voice : "en";
char *b[] = {"espeak", "-v", "en", "-s", rate, "-a", amp, text, NULL}; char *a[] = {"espeak-ng", "-v", (char *)voice, "-s", rate, "-a", amp, text, NULL};
char *b[] = {"espeak", "-v", (char *)voice, "-s", rate, "-a", amp, text, NULL};
#ifdef __APPLE__ #ifdef __APPLE__
char *c[] = {"say", "-r", rate, text, NULL}; char *c_en[] = {"say", "-r", rate, text, NULL};
char **tries[] = {c}; char *c_de[] = {"say", "-v", "Anna", "-r", rate, text, NULL};
char **tries[] = {g_str_has_prefix(voice, "de") ? c_de : c_en};
int n = 1; int n = 1;
#else #else
char *c[] = {"spd-say", "-l", "en", "-r", rate, text, NULL}; char *c[] = {"spd-say", "-l", (char *)voice, "-r", rate, text, NULL};
char **tries[] = {a, b, c}; char **tries[] = {a, b, c};
int n = 3; int n = 3;
#endif #endif
@ -1416,7 +1437,8 @@ static void en_check(GtkButton *button, gpointer data) {
set_feedback(ctx->feedback, TRUE, tr("feedback_ok")); set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
if (ctx->model) if (ctx->model)
gtk_widget_set_visible(ctx->model, TRUE); gtk_widget_set_visible(ctx->model, TRUE);
mark_en_done(ctx->year, ctx->index); if (ctx->on_done)
ctx->on_done(ctx->year, ctx->index);
} else if (ctx->essay && !write_ok && } else if (ctx->essay && !write_ok &&
mcq_ok == (int)ctx->picks->len && mcq_ok == (int)ctx->picks->len &&
gap_ok == ctx->lesson->n_gaps) { gap_ok == ctx->lesson->n_gaps) {
@ -1426,8 +1448,10 @@ static void en_check(GtkButton *button, gpointer data) {
} }
} }
static GtkWidget *build_en_ex_page(int year, int index) { GtkWidget *skill_ex_page(const EnLesson *lesson, int year, int index,
const EnLesson *lesson = &en_lessons[year][index]; void (*on_done)(int, int),
const char *back, const char *title_key,
const char *voice) {
EnEx *ctx = g_new0(EnEx, 1); EnEx *ctx = g_new0(EnEx, 1);
GtkWidget *page; GtkWidget *page;
GtkWidget *scroll; GtkWidget *scroll;
@ -1437,6 +1461,8 @@ static GtkWidget *build_en_ex_page(int year, int index) {
ctx->year = year; ctx->year = year;
ctx->index = index; ctx->index = index;
ctx->lesson = lesson; ctx->lesson = lesson;
ctx->on_done = on_done;
ctx->voice = voice && voice[0] ? voice : "en";
ctx->picks = g_array_new(FALSE, FALSE, sizeof(EnPick)); ctx->picks = g_array_new(FALSE, FALSE, sizeof(EnPick));
ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
ctx->gap_hints = g_array_new(FALSE, TRUE, sizeof(GtkWidget *)); ctx->gap_hints = g_array_new(FALSE, TRUE, sizeof(GtkWidget *));
@ -1446,9 +1472,7 @@ static GtkWidget *build_en_ex_page(int year, int index) {
gtk_widget_set_margin_end(page, 32); gtk_widget_set_margin_end(page, 32);
gtk_widget_set_margin_top(page, 24); gtk_widget_set_margin_top(page, 24);
gtk_widget_set_margin_bottom(page, 24); gtk_widget_set_margin_bottom(page, 24);
gtk_box_append(GTK_BOX(page), gtk_box_append(GTK_BOX(page), top_bar(back, title_key, NULL));
top_bar(en_unit_page[year][index],
en_title_key[year][index], NULL));
scroll = gtk_scrolled_window_new(); scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
@ -1586,6 +1610,18 @@ static GtkWidget *build_en_ex_page(int year, int index) {
return page; return page;
} }
static GtkWidget *build_en_unit_page(int year, int index) {
return skill_unit_page(&en_rt[year][index], &en_lessons[year][index],
en_map_page[year], en_title_key[year][index],
en_sub_key[year][index]);
}
static GtkWidget *build_en_ex_page(int year, int index) {
return skill_ex_page(&en_lessons[year][index], year, index, mark_en_done,
en_unit_page[year][index], en_title_key[year][index],
"en");
}
void add_en_pages(GtkStack *stack) { void add_en_pages(GtkStack *stack) {
en_prepare(); en_prepare();
for (int y = 0; y < EN_YEARS; y++) { for (int y = 0; y < EN_YEARS; y++) {

234
src/german_course.c Normal file
View file

@ -0,0 +1,234 @@
#include "graduately.h"
/* Skill course next to the three textbook units. Four years, same
* listening / reading / writing player as English. */
#define DE_YEARS 4
#define DE_N 6
#include "german_lessons.inc"
static NetLesson de_rt[DE_YEARS][DE_N];
static char de_map_page[DE_YEARS][16];
static char de_unit_page[DE_YEARS][DE_N][16];
static char de_ex_page[DE_YEARS][DE_N][16];
static char de_title_key[DE_YEARS][DE_N][16];
static char de_sub_key[DE_YEARS][DE_N][20];
static gboolean de_ready;
static void de_prepare(void) {
if (de_ready)
return;
de_ready = TRUE;
for (int y = 0; y < DE_YEARS; y++) {
g_snprintf(de_map_page[y], sizeof de_map_page[y], "de%dmap", y + 1);
for (int i = 0; i < DE_N; i++) {
g_snprintf(de_unit_page[y][i], sizeof de_unit_page[y][i],
"de%dunit%d", y + 1, i + 1);
g_snprintf(de_ex_page[y][i], sizeof de_ex_page[y][i],
"de%dex%d", y + 1, i + 1);
g_snprintf(de_title_key[y][i], sizeof de_title_key[y][i],
"de_y%d_u%d", y + 1, i + 1);
g_snprintf(de_sub_key[y][i], sizeof de_sub_key[y][i],
"de_y%d_u%d_sub", y + 1, i + 1);
de_rt[y][i].unit_page = de_unit_page[y][i];
de_rt[y][i].ex_page = de_ex_page[y][i];
de_rt[y][i].n_slides = (guint)de_lessons[y][i].n_slides;
}
}
}
static void de_save_progress(void) {
GKeyFile *kf = g_key_file_new();
gchar *data;
GError *err = NULL;
for (int y = 0; y < DE_YEARS; y++) {
for (int i = 0; i < DE_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1);
g_key_file_set_boolean(kf, "done", key, de_rt[y][i].done);
}
}
data = g_key_file_to_data(kf, NULL, NULL);
if (!g_file_set_contents(PROGRESS_DE, data, -1, &err)) {
g_warning("german course: %s", err->message);
g_error_free(err);
}
g_free(data);
g_key_file_free(kf);
}
void de_load_progress(void) {
GKeyFile *kf = g_key_file_new();
GError *err = NULL;
de_prepare();
if (!g_key_file_load_from_file(kf, PROGRESS_DE, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
return;
}
for (int y = 0; y < DE_YEARS; y++) {
for (int i = 0; i < DE_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1);
de_rt[y][i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
}
g_key_file_free(kf);
}
static void mark_de_done(int year, int index) {
if (year < 0 || year >= DE_YEARS || index < 0 || index >= DE_N)
return;
if (de_rt[year][index].done)
return;
de_rt[year][index].done = TRUE;
de_save_progress();
refresh_stats_ui();
}
void progress_for_de(ProgressSum *out) {
out->total_ex += DE_YEARS * DE_N;
out->open_units += DE_YEARS * DE_N;
for (int y = 0; y < DE_YEARS; y++) {
for (int i = 0; i < DE_N; i++) {
if (de_rt[y][i].done) {
out->done_ex++;
out->done_units++;
}
}
}
}
void de_lessons_apply_lang(void) {
for (int y = 0; y < DE_YEARS; y++)
for (int i = 0; i < DE_N; i++)
skill_slide_apply(&de_rt[y][i]);
}
static GtkWidget *de_nav_button(const char *title_key, const char *sub_key,
const char *target, const char *badge) {
GtkWidget *btn = gtk_button_new();
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
GtkWidget *num = gtk_label_new(badge);
GtkWidget *texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
GtkWidget *title = gtk_label_new(NULL);
GtkWidget *sub = gtk_label_new(NULL);
gtk_widget_add_css_class(btn, "unit-node");
gtk_widget_add_css_class(btn, "current");
gtk_widget_set_hexpand(btn, TRUE);
gtk_widget_set_margin_start(row, 18);
gtk_widget_set_margin_end(row, 18);
gtk_widget_set_margin_top(row, 10);
gtk_widget_set_margin_bottom(row, 10);
gtk_button_set_child(GTK_BUTTON(btn), row);
gtk_widget_add_css_class(num, "unit-number");
gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), num);
gtk_widget_set_hexpand(texts, TRUE);
gtk_box_append(GTK_BOX(row), texts);
i18n_bind(title, title_key, 0);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_add_css_class(title, "unit-name");
gtk_box_append(GTK_BOX(texts), title);
i18n_bind(sub, sub_key, 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_label_set_xalign(GTK_LABEL(sub), 0);
gtk_widget_add_css_class(sub, "quiz-intro");
gtk_box_append(GTK_BOX(texts), sub);
g_object_set_data_full(G_OBJECT(btn), "target", g_strdup(target), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
return btn;
}
static GtkWidget *de_list_page(const char *back, const char *title,
const char *sub, GtkWidget *list) {
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
gtk_widget_set_hexpand(page, TRUE);
gtk_widget_set_vexpand(page, TRUE);
gtk_widget_set_margin_start(page, 32);
gtk_widget_set_margin_end(page, 32);
gtk_widget_set_margin_top(page, 24);
gtk_widget_set_margin_bottom(page, 24);
gtk_box_append(GTK_BOX(page), top_bar(back, title, sub));
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 18);
gtk_box_append(GTK_BOX(page), scroll);
gtk_widget_set_halign(list, GTK_ALIGN_CENTER);
gtk_widget_set_size_request(list, 520, -1);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
return page;
}
GtkWidget *build_dehome_page(void) {
GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
static const char *years[] = {
"de_year1", "de_year2", "de_year3", "de_year4",
};
static const char *subs[] = {
"de_y1_sub", "de_y2_sub", "de_y3_sub", "de_y4_sub",
};
de_prepare();
gtk_box_append(GTK_BOX(list),
de_nav_button("de_book_title", "de_book_sub", "roadmap", "U"));
for (int y = 0; y < DE_YEARS; y++) {
char badge[8];
g_snprintf(badge, sizeof badge, "%d", y + 1);
gtk_box_append(GTK_BOX(list),
de_nav_button(years[y], subs[y], de_map_page[y], badge));
}
return de_list_page("subjects", "Deutsch", "de_home_sub", list);
}
static GtkWidget *build_de_map(int year) {
GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
static const char *year_keys[] = {
"de_year1", "de_year2", "de_year3", "de_year4",
};
static const char *sub_keys[] = {
"de_y1_sub", "de_y2_sub", "de_y3_sub", "de_y4_sub",
};
de_prepare();
for (int i = 0; i < DE_N; i++) {
char badge[8];
g_snprintf(badge, sizeof badge, "%d", i + 1);
gtk_box_append(GTK_BOX(list),
de_nav_button(de_title_key[year][i],
de_sub_key[year][i],
de_unit_page[year][i], badge));
}
return de_list_page("dehome", year_keys[year], sub_keys[year], list);
}
void add_de_pages(GtkStack *stack) {
de_prepare();
for (int y = 0; y < DE_YEARS; y++) {
gtk_stack_add_named(stack, build_de_map(y), de_map_page[y]);
for (int i = 0; i < DE_N; i++) {
gtk_stack_add_named(stack,
skill_unit_page(&de_rt[y][i], &de_lessons[y][i],
de_map_page[y], de_title_key[y][i],
de_sub_key[y][i]),
de_unit_page[y][i]);
gtk_stack_add_named(stack,
skill_ex_page(&de_lessons[y][i], y, i, mark_de_done,
de_unit_page[y][i], de_title_key[y][i], "de"),
de_ex_page[y][i]);
}
}
}

60
src/german_i18n.inc Normal file
View file

@ -0,0 +1,60 @@
{"de_home_sub", "Učebnice a čtyři ročníky s poslechem, čtením a psaním.", "The textbook plus four years of listening, reading and writing."},
{"de_book_title", "Učebnice", "Textbook"},
{"de_book_sub", "Neue Freunde, Aus aller Welt a Bei uns zu Hause.", "Neue Freunde, Aus aller Welt and Bei uns zu Hause."},
{"de_years_title", "Ročníky", "School years"},
{"de_y1_sub", "Pozdravy, rodina, škola, jídlo a den. Úroveň A1.", "Greetings, family, school, food and the day. Level A1."},
{"de_year1", "1. ročník", "Year 1"},
{"de_y1_u1", "Pozdravy a představení", "Greetings and introductions"},
{"de_y1_u1_sub", "Sein, heißen a woher. Doplňování a kvíz.", "Sein, heißen and woher. Gaps and a quiz."},
{"de_y1_u2", "Rodina", "Family"},
{"de_y1_u2_sub", "Haben a přivlastňovací zájmena. Čtení a doplňování.", "Haben and possessives. Reading and gaps."},
{"de_y1_u3", "Ve škole", "At school"},
{"de_y1_u3_sub", "Přítomný čas. Poslech a doplňování.", "The present tense. Listening and gaps."},
{"de_y1_u4", "Jídlo", "Food"},
{"de_y1_u4_sub", "Möchten a 4. pád. Krátký dialog a kvíz.", "Möchten and the accusative. A short dialogue and a quiz."},
{"de_y1_u5", "Můj den", "My day"},
{"de_y1_u5_sub", "Čas a odlučitelné předpony. Čtení a doplňování.", "Time and separable verbs. Reading and gaps."},
{"de_y1_u6", "Test 1. ročníku", "Year 1 test"},
{"de_y1_u6_sub", "Čtení, poslech, gramatika A1 a krátké psaní.", "Reading, listening, A1 grammar and a short text."},
{"de_y2_sub", "Město, nákupy, volný čas, bydlení a perfektum. Úroveň A2.", "Town, shopping, free time, home and the perfect. Level A2."},
{"de_year2", "2. ročník", "Year 2"},
{"de_y2_u1", "Ve městě", "In town"},
{"de_y2_u1_sub", "Wo a wohin, předložky. Doplňování a kvíz.", "Wo and wohin, prepositions. Gaps and a quiz."},
{"de_y2_u2", "Nákupy", "Shopping"},
{"de_y2_u2_sub", "Ceny a 4. pád v obchodě. Čtení a kvíz.", "Prices and the accusative in a shop. Reading and a quiz."},
{"de_y2_u3", "Volný čas", "Free time"},
{"de_y2_u3_sub", "Modální slovesa. Poslech a doplňování.", "Modal verbs. Listening and gaps."},
{"de_y2_u4", "Bydlení", "Home"},
{"de_y2_u4_sub", "Místnosti a předložky. Popis bytu.", "Rooms and prepositions. A description of a flat."},
{"de_y2_u5", "Perfektum", "The perfect tense"},
{"de_y2_u5_sub", "Haben nebo sein a příčestí. Doplňování a kvíz.", "Haben or sein plus the participle. Gaps and a quiz."},
{"de_y2_u6", "Test 2. ročníku", "Year 2 test"},
{"de_y2_u6_sub", "Čtení, poslech, A2 a krátké psaní.", "Reading, listening, A2 and a short text."},
{"de_y3_sub", "Zdraví, cestování, věty s weil, práce a srovnání. Úroveň A2+.", "Health, travel, weil-clauses, work and comparison. Level A2+."},
{"de_year3", "3. ročník", "Year 3"},
{"de_y3_u1", "U lékaře", "At the doctor's"},
{"de_y3_u1_sub", "Tělo, tut a rozkaz. Čtení a kvíz.", "The body, tut and the imperative. Reading and a quiz."},
{"de_y3_u2", "Cesta", "A trip"},
{"de_y3_u2_sub", "Perfektum v vyprávění. Poslech a doplňování.", "The perfect in a story. Listening and gaps."},
{"de_y3_u3", "Weil, dass, wenn", "Weil, dass, wenn"},
{"de_y3_u3_sub", "Vedlejší věta a sloveso na konci. Doplňování a kvíz.", "Subordinate clauses and the verb at the end. Gaps and a quiz."},
{"de_y3_u4", "Práce a vykání", "Work and Sie"},
{"de_y3_u4_sub", "Formální věta. E-mail a kvíz.", "A formal sentence. An email and a quiz."},
{"de_y3_u5", "Srovnání a příroda", "Comparison and nature"},
{"de_y3_u5_sub", "Komparativ a superlativ. Čtení a doplňování.", "Comparative and superlative. Reading and gaps."},
{"de_y3_u6", "Test 3. ročníku", "Year 3 test"},
{"de_y3_u6_sub", "Čtení, poslech, weil a krátký názor.", "Reading, listening, weil and a short opinion."},
{"de_y4_sub", "Média, formální dopis, čtení, konjunktiv a maturitní témata. Úroveň B1.", "Media, a formal letter, reading, the subjunctive and exam topics. Level B1."},
{"de_year4", "4. ročník", "Year 4"},
{"de_y4_u1", "Média", "The media"},
{"de_y4_u1_sub", "Poslech rozhovoru a kvíz k slovům.", "An interview and a vocabulary quiz."},
{"de_y4_u2", "Formální e-mail", "A formal email"},
{"de_y4_u2_sub", "Stížnost nebo žádost. Psaní a kvíz.", "A complaint or a request. Writing and a quiz."},
{"de_y4_u3", "Čtení: noční vlak", "Reading: the night train"},
{"de_y4_u3_sub", "Delší text a otázky jako u maturity.", "A longer text and exam-style questions."},
{"de_y4_u4", "Kdyby", "If I were"},
{"de_y4_u4_sub", "Konjunktiv II. Doplňování a kvíz.", "Konjunktiv II. Gaps and a quiz."},
{"de_y4_u5", "Témata k maturitě", "Exam topics"},
{"de_y4_u5_sub", "Kolokace k běžným okruhům. Doplňování a kvíz.", "Collocations for common topics. Gaps and a quiz."},
{"de_y4_u6", "Cvičná zkouška", "A practice paper"},
{"de_y4_u6_sub", "Čtení, poslech, gramatika a krátký text.", "Reading, listening, grammar and a short text."},

1453
src/german_lessons.inc Normal file

File diff suppressed because it is too large Load diff

View file

@ -58,6 +58,7 @@ 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_en;
extern char *app_progress_de;
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;
@ -87,6 +88,7 @@ extern char *app_progress_mat4;
#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 PROGRESS_EN app_progress_en
#define PROGRESS_DE app_progress_de
#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 */
@ -622,6 +624,27 @@ typedef struct {
const char *tip; const char *tip;
const char *line[8]; const char *line[8];
} NetSlide; } NetSlide;
typedef struct {
const NetSlide *slides;
int n_slides;
const char *reading;
const ChoiceQ *readq;
const char **read_hints;
int n_read;
const char *listening;
const ChoiceQ *listenq;
const char **listen_hints;
int n_listen;
const TypedQ *gaps;
int n_gaps;
const char *write_prompt;
const char *write_model;
const char *write_keys;
int write_min;
const ChoiceQ *quiz;
const char **quiz_hints;
int n_quiz;
} EnLesson;
typedef struct { typedef struct {
const ChoiceQ *qs; const ChoiceQ *qs;
int n; int n;
@ -911,6 +934,18 @@ void progress_for_en(ProgressSum *out);
void en_rail_theme_reset(void); void en_rail_theme_reset(void);
GtkWidget *build_enyears_page(void); GtkWidget *build_enyears_page(void);
void add_en_pages(GtkStack *stack); void add_en_pages(GtkStack *stack);
GtkWidget *skill_unit_page(NetLesson *L, const EnLesson *lesson,
const char *back, const char *title_key,
const char *sub_key);
GtkWidget *skill_ex_page(const EnLesson *lesson, int year, int index,
void (*on_done)(int, int),
const char *back, const char *title_key,
const char *voice);
void skill_slide_apply(NetLesson *L);
GtkWidget *build_dehome_page(void);
void add_de_pages(GtkStack *stack);
void de_load_progress(void);
void progress_for_de(ProgressSum *out);
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);

View file

@ -1620,6 +1620,7 @@ const TrEntry tr_ui[] = {
{"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 "english_i18n.inc"
#include "german_i18n.inc"
#include "math_i18n.inc" #include "math_i18n.inc"
{NULL, NULL, NULL} {NULL, NULL, NULL}
}; };
@ -2603,6 +2604,7 @@ 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 en_lessons_apply_lang(void);
void de_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);
@ -2626,6 +2628,7 @@ void apply_language(void) {
on3_lessons_apply_lang(); on3_lessons_apply_lang();
on4_lessons_apply_lang(); on4_lessons_apply_lang();
en_lessons_apply_lang(); en_lessons_apply_lang();
de_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

@ -418,6 +418,8 @@ static gboolean run_smoke_test(gpointer data) {
"on4unit1", "on4ex1", "on4unit16", "on4ex16", "on4unit1", "on4ex1", "on4unit16", "on4ex16",
"enyears", "en1map", "en4map", "enyears", "en1map", "en4map",
"en1unit1", "en1ex1", "en4unit8", "en4ex8", "en1unit1", "en1ex1", "en4unit8", "en4ex8",
"dehome", "de1map", "de4map",
"de1unit1", "de1ex1", "de4unit6", "de4ex6",
"unit1", "unit2", "unit3", "unit1", "unit2", "unit3",
"u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15",
"u1vocab", "u2vocab", "u3vocab", "u1vocab", "u2vocab", "u3vocab",
@ -523,6 +525,8 @@ void activate(GtkApplication *app, gpointer user_data) {
gtk_stack_add_named(main_stack, welcome_page, "welcome"); gtk_stack_add_named(main_stack, welcome_page, "welcome");
gtk_stack_add_named(main_stack, build_subjects_page(), "subjects"); gtk_stack_add_named(main_stack, build_subjects_page(), "subjects");
gtk_stack_add_named(main_stack, roadmap_page, "roadmap"); gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
gtk_stack_add_named(main_stack, build_dehome_page(), "dehome");
add_de_pages(main_stack);
gtk_stack_add_named(main_stack, build_stats_page(), "stats"); gtk_stack_add_named(main_stack, build_stats_page(), "stats");
gtk_stack_add_named(main_stack, build_netyears_page(), "netyears"); gtk_stack_add_named(main_stack, build_netyears_page(), "netyears");
gtk_stack_add_named(main_stack, build_netmap_page(), "netmap"); gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");

View file

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

View file

@ -363,7 +363,7 @@ GtkWidget *build_roadmap_page(void) {
gtk_widget_set_margin_bottom(page, 24); gtk_widget_set_margin_bottom(page, 24);
gtk_box_append(GTK_BOX(page), gtk_box_append(GTK_BOX(page),
top_bar("subjects", "roadmap_title", "roadmap_sub")); top_bar("dehome", "roadmap_title", "roadmap_sub"));
scroll = gtk_scrolled_window_new(); scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),

View file

@ -29,7 +29,7 @@ static gboolean subject_open(int i) {
static const char *subject_page(int i) { static const char *subject_page(int i) {
if (i == 0) if (i == 0)
return "roadmap"; return "dehome";
if (i == NET_SUBJ) if (i == NET_SUBJ)
return "netyears"; return "netyears";
if (i == HW_SUBJ) if (i == HW_SUBJ)
@ -248,6 +248,17 @@ static void catalog_rebuild(void) {
catalog_add_key("en_year2", "search_lesson", "en2map", 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_year3", "search_lesson", "en3map", FALSE, 1, "anglictina english");
catalog_add_key("en_year4", "search_lesson", "en4map", FALSE, 1, "anglictina english"); catalog_add_key("en_year4", "search_lesson", "en4map", FALSE, 1, "anglictina english");
catalog_add_key("de_book_title", "search_unit", "roadmap", FALSE, 1, "nemcina deutsch ucebnice");
for (i = 1; i <= 4; i++) {
g_snprintf(key, sizeof key, "de_year%d", i);
g_snprintf(page, sizeof page, "de%dmap", i);
catalog_add_key(key, "search_lesson", page, FALSE, 1, "nemcina deutsch rocnik");
for (n = 1; n <= 6; n++) {
g_snprintf(key, sizeof key, "de_y%d_u%d", i, n);
g_snprintf(page, sizeof page, "de%dunit%d", i, n);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "nemcina deutsch");
}
}
for (int y = 1; y <= 4; y++) { for (int y = 1; y <= 4; y++) {
for (i = 1; i <= 8; i++) { for (i = 1; i <= 8; i++) {
g_snprintf(key, sizeof(key), "en_y%d_u%d", y, i); g_snprintf(key, sizeof(key), "en_y%d_u%d", y, i);

View file

@ -83,8 +83,11 @@ void refresh_stats_ui(void) {
progress_for_fyz(&all); progress_for_fyz(&all);
else if (s == MATH_SUBJ) else if (s == MATH_SUBJ)
progress_for_math(&all); progress_for_math(&all);
else else {
progress_for_units(sub_unit_start[s], sub_unit_count[s], &all); progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
if (s == 0)
progress_for_de(&all);
}
} }
pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0; pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0;
@ -138,8 +141,11 @@ void refresh_stats_ui(void) {
progress_for_fyz(&sp); progress_for_fyz(&sp);
else if (s == MATH_SUBJ) else if (s == MATH_SUBJ)
progress_for_math(&sp); progress_for_math(&sp);
else else {
progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp); progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
if (s == 0)
progress_for_de(&sp);
}
if (has_content) { if (has_content) {
tmp = g_strdup_printf(tr("stats_ex_fmt"), sp.done_ex, sp.total_ex); tmp = g_strdup_printf(tr("stats_ex_fmt"), sp.done_ex, sp.total_ex);
gtk_label_set_text(GTK_LABEL(count), tmp); gtk_label_set_text(GTK_LABEL(count), tmp);

View file

@ -198,7 +198,7 @@ static GtkWidget *subject_icon(int i) {
static const char *subject_target(int i) { static const char *subject_target(int i) {
if (i == 0) if (i == 0)
return "roadmap"; return "dehome";
if (i == NET_SUBJ) if (i == NET_SUBJ)
return "netyears"; return "netyears";
if (i == HW_SUBJ) if (i == HW_SUBJ)

View file

@ -25,6 +25,7 @@ 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_en;
char *app_progress_de;
char *app_progress_mluvnice; char *app_progress_mluvnice;
char *app_progress_cetba; char *app_progress_cetba;
char *app_progress_lit; char *app_progress_lit;
@ -183,6 +184,7 @@ static void set_progress_paths(const char *write_root) {
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_en);
g_free(app_progress_de);
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);
@ -213,6 +215,7 @@ static void set_progress_paths(const char *write_root) {
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_en = g_build_filename(app_progress_dir, "english.conf", NULL);
app_progress_de = g_build_filename(app_progress_dir, "german-course.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);