Add year-3 civics lessons on the individual and the law.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-02 22:52:39 +02:00
commit 0ee2f8e960
26 changed files with 2321 additions and 23 deletions

View file

@ -69,7 +69,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
- **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba - **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba
(82 knih: zápisky, kvíz, sestavení děje); literární teorie je zatím zamčená (82 knih: zápisky, kvíz, sestavení děje); literární teorie je zatím zamčená
- **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); ročníky 3–4 jsou zamčené 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 je zamčený
- 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`
@ -264,7 +265,8 @@ Intel Mac, another architecture, or hacking on the code →
- **Czech language** — grammar (20 exercises) and required reading - **Czech language** — grammar (20 exercises) and required reading
(82 books: notes, quiz, plot ordering); literature theory is still locked (82 books: notes, quiz, plot ordering); literature theory is still locked
- **Civics** — year 1, 17 lessons (the individual and society); - **Civics** — year 1, 17 lessons (the individual and society);
year 2, 16 lessons (the citizen in a democratic state); years 3–4 are locked year 2, 16 lessons (the citizen in a democratic state);
year 3, 16 lessons (the individual and the law); year 4 is locked
- 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

@ -136,6 +136,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
refresh() refresh()
} }
fun markOn3(id: Int) {
progress.markOn3(id)
refresh()
}
fun markMluv(n: Int) { fun markMluv(n: Int) {
progress.markMluv(n) progress.markMluv(n)
refresh() refresh()

View file

@ -38,6 +38,7 @@ import org.maturita.maturita.ui.MluvExercise
import org.maturita.maturita.ui.NetQuizScreen import org.maturita.maturita.ui.NetQuizScreen
import org.maturita.maturita.ui.NetYearsScreen import org.maturita.maturita.ui.NetYearsScreen
import org.maturita.maturita.ui.On2QuizScreen import org.maturita.maturita.ui.On2QuizScreen
import org.maturita.maturita.ui.On3QuizScreen
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.PlotScreen import org.maturita.maturita.ui.PlotScreen
@ -258,6 +259,20 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.On2Ex -> On2QuizScreen(vm, r.id) is Route.On2Ex -> On2QuizScreen(vm, r.id)
Route.On3Map -> LessonListScreen(
vm, vm.tr("on_year3"), vm.tr("on3_sub"),
vm.content.on3.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.on3Done(id), Route.On3Lesson(id))
},
)
is Route.On3Lesson -> {
val l = vm.content.on3Lesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.On3Ex(r.id))
}
}
is Route.On3Ex -> On3QuizScreen(vm, r.id)
Route.CzechMap -> CzechMapScreen(vm) Route.CzechMap -> CzechMapScreen(vm)
Route.Mluvnice -> LessonListScreen( Route.Mluvnice -> LessonListScreen(
vm, vm.tr("Mluvnice"), vm.tr("mluv_sub"), vm, vm.tr("Mluvnice"), vm.tr("mluv_sub"),

View file

@ -24,6 +24,9 @@ sealed class Route(val page: String) {
data object On2Map : Route("on2map") data object On2Map : Route("on2map")
data class On2Lesson(val id: Int) : Route("on2unit$id") data class On2Lesson(val id: Int) : Route("on2unit$id")
data class On2Ex(val id: Int) : Route("on2ex$id") data class On2Ex(val id: Int) : Route("on2ex$id")
data object On3Map : Route("on3map")
data class On3Lesson(val id: Int) : Route("on3unit$id")
data class On3Ex(val id: Int) : Route("on3ex$id")
data object CzechMap : Route("czechmap") data object CzechMap : Route("czechmap")
data object Mluvnice : Route("mluvnice") data object Mluvnice : Route("mluvnice")
data class MluvEx(val n: Int) : Route("mluve$n") data class MluvEx(val n: Int) : Route("mluve$n")
@ -38,8 +41,8 @@ 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 MluvEx || this is BookQuiz || this is BookPlot || this is OnEx || this is On2Ex || this is On3Ex || this is MluvEx || this is BookQuiz || this is BookPlot ||
this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson
/** Full stack from the practice root to this screen, so back always goes up one level. */ /** Full stack from the practice root to this screen, so back always goes up one level. */
fun stack(): List<Route> = when (this) { fun stack(): List<Route> = when (this) {
@ -65,6 +68,9 @@ sealed class Route(val page: String) {
On2Map -> listOf(Subjects, OnYears, On2Map) On2Map -> listOf(Subjects, OnYears, On2Map)
is On2Lesson -> listOf(Subjects, OnYears, On2Map, this) is On2Lesson -> listOf(Subjects, OnYears, On2Map, this)
is On2Ex -> listOf(Subjects, OnYears, On2Map, On2Lesson(id), this) is On2Ex -> listOf(Subjects, OnYears, On2Map, On2Lesson(id), this)
On3Map -> listOf(Subjects, OnYears, On3Map)
is On3Lesson -> listOf(Subjects, OnYears, On3Map, this)
is On3Ex -> listOf(Subjects, OnYears, On3Map, On3Lesson(id), this)
CzechMap -> listOf(Subjects, CzechMap) CzechMap -> listOf(Subjects, CzechMap)
Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice)
is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this)
@ -88,6 +94,7 @@ fun routeFromPage(page: String): Route? = when (page) {
"onyears" -> Route.OnYears "onyears" -> Route.OnYears
"onmap" -> Route.OnMap "onmap" -> Route.OnMap
"on2map" -> Route.On2Map "on2map" -> Route.On2Map
"on3map" -> Route.On3Map
"czechmap" -> Route.CzechMap "czechmap" -> Route.CzechMap
"mluvnice" -> Route.Mluvnice "mluvnice" -> Route.Mluvnice
"readinglist" -> Route.ReadingList "readinglist" -> Route.ReadingList
@ -119,6 +126,12 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""hwex(\d+)""").matchEntire(page)?.let { Regex("""hwex(\d+)""").matchEntire(page)?.let {
return Route.HwEx(it.groupValues[1].toInt()) return Route.HwEx(it.groupValues[1].toInt())
} }
Regex("""on3unit(\d+)""").matchEntire(page)?.let {
return Route.On3Lesson(it.groupValues[1].toInt())
}
Regex("""on3ex(\d+)""").matchEntire(page)?.let {
return Route.On3Ex(it.groupValues[1].toInt())
}
Regex("""on2unit(\d+)""").matchEntire(page)?.let { Regex("""on2unit(\d+)""").matchEntire(page)?.let {
return Route.On2Lesson(it.groupValues[1].toInt()) return Route.On2Lesson(it.groupValues[1].toInt())
} }

View file

@ -31,6 +31,7 @@ class Content(root: JSONObject) {
val hw = raw.arr("hw") val hw = raw.arr("hw")
val on = raw.arr("on") val on = raw.arr("on")
val on2 = raw.arr("on2") val on2 = raw.arr("on2")
val on3 = raw.arr("on3")
val mluvnice = raw.arr("mluvnice") val mluvnice = raw.arr("mluvnice")
val books = raw.arr("books") val books = raw.arr("books")
val changelog = raw.arr("changelog") val changelog = raw.arr("changelog")
@ -40,6 +41,7 @@ class Content(root: JSONObject) {
fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id } fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id }
fun onLesson(id: Int) = on.firstOrNull { it.int("id") == id } fun onLesson(id: Int) = on.firstOrNull { it.int("id") == id }
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 mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n } fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n }
fun book(id: String) = books.firstOrNull { it.str("id") == id } fun book(id: String) = books.firstOrNull { it.str("id") == id }

View file

@ -38,6 +38,9 @@ class ProgressStore(context: Context) {
fun on2Done(id: Int) = prefs.getBoolean("on2.$id", false) fun on2Done(id: Int) = prefs.getBoolean("on2.$id", false)
fun markOn2(id: Int) { prefs.edit().putBoolean("on2.$id", true).apply() } fun markOn2(id: Int) { prefs.edit().putBoolean("on2.$id", true).apply() }
fun on3Done(id: Int) = prefs.getBoolean("on3.$id", false)
fun markOn3(id: Int) { prefs.edit().putBoolean("on3.$id", true).apply() }
fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false)
fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() }
@ -92,6 +95,14 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
content.on3.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.on3Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
sum.totalEx += content.mluvnice.size sum.totalEx += content.mluvnice.size
sum.openUnits += 1 sum.openUnits += 1
val md = content.mluvnice.count { p.mluvDone(it.int("id")) } val md = content.mluvnice.count { p.mluvDone(it.int("id")) }

View file

@ -872,6 +872,18 @@ fun On2QuizScreen(vm: AppViewModel, id: Int) {
DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn2(id) } DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn2(id) }
} }
@Composable
fun On3QuizScreen(vm: AppViewModel, id: Int) {
val lesson = vm.content.on3Lesson(id) ?: return
val spec = J(org.json.JSONObject().apply {
put("type", "choice")
put("title", vm.tr(lesson.str("exTitleKey")))
put("sub", lesson.str("quizHeadKey"))
put("questions", lesson.o.optJSONArray("quiz"))
})
DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn3(id) }
}
@Composable @Composable
fun HwQuizScreen(vm: AppViewModel, id: Int) { fun HwQuizScreen(vm: AppViewModel, id: Int) {
val lesson = vm.content.hwLesson(id) ?: return val lesson = vm.content.hwLesson(id) ?: return

View file

@ -300,10 +300,11 @@ 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")) }
} }
"onyears", "onmap", "on2map" -> { "onyears", "onmap", "on2map", "on3map" -> {
sum.totalEx = vm.content.on.size + vm.content.on2.size sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.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")) } +
vm.content.on2.count { vm.progress.on2Done(it.int("id")) } vm.content.on2.count { vm.progress.on2Done(it.int("id")) } +
vm.content.on3.count { vm.progress.on3Done(it.int("id")) }
} }
"czechmap" -> { "czechmap" -> {
sum.totalEx = vm.content.mluvnice.size sum.totalEx = vm.content.mluvnice.size
@ -494,7 +495,7 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
} }
add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra = "rocnik") add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra = "rocnik")
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"), "onyears", locked = true, extra = "rocnik") add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra = "rocnik")
add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik") add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik")
vm.content.on.forEach { l -> vm.content.on.forEach { l ->
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")
@ -504,6 +505,10 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit${l.int("id")}", extra = "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit${l.int("id")}", extra = "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex${l.int("id")}") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex${l.int("id")}")
} }
vm.content.on3.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on3unit${l.int("id")}", extra = "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on3ex${l.int("id")}")
}
add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice") add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice")
vm.content.mluvnice.forEach { m -> add(m.str("name"), vm.tr("Mluvnice"), "mluve${m.int("id")}") } vm.content.mluvnice.forEach { m -> add(m.str("name"), vm.tr("Mluvnice"), "mluve${m.int("id")}") }
add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist") add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
@ -609,8 +614,9 @@ fun OnYearsScreen(vm: AppViewModel) {
item { item {
NavRow(title = vm.tr("on_year1"), subtitle = vm.tr("on_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.OnMap) } NavRow(title = vm.tr("on_year1"), subtitle = vm.tr("on_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.OnMap) }
NavRow(title = vm.tr("on_year2"), subtitle = vm.tr("on2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On2Map) } NavRow(title = vm.tr("on_year2"), subtitle = vm.tr("on2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On2Map) }
NavRow(title = vm.tr("on_year3"), subtitle = vm.tr("on3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On3Map) }
} }
items(listOf("on_year3", "on_year4")) { key -> items(listOf("on_year4")) { key ->
NavRow(title = vm.tr(key), subtitle = vm.tr("on_year_locked_sub"), locked = true, onClick = null) NavRow(title = vm.tr(key), subtitle = vm.tr("on_year_locked_sub"), locked = true, onClick = null)
} }
} }

View file

@ -9,10 +9,12 @@
--- 2026-10-02 --- 2026-10-02
cs: Občanská nauka: 1. ročník, 17 lekcí (člověk v lidském společenství) cs: Občanská nauka: 1. ročník, 17 lekcí (člověk v lidském společenství)
cs: Občanská nauka: 2. ročník, 16 lekcí (člověk jako občan) cs: Občanská nauka: 2. ročník, 16 lekcí (člověk jako občan)
cs: Občanská nauka: 3. a 4. ročník jsou zatím zamčené cs: Občanská nauka: 3. ročník, 16 lekcí (člověk a právo)
cs: Občanská nauka: 4. ročník je zatím zamčený
en: Civics: year 1, 17 lessons (the individual and society) en: Civics: year 1, 17 lessons (the individual and society)
en: Civics: year 2, 16 lessons (the citizen in a democratic state) en: Civics: year 2, 16 lessons (the citizen in a democratic state)
en: Civics: years 3 and 4 are still locked en: Civics: year 3, 16 lessons (the individual and the law)
en: Civics: year 4 is still locked
--- 2026-09-25 --- 2026-09-25
cs: Technické vybavení: pět lekcí o digitalizaci zvuku (vzorkování, kvantování, PCM) cs: Technické vybavení: pět lekcí o digitalizaci zvuku (vzorkování, kvantování, PCM)

View file

@ -138,6 +138,11 @@ final class AppModel: ObservableObject {
refresh() refresh()
} }
func markOn3(_ id: Int) {
progress.markOn3(id)
refresh()
}
func markMluv(_ n: Int) { func markMluv(_ n: Int) {
progress.markMluv(n) progress.markMluv(n)
refresh() refresh()

View file

@ -33,6 +33,7 @@ final class Content {
let hw: [J] let hw: [J]
let on: [J] let on: [J]
let on2: [J] let on2: [J]
let on3: [J]
let mluvnice: [J] let mluvnice: [J]
let books: [J] let books: [J]
let changelog: [J] let changelog: [J]
@ -66,6 +67,7 @@ final class Content {
hw = raw.arr("hw") hw = raw.arr("hw")
on = raw.arr("on") on = raw.arr("on")
on2 = raw.arr("on2") on2 = raw.arr("on2")
on3 = raw.arr("on3")
mluvnice = raw.arr("mluvnice") mluvnice = raw.arr("mluvnice")
books = raw.arr("books") books = raw.arr("books")
changelog = raw.arr("changelog") changelog = raw.arr("changelog")
@ -76,6 +78,7 @@ final class Content {
func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } } func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } }
func onLesson(_ id: Int) -> J? { on.first { $0.int("id") == id } } func onLesson(_ id: Int) -> J? { on.first { $0.int("id") == id } }
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 mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } } func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
func book(_ id: String) -> J? { books.first { $0.str("id") == id } } func book(_ id: String) -> J? { books.first { $0.str("id") == id } }

View file

@ -40,6 +40,9 @@ final class ProgressStore {
func on2Done(_ id: Int) -> Bool { defaults.bool(forKey: "on2.\(id)") } func on2Done(_ id: Int) -> Bool { defaults.bool(forKey: "on2.\(id)") }
func markOn2(_ id: Int) { defaults.set(true, forKey: "on2.\(id)") } func markOn2(_ id: Int) { defaults.set(true, forKey: "on2.\(id)") }
func on3Done(_ id: Int) -> Bool { defaults.bool(forKey: "on3.\(id)") }
func markOn3(_ id: Int) { defaults.set(true, forKey: "on3.\(id)") }
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
@ -99,6 +102,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
for l in content.on3 {
sum.totalEx += 1
sum.openUnits += 1
if p.on3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
sum.totalEx += content.mluvnice.count sum.totalEx += content.mluvnice.count
sum.openUnits += 1 sum.openUnits += 1
let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count

View file

@ -30,6 +30,9 @@ enum Route: Hashable {
case on2Map case on2Map
case on2Lesson(Int) case on2Lesson(Int)
case on2Ex(Int) case on2Ex(Int)
case on3Map
case on3Lesson(Int)
case on3Ex(Int)
case czechMap case czechMap
case mluvnice case mluvnice
case mluvEx(Int) case mluvEx(Int)
@ -62,6 +65,9 @@ enum Route: Hashable {
case .on2Map: return "on2map" case .on2Map: return "on2map"
case .on2Lesson(let id): return "on2unit\(id)" case .on2Lesson(let id): return "on2unit\(id)"
case .on2Ex(let id): return "on2ex\(id)" case .on2Ex(let id): return "on2ex\(id)"
case .on3Map: return "on3map"
case .on3Lesson(let id): return "on3unit\(id)"
case .on3Ex(let id): return "on3ex\(id)"
case .czechMap: return "czechmap" case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice" case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)" case .mluvEx(let n): return "mluve\(n)"
@ -124,6 +130,12 @@ enum Route: Hashable {
return [.onYears, .on2Map, .on2Lesson(id)] return [.onYears, .on2Map, .on2Lesson(id)]
case .on2Ex(let id): case .on2Ex(let id):
return [.onYears, .on2Map, .on2Lesson(id), .on2Ex(id)] return [.onYears, .on2Map, .on2Lesson(id), .on2Ex(id)]
case .on3Map:
return [.onYears, .on3Map]
case .on3Lesson(let id):
return [.onYears, .on3Map, .on3Lesson(id)]
case .on3Ex(let id):
return [.onYears, .on3Map, .on3Lesson(id), .on3Ex(id)]
case .czechMap: case .czechMap:
return [.czechMap] return [.czechMap]
case .mluvnice: case .mluvnice:
@ -155,6 +167,7 @@ func routeFromPage(_ page: String) -> Route? {
case "onyears": return .onYears case "onyears": return .onYears
case "onmap": return .onMap case "onmap": return .onMap
case "on2map": return .on2Map case "on2map": return .on2Map
case "on3map": return .on3Map
case "czechmap": return .czechMap case "czechmap": return .czechMap
case "mluvnice": return .mluvnice case "mluvnice": return .mluvnice
case "readinglist": return .readingList case "readinglist": return .readingList
@ -198,6 +211,12 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) {
return .hwEx(n) return .hwEx(n)
} }
if let m = page.wholeMatch(of: /on3unit(\d+)/), let n = Int(m.1) {
return .on3Lesson(n)
}
if let m = page.wholeMatch(of: /on3ex(\d+)/), let n = Int(m.1) {
return .on3Ex(n)
}
if let m = page.wholeMatch(of: /on2unit(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /on2unit(\d+)/), let n = Int(m.1) {
return .on2Lesson(n) return .on2Lesson(n)
} }

View file

@ -663,6 +663,25 @@ struct On2QuizScreen: View {
} }
} }
struct On3QuizScreen: View {
@ObservedObject var vm: AppModel
let id: Int
var body: some View {
if let lesson = vm.content.on3Lesson(id) {
let spec = J([
"type": "choice",
"title": vm.tr(lesson.str("exTitleKey")),
"sub": lesson.str("quizHeadKey"),
"questions": lesson.rawArray("quiz"),
])
DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
vm.markOn3(id)
}
}
}
}
struct HwQuizScreen: View { struct HwQuizScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
let id: Int let id: Int

View file

@ -423,10 +423,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 "onyears", "onmap", "on2map": case "onyears", "onmap", "on2map", "on3map":
sum.totalEx = vm.content.on.count + vm.content.on2.count sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.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
+ vm.content.on2.filter { vm.progress.on2Done($0.int("id")) }.count + vm.content.on2.filter { vm.progress.on2Done($0.int("id")) }.count
+ vm.content.on3.filter { vm.progress.on3Done($0.int("id")) }.count
case "czechmap": case "czechmap":
sum.totalEx = vm.content.mluvnice.count sum.totalEx = vm.content.mluvnice.count
sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count
@ -567,7 +568,7 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
} }
add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik") add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik")
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"), "onyears", locked: true, extra: "rocnik") add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik") add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik")
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")
@ -577,6 +578,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit\(l.int("id"))", extra: "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex\(l.int("id"))") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex\(l.int("id"))")
} }
for l in vm.content.on3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on3unit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on3ex\(l.int("id"))")
}
add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice") add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice")
add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist") add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
for b in vm.content.books { for b in vm.content.books {
@ -804,7 +809,10 @@ struct OnYearsScreen: View {
NavigationLink(value: Route.on2Map) { NavigationLink(value: Route.on2Map) {
ListRowLabel(title: vm.tr("on_year2"), subtitle: vm.tr("on2_sub")) ListRowLabel(title: vm.tr("on_year2"), subtitle: vm.tr("on2_sub"))
} }
ForEach(["on_year3", "on_year4"], id: \.self) { key in NavigationLink(value: Route.on3Map) {
ListRowLabel(title: vm.tr("on_year3"), subtitle: vm.tr("on3_sub"))
}
ForEach(["on_year4"], id: \.self) { key in
Label { Label {
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("on_year_locked_sub")) ListRowLabel(title: vm.tr(key), subtitle: vm.tr("on_year_locked_sub"))
} icon: { } icon: {
@ -1431,6 +1439,27 @@ struct RouteDestination: View {
} }
case .on2Ex(let id): case .on2Ex(let id):
On2QuizScreen(vm: vm, id: id) On2QuizScreen(vm: vm, id: id)
case .on3Map:
LessonListScreen(
vm: vm,
title: vm.tr("on_year3"),
subtitle: vm.tr("on3_sub"),
rows: vm.content.on3.map { l in
let id = l.int("id")
return LessonRow(
id: "o3\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.on3Done(id),
destination: .on3Lesson(id)
)
}
)
case .on3Lesson(let id):
if let l = vm.content.on3Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on3Ex(id))
}
case .on3Ex(let id):
On3QuizScreen(vm: vm, id: id)
case .czechMap: case .czechMap:
CzechMapScreen(vm: vm) CzechMapScreen(vm: vm)
case .mluvnice: case .mluvnice:

View file

@ -1108,6 +1108,18 @@ def main() -> int:
"quiz": choice_items(A(f"build_on2_unit{i}_exercise_page.qs")), "quiz": choice_items(A(f"build_on2_unit{i}_exercise_page.qs")),
}) })
on3_lessons = []
for i in range(1, 17):
on3_lessons.append({
"id": i,
"titleKey": f"on3_unit{i}",
"subKey": f"on3_unit{i}_sub",
"exTitleKey": f"on3_ex{i}_title",
"quizHeadKey": f"on3_quiz{i}_head",
"slides": slides_items(A(f"build_on3_unit{i}_page.slides")),
"quiz": choice_items(A(f"build_on3_unit{i}_exercise_page.qs")),
})
hw_lessons = [] hw_lessons = []
for i in range(1, 30): for i in range(1, 30):
hw_lessons.append({ hw_lessons.append({
@ -1528,6 +1540,7 @@ def main() -> int:
"hw": hw_lessons, "hw": hw_lessons,
"on": on_lessons, "on": on_lessons,
"on2": on2_lessons, "on2": on2_lessons,
"on3": on3_lessons,
"mluvnice": mluvnice, "mluvnice": mluvnice,
"books": books, "books": books,
"changelog": parse_changelog(ROOT / "data" / "changelog.txt"), "changelog": parse_changelog(ROOT / "data" / "changelog.txt"),
@ -1564,6 +1577,7 @@ def main() -> int:
print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}") print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}")
print(f" on lessons: {sum(1 for L in on_lessons if L['slides'])}") print(f" on lessons: {sum(1 for L in on_lessons if L['slides'])}")
print(f" on2 lessons: {sum(1 for L in on2_lessons if L['slides'])}") print(f" on2 lessons: {sum(1 for L in on2_lessons if L['slides'])}")
print(f" on3 lessons: {sum(1 for L in on3_lessons if L['slides'])}")
print(f" mluvnice: {len(mluvnice)}") print(f" mluvnice: {len(mluvnice)}")
print(f" books quiz items: {[len(b['quiz']) for b in books]}") print(f" books quiz items: {[len(b['quiz']) for b in books]}")
print(f" changelog entries: {len(payload.get('changelog', []))}") print(f" changelog entries: {len(payload.get('changelog', []))}")
@ -1607,6 +1621,11 @@ def main() -> int:
missing.append(f"on2{L['id']} slides") missing.append(f"on2{L['id']} slides")
if not L["quiz"]: if not L["quiz"]:
missing.append(f"on2{L['id']} quiz") missing.append(f"on2{L['id']} quiz")
for L in on3_lessons:
if not L["slides"]:
missing.append(f"on3{L['id']} slides")
if not L["quiz"]:
missing.append(f"on3{L['id']} quiz")
for m in mluvnice: for m in mluvnice:
if m["type"] in ("typed", "reveal") and not m.get("items"): if m["type"] in ("typed", "reveal") and not m.get("items"):
missing.append(f"mluv{m['id']}") missing.append(f"mluv{m['id']}")

View file

@ -51,6 +51,7 @@ extern char *app_progress_net;
extern char *app_progress_hw; extern char *app_progress_hw;
extern char *app_progress_on; extern char *app_progress_on;
extern char *app_progress_on2; extern char *app_progress_on2;
extern char *app_progress_on3;
extern char *app_progress_mluvnice; extern char *app_progress_mluvnice;
extern char *app_progress_cetba; extern char *app_progress_cetba;
#define PROGRESS_DIR app_progress_dir #define PROGRESS_DIR app_progress_dir
@ -62,6 +63,7 @@ extern char *app_progress_cetba;
#define PROGRESS_HW app_progress_hw #define PROGRESS_HW app_progress_hw
#define PROGRESS_ON app_progress_on #define PROGRESS_ON app_progress_on
#define PROGRESS_ON2 app_progress_on2 #define PROGRESS_ON2 app_progress_on2
#define PROGRESS_ON3 app_progress_on3
#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 */
@ -123,6 +125,8 @@ extern char *app_progress_cetba;
#define ON_LESSONS 17 #define ON_LESSONS 17
#define ON2_UNITS 16 #define ON2_UNITS 16
#define ON2_LESSONS 16 #define ON2_LESSONS 16
#define ON3_UNITS 16
#define ON3_LESSONS 16
#define HW_SLIDES 3 #define HW_SLIDES 3
#define HW2_SLIDES 6 #define HW2_SLIDES 6
#define HW3_SLIDES 2 #define HW3_SLIDES 2
@ -767,6 +771,8 @@ extern NetLesson on_lessons[ON_LESSONS];
extern GtkWidget *on_nodes[ON_UNITS]; extern GtkWidget *on_nodes[ON_UNITS];
extern NetLesson on2_lessons[ON2_LESSONS]; extern NetLesson on2_lessons[ON2_LESSONS];
extern GtkWidget *on2_nodes[ON2_UNITS]; extern GtkWidget *on2_nodes[ON2_UNITS];
extern NetLesson on3_lessons[ON3_LESSONS];
extern GtkWidget *on3_nodes[ON3_UNITS];
extern GtkWidget *net_note_host; extern GtkWidget *net_note_host;
extern int net_note_last_w; extern int net_note_last_w;
extern GtkWidget *net_scroll; extern GtkWidget *net_scroll;
@ -872,6 +878,14 @@ void on2_rail_theme_reset(void);
void on2_lessons_apply_lang(void); void on2_lessons_apply_lang(void);
GtkWidget *build_on2map_page(void); GtkWidget *build_on2map_page(void);
void add_on2_pages(GtkStack *stack); void add_on2_pages(GtkStack *stack);
void on3_load_progress(void);
void on3_save_progress(void);
void mark_on3_done(int lesson_id);
void refresh_on3_completion_ui(void);
void on3_rail_theme_reset(void);
void on3_lessons_apply_lang(void);
GtkWidget *build_on3map_page(void);
void add_on3_pages(GtkStack *stack);
void czech_rail_theme_reset(void); void czech_rail_theme_reset(void);
void hw_lessons_apply_lang(void); void hw_lessons_apply_lang(void);
GtkWidget *build_hwyears_page(void); GtkWidget *build_hwyears_page(void);

View file

@ -902,6 +902,104 @@ const TrEntry tr_ui[] = {
{"on2_ex16_title", "Kvíz: média", "Quiz: the media"}, {"on2_ex16_title", "Kvíz: média", "Quiz: the media"},
{"on2_quiz16_head", "Kvíz k médiím", {"on2_quiz16_head", "Kvíz k médiím",
"Quiz on the media"}, "Quiz on the media"},
{"on3_sub", "Člověk a právo. Vyberte lekci na cestě.",
"The individual and the law. Pick a lesson on the path."},
{"on3_unit1", "Právo a spravedlnost", "Law and justice"},
{"on3_unit1_sub", "2 snímky o právu a spravedlnosti a pak kvíz.",
"2 slides on law and justice, then a quiz."},
{"on3_ex1_title", "Kvíz: právo a spravedlnost", "Quiz: law and justice"},
{"on3_quiz1_head", "Kvíz k právu a spravedlnosti",
"Quiz on law and justice"},
{"on3_unit2", "Právní stát", "The rule of law"},
{"on3_unit2_sub", "2 snímky o právním státu a pak kvíz.",
"2 slides on the rule of law, then a quiz."},
{"on3_ex2_title", "Kvíz: právní stát", "Quiz: the rule of law"},
{"on3_quiz2_head", "Kvíz k právnímu státu",
"Quiz on the rule of law"},
{"on3_unit3", "Právní řád", "The legal order"},
{"on3_unit3_sub", "2 snímky o předpisech a odvětvích práva a pak kvíz.",
"2 slides on legal rules and branches of law, then a quiz."},
{"on3_ex3_title", "Kvíz: právní řád", "Quiz: the legal order"},
{"on3_quiz3_head", "Kvíz k právnímu řádu",
"Quiz on the legal order"},
{"on3_unit4", "Osoby a svéprávnost", "Persons and legal capacity"},
{"on3_unit4_sub", "2 snímky o osobách, svéprávnosti a odpovědnosti a pak kvíz.",
"2 slides on persons, capacity and liability, then a quiz."},
{"on3_ex4_title", "Kvíz: osoby a svéprávnost", "Quiz: persons and capacity"},
{"on3_quiz4_head", "Kvíz k osobám a svéprávnosti",
"Quiz on persons and capacity"},
{"on3_unit5", "Vlastnictví a škoda", "Ownership and damage"},
{"on3_unit5_sub", "2 snímky o vlastnictví, duševním vlastnictví a škodě a pak kvíz.",
"2 slides on ownership, intellectual property and damage, then a quiz."},
{"on3_ex5_title", "Kvíz: vlastnictví a škoda", "Quiz: ownership and damage"},
{"on3_quiz5_head", "Kvíz k vlastnictví a škodě",
"Quiz on ownership and damage"},
{"on3_unit6", "Smlouvy", "Contracts"},
{"on3_unit6_sub", "2 snímky o smlouvách a pak kvíz.",
"2 slides on contracts, then a quiz."},
{"on3_ex6_title", "Kvíz: smlouvy", "Quiz: contracts"},
{"on3_quiz6_head", "Kvíz ke smlouvám",
"Quiz on contracts"},
{"on3_unit7", "Spotřebitel", "The consumer"},
{"on3_unit7_sub", "2 snímky o reklamaci a odstoupení od smlouvy a pak kvíz.",
"2 slides on complaints and withdrawing from a contract, then a quiz."},
{"on3_ex7_title", "Kvíz: spotřebitel", "Quiz: the consumer"},
{"on3_quiz7_head", "Kvíz ke spotřebiteli",
"Quiz on the consumer"},
{"on3_unit8", "Dědictví", "Inheritance"},
{"on3_unit8_sub", "2 snímky o dědění a dluzích a pak kvíz.",
"2 slides on inheritance and debts, then a quiz."},
{"on3_ex8_title", "Kvíz: dědictví", "Quiz: inheritance"},
{"on3_quiz8_head", "Kvíz k dědictví",
"Quiz on inheritance"},
{"on3_unit9", "Rodina a právo", "Family and the law"},
{"on3_unit9_sub", "2 snímky o manželství, partnerství a péči o dítě a pak kvíz.",
"2 slides on marriage, partnership and care of a child, then a quiz."},
{"on3_ex9_title", "Kvíz: rodina a právo", "Quiz: family and the law"},
{"on3_quiz9_head", "Kvíz k rodině a právu",
"Quiz on family and the law"},
{"on3_unit10", "Pracovní právo", "Employment law"},
{"on3_unit10_sub", "2 snímky o pracovní smlouvě, výpovědi a bezpečnosti a pak kvíz.",
"2 slides on the employment contract, notice and safety, then a quiz."},
{"on3_ex10_title", "Kvíz: pracovní právo", "Quiz: employment law"},
{"on3_quiz10_head", "Kvíz k pracovnímu právu",
"Quiz on employment law"},
{"on3_unit11", "Soustava soudů", "The court system"},
{"on3_unit11_sub", "2 snímky o soustavě soudů a pak kvíz.",
"2 slides on the court system, then a quiz."},
{"on3_ex11_title", "Kvíz: soustava soudů", "Quiz: the court system"},
{"on3_quiz11_head", "Kvíz k soustavě soudů",
"Quiz on the court system"},
{"on3_unit12", "Soudní a správní řízení", "Court and administrative proceedings"},
{"on3_unit12_sub", "2 snímky o žalobě, odvolání a správním řízení a pak kvíz.",
"2 slides on lawsuits, appeals and administrative proceedings, then a quiz."},
{"on3_ex12_title", "Kvíz: řízení", "Quiz: proceedings"},
{"on3_quiz12_head", "Kvíz k soudnímu a správnímu řízení",
"Quiz on court and administrative proceedings"},
{"on3_unit13", "Právnická povolání", "Legal professions"},
{"on3_unit13_sub", "2 snímky o soudci, žalobci, advokátovi, notáři a exekutorovi a pak kvíz.",
"2 slides on judges, prosecutors, lawyers, notaries and bailiffs, then a quiz."},
{"on3_ex13_title", "Kvíz: právnická povolání", "Quiz: legal professions"},
{"on3_quiz13_head", "Kvíz k právnickým povoláním",
"Quiz on legal professions"},
{"on3_unit14", "Trestný čin a přestupek", "Crimes and misdemeanours"},
{"on3_unit14_sub", "2 snímky o trestném činu, přestupku a vině a pak kvíz.",
"2 slides on crimes, misdemeanours and guilt, then a quiz."},
{"on3_ex14_title", "Kvíz: trestný čin a přestupek", "Quiz: crimes and misdemeanours"},
{"on3_quiz14_head", "Kvíz k trestnému činu a přestupku",
"Quiz on crimes and misdemeanours"},
{"on3_unit15", "Tresty a trestní řízení", "Punishments and criminal procedure"},
{"on3_unit15_sub", "2 snímky o trestech, vězení a průběhu řízení a pak kvíz.",
"2 slides on punishments, prison and the course of a case, then a quiz."},
{"on3_ex15_title", "Kvíz: tresty a řízení", "Quiz: punishments and procedure"},
{"on3_quiz15_head", "Kvíz k trestům a trestnímu řízení",
"Quiz on punishments and criminal procedure"},
{"on3_unit16", "Mladiství, oběť a svědek", "Juveniles, victims and witnesses"},
{"on3_unit16_sub", "2 snímky o mladistvých a o tom, co dělat jako oběť nebo svědek, a pak kvíz.",
"2 slides on juveniles and what to do as a victim or witness, then a quiz."},
{"on3_ex16_title", "Kvíz: mladiství, oběť a svědek", "Quiz: juveniles, victims and witnesses"},
{"on3_quiz16_head", "Kvíz k mladistvým, oběti a svědkovi",
"Quiz on juveniles, victims and witnesses"},
{"czech_sub", "Vyberte oblast a začněte procvičovat.", {"czech_sub", "Vyberte oblast a začněte procvičovat.",
"Pick an area and start practicing."}, "Pick an area and start practicing."},
{"Literatura", "Literatura", "Literature"}, {"Literatura", "Literatura", "Literature"},
@ -1927,6 +2025,7 @@ void net_lessons_apply_lang(void);
void hw_lessons_apply_lang(void); void hw_lessons_apply_lang(void);
void on_lessons_apply_lang(void); 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 net_rail_theme_reset(void); void net_rail_theme_reset(void);
void apply_language(void) { void apply_language(void) {
@ -1943,5 +2042,6 @@ void apply_language(void) {
hw_lessons_apply_lang(); hw_lessons_apply_lang();
on_lessons_apply_lang(); on_lessons_apply_lang();
on2_lessons_apply_lang(); on2_lessons_apply_lang();
on3_lessons_apply_lang();
search_apply_lang(); search_apply_lang();
} }

View file

@ -386,6 +386,7 @@ static gboolean run_smoke_test(gpointer data) {
static const char *pages[] = { static const char *pages[] = {
"welcome", "subjects", "roadmap", "stats", "welcome", "subjects", "roadmap", "stats",
"netyears", "netmap", "hwyears", "hwmap", "onyears", "onmap", "on2map", "netyears", "netmap", "hwyears", "hwmap", "onyears", "onmap", "on2map",
"on3map",
"czechmap", "czechmap",
"mluvnice", "readinglist", "mluvnice", "readinglist",
"cetba1984", "cetba1984quiz", "cetba1984dej", "cetba1984", "cetba1984quiz", "cetba1984dej",
@ -399,6 +400,7 @@ static gboolean run_smoke_test(gpointer data) {
"hwunit29", "hwex29", "hwunit29", "hwex29",
"onunit1", "onex1", "onunit17", "onex17", "onunit1", "onex1", "onunit17", "onex17",
"on2unit1", "on2ex1", "on2unit16", "on2ex16", "on2unit1", "on2ex1", "on2unit16", "on2ex16",
"on3unit1", "on3ex1", "on3unit16", "on3ex16",
"unit1", "unit2", "unit3", "unit1", "unit2", "unit3",
"u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15",
"u1vocab", "u2vocab", "u3vocab", "u1vocab", "u2vocab", "u3vocab",
@ -514,6 +516,8 @@ void activate(GtkApplication *app, gpointer user_data) {
add_on_pages(main_stack); add_on_pages(main_stack);
gtk_stack_add_named(main_stack, build_on2map_page(), "on2map"); gtk_stack_add_named(main_stack, build_on2map_page(), "on2map");
add_on2_pages(main_stack); add_on2_pages(main_stack);
gtk_stack_add_named(main_stack, build_on3map_page(), "on3map");
add_on3_pages(main_stack);
gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap"); gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap");
gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice"); gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice");
for (int n = 1; n <= 20; n++) { for (int n = 1; n <= 20; n++) {

View file

@ -3,8 +3,7 @@
/* Year 1 of Občanská nauka. /* Year 1 of Občanská nauka.
* Thematic unit "Člověk v lidském společenství" from the SOV framework * Thematic unit "Člověk v lidském společenství" from the SOV framework
* (společenskovědní vzdělávání) and the MŠMT model syllabus, which schools * (společenskovědní vzdělávání) and the MŠMT model syllabus, which schools
* place in the first year. Year 2 is the citizen and the state; years 3 and 4 * place in the first year. Years 2 and 3 are open; year 4 stays locked. */
* stay locked. */
#define ON_ENTRY(n) \ #define ON_ENTRY(n) \
{ .n_slides = 2, .unit_page = "onunit" #n, .ex_page = "onex" #n } { .n_slides = 2, .unit_page = "onunit" #n, .ex_page = "onex" #n }
@ -413,7 +412,7 @@ GtkWidget *build_onyears_page(void) {
GtkWidget *scroll; GtkWidget *scroll;
GtkWidget *list; GtkWidget *list;
int y; int y;
static const char *targets[] = {"onmap", "on2map", NULL, NULL}; static const char *targets[] = {"onmap", "on2map", "on3map", NULL};
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_hexpand(page, TRUE); gtk_widget_set_hexpand(page, TRUE);

View file

@ -3,8 +3,8 @@
/* Year 2 of Občanská nauka. /* Year 2 of Občanská nauka.
* Thematic unit "Člověk jako občan v demokratickém státě" from the MŠMT * Thematic unit "Člověk jako občan v demokratickém státě" from the MŠMT
* model syllabus (č.j. 18 396/2002-23) and the SOV civic framework, which * model syllabus (č.j. 18 396/2002-23) and the SOV civic framework, which
* schools place in the second year. Law, the economy and philosophy stay * schools place in the second year. Law is year 3; the economy and
* in later years. */ * philosophy stay in the fourth year. */
#define ON2_ENTRY(n) \ #define ON2_ENTRY(n) \
{ .n_slides = 2, .unit_page = "on2unit" #n, .ex_page = "on2ex" #n } { .n_slides = 2, .unit_page = "on2unit" #n, .ex_page = "on2ex" #n }

1910
src/obcanka3.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -56,6 +56,7 @@ void load_progress(void) {
hw_load_progress(); hw_load_progress();
on_load_progress(); on_load_progress();
on2_load_progress(); on2_load_progress();
on3_load_progress();
} }
/* Redraw the exercise rails of every unit that already has a page. */ /* Redraw the exercise rails of every unit that already has a page. */
@ -425,8 +426,8 @@ void mark_on_done(int lesson_id) {
} }
void progress_for_on(ProgressSum *out) { void progress_for_on(ProgressSum *out) {
out->total_ex += ON_LESSONS + ON2_LESSONS; out->total_ex += ON_LESSONS + ON2_LESSONS + ON3_LESSONS;
out->open_units += ON_LESSONS + ON2_LESSONS; out->open_units += ON_LESSONS + ON2_LESSONS + ON3_LESSONS;
for (int i = 0; i < ON_LESSONS; i++) { for (int i = 0; i < ON_LESSONS; i++) {
if (on_lessons[i].done) { if (on_lessons[i].done) {
out->done_ex++; out->done_ex++;
@ -439,6 +440,12 @@ void progress_for_on(ProgressSum *out) {
out->done_units++; out->done_units++;
} }
} }
for (int i = 0; i < ON3_LESSONS; i++) {
if (on3_lessons[i].done) {
out->done_ex++;
out->done_units++;
}
}
} }
void on2_load_progress(void) { void on2_load_progress(void) {
@ -520,3 +527,83 @@ void mark_on2_done(int lesson_id) {
refresh_on2_completion_ui(); refresh_on2_completion_ui();
refresh_stats_ui(); refresh_stats_ui();
} }
void on3_load_progress(void) {
GKeyFile *kf = g_key_file_new();
GError *err = NULL;
if (!g_key_file_load_from_file(kf, PROGRESS_ON3, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
return;
}
for (int i = 0; i < ON3_LESSONS; i++) {
gchar key[8];
g_snprintf(key, sizeof(key), "%d", i + 1);
on3_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
g_key_file_free(kf);
}
void on3_save_progress(void) {
GKeyFile *kf = g_key_file_new();
for (int i = 0; i < ON3_LESSONS; i++) {
gchar key[8];
g_snprintf(key, sizeof(key), "%d", i + 1);
g_key_file_set_boolean(kf, "done", key, on3_lessons[i].done);
}
if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
g_key_file_free(kf);
return;
}
gchar *data = g_key_file_to_data(kf, NULL, NULL);
g_key_file_free(kf);
if (data) {
GError *err = NULL;
g_file_set_contents(PROGRESS_ON3, data, -1, &err);
if (err)
g_error_free(err);
g_free(data);
}
}
void refresh_on3_completion_ui(void) {
for (int i = 0; i < ON3_LESSONS; i++) {
GtkWidget *node = on3_nodes[i];
GtkWidget *icon = on3_lessons[i].done_icon;
if (!node)
continue;
if (on3_lessons[i].done) {
gtk_widget_remove_css_class(node, "current");
gtk_widget_add_css_class(node, "done");
if (icon)
gtk_widget_set_visible(icon, TRUE);
} else {
gtk_widget_remove_css_class(node, "done");
gtk_widget_add_css_class(node, "current");
if (icon)
gtk_widget_set_visible(icon, FALSE);
}
}
}
void mark_on3_done(int lesson_id) {
if (lesson_id < 0 || lesson_id >= ON3_LESSONS)
return;
if (on3_lessons[lesson_id].done)
return;
on3_lessons[lesson_id].done = TRUE;
on3_save_progress();
refresh_on3_completion_ui();
refresh_stats_ui();
}

View file

@ -204,7 +204,7 @@ static void catalog_rebuild(void) {
catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik"); catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik");
catalog_add_key("on_year2", "search_lesson", "on2map", FALSE, 1, "rocnik"); catalog_add_key("on_year2", "search_lesson", "on2map", FALSE, 1, "rocnik");
catalog_add_key("on_year3", "search_lesson", "onyears", TRUE, 1, "rocnik"); catalog_add_key("on_year3", "search_lesson", "on3map", FALSE, 1, "rocnik");
catalog_add_key("on_year4", "search_lesson", "onyears", TRUE, 1, "rocnik"); catalog_add_key("on_year4", "search_lesson", "onyears", TRUE, 1, "rocnik");
for (i = 1; i <= ON_LESSONS; i++) { for (i = 1; i <= ON_LESSONS; i++) {
g_snprintf(key, sizeof(key), "on_unit%d", i); g_snprintf(key, sizeof(key), "on_unit%d", i);
@ -220,6 +220,13 @@ static void catalog_rebuild(void) {
g_snprintf(page, sizeof(page), "on2ex%d", i); g_snprintf(page, sizeof(page), "on2ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
} }
for (i = 1; i <= ON3_LESSONS; i++) {
g_snprintf(key, sizeof(key), "on3_unit%d", i);
g_snprintf(page, sizeof(page), "on3unit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "obcanka");
g_snprintf(page, sizeof(page), "on3ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
}
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");

View file

@ -428,6 +428,7 @@ void apply_theme(void) {
hw_rail_theme_reset(); hw_rail_theme_reset();
on_rail_theme_reset(); on_rail_theme_reset();
on2_rail_theme_reset(); on2_rail_theme_reset();
on3_rail_theme_reset();
czech_rail_theme_reset(); czech_rail_theme_reset();
book_rail_theme_reset(); book_rail_theme_reset();
} }

View file

@ -22,6 +22,7 @@ char *app_progress_net;
char *app_progress_hw; char *app_progress_hw;
char *app_progress_on; char *app_progress_on;
char *app_progress_on2; char *app_progress_on2;
char *app_progress_on3;
char *app_progress_mluvnice; char *app_progress_mluvnice;
char *app_progress_cetba; char *app_progress_cetba;
@ -162,6 +163,7 @@ static void set_progress_paths(const char *write_root) {
g_free(app_progress_hw); g_free(app_progress_hw);
g_free(app_progress_on); g_free(app_progress_on);
g_free(app_progress_on2); g_free(app_progress_on2);
g_free(app_progress_on3);
g_free(app_progress_mluvnice); g_free(app_progress_mluvnice);
g_free(app_progress_cetba); g_free(app_progress_cetba);
@ -174,6 +176,7 @@ static void set_progress_paths(const char *write_root) {
app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL); app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL);
app_progress_on = g_build_filename(app_progress_dir, "on.conf", NULL); app_progress_on = g_build_filename(app_progress_dir, "on.conf", NULL);
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_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);
} }