From d453c54e526f5eb712ab9563bccc8a35b1b4db11 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Fri, 2 Oct 2026 22:38:06 +0200 Subject: [PATCH] Add year-2 civics lessons on the citizen in a democratic state. Co-authored-by: Cursor --- README.md | 4 +- .../org/maturita/maturita/AppViewModel.kt | 5 + .../org/maturita/maturita/MainActivity.kt | 15 + .../main/java/org/maturita/maturita/Route.kt | 17 +- .../org/maturita/maturita/data/Content.kt | 2 + .../maturita/maturita/data/ProgressStore.kt | 11 + .../org/maturita/maturita/ui/Exercises.kt | 12 + .../java/org/maturita/maturita/ui/Screens.kt | 16 +- data/changelog.txt | 6 +- ios/Graduately/AppModel.swift | 5 + ios/Graduately/Data/Content.swift | 3 + ios/Graduately/Data/ProgressStore.swift | 11 + ios/Graduately/Route.swift | 19 + ios/Graduately/UI/ExercisesMore.swift | 19 + ios/Graduately/UI/Screens.swift | 37 +- scripts/extract-android-content.py | 19 + src/graduately.h | 14 + src/i18n.c | 100 + src/main.c | 5 +- src/obcanka.c | 11 +- src/obcanka2.c | 1911 +++++++++++++++++ src/progress.c | 91 +- src/search.c | 9 +- src/theme.c | 1 + src/util.c | 3 + 25 files changed, 2323 insertions(+), 23 deletions(-) create mode 100644 src/obcanka2.c diff --git a/README.md b/README.md index 20278b7..9604d3b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses - **Č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á - **Občanská nauka** — 1. ročník, 17 lekcí (člověk v lidském společenství); - ročníky 2–4 jsou zamčené + 2. ročník, 16 lekcí (člověk jako občan); ročníky 3–4 jsou zamčené - 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` @@ -264,7 +264,7 @@ Intel Mac, another architecture, or hacking on the code → - **Czech language** — grammar (20 exercises) and required reading (82 books: notes, quiz, plot ordering); literature theory is still locked - **Civics** — year 1, 17 lessons (the individual and society); - years 2–4 are locked + year 2, 16 lessons (the citizen in a democratic state); years 3–4 are locked - statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English - quit with `Cmd/Super+Q` or `Alt+F4` diff --git a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt index e04e096..463d831 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -131,6 +131,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markOn2(id: Int) { + progress.markOn2(id) + refresh() + } + fun markMluv(n: Int) { progress.markMluv(n) refresh() diff --git a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt index ae2051d..c4653d1 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -37,6 +37,7 @@ import org.maturita.maturita.ui.LitQuizScreen import org.maturita.maturita.ui.MluvExercise import org.maturita.maturita.ui.NetQuizScreen import org.maturita.maturita.ui.NetYearsScreen +import org.maturita.maturita.ui.On2QuizScreen import org.maturita.maturita.ui.OnQuizScreen import org.maturita.maturita.ui.OnYearsScreen import org.maturita.maturita.ui.PlotScreen @@ -243,6 +244,20 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.OnEx -> OnQuizScreen(vm, r.id) + Route.On2Map -> LessonListScreen( + vm, vm.tr("on_year2"), vm.tr("on2_sub"), + vm.content.on2.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.on2Done(id), Route.On2Lesson(id)) + }, + ) + is Route.On2Lesson -> { + val l = vm.content.on2Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.On2Ex(r.id)) + } + } + is Route.On2Ex -> On2QuizScreen(vm, r.id) Route.CzechMap -> CzechMapScreen(vm) Route.Mluvnice -> LessonListScreen( vm, vm.tr("Mluvnice"), vm.tr("mluv_sub"), diff --git a/android/app/src/main/java/org/maturita/maturita/Route.kt b/android/app/src/main/java/org/maturita/maturita/Route.kt index d696205..f1ea5a8 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -21,6 +21,9 @@ sealed class Route(val page: String) { data object OnMap : Route("onmap") data class OnLesson(val id: Int) : Route("onunit$id") data class OnEx(val id: Int) : Route("onex$id") + data object On2Map : Route("on2map") + data class On2Lesson(val id: Int) : Route("on2unit$id") + data class On2Ex(val id: Int) : Route("on2ex$id") data object CzechMap : Route("czechmap") data object Mluvnice : Route("mluvnice") data class MluvEx(val n: Int) : Route("mluve$n") @@ -35,8 +38,8 @@ sealed class Route(val page: String) { /** Screens with their own bottom action bar; the navigation bar stays hidden there. */ val isExercise: Boolean get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx || - this is OnEx || this is MluvEx || this is BookQuiz || this is BookPlot || - this is NetLesson || this is HwLesson || this is OnLesson + this is OnEx || this is On2Ex || this is MluvEx || this is BookQuiz || this is BookPlot || + this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson /** Full stack from the practice root to this screen, so back always goes up one level. */ fun stack(): List = when (this) { @@ -59,6 +62,9 @@ sealed class Route(val page: String) { OnMap -> listOf(Subjects, OnYears, OnMap) is OnLesson -> listOf(Subjects, OnYears, OnMap, this) is OnEx -> listOf(Subjects, OnYears, OnMap, OnLesson(id), this) + On2Map -> listOf(Subjects, OnYears, On2Map) + is On2Lesson -> listOf(Subjects, OnYears, On2Map, this) + is On2Ex -> listOf(Subjects, OnYears, On2Map, On2Lesson(id), this) CzechMap -> listOf(Subjects, CzechMap) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) @@ -81,6 +87,7 @@ fun routeFromPage(page: String): Route? = when (page) { "hwmap" -> Route.HwMap "onyears" -> Route.OnYears "onmap" -> Route.OnMap + "on2map" -> Route.On2Map "czechmap" -> Route.CzechMap "mluvnice" -> Route.Mluvnice "readinglist" -> Route.ReadingList @@ -112,6 +119,12 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""hwex(\d+)""").matchEntire(page)?.let { return Route.HwEx(it.groupValues[1].toInt()) } + Regex("""on2unit(\d+)""").matchEntire(page)?.let { + return Route.On2Lesson(it.groupValues[1].toInt()) + } + Regex("""on2ex(\d+)""").matchEntire(page)?.let { + return Route.On2Ex(it.groupValues[1].toInt()) + } Regex("""onunit(\d+)""").matchEntire(page)?.let { return Route.OnLesson(it.groupValues[1].toInt()) } diff --git a/android/app/src/main/java/org/maturita/maturita/data/Content.kt b/android/app/src/main/java/org/maturita/maturita/data/Content.kt index 09fdc68..7cea228 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/Content.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/Content.kt @@ -30,6 +30,7 @@ class Content(root: JSONObject) { } val hw = raw.arr("hw") val on = raw.arr("on") + val on2 = raw.arr("on2") val mluvnice = raw.arr("mluvnice") val books = raw.arr("books") val changelog = raw.arr("changelog") @@ -38,6 +39,7 @@ class Content(root: JSONObject) { fun netLesson(id: Int) = netLessons.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 on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id } fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n } fun book(id: String) = books.firstOrNull { it.str("id") == id } diff --git a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt index b6b52e5..ce087da 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt @@ -35,6 +35,9 @@ class ProgressStore(context: Context) { fun onDone(id: Int) = prefs.getBoolean("on.$id", false) fun markOn(id: Int) { prefs.edit().putBoolean("on.$id", true).apply() } + fun on2Done(id: Int) = prefs.getBoolean("on2.$id", false) + fun markOn2(id: Int) { prefs.edit().putBoolean("on2.$id", true).apply() } + fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } @@ -81,6 +84,14 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + content.on2.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.on2Done(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } sum.totalEx += content.mluvnice.size sum.openUnits += 1 val md = content.mluvnice.count { p.mluvDone(it.int("id")) } diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt index 85e1487..495fe50 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt @@ -860,6 +860,18 @@ fun OnQuizScreen(vm: AppViewModel, id: Int) { DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn(id) } } +@Composable +fun On2QuizScreen(vm: AppViewModel, id: Int) { + val lesson = vm.content.on2Lesson(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.markOn2(id) } +} + @Composable fun HwQuizScreen(vm: AppViewModel, id: Int) { val lesson = vm.content.hwLesson(id) ?: return diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index 6496900..5b6a0af 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -300,9 +300,10 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { sum.totalEx = vm.content.hw.size sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } } - "onyears", "onmap" -> { - sum.totalEx = vm.content.on.size - sum.doneEx = vm.content.on.count { vm.progress.onDone(it.int("id")) } + "onyears", "onmap", "on2map" -> { + sum.totalEx = vm.content.on.size + vm.content.on2.size + sum.doneEx = vm.content.on.count { vm.progress.onDone(it.int("id")) } + + vm.content.on2.count { vm.progress.on2Done(it.int("id")) } } "czechmap" -> { sum.totalEx = vm.content.mluvnice.size @@ -492,13 +493,17 @@ private fun buildSearch(vm: AppViewModel): List { add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}") } add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra = "rocnik") - add(vm.tr("on_year2"), vm.tr("search_lesson"), "onyears", locked = true, 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_year4"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik") 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_exercise"), "onex${l.int("id")}") } + vm.content.on2.forEach { l -> + 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("Mluvnice"), vm.tr("search_lesson"), "mluvnice") 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") @@ -603,8 +608,9 @@ fun OnYearsScreen(vm: AppViewModel) { LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { 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_year2"), subtitle = vm.tr("on2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On2Map) } } - items(listOf("on_year2", "on_year3", "on_year4")) { key -> + items(listOf("on_year3", "on_year4")) { key -> NavRow(title = vm.tr(key), subtitle = vm.tr("on_year_locked_sub"), locked = true, onClick = null) } } diff --git a/data/changelog.txt b/data/changelog.txt index 09fe6d7..cacdc02 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -8,9 +8,11 @@ --- 2026-10-02 cs: Občanská nauka: 1. ročník, 17 lekcí (člověk v lidském společenství) -cs: Občanská nauka: další ročníky jsou zatím zamčené +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é en: Civics: year 1, 17 lessons (the individual and society) -en: Civics: later years are still locked +en: Civics: year 2, 16 lessons (the citizen in a democratic state) +en: Civics: years 3 and 4 are still locked --- 2026-09-25 cs: Technické vybavení: pět lekcí o digitalizaci zvuku (vzorkování, kvantování, PCM) diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift index 12f043a..cf5f162 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -133,6 +133,11 @@ final class AppModel: ObservableObject { refresh() } + func markOn2(_ id: Int) { + progress.markOn2(id) + refresh() + } + func markMluv(_ n: Int) { progress.markMluv(n) refresh() diff --git a/ios/Graduately/Data/Content.swift b/ios/Graduately/Data/Content.swift index fa5fb99..e0c3b78 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -32,6 +32,7 @@ final class Content { let netAnswers: [[J]] let hw: [J] let on: [J] + let on2: [J] let mluvnice: [J] let books: [J] let changelog: [J] @@ -64,6 +65,7 @@ final class Content { } hw = raw.arr("hw") on = raw.arr("on") + on2 = raw.arr("on2") mluvnice = raw.arr("mluvnice") books = raw.arr("books") changelog = raw.arr("changelog") @@ -73,6 +75,7 @@ final class Content { func netLesson(_ id: Int) -> J? { netLessons.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 on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } } func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } } func book(_ id: String) -> J? { books.first { $0.str("id") == id } } diff --git a/ios/Graduately/Data/ProgressStore.swift b/ios/Graduately/Data/ProgressStore.swift index 2f694d4..968ea01 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -37,6 +37,9 @@ final class ProgressStore { func onDone(_ id: Int) -> Bool { defaults.bool(forKey: "on.\(id)") } func markOn(_ id: Int) { defaults.set(true, forKey: "on.\(id)") } + func on2Done(_ id: Int) -> Bool { defaults.bool(forKey: "on2.\(id)") } + func markOn2(_ id: Int) { defaults.set(true, forKey: "on2.\(id)") } + func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } @@ -88,6 +91,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for l in content.on2 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.on2Done(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } sum.totalEx += content.mluvnice.count sum.openUnits += 1 let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count diff --git a/ios/Graduately/Route.swift b/ios/Graduately/Route.swift index eace6de..b650b94 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -27,6 +27,9 @@ enum Route: Hashable { case onMap case onLesson(Int) case onEx(Int) + case on2Map + case on2Lesson(Int) + case on2Ex(Int) case czechMap case mluvnice case mluvEx(Int) @@ -56,6 +59,9 @@ enum Route: Hashable { case .onMap: return "onmap" case .onLesson(let id): return "onunit\(id)" case .onEx(let id): return "onex\(id)" + case .on2Map: return "on2map" + case .on2Lesson(let id): return "on2unit\(id)" + case .on2Ex(let id): return "on2ex\(id)" case .czechMap: return "czechmap" case .mluvnice: return "mluvnice" case .mluvEx(let n): return "mluve\(n)" @@ -112,6 +118,12 @@ enum Route: Hashable { return [.onYears, .onMap, .onLesson(id)] case .onEx(let id): return [.onYears, .onMap, .onLesson(id), .onEx(id)] + case .on2Map: + return [.onYears, .on2Map] + case .on2Lesson(let id): + return [.onYears, .on2Map, .on2Lesson(id)] + case .on2Ex(let id): + return [.onYears, .on2Map, .on2Lesson(id), .on2Ex(id)] case .czechMap: return [.czechMap] case .mluvnice: @@ -142,6 +154,7 @@ func routeFromPage(_ page: String) -> Route? { case "hwmap": return .hwMap case "onyears": return .onYears case "onmap": return .onMap + case "on2map": return .on2Map case "czechmap": return .czechMap case "mluvnice": return .mluvnice case "readinglist": return .readingList @@ -185,6 +198,12 @@ func routeFromPage(_ page: String) -> Route? { if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) { return .hwEx(n) } + if let m = page.wholeMatch(of: /on2unit(\d+)/), let n = Int(m.1) { + return .on2Lesson(n) + } + if let m = page.wholeMatch(of: /on2ex(\d+)/), let n = Int(m.1) { + return .on2Ex(n) + } if let m = page.wholeMatch(of: /onunit(\d+)/), let n = Int(m.1) { return .onLesson(n) } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index fd6ccf8..eed50a3 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -644,6 +644,25 @@ struct OnQuizScreen: View { } } +struct On2QuizScreen: View { + @ObservedObject var vm: AppModel + let id: Int + + var body: some View { + if let lesson = vm.content.on2Lesson(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.markOn2(id) + } + } + } +} + struct HwQuizScreen: View { @ObservedObject var vm: AppModel let id: Int diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index fdf1374..0780176 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -423,9 +423,10 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { case "hwyears", "hwmap": sum.totalEx = vm.content.hw.count sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count - case "onyears", "onmap": - sum.totalEx = vm.content.on.count + case "onyears", "onmap", "on2map": + sum.totalEx = vm.content.on.count + vm.content.on2.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 case "czechmap": sum.totalEx = vm.content.mluvnice.count sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count @@ -565,13 +566,17 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))") } add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik") - add(vm.tr("on_year2"), vm.tr("search_lesson"), "onyears", locked: true, 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_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik") 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_exercise"), "onex\(l.int("id"))") } + for l in vm.content.on2 { + 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("Mluvnice"), vm.tr("search_lesson"), "mluvnice") add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist") for b in vm.content.books { @@ -796,7 +801,10 @@ struct OnYearsScreen: View { NavigationLink(value: Route.onMap) { ListRowLabel(title: vm.tr("on_year1"), subtitle: vm.tr("on_sub")) } - ForEach(["on_year2", "on_year3", "on_year4"], id: \.self) { key in + NavigationLink(value: Route.on2Map) { + ListRowLabel(title: vm.tr("on_year2"), subtitle: vm.tr("on2_sub")) + } + ForEach(["on_year3", "on_year4"], id: \.self) { key in Label { ListRowLabel(title: vm.tr(key), subtitle: vm.tr("on_year_locked_sub")) } icon: { @@ -1402,6 +1410,27 @@ struct RouteDestination: View { } case .onEx(let id): OnQuizScreen(vm: vm, id: id) + case .on2Map: + LessonListScreen( + vm: vm, + title: vm.tr("on_year2"), + subtitle: vm.tr("on2_sub"), + rows: vm.content.on2.map { l in + let id = l.int("id") + return LessonRow( + id: "o2\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.on2Done(id), + destination: .on2Lesson(id) + ) + } + ) + case .on2Lesson(let id): + if let l = vm.content.on2Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on2Ex(id)) + } + case .on2Ex(let id): + On2QuizScreen(vm: vm, id: id) case .czechMap: CzechMapScreen(vm: vm) case .mluvnice: diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index c48baeb..2f41ddb 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -1096,6 +1096,18 @@ def main() -> int: "quiz": choice_items(A(f"build_on_unit{i}_exercise_page.qs")), }) + on2_lessons = [] + for i in range(1, 17): + on2_lessons.append({ + "id": i, + "titleKey": f"on2_unit{i}", + "subKey": f"on2_unit{i}_sub", + "exTitleKey": f"on2_ex{i}_title", + "quizHeadKey": f"on2_quiz{i}_head", + "slides": slides_items(A(f"build_on2_unit{i}_page.slides")), + "quiz": choice_items(A(f"build_on2_unit{i}_exercise_page.qs")), + }) + hw_lessons = [] for i in range(1, 30): hw_lessons.append({ @@ -1515,6 +1527,7 @@ def main() -> int: }, "hw": hw_lessons, "on": on_lessons, + "on2": on2_lessons, "mluvnice": mluvnice, "books": books, "changelog": parse_changelog(ROOT / "data" / "changelog.txt"), @@ -1550,6 +1563,7 @@ def main() -> int: print(f" net quizzes: {sum(1 for L in net_lessons if L['quiz'])}") 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" on2 lessons: {sum(1 for L in on2_lessons if L['slides'])}") print(f" mluvnice: {len(mluvnice)}") print(f" books quiz items: {[len(b['quiz']) for b in books]}") print(f" changelog entries: {len(payload.get('changelog', []))}") @@ -1588,6 +1602,11 @@ def main() -> int: missing.append(f"on{L['id']} slides") if not L["quiz"]: missing.append(f"on{L['id']} quiz") + for L in on2_lessons: + if not L["slides"]: + missing.append(f"on2{L['id']} slides") + if not L["quiz"]: + missing.append(f"on2{L['id']} quiz") for m in mluvnice: if m["type"] in ("typed", "reveal") and not m.get("items"): missing.append(f"mluv{m['id']}") diff --git a/src/graduately.h b/src/graduately.h index 742aa8a..fbe28c0 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -50,6 +50,7 @@ extern char *app_settings_file; extern char *app_progress_net; extern char *app_progress_hw; extern char *app_progress_on; +extern char *app_progress_on2; extern char *app_progress_mluvnice; extern char *app_progress_cetba; #define PROGRESS_DIR app_progress_dir @@ -60,6 +61,7 @@ extern char *app_progress_cetba; #define PROGRESS_NET app_progress_net #define PROGRESS_HW app_progress_hw #define PROGRESS_ON app_progress_on +#define PROGRESS_ON2 app_progress_on2 #define NODE_SIZE 88.0 #define PATH_SPAC 240.0 #define ROAD_MX 150.0 /* horizontal canvas margin */ @@ -119,6 +121,8 @@ extern char *app_progress_cetba; #define HW_LESSONS 29 #define ON_UNITS 17 #define ON_LESSONS 17 +#define ON2_UNITS 16 +#define ON2_LESSONS 16 #define HW_SLIDES 3 #define HW2_SLIDES 6 #define HW3_SLIDES 2 @@ -761,6 +765,8 @@ extern NetLesson hw_lessons[HW_LESSONS]; extern GtkWidget *hw_nodes[HW_UNITS]; extern NetLesson on_lessons[ON_LESSONS]; extern GtkWidget *on_nodes[ON_UNITS]; +extern NetLesson on2_lessons[ON2_LESSONS]; +extern GtkWidget *on2_nodes[ON2_UNITS]; extern GtkWidget *net_note_host; extern int net_note_last_w; extern GtkWidget *net_scroll; @@ -858,6 +864,14 @@ void on_lessons_apply_lang(void); GtkWidget *build_onyears_page(void); GtkWidget *build_onmap_page(void); void add_on_pages(GtkStack *stack); +void on2_load_progress(void); +void on2_save_progress(void); +void mark_on2_done(int lesson_id); +void refresh_on2_completion_ui(void); +void on2_rail_theme_reset(void); +void on2_lessons_apply_lang(void); +GtkWidget *build_on2map_page(void); +void add_on2_pages(GtkStack *stack); void czech_rail_theme_reset(void); void hw_lessons_apply_lang(void); GtkWidget *build_hwyears_page(void); diff --git a/src/i18n.c b/src/i18n.c index 5320aa8..fb2c837 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -804,6 +804,104 @@ const TrEntry tr_ui[] = { "2 slides on the household budget and money trouble, then a quiz."}, {"on_ex17_title", "Kvíz: rozpočet domácnosti", "Quiz: household budget"}, {"on_quiz17_head", "Kvíz k rozpočtu domácnosti", "Quiz on the household budget"}, + {"on2_sub", "Člověk jako občan v demokratickém státě. Vyberte lekci na cestě.", + "The citizen in a democratic state. Pick a lesson on the path."}, + {"on2_unit1", "Stát a občan", "The state and the citizen"}, + {"on2_unit1_sub", "2 snímky o státu a občanovi a pak kvíz.", + "2 slides on the state and the citizen, then a quiz."}, + {"on2_ex1_title", "Kvíz: stát a občan", "Quiz: the state and the citizen"}, + {"on2_quiz1_head", "Kvíz ke státu a občanovi", + "Quiz on the state and the citizen"}, + {"on2_unit2", "Státní občanství", "Citizenship"}, + {"on2_unit2_sub", "2 snímky o nabývání občanství a pak kvíz.", + "2 slides on how citizenship is acquired, then a quiz."}, + {"on2_ex2_title", "Kvíz: státní občanství", "Quiz: citizenship"}, + {"on2_quiz2_head", "Kvíz ke státnímu občanství", + "Quiz on citizenship"}, + {"on2_unit3", "Státní symboly", "State symbols"}, + {"on2_unit3_sub", "2 snímky o znacích, vlajce a hymně a pak kvíz.", + "2 slides on the arms, flag and anthem, then a quiz."}, + {"on2_ex3_title", "Kvíz: státní symboly", "Quiz: state symbols"}, + {"on2_quiz3_head", "Kvíz ke státním symbolům", + "Quiz on the state symbols"}, + {"on2_unit4", "Ústava České republiky", "The Constitution"}, + {"on2_unit4_sub", "2 snímky o ústavě a Listině a pak kvíz.", + "2 slides on the Constitution and the Charter, then a quiz."}, + {"on2_ex4_title", "Kvíz: ústava", "Quiz: the Constitution"}, + {"on2_quiz4_head", "Kvíz k ústavě", + "Quiz on the Constitution"}, + {"on2_unit5", "Dělba státní moci", "Separation of powers"}, + {"on2_unit5_sub", "2 snímky o složkách moci a pak kvíz.", + "2 slides on the branches of power, then a quiz."}, + {"on2_ex5_title", "Kvíz: dělba moci", "Quiz: separation of powers"}, + {"on2_quiz5_head", "Kvíz k dělbě moci", + "Quiz on the separation of powers"}, + {"on2_unit6", "Parlament", "Parliament"}, + {"on2_unit6_sub", "2 snímky o Sněmovně, Senátu a zákonech a pak kvíz.", + "2 slides on the Chamber, the Senate and laws, then a quiz."}, + {"on2_ex6_title", "Kvíz: Parlament", "Quiz: Parliament"}, + {"on2_quiz6_head", "Kvíz k Parlamentu", + "Quiz on Parliament"}, + {"on2_unit7", "Prezident a vláda", "President and government"}, + {"on2_unit7_sub", "2 snímky o hlavě státu a vládě a pak kvíz.", + "2 slides on the head of state and the government, then a quiz."}, + {"on2_ex7_title", "Kvíz: prezident a vláda", "Quiz: president and government"}, + {"on2_quiz7_head", "Kvíz k prezidentovi a vládě", + "Quiz on the president and the government"}, + {"on2_unit8", "Veřejná správa a samospráva", "Administration and self-government"}, + {"on2_unit8_sub", "2 snímky o úřadech, obci a kraji a pak kvíz.", + "2 slides on offices, the municipality and the region, then a quiz."}, + {"on2_ex8_title", "Kvíz: správa a samospráva", "Quiz: administration and self-government"}, + {"on2_quiz8_head", "Kvíz k veřejné správě a samosprávě", + "Quiz on administration and self-government"}, + {"on2_unit9", "Principy demokracie", "Principles of democracy"}, + {"on2_unit9_sub", "2 snímky o hodnotách demokracie a jejích ohroženích a pak kvíz.", + "2 slides on democratic values and what threatens them, then a quiz."}, + {"on2_ex9_title", "Kvíz: demokracie", "Quiz: democracy"}, + {"on2_quiz9_head", "Kvíz k principům demokracie", + "Quiz on the principles of democracy"}, + {"on2_unit10", "Lidská práva", "Human rights"}, + {"on2_unit10_sub", "2 snímky o Listině a ochraně práv a pak kvíz.", + "2 slides on the Charter and the protection of rights, then a quiz."}, + {"on2_ex10_title", "Kvíz: lidská práva", "Quiz: human rights"}, + {"on2_quiz10_head", "Kvíz k lidským právům", + "Quiz on human rights"}, + {"on2_unit11", "Práva dětí a ombudsman", "Children's rights and the ombudsman"}, + {"on2_unit11_sub", "2 snímky o právech dítěte a veřejném ochránci práv a pak kvíz.", + "2 slides on children's rights and the public defender of rights, then a quiz."}, + {"on2_ex11_title", "Kvíz: děti a ombudsman", "Quiz: children and the ombudsman"}, + {"on2_quiz11_head", "Kvíz k právům dětí a ombudsmanovi", + "Quiz on children's rights and the ombudsman"}, + {"on2_unit12", "Politické ideologie", "Political ideologies"}, + {"on2_unit12_sub", "2 snímky o hlavních politických směrech a pak kvíz.", + "2 slides on the main political outlooks, then a quiz."}, + {"on2_ex12_title", "Kvíz: ideologie", "Quiz: ideologies"}, + {"on2_quiz12_head", "Kvíz k politickým ideologiím", + "Quiz on political ideologies"}, + {"on2_unit13", "Politické strany a volby", "Parties and elections"}, + {"on2_unit13_sub", "2 snímky o stranách, volebním právu a systémech a pak kvíz.", + "2 slides on parties, the right to vote and electoral systems, then a quiz."}, + {"on2_ex13_title", "Kvíz: strany a volby", "Quiz: parties and elections"}, + {"on2_quiz13_head", "Kvíz ke stranám a volbám", + "Quiz on parties and elections"}, + {"on2_unit14", "Občanská společnost", "Civil society"}, + {"on2_unit14_sub", "2 snímky o spolcích a občanských ctnostech a pak kvíz.", + "2 slides on associations and civic virtues, then a quiz."}, + {"on2_ex14_title", "Kvíz: občanská společnost", "Quiz: civil society"}, + {"on2_quiz14_head", "Kvíz k občanské společnosti", + "Quiz on civil society"}, + {"on2_unit15", "Radikalismus a extremismus", "Radicalism and extremism"}, + {"on2_unit15_sub", "2 snímky o radikalismu, extremismu a terorismu a pak kvíz.", + "2 slides on radicalism, extremism and terrorism, then a quiz."}, + {"on2_ex15_title", "Kvíz: extremismus", "Quiz: extremism"}, + {"on2_quiz15_head", "Kvíz k radikalismu a extremismu", + "Quiz on radicalism and extremism"}, + {"on2_unit16", "Média", "The media"}, + {"on2_unit16_sub", "2 snímky o úloze médií a kritickém čtení a pak kvíz.", + "2 slides on the role of the media and reading them critically, then a quiz."}, + {"on2_ex16_title", "Kvíz: média", "Quiz: the media"}, + {"on2_quiz16_head", "Kvíz k médiím", + "Quiz on the media"}, {"czech_sub", "Vyberte oblast a začněte procvičovat.", "Pick an area and start practicing."}, {"Literatura", "Literatura", "Literature"}, @@ -1828,6 +1926,7 @@ void refresh_stats_ui(void); void net_lessons_apply_lang(void); void hw_lessons_apply_lang(void); void on_lessons_apply_lang(void); +void on2_lessons_apply_lang(void); void net_rail_theme_reset(void); void apply_language(void) { @@ -1843,5 +1942,6 @@ void apply_language(void) { net_lessons_apply_lang(); hw_lessons_apply_lang(); on_lessons_apply_lang(); + on2_lessons_apply_lang(); search_apply_lang(); } diff --git a/src/main.c b/src/main.c index e60a5a0..57d45f5 100644 --- a/src/main.c +++ b/src/main.c @@ -385,7 +385,7 @@ static gboolean shot_later(gpointer data) { static gboolean run_smoke_test(gpointer data) { static const char *pages[] = { "welcome", "subjects", "roadmap", "stats", - "netyears", "netmap", "hwyears", "hwmap", "onyears", "onmap", + "netyears", "netmap", "hwyears", "hwmap", "onyears", "onmap", "on2map", "czechmap", "mluvnice", "readinglist", "cetba1984", "cetba1984quiz", "cetba1984dej", @@ -398,6 +398,7 @@ static gboolean run_smoke_test(gpointer data) { "hwunit20", "hwex20", "hwunit25", "hwex25", "hwunit29", "hwex29", "onunit1", "onex1", "onunit17", "onex17", + "on2unit1", "on2ex1", "on2unit16", "on2ex16", "unit1", "unit2", "unit3", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1vocab", "u2vocab", "u3vocab", @@ -511,6 +512,8 @@ void activate(GtkApplication *app, gpointer user_data) { gtk_stack_add_named(main_stack, build_onyears_page(), "onyears"); gtk_stack_add_named(main_stack, build_onmap_page(), "onmap"); add_on_pages(main_stack); + gtk_stack_add_named(main_stack, build_on2map_page(), "on2map"); + add_on2_pages(main_stack); gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap"); gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice"); for (int n = 1; n <= 20; n++) { diff --git a/src/obcanka.c b/src/obcanka.c index f324cf2..10037e2 100644 --- a/src/obcanka.c +++ b/src/obcanka.c @@ -3,7 +3,8 @@ /* Year 1 of Občanská nauka. * 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 - * place in the first year. Later years stay locked. */ + * place in the first year. Year 2 is the citizen and the state; years 3 and 4 + * stay locked. */ #define ON_ENTRY(n) \ { .n_slides = 2, .unit_page = "onunit" #n, .ex_page = "onex" #n } @@ -339,10 +340,11 @@ static void on_add_node(GtkFixed *fixed, int index) { gtk_fixed_put(fixed, name, 0, 0); } -static GtkWidget *on_year_button(int year, gboolean locked) { +static GtkWidget *on_year_button(int year, const char *target) { static const char *year_keys[] = { "on_year1", "on_year2", "on_year3", "on_year4", }; + gboolean locked = target == NULL; GtkWidget *btn; GtkWidget *row; GtkWidget *num; @@ -399,7 +401,7 @@ static GtkWidget *on_year_button(int year, gboolean locked) { gtk_box_append(GTK_BOX(row), icon); } else { g_object_set_data_full(G_OBJECT(btn), "target", - g_strdup("onmap"), g_free); + g_strdup(target), g_free); g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); } @@ -411,6 +413,7 @@ GtkWidget *build_onyears_page(void) { GtkWidget *scroll; GtkWidget *list; int y; + static const char *targets[] = {"onmap", "on2map", NULL, NULL}; page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_hexpand(page, TRUE); @@ -437,7 +440,7 @@ GtkWidget *build_onyears_page(void) { gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); for (y = 1; y <= 4; y++) - gtk_box_append(GTK_BOX(list), on_year_button(y, y > 1)); + gtk_box_append(GTK_BOX(list), on_year_button(y, targets[y - 1])); return page; } diff --git a/src/obcanka2.c b/src/obcanka2.c new file mode 100644 index 0000000..f0630a3 --- /dev/null +++ b/src/obcanka2.c @@ -0,0 +1,1911 @@ +#include "graduately.h" + +/* Year 2 of Občanská nauka. + * 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 + * schools place in the second year. Law, the economy and philosophy stay + * in later years. */ + +#define ON2_ENTRY(n) \ + { .n_slides = 2, .unit_page = "on2unit" #n, .ex_page = "on2ex" #n } + +NetLesson on2_lessons[ON2_LESSONS] = { + ON2_ENTRY(1), + ON2_ENTRY(2), + ON2_ENTRY(3), + ON2_ENTRY(4), + ON2_ENTRY(5), + ON2_ENTRY(6), + ON2_ENTRY(7), + ON2_ENTRY(8), + ON2_ENTRY(9), + ON2_ENTRY(10), + ON2_ENTRY(11), + ON2_ENTRY(12), + ON2_ENTRY(13), + ON2_ENTRY(14), + ON2_ENTRY(15), + ON2_ENTRY(16), +}; + +#undef ON2_ENTRY + +GtkWidget *on2_scroll; +GtkWidget *on2_fixed; +GtkWidget *on2_rail; +GtkWidget *on2_nodes[ON2_UNITS]; +GtkWidget *on2_labels[ON2_UNITS]; +double on2_cx[ON2_UNITS]; +double on2_cy[ON2_UNITS]; +int on2_cols = 1; +int on2_rows = 1; +int on2_cw = (int)(2.0 * ROAD_MX + (ON2_UNITS - 1) * PATH_SPAC); +int on2_ch = (int)(2.0 * ROAD_MY); +guint on2_idle; +double on2_last_avail = -1.0; +int on2_last_cols = -1; +int on2_last_rows = -1; +int on2_last_cw = -1; +int on2_last_ch = -1; +cairo_surface_t *on2_rail_cache; +int on2_cache_w; +int on2_cache_h; + +static void on2_point(double t, double *ox, double *oy) { + int k = (int)t; + double u = t - (double)k; + double u2, u3; + double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y; + + if (k < 0) { k = 0; u = 0.0; } + if (k >= ON2_UNITS - 1) { k = ON2_UNITS - 2; u = 1.0; } + + p1x = on2_cx[k]; p1y = on2_cy[k]; + p2x = on2_cx[k + 1]; p2y = on2_cy[k + 1]; + if (k - 1 >= 0) { p0x = on2_cx[k - 1]; p0y = on2_cy[k - 1]; } + else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); } + if (k + 2 < ON2_UNITS) { p3x = on2_cx[k + 2]; p3y = on2_cy[k + 2]; } + else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); } + + u2 = u * u; + u3 = u2 * u; + *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u + + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2 + + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3); + *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u + + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2 + + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3); +} + +static void on2_path(cairo_t *cr, double t0, double t1) { + const double steps_per_unit = 24.0; + int n = (int)((t1 - t0) * steps_per_unit); + double x, y; + int s; + + if (n < 1) + n = 1; + on2_point(t0, &x, &y); + cairo_move_to(cr, x, y); + for (s = 1; s <= n; s++) { + on2_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y); + cairo_line_to(cr, x, y); + } +} + +static void on2_geometry(double avail) { + const double phase = 2.0 * G_PI * 1.7 / (double)(ON2_UNITS - 1); + int rows; + int r, c, i; + + for (rows = 1; rows <= ON2_UNITS; rows++) { + int cols = (ON2_UNITS + rows - 1) / rows; + if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0) + break; + } + if (rows > ON2_UNITS) + rows = ON2_UNITS; + on2_rows = rows; + on2_cols = (ON2_UNITS + rows - 1) / rows; + if (on2_cols < 1) + on2_cols = 1; + on2_cw = (int)(2.0 * ROAD_MX + (on2_cols - 1) * PATH_SPAC); + on2_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP); + + for (r = 0; r < rows; r++) { + int base = r * on2_cols; + int len = MIN(on2_cols, ON2_UNITS - base); + int fwd = (r % 2) == 0; + for (c = 0; c < len; c++) { + i = base + c; + int cc = fwd ? c : (on2_cols - 1 - c); + on2_cx[i] = ROAD_MX + cc * PATH_SPAC; + on2_cy[i] = ROAD_MY + r * ROAD_GAP + + ROAD_WAVE * sin((double)i * phase); + } + } +} + +static void on2_apply_layout(void) { + if (!on2_fixed) + return; + + if (on2_cols == on2_last_cols && on2_rows == on2_last_rows && + on2_cw == on2_last_cw && on2_ch == on2_last_ch) + return; + + on2_last_cols = on2_cols; + on2_last_rows = on2_rows; + on2_last_cw = on2_cw; + on2_last_ch = on2_ch; + if (on2_rail_cache) { + cairo_surface_destroy(on2_rail_cache); + on2_rail_cache = NULL; + } + + for (int i = 0; i < ON2_UNITS; i++) { + if (!on2_nodes[i] || !on2_labels[i]) + continue; + gtk_fixed_move(GTK_FIXED(on2_fixed), on2_nodes[i], + (int)(on2_cx[i] - NODE_SIZE / 2.0), + (int)(on2_cy[i] - NODE_SIZE / 2.0)); + gtk_fixed_move(GTK_FIXED(on2_fixed), on2_labels[i], + (int)(on2_cx[i] - (PATH_SPAC - 20.0) / 2.0), + (int)(on2_cy[i] + NODE_SIZE / 2.0 + 10.0)); + } + + gtk_widget_set_size_request(on2_fixed, on2_cw, on2_ch); + gtk_widget_set_size_request(on2_rail, on2_cw, on2_ch); + gtk_fixed_move(GTK_FIXED(on2_fixed), on2_rail, 0, 0); + gtk_widget_queue_draw(on2_rail); +} + +static void on2_relayout(void) { + GtkAdjustment *hadj; + double avail; + + if (!on2_scroll) + return; + hadj = gtk_scrolled_window_get_hadjustment( + GTK_SCROLLED_WINDOW(on2_scroll)); + avail = gtk_adjustment_get_page_size(hadj); + if (avail < 1.0) + return; + if (fabs(avail - on2_last_avail) < 1.0) + return; + on2_last_avail = avail; + on2_geometry(avail); + on2_apply_layout(); +} + +static gboolean on2_relayout_idle(gpointer data) { + (void)data; + on2_idle = 0; + on2_relayout(); + return G_SOURCE_REMOVE; +} + +static void on2_relayout_later(void) { + if (on2_idle == 0) + on2_idle = g_idle_add(on2_relayout_idle, NULL); +} + +static void on2_adjust_notify(GtkAdjustment *adj, GParamSpec *ps, + gpointer data) { + (void)adj; + (void)ps; + (void)data; + on2_relayout_later(); +} + +static void on2_render_rail_to(cairo_t *cr) { + const double t_end = (double)(ON2_UNITS - 1); + const Rgb rail = color_from_hex(app_theme.rail); + const Rgb muted = color_from_hex(app_theme.subtext); + + cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + + cairo_new_path(cr); + on2_path(cr, 0.0, t_end); + cairo_set_line_width(cr, 12); + cairo_set_source_rgb(cr, rail.r, rail.g, rail.b); + cairo_stroke(cr); + + cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17); + cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0); + cairo_new_path(cr); + on2_path(cr, 0.0, t_end); + cairo_stroke(cr); + cairo_set_dash(cr, NULL, 0, 0.0); +} + +static void on2_draw_rail(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + (void)area; + (void)width; + (void)height; + (void)data; + + if (!on2_rail_cache || on2_cache_w != on2_cw || on2_cache_h != on2_ch) { + cairo_t *rcr; + + if (on2_rail_cache) + cairo_surface_destroy(on2_rail_cache); + on2_cache_w = on2_cw; + on2_cache_h = on2_ch; + if (on2_cache_w < 1) + on2_cache_w = 1; + if (on2_cache_h < 1) + on2_cache_h = 1; + on2_rail_cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + on2_cache_w, on2_cache_h); + rcr = cairo_create(on2_rail_cache); + on2_render_rail_to(rcr); + cairo_destroy(rcr); + } + + cairo_set_source_surface(cr, on2_rail_cache, 0, 0); + cairo_paint(cr); +} + +void on2_rail_theme_reset(void) { + if (on2_rail_cache) { + cairo_surface_destroy(on2_rail_cache); + on2_rail_cache = NULL; + } + if (on2_rail) + gtk_widget_queue_draw(on2_rail); +} + +static void on2_open_unit(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + L->idx = 0; + if (L->stack) { + char name[16]; + + g_snprintf(name, sizeof(name), "on2_slide%u", L->idx); + gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name); + if (L->prev_btn) + gtk_widget_set_sensitive(L->prev_btn, FALSE); + if (L->next_btn) + gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next")); + } + gtk_stack_set_visible_child_name(main_stack, L->unit_page); +} + +static void on2_add_node(GtkFixed *fixed, int index) { + int num = index + 1; + gboolean locked = index >= ON2_LESSONS; + GtkWidget *card; + GtkWidget *vbox; + GtkWidget *number; + GtkWidget *icon; + GtkWidget *name; + char *text; + static char unit_keys[ON2_LESSONS][16]; + static gboolean unit_keys_ready = FALSE; + + if (!unit_keys_ready) { + for (int i = 0; i < ON2_LESSONS; i++) + g_snprintf(unit_keys[i], sizeof unit_keys[i], "on2_unit%d", i + 1); + unit_keys_ready = TRUE; + } + + card = gtk_button_new(); + gtk_widget_add_css_class(card, "unit-node"); + gtk_widget_set_can_focus(card, FALSE); + gtk_widget_set_sensitive(card, !locked); + gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE); + + if (locked) + gtk_widget_add_css_class(card, "locked"); + else + gtk_widget_add_css_class(card, "current"); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER); + gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER); + gtk_button_set_child(GTK_BUTTON(card), vbox); + + text = g_strdup_printf("%d", num); + number = gtk_label_new(text); + g_free(text); + gtk_widget_add_css_class(number, "unit-number"); + gtk_box_append(GTK_BOX(vbox), number); + + if (locked) { + icon = icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 16); + gtk_box_append(GTK_BOX(vbox), icon); + } else { + icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18); + gtk_widget_set_visible(icon, FALSE); + gtk_box_append(GTK_BOX(vbox), icon); + on2_lessons[index].done_icon = icon; + g_signal_connect(card, "clicked", G_CALLBACK(on2_open_unit), + &on2_lessons[index]); + } + + on2_nodes[index] = card; + gtk_fixed_put(fixed, card, 0, 0); + + name = gtk_label_new(NULL); + gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1); + gtk_widget_set_halign(name, GTK_ALIGN_CENTER); + gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER); + gtk_label_set_wrap(GTK_LABEL(name), TRUE); + gtk_widget_add_css_class(name, "unit-name"); + if (index < ON2_LESSONS) { + i18n_bind(name, unit_keys[index], 0); + } else { + gtk_widget_add_css_class(name, "unit-name-locked"); + } + on2_labels[index] = name; + gtk_fixed_put(fixed, name, 0, 0); +} +GtkWidget *build_on2map_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *wrap; + GtkWidget *fixed; + GtkWidget *rail; + GtkAdjustment *ha; + GtkAdjustment *va; + + on2_geometry(1000.0); + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + 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("onyears", "on_year2", "on2_sub")); + + scroll = gtk_scrolled_window_new(); + 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, 14); + gtk_box_append(GTK_BOX(page), scroll); + on2_scroll = scroll; + + wrap = gtk_center_box_new(); + gtk_widget_set_vexpand(wrap, TRUE); + gtk_widget_set_hexpand(wrap, TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap); + + fixed = gtk_fixed_new(); + gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER); + gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(fixed, on2_cw, on2_ch); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); + on2_fixed = fixed; + + rail = gtk_drawing_area_new(); + gtk_widget_set_size_request(rail, on2_cw, on2_ch); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), on2_draw_rail, + NULL, NULL); + gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0); + on2_rail = rail; + + for (int i = 0; i < ON2_UNITS; i++) + on2_add_node(GTK_FIXED(fixed), i); + + ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll)); + va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll)); + g_signal_connect(ha, "notify::page-size", + G_CALLBACK(on2_adjust_notify), NULL); + g_signal_connect(va, "notify::page-size", + G_CALLBACK(on2_adjust_notify), NULL); + + on2_apply_layout(); + on2_relayout_later(); + refresh_on2_completion_ui(); + + return page; +} + +static void on2_slide_apply(NetLesson *L); +static void on2_slide_prev(GtkButton *button, gpointer data); +static void on2_slide_next(GtkButton *button, gpointer data); + +static int on2_note_last_w = -1; + +static void on2_rescale_notes_at(int w) { + int body, head, kick; + + if (w < 160) + w = 160; + body = w / 46; + if (body < 14) body = 14; + if (body > 26) body = 26; + head = body + 14; + if (head > 46) head = 46; + kick = body - 3; + if (kick < 11) kick = 11; + for (int i = 0; i < ON2_LESSONS; i++) + net_rescale_lesson_notes(&on2_lessons[i], body, head, kick); +} + +static gboolean on2_note_tick(GtkWidget *w, GdkFrameClock *clock, + gpointer data) { + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + (void)data; + if (width != on2_note_last_w) { + on2_note_last_w = width; + on2_rescale_notes_at(width); + } + return G_SOURCE_CONTINUE; +} + +static GtkWidget *build_on2_unit_page(NetLesson *L, const char *title_key, + const char *sub_key, + const NetSlide *slides, guint n_slides) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *nav; + + net_lesson_notes_ensure(L); + net_notes_target = L; + L->n_slides = n_slides; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + 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("on2map", title_key, sub_key)); + + scroll = gtk_scrolled_window_new(); + 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 (guint s = 0; s < n_slides; s++) { + GtkWidget *stage = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + GtkWidget *card; + char name[16]; + + gtk_widget_set_vexpand(stage, TRUE); + g_snprintf(name, sizeof(name), "on2_slide%u", s); + gtk_stack_add_named(GTK_STACK(L->stack), stage, name); + + card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16); + gtk_widget_add_css_class(card, "notes-card"); + gtk_widget_set_valign(card, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(stage), card); + + 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_widget_add_tick_callback(L->stack, on2_note_tick, NULL, NULL); + on2_note_last_w = -1; + on2_rescale_notes_at(0); + net_notes_target = NULL; + + 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"); + g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(on2_slide_prev), L); + gtk_box_append(GTK_BOX(nav), L->prev_btn); + + L->next_btn = gtk_button_new_with_label(tr("net_slide_next")); + gtk_widget_add_css_class(L->next_btn, "btn-primary"); + g_signal_connect(L->next_btn, "clicked", G_CALLBACK(on2_slide_next), L); + gtk_box_append(GTK_BOX(nav), L->next_btn); + + L->idx = 0; + on2_slide_apply(L); + return page; +} + +static void on2_slide_apply(NetLesson *L) { + char name[16]; + + if (!L || !L->stack) + return; + g_snprintf(name, sizeof(name), "on2_slide%u", L->idx); + gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name); + if (L->prev_btn) { + gtk_button_set_label(GTK_BUTTON(L->prev_btn), tr("net_slide_prev")); + gtk_widget_set_sensitive(L->prev_btn, L->idx > 0); + } + if (L->next_btn) { + gtk_button_set_label( + GTK_BUTTON(L->next_btn), + L->idx + 1 >= L->n_slides ? tr("net_slide_start") + : tr("net_slide_next")); + } +} + +static void on2_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + (void)button; + if (!L) return; + if (L->idx > 0) { + L->idx--; + on2_slide_apply(L); + } +} + +static void on2_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + (void)button; + if (!L) return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + on2_slide_apply(L); + } else { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +void on2_lessons_apply_lang(void) { + for (int i = 0; i < ON2_LESSONS; i++) + on2_slide_apply(&on2_lessons[i]); +} + +static void on2_mcq_check(GtkButton *button, gpointer data) { + NetMcqCtx *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++; + 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")); + mark_on2_done(ctx->lesson_id); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +static GtkWidget *build_on2_mcq_page(int lesson_id, const char *back_page, + const char *title_key, + const char *heading_key, + const ChoiceQ *qs, const char **hints, + int n) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *body; + GtkWidget *check; + GtkWidget *btns; + GtkWidget *head; + GtkWidget *intro; + NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); + int n_opts = qs[0].n_options; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + + gtk_box_append(GTK_BOX(page), top_bar(back_page, title_key, NULL)); + + scroll = gtk_scrolled_window_new(); + 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); + + body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14); + gtk_widget_set_halign(body, GTK_ALIGN_FILL); + gtk_widget_set_hexpand(body, TRUE); + gtk_widget_set_margin_start(body, 4); + gtk_widget_set_margin_end(body, 4); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); + + head = gtk_label_new(NULL); + i18n_bind(head, heading_key, 0); + gtk_widget_set_halign(head, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(head), TRUE); + gtk_widget_set_margin_top(head, 4); + gtk_widget_add_css_class(head, "quiz-heading"); + gtk_box_append(GTK_BOX(body), head); + + intro = gtk_label_new(NULL); + 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_widget_set_margin_bottom(intro, 4); + gtk_box_append(GTK_BOX(body), intro); + + ctx->qs = qs; + ctx->n = n; + ctx->n_opts = n_opts; + ctx->lesson_id = lesson_id; + 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[i]); + } + + btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_top(btns, 8); + gtk_widget_set_margin_bottom(btns, 8); + gtk_box_append(GTK_BOX(body), btns); + + 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_box_append(GTK_BOX(btns), check); + g_signal_connect(check, "clicked", G_CALLBACK(on2_mcq_check), ctx); + + ctx->feedback = gtk_label_new(""); + gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), ctx->feedback); + + return page; +} +static GtkWidget *build_on2_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Stát", "Co dělá stát státem", + "Tip: bydliště ještě není občanství.", + { + "Stát je organizace veřejné moci na určitém území a nad lidmi, kteří tam žijí.", + "Má území, obyvatelstvo, veřejnou moc a právní řád.", + "Občan má ke státu trvalý právní svazek, kterému se říká státní občanství.", + "Cizinec může na území žít, občanem se tím sám nestává.", + NULL, + }, + }, + { + "2 / 2 • Podíl", "Práva a povinnosti občana", + "Tip: demokratický stát má občanovi sloužit.", + { + "Z občanství plynou práva, třeba volit do Parlamentu, a také povinnosti.", + "Povinností je dodržovat zákony, platit daně a respektovat práva druhých.", + "Moc ve státě má být kontrolovatelná, ne neomezená.", + "Občan se podílí volbami, kontrolou moci a slušným jednáním.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[0], "on2_unit1", "on2_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je stát?", + { + "Jen mapa a hymna", + "Organizace veřejné moci na území s obyvatelstvem a právním řádem", + "Každá skupina lidí", + "Pouze vláda", + }, + 4, 1}, + {"Čím se občan liší od cizince?", + { + "Má trvalý právní svazek se státem", + "Bydlí v hlavním městě", + "Mluví úředním jazykem", + "Platí jízdné", + }, + 4, 0}, + {"Co z občanství plyne?", + { + "Jen povinnost volit", + "Jen právo na pas", + "Práva i povinnosti", + "Nic, dokud člověk nepracuje", + }, + 4, 2}, + {"Jak se občan na demokratickém státě podílí?", + { + "Jen poslušností bez ptaní", + "Volbami, kontrolou moci a dodržováním zákonů", + "Tím, že moc nechá bez dozoru", + "Jen placením pokut", + }, + 4, 1}, + }; + static const char *hints[] = { + "Stát má území, obyvatele, moc a právo", + "Občanství je právní svazek, ne jen pobyt", + "Občan má práva a také povinnosti", + "Podíl je volba, kontrola i respekt k právu", + }; + return build_on2_mcq_page(0, "on2unit1", "on2_ex1_title", + "on2_quiz1_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Nabytí", "Jak se české občanství získává", + "Tip: narození na území ČR samo o sobě občanství nedává.", + { + "Dítě ho nabývá narozením, je-li alespoň jeden rodič občanem České republiky.", + "Dále ho lze nabýt určením otcovství, osvojením nebo nalezením dítěte, jehož totožnost se nezjistí.", + "Dospělý cizinec ho může získat udělením, v některých případech prohlášením.", + "Podrobnosti určuje zákon o státním občanství.", + NULL, + }, + }, + { + "2 / 2 • Udělení", "Když o občanství žádá cizinec", + "Tip: na udělení není právní nárok.", + { + "Udělení je rozhodnutí státu, ne automatický nárok žadatele.", + "Žadatel musí splnit podmínky, mimo jiné pobyt, bezúhonnost a znalost češtiny.", + "Patří sem i základní znalost ústavního systému a plnění povinností vůči státu.", + "Občan České republiky může mít i jiné občanství, dovoluje-li to zákon.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[1], "on2_unit2", "on2_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdy dítě nabývá české občanství narozením?", + { + "Když se narodí v Praze", + "Když je alespoň jeden rodič občanem ČR", + "Když chodí do české školy", + "Když mu je osmnáct", + }, + 4, 1}, + {"Dává samotné narození v ČR občanství?", + { + "Ano, každému", + "Ne, kromě zvláštních případů, třeba nalezeného dítěte", + "Ano, pokud rodiče platí daň", + "Ano, po roce pobytu", + }, + 4, 1}, + {"Je na udělení občanství právní nárok?", + { + "Ano, po týdnu pobytu", + "Ne, stát ho uděluje po splnění podmínek", + "Ano, každému obyvateli EU", + "Ano, kdo složí maturitu", + }, + 4, 1}, + {"Co se při udělení obvykle požaduje?", + { + "Jen fotografii", + "Pobyt, bezúhonnost a znalost češtiny", + "Vlastnictví bytu", + "Členství v politické straně", + }, + 4, 1}, + }; + static const char *hints[] = { + "Rozhoduje občanství rodiče, ne samo místo narození", + "Platí původ po rodičích, ne místo narození", + "Udělení je rozhodnutí státu", + "Podmínky chrání, aby šlo o skutečný vztah ke státu", + }; + return build_on2_mcq_page(1, "on2unit2", "on2_ex2_title", + "on2_quiz2_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Výčet", "Které symboly Česká republika má", + "Tip: hymnou je oficiálně jen první sloka.", + { + "Zákon uvádí velký a malý státní znak, státní barvy, vlajku a vlajku prezidenta.", + "Patří k nim také státní pečeť a státní hymna.", + "Státní barvy jsou bílá, červená a modrá.", + "Hymnou je první sloka písně Kde domov můj.", + NULL, + }, + }, + { + "2 / 2 • Úcta", "K čemu symboly jsou", + "Tip: velký znak spojuje Čechy, Moravu a Slezsko.", + { + "Symboly představují stát a lidé jim mají prokazovat úctu.", + "Velký znak nese českého lva a moravskou a slezskou orlici.", + "Vlajka je bílá a červená s modrým klínem u žerdi.", + "Užívají se podle zákona, ne jako libovolná ozdoba.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[2], "on2_unit3", "on2_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Která píseň je státní hymna?", + { + "První sloka Kde domov můj", + "Celá píseň včetně druhé sloky jako povinný text", + "Hej, Slované", + "Ódu na radost", + }, + 4, 0}, + {"Jaké jsou státní barvy?", + { + "Červená, bílá a zelená", + "Bílá, červená a modrá", + "Modrá a žlutá", + "Černá a zlatá", + }, + 4, 1}, + {"Co spojuje velký státní znak?", + { + "Jen Prahu a Brno", + "Znaky Čech, Moravy a Slezska", + "Vlajky sousedních států", + "Znak Evropské unie", + }, + 4, 1}, + {"Patří státní pečeť mezi státní symboly?", + { + "Ne, je to jen razítko úřadu", + "Ano", + "Jen když ji použije obec", + "Ne, zrušila ji ústava", + }, + 4, 1}, + }; + static const char *hints[] = { + "Zákon stanoví první sloku", + "Bílá, červená a modrá", + "Lev a dvě orlice", + "Pečeť je v zákonném výčtu symbolů", + }; + return build_on2_mcq_page(2, "on2unit3", "on2_ex3_title", + "on2_quiz3_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Pořádek", "Nejvyšší pravidla státu", + "Tip: Listina je součást ústavního pořádku, ne obyčejný zákon.", + { + "Ústava je základní zákon a běžný zákon jí nesmí odporovat.", + "Ústavní pořádek tvoří Ústava, Listina základních práv a svobod a další ústavní zákony.", + "Ústava České republiky platí od 1. ledna 1993.", + "Popisuje území, moc a základní pravidla politického systému.", + NULL, + }, + }, + { + "2 / 2 • Změna", "Proč se ústava mění obtížně", + "Tip: rigidní ústava chrání pravidla před chvilkovou většinou.", + { + "Ústava se mění obtížněji než obyčejný zákon.", + "Ústavní zákon potřebuje tři pětiny všech poslanců.", + "V Senátu potřebuje tři pětiny přítomných senátorů.", + "Listina vedle ústavy chrání práva člověka a občana.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[3], "on2_unit4", "on2_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je ústava?", + { + "Volební program vlády", + "Základní zákon, kterému nesmí odporovat běžné zákony", + "Jednací řád školy", + "Rozpočet na jeden rok", + }, + 4, 1}, + {"Je Listina základních práv a svobod součást ústavního pořádku?", + { + "Ne, je to doporučení", + "Ano", + "Jen pro cizince", + "Jen do roku 1993", + }, + 4, 1}, + {"Jaká většina je potřeba k ústavnímu zákonu?", + { + "Nadpoloviční většina přítomných poslanců", + "Tři pětiny všech poslanců a tři pětiny přítomných senátorů", + "Souhlas prezidenta stačí", + "Jednomyslnost vlády", + }, + 4, 1}, + {"Od kdy ústava České republiky platí?", + { + "Od roku 1918", + "Od 1. ledna 1993", + "Od vstupu do Evropské unie", + "Od roku 1960", + }, + 4, 1}, + }; + static const char *hints[] = { + "Ústava stojí nad obyčejnými zákony", + "Listina má sílu ústavního zákona", + "Ústava je rigidní", + "Platí od vzniku samostatné České republiky", + }; + return build_on2_mcq_page(3, "on2unit4", "on2_ex4_title", + "on2_quiz4_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Složky", "Tři moci vedle sebe", + "Tip: soudce nemá být podřízený ministrovi.", + { + "Moc se dělí, aby ji jeden orgán nemohl užívat bez kontroly.", + "Zákonodárná moc dává zákony a v České republice ji má Parlament.", + "Výkonná moc zákony provádí: patří k ní vláda a prezident.", + "Soudní moc rozhoduje spory a je na ostatních složkách nezávislá.", + NULL, + }, + }, + { + "2 / 2 • Rovnováha", "Brzdy mezi složkami", + "Tip: kontrola moci je součást demokracie, ne překážka.", + { + "Složky se navzájem omezují, tomu se říká brzdy a rovnováhy.", + "Prezident může zákon vrátit a Sněmovna může vyslovit vládě nedůvěru.", + "Ústavní soud hlídá, aby právo drželo ústavní pořádek.", + "Nejvyšší kontrolní úřad kontroluje hospodaření se státním majetkem a Česká národní banka pečuje o cenovou stabilitu.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[4], "on2_unit5", "on2_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč se státní moc dělí?", + { + "Aby bylo více ministerstev", + "Aby ji nikdo neužíval bez kontroly", + "Aby soudil prezident", + "Aby zákony psala armáda", + }, + 4, 1}, + {"Kdo vykonává zákonodárnou moc?", + { + "Vláda", + "Parlament", + "Ústavní soud", + "Hejtmani", + }, + 4, 1}, + {"Kdo patří k výkonné moci?", + { + "Jen Senát", + "Vláda a prezident", + "Jen obecní zastupitelstva", + "Jen advokáti", + }, + 4, 1}, + {"Co dělá Ústavní soud?", + { + "Schvaluje státní rozpočet", + "Hlídá soulad s ústavním pořádkem", + "Řídí policii", + "Jmenuje starosty", + }, + 4, 1}, + }; + static const char *hints[] = { + "Dělba brání soustředění moci", + "Zákony dává Parlament", + "Výkonná moc zákony provádí", + "Ústavní soud není další komora Parlamentu", + }; + return build_on2_mcq_page(4, "on2unit5", "on2_ex5_title", + "on2_quiz5_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Komory", "Sněmovna a Senát", + "Tip: senátor má delší mandát než poslanec.", + { + "Parlament tvoří Poslanecká sněmovna a Senát.", + "Sněmovna má 200 poslanců volených na čtyři roky.", + "Senát má 81 senátorů volených na šest let a každé dva roky se obmění třetina.", + "Poslancem lze být od 21 let, senátorem od 40 let.", + NULL, + }, + }, + { + "2 / 2 • Zákon", "Jak vzniká obyčejný zákon", + "Tip: kraj smí podat návrh zákona.", + { + "Návrh může podat poslanec, Senát, vláda nebo zastupitelstvo kraje.", + "Návrh schvaluje Poslanecká sněmovna.", + "Senát ho může vrátit nebo zamítnout, Sněmovna může senátní nesouhlas přehlasovat.", + "Prezident zákon podepíše, nebo ho vrátí; i jeho veto může Sněmovna přehlasovat.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[5], "on2_unit6", "on2_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kolik poslanců má Poslanecká sněmovna?", + { + "81", + "200", + "150", + "26", + }, + 4, 1}, + {"Jak dlouhý je mandát senátora?", + { + "Dva roky", + "Čtyři roky", + "Šest let", + "Do odvolání vládou", + }, + 4, 2}, + {"Od kolika let lze být zvolen poslancem?", + { + "Od 18 let", + "Od 21 let", + "Od 40 let", + "Od 35 let", + }, + 4, 1}, + {"Kdo smí podat návrh zákona?", + { + "Jen prezident", + "Poslanec, Senát, vláda nebo kraj", + "Jen Ústavní soud", + "Každý občan přímo do Sbírky", + }, + 4, 1}, + }; + static const char *hints[] = { + "Sněmovna má dvě stě poslanců", + "Senát se obměňuje po třetinách", + "Do Sněmovny od 21, do Senátu od 40", + "Návrh zákona je v ústavě vyhrazen určeným subjektům", + }; + return build_on2_mcq_page(5, "on2unit6", "on2_ex6_title", + "on2_quiz6_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Hlava státu", "Prezident republiky", + "Tip: prezident se volí přímo.", + { + "Prezident je hlava státu a volí se přímo na pět let.", + "Stejný člověk může úřad vykonávat nejvýše dvě období po sobě.", + "Kandidát musí mít nejméně 40 let.", + "Zastupuje stát navenek, jmenuje vládu, je vrchním velitelem ozbrojených sil a může udělit milost.", + NULL, + }, + }, + { + "2 / 2 • Vláda", "Vrchol výkonné moci", + "Tip: vládě předsedá premiér, ne prezident.", + { + "Vládu tvoří předseda, místopředsedové a ministři.", + "Je vrcholným orgánem výkonné moci.", + "Odpovídá Poslanecké sněmovně, která jí může vyslovit nedůvěru.", + "Po jmenování žádá Sněmovnu o důvěru a řídí ministerstva.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[6], "on2_unit7", "on2_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jak se volí prezident republiky?", + { + "Parlamentem na doživotí", + "Přímo občany, na pět let", + "Vládou na dva roky", + "Senátem na deset let", + }, + 4, 1}, + {"Kolikrát za sebou může být člověk prezidentem?", + { + "Jen jednou", + "Nejvýše dvakrát", + "Bez omezení", + "Třikrát", + }, + 4, 1}, + {"Komu je odpovědná vláda?", + { + "Senátu", + "Poslanecké sněmovně", + "Ústavnímu soudu", + "Hejtmanům", + }, + 4, 1}, + {"Kdo vládě předsedá?", + { + "Prezident", + "Předseda vlády", + "Předseda Senátu", + "Guvernér České národní banky", + }, + 4, 1}, + }; + static const char *hints[] = { + "Přímá volba od roku 2013, mandát pět let", + "Ústava zakazuje třetí období v řadě", + "Sněmovna může vyslovit nedůvěru", + "Premiér není totéž co hlava státu", + }; + return build_on2_mcq_page(6, "on2unit7", "on2_ex7_title", + "on2_quiz7_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Správa", "Stát a územní celky", + "Tip: samospráva není totéž co ministerstvo.", + { + "Veřejná správa je správa veřejných záležitostí.", + "Státní správu vykonávají jménem státu třeba ministerstva a další úřady.", + "Samospráva patří obcím a krajům: rozhodují o svých věcech.", + "Obec a kraj mají vlastní působnost a plní i úkoly, které jim svěří stát.", + NULL, + }, + }, + { + "2 / 2 • Obec a kraj", "Kdo stojí v čele", + "Tip: hejtman stojí v čele kraje, starosta v čele obce.", + { + "V obci je volené zastupitelstvo, rada a starosta.", + "V kraji je zastupitelstvo, rada a hejtman.", + "Zastupitelstvo je nejvyšší orgán územní samosprávy.", + "Praha je hlavní město a má postavení obce i kraje.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[7], "on2_unit8", "on2_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je územní samospráva?", + { + "Řízení ministerstva z Prahy", + "Rozhodování obce a kraje o vlastních záležitostech", + "Jen práce policie", + "Jen soudní řízení", + }, + 4, 1}, + {"Kdo stojí v čele obce?", + { + "Hejtman", + "Starosta", + "Ministr vnitra", + "Senátor za okres", + }, + 4, 1}, + {"Kdo stojí v čele kraje?", + { + "Starosta", + "Hejtman", + "Prezident", + "Ředitel školy", + }, + 4, 1}, + {"Jaké postavení má Praha?", + { + "Je jen městská část", + "Je obec a zároveň kraj", + "Není samospráva", + "Řídí ji přímo vláda bez zastupitelstva", + }, + 4, 1}, + }; + static const char *hints[] = { + "Samospráva patří obci a kraji", + "Obec vede starosta", + "Kraj vede hejtman", + "Hlavní město je obec i kraj", + }; + return build_on2_mcq_page(7, "on2unit8", "on2_ex8_title", + "on2_quiz8_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit9_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Hodnoty", "Od čeho se demokracie pozná", + "Tip: většina nesmí menšině vzít základní práva.", + { + "V demokracii veřejná moc pochází od lidu.", + "Patří k ní svobodné volby, vláda většiny a ochrana menšiny.", + "Dál právní stát, dělba moci a soutěž více politických stran.", + "Svoboda člověka jde ruku v ruce s odpovědností vůči druhým.", + NULL, + }, + }, + { + "2 / 2 • Ohrožení", "Co demokracii podrývá", + "Tip: korupce je zneužití postavení k soukromému prospěchu.", + { + "Škodí jí korupce, násilí a neodpovědnost.", + "Škodí i snaha zrušit kontrolu moci a pravidla soutěže.", + "Demokracie není vláda bez pravidel.", + "Občan ji chrání účastí, ne lhostejností.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[8], "on2_unit9", "on2_unit9_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit9_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Od koho pochází veřejná moc v demokracii?", + { + "Od armády", + "Od lidu", + "Od nejbohatších firem", + "Od jednoho vůdce", + }, + 4, 1}, + {"Chrání demokracie jen vítěznou většinu?", + { + "Ano, menšina nemá práva", + "Ne, chrání i základní práva menšiny", + "Menšinu chrání jen v obci", + "Práva platí jen ve volební den", + }, + 4, 1}, + {"Co je korupce?", + { + "Svobodná volba", + "Zneužití postavení k soukromému prospěchu", + "Kritika vlády", + "Práce zastupitelstva", + }, + 4, 1}, + {"Co k demokracii patří?", + { + "Jedna povolená strana", + "Svobodné volby a právní stát", + "Moc bez soudů", + "Zákaz menšinových názorů", + }, + 4, 1}, + }; + static const char *hints[] = { + "Lid je zdroj moci", + "Vláda většiny má mez v právech menšiny", + "Korupce demokracii ohrožuje", + "Volby a právo drží demokracii pohromadě", + }; + return build_on2_mcq_page(8, "on2unit9", "on2_ex9_title", + "on2_quiz9_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit10_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Listina", "Práva člověka a občana", + "Tip: stát práva nedaruje podle nálady.", + { + "Lidská práva má člověk proto, že je člověk.", + "Listina chrání základní svobody, politická práva, práva menšin i hospodářská, sociální a kulturní práva.", + "Patří sem třeba život, osobní svoboda, vlastnictví, volební právo a vzdělání.", + "Samostatná hlava chrání právo domáhat se svých práv u nezávislého soudu.", + NULL, + }, + }, + { + "2 / 2 • Meze", "Ochrana a hranice svobody", + "Tip: svoboda projevu nechrání pomluvu ani navádění k násilí.", + { + "Práva lze omezit jen zákonem a jen z vážných důvodů, třeba ochrany druhých.", + "Ohrožená práva se hájí u soudu a lze se obrátit i na Ústavní soud.", + "Ve světě je připomíná Všeobecná deklarace lidských práv a evropská úmluva.", + "Právo jednoho končí tam, kde začíná stejně vážené právo druhého.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[9], "on2_unit10", "on2_unit10_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit10_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč má člověk lidská práva?", + { + "Protože je člověk", + "Jen když vyhraje volby", + "Jen jako odměnu státu", + "Jen do 18 let", + }, + 4, 0}, + {"Kde jsou v České republice ústavně zakotvena?", + { + "Jen v jednacím řádu Sněmovny", + "V Listině základních práv a svobod", + "Jen v občanském zákoníku", + "V obecní vyhlášce", + }, + 4, 1}, + {"Lze lidská práva omezit libovolně?", + { + "Ano, rozhodnutím starosty", + "Ne, jen zákonem a z vážných důvodů", + "Ano, většinou v anketě", + "Ne, nelze je omezit nikdy a v ničem", + }, + 4, 1}, + {"Kam se obrátit, jsou-li práva ohrožena?", + { + "Na nikoho, práva se nehájí", + "Třeba k soudu", + "Jen na politickou stranu", + "Jen do novin", + }, + 4, 1}, + }; + static const char *hints[] = { + "Práva nejsou milost úřadu", + "Listina je součást ústavního pořádku", + "Omezení má zákonný důvod", + "Soudní ochrana je součást Listiny", + }; + return build_on2_mcq_page(9, "on2unit10", "on2_ex10_title", + "on2_quiz10_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit11_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Dítě", "Úmluva o právech dítěte", + "Tip: dítětem je člověk mladší 18 let.", + { + "Dítě má stejnou důstojnost jako dospělý.", + "Úmluva zdůrazňuje zájem dítěte, právo na život, vzdělání a ochranu před násilím.", + "Dítě má právo být slyšeno ve věcech, které se ho týkají.", + "Rodiče i stát mají dítě chránit.", + NULL, + }, + }, + { + "2 / 2 • Ochránce", "Veřejný ochránce práv", + "Tip: ombudsman není soudce a neruší rozsudky.", + { + "Ombudsman chrání lidi před pochybením a nečinností úřadů.", + "Může věc prošetřit a žádat nápravu.", + "Nerozhoduje místo soudu a neukládá tresty.", + "Obrátit se na něj může člověk, který si neví rady s úřadem.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[10], "on2_unit11", "on2_unit11_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit11_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo je podle Úmluvy o právech dítěte dítě?", + { + "Každý žák střední školy do maturity", + "Člověk mladší 18 let", + "Jen občan České republiky", + "Jen ten, kdo žije s rodiči", + }, + 4, 1}, + {"Co úmluva zvlášť zdůrazňuje?", + { + "Povinnost dítěte volit", + "Život, vzdělání a ochranu před násilím", + "Právo řídit obec", + "Povinnou práci od 14 let", + }, + 4, 1}, + {"Co dělá veřejný ochránce práv?", + { + "Prošetřuje pochybení úřadů", + "Ruší rozsudky trestních soudů", + "Jmenuje ministry", + "Schvaluje zákony", + }, + 4, 0}, + {"Může ombudsman uložit trest?", + { + "Ano, jako soudce", + "Ne", + "Ano, pokutu do milionu", + "Ano, pokud jde o dítě", + }, + 4, 1}, + }; + static const char *hints[] = { + "Hranicí je 18 let", + "Zájem dítěte a ochrana", + "Ombudsman není soud", + "Tresty ukládají soudy a správní orgány, ne ombudsman", + }; + return build_on2_mcq_page(10, "on2unit11", "on2_ex11_title", + "on2_quiz11_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit12_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Směry", "Čtyři klasické postoje", + "Tip: ideologie není totéž co jedna konkrétní strana.", + { + "Ideologie je soustava názorů na to, jak má vypadat společnost a stát.", + "Liberalismus staví na svobodě jednotlivce a na omezené moci státu.", + "Konzervatismus brání osvědčené pořádky a změny chce postupné.", + "Socialismus zdůrazňuje rovnost a silnější roli státu v hospodářství.", + NULL, + }, + }, + { + "2 / 2 • Mez", "Další směry a co demokracie nepřipouští", + "Tip: fašismus a nacismus jsou s ústavním pořádkem neslučitelné.", + { + "Nacionalismus staví národ na první místo a může být umírněný, nebo nepřátelský k jiným.", + "Environmentalismus klade důraz na ochranu přírody, feminismus na rovnost žen a mužů.", + "Anarchismus odmítá stát jako takový.", + "Fašismus a nacismus popírají demokracii a rovnost lidí.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[11], "on2_unit12", "on2_unit12_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit12_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co zdůrazňuje liberalismus?", + { + "Vládu jednoho vůdce", + "Svobodu jednotlivce", + "Zrušení voleb", + "Národ jako jedinou hodnotu", + }, + 4, 1}, + {"Co je typické pro konzervatismus?", + { + "Změna za každou cenu", + "Postupná změna a úcta k osvědčeným pořádkům", + "Odmítnutí státu", + "Zákaz tradice", + }, + 4, 1}, + {"Jsou fašismus a nacismus slučitelné s demokracií?", + { + "Ano, jsou jen další strany", + "Ne, popírají rovnost a demokratická pravidla", + "Ano, pokud vyhrají volby", + "Záleží na vlajce", + }, + 4, 1}, + {"Co je politická ideologie?", + { + "Jednací řád úřadu", + "Soustava názorů na uspořádání společnosti", + "Státní symbol", + "Druh daně", + }, + 4, 1}, + }; + static const char *hints[] = { + "Liberalismus hlídá svobodu a meze státu", + "Konzervatismus nespěchá se zlomy", + "Ústavní pořádek je nepřipouští", + "Ideologie je názorový celek, ne úřad", + }; + return build_on2_mcq_page(11, "on2unit12", "on2_ex12_title", + "on2_quiz12_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit13_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Strany", "K čemu je politická strana", + "Tip: občan se dívá na program, ne jen na heslo.", + { + "Strana sdružuje lidi s podobným programem a uchází se o hlasy.", + "V demokracii soutěží více stran a žádná nemá moc natrvalo zaručenou.", + "Strany se v čase mění, proto je důležitější program než naučený seznam názvů.", + "Jedna strana potřebuje ke vstupu do Sněmovny alespoň 5 procent hlasů.", + NULL, + }, + }, + { + "2 / 2 • Volby", "Právo volit a být volen", + "Tip: kdo nevolí, nechává rozhodnutí na ostatních.", + { + "Aktivní volební právo je právo volit, pasivní je právo být volen.", + "Do Sněmovny se volí poměrným systémem, do Senátu většinovým ve dvou kolech.", + "Prezident se volí přímo, nejvýše ve dvou kolech.", + "Do Parlamentu volí občané České republiky od 18 let; do obecního zastupitelstva mohou volit i občané EU s trvalým pobytem v obci.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[12], "on2_unit13", "on2_unit13_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit13_exercise_page(void) { + static const ChoiceQ qs[] = { + {"K čemu je politická strana?", + { + "Nahrazuje soud", + "Sdružuje lidi s programem a uchází se o hlasy", + "Vybírá daně", + "Jmenuje soudce", + }, + 4, 1}, + {"Jak se volí Poslanecká sněmovna?", + { + "Losem", + "Poměrným systémem", + "Jen většinově v jednom kole jako Senát", + "Vládou", + }, + 4, 1}, + {"Jak se volí Senát?", + { + "Poměrně jednou kandidátkou", + "Většinově ve dvou kolech", + "Prezidentem", + "Stejně jako evropský parlament", + }, + 4, 1}, + {"Od kolika let smí občan ČR volit do Parlamentu?", + { + "Od 15 let", + "Od 18 let", + "Od 21 let", + "Od 40 let", + }, + 4, 1}, + }; + static const char *hints[] = { + "Strana soutěží ve volbách", + "Hlasy se ve Sněmovně rozdělují poměrně", + "Senátní obvody jsou většinové", + "Aktivní právo je od 18 let", + }; + return build_on2_mcq_page(12, "on2unit13", "on2_ex13_title", + "on2_quiz13_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit14_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Spolky", "Lidé vedle státu", + "Tip: spolek není úřad.", + { + "Občanská společnost jsou lidé, kteří se sdružují nezávisle na státu.", + "Patří sem spolky, odbory, nadační organizace i místní iniciativy.", + "Není to úřad a není to totéž co politická strana, i když s nimi může jednat.", + "Hlídá moc, pomáhá a přináší témata, která stát sám nevidí.", + NULL, + }, + }, + { + "2 / 2 • Ctnosti", "Co demokracie potřebuje od lidí", + "Tip: ctnost není slepá poslušnost.", + { + "Patří sem odpovědnost, tolerance a respekt k právu.", + "Patří sem i ochota domluvit se a solidarita.", + "Občanská ctnost demokracii nepodrývá.", + "Angažovat se lze ve spolku, v obci i tím, že člověk neničí společné věci.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[13], "on2_unit14", "on2_unit14_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit14_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je občanská společnost?", + { + "Soubor ministerstev", + "Sdružování lidí nezávisle na státu", + "Jen Poslanecká sněmovna", + "Armáda", + }, + 4, 1}, + {"Je spolek státní úřad?", + { + "Ano", + "Ne", + "Ano, pokud má stanovy", + "Jen v kraji", + }, + 4, 1}, + {"Která vlastnost demokracii pomáhá?", + { + "Lhostejnost", + "Odpovědnost a tolerance", + "Pohrdání právem", + "Nenávist k menšině", + }, + 4, 1}, + {"Je slepá poslušnost občanská ctnost?", + { + "Ano, občan se neptá", + "Ne", + "Ano, pokud to řekne úřad", + "Jen ve škole", + }, + 4, 1}, + }; + static const char *hints[] = { + "Žije vedle státu, ne místo úřadů", + "Spolek je soukromé sdružení", + "Ctnosti drží soužití", + "Ctnost zahrnuje úsudek a respekt k právu", + }; + return build_on2_mcq_page(13, "on2unit14", "on2_ex14_title", + "on2_quiz14_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit15_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Pojmy", "Není to totéž", + "Tip: kritika vlády ještě není extremismus.", + { + "Radikální názor chce výraznou změnu a sám o sobě ještě nemusí rušit demokracii.", + "Extremismus popírá rovnost lidí nebo demokratická pravidla a často sahá k nenávisti.", + "Terorismus je násilí proti nezúčastněným, aby společnost dostala strach a politicky ustoupila.", + "Nesouhlas s vládou se v demokracii řeší argumentem a volbami.", + NULL, + }, + }, + { + "2 / 2 • Mez", "Proč to demokracii ohrožuje", + "Tip: symbol nenávistného hnutí není nevinný vtip.", + { + "Extremismus bere části lidí bezpečí a rovné postavení.", + "Propagace hnutí, které směřuje k potlačení práv a svobod, je nepřijatelná a může být trestná.", + "Extremistická symbolika na takové hnutí odkazuje.", + "Když parta tlačí k nenávisti, pomoc je u dospělého, ve škole nebo u policie.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[14], "on2_unit15", "on2_unit15_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit15_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Je každá ostrá kritika vlády extremismus?", + { + "Ano", + "Ne", + "Ano, pokud je v novinách", + "Ano, pokud kritik nevolil", + }, + 4, 1}, + {"Co je terorismus?", + { + "Pokojná demonstrace", + "Násilí proti nezúčastněným s cílem nahnat strach", + "Volební kampaň", + "Žaloba k soudu", + }, + 4, 1}, + {"Proč není extremistická symbolika nevinná?", + { + "Protože je drahá", + "Odkazuje na hnutí, která popírají práva a demokracii", + "Protože ji zakazuje škola bez zákona", + "Protože nepatří do módy", + }, + 4, 1}, + {"Kam se obrátit, když skupina tlačí k nenávisti?", + { + "Hlouběji do té skupiny", + "Na dospělého, školu nebo policii", + "Nikam, je to soukromá věc strany", + "Na autora symbolu s prosbou o radu", + }, + 4, 1}, + }; + static const char *hints[] = { + "Kritika patří k demokracii", + "Teror cílí na strach veřejnosti", + "Symbol nese program nenávisti", + "Pomoc je mimo tu skupinu", + }; + return build_on2_mcq_page(14, "on2unit15", "on2_ex15_title", + "on2_quiz15_head", qs, hints, 4); +} +static GtkWidget *build_on2_unit16_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Úloha", "Tisk, vysílání a internet", + "Tip: bez informací se moc špatně kontroluje.", + { + "Média jsou tisk, televize, rozhlas i internet.", + "Mají informovat, kontrolovat moc, komentovat a také bavit.", + "Svobodný přístup k informacím je pro demokracii nutný.", + "Veřejnoprávní médium má sloužit veřejnosti, soukromé žije i z reklamy a rozhodnutí vlastníků.", + NULL, + }, + }, + { + "2 / 2 • Kritika", "Jak se nenechat zmást", + "Tip: jedna sdílená zpráva ještě není důkaz.", + { + "Zpráva, komentář a reklama nejsou totéž.", + "Ptáme se, kdo informaci vydal a jaký k ní má zájem.", + "Ověření znamená podívat se i do dalšího zdroje.", + "Svoboda projevu má meze: pomluvu a navádění k násilí nechrání.", + NULL, + }, + }, + }; + + return build_on2_unit_page(&on2_lessons[15], "on2_unit16", "on2_unit16_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on2_unit16_exercise_page(void) { + static const ChoiceQ qs[] = { + {"K čemu jsou média v demokracii?", + { + "Jen k reklamě", + "Informovat a pomáhat kontrolovat moc", + "Nahrazovat soudy", + "Schvalovat zákony", + }, + 4, 1}, + {"Je komentář totéž co zpráva?", + { + "Ano", + "Ne, komentář je názor", + "Ano, pokud má titulek", + "Zpráva je vždy reklama", + }, + 4, 1}, + {"Jak si ověřit informaci?", + { + "Stačí počet sdílení", + "Podívat se na zdroj, zájem autora a další zdroj", + "Věřit nejhlasitějšímu titulku", + "Zeptat se jen toho, kdo ji poslal", + }, + 4, 1}, + {"Chrání svoboda projevu navádění k násilí?", + { + "Ano, bez výjimky", + "Ne", + "Ano, na internetu", + "Ano, pokud jde o politiku", + }, + 4, 1}, + }; + static const char *hints[] = { + "Informace a kontrola", + "Fakt a názor se rozlišují", + "Kritický přístup srovnává zdroje", + "Svoboda projevu má zákonné meze", + }; + return build_on2_mcq_page(15, "on2unit16", "on2_ex16_title", + "on2_quiz16_head", qs, hints, 4); +} + +void add_on2_pages(GtkStack *stack) { + typedef GtkWidget *(*OnBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + OnBuilder build_unit; + OnBuilder build_ex; + } pages[] = { + {"on2unit1", "on2ex1", build_on2_unit1_page, build_on2_unit1_exercise_page}, + {"on2unit2", "on2ex2", build_on2_unit2_page, build_on2_unit2_exercise_page}, + {"on2unit3", "on2ex3", build_on2_unit3_page, build_on2_unit3_exercise_page}, + {"on2unit4", "on2ex4", build_on2_unit4_page, build_on2_unit4_exercise_page}, + {"on2unit5", "on2ex5", build_on2_unit5_page, build_on2_unit5_exercise_page}, + {"on2unit6", "on2ex6", build_on2_unit6_page, build_on2_unit6_exercise_page}, + {"on2unit7", "on2ex7", build_on2_unit7_page, build_on2_unit7_exercise_page}, + {"on2unit8", "on2ex8", build_on2_unit8_page, build_on2_unit8_exercise_page}, + {"on2unit9", "on2ex9", build_on2_unit9_page, build_on2_unit9_exercise_page}, + {"on2unit10", "on2ex10", build_on2_unit10_page, build_on2_unit10_exercise_page}, + {"on2unit11", "on2ex11", build_on2_unit11_page, build_on2_unit11_exercise_page}, + {"on2unit12", "on2ex12", build_on2_unit12_page, build_on2_unit12_exercise_page}, + {"on2unit13", "on2ex13", build_on2_unit13_page, build_on2_unit13_exercise_page}, + {"on2unit14", "on2ex14", build_on2_unit14_page, build_on2_unit14_exercise_page}, + {"on2unit15", "on2ex15", build_on2_unit15_page, build_on2_unit15_exercise_page}, + {"on2unit16", "on2ex16", build_on2_unit16_page, build_on2_unit16_exercise_page} + }; + + for (guint i = 0; i < G_N_ELEMENTS(pages); i++) { + gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name); + gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name); + } +} diff --git a/src/progress.c b/src/progress.c index d5622c6..7c3fef3 100644 --- a/src/progress.c +++ b/src/progress.c @@ -55,6 +55,7 @@ void load_progress(void) { net_load_progress(); hw_load_progress(); on_load_progress(); + on2_load_progress(); } /* Redraw the exercise rails of every unit that already has a page. */ @@ -424,12 +425,98 @@ void mark_on_done(int lesson_id) { } void progress_for_on(ProgressSum *out) { - out->total_ex += ON_LESSONS; - out->open_units += ON_LESSONS; + out->total_ex += ON_LESSONS + ON2_LESSONS; + out->open_units += ON_LESSONS + ON2_LESSONS; for (int i = 0; i < ON_LESSONS; i++) { if (on_lessons[i].done) { out->done_ex++; out->done_units++; } } + for (int i = 0; i < ON2_LESSONS; i++) { + if (on2_lessons[i].done) { + out->done_ex++; + out->done_units++; + } + } +} + +void on2_load_progress(void) { + GKeyFile *kf = g_key_file_new(); + GError *err = NULL; + + if (!g_key_file_load_from_file(kf, PROGRESS_ON2, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + + for (int i = 0; i < ON2_LESSONS; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + on2_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + + g_key_file_free(kf); +} + +void on2_save_progress(void) { + GKeyFile *kf = g_key_file_new(); + + for (int i = 0; i < ON2_LESSONS; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + g_key_file_set_boolean(kf, "done", key, on2_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_ON2, data, -1, &err); + if (err) + g_error_free(err); + g_free(data); + } +} + +void refresh_on2_completion_ui(void) { + for (int i = 0; i < ON2_LESSONS; i++) { + GtkWidget *node = on2_nodes[i]; + GtkWidget *icon = on2_lessons[i].done_icon; + + if (!node) + continue; + if (on2_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_on2_done(int lesson_id) { + if (lesson_id < 0 || lesson_id >= ON2_LESSONS) + return; + if (on2_lessons[lesson_id].done) + return; + on2_lessons[lesson_id].done = TRUE; + on2_save_progress(); + refresh_on2_completion_ui(); + refresh_stats_ui(); } diff --git a/src/search.c b/src/search.c index 1e9af07..3a4dcf1 100644 --- a/src/search.c +++ b/src/search.c @@ -203,7 +203,7 @@ static void catalog_rebuild(void) { } catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik"); - catalog_add_key("on_year2", "search_lesson", "onyears", TRUE, 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_year4", "search_lesson", "onyears", TRUE, 1, "rocnik"); for (i = 1; i <= ON_LESSONS; i++) { @@ -213,6 +213,13 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "onex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + for (i = 1; i <= ON2_LESSONS; i++) { + g_snprintf(key, sizeof(key), "on2_unit%d", i); + g_snprintf(page, sizeof(page), "on2unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "obcanka"); + g_snprintf(page, sizeof(page), "on2ex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); + } catalog_add_key("Český jazyk a literatura", "search_subject", "czechmap", FALSE, 0, "cestina czech"); diff --git a/src/theme.c b/src/theme.c index cf573d0..ad3196f 100644 --- a/src/theme.c +++ b/src/theme.c @@ -427,6 +427,7 @@ void apply_theme(void) { net_rail_theme_reset(); hw_rail_theme_reset(); on_rail_theme_reset(); + on2_rail_theme_reset(); czech_rail_theme_reset(); book_rail_theme_reset(); } diff --git a/src/util.c b/src/util.c index c613f17..bd4c9a3 100644 --- a/src/util.c +++ b/src/util.c @@ -21,6 +21,7 @@ char *app_settings_file; char *app_progress_net; char *app_progress_hw; char *app_progress_on; +char *app_progress_on2; char *app_progress_mluvnice; char *app_progress_cetba; @@ -160,6 +161,7 @@ static void set_progress_paths(const char *write_root) { g_free(app_progress_net); g_free(app_progress_hw); g_free(app_progress_on); + g_free(app_progress_on2); g_free(app_progress_mluvnice); g_free(app_progress_cetba); @@ -171,6 +173,7 @@ static void set_progress_paths(const char *write_root) { app_progress_net = g_build_filename(app_progress_dir, "net.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_on2 = g_build_filename(app_progress_dir, "on2.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); }