Fill in years 2–4 of networks and hardware.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-07 21:07:47 +02:00
commit 10980f24b5
27 changed files with 6345 additions and 64 deletions

View file

@ -63,9 +63,12 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
- **Deutsch** — učebnice (Neue Freunde, Aus aller Welt, Bei uns zu Hause) - **Deutsch** — učebnice (Neue Freunde, Aus aller Welt, Bei uns zu Hause)
a 4 ročníky, 24 lekcí od A1 k B1: poslech, čtení, psaní, doplňování a kvízy 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í** — 4 ročníky: 1. ročník má 27 lekcí
ročníky 2–4 jsou zamčené (adresy, modely, přepínače a bezdrát), ročníky 2–4 po 8 lekcích
- **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é (VLAN, směrování, firewall, návrh a maturita)
- **Technické vybavení** — 4 ročníky: 1. ročník má 29 lekcí
(architektura, data, skříň, deska, karty, disky a vstup),
ročníky 2–4 po 8 lekcích (monitory, sestavení, RAID, servery, údržba)
- **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov - **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov
středních škol), mluvnice (20 cvičení) a maturitní četba středních škol), mluvnice (20 cvičení) a maturitní četba
(82 knih: zápisky, kvíz, sestavení děje) (82 knih: zápisky, kvíz, sestavení děje)
@ -268,9 +271,12 @@ Intel Mac, another architecture, or hacking on the code →
- **Deutsch** — the textbook (Neue Freunde, Aus aller Welt, Bei uns zu Hause) - **Deutsch** — the textbook (Neue Freunde, Aus aller Welt, Bei uns zu Hause)
plus 4 years, 24 lessons from A1 to B1: listening, reading, writing, gap-fill and quizzes 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** — 4 years: year 1 has 27 lessons
years 2–4 are locked (addresses, models, switches and wireless), years 2–4 have 8 lessons each
- **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked (VLANs, routing, firewalls, design and the exam)
- **Computer hardware** — 4 years: year 1 has 29 lessons
(architecture, data, case, board, cards, drives and input),
years 2–4 have 8 lessons each (displays, assembly, RAID, servers, maintenance)
- **Czech language** — literature (4 years, 32 lessons from the secondary-school - **Czech language** — literature (4 years, 32 lessons from the secondary-school
syllabus), grammar (20 exercises) and required reading syllabus), grammar (20 exercises) and required reading
(82 books: notes, quiz, plot ordering) (82 books: notes, quiz, plot ordering)

View file

@ -156,6 +156,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
refresh() refresh()
} }
fun markCourse(course: String, id: Int) {
progress.markCourse(course, id)
refresh()
}
fun markLit(id: Int) { fun markLit(id: Int) {
progress.markLit(id) progress.markLit(id)
refresh() refresh()

View file

@ -51,7 +51,9 @@ 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.ChoiceLessonScreen
import org.maturita.maturita.ui.DeHomeScreen import org.maturita.maturita.ui.DeHomeScreen
import org.maturita.maturita.ui.ItYearScreen
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
@ -244,6 +246,30 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.NetEx -> NetQuizScreen(vm, r.id) is Route.NetEx -> NetQuizScreen(vm, r.id)
Route.Net2Map -> ItYearScreen(vm, "net2", "net_year2", "net2_sub") { Route.Net2Lesson(it) }
is Route.Net2Lesson -> {
val l = vm.content.itLesson("net2", r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Net2Ex(r.id))
}
}
is Route.Net2Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net2", r.id)) { vm.markCourse("net2", r.id) }
Route.Net3Map -> ItYearScreen(vm, "net3", "net_year3", "net3_sub") { Route.Net3Lesson(it) }
is Route.Net3Lesson -> {
val l = vm.content.itLesson("net3", r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Net3Ex(r.id))
}
}
is Route.Net3Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net3", r.id)) { vm.markCourse("net3", r.id) }
Route.Net4Map -> ItYearScreen(vm, "net4", "net_year4", "net4_sub") { Route.Net4Lesson(it) }
is Route.Net4Lesson -> {
val l = vm.content.itLesson("net4", r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Net4Ex(r.id))
}
}
is Route.Net4Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net4", r.id)) { vm.markCourse("net4", r.id) }
Route.HwYears -> HwYearsScreen(vm) Route.HwYears -> HwYearsScreen(vm)
Route.HwMap -> LessonListScreen( Route.HwMap -> LessonListScreen(
vm, vm.tr("hw_year1"), vm.tr("hw_sub"), vm, vm.tr("hw_year1"), vm.tr("hw_sub"),
@ -259,6 +285,30 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.HwEx -> HwQuizScreen(vm, r.id) is Route.HwEx -> HwQuizScreen(vm, r.id)
Route.Hw2Map -> ItYearScreen(vm, "hw2", "hw_year2", "hw2_sub") { Route.Hw2Lesson(it) }
is Route.Hw2Lesson -> {
val l = vm.content.itLesson("hw2", r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Hw2Ex(r.id))
}
}
is Route.Hw2Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw2", r.id)) { vm.markCourse("hw2", r.id) }
Route.Hw3Map -> ItYearScreen(vm, "hw3", "hw_year3", "hw3_sub") { Route.Hw3Lesson(it) }
is Route.Hw3Lesson -> {
val l = vm.content.itLesson("hw3", r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Hw3Ex(r.id))
}
}
is Route.Hw3Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw3", r.id)) { vm.markCourse("hw3", r.id) }
Route.Hw4Map -> ItYearScreen(vm, "hw4", "hw_year4", "hw4_sub") { Route.Hw4Lesson(it) }
is Route.Hw4Lesson -> {
val l = vm.content.itLesson("hw4", r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Hw4Ex(r.id))
}
}
is Route.Hw4Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw4", r.id)) { vm.markCourse("hw4", r.id) }
Route.EnYears -> EnYearsScreen(vm) Route.EnYears -> EnYearsScreen(vm)
is Route.EnMap -> LessonListScreen( is Route.EnMap -> LessonListScreen(
vm, vm.tr("en_year${r.year}"), vm.tr("en_y${r.year}_sub"), vm, vm.tr("en_year${r.year}"), vm.tr("en_y${r.year}_sub"),

View file

@ -17,10 +17,28 @@ sealed class Route(val page: String) {
data object NetMap : Route("netmap") data object NetMap : Route("netmap")
data class NetLesson(val id: Int) : Route("netunit$id") data class NetLesson(val id: Int) : Route("netunit$id")
data class NetEx(val id: Int) : Route("netex$id") data class NetEx(val id: Int) : Route("netex$id")
data object Net2Map : Route("net2map")
data class Net2Lesson(val id: Int) : Route("net2unit$id")
data class Net2Ex(val id: Int) : Route("net2ex$id")
data object Net3Map : Route("net3map")
data class Net3Lesson(val id: Int) : Route("net3unit$id")
data class Net3Ex(val id: Int) : Route("net3ex$id")
data object Net4Map : Route("net4map")
data class Net4Lesson(val id: Int) : Route("net4unit$id")
data class Net4Ex(val id: Int) : Route("net4ex$id")
data object HwYears : Route("hwyears") data object HwYears : Route("hwyears")
data object HwMap : Route("hwmap") data object HwMap : Route("hwmap")
data class HwLesson(val id: Int) : Route("hwunit$id") data class HwLesson(val id: Int) : Route("hwunit$id")
data class HwEx(val id: Int) : Route("hwex$id") data class HwEx(val id: Int) : Route("hwex$id")
data object Hw2Map : Route("hw2map")
data class Hw2Lesson(val id: Int) : Route("hw2unit$id")
data class Hw2Ex(val id: Int) : Route("hw2ex$id")
data object Hw3Map : Route("hw3map")
data class Hw3Lesson(val id: Int) : Route("hw3unit$id")
data class Hw3Ex(val id: Int) : Route("hw3ex$id")
data object Hw4Map : Route("hw4map")
data class Hw4Lesson(val id: Int) : Route("hw4unit$id")
data class Hw4Ex(val id: Int) : Route("hw4ex$id")
data object OnYears : Route("onyears") data object OnYears : Route("onyears")
data object OnMap : Route("onmap") data object OnMap : Route("onmap")
data class OnLesson(val id: Int) : Route("onunit$id") data class OnLesson(val id: Int) : Route("onunit$id")
@ -102,6 +120,8 @@ sealed class Route(val page: String) {
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 DeLesson || this is DeEx || 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 Net2Lesson || this is Net2Ex || this is Net3Lesson || this is Net3Ex || this is Net4Lesson || this is Net4Ex ||
this is Hw2Lesson || this is Hw2Ex || this is Hw3Lesson || this is Hw3Ex || this is Hw4Lesson || this is Hw4Ex ||
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 ||
@ -129,10 +149,28 @@ sealed class Route(val page: String) {
NetMap -> listOf(Subjects, NetYears, NetMap) NetMap -> listOf(Subjects, NetYears, NetMap)
is NetLesson -> listOf(Subjects, NetYears, NetMap, this) is NetLesson -> listOf(Subjects, NetYears, NetMap, this)
is NetEx -> listOf(Subjects, NetYears, NetMap, NetLesson(id), this) is NetEx -> listOf(Subjects, NetYears, NetMap, NetLesson(id), this)
Net2Map -> listOf(Subjects, NetYears, Net2Map)
is Net2Lesson -> listOf(Subjects, NetYears, Net2Map, this)
is Net2Ex -> listOf(Subjects, NetYears, Net2Map, Net2Lesson(id), this)
Net3Map -> listOf(Subjects, NetYears, Net3Map)
is Net3Lesson -> listOf(Subjects, NetYears, Net3Map, this)
is Net3Ex -> listOf(Subjects, NetYears, Net3Map, Net3Lesson(id), this)
Net4Map -> listOf(Subjects, NetYears, Net4Map)
is Net4Lesson -> listOf(Subjects, NetYears, Net4Map, this)
is Net4Ex -> listOf(Subjects, NetYears, Net4Map, Net4Lesson(id), this)
HwYears -> listOf(Subjects, HwYears) HwYears -> listOf(Subjects, HwYears)
HwMap -> listOf(Subjects, HwYears, HwMap) HwMap -> listOf(Subjects, HwYears, HwMap)
is HwLesson -> listOf(Subjects, HwYears, HwMap, this) is HwLesson -> listOf(Subjects, HwYears, HwMap, this)
is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this) is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this)
Hw2Map -> listOf(Subjects, HwYears, Hw2Map)
is Hw2Lesson -> listOf(Subjects, HwYears, Hw2Map, this)
is Hw2Ex -> listOf(Subjects, HwYears, Hw2Map, Hw2Lesson(id), this)
Hw3Map -> listOf(Subjects, HwYears, Hw3Map)
is Hw3Lesson -> listOf(Subjects, HwYears, Hw3Map, this)
is Hw3Ex -> listOf(Subjects, HwYears, Hw3Map, Hw3Lesson(id), this)
Hw4Map -> listOf(Subjects, HwYears, Hw4Map)
is Hw4Lesson -> listOf(Subjects, HwYears, Hw4Map, this)
is Hw4Ex -> listOf(Subjects, HwYears, Hw4Map, Hw4Lesson(id), this)
OnYears -> listOf(Subjects, OnYears) OnYears -> listOf(Subjects, OnYears)
OnMap -> listOf(Subjects, OnYears, OnMap) OnMap -> listOf(Subjects, OnYears, OnMap)
is OnLesson -> listOf(Subjects, OnYears, OnMap, this) is OnLesson -> listOf(Subjects, OnYears, OnMap, this)
@ -218,6 +256,12 @@ fun routeFromPage(page: String): Route? = when (page) {
"dehome" -> Route.DeHome "dehome" -> Route.DeHome
"netyears" -> Route.NetYears "netyears" -> Route.NetYears
"netmap" -> Route.NetMap "netmap" -> Route.NetMap
"net2map" -> Route.Net2Map
"net3map" -> Route.Net3Map
"net4map" -> Route.Net4Map
"hw2map" -> Route.Hw2Map
"hw3map" -> Route.Hw3Map
"hw4map" -> Route.Hw4Map
"hwyears" -> Route.HwYears "hwyears" -> Route.HwYears
"hwmap" -> Route.HwMap "hwmap" -> Route.HwMap
"onyears" -> Route.OnYears "onyears" -> Route.OnYears
@ -375,6 +419,42 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""de(\d)ex(\d+)""").matchEntire(page)?.let { Regex("""de(\d)ex(\d+)""").matchEntire(page)?.let {
return Route.DeEx(it.groupValues[1].toInt(), it.groupValues[2].toInt()) return Route.DeEx(it.groupValues[1].toInt(), it.groupValues[2].toInt())
} }
Regex("""net([234])unit(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Net3Lesson(id)
4 -> Route.Net4Lesson(id)
else -> Route.Net2Lesson(id)
}
}
Regex("""net([234])ex(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Net3Ex(id)
4 -> Route.Net4Ex(id)
else -> Route.Net2Ex(id)
}
}
Regex("""hw([234])unit(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Hw3Lesson(id)
4 -> Route.Hw4Lesson(id)
else -> Route.Hw2Lesson(id)
}
}
Regex("""hw([234])ex(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Hw3Ex(id)
4 -> Route.Hw4Ex(id)
else -> Route.Hw2Ex(id)
}
}
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

@ -29,6 +29,12 @@ class Content(root: JSONObject) {
} }
} }
val hw = raw.arr("hw") val hw = raw.arr("hw")
val net2 = raw.arr("net2")
val net3 = raw.arr("net3")
val net4 = raw.arr("net4")
val hw2 = raw.arr("hw2")
val hw3 = raw.arr("hw3")
val hw4 = raw.arr("hw4")
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 on3 = raw.arr("on3")
@ -63,6 +69,16 @@ class Content(root: JSONObject) {
fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id } fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id }
fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id } fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id }
fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id } fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id }
fun itLessons(course: String) = when (course) {
"net2" -> net2
"net3" -> net3
"net4" -> net4
"hw2" -> hw2
"hw3" -> hw3
"hw4" -> hw4
else -> emptyList()
}
fun itLesson(course: String, id: Int) = itLessons(course).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 on3Lesson(id: Int) = on3.firstOrNull { it.int("id") == id }

View file

@ -48,6 +48,8 @@ class ProgressStore(context: Context) {
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 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 markDe(year: Int, id: Int) { prefs.edit().putBoolean("de.$year.$id", true).apply() }
fun courseDone(course: String, id: Int) = prefs.getBoolean("$course.$id", false)
fun markCourse(course: String, id: Int) { prefs.edit().putBoolean("$course.$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() }
@ -119,6 +121,16 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
listOf("net2", "net3", "net4", "hw2", "hw3", "hw4").forEach { course ->
content.itLessons(course).forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.courseDone(course, l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
content.on.forEach { l -> content.on.forEach { l ->
sum.totalEx += 1 sum.totalEx += 1
sum.openUnits += 1 sum.openUnits += 1

View file

@ -1077,6 +1077,18 @@ private fun MathBoard(
} }
} }
@Composable
fun ChoiceLessonScreen(vm: AppViewModel, lesson: J?, onDone: () -> Unit) {
if (lesson == null) 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")), onDone)
}
@Composable @Composable
fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) { fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) {
val lesson = when (year) { val lesson = when (year) {

View file

@ -306,12 +306,16 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
} }
} }
"netyears" -> { "netyears" -> {
sum.totalEx = vm.content.netLessons.size val extra = listOf("net2", "net3", "net4")
sum.doneEx = vm.content.netLessons.count { vm.progress.netDone(it.int("id")) } sum.totalEx = vm.content.netLessons.size + extra.sumOf { vm.content.itLessons(it).size }
sum.doneEx = vm.content.netLessons.count { vm.progress.netDone(it.int("id")) } +
extra.sumOf { course -> vm.content.itLessons(course).count { vm.progress.courseDone(course, it.int("id")) } }
} }
"hwyears", "hwmap" -> { "hwyears", "hwmap" -> {
sum.totalEx = vm.content.hw.size val extra = listOf("hw2", "hw3", "hw4")
sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } sum.totalEx = vm.content.hw.size + extra.sumOf { vm.content.itLessons(it).size }
sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } +
extra.sumOf { course -> vm.content.itLessons(course).count { vm.progress.courseDone(course, it.int("id")) } }
} }
"enyears", "en1map", "en2map", "en3map", "en4map" -> { "enyears", "en1map", "en2map", "en3map", "en4map" -> {
sum.totalEx = (1..4).sumOf { vm.content.enYear(it).size } sum.totalEx = (1..4).sumOf { vm.content.enYear(it).size }
@ -530,10 +534,19 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit${l.int("id")}", extra = "site") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit${l.int("id")}", extra = "site")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex${l.int("id")}") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex${l.int("id")}")
} }
listOf("net2", "net3", "net4").forEach { course ->
add(vm.tr("net_year${course.drop(3)}"), vm.tr("search_lesson"), "${course}map", extra = "rocnik site")
vm.content.itLessons(course).forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "${course}unit${l.int("id")}", extra = "site")
}
}
add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra = "rocnik") add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra = "rocnik")
add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") listOf("hw2", "hw3", "hw4").forEach { course ->
add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") add(vm.tr("hw_year${course.drop(2)}"), vm.tr("search_lesson"), "${course}map", extra = "rocnik hardware")
add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") vm.content.itLessons(course).forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "${course}unit${l.int("id")}", extra = "hardware")
}
}
vm.content.hw.forEach { l -> vm.content.hw.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit${l.int("id")}", extra = "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit${l.int("id")}", extra = "hardware")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}")
@ -718,15 +731,26 @@ fun UnitMapScreen(vm: AppViewModel, unitId: Int) {
} }
} }
@Composable
fun ItYearScreen(vm: AppViewModel, course: String, titleKey: String, subKey: String, lesson: (Int) -> Route) {
LessonListScreen(
vm, vm.tr(titleKey), vm.tr(subKey),
vm.content.itLessons(course).map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.courseDone(course, id), lesson(id))
},
)
}
@Composable @Composable
fun NetYearsScreen(vm: AppViewModel) { fun NetYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("net_years_title"), vm.tr("net_years_sub")) { inner -> DetailScaffold(vm, vm.tr("net_years_title"), vm.tr("net_years_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
item { item {
NavRow(title = vm.tr("net_year1"), subtitle = vm.tr("net_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.NetMap) } NavRow(title = vm.tr("net_year1"), subtitle = vm.tr("net_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.NetMap) }
} NavRow(title = vm.tr("net_year2"), subtitle = vm.tr("net2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net2Map) }
items(listOf("net_year2", "net_year3", "net_year4")) { key -> NavRow(title = vm.tr("net_year3"), subtitle = vm.tr("net3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net3Map) }
NavRow(title = vm.tr(key), subtitle = vm.tr("net_year_locked_sub"), locked = true, onClick = null) NavRow(title = vm.tr("net_year4"), subtitle = vm.tr("net4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net4Map) }
} }
} }
} }
@ -738,9 +762,9 @@ fun HwYearsScreen(vm: AppViewModel) {
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
item { item {
NavRow(title = vm.tr("hw_year1"), subtitle = vm.tr("hw_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.HwMap) } NavRow(title = vm.tr("hw_year1"), subtitle = vm.tr("hw_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.HwMap) }
} NavRow(title = vm.tr("hw_year2"), subtitle = vm.tr("hw2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw2Map) }
items(listOf("hw_year2", "hw_year3", "hw_year4")) { key -> NavRow(title = vm.tr("hw_year3"), subtitle = vm.tr("hw3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw3Map) }
NavRow(title = vm.tr(key), subtitle = vm.tr("hw_year_locked_sub"), locked = true, onClick = null) NavRow(title = vm.tr("hw_year4"), subtitle = vm.tr("hw4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw4Map) }
} }
} }
} }

View file

@ -6,6 +6,12 @@
# cs: bullet # cs: bullet
# en: bullet # en: bullet
--- 2026-10-07
cs: Sítě a technické vybavení: doplněné 2. až 4. ročníky, v každém 8 lekcí
cs: Výklad a kvíz od VLAN a sestavení počítače po maturitu
en: Networks and hardware: years 2 to 4 are filled in, eight lessons each
en: Slides and a quiz, from VLANs and PC assembly through to the exam
--- 2026-10-07 --- 2026-10-07
cs: Němčina: čtyři ročníky, 24 lekcí s poslechem, čtením a psaním 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 cs: Učebnice Neue Freunde zůstává, k ní přibyl kurz od A1 k B1

View file

@ -176,6 +176,11 @@ final class AppModel: ObservableObject {
refresh() refresh()
} }
func markCourse(_ course: String, _ id: Int) {
progress.markCourse(course, id)
refresh()
}
func markLit(_ id: Int) { func markLit(_ id: Int) {
progress.markLit(id) progress.markLit(id)
refresh() refresh()

View file

@ -31,6 +31,12 @@ final class Content {
let netTasks: [J] let netTasks: [J]
let netAnswers: [[J]] let netAnswers: [[J]]
let hw: [J] let hw: [J]
let net2: [J]
let net3: [J]
let net4: [J]
let hw2: [J]
let hw3: [J]
let hw4: [J]
let on: [J] let on: [J]
let on2: [J] let on2: [J]
let on3: [J] let on3: [J]
@ -89,6 +95,12 @@ final class Content {
netAnswers = [] netAnswers = []
} }
hw = raw.arr("hw") hw = raw.arr("hw")
net2 = raw.arr("net2")
net3 = raw.arr("net3")
net4 = raw.arr("net4")
hw2 = raw.arr("hw2")
hw3 = raw.arr("hw3")
hw4 = raw.arr("hw4")
on = raw.arr("on") on = raw.arr("on")
on2 = raw.arr("on2") on2 = raw.arr("on2")
on3 = raw.arr("on3") on3 = raw.arr("on3")
@ -124,6 +136,20 @@ final class Content {
func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } } func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } }
func netLesson(_ id: Int) -> J? { netLessons.first { $0.int("id") == id } } func netLesson(_ id: Int) -> J? { netLessons.first { $0.int("id") == id } }
func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } } func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } }
func itLessons(_ course: String) -> [J] {
switch course {
case "net2": return net2
case "net3": return net3
case "net4": return net4
case "hw2": return hw2
case "hw3": return hw3
case "hw4": return hw4
default: return []
}
}
func itLesson(_ course: String, _ id: Int) -> J? {
itLessons(course).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 on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } }

View file

@ -50,6 +50,8 @@ final class ProgressStore {
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 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 markDe(_ year: Int, _ id: Int) { defaults.set(true, forKey: "de.\(year).\(id)") }
func courseDone(_ course: String, _ id: Int) -> Bool { defaults.bool(forKey: "\(course).\(id)") }
func markCourse(_ course: String, _ id: Int) { defaults.set(true, forKey: "\(course).\(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)") }
@ -126,6 +128,16 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
for course in ["net2", "net3", "net4", "hw2", "hw3", "hw4"] {
for l in content.itLessons(course) {
sum.totalEx += 1
sum.openUnits += 1
if p.courseDone(course, l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
for l in content.on { for l in content.on {
sum.totalEx += 1 sum.totalEx += 1
sum.openUnits += 1 sum.openUnits += 1

View file

@ -23,10 +23,28 @@ enum Route: Hashable {
case netMap case netMap
case netLesson(Int) case netLesson(Int)
case netEx(Int) case netEx(Int)
case net2Map
case net2Lesson(Int)
case net2Ex(Int)
case net3Map
case net3Lesson(Int)
case net3Ex(Int)
case net4Map
case net4Lesson(Int)
case net4Ex(Int)
case hwYears case hwYears
case hwMap case hwMap
case hwLesson(Int) case hwLesson(Int)
case hwEx(Int) case hwEx(Int)
case hw2Map
case hw2Lesson(Int)
case hw2Ex(Int)
case hw3Map
case hw3Lesson(Int)
case hw3Ex(Int)
case hw4Map
case hw4Lesson(Int)
case hw4Ex(Int)
case onYears case onYears
case onMap case onMap
case onLesson(Int) case onLesson(Int)
@ -118,10 +136,28 @@ enum Route: Hashable {
case .netMap: return "netmap" case .netMap: return "netmap"
case .netLesson(let id): return "netunit\(id)" case .netLesson(let id): return "netunit\(id)"
case .netEx(let id): return "netex\(id)" case .netEx(let id): return "netex\(id)"
case .net2Map: return "net2map"
case .net2Lesson(let id): return "net2unit\(id)"
case .net2Ex(let id): return "net2ex\(id)"
case .net3Map: return "net3map"
case .net3Lesson(let id): return "net3unit\(id)"
case .net3Ex(let id): return "net3ex\(id)"
case .net4Map: return "net4map"
case .net4Lesson(let id): return "net4unit\(id)"
case .net4Ex(let id): return "net4ex\(id)"
case .hwYears: return "hwyears" case .hwYears: return "hwyears"
case .hwMap: return "hwmap" case .hwMap: return "hwmap"
case .hwLesson(let id): return "hwunit\(id)" case .hwLesson(let id): return "hwunit\(id)"
case .hwEx(let id): return "hwex\(id)" case .hwEx(let id): return "hwex\(id)"
case .hw2Map: return "hw2map"
case .hw2Lesson(let id): return "hw2unit\(id)"
case .hw2Ex(let id): return "hw2ex\(id)"
case .hw3Map: return "hw3map"
case .hw3Lesson(let id): return "hw3unit\(id)"
case .hw3Ex(let id): return "hw3ex\(id)"
case .hw4Map: return "hw4map"
case .hw4Lesson(let id): return "hw4unit\(id)"
case .hw4Ex(let id): return "hw4ex\(id)"
case .onYears: return "onyears" case .onYears: return "onyears"
case .onMap: return "onmap" case .onMap: return "onmap"
case .onLesson(let id): return "onunit\(id)" case .onLesson(let id): return "onunit\(id)"
@ -236,6 +272,24 @@ enum Route: Hashable {
return [.netYears, .netMap, .netLesson(id)] return [.netYears, .netMap, .netLesson(id)]
case .netEx(let id): case .netEx(let id):
return [.netYears, .netMap, .netLesson(id), .netEx(id)] return [.netYears, .netMap, .netLesson(id), .netEx(id)]
case .net2Map:
return [.netYears, .net2Map]
case .net2Lesson(let id):
return [.netYears, .net2Map, .net2Lesson(id)]
case .net2Ex(let id):
return [.netYears, .net2Map, .net2Lesson(id), .net2Ex(id)]
case .net3Map:
return [.netYears, .net3Map]
case .net3Lesson(let id):
return [.netYears, .net3Map, .net3Lesson(id)]
case .net3Ex(let id):
return [.netYears, .net3Map, .net3Lesson(id), .net3Ex(id)]
case .net4Map:
return [.netYears, .net4Map]
case .net4Lesson(let id):
return [.netYears, .net4Map, .net4Lesson(id)]
case .net4Ex(let id):
return [.netYears, .net4Map, .net4Lesson(id), .net4Ex(id)]
case .hwYears: case .hwYears:
return [.hwYears] return [.hwYears]
case .hwMap: case .hwMap:
@ -244,6 +298,24 @@ enum Route: Hashable {
return [.hwYears, .hwMap, .hwLesson(id)] return [.hwYears, .hwMap, .hwLesson(id)]
case .hwEx(let id): case .hwEx(let id):
return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)] return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)]
case .hw2Map:
return [.hwYears, .hw2Map]
case .hw2Lesson(let id):
return [.hwYears, .hw2Map, .hw2Lesson(id)]
case .hw2Ex(let id):
return [.hwYears, .hw2Map, .hw2Lesson(id), .hw2Ex(id)]
case .hw3Map:
return [.hwYears, .hw3Map]
case .hw3Lesson(let id):
return [.hwYears, .hw3Map, .hw3Lesson(id)]
case .hw3Ex(let id):
return [.hwYears, .hw3Map, .hw3Lesson(id), .hw3Ex(id)]
case .hw4Map:
return [.hwYears, .hw4Map]
case .hw4Lesson(let id):
return [.hwYears, .hw4Map, .hw4Lesson(id)]
case .hw4Ex(let id):
return [.hwYears, .hw4Map, .hw4Lesson(id), .hw4Ex(id)]
case .onYears: case .onYears:
return [.onYears] return [.onYears]
case .onMap: case .onMap:
@ -403,6 +475,12 @@ func routeFromPage(_ page: String) -> Route? {
case "dehome": return .deHome case "dehome": return .deHome
case "netyears": return .netYears case "netyears": return .netYears
case "netmap": return .netMap case "netmap": return .netMap
case "net2map": return .net2Map
case "net3map": return .net3Map
case "net4map": return .net4Map
case "hw2map": return .hw2Map
case "hw3map": return .hw3Map
case "hw4map": return .hw4Map
case "hwyears": return .hwYears case "hwyears": return .hwYears
case "hwmap": return .hwMap case "hwmap": return .hwMap
case "onyears": return .onYears case "onyears": return .onYears
@ -572,6 +650,34 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /de(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) { if let m = page.wholeMatch(of: /de(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
return .deEx(y, n) return .deEx(y, n)
} }
if let m = page.wholeMatch(of: /net([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .net3Lesson(n)
case 4: return .net4Lesson(n)
default: return .net2Lesson(n)
}
}
if let m = page.wholeMatch(of: /net([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .net3Ex(n)
case 4: return .net4Ex(n)
default: return .net2Ex(n)
}
}
if let m = page.wholeMatch(of: /hw([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .hw3Lesson(n)
case 4: return .hw4Lesson(n)
default: return .hw2Lesson(n)
}
}
if let m = page.wholeMatch(of: /hw([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .hw3Ex(n)
case 4: return .hw4Ex(n)
default: return .hw2Ex(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

@ -896,6 +896,62 @@ private struct MathGrid: View {
} }
} }
struct ItYearList: View {
@ObservedObject var vm: AppModel
let course: String
let titleKey: String
let subKey: String
let lesson: (Int) -> Route
var body: some View {
LessonListScreen(
vm: vm,
title: vm.tr(titleKey),
subtitle: vm.tr(subKey),
rows: vm.content.itLessons(course).map { l in
let id = l.int("id")
return LessonRow(
id: "\(course)\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.courseDone(course, id),
destination: lesson(id)
)
}
)
}
}
struct ItSlides: View {
@ObservedObject var vm: AppModel
let course: String
let id: Int
let next: Route
var body: some View {
if let l = vm.content.itLesson(course, id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: next)
}
}
}
struct ChoiceLessonScreen: View {
@ObservedObject var vm: AppModel
let lesson: J?
let onDone: () -> Void
var body: some View {
if let lesson {
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")), onDone: onDone)
}
}
}
struct FyzQuizScreen: View { struct FyzQuizScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
let year: Int let year: Int

View file

@ -674,10 +674,18 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
} }
case "netyears": case "netyears":
sum.totalEx = vm.content.netLessons.count sum.totalEx = vm.content.netLessons.count
+ vm.content.net2.count + vm.content.net3.count + vm.content.net4.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
+ ["net2", "net3", "net4"].reduce(0) { acc, course in
acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count
}
case "hwyears", "hwmap": case "hwyears", "hwmap":
sum.totalEx = vm.content.hw.count sum.totalEx = vm.content.hw.count
+ vm.content.hw2.count + vm.content.hw3.count + vm.content.hw4.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
+ ["hw2", "hw3", "hw4"].reduce(0) { acc, course in
acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count
}
case "enyears", "en1map", "en2map", "en3map", "en4map": case "enyears", "en1map", "en2map", "en3map", "en4map":
sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count } sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count }
sum.doneEx = (1...4).reduce(0) { acc, year in sum.doneEx = (1...4).reduce(0) { acc, year in
@ -887,14 +895,23 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit\(l.int("id"))", extra: "site") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit\(l.int("id"))", extra: "site")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex\(l.int("id"))") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex\(l.int("id"))")
} }
for course in ["net2", "net3", "net4"] {
add(vm.tr("net_year\(course.dropFirst(3))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik site")
for l in vm.content.itLessons(course) {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "site")
}
}
add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra: "rocnik") add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra: "rocnik")
add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik")
add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik")
add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik")
for l in vm.content.hw { for l in vm.content.hw {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))")
} }
for course in ["hw2", "hw3", "hw4"] {
add(vm.tr("hw_year\(course.dropFirst(2))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik hardware")
for l in vm.content.itLessons(course) {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "hardware")
}
}
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"), "on3map", extra: "rocnik") add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
@ -1178,16 +1195,17 @@ struct NetYearsScreen: View {
var body: some View { var body: some View {
#if os(macOS) #if os(macOS)
let keys = ["net_year1", "net_year2", "net_year3", "net_year4"] let years: [(String, String, Route)] = [
MacFillGrid(count: keys.count, minWidth: 260, minHeight: 180) { i in ("net_year1", "net_sub", .netMap),
if i == 0 { ("net_year2", "net2_sub", .net2Map),
NavigationLink(value: Route.netMap) { ("net_year3", "net3_sub", .net3Map),
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("net_sub"), badge: "\(i + 1).") ("net_year4", "net4_sub", .net4Map),
} ]
.buttonStyle(.plain) MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
} else { NavigationLink(value: years[i].2) {
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("net_year_locked_sub"), badge: "\(i + 1).", locked: true) MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
} }
.buttonStyle(.plain)
} }
.navigationTitle(vm.tr("net_years_title")) .navigationTitle(vm.tr("net_years_title"))
.navigationSubtitle(vm.tr("net_years_sub")) .navigationSubtitle(vm.tr("net_years_sub"))
@ -1196,13 +1214,14 @@ struct NetYearsScreen: View {
NavigationLink(value: Route.netMap) { NavigationLink(value: Route.netMap) {
ListRowLabel(title: vm.tr("net_year1"), subtitle: vm.tr("net_sub")) ListRowLabel(title: vm.tr("net_year1"), subtitle: vm.tr("net_sub"))
} }
ForEach(["net_year2", "net_year3", "net_year4"], id: \.self) { key in NavigationLink(value: Route.net2Map) {
Label { ListRowLabel(title: vm.tr("net_year2"), subtitle: vm.tr("net2_sub"))
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("net_year_locked_sub")) }
} icon: { NavigationLink(value: Route.net3Map) {
Image(systemName: "lock.fill") ListRowLabel(title: vm.tr("net_year3"), subtitle: vm.tr("net3_sub"))
} }
.foregroundStyle(.secondary) NavigationLink(value: Route.net4Map) {
ListRowLabel(title: vm.tr("net_year4"), subtitle: vm.tr("net4_sub"))
} }
} }
.navigationTitle(vm.tr("net_years_title")) .navigationTitle(vm.tr("net_years_title"))
@ -1217,16 +1236,17 @@ struct HwYearsScreen: View {
var body: some View { var body: some View {
#if os(macOS) #if os(macOS)
let keys = ["hw_year1", "hw_year2", "hw_year3", "hw_year4"] let years: [(String, String, Route)] = [
MacFillGrid(count: keys.count, minWidth: 260, minHeight: 180) { i in ("hw_year1", "hw_sub", .hwMap),
if i == 0 { ("hw_year2", "hw2_sub", .hw2Map),
NavigationLink(value: Route.hwMap) { ("hw_year3", "hw3_sub", .hw3Map),
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("hw_sub"), badge: "\(i + 1).") ("hw_year4", "hw4_sub", .hw4Map),
} ]
.buttonStyle(.plain) MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
} else { NavigationLink(value: years[i].2) {
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("hw_year_locked_sub"), badge: "\(i + 1).", locked: true) MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
} }
.buttonStyle(.plain)
} }
.navigationTitle(vm.tr("Technické vybavení")) .navigationTitle(vm.tr("Technické vybavení"))
.navigationSubtitle(vm.tr("hw_years_sub")) .navigationSubtitle(vm.tr("hw_years_sub"))
@ -1235,13 +1255,14 @@ struct HwYearsScreen: View {
NavigationLink(value: Route.hwMap) { NavigationLink(value: Route.hwMap) {
ListRowLabel(title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub")) ListRowLabel(title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub"))
} }
ForEach(["hw_year2", "hw_year3", "hw_year4"], id: \.self) { key in NavigationLink(value: Route.hw2Map) {
Label { ListRowLabel(title: vm.tr("hw_year2"), subtitle: vm.tr("hw2_sub"))
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("hw_year_locked_sub")) }
} icon: { NavigationLink(value: Route.hw3Map) {
Image(systemName: "lock.fill") ListRowLabel(title: vm.tr("hw_year3"), subtitle: vm.tr("hw3_sub"))
} }
.foregroundStyle(.secondary) NavigationLink(value: Route.hw4Map) {
ListRowLabel(title: vm.tr("hw_year4"), subtitle: vm.tr("hw4_sub"))
} }
} }
.navigationTitle(vm.tr("Technické vybavení")) .navigationTitle(vm.tr("Technické vybavení"))
@ -2256,6 +2277,24 @@ struct RouteDestination: View {
} }
case .netEx(let id): case .netEx(let id):
NetQuizScreen(vm: vm, id: id) NetQuizScreen(vm: vm, id: id)
case .net2Map:
ItYearList(vm: vm, course: "net2", titleKey: "net_year2", subKey: "net2_sub") { .net2Lesson($0) }
case .net2Lesson(let id):
ItSlides(vm: vm, course: "net2", id: id, next: .net2Ex(id))
case .net2Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net2", id)) { vm.markCourse("net2", id) }
case .net3Map:
ItYearList(vm: vm, course: "net3", titleKey: "net_year3", subKey: "net3_sub") { .net3Lesson($0) }
case .net3Lesson(let id):
ItSlides(vm: vm, course: "net3", id: id, next: .net3Ex(id))
case .net3Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net3", id)) { vm.markCourse("net3", id) }
case .net4Map:
ItYearList(vm: vm, course: "net4", titleKey: "net_year4", subKey: "net4_sub") { .net4Lesson($0) }
case .net4Lesson(let id):
ItSlides(vm: vm, course: "net4", id: id, next: .net4Ex(id))
case .net4Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net4", id)) { vm.markCourse("net4", id) }
case .hwYears: case .hwYears:
HwYearsScreen(vm: vm) HwYearsScreen(vm: vm)
case .hwMap: case .hwMap:
@ -2279,6 +2318,24 @@ struct RouteDestination: View {
} }
case .hwEx(let id): case .hwEx(let id):
HwQuizScreen(vm: vm, id: id) HwQuizScreen(vm: vm, id: id)
case .hw2Map:
ItYearList(vm: vm, course: "hw2", titleKey: "hw_year2", subKey: "hw2_sub") { .hw2Lesson($0) }
case .hw2Lesson(let id):
ItSlides(vm: vm, course: "hw2", id: id, next: .hw2Ex(id))
case .hw2Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw2", id)) { vm.markCourse("hw2", id) }
case .hw3Map:
ItYearList(vm: vm, course: "hw3", titleKey: "hw_year3", subKey: "hw3_sub") { .hw3Lesson($0) }
case .hw3Lesson(let id):
ItSlides(vm: vm, course: "hw3", id: id, next: .hw3Ex(id))
case .hw3Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw3", id)) { vm.markCourse("hw3", id) }
case .hw4Map:
ItYearList(vm: vm, course: "hw4", titleKey: "hw_year4", subKey: "hw4_sub") { .hw4Lesson($0) }
case .hw4Lesson(let id):
ItSlides(vm: vm, course: "hw4", id: id, next: .hw4Ex(id))
case .hw4Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw4", id)) { vm.markCourse("hw4", id) }
case .onYears: case .onYears:
OnYearsScreen(vm: vm) OnYearsScreen(vm: vm)
case .enYears: case .enYears:

View file

@ -1142,6 +1142,12 @@ def main() -> int:
lit4_lessons = lit_pack("lit4") lit4_lessons = lit_pack("lit4")
chem_lessons = lit_pack("chem") chem_lessons = lit_pack("chem")
bio_lessons = lit_pack("bio") bio_lessons = lit_pack("bio")
net2_lessons = lit_pack("net2")
net3_lessons = lit_pack("net3")
net4_lessons = lit_pack("net4")
hw2_lessons = lit_pack("hw2")
hw3_lessons = lit_pack("hw3")
hw4_lessons = lit_pack("hw4")
fyz_lessons = lit_pack("fyz") fyz_lessons = lit_pack("fyz")
fyz2_lessons = lit_pack("fyz2") fyz2_lessons = lit_pack("fyz2")
fyz3_lessons = lit_pack("fyz3") fyz3_lessons = lit_pack("fyz3")
@ -1692,6 +1698,12 @@ def main() -> int:
"answers": net_ans(A("net_ans")), "answers": net_ans(A("net_ans")),
}, },
"hw": hw_lessons, "hw": hw_lessons,
"net2": net2_lessons,
"net3": net3_lessons,
"net4": net4_lessons,
"hw2": hw2_lessons,
"hw3": hw3_lessons,
"hw4": hw4_lessons,
"on": on_lessons, "on": on_lessons,
"lit": lit_lessons, "lit": lit_lessons,
"lit2": lit2_lessons, "lit2": lit2_lessons,

2472
scripts/gen-it-years.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -946,6 +946,26 @@ GtkWidget *build_dehome_page(void);
void add_de_pages(GtkStack *stack); void add_de_pages(GtkStack *stack);
void de_load_progress(void); void de_load_progress(void);
void progress_for_de(ProgressSum *out); void progress_for_de(ProgressSum *out);
typedef struct QuizYear QuizYear;
extern QuizYear *qy_net2;
extern QuizYear *qy_net3;
extern QuizYear *qy_net4;
extern QuizYear *qy_hw2;
extern QuizYear *qy_hw3;
extern QuizYear *qy_hw4;
void quizyear_boot(void);
void quizyear_load_all(void);
void quizyear_apply_lang(void);
void progress_for_net_years(ProgressSum *out);
void progress_for_hw_years(ProgressSum *out);
GtkWidget *quizyear_map(QuizYear *Y);
GtkWidget *quizyear_unit(QuizYear *Y, int index, const char *title_key,
const char *sub_key, const NetSlide *slides,
int n_slides);
GtkWidget *quizyear_quiz(QuizYear *Y, int index, const char *title_key,
const char *head_key, const ChoiceQ *qs,
const char **hints, int n);
void add_it_year_pages(GtkStack *stack);
void on2_load_progress(void); void on2_load_progress(void);
void on2_save_progress(void); void on2_save_progress(void);
void mark_on2_done(int lesson_id); void mark_on2_done(int lesson_id);

View file

@ -383,8 +383,21 @@ static GtkWidget *hw_year_button(int year, gboolean locked) {
gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); gtk_widget_set_valign(icon, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), icon); gtk_box_append(GTK_BOX(row), icon);
} else { } else {
static const char *pages[] = {
"hwmap", "hw2map", "hw3map", "hw4map",
};
static const char *subs[] = {
"hw_sub", "hw2_sub", "hw3_sub", "hw4_sub",
};
GtkWidget *sub = gtk_label_new(NULL);
i18n_bind(sub, subs[year - 1], 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
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_object_set_data_full(G_OBJECT(btn), "target",
g_strdup("hwmap"), g_free); g_strdup(pages[year - 1]), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} }
@ -422,7 +435,7 @@ GtkWidget *build_hwyears_page(void) {
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
for (y = 1; y <= 4; y++) for (y = 1; y <= 4; y++)
gtk_box_append(GTK_BOX(list), hw_year_button(y, y > 1)); gtk_box_append(GTK_BOX(list), hw_year_button(y, FALSE));
return page; return page;
} }

View file

@ -1621,6 +1621,7 @@ const TrEntry tr_ui[] = {
"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 "german_i18n.inc"
#include "it_years_i18n.inc"
#include "math_i18n.inc" #include "math_i18n.inc"
{NULL, NULL, NULL} {NULL, NULL, NULL}
}; };
@ -2605,6 +2606,7 @@ 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 de_lessons_apply_lang(void);
void quizyear_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);
@ -2629,6 +2631,7 @@ void apply_language(void) {
on4_lessons_apply_lang(); on4_lessons_apply_lang();
en_lessons_apply_lang(); en_lessons_apply_lang();
de_lessons_apply_lang(); de_lessons_apply_lang();
quizyear_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();

2606
src/it_year_pages.c Normal file

File diff suppressed because it is too large Load diff

198
src/it_years_i18n.inc Normal file
View file

@ -0,0 +1,198 @@
{"net2_sub", "VLAN, STP, DHCP, DNS, NAT, IPv6, statické směrování a ACL.", "VLANs, STP, DHCP, DNS, NAT, IPv6, static routing and ACLs."},
{"net2_unit1", "VLAN a trunk", "VLANs and trunks"},
{"net2_unit1_sub", "Logické sítě na jednom přepínači a značkovaný spoj.", "Logical networks on one switch and a tagged link."},
{"net2_ex1_title", "Kvíz: VLAN a trunk", "Quiz: VLANs and trunks"},
{"net2_quiz1_head", "Kvíz k tématu VLAN a trunk", "Quiz on VLANs and trunks"},
{"net2_unit2", "STP a smyčky", "STP and loops"},
{"net2_unit2_sub", "Proč smyčka na druhé vrstvě shodí síť a jak ji STP přeruší.", "Why a layer-2 loop takes the network down, and how STP breaks it."},
{"net2_ex2_title", "Kvíz: STP a smyčky", "Quiz: STP and loops"},
{"net2_quiz2_head", "Kvíz k tématu STP a smyčky", "Quiz on STP and loops"},
{"net2_unit3", "DHCP a DNS", "DHCP and DNS"},
{"net2_unit3_sub", "Kdo přiděluje adresu a kdo překládá jméno.", "Who hands out an address, and who translates a name."},
{"net2_ex3_title", "Kvíz: DHCP a DNS", "Quiz: DHCP and DNS"},
{"net2_quiz3_head", "Kvíz k tématu DHCP a DNS", "Quiz on DHCP and DNS"},
{"net2_unit4", "NAT a PAT", "NAT and PAT"},
{"net2_unit4_sub", "Jedna veřejná adresa pro mnoho počítačů.", "One public address for many computers."},
{"net2_ex4_title", "Kvíz: NAT a PAT", "Quiz: NAT and PAT"},
{"net2_quiz4_head", "Kvíz k tématu NAT a PAT", "Quiz on NAT and PAT"},
{"net2_unit5", "IPv6 základy", "IPv6 basics"},
{"net2_unit5_sub", "Delší adresa, jiný zápis a co se nemění.", "A longer address, a different notation, and what stays the same."},
{"net2_ex5_title", "Kvíz: IPv6 základy", "Quiz: IPv6 basics"},
{"net2_quiz5_head", "Kvíz k tématu IPv6 základy", "Quiz on IPv6 basics"},
{"net2_unit6", "Statické směrování", "Static routing"},
{"net2_unit6_sub", "Ruční cesta, metrika a kdy přestane stačit.", "A hand-written route, the metric, and when it is no longer enough."},
{"net2_ex6_title", "Kvíz: Statické směrování", "Quiz: Static routing"},
{"net2_quiz6_head", "Kvíz k tématu Statické směrování", "Quiz on Static routing"},
{"net2_unit7", "ACL a filtrování", "ACLs and filtering"},
{"net2_unit7_sub", "Seznam, který provoz pustí a který zahodí.", "A list that permits traffic or drops it."},
{"net2_ex7_title", "Kvíz: ACL a filtrování", "Quiz: ACLs and filtering"},
{"net2_quiz7_head", "Kvíz k tématu ACL a filtrování", "Quiz on ACLs and filtering"},
{"net2_unit8", "Podniková Wi-Fi", "Enterprise Wi-Fi"},
{"net2_unit8_sub", "SSID, WPA3 a proč jedno heslo na nástěnce nestačí.", "SSIDs, WPA3, and why one password on the wall is not enough."},
{"net2_ex8_title", "Kvíz: Podniková Wi-Fi", "Quiz: Enterprise Wi-Fi"},
{"net2_quiz8_head", "Kvíz k tématu Podniková Wi-Fi", "Quiz on Enterprise Wi-Fi"},
{"net3_sub", "OSPF, směrování mezi VLAN, WAN, VPN, firewall, QoS a dohled.", "OSPF, inter-VLAN routing, WAN, VPN, firewalls, QoS and monitoring."},
{"net3_unit1", "OSPF", "OSPF"},
{"net3_unit1_sub", "Směrovače si samy řeknou, jaké sítě znají.", "Routers tell each other which networks they know."},
{"net3_ex1_title", "Kvíz: OSPF", "Quiz: OSPF"},
{"net3_quiz1_head", "Kvíz k tématu OSPF", "Quiz on OSPF"},
{"net3_unit2", "Směrování mezi VLAN", "Inter-VLAN routing"},
{"net3_unit2_sub", "Jedna brána pro každou VLAN.", "One gateway for each VLAN."},
{"net3_ex2_title", "Kvíz: Směrování mezi VLAN", "Quiz: Inter-VLAN routing"},
{"net3_quiz2_head", "Kvíz k tématu Směrování mezi VLAN", "Quiz on Inter-VLAN routing"},
{"net3_unit3", "WAN", "WAN"},
{"net3_unit3_sub", "Spoj mezi pobočkami a k poskytovateli.", "A link between sites and towards the provider."},
{"net3_ex3_title", "Kvíz: WAN", "Quiz: WAN"},
{"net3_quiz3_head", "Kvíz k tématu WAN", "Quiz on WAN"},
{"net3_unit4", "VPN", "VPN"},
{"net3_unit4_sub", "Šifrovaný tunel přes síť, které nevěříte.", "An encrypted tunnel across a network you do not trust."},
{"net3_ex4_title", "Kvíz: VPN", "Quiz: VPN"},
{"net3_quiz4_head", "Kvíz k tématu VPN", "Quiz on VPN"},
{"net3_unit5", "Firewall a zóny", "Firewalls and zones"},
{"net3_unit5_sub", "Kdo s kým smí mluvit, ne jen jestli kabel svítí.", "Who may talk to whom, not only whether the cable is lit."},
{"net3_ex5_title", "Kvíz: Firewall a zóny", "Quiz: Firewalls and zones"},
{"net3_quiz5_head", "Kvíz k tématu Firewall a zóny", "Quiz on Firewalls and zones"},
{"net3_unit6", "QoS", "QoS"},
{"net3_unit6_sub", "Když je linka plná, někdo musí dostat přednost.", "When the link is full, someone has to go first."},
{"net3_ex6_title", "Kvíz: QoS", "Quiz: QoS"},
{"net3_quiz6_head", "Kvíz k tématu QoS", "Quiz on QoS"},
{"net3_unit7", "Dohled sítě", "Watching the network"},
{"net3_unit7_sub", "SNMP, syslog a co z grafu opravdu plyne.", "SNMP, syslog, and what a graph really tells you."},
{"net3_ex7_title", "Kvíz: Dohled sítě", "Quiz: Watching the network"},
{"net3_quiz7_head", "Kvíz k tématu Dohled sítě", "Quiz on Watching the network"},
{"net3_unit8", "Záloha konfigurace", "Configuration backups"},
{"net3_unit8_sub", "Co uložit, než se přepínač vymění.", "What to save before a switch is replaced."},
{"net3_ex8_title", "Kvíz: Záloha konfigurace", "Quiz: Configuration backups"},
{"net3_quiz8_head", "Kvíz k tématu Záloha konfigurace", "Quiz on Configuration backups"},
{"net4_sub", "Návrh adres, redundance, 802.1X, hledání chyb, cloud a maturita.", "Address plans, redundancy, 802.1X, troubleshooting, cloud and the exam."},
{"net4_unit1", "Adresní plán", "An address plan"},
{"net4_unit1_sub", "Sítě se navrhují dřív, než se zapojí první kabel.", "Networks are designed before the first cable is patched."},
{"net4_ex1_title", "Kvíz: Adresní plán", "Quiz: An address plan"},
{"net4_quiz1_head", "Kvíz k tématu Adresní plán", "Quiz on An address plan"},
{"net4_unit2", "Redundance", "Redundancy"},
{"net4_unit2_sub", "Druhá cesta, která se opravdu použije.", "A second path that is actually used."},
{"net4_ex2_title", "Kvíz: Redundance", "Quiz: Redundancy"},
{"net4_quiz2_head", "Kvíz k tématu Redundance", "Quiz on Redundancy"},
{"net4_unit3", "802.1X", "802.1X"},
{"net4_unit3_sub", "Port pustí data, až když se zařízení představí.", "A port forwards data only after the device identifies itself."},
{"net4_ex3_title", "Kvíz: 802.1X", "Quiz: 802.1X"},
{"net4_quiz3_head", "Kvíz k tématu 802.1X", "Quiz on 802.1X"},
{"net4_unit4", "Hledání chyby", "Troubleshooting"},
{"net4_unit4_sub", "Postup zdola nahoru, ne náhodné restarty.", "Bottom-up steps, not random reboots."},
{"net4_ex4_title", "Kvíz: Hledání chyby", "Quiz: Troubleshooting"},
{"net4_quiz4_head", "Kvíz k tématu Hledání chyby", "Quiz on Troubleshooting"},
{"net4_unit5", "Cloud a hybrid", "Cloud and hybrid networks"},
{"net4_unit5_sub", "Část služeb není ve skříni na chodbě.", "Some services are no longer in the cupboard in the corridor."},
{"net4_ex5_title", "Kvíz: Cloud a hybrid", "Quiz: Cloud and hybrid networks"},
{"net4_quiz5_head", "Kvíz k tématu Cloud a hybrid", "Quiz on Cloud and hybrid networks"},
{"net4_unit6", "Automatizace", "Automation"},
{"net4_unit6_sub", "Opakovanou práci ať dělá skript, ne deset kliknutí.", "Let a script do the repeated work, not ten clicks."},
{"net4_ex6_title", "Kvíz: Automatizace", "Quiz: Automation"},
{"net4_quiz6_head", "Kvíz k tématu Automatizace", "Quiz on Automation"},
{"net4_unit7", "Provoz a maturita", "Operations and the exam"},
{"net4_unit7_sub", "Síť ve škole má pravidla, ne jen blikající porty.", "A school network has rules, not only blinking ports."},
{"net4_ex7_title", "Kvíz: Provoz a maturita", "Quiz: Operations and the exam"},
{"net4_quiz7_head", "Kvíz k tématu Provoz a maturita", "Quiz on Operations and the exam"},
{"net4_unit8", "Dokumentace sítě", "Network documentation"},
{"net4_unit8_sub", "Schéma, které sedí s tím, co je ve zdi.", "A diagram that matches what is actually in the wall."},
{"net4_ex8_title", "Kvíz: Dokumentace sítě", "Quiz: Network documentation"},
{"net4_quiz8_head", "Kvíz k tématu Dokumentace sítě", "Quiz on Network documentation"},
{"hw2_sub", "Monitory, tisk, sběrnice, chlazení, sestavení, UEFI a diagnostika.", "Displays, printing, buses, cooling, assembly, UEFI and diagnostics."},
{"hw2_unit1", "Monitory", "Displays"},
{"hw2_unit1_sub", "Panel, rozlišení a proč 60 Hz není jediné číslo.", "The panel, the resolution, and why 60 Hz is not the only number."},
{"hw2_ex1_title", "Kvíz: Monitory", "Quiz: Displays"},
{"hw2_quiz1_head", "Kvíz k tématu Monitory", "Quiz on Displays"},
{"hw2_unit2", "Tisk a sken", "Print and scan"},
{"hw2_unit2_sub", "Laser, inkoust a síťová fronta.", "Laser, inkjet, and a network queue."},
{"hw2_ex2_title", "Kvíz: Tisk a sken", "Quiz: Print and scan"},
{"hw2_quiz2_head", "Kvíz k tématu Tisk a sken", "Quiz on Print and scan"},
{"hw2_unit3", "Sběrnice a disky", "Buses and drives"},
{"hw2_unit3_sub", "USB, SATA a NVMe nejsou zaměnitelné jen konektorem.", "USB, SATA and NVMe are not interchangeable just because of the plug."},
{"hw2_ex3_title", "Kvíz: Sběrnice a disky", "Quiz: Buses and drives"},
{"hw2_quiz3_head", "Kvíz k tématu Sběrnice a disky", "Quiz on Buses and drives"},
{"hw2_unit4", "Chlazení", "Cooling"},
{"hw2_unit4_sub", "Vzduch musí mít kudy dovnitř a ven.", "Air needs a way in and a way out."},
{"hw2_ex4_title", "Kvíz: Chlazení", "Quiz: Cooling"},
{"hw2_quiz4_head", "Kvíz k tématu Chlazení", "Quiz on Cooling"},
{"hw2_unit5", "Sestavení", "Building a PC"},
{"hw2_unit5_sub", "Pořadí, které nerozbije desku.", "An order that does not break the board."},
{"hw2_ex5_title", "Kvíz: Sestavení", "Quiz: Building a PC"},
{"hw2_quiz5_head", "Kvíz k tématu Sestavení", "Quiz on Building a PC"},
{"hw2_unit6", "UEFI a boot", "UEFI and boot"},
{"hw2_unit6_sub", "Co se děje, než naběhne systém.", "What happens before the operating system starts."},
{"hw2_ex6_title", "Kvíz: UEFI a boot", "Quiz: UEFI and boot"},
{"hw2_quiz6_head", "Kvíz k tématu UEFI a boot", "Quiz on UEFI and boot"},
{"hw2_unit7", "Diagnostika", "Diagnostics"},
{"hw2_unit7_sub", "POST, pípání a minimum, které ještě naběhne.", "POST, beep codes, and the smallest build that still starts."},
{"hw2_ex7_title", "Kvíz: Diagnostika", "Quiz: Diagnostics"},
{"hw2_quiz7_head", "Kvíz k tématu Diagnostika", "Quiz on Diagnostics"},
{"hw2_unit8", "Porty a dokování", "Ports and docks"},
{"hw2_unit8_sub", "Co z notebooku opravdu vyleze.", "What a laptop can actually drive."},
{"hw2_ex8_title", "Kvíz: Porty a dokování", "Quiz: Ports and docks"},
{"hw2_quiz8_head", "Kvíz k tématu Porty a dokování", "Quiz on Ports and docks"},
{"hw3_sub", "RAID, NAS, servery, virtualizace, zálohy, UPS a firmware.", "RAID, NAS, servers, virtualisation, backups, UPS and firmware."},
{"hw3_unit1", "RAID", "RAID"},
{"hw3_unit1_sub", "Více disků jako jeden svazek, ne jako záloha.", "Several drives as one volume, not as a backup."},
{"hw3_ex1_title", "Kvíz: RAID", "Quiz: RAID"},
{"hw3_quiz1_head", "Kvíz k tématu RAID", "Quiz on RAID"},
{"hw3_unit2", "NAS a sítě disků", "NAS and storage networks"},
{"hw3_unit2_sub", "Disky, které nejsou uvnitř jednoho počítače.", "Drives that are not inside one computer."},
{"hw3_ex2_title", "Kvíz: NAS a sítě disků", "Quiz: NAS and storage networks"},
{"hw3_quiz2_head", "Kvíz k tématu NAS a sítě disků", "Quiz on NAS and storage networks"},
{"hw3_unit3", "Servery a rack", "Servers and racks"},
{"hw3_unit3_sub", "Skříň, do které se nedává herní počítač naležato.", "A cabinet that is not a gaming PC on its side."},
{"hw3_ex3_title", "Kvíz: Servery a rack", "Quiz: Servers and racks"},
{"hw3_quiz3_head", "Kvíz k tématu Servery a rack", "Quiz on Servers and racks"},
{"hw3_unit4", "Virtualizace", "Virtualisation"},
{"hw3_unit4_sub", "Více systémů na jednom železe.", "Several systems on one piece of hardware."},
{"hw3_ex4_title", "Kvíz: Virtualizace", "Quiz: Virtualisation"},
{"hw3_quiz4_head", "Kvíz k tématu Virtualizace", "Quiz on Virtualisation"},
{"hw3_unit5", "Zálohování", "Backups"},
{"hw3_unit5_sub", "Kopie, kterou umíte vrátit.", "A copy you can actually restore."},
{"hw3_ex5_title", "Kvíz: Zálohování", "Quiz: Backups"},
{"hw3_quiz5_head", "Kvíz k tématu Zálohování", "Quiz on Backups"},
{"hw3_unit6", "UPS", "UPS"},
{"hw3_unit6_sub", "Minuty na uložení, ne náhradní elektrárna.", "Minutes to shut down cleanly, not a spare power station."},
{"hw3_ex6_title", "Kvíz: UPS", "Quiz: UPS"},
{"hw3_quiz6_head", "Kvíz k tématu UPS", "Quiz on UPS"},
{"hw3_unit7", "Firmware", "Firmware"},
{"hw3_unit7_sub", "Aktualizace, která nejde udělat uprostřed hodiny.", "An update that is not applied in the middle of a lesson."},
{"hw3_ex7_title", "Kvíz: Firmware", "Quiz: Firmware"},
{"hw3_quiz7_head", "Kvíz k tématu Firmware", "Quiz on Firmware"},
{"hw3_unit8", "Měření výkonu", "Measuring performance"},
{"hw3_unit8_sub", "Číslo bez podmínek nic neznamená.", "A number without the conditions means nothing."},
{"hw3_ex8_title", "Kvíz: Měření výkonu", "Quiz: Measuring performance"},
{"hw3_quiz8_head", "Kvíz k tématu Měření výkonu", "Quiz on Measuring performance"},
{"hw4_sub", "Údržba, TPM, nákup, mobily, periferie, ekologie a maturita.", "Maintenance, TPM, buying, mobiles, peripherals, disposal and the exam."},
{"hw4_unit1", "Údržba", "Maintenance"},
{"hw4_unit1_sub", "Čištění a revize dřív, než něco shoří.", "Cleaning and checks before something burns."},
{"hw4_ex1_title", "Kvíz: Údržba", "Quiz: Maintenance"},
{"hw4_quiz1_head", "Kvíz k tématu Údržba", "Quiz on Maintenance"},
{"hw4_unit2", "TPM a Secure Boot", "TPM and Secure Boot"},
{"hw4_unit2_sub", "Železo, které hlídá start systému.", "Hardware that watches the system start."},
{"hw4_ex2_title", "Kvíz: TPM a Secure Boot", "Quiz: TPM and Secure Boot"},
{"hw4_quiz2_head", "Kvíz k tématu TPM a Secure Boot", "Quiz on TPM and Secure Boot"},
{"hw4_unit3", "Nákup sestavy", "Buying a computer"},
{"hw4_unit3_sub", "Nejdřív práce, potom krabice.", "The work comes first, then the box."},
{"hw4_ex3_title", "Kvíz: Nákup sestavy", "Quiz: Buying a computer"},
{"hw4_quiz3_head", "Kvíz k tématu Nákup sestavy", "Quiz on Buying a computer"},
{"hw4_unit4", "Mobilní zařízení", "Mobile devices"},
{"hw4_unit4_sub", "Tablet a telefon ve školní síti.", "Tablets and phones on the school network."},
{"hw4_ex4_title", "Kvíz: Mobilní zařízení", "Quiz: Mobile devices"},
{"hw4_quiz4_head", "Kvíz k tématu Mobilní zařízení", "Quiz on Mobile devices"},
{"hw4_unit5", "Periferie v provozu", "Peripherals in daily use"},
{"hw4_unit5_sub", "Tiskárny, projektory a co se na nich láme.", "Printers, projectors, and what actually breaks."},
{"hw4_ex5_title", "Kvíz: Periferie v provozu", "Quiz: Peripherals in daily use"},
{"hw4_quiz5_head", "Kvíz k tématu Periferie v provozu", "Quiz on Peripherals in daily use"},
{"hw4_unit6", "Ekologie a data", "Disposal and data"},
{"hw4_unit6_sub", "Vyhozený disk pořád obsahuje školu.", "A discarded drive still contains the school."},
{"hw4_ex6_title", "Kvíz: Ekologie a data", "Quiz: Disposal and data"},
{"hw4_quiz6_head", "Kvíz k tématu Ekologie a data", "Quiz on Disposal and data"},
{"hw4_unit7", "Metodika závady", "A fault method"},
{"hw4_unit7_sub", "Jedna věta, která drží celý postup.", "One sentence that holds the whole procedure."},
{"hw4_ex7_title", "Kvíz: Metodika závady", "Quiz: A fault method"},
{"hw4_quiz7_head", "Kvíz k tématu Metodika závady", "Quiz on A fault method"},
{"hw4_unit8", "Opakování k maturitě", "Exam review"},
{"hw4_unit8_sub", "Železo, které umíte pojmenovat a zdůvodnit.", "Hardware you can name and justify."},
{"hw4_ex8_title", "Kvíz: Opakování k maturitě", "Quiz: Exam review"},
{"hw4_quiz8_head", "Kvíz k tématu Opakování k maturitě", "Quiz on Exam review"},

View file

@ -426,6 +426,8 @@ static gboolean run_smoke_test(gpointer data) {
"mluve1", "mluve20", "mluve1", "mluve20",
"netunit1", "netex1", "netunit10", "netex10", "netunit1", "netex1", "netunit10", "netex10",
"netunit27", "netex27", "netunit27", "netex27",
"net2map", "net2unit1", "net2ex1", "net4unit8", "net4ex8",
"hw2map", "hw2unit1", "hw2ex1", "hw4unit8", "hw4ex8",
NULL NULL
}; };
const char *report = g_getenv("MATURITA_SMOKE"); const char *report = g_getenv("MATURITA_SMOKE");
@ -532,6 +534,7 @@ void activate(GtkApplication *app, gpointer user_data) {
gtk_stack_add_named(main_stack, build_netmap_page(), "netmap"); gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");
gtk_stack_add_named(main_stack, build_hwyears_page(), "hwyears"); gtk_stack_add_named(main_stack, build_hwyears_page(), "hwyears");
gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap"); gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap");
add_it_year_pages(main_stack);
gtk_stack_add_named(main_stack, build_onyears_page(), "onyears"); gtk_stack_add_named(main_stack, build_onyears_page(), "onyears");
gtk_stack_add_named(main_stack, build_onmap_page(), "onmap"); gtk_stack_add_named(main_stack, build_onmap_page(), "onmap");
add_on_pages(main_stack); add_on_pages(main_stack);

View file

@ -584,8 +584,21 @@ static GtkWidget *net_year_button(int year, gboolean locked) {
gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); gtk_widget_set_valign(icon, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), icon); gtk_box_append(GTK_BOX(row), icon);
} else { } else {
static const char *pages[] = {
"netmap", "net2map", "net3map", "net4map",
};
static const char *subs[] = {
"net_sub", "net2_sub", "net3_sub", "net4_sub",
};
GtkWidget *sub = gtk_label_new(NULL);
i18n_bind(sub, subs[year - 1], 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
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_object_set_data_full(G_OBJECT(btn), "target",
g_strdup("netmap"), g_free); g_strdup(pages[year - 1]), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} }
@ -623,7 +636,7 @@ GtkWidget *build_netyears_page(void) {
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
for (y = 1; y <= 4; y++) for (y = 1; y <= 4; y++)
gtk_box_append(GTK_BOX(list), net_year_button(y, y > 1)); gtk_box_append(GTK_BOX(list), net_year_button(y, FALSE));
return page; return page;
} }

View file

@ -60,6 +60,7 @@ void load_progress(void) {
on4_load_progress(); on4_load_progress();
en_load_progress(); en_load_progress();
de_load_progress(); de_load_progress();
quizyear_load_all();
lit_load_progress(); lit_load_progress();
sci_load_progress(); sci_load_progress();
fyz_load_progress(); fyz_load_progress();
@ -259,6 +260,7 @@ void progress_for_net(ProgressSum *out) {
out->done_units++; out->done_units++;
} }
} }
progress_for_net_years(out);
} }
void hw_load_progress(void) { void hw_load_progress(void) {
@ -350,6 +352,7 @@ void progress_for_hw(ProgressSum *out) {
out->done_units++; out->done_units++;
} }
} }
progress_for_hw_years(out);
} }
void on_load_progress(void) { void on_load_progress(void) {

451
src/quizyear.c Normal file
View file

@ -0,0 +1,451 @@
#include "graduately.h"
/* Later years of networks and hardware. Year 1 keeps its own path.
* These years are a list of lessons, slides and a four-question quiz. */
#define QY_N 8
#define QY_COUNT 6
struct QuizYear {
const char *prefix;
const char *map_page;
const char *back_page;
const char *title_key;
const char *sub_key;
const char *file;
NetLesson lessons[QY_N];
char unit_page[QY_N][16];
char ex_page[QY_N][16];
char unit_key[QY_N][20];
char sub_keybuf[QY_N][24];
GtkWidget *nodes[QY_N];
};
static QuizYear years[QY_COUNT];
static gboolean qy_ready;
QuizYear *qy_net2;
QuizYear *qy_net3;
QuizYear *qy_net4;
QuizYear *qy_hw2;
QuizYear *qy_hw3;
QuizYear *qy_hw4;
static char map_store[QY_COUNT][16];
static char file_store[QY_COUNT][16];
void quizyear_boot(void) {
static const char *prefix[] = {
"net2", "net3", "net4", "hw2", "hw3", "hw4",
};
static const char *back[] = {
"netyears", "netyears", "netyears",
"hwyears", "hwyears", "hwyears",
};
static const char *title[] = {
"net_year2", "net_year3", "net_year4",
"hw_year2", "hw_year3", "hw_year4",
};
static const char *sub[] = {
"net2_sub", "net3_sub", "net4_sub",
"hw2_sub", "hw3_sub", "hw4_sub",
};
QuizYear **slot[] = {
&qy_net2, &qy_net3, &qy_net4, &qy_hw2, &qy_hw3, &qy_hw4,
};
if (qy_ready)
return;
for (int y = 0; y < QY_COUNT; y++) {
QuizYear *Y = &years[y];
Y->prefix = prefix[y];
Y->back_page = back[y];
Y->title_key = title[y];
Y->sub_key = sub[y];
g_snprintf(map_store[y], sizeof map_store[y], "%smap", prefix[y]);
g_snprintf(file_store[y], sizeof file_store[y], "%s.conf", prefix[y]);
Y->map_page = map_store[y];
Y->file = file_store[y];
for (int i = 0; i < QY_N; i++) {
g_snprintf(Y->unit_page[i], sizeof Y->unit_page[i],
"%sunit%d", prefix[y], i + 1);
g_snprintf(Y->ex_page[i], sizeof Y->ex_page[i],
"%sex%d", prefix[y], i + 1);
g_snprintf(Y->unit_key[i], sizeof Y->unit_key[i],
"%s_unit%d", prefix[y], i + 1);
g_snprintf(Y->sub_keybuf[i], sizeof Y->sub_keybuf[i],
"%s_unit%d_sub", prefix[y], i + 1);
Y->lessons[i].unit_page = Y->unit_page[i];
Y->lessons[i].ex_page = Y->ex_page[i];
Y->lessons[i].n_slides = 3;
}
*slot[y] = Y;
}
qy_ready = TRUE;
}
static char *qy_path(QuizYear *Y) {
return g_build_filename(app_progress_dir, Y->file, NULL);
}
static void qy_save(QuizYear *Y) {
GKeyFile *kf = g_key_file_new();
char *path = qy_path(Y);
gchar *data;
GError *err = NULL;
for (int i = 0; i < QY_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d", i + 1);
g_key_file_set_boolean(kf, "done", key, Y->lessons[i].done);
}
data = g_key_file_to_data(kf, NULL, NULL);
if (!g_file_set_contents(path, data, -1, &err)) {
g_warning("quiz year: %s", err->message);
g_error_free(err);
}
g_free(data);
g_free(path);
g_key_file_free(kf);
}
static void qy_load_one(QuizYear *Y) {
GKeyFile *kf = g_key_file_new();
char *path = qy_path(Y);
GError *err = NULL;
if (!g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
g_free(path);
return;
}
for (int i = 0; i < QY_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d", i + 1);
Y->lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
g_key_file_free(kf);
g_free(path);
}
void quizyear_load_all(void) {
quizyear_boot();
for (int y = 0; y < QY_COUNT; y++)
qy_load_one(&years[y]);
}
static void qy_refresh_node(QuizYear *Y, int index) {
GtkWidget *node = Y->nodes[index];
if (!node)
return;
if (Y->lessons[index].done) {
gtk_widget_remove_css_class(node, "current");
gtk_widget_add_css_class(node, "done");
} else {
gtk_widget_remove_css_class(node, "done");
gtk_widget_add_css_class(node, "current");
}
}
static void qy_mark(QuizYear *Y, int index) {
if (index < 0 || index >= QY_N || Y->lessons[index].done)
return;
Y->lessons[index].done = TRUE;
qy_save(Y);
qy_refresh_node(Y, index);
refresh_stats_ui();
}
static void qy_add(ProgressSum *out, int first, int n) {
for (int y = first; y < first + n; y++) {
out->total_ex += QY_N;
out->open_units += QY_N;
for (int i = 0; i < QY_N; i++) {
if (years[y].lessons[i].done) {
out->done_ex++;
out->done_units++;
}
}
}
}
void progress_for_net_years(ProgressSum *out) {
quizyear_boot();
qy_add(out, 0, 3);
}
void progress_for_hw_years(ProgressSum *out) {
quizyear_boot();
qy_add(out, 3, 3);
}
static void qy_slide_prev(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (L && L->idx > 0) {
L->idx--;
skill_slide_apply(L);
}
}
static void qy_slide_next(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (!L)
return;
if (L->idx + 1 < L->n_slides) {
L->idx++;
skill_slide_apply(L);
} else if (L->ex_page) {
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
}
}
void quizyear_apply_lang(void) {
if (!qy_ready)
return;
for (int y = 0; y < QY_COUNT; y++)
for (int i = 0; i < QY_N; i++)
skill_slide_apply(&years[y].lessons[i]);
}
GtkWidget *quizyear_unit(QuizYear *Y, int index, const char *title_key,
const char *sub_key, const NetSlide *slides,
int n_slides) {
NetLesson *L = &Y->lessons[index];
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *nav;
net_lesson_notes_ensure(L);
net_notes_target = L;
L->n_slides = (guint)n_slides;
gtk_widget_set_margin_start(page, 28);
gtk_widget_set_margin_end(page, 28);
gtk_widget_set_margin_top(page, 20);
gtk_widget_set_margin_bottom(page, 20);
gtk_box_append(GTK_BOX(page), top_bar(Y->map_page, title_key, sub_key));
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, 10);
gtk_box_append(GTK_BOX(page), scroll);
L->stack = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(L->stack),
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack);
for (int s = 0; s < n_slides; s++) {
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
char name[16];
g_snprintf(name, sizeof name, "slide%d", s);
gtk_widget_set_margin_top(card, 8);
gtk_widget_set_margin_bottom(card, 8);
net_note_kicker(card, slides[s].kicker);
net_note_title(card, slides[s].title);
for (int i = 0; i < 8 && slides[s].line[i]; i++)
net_note_line(card, slides[s].line[i], FALSE);
if (slides[s].tip)
net_note_line(card, slides[s].tip, TRUE);
gtk_stack_add_named(GTK_STACK(L->stack), card, name);
}
nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_box_set_homogeneous(GTK_BOX(nav), TRUE);
gtk_widget_set_margin_top(nav, 10);
gtk_box_append(GTK_BOX(page), nav);
L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev"));
gtk_widget_add_css_class(L->prev_btn, "btn-primary");
gtk_box_append(GTK_BOX(nav), L->prev_btn);
g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(qy_slide_prev), L);
L->next_btn = gtk_button_new_with_label(tr("net_slide_next"));
gtk_widget_add_css_class(L->next_btn, "btn-primary");
gtk_box_append(GTK_BOX(nav), L->next_btn);
g_signal_connect(L->next_btn, "clicked", G_CALLBACK(qy_slide_next), L);
net_rescale_lesson_notes(L, 18, 32, 15);
net_notes_target = NULL;
L->idx = 0;
skill_slide_apply(L);
return page;
}
typedef struct {
QuizYear *year;
int index;
const ChoiceQ *qs;
int n;
int n_opts;
GtkWidget *feedback;
GtkToggleButton **toggles;
GtkWidget **hints;
} QyQuiz;
static void qy_check(GtkButton *button, gpointer data) {
QyQuiz *ctx = data;
int ok = 0;
(void)button;
for (int i = 0; i < ctx->n; i++) {
int active = -1;
for (int o = 0; o < ctx->n_opts; o++) {
GtkToggleButton *tb = ctx->toggles[i * ctx->n_opts + o];
gtk_widget_remove_css_class(GTK_WIDGET(tb), "ok");
gtk_widget_remove_css_class(GTK_WIDGET(tb), "wrong");
if (gtk_toggle_button_get_active(tb))
active = o;
}
if (active == ctx->qs[i].correct) {
ok++;
if (active >= 0)
gtk_widget_add_css_class(
GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "ok");
} else if (active >= 0) {
gtk_widget_add_css_class(
GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "wrong");
}
if (ctx->hints && ctx->hints[i])
gtk_widget_set_visible(ctx->hints[i], TRUE);
}
if (ok == ctx->n) {
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
qy_mark(ctx->year, ctx->index);
} else {
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
}
}
GtkWidget *quizyear_quiz(QuizYear *Y, int index, const char *title_key,
const char *head_key, const ChoiceQ *qs,
const char **hints, int n) {
QyQuiz *ctx = g_new0(QyQuiz, 1);
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
GtkWidget *head = gtk_label_new(NULL);
GtkWidget *intro = gtk_label_new(NULL);
GtkWidget *check;
int n_opts = qs[0].n_options;
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(Y->lessons[index].unit_page, title_key, NULL));
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 12);
gtk_box_append(GTK_BOX(page), scroll);
gtk_widget_set_hexpand(body, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
i18n_bind(head, head_key, 0);
gtk_widget_set_halign(head, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(head), TRUE);
gtk_widget_add_css_class(head, "quiz-heading");
gtk_box_append(GTK_BOX(body), head);
i18n_bind(intro, "net_quiz_intro", 0);
gtk_widget_set_halign(intro, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(intro), TRUE);
gtk_widget_add_css_class(intro, "quiz-intro");
gtk_box_append(GTK_BOX(body), intro);
ctx->year = Y;
ctx->index = index;
ctx->qs = qs;
ctx->n = n;
ctx->n_opts = n_opts;
ctx->toggles = g_new0(GtkToggleButton *, n * n_opts);
ctx->hints = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) {
GtkWidget *card = mcq_append_question(body, i + 1, &qs[i],
&ctx->toggles[i * n_opts]);
ctx->hints[i] = meaning_add(card, hints ? hints[i] : NULL);
}
check = gtk_button_new();
i18n_bind(check, "check", 1);
gtk_widget_add_css_class(check, "btn-primary");
gtk_widget_set_halign(check, GTK_ALIGN_START);
gtk_widget_set_margin_top(check, 8);
gtk_box_append(GTK_BOX(body), check);
g_signal_connect(check, "clicked", G_CALLBACK(qy_check), ctx);
ctx->feedback = gtk_label_new("");
gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(ctx->feedback), TRUE);
gtk_box_append(GTK_BOX(body), ctx->feedback);
return page;
}
GtkWidget *quizyear_map(QuizYear *Y) {
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
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(Y->back_page, Y->title_key, Y->sub_key));
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);
for (int i = 0; i < QY_N; i++) {
GtkWidget *btn = gtk_button_new();
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
GtkWidget *texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
GtkWidget *title = gtk_label_new(NULL);
GtkWidget *sub = gtk_label_new(NULL);
char badge[8];
GtkWidget *num;
g_snprintf(badge, sizeof badge, "%d", i + 1);
num = gtk_label_new(badge);
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, Y->unit_key[i], 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, Y->sub_keybuf[i], 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(Y->unit_page[i]), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
Y->nodes[i] = btn;
qy_refresh_node(Y, i);
gtk_box_append(GTK_BOX(list), btn);
}
return page;
}

View file

@ -188,9 +188,9 @@ static void catalog_rebuild(void) {
catalog_add_key("net_years_title", "search_subject", "netyears", FALSE, 0, catalog_add_key("net_years_title", "search_subject", "netyears", FALSE, 0,
"site network ip"); "site network ip");
catalog_add_key("net_year1", "search_lesson", "netmap", FALSE, 1, "rocnik"); catalog_add_key("net_year1", "search_lesson", "netmap", FALSE, 1, "rocnik");
catalog_add_key("net_year2", "search_lesson", "netyears", TRUE, 1, "rocnik"); catalog_add_key("net_year2", "search_lesson", "net2map", FALSE, 1, "rocnik");
catalog_add_key("net_year3", "search_lesson", "netyears", TRUE, 1, "rocnik"); catalog_add_key("net_year3", "search_lesson", "net3map", FALSE, 1, "rocnik");
catalog_add_key("net_year4", "search_lesson", "netyears", TRUE, 1, "rocnik"); catalog_add_key("net_year4", "search_lesson", "net4map", FALSE, 1, "rocnik");
for (i = 1; i <= NET_LESSONS; i++) { for (i = 1; i <= NET_LESSONS; i++) {
g_snprintf(key, sizeof(key), "net_unit%d", i); g_snprintf(key, sizeof(key), "net_unit%d", i);
g_snprintf(page, sizeof(page), "netunit%d", i); g_snprintf(page, sizeof(page), "netunit%d", i);
@ -198,11 +198,18 @@ static void catalog_rebuild(void) {
g_snprintf(page, sizeof(page), "netex%d", i); g_snprintf(page, sizeof(page), "netex%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 = 2; i <= 4; i++) {
for (n = 1; n <= 8; n++) {
g_snprintf(key, sizeof key, "net%d_unit%d", i, n);
g_snprintf(page, sizeof page, "net%dunit%d", i, n);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "site sit");
}
}
catalog_add_key("hw_year1", "search_lesson", "hwmap", FALSE, 1, "rocnik"); catalog_add_key("hw_year1", "search_lesson", "hwmap", FALSE, 1, "rocnik");
catalog_add_key("hw_year2", "search_lesson", "hwyears", TRUE, 1, "rocnik"); catalog_add_key("hw_year2", "search_lesson", "hw2map", FALSE, 1, "rocnik");
catalog_add_key("hw_year3", "search_lesson", "hwyears", TRUE, 1, "rocnik"); catalog_add_key("hw_year3", "search_lesson", "hw3map", FALSE, 1, "rocnik");
catalog_add_key("hw_year4", "search_lesson", "hwyears", TRUE, 1, "rocnik"); catalog_add_key("hw_year4", "search_lesson", "hw4map", FALSE, 1, "rocnik");
for (i = 1; i <= HW_LESSONS; i++) { for (i = 1; i <= HW_LESSONS; i++) {
g_snprintf(key, sizeof(key), "hw_unit%d", i); g_snprintf(key, sizeof(key), "hw_unit%d", i);
g_snprintf(page, sizeof(page), "hwunit%d", i); g_snprintf(page, sizeof(page), "hwunit%d", i);
@ -210,6 +217,13 @@ static void catalog_rebuild(void) {
g_snprintf(page, sizeof(page), "hwex%d", i); g_snprintf(page, sizeof(page), "hwex%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 = 2; i <= 4; i++) {
for (n = 1; n <= 8; n++) {
g_snprintf(key, sizeof key, "hw%d_unit%d", i, n);
g_snprintf(page, sizeof page, "hw%dunit%d", i, n);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "hardware hw");
}
}
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");