From 0ee2f8e9605871a7c564a492c126ea78e27c48f1 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Fri, 2 Oct 2026 22:52:39 +0200 Subject: [PATCH] Add year-3 civics lessons on the individual and the law. Co-authored-by: Cursor --- README.md | 6 +- .../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 | 4 + src/obcanka.c | 5 +- src/obcanka2.c | 4 +- src/obcanka3.c | 1910 +++++++++++++++++ src/progress.c | 91 +- src/search.c | 9 +- src/theme.c | 1 + src/util.c | 3 + 26 files changed, 2321 insertions(+), 23 deletions(-) create mode 100644 src/obcanka3.c diff --git a/README.md b/README.md index 9604d3b..8f3550b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses - **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba (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í); - 2. ročník, 16 lekcí (člověk jako občan); ročníky 3–4 jsou zamčené + 2. ročník, 16 lekcí (člověk jako občan); 3. ročník, 16 lekcí (člověk a právo); + 4. ročník je zamčený - statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina - ukončení: `Cmd/Super+Q` nebo `Alt+F4` @@ -264,7 +265,8 @@ Intel Mac, another architecture, or hacking on the code → - **Czech language** — grammar (20 exercises) and required reading (82 books: notes, quiz, plot ordering); literature theory is still locked - **Civics** — year 1, 17 lessons (the individual and society); - year 2, 16 lessons (the citizen in a democratic state); years 3–4 are locked + year 2, 16 lessons (the citizen in a democratic state); + year 3, 16 lessons (the individual and the law); year 4 is locked - statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English - 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 463d831..df1f226 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -136,6 +136,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markOn3(id: Int) { + progress.markOn3(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 c4653d1..794337e 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -38,6 +38,7 @@ 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.On3QuizScreen import org.maturita.maturita.ui.OnQuizScreen import org.maturita.maturita.ui.OnYearsScreen import org.maturita.maturita.ui.PlotScreen @@ -258,6 +259,20 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.On2Ex -> On2QuizScreen(vm, r.id) + Route.On3Map -> LessonListScreen( + vm, vm.tr("on_year3"), vm.tr("on3_sub"), + vm.content.on3.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.on3Done(id), Route.On3Lesson(id)) + }, + ) + is Route.On3Lesson -> { + val l = vm.content.on3Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.On3Ex(r.id)) + } + } + is Route.On3Ex -> On3QuizScreen(vm, r.id) Route.CzechMap -> CzechMapScreen(vm) Route.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 f1ea5a8..f5db2b9 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -24,6 +24,9 @@ sealed class Route(val page: String) { 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 On3Map : Route("on3map") + data class On3Lesson(val id: Int) : Route("on3unit$id") + data class On3Ex(val id: Int) : Route("on3ex$id") data object CzechMap : Route("czechmap") data object Mluvnice : Route("mluvnice") data class MluvEx(val n: Int) : Route("mluve$n") @@ -38,8 +41,8 @@ sealed class Route(val page: String) { /** Screens with their own bottom action bar; the navigation bar stays hidden there. */ val isExercise: Boolean get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx || - this is OnEx || this is On2Ex || this is MluvEx || this is BookQuiz || this is BookPlot || - this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson + this is OnEx || this is On2Ex || this is On3Ex || this is MluvEx || this is BookQuiz || this is BookPlot || + this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson /** Full stack from the practice root to this screen, so back always goes up one level. */ fun stack(): List = when (this) { @@ -65,6 +68,9 @@ sealed class Route(val page: String) { On2Map -> listOf(Subjects, OnYears, On2Map) is On2Lesson -> listOf(Subjects, OnYears, On2Map, this) is On2Ex -> listOf(Subjects, OnYears, On2Map, On2Lesson(id), this) + On3Map -> listOf(Subjects, OnYears, On3Map) + is On3Lesson -> listOf(Subjects, OnYears, On3Map, this) + is On3Ex -> listOf(Subjects, OnYears, On3Map, On3Lesson(id), this) CzechMap -> listOf(Subjects, CzechMap) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) @@ -88,6 +94,7 @@ fun routeFromPage(page: String): Route? = when (page) { "onyears" -> Route.OnYears "onmap" -> Route.OnMap "on2map" -> Route.On2Map + "on3map" -> Route.On3Map "czechmap" -> Route.CzechMap "mluvnice" -> Route.Mluvnice "readinglist" -> Route.ReadingList @@ -119,6 +126,12 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""hwex(\d+)""").matchEntire(page)?.let { return Route.HwEx(it.groupValues[1].toInt()) } + Regex("""on3unit(\d+)""").matchEntire(page)?.let { + return Route.On3Lesson(it.groupValues[1].toInt()) + } + Regex("""on3ex(\d+)""").matchEntire(page)?.let { + return Route.On3Ex(it.groupValues[1].toInt()) + } Regex("""on2unit(\d+)""").matchEntire(page)?.let { return Route.On2Lesson(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 7cea228..5f38820 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 @@ -31,6 +31,7 @@ class Content(root: JSONObject) { val hw = raw.arr("hw") val on = raw.arr("on") val on2 = raw.arr("on2") + val on3 = raw.arr("on3") val mluvnice = raw.arr("mluvnice") val books = raw.arr("books") val changelog = raw.arr("changelog") @@ -40,6 +41,7 @@ class Content(root: JSONObject) { 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 on3Lesson(id: Int) = on3.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 ce087da..dae4b17 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 @@ -38,6 +38,9 @@ class ProgressStore(context: Context) { fun on2Done(id: Int) = prefs.getBoolean("on2.$id", false) fun markOn2(id: Int) { prefs.edit().putBoolean("on2.$id", true).apply() } + fun on3Done(id: Int) = prefs.getBoolean("on3.$id", false) + fun markOn3(id: Int) { prefs.edit().putBoolean("on3.$id", true).apply() } + fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } @@ -92,6 +95,14 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + content.on3.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.on3Done(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } sum.totalEx += content.mluvnice.size sum.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 495fe50..f6c8a10 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 @@ -872,6 +872,18 @@ fun On2QuizScreen(vm: AppViewModel, id: Int) { DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn2(id) } } +@Composable +fun On3QuizScreen(vm: AppViewModel, id: Int) { + val lesson = vm.content.on3Lesson(id) ?: return + val spec = J(org.json.JSONObject().apply { + put("type", "choice") + put("title", vm.tr(lesson.str("exTitleKey"))) + put("sub", lesson.str("quizHeadKey")) + put("questions", lesson.o.optJSONArray("quiz")) + }) + DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn3(id) } +} + @Composable 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 5b6a0af..760cfca 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,10 +300,11 @@ 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", "on2map" -> { - sum.totalEx = vm.content.on.size + vm.content.on2.size + "onyears", "onmap", "on2map", "on3map" -> { + sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.size sum.doneEx = vm.content.on.count { vm.progress.onDone(it.int("id")) } + - vm.content.on2.count { vm.progress.on2Done(it.int("id")) } + vm.content.on2.count { vm.progress.on2Done(it.int("id")) } + + vm.content.on3.count { vm.progress.on3Done(it.int("id")) } } "czechmap" -> { sum.totalEx = vm.content.mluvnice.size @@ -494,7 +495,7 @@ private fun buildSearch(vm: AppViewModel): List { } add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra = "rocnik") add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra = "rocnik") - add(vm.tr("on_year3"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik") + add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra = "rocnik") add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik") vm.content.on.forEach { l -> add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit${l.int("id")}", extra = "obcanka") @@ -504,6 +505,10 @@ private fun buildSearch(vm: AppViewModel): List { 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")}") } + vm.content.on3.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on3unit${l.int("id")}", extra = "obcanka") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on3ex${l.int("id")}") + } add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice") 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") @@ -609,8 +614,9 @@ fun OnYearsScreen(vm: AppViewModel) { 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) } + NavRow(title = vm.tr("on_year3"), subtitle = vm.tr("on3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On3Map) } } - items(listOf("on_year3", "on_year4")) { key -> + items(listOf("on_year4")) { key -> NavRow(title = vm.tr(key), subtitle = vm.tr("on_year_locked_sub"), locked = true, onClick = null) } } diff --git a/data/changelog.txt b/data/changelog.txt index cacdc02..9508f53 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -9,10 +9,12 @@ --- 2026-10-02 cs: Občanská nauka: 1. ročník, 17 lekcí (člověk v lidském společenství) cs: Občanská nauka: 2. ročník, 16 lekcí (člověk jako občan) -cs: Občanská nauka: 3. a 4. ročník jsou zatím zamčené +cs: Občanská nauka: 3. ročník, 16 lekcí (člověk a právo) +cs: Občanská nauka: 4. ročník je zatím zamčený en: Civics: year 1, 17 lessons (the individual and society) en: Civics: year 2, 16 lessons (the citizen in a democratic state) -en: Civics: years 3 and 4 are still locked +en: Civics: year 3, 16 lessons (the individual and the law) +en: Civics: year 4 is still locked --- 2026-09-25 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 cf5f162..382a20e 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -138,6 +138,11 @@ final class AppModel: ObservableObject { refresh() } + func markOn3(_ id: Int) { + progress.markOn3(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 e0c3b78..5b36be0 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -33,6 +33,7 @@ final class Content { let hw: [J] let on: [J] let on2: [J] + let on3: [J] let mluvnice: [J] let books: [J] let changelog: [J] @@ -66,6 +67,7 @@ final class Content { hw = raw.arr("hw") on = raw.arr("on") on2 = raw.arr("on2") + on3 = raw.arr("on3") mluvnice = raw.arr("mluvnice") books = raw.arr("books") changelog = raw.arr("changelog") @@ -76,6 +78,7 @@ final class Content { 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 on3Lesson(_ id: Int) -> J? { on3.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 968ea01..c6ad454 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -40,6 +40,9 @@ final class ProgressStore { func on2Done(_ id: Int) -> Bool { defaults.bool(forKey: "on2.\(id)") } func markOn2(_ id: Int) { defaults.set(true, forKey: "on2.\(id)") } + func on3Done(_ id: Int) -> Bool { defaults.bool(forKey: "on3.\(id)") } + func markOn3(_ id: Int) { defaults.set(true, forKey: "on3.\(id)") } + func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } @@ -99,6 +102,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for l in content.on3 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.on3Done(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } sum.totalEx += content.mluvnice.count sum.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 b650b94..0256dde 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -30,6 +30,9 @@ enum Route: Hashable { case on2Map case on2Lesson(Int) case on2Ex(Int) + case on3Map + case on3Lesson(Int) + case on3Ex(Int) case czechMap case mluvnice case mluvEx(Int) @@ -62,6 +65,9 @@ enum Route: Hashable { case .on2Map: return "on2map" case .on2Lesson(let id): return "on2unit\(id)" case .on2Ex(let id): return "on2ex\(id)" + case .on3Map: return "on3map" + case .on3Lesson(let id): return "on3unit\(id)" + case .on3Ex(let id): return "on3ex\(id)" case .czechMap: return "czechmap" case .mluvnice: return "mluvnice" case .mluvEx(let n): return "mluve\(n)" @@ -124,6 +130,12 @@ enum Route: Hashable { return [.onYears, .on2Map, .on2Lesson(id)] case .on2Ex(let id): return [.onYears, .on2Map, .on2Lesson(id), .on2Ex(id)] + case .on3Map: + return [.onYears, .on3Map] + case .on3Lesson(let id): + return [.onYears, .on3Map, .on3Lesson(id)] + case .on3Ex(let id): + return [.onYears, .on3Map, .on3Lesson(id), .on3Ex(id)] case .czechMap: return [.czechMap] case .mluvnice: @@ -155,6 +167,7 @@ func routeFromPage(_ page: String) -> Route? { case "onyears": return .onYears case "onmap": return .onMap case "on2map": return .on2Map + case "on3map": return .on3Map case "czechmap": return .czechMap case "mluvnice": return .mluvnice case "readinglist": return .readingList @@ -198,6 +211,12 @@ func routeFromPage(_ page: String) -> Route? { if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) { return .hwEx(n) } + if let m = page.wholeMatch(of: /on3unit(\d+)/), let n = Int(m.1) { + return .on3Lesson(n) + } + if let m = page.wholeMatch(of: /on3ex(\d+)/), let n = Int(m.1) { + return .on3Ex(n) + } if let m = page.wholeMatch(of: /on2unit(\d+)/), let n = Int(m.1) { return .on2Lesson(n) } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index eed50a3..152ec3b 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -663,6 +663,25 @@ struct On2QuizScreen: View { } } +struct On3QuizScreen: View { + @ObservedObject var vm: AppModel + let id: Int + + var body: some View { + if let lesson = vm.content.on3Lesson(id) { + let spec = J([ + "type": "choice", + "title": vm.tr(lesson.str("exTitleKey")), + "sub": lesson.str("quizHeadKey"), + "questions": lesson.rawArray("quiz"), + ]) + DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) { + vm.markOn3(id) + } + } + } +} + struct HwQuizScreen: View { @ObservedObject var vm: AppModel let id: Int diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index 0780176..cac36ff 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -423,10 +423,11 @@ 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", "on2map": - sum.totalEx = vm.content.on.count + vm.content.on2.count + case "onyears", "onmap", "on2map", "on3map": + sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count + vm.content.on2.filter { vm.progress.on2Done($0.int("id")) }.count + + vm.content.on3.filter { vm.progress.on3Done($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 @@ -567,7 +568,7 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { } add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik") add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik") - add(vm.tr("on_year3"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik") + add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik") add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik") for l in vm.content.on { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka") @@ -577,6 +578,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit\(l.int("id"))", extra: "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex\(l.int("id"))") } + for l in vm.content.on3 { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on3unit\(l.int("id"))", extra: "obcanka") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on3ex\(l.int("id"))") + } add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice") add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist") for b in vm.content.books { @@ -804,7 +809,10 @@ struct OnYearsScreen: View { NavigationLink(value: Route.on2Map) { ListRowLabel(title: vm.tr("on_year2"), subtitle: vm.tr("on2_sub")) } - ForEach(["on_year3", "on_year4"], id: \.self) { key in + NavigationLink(value: Route.on3Map) { + ListRowLabel(title: vm.tr("on_year3"), subtitle: vm.tr("on3_sub")) + } + ForEach(["on_year4"], id: \.self) { key in Label { ListRowLabel(title: vm.tr(key), subtitle: vm.tr("on_year_locked_sub")) } icon: { @@ -1431,6 +1439,27 @@ struct RouteDestination: View { } case .on2Ex(let id): On2QuizScreen(vm: vm, id: id) + case .on3Map: + LessonListScreen( + vm: vm, + title: vm.tr("on_year3"), + subtitle: vm.tr("on3_sub"), + rows: vm.content.on3.map { l in + let id = l.int("id") + return LessonRow( + id: "o3\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.on3Done(id), + destination: .on3Lesson(id) + ) + } + ) + case .on3Lesson(let id): + if let l = vm.content.on3Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on3Ex(id)) + } + case .on3Ex(let id): + On3QuizScreen(vm: vm, id: id) case .czechMap: CzechMapScreen(vm: vm) case .mluvnice: diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index 2f41ddb..6a2b468 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -1108,6 +1108,18 @@ def main() -> int: "quiz": choice_items(A(f"build_on2_unit{i}_exercise_page.qs")), }) + on3_lessons = [] + for i in range(1, 17): + on3_lessons.append({ + "id": i, + "titleKey": f"on3_unit{i}", + "subKey": f"on3_unit{i}_sub", + "exTitleKey": f"on3_ex{i}_title", + "quizHeadKey": f"on3_quiz{i}_head", + "slides": slides_items(A(f"build_on3_unit{i}_page.slides")), + "quiz": choice_items(A(f"build_on3_unit{i}_exercise_page.qs")), + }) + hw_lessons = [] for i in range(1, 30): hw_lessons.append({ @@ -1528,6 +1540,7 @@ def main() -> int: "hw": hw_lessons, "on": on_lessons, "on2": on2_lessons, + "on3": on3_lessons, "mluvnice": mluvnice, "books": books, "changelog": parse_changelog(ROOT / "data" / "changelog.txt"), @@ -1564,6 +1577,7 @@ def main() -> int: print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}") print(f" 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" on3 lessons: {sum(1 for L in on3_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', []))}") @@ -1607,6 +1621,11 @@ def main() -> int: missing.append(f"on2{L['id']} slides") if not L["quiz"]: missing.append(f"on2{L['id']} quiz") + for L in on3_lessons: + if not L["slides"]: + missing.append(f"on3{L['id']} slides") + if not L["quiz"]: + missing.append(f"on3{L['id']} quiz") for m in mluvnice: 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 fbe28c0..e4d655c 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -51,6 +51,7 @@ extern char *app_progress_net; extern char *app_progress_hw; extern char *app_progress_on; extern char *app_progress_on2; +extern char *app_progress_on3; extern char *app_progress_mluvnice; extern char *app_progress_cetba; #define PROGRESS_DIR app_progress_dir @@ -62,6 +63,7 @@ extern char *app_progress_cetba; #define PROGRESS_HW app_progress_hw #define PROGRESS_ON app_progress_on #define PROGRESS_ON2 app_progress_on2 +#define PROGRESS_ON3 app_progress_on3 #define NODE_SIZE 88.0 #define PATH_SPAC 240.0 #define ROAD_MX 150.0 /* horizontal canvas margin */ @@ -123,6 +125,8 @@ extern char *app_progress_cetba; #define ON_LESSONS 17 #define ON2_UNITS 16 #define ON2_LESSONS 16 +#define ON3_UNITS 16 +#define ON3_LESSONS 16 #define HW_SLIDES 3 #define HW2_SLIDES 6 #define HW3_SLIDES 2 @@ -767,6 +771,8 @@ 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 NetLesson on3_lessons[ON3_LESSONS]; +extern GtkWidget *on3_nodes[ON3_UNITS]; extern GtkWidget *net_note_host; extern int net_note_last_w; extern GtkWidget *net_scroll; @@ -872,6 +878,14 @@ void on2_rail_theme_reset(void); void on2_lessons_apply_lang(void); GtkWidget *build_on2map_page(void); void add_on2_pages(GtkStack *stack); +void on3_load_progress(void); +void on3_save_progress(void); +void mark_on3_done(int lesson_id); +void refresh_on3_completion_ui(void); +void on3_rail_theme_reset(void); +void on3_lessons_apply_lang(void); +GtkWidget *build_on3map_page(void); +void add_on3_pages(GtkStack *stack); void czech_rail_theme_reset(void); void hw_lessons_apply_lang(void); GtkWidget *build_hwyears_page(void); diff --git a/src/i18n.c b/src/i18n.c index fb2c837..8c4e610 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -902,6 +902,104 @@ const TrEntry tr_ui[] = { {"on2_ex16_title", "Kvíz: média", "Quiz: the media"}, {"on2_quiz16_head", "Kvíz k médiím", "Quiz on the media"}, + {"on3_sub", "Člověk a právo. Vyberte lekci na cestě.", + "The individual and the law. Pick a lesson on the path."}, + {"on3_unit1", "Právo a spravedlnost", "Law and justice"}, + {"on3_unit1_sub", "2 snímky o právu a spravedlnosti a pak kvíz.", + "2 slides on law and justice, then a quiz."}, + {"on3_ex1_title", "Kvíz: právo a spravedlnost", "Quiz: law and justice"}, + {"on3_quiz1_head", "Kvíz k právu a spravedlnosti", + "Quiz on law and justice"}, + {"on3_unit2", "Právní stát", "The rule of law"}, + {"on3_unit2_sub", "2 snímky o právním státu a pak kvíz.", + "2 slides on the rule of law, then a quiz."}, + {"on3_ex2_title", "Kvíz: právní stát", "Quiz: the rule of law"}, + {"on3_quiz2_head", "Kvíz k právnímu státu", + "Quiz on the rule of law"}, + {"on3_unit3", "Právní řád", "The legal order"}, + {"on3_unit3_sub", "2 snímky o předpisech a odvětvích práva a pak kvíz.", + "2 slides on legal rules and branches of law, then a quiz."}, + {"on3_ex3_title", "Kvíz: právní řád", "Quiz: the legal order"}, + {"on3_quiz3_head", "Kvíz k právnímu řádu", + "Quiz on the legal order"}, + {"on3_unit4", "Osoby a svéprávnost", "Persons and legal capacity"}, + {"on3_unit4_sub", "2 snímky o osobách, svéprávnosti a odpovědnosti a pak kvíz.", + "2 slides on persons, capacity and liability, then a quiz."}, + {"on3_ex4_title", "Kvíz: osoby a svéprávnost", "Quiz: persons and capacity"}, + {"on3_quiz4_head", "Kvíz k osobám a svéprávnosti", + "Quiz on persons and capacity"}, + {"on3_unit5", "Vlastnictví a škoda", "Ownership and damage"}, + {"on3_unit5_sub", "2 snímky o vlastnictví, duševním vlastnictví a škodě a pak kvíz.", + "2 slides on ownership, intellectual property and damage, then a quiz."}, + {"on3_ex5_title", "Kvíz: vlastnictví a škoda", "Quiz: ownership and damage"}, + {"on3_quiz5_head", "Kvíz k vlastnictví a škodě", + "Quiz on ownership and damage"}, + {"on3_unit6", "Smlouvy", "Contracts"}, + {"on3_unit6_sub", "2 snímky o smlouvách a pak kvíz.", + "2 slides on contracts, then a quiz."}, + {"on3_ex6_title", "Kvíz: smlouvy", "Quiz: contracts"}, + {"on3_quiz6_head", "Kvíz ke smlouvám", + "Quiz on contracts"}, + {"on3_unit7", "Spotřebitel", "The consumer"}, + {"on3_unit7_sub", "2 snímky o reklamaci a odstoupení od smlouvy a pak kvíz.", + "2 slides on complaints and withdrawing from a contract, then a quiz."}, + {"on3_ex7_title", "Kvíz: spotřebitel", "Quiz: the consumer"}, + {"on3_quiz7_head", "Kvíz ke spotřebiteli", + "Quiz on the consumer"}, + {"on3_unit8", "Dědictví", "Inheritance"}, + {"on3_unit8_sub", "2 snímky o dědění a dluzích a pak kvíz.", + "2 slides on inheritance and debts, then a quiz."}, + {"on3_ex8_title", "Kvíz: dědictví", "Quiz: inheritance"}, + {"on3_quiz8_head", "Kvíz k dědictví", + "Quiz on inheritance"}, + {"on3_unit9", "Rodina a právo", "Family and the law"}, + {"on3_unit9_sub", "2 snímky o manželství, partnerství a péči o dítě a pak kvíz.", + "2 slides on marriage, partnership and care of a child, then a quiz."}, + {"on3_ex9_title", "Kvíz: rodina a právo", "Quiz: family and the law"}, + {"on3_quiz9_head", "Kvíz k rodině a právu", + "Quiz on family and the law"}, + {"on3_unit10", "Pracovní právo", "Employment law"}, + {"on3_unit10_sub", "2 snímky o pracovní smlouvě, výpovědi a bezpečnosti a pak kvíz.", + "2 slides on the employment contract, notice and safety, then a quiz."}, + {"on3_ex10_title", "Kvíz: pracovní právo", "Quiz: employment law"}, + {"on3_quiz10_head", "Kvíz k pracovnímu právu", + "Quiz on employment law"}, + {"on3_unit11", "Soustava soudů", "The court system"}, + {"on3_unit11_sub", "2 snímky o soustavě soudů a pak kvíz.", + "2 slides on the court system, then a quiz."}, + {"on3_ex11_title", "Kvíz: soustava soudů", "Quiz: the court system"}, + {"on3_quiz11_head", "Kvíz k soustavě soudů", + "Quiz on the court system"}, + {"on3_unit12", "Soudní a správní řízení", "Court and administrative proceedings"}, + {"on3_unit12_sub", "2 snímky o žalobě, odvolání a správním řízení a pak kvíz.", + "2 slides on lawsuits, appeals and administrative proceedings, then a quiz."}, + {"on3_ex12_title", "Kvíz: řízení", "Quiz: proceedings"}, + {"on3_quiz12_head", "Kvíz k soudnímu a správnímu řízení", + "Quiz on court and administrative proceedings"}, + {"on3_unit13", "Právnická povolání", "Legal professions"}, + {"on3_unit13_sub", "2 snímky o soudci, žalobci, advokátovi, notáři a exekutorovi a pak kvíz.", + "2 slides on judges, prosecutors, lawyers, notaries and bailiffs, then a quiz."}, + {"on3_ex13_title", "Kvíz: právnická povolání", "Quiz: legal professions"}, + {"on3_quiz13_head", "Kvíz k právnickým povoláním", + "Quiz on legal professions"}, + {"on3_unit14", "Trestný čin a přestupek", "Crimes and misdemeanours"}, + {"on3_unit14_sub", "2 snímky o trestném činu, přestupku a vině a pak kvíz.", + "2 slides on crimes, misdemeanours and guilt, then a quiz."}, + {"on3_ex14_title", "Kvíz: trestný čin a přestupek", "Quiz: crimes and misdemeanours"}, + {"on3_quiz14_head", "Kvíz k trestnému činu a přestupku", + "Quiz on crimes and misdemeanours"}, + {"on3_unit15", "Tresty a trestní řízení", "Punishments and criminal procedure"}, + {"on3_unit15_sub", "2 snímky o trestech, vězení a průběhu řízení a pak kvíz.", + "2 slides on punishments, prison and the course of a case, then a quiz."}, + {"on3_ex15_title", "Kvíz: tresty a řízení", "Quiz: punishments and procedure"}, + {"on3_quiz15_head", "Kvíz k trestům a trestnímu řízení", + "Quiz on punishments and criminal procedure"}, + {"on3_unit16", "Mladiství, oběť a svědek", "Juveniles, victims and witnesses"}, + {"on3_unit16_sub", "2 snímky o mladistvých a o tom, co dělat jako oběť nebo svědek, a pak kvíz.", + "2 slides on juveniles and what to do as a victim or witness, then a quiz."}, + {"on3_ex16_title", "Kvíz: mladiství, oběť a svědek", "Quiz: juveniles, victims and witnesses"}, + {"on3_quiz16_head", "Kvíz k mladistvým, oběti a svědkovi", + "Quiz on juveniles, victims and witnesses"}, {"czech_sub", "Vyberte oblast a začněte procvičovat.", "Pick an area and start practicing."}, {"Literatura", "Literatura", "Literature"}, @@ -1927,6 +2025,7 @@ 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 on3_lessons_apply_lang(void); void net_rail_theme_reset(void); void apply_language(void) { @@ -1943,5 +2042,6 @@ void apply_language(void) { hw_lessons_apply_lang(); on_lessons_apply_lang(); on2_lessons_apply_lang(); + on3_lessons_apply_lang(); search_apply_lang(); } diff --git a/src/main.c b/src/main.c index 57d45f5..eaa7f34 100644 --- a/src/main.c +++ b/src/main.c @@ -386,6 +386,7 @@ static gboolean run_smoke_test(gpointer data) { static const char *pages[] = { "welcome", "subjects", "roadmap", "stats", "netyears", "netmap", "hwyears", "hwmap", "onyears", "onmap", "on2map", + "on3map", "czechmap", "mluvnice", "readinglist", "cetba1984", "cetba1984quiz", "cetba1984dej", @@ -399,6 +400,7 @@ static gboolean run_smoke_test(gpointer data) { "hwunit29", "hwex29", "onunit1", "onex1", "onunit17", "onex17", "on2unit1", "on2ex1", "on2unit16", "on2ex16", + "on3unit1", "on3ex1", "on3unit16", "on3ex16", "unit1", "unit2", "unit3", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1vocab", "u2vocab", "u3vocab", @@ -514,6 +516,8 @@ void activate(GtkApplication *app, gpointer user_data) { 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_on3map_page(), "on3map"); + add_on3_pages(main_stack); gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap"); gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice"); for (int n = 1; n <= 20; n++) { diff --git a/src/obcanka.c b/src/obcanka.c index 10037e2..9900b36 100644 --- a/src/obcanka.c +++ b/src/obcanka.c @@ -3,8 +3,7 @@ /* 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. Year 2 is the citizen and the state; years 3 and 4 - * stay locked. */ + * place in the first year. Years 2 and 3 are open; year 4 stays locked. */ #define ON_ENTRY(n) \ { .n_slides = 2, .unit_page = "onunit" #n, .ex_page = "onex" #n } @@ -413,7 +412,7 @@ GtkWidget *build_onyears_page(void) { GtkWidget *scroll; GtkWidget *list; int y; - static const char *targets[] = {"onmap", "on2map", NULL, NULL}; + static const char *targets[] = {"onmap", "on2map", "on3map", NULL}; page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_hexpand(page, TRUE); diff --git a/src/obcanka2.c b/src/obcanka2.c index f0630a3..f4f4ddd 100644 --- a/src/obcanka2.c +++ b/src/obcanka2.c @@ -3,8 +3,8 @@ /* 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. */ + * schools place in the second year. Law is year 3; the economy and + * philosophy stay in the fourth year. */ #define ON2_ENTRY(n) \ { .n_slides = 2, .unit_page = "on2unit" #n, .ex_page = "on2ex" #n } diff --git a/src/obcanka3.c b/src/obcanka3.c new file mode 100644 index 0000000..c74117e --- /dev/null +++ b/src/obcanka3.c @@ -0,0 +1,1910 @@ +#include "graduately.h" + +/* Year 3 of Občanská nauka. + * Thematic unit "Člověk a právo" from the MŠMT model syllabus + * (č.j. 18 396/2002-23) and the SOV civic framework, which schools place + * in the third year. The economy and philosophy stay in the fourth year. */ + +#define ON3_ENTRY(n) \ + { .n_slides = 2, .unit_page = "on3unit" #n, .ex_page = "on3ex" #n } + +NetLesson on3_lessons[ON3_LESSONS] = { + ON3_ENTRY(1), + ON3_ENTRY(2), + ON3_ENTRY(3), + ON3_ENTRY(4), + ON3_ENTRY(5), + ON3_ENTRY(6), + ON3_ENTRY(7), + ON3_ENTRY(8), + ON3_ENTRY(9), + ON3_ENTRY(10), + ON3_ENTRY(11), + ON3_ENTRY(12), + ON3_ENTRY(13), + ON3_ENTRY(14), + ON3_ENTRY(15), + ON3_ENTRY(16), +}; + +#undef ON3_ENTRY + +GtkWidget *on3_scroll; +GtkWidget *on3_fixed; +GtkWidget *on3_rail; +GtkWidget *on3_nodes[ON3_UNITS]; +GtkWidget *on3_labels[ON3_UNITS]; +double on3_cx[ON3_UNITS]; +double on3_cy[ON3_UNITS]; +int on3_cols = 1; +int on3_rows = 1; +int on3_cw = (int)(2.0 * ROAD_MX + (ON3_UNITS - 1) * PATH_SPAC); +int on3_ch = (int)(2.0 * ROAD_MY); +guint on3_idle; +double on3_last_avail = -1.0; +int on3_last_cols = -1; +int on3_last_rows = -1; +int on3_last_cw = -1; +int on3_last_ch = -1; +cairo_surface_t *on3_rail_cache; +int on3_cache_w; +int on3_cache_h; + +static void on3_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 >= ON3_UNITS - 1) { k = ON3_UNITS - 2; u = 1.0; } + + p1x = on3_cx[k]; p1y = on3_cy[k]; + p2x = on3_cx[k + 1]; p2y = on3_cy[k + 1]; + if (k - 1 >= 0) { p0x = on3_cx[k - 1]; p0y = on3_cy[k - 1]; } + else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); } + if (k + 2 < ON3_UNITS) { p3x = on3_cx[k + 2]; p3y = on3_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 on3_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; + on3_point(t0, &x, &y); + cairo_move_to(cr, x, y); + for (s = 1; s <= n; s++) { + on3_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y); + cairo_line_to(cr, x, y); + } +} + +static void on3_geometry(double avail) { + const double phase = 2.0 * G_PI * 1.7 / (double)(ON3_UNITS - 1); + int rows; + int r, c, i; + + for (rows = 1; rows <= ON3_UNITS; rows++) { + int cols = (ON3_UNITS + rows - 1) / rows; + if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0) + break; + } + if (rows > ON3_UNITS) + rows = ON3_UNITS; + on3_rows = rows; + on3_cols = (ON3_UNITS + rows - 1) / rows; + if (on3_cols < 1) + on3_cols = 1; + on3_cw = (int)(2.0 * ROAD_MX + (on3_cols - 1) * PATH_SPAC); + on3_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP); + + for (r = 0; r < rows; r++) { + int base = r * on3_cols; + int len = MIN(on3_cols, ON3_UNITS - base); + int fwd = (r % 2) == 0; + for (c = 0; c < len; c++) { + i = base + c; + int cc = fwd ? c : (on3_cols - 1 - c); + on3_cx[i] = ROAD_MX + cc * PATH_SPAC; + on3_cy[i] = ROAD_MY + r * ROAD_GAP + + ROAD_WAVE * sin((double)i * phase); + } + } +} + +static void on3_apply_layout(void) { + if (!on3_fixed) + return; + + if (on3_cols == on3_last_cols && on3_rows == on3_last_rows && + on3_cw == on3_last_cw && on3_ch == on3_last_ch) + return; + + on3_last_cols = on3_cols; + on3_last_rows = on3_rows; + on3_last_cw = on3_cw; + on3_last_ch = on3_ch; + if (on3_rail_cache) { + cairo_surface_destroy(on3_rail_cache); + on3_rail_cache = NULL; + } + + for (int i = 0; i < ON3_UNITS; i++) { + if (!on3_nodes[i] || !on3_labels[i]) + continue; + gtk_fixed_move(GTK_FIXED(on3_fixed), on3_nodes[i], + (int)(on3_cx[i] - NODE_SIZE / 2.0), + (int)(on3_cy[i] - NODE_SIZE / 2.0)); + gtk_fixed_move(GTK_FIXED(on3_fixed), on3_labels[i], + (int)(on3_cx[i] - (PATH_SPAC - 20.0) / 2.0), + (int)(on3_cy[i] + NODE_SIZE / 2.0 + 10.0)); + } + + gtk_widget_set_size_request(on3_fixed, on3_cw, on3_ch); + gtk_widget_set_size_request(on3_rail, on3_cw, on3_ch); + gtk_fixed_move(GTK_FIXED(on3_fixed), on3_rail, 0, 0); + gtk_widget_queue_draw(on3_rail); +} + +static void on3_relayout(void) { + GtkAdjustment *hadj; + double avail; + + if (!on3_scroll) + return; + hadj = gtk_scrolled_window_get_hadjustment( + GTK_SCROLLED_WINDOW(on3_scroll)); + avail = gtk_adjustment_get_page_size(hadj); + if (avail < 1.0) + return; + if (fabs(avail - on3_last_avail) < 1.0) + return; + on3_last_avail = avail; + on3_geometry(avail); + on3_apply_layout(); +} + +static gboolean on3_relayout_idle(gpointer data) { + (void)data; + on3_idle = 0; + on3_relayout(); + return G_SOURCE_REMOVE; +} + +static void on3_relayout_later(void) { + if (on3_idle == 0) + on3_idle = g_idle_add(on3_relayout_idle, NULL); +} + +static void on3_adjust_notify(GtkAdjustment *adj, GParamSpec *ps, + gpointer data) { + (void)adj; + (void)ps; + (void)data; + on3_relayout_later(); +} + +static void on3_render_rail_to(cairo_t *cr) { + const double t_end = (double)(ON3_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); + on3_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); + on3_path(cr, 0.0, t_end); + cairo_stroke(cr); + cairo_set_dash(cr, NULL, 0, 0.0); +} + +static void on3_draw_rail(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + (void)area; + (void)width; + (void)height; + (void)data; + + if (!on3_rail_cache || on3_cache_w != on3_cw || on3_cache_h != on3_ch) { + cairo_t *rcr; + + if (on3_rail_cache) + cairo_surface_destroy(on3_rail_cache); + on3_cache_w = on3_cw; + on3_cache_h = on3_ch; + if (on3_cache_w < 1) + on3_cache_w = 1; + if (on3_cache_h < 1) + on3_cache_h = 1; + on3_rail_cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + on3_cache_w, on3_cache_h); + rcr = cairo_create(on3_rail_cache); + on3_render_rail_to(rcr); + cairo_destroy(rcr); + } + + cairo_set_source_surface(cr, on3_rail_cache, 0, 0); + cairo_paint(cr); +} + +void on3_rail_theme_reset(void) { + if (on3_rail_cache) { + cairo_surface_destroy(on3_rail_cache); + on3_rail_cache = NULL; + } + if (on3_rail) + gtk_widget_queue_draw(on3_rail); +} + +static void on3_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), "on3_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 on3_add_node(GtkFixed *fixed, int index) { + int num = index + 1; + gboolean locked = index >= ON3_LESSONS; + GtkWidget *card; + GtkWidget *vbox; + GtkWidget *number; + GtkWidget *icon; + GtkWidget *name; + char *text; + static char unit_keys[ON3_LESSONS][16]; + static gboolean unit_keys_ready = FALSE; + + if (!unit_keys_ready) { + for (int i = 0; i < ON3_LESSONS; i++) + g_snprintf(unit_keys[i], sizeof unit_keys[i], "on3_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); + on3_lessons[index].done_icon = icon; + g_signal_connect(card, "clicked", G_CALLBACK(on3_open_unit), + &on3_lessons[index]); + } + + on3_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 < ON3_LESSONS) { + i18n_bind(name, unit_keys[index], 0); + } else { + gtk_widget_add_css_class(name, "unit-name-locked"); + } + on3_labels[index] = name; + gtk_fixed_put(fixed, name, 0, 0); +} +GtkWidget *build_on3map_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *wrap; + GtkWidget *fixed; + GtkWidget *rail; + GtkAdjustment *ha; + GtkAdjustment *va; + + on3_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_year3", "on3_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); + on3_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, on3_cw, on3_ch); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); + on3_fixed = fixed; + + rail = gtk_drawing_area_new(); + gtk_widget_set_size_request(rail, on3_cw, on3_ch); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), on3_draw_rail, + NULL, NULL); + gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0); + on3_rail = rail; + + for (int i = 0; i < ON3_UNITS; i++) + on3_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(on3_adjust_notify), NULL); + g_signal_connect(va, "notify::page-size", + G_CALLBACK(on3_adjust_notify), NULL); + + on3_apply_layout(); + on3_relayout_later(); + refresh_on3_completion_ui(); + + return page; +} + +static void on3_slide_apply(NetLesson *L); +static void on3_slide_prev(GtkButton *button, gpointer data); +static void on3_slide_next(GtkButton *button, gpointer data); + +static int on3_note_last_w = -1; + +static void on3_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 < ON3_LESSONS; i++) + net_rescale_lesson_notes(&on3_lessons[i], body, head, kick); +} + +static gboolean on3_note_tick(GtkWidget *w, GdkFrameClock *clock, + gpointer data) { + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + (void)data; + if (width != on3_note_last_w) { + on3_note_last_w = width; + on3_rescale_notes_at(width); + } + return G_SOURCE_CONTINUE; +} + +static GtkWidget *build_on3_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("on3map", 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), "on3_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, on3_note_tick, NULL, NULL); + on3_note_last_w = -1; + on3_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(on3_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(on3_slide_next), L); + gtk_box_append(GTK_BOX(nav), L->next_btn); + + L->idx = 0; + on3_slide_apply(L); + return page; +} + +static void on3_slide_apply(NetLesson *L) { + char name[16]; + + if (!L || !L->stack) + return; + g_snprintf(name, sizeof(name), "on3_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 on3_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + (void)button; + if (!L) return; + if (L->idx > 0) { + L->idx--; + on3_slide_apply(L); + } +} + +static void on3_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + (void)button; + if (!L) return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + on3_slide_apply(L); + } else { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +void on3_lessons_apply_lang(void) { + for (int i = 0; i < ON3_LESSONS; i++) + on3_slide_apply(&on3_lessons[i]); +} + +static void on3_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_on3_done(ctx->lesson_id); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +static GtkWidget *build_on3_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(on3_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_on3_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Právo", "Pravidla, která stát vynutí", + "Tip: ne každé neslušné jednání je nezákonné.", + { + "Právo je soubor pravidel, která může stát vynutit.", + "Morálka říká, co je slušné. Právo říká, co je závazné.", + "Obojí se často kryje, ale není to totéž.", + "Bez vynutitelného práva by rozhodovala síla, ne pravidlo.", + NULL, + }, + }, + { + "2 / 2 • Spravedlnost", "Podobné případy podobně", + "Tip: spravedlnost je cíl práva, ne jeho automatický výsledek.", + { + "Spravedlnost chce, aby se podobné případy posuzovaly podobně.", + "Právo má chránit slabšího před svévolí silnějšího.", + "Sankce následuje, když někdo pravidlo poruší.", + "Třetí ročník bere právní řád, soudy, smlouvy, rodinu, práci a tresty.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[0], "on3_unit1", "on3_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je právo?", + { + "Jen dobré mravy", + "Soubor pravidel, která může stát vynutit", + "Názor většiny na sociálních sítích", + "Zvyk jedné rodiny", + }, + 4, 1}, + {"Je morálka totéž co právo?", + { + "Ano", + "Ne, často se kryjí, ale nejsou totéž", + "Morálka je vždy přísnější zákon", + "Právo je jen náboženství", + }, + 4, 1}, + {"K čemu spravedlnost v právu směřuje?", + { + "Aby vyhrál hlasitější", + "Aby se podobné případy posuzovaly podobně", + "Aby trest byl vždy nejvyšší", + "Aby soudil ten, kdo je u moci", + }, + 4, 1}, + {"Co by bez vynutitelného práva rozhodovalo?", + { + "Síla", + "Los", + "Školní řád", + "Anketa", + }, + 4, 0}, + }; + static const char *hints[] = { + "Právo je vynutitelné státní mocí", + "Slušnost a zákon se nemusí krýt", + "Stejné se má měřit stejným metrem", + "Právo má nahradit právo silnějšího", + }; + return build_on3_mcq_page(0, "on3unit1", "on3_ex1_title", + "on3_quiz1_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Vázanost", "I stát má meze", + "Tip: úřad smí jen to, co mu dovoluje zákon.", + { + "V právním státě je právem vázán i stát, ne jen občan.", + "Úřad smí zasahovat do práv člověka jen tam, kde mu to zákon dovolí.", + "Moc se dělí, aby ji nikdo neužíval bez kontroly.", + "Soudy mají být nezávislé na vládě a na politicích.", + NULL, + }, + }, + { + "2 / 2 • Ochrana", "Kam se člověk obrátí", + "Tip: právní stát stojí na tom, že se práva dá dovolat.", + { + "Člověk se může domáhat ochrany u nezávislého soudu.", + "Rozhodnutí úřadu má být přezkoumatelné.", + "Nikdo nesmí být trestán bez zákona, který čin už předtím zakazoval.", + "Právní stát není stát bez pravidel. Je to stát, který svá pravidla sám dodržuje.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[1], "on3_unit2", "on3_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo je v právním státě vázán právem?", + { + "Jen občané", + "I stát", + "Jen cizinci", + "Jen firmy", + }, + 4, 1}, + {"Kdy smí úřad zasáhnout do práv člověka?", + { + "Kdykoli se mu to hodí", + "Jen když mu to dovoluje zákon", + "Když o to požádá soused", + "Když to schválí starosta ústně", + }, + 4, 1}, + {"Jakou roli mají soudy?", + { + "Plnit pokyny ministra", + "Rozhodovat nezávisle", + "Psát zákony místo Parlamentu", + "Vybírat daně", + }, + 4, 1}, + {"Lze trestat čin, který v době spáchání zákon nezakazoval?", + { + "Ano, zpětně", + "Ne", + "Ano, pokud šlo o přestupek školy", + "Ano, rozhodnutím vlády", + }, + 4, 1}, + }; + static const char *hints[] = { + "Právo váže i veřejnou moc", + "Veřejná moc jedná podle zákona", + "Nezávislost soudů je součást právního státu", + "Nullum crimen sine lege", + }; + return build_on3_mcq_page(1, "on3unit2", "on3_ex2_title", + "on3_quiz2_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Pyramida", "Co stojí nad čím", + "Tip: nižší předpis nesmí odporovat vyššímu.", + { + "Nejvýš stojí ústavní zákony, včetně Ústavy a Listiny.", + "Pod nimi jsou zákony, pak nařízení vlády a vyhlášky ministerstev.", + "Obec a kraj mohou vydávat vyhlášky jen v mezích zákona.", + "Platné předpisy se vyhlašují ve Sbírce zákonů.", + NULL, + }, + }, + { + "2 / 2 • Odvětví", "Veřejné a soukromé právo", + "Tip: trestní právo je veřejné, kupní smlouva je soukromé.", + { + "Veřejné právo chrání zájem celku. Patří sem třeba trestní a správní právo.", + "Soukromé právo upravuje vztahy mezi lidmi. Patří sem občanské, rodinné a pracovní právo.", + "Když si předpisy odporují, použije se ten vyšší.", + "Neznalost zákona neomlouvá, proto se předpisy zveřejňují.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[2], "on3_unit3", "on3_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co stojí v právním řádu nejvýš?", + { + "Obecní vyhláška", + "Ústavní zákony", + "Nařízení vlády", + "Vnitřní pokyn úřadu", + }, + 4, 1}, + {"Smí vyhláška ministerstva zrušit zákon?", + { + "Ano", + "Ne, nižší předpis nesmí odporovat vyššímu", + "Ano, pokud ji podepíše ministr", + "Ano, na jeden rok", + }, + 4, 1}, + {"Kam patří trestní právo?", + { + "Do soukromého práva", + "Do veřejného práva", + "Mimo právní řád", + "Jen do školního řádu", + }, + 4, 1}, + {"Kde se právní předpisy vyhlašují?", + { + "Ve Sbírce zákonů", + "Jen na nástěnce obce", + "Jen v televizi", + "V notářském zápisu", + }, + 4, 0}, + }; + static const char *hints[] = { + "Ústava a Listina jsou nad zákony", + "Pyramida předpisů se nepřeskakuje", + "Trestní právo chrání společnost", + "Bez vyhlášení předpis nezavazuje", + }; + return build_on3_mcq_page(2, "on3unit3", "on3_ex3_title", + "on3_quiz3_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Osoby", "Kdo může mít práva", + "Tip: právnická osoba není člověk, ale právo s ní jedná.", + { + "Fyzická osoba je člověk.", + "Právnická osoba je třeba firma, spolek, obec nebo stát.", + "Právní osobnost má člověk od narození: může mít práva a povinnosti.", + "Právní vztah spojuje subjekty právy a povinnostmi.", + NULL, + }, + }, + { + "2 / 2 • Věk", "Svéprávnost a trestní odpovědnost", + "Tip: patnáct let ještě není plná svéprávnost.", + { + "Svéprávnost je způsobilost sám právně jednat.", + "Plná svéprávnost přichází zletilostí v 18 letech.", + "Soud může svéprávnost z vážných důvodů omezit.", + "Trestně odpovědný je člověk od 15 let, je-li příčetný.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[3], "on3_unit4", "on3_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je fyzická osoba?", + { + "Firma", + "Člověk", + "Úřad", + "Smlouva", + }, + 4, 1}, + {"Od kdy má člověk právní osobnost?", + { + "Od 18 let", + "Od narození", + "Od 15 let", + "Od první smlouvy", + }, + 4, 1}, + {"Kdy nastává plná svéprávnost?", + { + "V 15 letech", + "Zletilostí v 18 letech", + "Nástupem do práce", + "Zápisem do občanského průkazu", + }, + 4, 1}, + {"Od kolika let je člověk trestně odpovědný?", + { + "Od 18 let", + "Od 15 let, je-li příčetný", + "Od 10 let", + "Až od první výplaty", + }, + 4, 1}, + }; + static const char *hints[] = { + "Fyzická osoba je člověk", + "Práva může mít i dítě", + "Svéprávnost je způsobilost jednat", + "Pod 15 let trestní odpovědnost není", + }; + return build_on3_mcq_page(3, "on3unit4", "on3_ex4_title", + "on3_quiz4_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Věc", "Co vlastník smí", + "Tip: vlastnictví není bez mezí.", + { + "Vlastník věc drží, užívá a může s ní nakládat.", + "Zákon ho omezuje, třeba kvůli sousedům.", + "Vyvlastnit lze jen ve veřejném zájmu, na základě zákona a za náhradu.", + "Duševní vlastnictví chrání třeba autorské dílo nebo ochrannou známku.", + NULL, + }, + }, + { + "2 / 2 • Škoda", "Kdo ji nahradí", + "Tip: cizí dílo se nesmí jen tak zkopírovat.", + { + "Kdo způsobí škodu, má ji zpravidla nahradit.", + "Škoda může být na věci, na zdraví i v penězích, o které člověk přišel.", + "Náhrada má uvést stav co nejblíž tomu, jaký byl před škodou.", + "U autora se bere i to, že dílo někdo užije bez svolení.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[4], "on3_unit5", "on3_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co vlastník se svou věcí smí?", + { + "Jen se na ni dívat", + "Držet ji, užívat ji a nakládat s ní", + "Jen ji půjčovat státu", + "Nic, dokud ji nepojistí", + }, + 4, 1}, + {"Lze v České republice vyvlastnit bez náhrady?", + { + "Ano, kdykoli", + "Ne, jen ve veřejném zájmu, podle zákona a za náhradu", + "Ano, rozhodnutím souseda", + "Ano, pokud jde o auto", + }, + 4, 1}, + {"Co chrání duševní vlastnictví?", + { + "Jen pozemky", + "Třeba autorské dílo", + "Jen hotovost", + "Jen nemovitosti v katastru", + }, + 4, 1}, + {"Kdo má zpravidla nahradit způsobenou škodu?", + { + "Stát vždy", + "Ten, kdo ji způsobil", + "Poškozený sám", + "Soused", + }, + 4, 1}, + }; + static const char *hints[] = { + "Vlastnictví zahrnuje držbu, užívání i nakládání", + "Vyvlastnění má přísné podmínky", + "Dílo není totéž co hmotná věc", + "Škůdce škodu nahrazuje", + }; + return build_on3_mcq_page(4, "on3unit5", "on3_ex5_title", + "on3_quiz5_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Vůle", "Smlouva zavazuje", + "Tip: ústní smlouva může platit, ale hůř se dokazuje.", + { + "Smlouva je souhlasný projev vůle dvou nebo více stran.", + "Strany si sjednají, kdo, co a za jakých podmínek plní.", + "Platná smlouva zavazuje. Nelze od ní odejít jen proto, že se to přestalo hodit.", + "Zákon u některých smluv žádá písemnou formu.", + NULL, + }, + }, + { + "2 / 2 • Druhy", "Koupě, nájem, dar a dílo", + "Tip: dům se nepřevede jen podáním ruky.", + { + "Kupní smlouvou přechází věc za kupní cenu.", + "Nájem přenechává věc k dočasnému užívání za nájemné.", + "Darování je bezúplatné. Smlouvou o dílo se zhotoví, opraví nebo upraví věc.", + "Vlastnictví k nemovitosti se zapisuje do katastru.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[5], "on3_unit6", "on3_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je smlouva?", + { + "Jednostranný rozkaz úřadu", + "Souhlasný projev vůle, který strany zavazuje", + "Reklama", + "Ústní slib bez obsahu", + }, + 4, 1}, + {"Lze od každé smlouvy odejít, když se to přestane hodit?", + { + "Ano", + "Ne, platná smlouva zavazuje", + "Ano, do hodiny", + "Ano, pokud druhá strana nesouhlasí", + }, + 4, 1}, + {"Co je nájem?", + { + "Darování věci", + "Dočasné užívání věci za nájemné", + "Trest za škodu", + "Převod nemovitosti darem", + }, + 4, 1}, + {"Kam se zapisuje vlastnictví k nemovitosti?", + { + "Do občanského průkazu", + "Do katastru nemovitostí", + "Jen do smlouvy v šuplíku", + "Na finanční úřad", + }, + 4, 1}, + }; + static const char *hints[] = { + "Smlouva stojí na shodě stran", + "Závazek se plní", + "Nájem není převod vlastnictví", + "U nemovitosti rozhoduje vklad do katastru", + }; + return build_on3_mcq_page(5, "on3unit6", "on3_ex6_title", + "on3_quiz6_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Ochrana", "Slabší strana smlouvy", + "Tip: spotřebitel je člověk, který nejedná jako podnikatel.", + { + "Spotřebitel uzavírá smlouvu s podnikatelem mimo své podnikání.", + "Zákon ho chrání, protože podnikatel zná podmínky líp.", + "Nekalé obchodní praktiky a klamavá reklama jsou zakázané.", + "Smlouva nemá schovávat podstatné podmínky do nečitelného textu.", + NULL, + }, + }, + { + "2 / 2 • Reklamace", "Vada a čtrnáct dnů", + "Tip: účtenka pomáhá, ale není jediný důkaz koupě.", + { + "Vadu zboží nebo služby lze reklamovat u prodávajícího.", + "Smlouvu sjednanou po internetu nebo po telefonu lze často zrušit do 14 dnů.", + "Výjimkou je třeba zboží vyrobené na míru nebo rychle se kazící věc.", + "Když obchodník neplní, pomáhá Česká obchodní inspekce.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[6], "on3_unit7", "on3_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo je spotřebitel?", + { + "Každá firma", + "Člověk, který s podnikatelem nejedná ve svém podnikání", + "Jen nezletilý", + "Jen vlastník obchodu", + }, + 4, 1}, + {"Co je reklamace?", + { + "Darování věci", + "Uplatnění vady zboží nebo služby", + "Výpověď z práce", + "Žaloba na souseda", + }, + 4, 1}, + {"Dokdy lze často odstoupit od smlouvy uzavřené po internetu?", + { + "Do 14 dnů", + "Do druhého dne", + "Do jednoho roku vždy", + "Nelze nikdy", + }, + 4, 0}, + {"Kdo pomáhá, když obchodník spotřebiteli neplní?", + { + "Česká obchodní inspekce", + "Jen školní parlament", + "Katastr nemovitostí", + "Česká národní banka u každého nákupu", + }, + 4, 0}, + }; + static const char *hints[] = { + "Spotřebitel je slabší strana", + "Reklamace se obrací na prodávajícího", + "U smluv na dálku běží čtrnáctidenní lhůta", + "ČOI dohlíží na ochranu spotřebitele", + }; + return build_on3_mcq_page(6, "on3unit7", "on3_ex7_title", + "on3_quiz7_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Nabytí", "Zákon, nebo závěť", + "Tip: dědictví lze odmítnout.", + { + "Dědí se podle závěti, a kde závěť není, podle zákona.", + "Ze zákona dědí především děti, manžel nebo partner a další blízcí.", + "Pozůstalost projednává notář jako soudní komisař.", + "Dědic může dědictví odmítnout. Odmítnutí se nedá jen tak vzít zpět.", + NULL, + }, + }, + { + "2 / 2 • Dluhy", "Soupis pozůstalosti", + "Tip: dědictví nejsou jen věci, ale i dluhy.", + { + "S majetkem mohou přejít i dluhy zůstavitele.", + "Bez soupisu může dědic ručit i nad hodnotu toho, co nabyl.", + "Požádá-li o soupis pozůstalosti, hradí dluhy jen do výše nabytého majetku.", + "Proto se před přijetím vyplatí vědět, co v pozůstalosti je.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[7], "on3_unit8", "on3_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Podle čeho se dědí, když není závěť?", + { + "Podle losu", + "Podle zákona", + "Podle přání sousedů", + "Majetek propadá vždy obci", + }, + 4, 1}, + {"Kdo projednává pozůstalost?", + { + "Notář", + "Starosta", + "Zaměstnavatel", + "Škola", + }, + 4, 0}, + {"Lze dědictví odmítnout?", + { + "Ne", + "Ano", + "Jen nemovitost", + "Jen se souhlasem věřitele", + }, + 4, 1}, + {"K čemu je soupis pozůstalosti?", + { + "Aby se dluhy hradily jen do výše nabytého majetku", + "Aby dědic přišel o všechno", + "Aby se závěť zrušila", + "Aby se dědictví nedalo odmítnout", + }, + 4, 0}, + }; + static const char *hints[] = { + "Zákon určuje okruh dědiců", + "Notář jedná jako soudní komisař", + "Odmítnutí je právo dědice", + "Soupis omezuje rozsah dluhů", + }; + return build_on3_mcq_page(7, "on3unit8", "on3_ex8_title", + "on3_quiz8_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit9_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Svazek", "Manželství a partnerství", + "Tip: partnerství z roku 2025 není totéž co staré registrované partnerství.", + { + "Manželství je trvalý svazek muže a ženy.", + "Uzavírá se svobodně, zpravidla od 18 let. Výjimečně od 16 let se souhlasem soudu.", + "Dva lidé stejného pohlaví mohou od roku 2025 uzavřít partnerství.", + "Partnerství se v řadě práv podobá manželství. Dřívější registrovaná partnerství zůstávají v platnosti.", + NULL, + }, + }, + { + "2 / 2 • Dítě", "Péče, výživa a náhradní výchova", + "Tip: pěstoun není totéž co osvojitel.", + { + "Rodiče mají o dítě pečovat, zastupovat je a živit je.", + "Dítě má právo být slyšeno ve věcech, které se ho týkají.", + "Osvojením se dítě přijímá za vlastní. Pěstoun o něj pečuje, když nemůže být u rodičů.", + "Poručník dítě zastupuje, když to rodiče nemohou.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[8], "on3_unit9", "on3_unit9_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit9_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je manželství podle občanského zákoníku?", + { + "Jakýkoli společný byt", + "Trvalý svazek muže a ženy", + "Pracovní smlouva", + "Smlouva o nájmu", + }, + 4, 1}, + {"Co mohou od roku 2025 uzavřít dva lidé stejného pohlaví?", + { + "Nic", + "Partnerství", + "Jen pracovní poměr", + "Už jen registrované partnerství jako dřív", + }, + 4, 1}, + {"Od kolika let lze zpravidla uzavřít manželství?", + { + "Od 15 let", + "Od 18 let", + "Od 21 let", + "Od 16 let bez další podmínky", + }, + 4, 1}, + {"Čím se liší pěstoun od osvojitele?", + { + "Pěstoun o dítě pečuje, osvojení je přijímá za vlastní", + "Ničím", + "Pěstoun je vždy poručník soudu", + "Osvojitel jen platí nájem", + }, + 4, 0}, + }; + static const char *hints[] = { + "Manželství je svazek muže a ženy", + "Nové registrované partnerství se už neuzavírá", + "Šestnáct let jen výjimečně se souhlasem soudu", + "Osvojení mění právní vztah rodiče a dítěte", + }; + return build_on3_mcq_page(8, "on3unit9", "on3_ex9_title", + "on3_quiz9_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit10_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Smlouva", "Vznik pracovního poměru", + "Tip: zkušební doba musí být písemná.", + { + "Pracovní smlouva určí druh práce, místo výkonu a den nástupu.", + "Uzavírá se písemně.", + "Zkušební doba smí trvat nejvýše 4 měsíce, u vedoucího zaměstnance 8 měsíců.", + "Ve zkušební době lze poměr zrušit i bez důvodu, ne však v prvních 14 dnech neschopnosti.", + NULL, + }, + }, + { + "2 / 2 • Konec", "Výpověď a bezpečná práce", + "Tip: zaměstnavatel si důvod výpovědi nesmí vymyslet.", + { + "Zaměstnanec může dát výpověď z jakéhokoli důvodu.", + "Zaměstnavatel jen ze zákonných důvodů, třeba organizačních nebo pro porušení povinností.", + "Výpovědní doba trvá nejméně dva měsíce a začíná dnem doručení.", + "Zaměstnanec má právo na mzdu, odpočinek, dovolenou a bezpečnou práci.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[9], "on3_unit10", "on3_unit10_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit10_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co musí pracovní smlouva určit?", + { + "Jen výši prémií", + "Druh práce, místo a den nástupu", + "Jméno manžela", + "Číslo občanského průkazu rodičů", + }, + 4, 1}, + {"Jak dlouhá smí být zkušební doba řadového zaměstnance?", + { + "Nejvýše 4 měsíce", + "Nejvýše jeden rok", + "Bez omezení", + "Jen jeden týden", + }, + 4, 0}, + {"Kdy smí zaměstnavatel dát výpověď?", + { + "Z jakéhokoli důvodu", + "Jen ze zákonných důvodů", + "Jen ústně na chodbě", + "Nikdy", + }, + 4, 1}, + {"Jak dlouhá je základní výpovědní doba?", + { + "Jeden týden", + "Nejméně dva měsíce", + "Půl roku vždy", + "Do konce směny", + }, + 4, 1}, + }; + static const char *hints[] = { + "To jsou podstatné náležitosti", + "U vedoucího je mez osm měsíců", + "Důvody výpovědi stanoví zákoník práce", + "Běží ode dne doručení výpovědi", + }; + return build_on3_mcq_page(9, "on3unit10", "on3_ex10_title", + "on3_quiz10_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit11_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Stupně", "Od okresního soudu výš", + "Tip: Ústavní soud není další odvolací stupeň.", + { + "Nejblíž lidem jsou okresní soudy. V Praze se jim říká obvodní.", + "Nad nimi jsou krajské soudy. V Praze je to Městský soud.", + "Vrchní soudy sídlí v Praze a v Olomouci.", + "Nejvyšší soud sjednocuje rozhodování obecných soudů.", + NULL, + }, + }, + { + "2 / 2 • Mimo řadu", "Správní a ústavní soudnictví", + "Tip: každý soudce je nezávislý.", + { + "Žaloby proti rozhodnutím úřadů řeší správní soudy.", + "Na vrcholu správního soudnictví je Nejvyšší správní soud.", + "Ústavní soud stojí mimo tuto soustavu a hlídá ústavní pořádek.", + "Soudce není podřízený ministrovi ani politikovi.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[10], "on3_unit11", "on3_unit11_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit11_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jak se jmenuje první stupeň soudů v Praze?", + { + "Krajský soud", + "Obvodní soudy", + "Vrchní soud", + "Ústavní soud", + }, + 4, 1}, + {"Kde sídlí vrchní soudy?", + { + "V Brně a Ostravě", + "V Praze a Olomouci", + "Jen v Praze", + "V každém kraji", + }, + 4, 1}, + {"Kdo završuje správní soudnictví?", + { + "Nejvyšší správní soud", + "Obecní úřad", + "Poslanecká sněmovna", + "Notářská komora", + }, + 4, 0}, + {"Je Ústavní soud běžný odvolací soud?", + { + "Ano, je nad každým rozsudkem automaticky", + "Ne, stojí mimo soustavu obecných soudů", + "Ano, nahrazuje okresní soud", + "Ano, rozhoduje každou pokutu", + }, + 4, 1}, + }; + static const char *hints[] = { + "Mimo Prahu jsou to okresní soudy", + "Vrchní soudy jsou dva", + "Správní soudy přezkoumávají úřady", + "Ústavní soud hlídá ústavní pořádek", + }; + return build_on3_mcq_page(10, "on3unit11", "on3_ex11_title", + "on3_quiz11_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit12_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Žaloba", "Občanské soudní řízení", + "Tip: kdo žaluje, je žalobce. Koho žaluje, je žalovaný.", + { + "Občanské soudní řízení začíná zpravidla žalobou.", + "Žalobce se domáhá svého práva proti žalovanému.", + "Soud vyslechne obě strany a rozhodne rozsudkem, nebo řízení jinak skončí.", + "Proti rozsudku je odvolání. Dovolání k Nejvyššímu soudu je výjimečné.", + NULL, + }, + }, + { + "2 / 2 • Úřad", "Správní řízení", + "Tip: odvolání je řádný opravný prostředek.", + { + "Správní řízení vede úřad, třeba když vydává povolení nebo ukládá povinnost.", + "Řídí se správním řádem.", + "Proti rozhodnutí se lze odvolat k nadřízenému úřadu.", + "Když to nepomůže, zbývá žaloba ke správnímu soudu.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[11], "on3_unit12", "on3_unit12_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit12_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo je žalobce?", + { + "Soudce", + "Ten, kdo se žalobou domáhá svého práva", + "Svědek", + "Každý, kdo sedí v síni", + }, + 4, 1}, + {"Čím občanské soudní řízení zpravidla začíná?", + { + "Výpovědí", + "Žalobou", + "Závětí", + "Reklamací v obchodě", + }, + 4, 1}, + {"Co je odvolání?", + { + "Nová pracovní smlouva", + "Řádný opravný prostředek proti rozhodnutí", + "Odmítnutí dědictví", + "Stížnost na počasí", + }, + 4, 1}, + {"Kam se lze obrátit, když neuspěje odvolání proti úřadu?", + { + "Ke správnímu soudu", + "Do katastru", + "K zaměstnavateli", + "Na obecnou nástěnku", + }, + 4, 0}, + }; + static const char *hints[] = { + "Žalovaný je ten, proti komu žaloba míří", + "Žaloba otevírá spor", + "Odvolání přezkoumá vyšší stupeň", + "Správní soud přezkoumává rozhodnutí úřadu", + }; + return build_on3_mcq_page(11, "on3unit12", "on3_ex12_title", + "on3_quiz12_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit13_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Soud a žaloba", "Kdo rozhoduje a kdo žaluje", + "Tip: advokát není soudce.", + { + "Soudce je nezávislý, nestranný a spor rozhoduje.", + "Státní zástupce podává obžalobu a ve veřejném zájmu hlídá zákonnost trestního řízení.", + "Advokát zastupuje klienta a váže ho povinnost mlčenlivosti.", + "Kdo si advokáta nemůže dovolit, může v zákonem daných případech dostat právní pomoc.", + NULL, + }, + }, + { + "2 / 2 • Listiny", "Notář a exekutor", + "Tip: exekutor nevymýšlí dluh, vymáhá už uloženou povinnost.", + { + "Notář sepisuje veřejné listiny a projednává dědictví.", + "Veřejná listina má silnější důkazní váhu než obyčejné potvrzení.", + "Exekutor vymáhá splnění rozhodnutí, když je dlužník sám neplní.", + "Veřejný ochránce práv není soudce a rozsudky neruší.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[12], "on3_unit13", "on3_unit13_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit13_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo v trestním řízení podává obžalobu?", + { + "Advokát obžalovaného", + "Státní zástupce", + "Notář", + "Starosta", + }, + 4, 1}, + {"Čím je vázán advokát?", + { + "Pokyny protistrany", + "Povinností mlčenlivosti vůči klientovi", + "Příkazem ministra v každém sporu", + "Ničím", + }, + 4, 1}, + {"Kdo projednává dědictví?", + { + "Exekutor", + "Notář", + "Policie vždy", + "Zaměstnavatel", + }, + 4, 1}, + {"Co dělá exekutor?", + { + "Ruší zákony", + "Vymáhá splnění uložené povinnosti", + "Uzavírá manželství", + "Rozhoduje o vině v trestním řízení", + }, + 4, 1}, + }; + static const char *hints[] = { + "Státní zástupce žaluje ve veřejném zájmu", + "Advokát hájí klienta, ne úřad", + "Notář je soudní komisař", + "Exekuce není nový soud o vině", + }; + return build_on3_mcq_page(12, "on3unit13", "on3_ex13_title", + "on3_quiz13_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit14_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Rozdíl", "Závažnost rozhoduje", + "Tip: přestupek není malý trestný čin.", + { + "Trestný čin je závažný protiprávní čin, který tak označuje trestní zákoník.", + "Přestupek je mírnější protiprávní čin a trestným činem není.", + "Rušení nočního klidu bývá přestupek. Úmyslné těžké ublížení na zdraví je trestný čin.", + "Za každý z nich odpovídá jiný zákon a jiné řízení.", + NULL, + }, + }, + { + "2 / 2 • Vina", "Věk, příčetnost a presumpce", + "Tip: obviněný není odsouzený.", + { + "Trestní odpovědnost vyžaduje věk aspoň 15 let, příčetnost a zavinění.", + "Bez zavinění, tedy úmyslu nebo nedbalosti, o trestný čin nejde.", + "Dokud soud pravomocně nerozhodne, hledí se na člověka jako na nevinného.", + "Tomu se říká presumpce neviny.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[13], "on3_unit14", "on3_unit14_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit14_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Čím se trestný čin liší od přestupku?", + { + "Ničím", + "Trestný čin je závažnější a popisuje ho trestní zákoník", + "Přestupek řeší jen Ústavní soud", + "Trestný čin nemá oběť", + }, + 4, 1}, + {"Kam spíš patří rušení nočního klidu?", + { + "Mezi přestupky", + "Vždy mezi zločiny", + "Mimo právo", + "Jen do pracovní smlouvy", + }, + 4, 0}, + {"Co presumpce neviny znamená?", + { + "Policie nesmí nikoho vyslechnout", + "Dokud soud nerozhodne, hledí se na člověka jako na nevinného", + "Trest se ukládá před soudem", + "Obviněný se nesmí hájit", + }, + 4, 1}, + {"Stačí ke trestnému činu samotný následek bez zavinění?", + { + "Ano", + "Ne, trestní právo vyžaduje zavinění", + "Ano u každého přestupku školy", + "Ano, pokud o tom napíší noviny", + }, + 4, 1}, + }; + static const char *hints[] = { + "Závažnost a zákon, který čin upravuje", + "Jde o mírnější protiprávní čin", + "Vinu vyslovuje až soud", + "Úmysl nebo nedbalost jsou podmínkou", + }; + return build_on3_mcq_page(13, "on3unit14", "on3_ex14_title", + "on3_quiz14_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit15_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Trest", "Ne jen vězení", + "Tip: trest smrti se v České republice neukládá.", + { + "Trest má chránit společnost a vést pachatele k nápravě, ne být pomstou.", + "Vedle odnětí svobody je třeba podmíněné odsouzení, obecně prospěšné práce nebo peněžitý trest.", + "Patří sem i domácí vězení nebo zákaz činnosti.", + "Tomuto posunu od pouhého zavírání se říká humanizace trestání.", + NULL, + }, + }, + { + "2 / 2 • Řízení", "Policie, žalobce, soud", + "Tip: obhájce má obviněný právo mít.", + { + "Policie věc prověřuje a vyšetřuje.", + "Státní zástupce rozhoduje, zda podá obžalobu.", + "O vině a trestu rozhoduje soud.", + "Obviněný se může hájit a v zákonem daných případech musí mít obhájce.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[14], "on3_unit15", "on3_unit15_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit15_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jaký má být smysl trestu?", + { + "Pomsta", + "Ochrana společnosti a náprava pachatele", + "Zisk pro soudce", + "Veřejné ponížení", + }, + 4, 1}, + {"Ukládá Česká republika trest smrti?", + { + "Ano", + "Ne", + "Ano u mladistvých", + "Ano, pokud o to oběť požádá", + }, + 4, 1}, + {"Kdo rozhoduje o vině a trestu?", + { + "Policie", + "Soud", + "Noviny", + "Poškozený sám", + }, + 4, 1}, + {"Co je podmíněné odsouzení?", + { + "Trest smrti s odkladem", + "Odklad výkonu trestu, pokud se odsouzený osvědčí", + "Zproštění obžaloby", + "Přestupek", + }, + 4, 1}, + }; + static const char *hints[] = { + "Humanizace odmítá pomstu jako účel", + "Trest smrti byl zrušen", + "Policie vyšetřuje, soud rozhoduje", + "Je to alternativa k nástupu do vězení", + }; + return build_on3_mcq_page(14, "on3unit15", "on3_ex15_title", + "on3_quiz15_head", qs, hints, 4); +} +static GtkWidget *build_on3_unit16_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Věk", "Děti a mladiství", + "Tip: patnáct let je hranice trestní odpovědnosti, ne dospělosti.", + { + "Dítě mladší 15 let není trestně odpovědné.", + "Mladistvý je člověk od 15 do 18 let.", + "U mladistvého zákon hledí i na výchovu, ne jen na potrestání.", + "I čin dítěte pod 15 let se řeší, třeba opatřeními na ochranu dítěte a okolí.", + NULL, + }, + }, + { + "2 / 2 • Pomoc", "Když se stanete obětí nebo svědkem", + "Tip: linky 158 a 112 jsou pro přivolání pomoci.", + { + "Šikanu, násilí, vydírání, lichvu nebo korupci si nenechávejte pro sebe.", + "Řekněte to důvěryhodnému dospělému, škole nebo policii.", + "Při ohrožení odejděte do bezpečí a zavolejte 158 nebo 112. Do střetu se nepouštějte.", + "Svědek má mluvit pravdu. Lichva je půjčka, která zneužívá cizí tísně, a patří na policii, ne do další půjčky.", + NULL, + }, + }, + }; + + return build_on3_unit_page(&on3_lessons[15], "on3_unit16", "on3_unit16_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_on3_unit16_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Je dítě mladší 15 let trestně odpovědné?", + { + "Ano, jako dospělý", + "Ne", + "Ano, od 10 let", + "Jen když chodí do školy", + }, + 4, 1}, + {"Kdo je mladistvý?", + { + "Každý student", + "Člověk od 15 do 18 let", + "Člověk do 15 let", + "Jen pachatel přestupku", + }, + 4, 1}, + {"Co udělat při šikaně nebo vydírání?", + { + "Nechat si to pro sebe", + "Říct dospělému, škole nebo policii", + "Vrátit to stejnou silou", + "Půjčit si peníze na vyplacení", + }, + 4, 1}, + {"Co má udělat svědek u policie?", + { + "Mlčet", + "Mluvit pravdu", + "Domyslet si, co policie chce slyšet", + "Oznámit jen to, co se hodí kamarádovi", + }, + 4, 1}, + }; + static const char *hints[] = { + "Trestní odpovědnost začíná v 15 letech", + "Mladistvý už je trestně odpovědný, ale jinak než dospělý", + "Pomoc je mimo tu situaci", + "Křivá výpověď věc zhoršuje", + }; + return build_on3_mcq_page(15, "on3unit16", "on3_ex16_title", + "on3_quiz16_head", qs, hints, 4); +} + +void add_on3_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[] = { + {"on3unit1", "on3ex1", build_on3_unit1_page, build_on3_unit1_exercise_page}, + {"on3unit2", "on3ex2", build_on3_unit2_page, build_on3_unit2_exercise_page}, + {"on3unit3", "on3ex3", build_on3_unit3_page, build_on3_unit3_exercise_page}, + {"on3unit4", "on3ex4", build_on3_unit4_page, build_on3_unit4_exercise_page}, + {"on3unit5", "on3ex5", build_on3_unit5_page, build_on3_unit5_exercise_page}, + {"on3unit6", "on3ex6", build_on3_unit6_page, build_on3_unit6_exercise_page}, + {"on3unit7", "on3ex7", build_on3_unit7_page, build_on3_unit7_exercise_page}, + {"on3unit8", "on3ex8", build_on3_unit8_page, build_on3_unit8_exercise_page}, + {"on3unit9", "on3ex9", build_on3_unit9_page, build_on3_unit9_exercise_page}, + {"on3unit10", "on3ex10", build_on3_unit10_page, build_on3_unit10_exercise_page}, + {"on3unit11", "on3ex11", build_on3_unit11_page, build_on3_unit11_exercise_page}, + {"on3unit12", "on3ex12", build_on3_unit12_page, build_on3_unit12_exercise_page}, + {"on3unit13", "on3ex13", build_on3_unit13_page, build_on3_unit13_exercise_page}, + {"on3unit14", "on3ex14", build_on3_unit14_page, build_on3_unit14_exercise_page}, + {"on3unit15", "on3ex15", build_on3_unit15_page, build_on3_unit15_exercise_page}, + {"on3unit16", "on3ex16", build_on3_unit16_page, build_on3_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 7c3fef3..76343e2 100644 --- a/src/progress.c +++ b/src/progress.c @@ -56,6 +56,7 @@ void load_progress(void) { hw_load_progress(); on_load_progress(); on2_load_progress(); + on3_load_progress(); } /* Redraw the exercise rails of every unit that already has a page. */ @@ -425,8 +426,8 @@ void mark_on_done(int lesson_id) { } void progress_for_on(ProgressSum *out) { - out->total_ex += ON_LESSONS + ON2_LESSONS; - out->open_units += ON_LESSONS + ON2_LESSONS; + out->total_ex += ON_LESSONS + ON2_LESSONS + ON3_LESSONS; + out->open_units += ON_LESSONS + ON2_LESSONS + ON3_LESSONS; for (int i = 0; i < ON_LESSONS; i++) { if (on_lessons[i].done) { out->done_ex++; @@ -439,6 +440,12 @@ void progress_for_on(ProgressSum *out) { out->done_units++; } } + for (int i = 0; i < ON3_LESSONS; i++) { + if (on3_lessons[i].done) { + out->done_ex++; + out->done_units++; + } + } } void on2_load_progress(void) { @@ -520,3 +527,83 @@ void mark_on2_done(int lesson_id) { refresh_on2_completion_ui(); refresh_stats_ui(); } + +void on3_load_progress(void) { + GKeyFile *kf = g_key_file_new(); + GError *err = NULL; + + if (!g_key_file_load_from_file(kf, PROGRESS_ON3, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + + for (int i = 0; i < ON3_LESSONS; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + on3_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + + g_key_file_free(kf); +} + +void on3_save_progress(void) { + GKeyFile *kf = g_key_file_new(); + + for (int i = 0; i < ON3_LESSONS; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + g_key_file_set_boolean(kf, "done", key, on3_lessons[i].done); + } + + if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) { + g_key_file_free(kf); + return; + } + + gchar *data = g_key_file_to_data(kf, NULL, NULL); + g_key_file_free(kf); + + if (data) { + GError *err = NULL; + g_file_set_contents(PROGRESS_ON3, data, -1, &err); + if (err) + g_error_free(err); + g_free(data); + } +} + +void refresh_on3_completion_ui(void) { + for (int i = 0; i < ON3_LESSONS; i++) { + GtkWidget *node = on3_nodes[i]; + GtkWidget *icon = on3_lessons[i].done_icon; + + if (!node) + continue; + if (on3_lessons[i].done) { + gtk_widget_remove_css_class(node, "current"); + gtk_widget_add_css_class(node, "done"); + if (icon) + gtk_widget_set_visible(icon, TRUE); + } else { + gtk_widget_remove_css_class(node, "done"); + gtk_widget_add_css_class(node, "current"); + if (icon) + gtk_widget_set_visible(icon, FALSE); + } + } +} + +void mark_on3_done(int lesson_id) { + if (lesson_id < 0 || lesson_id >= ON3_LESSONS) + return; + if (on3_lessons[lesson_id].done) + return; + on3_lessons[lesson_id].done = TRUE; + on3_save_progress(); + refresh_on3_completion_ui(); + refresh_stats_ui(); +} diff --git a/src/search.c b/src/search.c index 3a4dcf1..17e6be3 100644 --- a/src/search.c +++ b/src/search.c @@ -204,7 +204,7 @@ static void catalog_rebuild(void) { catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik"); catalog_add_key("on_year2", "search_lesson", "on2map", FALSE, 1, "rocnik"); - catalog_add_key("on_year3", "search_lesson", "onyears", TRUE, 1, "rocnik"); + catalog_add_key("on_year3", "search_lesson", "on3map", FALSE, 1, "rocnik"); catalog_add_key("on_year4", "search_lesson", "onyears", TRUE, 1, "rocnik"); for (i = 1; i <= ON_LESSONS; i++) { g_snprintf(key, sizeof(key), "on_unit%d", i); @@ -220,6 +220,13 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "on2ex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + for (i = 1; i <= ON3_LESSONS; i++) { + g_snprintf(key, sizeof(key), "on3_unit%d", i); + g_snprintf(page, sizeof(page), "on3unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "obcanka"); + g_snprintf(page, sizeof(page), "on3ex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); + } catalog_add_key("Český jazyk a literatura", "search_subject", "czechmap", FALSE, 0, "cestina czech"); diff --git a/src/theme.c b/src/theme.c index ad3196f..b6e7f67 100644 --- a/src/theme.c +++ b/src/theme.c @@ -428,6 +428,7 @@ void apply_theme(void) { hw_rail_theme_reset(); on_rail_theme_reset(); on2_rail_theme_reset(); + on3_rail_theme_reset(); czech_rail_theme_reset(); book_rail_theme_reset(); } diff --git a/src/util.c b/src/util.c index bd4c9a3..49145d3 100644 --- a/src/util.c +++ b/src/util.c @@ -22,6 +22,7 @@ char *app_progress_net; char *app_progress_hw; char *app_progress_on; char *app_progress_on2; +char *app_progress_on3; char *app_progress_mluvnice; char *app_progress_cetba; @@ -162,6 +163,7 @@ static void set_progress_paths(const char *write_root) { g_free(app_progress_hw); g_free(app_progress_on); g_free(app_progress_on2); + g_free(app_progress_on3); g_free(app_progress_mluvnice); g_free(app_progress_cetba); @@ -174,6 +176,7 @@ static void set_progress_paths(const char *write_root) { app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL); app_progress_on = g_build_filename(app_progress_dir, "on.conf", NULL); app_progress_on2 = g_build_filename(app_progress_dir, "on2.conf", NULL); + app_progress_on3 = g_build_filename(app_progress_dir, "on3.conf", NULL); app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL); app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL); }