diff --git a/README.md b/README.md index 8f3550b..0e5304e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses (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); 3. ročník, 16 lekcí (člověk a právo); - 4. ročník je zamčený + 4. ročník, 16 lekcí (hospodářství, svět a filozofie) - 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` @@ -266,7 +266,8 @@ Intel Mac, another architecture, or hacking on the code → (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); - year 3, 16 lessons (the individual and the law); year 4 is locked + year 3, 16 lessons (the individual and the law); + year 4, 16 lessons (the economy, the world and philosophy) - 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 df1f226..ebab877 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -141,6 +141,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markOn4(id: Int) { + progress.markOn4(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 794337e..030ec78 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -39,6 +39,7 @@ 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.On4QuizScreen import org.maturita.maturita.ui.OnQuizScreen import org.maturita.maturita.ui.OnYearsScreen import org.maturita.maturita.ui.PlotScreen @@ -273,6 +274,20 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.On3Ex -> On3QuizScreen(vm, r.id) + Route.On4Map -> LessonListScreen( + vm, vm.tr("on_year4"), vm.tr("on4_sub"), + vm.content.on4.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.on4Done(id), Route.On4Lesson(id)) + }, + ) + is Route.On4Lesson -> { + val l = vm.content.on4Lesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.On4Ex(r.id)) + } + } + is Route.On4Ex -> On4QuizScreen(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 f5db2b9..41a6960 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -27,6 +27,9 @@ sealed class Route(val page: String) { 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 On4Map : Route("on4map") + data class On4Lesson(val id: Int) : Route("on4unit$id") + data class On4Ex(val id: Int) : Route("on4ex$id") data object CzechMap : Route("czechmap") data object Mluvnice : Route("mluvnice") data class MluvEx(val n: Int) : Route("mluve$n") @@ -41,8 +44,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 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 + this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is MluvEx || this is BookQuiz || this is BookPlot || + this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson || this is On4Lesson /** Full stack from the practice root to this screen, so back always goes up one level. */ fun stack(): List = when (this) { @@ -71,6 +74,9 @@ sealed class Route(val page: String) { On3Map -> listOf(Subjects, OnYears, On3Map) is On3Lesson -> listOf(Subjects, OnYears, On3Map, this) is On3Ex -> listOf(Subjects, OnYears, On3Map, On3Lesson(id), this) + On4Map -> listOf(Subjects, OnYears, On4Map) + is On4Lesson -> listOf(Subjects, OnYears, On4Map, this) + is On4Ex -> listOf(Subjects, OnYears, On4Map, On4Lesson(id), this) CzechMap -> listOf(Subjects, CzechMap) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) @@ -95,6 +101,7 @@ fun routeFromPage(page: String): Route? = when (page) { "onmap" -> Route.OnMap "on2map" -> Route.On2Map "on3map" -> Route.On3Map + "on4map" -> Route.On4Map "czechmap" -> Route.CzechMap "mluvnice" -> Route.Mluvnice "readinglist" -> Route.ReadingList @@ -126,6 +133,12 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""hwex(\d+)""").matchEntire(page)?.let { return Route.HwEx(it.groupValues[1].toInt()) } + Regex("""on4unit(\d+)""").matchEntire(page)?.let { + return Route.On4Lesson(it.groupValues[1].toInt()) + } + Regex("""on4ex(\d+)""").matchEntire(page)?.let { + return Route.On4Ex(it.groupValues[1].toInt()) + } Regex("""on3unit(\d+)""").matchEntire(page)?.let { return Route.On3Lesson(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 5f38820..c44b3c2 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 @@ -32,6 +32,7 @@ class Content(root: JSONObject) { val on = raw.arr("on") val on2 = raw.arr("on2") val on3 = raw.arr("on3") + val on4 = raw.arr("on4") val mluvnice = raw.arr("mluvnice") val books = raw.arr("books") val changelog = raw.arr("changelog") @@ -42,6 +43,7 @@ class Content(root: JSONObject) { 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 on4Lesson(id: Int) = on4.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 dae4b17..924ae6a 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 @@ -41,6 +41,9 @@ class ProgressStore(context: Context) { fun on3Done(id: Int) = prefs.getBoolean("on3.$id", false) fun markOn3(id: Int) { prefs.edit().putBoolean("on3.$id", true).apply() } + fun on4Done(id: Int) = prefs.getBoolean("on4.$id", false) + fun markOn4(id: Int) { prefs.edit().putBoolean("on4.$id", true).apply() } + fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } @@ -103,6 +106,14 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + content.on4.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.on4Done(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 f6c8a10..22ad84a 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 @@ -884,6 +884,18 @@ fun On3QuizScreen(vm: AppViewModel, id: Int) { DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn3(id) } } +@Composable +fun On4QuizScreen(vm: AppViewModel, id: Int) { + val lesson = vm.content.on4Lesson(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.markOn4(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 760cfca..25b5206 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,11 +300,12 @@ 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", "on3map" -> { - sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.size + "onyears", "onmap", "on2map", "on3map", "on4map" -> { + sum.totalEx = vm.content.on.size + vm.content.on2.size + vm.content.on3.size + vm.content.on4.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.on3.count { vm.progress.on3Done(it.int("id")) } + vm.content.on3.count { vm.progress.on3Done(it.int("id")) } + + vm.content.on4.count { vm.progress.on4Done(it.int("id")) } } "czechmap" -> { sum.totalEx = vm.content.mluvnice.size @@ -496,7 +497,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"), "on3map", extra = "rocnik") - add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik") + add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra = "rocnik") vm.content.on.forEach { l -> add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit${l.int("id")}", extra = "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex${l.int("id")}") @@ -509,6 +510,10 @@ private fun buildSearch(vm: AppViewModel): List { 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")}") } + vm.content.on4.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on4unit${l.int("id")}", extra = "obcanka") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex${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") @@ -615,9 +620,7 @@ fun OnYearsScreen(vm: AppViewModel) { 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_year4")) { key -> - NavRow(title = vm.tr(key), subtitle = vm.tr("on_year_locked_sub"), locked = true, onClick = null) + NavRow(title = vm.tr("on_year4"), subtitle = vm.tr("on4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On4Map) } } } } diff --git a/data/changelog.txt b/data/changelog.txt index 9508f53..7aa382f 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -6,15 +6,17 @@ # cs: bullet # en: bullet +--- 2026-10-03 +cs: Občanská nauka: 4. ročník, 16 lekcí (hospodářství, svět a filozofie) +en: Civics: year 4, 16 lessons (the economy, the world and philosophy) + --- 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. 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: 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 382a20e..d78c076 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -143,6 +143,11 @@ final class AppModel: ObservableObject { refresh() } + func markOn4(_ id: Int) { + progress.markOn4(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 5b36be0..c7899fc 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -34,6 +34,7 @@ final class Content { let on: [J] let on2: [J] let on3: [J] + let on4: [J] let mluvnice: [J] let books: [J] let changelog: [J] @@ -68,6 +69,7 @@ final class Content { on = raw.arr("on") on2 = raw.arr("on2") on3 = raw.arr("on3") + on4 = raw.arr("on4") mluvnice = raw.arr("mluvnice") books = raw.arr("books") changelog = raw.arr("changelog") @@ -79,6 +81,7 @@ final class Content { 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 on4Lesson(_ id: Int) -> J? { on4.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 c6ad454..c7b6bef 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -43,6 +43,9 @@ final class ProgressStore { func on3Done(_ id: Int) -> Bool { defaults.bool(forKey: "on3.\(id)") } func markOn3(_ id: Int) { defaults.set(true, forKey: "on3.\(id)") } + func on4Done(_ id: Int) -> Bool { defaults.bool(forKey: "on4.\(id)") } + func markOn4(_ id: Int) { defaults.set(true, forKey: "on4.\(id)") } + func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } @@ -110,6 +113,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for l in content.on4 { + sum.totalEx += 1 + sum.openUnits += 1 + if p.on4Done(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 0256dde..e303ad5 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -33,6 +33,9 @@ enum Route: Hashable { case on3Map case on3Lesson(Int) case on3Ex(Int) + case on4Map + case on4Lesson(Int) + case on4Ex(Int) case czechMap case mluvnice case mluvEx(Int) @@ -68,6 +71,9 @@ enum Route: Hashable { case .on3Map: return "on3map" case .on3Lesson(let id): return "on3unit\(id)" case .on3Ex(let id): return "on3ex\(id)" + case .on4Map: return "on4map" + case .on4Lesson(let id): return "on4unit\(id)" + case .on4Ex(let id): return "on4ex\(id)" case .czechMap: return "czechmap" case .mluvnice: return "mluvnice" case .mluvEx(let n): return "mluve\(n)" @@ -136,6 +142,12 @@ enum Route: Hashable { return [.onYears, .on3Map, .on3Lesson(id)] case .on3Ex(let id): return [.onYears, .on3Map, .on3Lesson(id), .on3Ex(id)] + case .on4Map: + return [.onYears, .on4Map] + case .on4Lesson(let id): + return [.onYears, .on4Map, .on4Lesson(id)] + case .on4Ex(let id): + return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)] case .czechMap: return [.czechMap] case .mluvnice: @@ -168,6 +180,7 @@ func routeFromPage(_ page: String) -> Route? { case "onmap": return .onMap case "on2map": return .on2Map case "on3map": return .on3Map + case "on4map": return .on4Map case "czechmap": return .czechMap case "mluvnice": return .mluvnice case "readinglist": return .readingList @@ -211,6 +224,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: /on4unit(\d+)/), let n = Int(m.1) { + return .on4Lesson(n) + } + if let m = page.wholeMatch(of: /on4ex(\d+)/), let n = Int(m.1) { + return .on4Ex(n) + } if let m = page.wholeMatch(of: /on3unit(\d+)/), let n = Int(m.1) { return .on3Lesson(n) } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index 152ec3b..797c305 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -682,6 +682,25 @@ struct On3QuizScreen: View { } } +struct On4QuizScreen: View { + @ObservedObject var vm: AppModel + let id: Int + + var body: some View { + if let lesson = vm.content.on4Lesson(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.markOn4(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 cac36ff..0017bce 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -423,11 +423,12 @@ 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", "on3map": - sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + case "onyears", "onmap", "on2map", "on3map", "on4map": + sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + vm.content.on4.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 + + vm.content.on4.filter { vm.progress.on4Done($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 @@ -569,7 +570,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"), "on3map", extra: "rocnik") - add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik") + add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra: "rocnik") for l in vm.content.on { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex\(l.int("id"))") @@ -582,6 +583,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { 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"))") } + for l in vm.content.on4 { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on4unit\(l.int("id"))", extra: "obcanka") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex\(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 { @@ -812,13 +817,8 @@ struct OnYearsScreen: View { 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: { - Image(systemName: "lock.fill") - } - .foregroundStyle(.secondary) + NavigationLink(value: Route.on4Map) { + ListRowLabel(title: vm.tr("on_year4"), subtitle: vm.tr("on4_sub")) } } .navigationTitle(vm.tr("Občanská nauka")) @@ -1460,6 +1460,27 @@ struct RouteDestination: View { } case .on3Ex(let id): On3QuizScreen(vm: vm, id: id) + case .on4Map: + LessonListScreen( + vm: vm, + title: vm.tr("on_year4"), + subtitle: vm.tr("on4_sub"), + rows: vm.content.on4.map { l in + let id = l.int("id") + return LessonRow( + id: "o4\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.on4Done(id), + destination: .on4Lesson(id) + ) + } + ) + case .on4Lesson(let id): + if let l = vm.content.on4Lesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on4Ex(id)) + } + case .on4Ex(let id): + On4QuizScreen(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 6a2b468..02711da 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -1120,6 +1120,18 @@ def main() -> int: "quiz": choice_items(A(f"build_on3_unit{i}_exercise_page.qs")), }) + on4_lessons = [] + for i in range(1, 17): + on4_lessons.append({ + "id": i, + "titleKey": f"on4_unit{i}", + "subKey": f"on4_unit{i}_sub", + "exTitleKey": f"on4_ex{i}_title", + "quizHeadKey": f"on4_quiz{i}_head", + "slides": slides_items(A(f"build_on4_unit{i}_page.slides")), + "quiz": choice_items(A(f"build_on4_unit{i}_exercise_page.qs")), + }) + hw_lessons = [] for i in range(1, 30): hw_lessons.append({ @@ -1541,6 +1553,7 @@ def main() -> int: "on": on_lessons, "on2": on2_lessons, "on3": on3_lessons, + "on4": on4_lessons, "mluvnice": mluvnice, "books": books, "changelog": parse_changelog(ROOT / "data" / "changelog.txt"), @@ -1578,6 +1591,7 @@ def main() -> int: 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" on4 lessons: {sum(1 for L in on4_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', []))}") @@ -1626,6 +1640,11 @@ def main() -> int: missing.append(f"on3{L['id']} slides") if not L["quiz"]: missing.append(f"on3{L['id']} quiz") + for L in on4_lessons: + if not L["slides"]: + missing.append(f"on4{L['id']} slides") + if not L["quiz"]: + missing.append(f"on4{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 e4d655c..1b3f08b 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -52,6 +52,7 @@ extern char *app_progress_hw; extern char *app_progress_on; extern char *app_progress_on2; extern char *app_progress_on3; +extern char *app_progress_on4; extern char *app_progress_mluvnice; extern char *app_progress_cetba; #define PROGRESS_DIR app_progress_dir @@ -64,6 +65,7 @@ extern char *app_progress_cetba; #define PROGRESS_ON app_progress_on #define PROGRESS_ON2 app_progress_on2 #define PROGRESS_ON3 app_progress_on3 +#define PROGRESS_ON4 app_progress_on4 #define NODE_SIZE 88.0 #define PATH_SPAC 240.0 #define ROAD_MX 150.0 /* horizontal canvas margin */ @@ -127,6 +129,8 @@ extern char *app_progress_cetba; #define ON2_LESSONS 16 #define ON3_UNITS 16 #define ON3_LESSONS 16 +#define ON4_UNITS 16 +#define ON4_LESSONS 16 #define HW_SLIDES 3 #define HW2_SLIDES 6 #define HW3_SLIDES 2 @@ -773,6 +777,8 @@ 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 NetLesson on4_lessons[ON4_LESSONS]; +extern GtkWidget *on4_nodes[ON4_UNITS]; extern GtkWidget *net_note_host; extern int net_note_last_w; extern GtkWidget *net_scroll; @@ -886,6 +892,14 @@ void on3_rail_theme_reset(void); void on3_lessons_apply_lang(void); GtkWidget *build_on3map_page(void); void add_on3_pages(GtkStack *stack); +void on4_load_progress(void); +void on4_save_progress(void); +void mark_on4_done(int lesson_id); +void refresh_on4_completion_ui(void); +void on4_rail_theme_reset(void); +void on4_lessons_apply_lang(void); +GtkWidget *build_on4map_page(void); +void add_on4_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 8c4e610..412d672 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -1000,6 +1000,104 @@ const TrEntry tr_ui[] = { {"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"}, + {"on4_sub", "Hospodářství, svět a filozofie. Vyberte lekci na cestě.", + "The economy, the world and philosophy. Pick a lesson on the path."}, + {"on4_unit1", "Trh", "The market"}, + {"on4_unit1_sub", "2 snímky o nabídce, poptávce a ceně a pak kvíz.", + "2 slides on supply, demand and price, then a quiz."}, + {"on4_ex1_title", "Kvíz: trh", "Quiz: the market"}, + {"on4_quiz1_head", "Kvíz k trhu", + "Quiz on the market"}, + {"on4_unit2", "HDP a cyklus", "GDP and the cycle"}, + {"on4_unit2_sub", "2 snímky o hrubém domácím produktu, růstu a recesi a pak kvíz.", + "2 slides on gross domestic product, growth and recession, then a quiz."}, + {"on4_ex2_title", "Kvíz: HDP a cyklus", "Quiz: GDP and the cycle"}, + {"on4_quiz2_head", "Kvíz k HDP a cyklu", + "Quiz on GDP and the cycle"}, + {"on4_unit3", "Peníze a inflace", "Money and inflation"}, + {"on4_unit3_sub", "2 snímky o koruně, euru a inflaci a pak kvíz.", + "2 slides on the koruna, the euro and inflation, then a quiz."}, + {"on4_ex3_title", "Kvíz: peníze a inflace", "Quiz: money and inflation"}, + {"on4_quiz3_head", "Kvíz k penězům a inflaci", + "Quiz on money and inflation"}, + {"on4_unit4", "Banky a ČNB", "Banks and the CNB"}, + {"on4_unit4_sub", "2 snímky o centrální bance, úvěrech a ochraně vkladů a pak kvíz.", + "2 slides on the central bank, loans and deposit protection, then a quiz."}, + {"on4_ex4_title", "Kvíz: banky a ČNB", "Quiz: banks and the CNB"}, + {"on4_quiz4_head", "Kvíz k bankám a ČNB", + "Quiz on banks and the CNB"}, + {"on4_unit5", "Daně a rozpočet", "Taxes and the budget"}, + {"on4_unit5_sub", "2 snímky o daních, DPH, schodku a dluhu a pak kvíz.", + "2 slides on taxes, VAT, the deficit and the debt, then a quiz."}, + {"on4_ex5_title", "Kvíz: daně a rozpočet", "Quiz: taxes and the budget"}, + {"on4_quiz5_head", "Kvíz k daním a rozpočtu", + "Quiz on taxes and the budget"}, + {"on4_unit6", "Podnikání", "Enterprise"}, + {"on4_unit6_sub", "2 snímky o živnosti, s.r.o. a akciové společnosti a pak kvíz.", + "2 slides on trades, limited companies and joint-stock companies, then a quiz."}, + {"on4_ex6_title", "Kvíz: podnikání", "Quiz: enterprise"}, + {"on4_quiz6_head", "Kvíz k podnikání", + "Quiz on enterprise"}, + {"on4_unit7", "Trh práce", "The labour market"}, + {"on4_unit7_sub", "2 snímky o mzdě, nezaměstnanosti a Úřadu práce a pak kvíz.", + "2 slides on wages, unemployment and the Labour Office, then a quiz."}, + {"on4_ex7_title", "Kvíz: trh práce", "Quiz: the labour market"}, + {"on4_quiz7_head", "Kvíz k trhu práce", + "Quiz on the labour market"}, + {"on4_unit8", "Rozpočet domácnosti", "The household budget"}, + {"on4_unit8_sub", "2 snímky o příjmech, výdajích a rezervě a pak kvíz.", + "2 slides on income, spending and a reserve, then a quiz."}, + {"on4_ex8_title", "Kvíz: rozpočet domácnosti", "Quiz: the household budget"}, + {"on4_quiz8_head", "Kvíz k rozpočtu domácnosti", + "Quiz on the household budget"}, + {"on4_unit9", "Úvěry a pojištění", "Loans and insurance"}, + {"on4_unit9_sub", "2 snímky o úroku, RPSN a povinném pojištění a pak kvíz.", + "2 slides on interest, the APRC and compulsory insurance, then a quiz."}, + {"on4_ex9_title", "Kvíz: úvěry a pojištění", "Quiz: loans and insurance"}, + {"on4_quiz9_head", "Kvíz k úvěrům a pojištění", + "Quiz on loans and insurance"}, + {"on4_unit10", "Evropská unie", "The European Union"}, + {"on4_unit10_sub", "2 snímky o členství, Schengenu, euru a institucích a pak kvíz.", + "2 slides on membership, Schengen, the euro and the institutions, then a quiz."}, + {"on4_ex10_title", "Kvíz: Evropská unie", "Quiz: the European Union"}, + {"on4_quiz10_head", "Kvíz k Evropské unii", + "Quiz on the European Union"}, + {"on4_unit11", "NATO a OSN", "NATO and the UN"}, + {"on4_unit11_sub", "2 snímky o obranné alianci, společné obraně a OSN a pak kvíz.", + "2 slides on the defence alliance, collective defence and the UN, then a quiz."}, + {"on4_ex11_title", "Kvíz: NATO a OSN", "Quiz: NATO and the UN"}, + {"on4_quiz11_head", "Kvíz k NATO a OSN", + "Quiz on NATO and the UN"}, + {"on4_unit12", "Globalizace", "Globalisation"}, + {"on4_unit12_sub", "2 snímky o propojení světa, přínosech a rizicích a pak kvíz.", + "2 slides on an interconnected world, its benefits and its risks, then a quiz."}, + {"on4_ex12_title", "Kvíz: globalizace", "Quiz: globalisation"}, + {"on4_quiz12_head", "Kvíz ke globalizaci", + "Quiz on globalisation"}, + {"on4_unit13", "Filozofie", "Philosophy"}, + {"on4_unit13_sub", "2 snímky o filozofických otázkách, Sókratovi a Aristotelovi a pak kvíz.", + "2 slides on philosophical questions, Socrates and Aristotle, then a quiz."}, + {"on4_ex13_title", "Kvíz: filozofie", "Quiz: philosophy"}, + {"on4_quiz13_head", "Kvíz k filozofii", + "Quiz on philosophy"}, + {"on4_unit14", "Etika", "Ethics"}, + {"on4_unit14_sub", "2 snímky o mravnosti, svědomí, svobodě a odpovědnosti a pak kvíz.", + "2 slides on morality, conscience, freedom and responsibility, then a quiz."}, + {"on4_ex14_title", "Kvíz: etika", "Quiz: ethics"}, + {"on4_quiz14_head", "Kvíz k etice", + "Quiz on ethics"}, + {"on4_unit15", "Víra a svědomí", "Faith and conscience"}, + {"on4_unit15_sub", "2 snímky o víře, ateismu a svobodě svědomí a pak kvíz.", + "2 slides on faith, atheism and freedom of conscience, then a quiz."}, + {"on4_ex15_title", "Kvíz: víra a svědomí", "Quiz: faith and conscience"}, + {"on4_quiz15_head", "Kvíz k víře a svědomí", + "Quiz on faith and conscience"}, + {"on4_unit16", "Hodnoty", "Values"}, + {"on4_unit16_sub", "2 snímky o hodnotách, pravdě, solidaritě a svobodě a pak kvíz.", + "2 slides on values, truth, solidarity and freedom, then a quiz."}, + {"on4_ex16_title", "Kvíz: hodnoty", "Quiz: values"}, + {"on4_quiz16_head", "Kvíz k hodnotám", + "Quiz on values"}, {"czech_sub", "Vyberte oblast a začněte procvičovat.", "Pick an area and start practicing."}, {"Literatura", "Literatura", "Literature"}, @@ -2026,6 +2124,7 @@ 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 on4_lessons_apply_lang(void); void net_rail_theme_reset(void); void apply_language(void) { @@ -2043,5 +2142,6 @@ void apply_language(void) { on_lessons_apply_lang(); on2_lessons_apply_lang(); on3_lessons_apply_lang(); + on4_lessons_apply_lang(); search_apply_lang(); } diff --git a/src/main.c b/src/main.c index eaa7f34..1d098b1 100644 --- a/src/main.c +++ b/src/main.c @@ -386,7 +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", + "on3map", "on4map", "czechmap", "mluvnice", "readinglist", "cetba1984", "cetba1984quiz", "cetba1984dej", @@ -401,6 +401,7 @@ static gboolean run_smoke_test(gpointer data) { "onunit1", "onex1", "onunit17", "onex17", "on2unit1", "on2ex1", "on2unit16", "on2ex16", "on3unit1", "on3ex1", "on3unit16", "on3ex16", + "on4unit1", "on4ex1", "on4unit16", "on4ex16", "unit1", "unit2", "unit3", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1vocab", "u2vocab", "u3vocab", @@ -518,6 +519,8 @@ void activate(GtkApplication *app, gpointer user_data) { 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_on4map_page(), "on4map"); + add_on4_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 9900b36..3563f91 100644 --- a/src/obcanka.c +++ b/src/obcanka.c @@ -3,7 +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. Years 2 and 3 are open; year 4 stays locked. */ + * place in the first year. Years 2, 3 and 4 are open. */ #define ON_ENTRY(n) \ { .n_slides = 2, .unit_page = "onunit" #n, .ex_page = "onex" #n } @@ -412,7 +412,7 @@ GtkWidget *build_onyears_page(void) { GtkWidget *scroll; GtkWidget *list; int y; - static const char *targets[] = {"onmap", "on2map", "on3map", NULL}; + static const char *targets[] = {"onmap", "on2map", "on3map", "on4map"}; 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 f4f4ddd..45d760b 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 is year 3; the economy and - * philosophy stay in the fourth year. */ + * schools place in the second year. Law is year 3. The economy, the + * world and philosophy are year 4. */ #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 index c74117e..72bc3cf 100644 --- a/src/obcanka3.c +++ b/src/obcanka3.c @@ -3,7 +3,7 @@ /* 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. */ + * in the third year. The economy, the world and philosophy are year 4. */ #define ON3_ENTRY(n) \ { .n_slides = 2, .unit_page = "on3unit" #n, .ex_page = "on3ex" #n } diff --git a/src/obcanka4.c b/src/obcanka4.c new file mode 100644 index 0000000..efc4878 --- /dev/null +++ b/src/obcanka4.c @@ -0,0 +1,1909 @@ +#include "graduately.h" + +/* Year 4 of Občanská nauka. + * The remaining units of the MŠMT model syllabus + * (č.j. 18 396/2002-23) and the SOV civic framework: the economy, + * Czechia in Europe and the world, and practical philosophy. + * Schools place them in the fourth year. */ + +#define ON4_ENTRY(n) \ + { .n_slides = 2, .unit_page = "on4unit" #n, .ex_page = "on4ex" #n } + +NetLesson on4_lessons[ON4_LESSONS] = { + ON4_ENTRY(1), + ON4_ENTRY(2), + ON4_ENTRY(3), + ON4_ENTRY(4), + ON4_ENTRY(5), + ON4_ENTRY(6), + ON4_ENTRY(7), + ON4_ENTRY(8), + ON4_ENTRY(9), + ON4_ENTRY(10), + ON4_ENTRY(11), + ON4_ENTRY(12), + ON4_ENTRY(13), + ON4_ENTRY(14), + ON4_ENTRY(15), + ON4_ENTRY(16), +}; + +#undef ON4_ENTRY + +GtkWidget *on4_scroll; +GtkWidget *on4_fixed; +GtkWidget *on4_rail; +GtkWidget *on4_nodes[ON4_UNITS]; +GtkWidget *on4_labels[ON4_UNITS]; +double on4_cx[ON4_UNITS]; +double on4_cy[ON4_UNITS]; +int on4_cols = 1; +int on4_rows = 1; +int on4_cw = (int)(2.0 * ROAD_MX + (ON4_UNITS - 1) * PATH_SPAC); +int on4_ch = (int)(2.0 * ROAD_MY); +guint on4_idle; +double on4_last_avail = -1.0; +int on4_last_cols = -1; +int on4_last_rows = -1; +int on4_last_cw = -1; +int on4_last_ch = -1; +cairo_surface_t *on4_rail_cache; +int on4_cache_w; +int on4_cache_h; + +static void on4_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 >= ON4_UNITS - 1) { k = ON4_UNITS - 2; u = 1.0; } + + p1x = on4_cx[k]; p1y = on4_cy[k]; + p2x = on4_cx[k + 1]; p2y = on4_cy[k + 1]; + if (k - 1 >= 0) { p0x = on4_cx[k - 1]; p0y = on4_cy[k - 1]; } + else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); } + if (k + 2 < ON4_UNITS) { p3x = on4_cx[k + 2]; p3y = on4_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 on4_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; + on4_point(t0, &x, &y); + cairo_move_to(cr, x, y); + for (s = 1; s <= n; s++) { + on4_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y); + cairo_line_to(cr, x, y); + } +} + +static void on4_geometry(double avail) { + const double phase = 2.0 * G_PI * 1.7 / (double)(ON4_UNITS - 1); + int rows; + int r, c, i; + + for (rows = 1; rows <= ON4_UNITS; rows++) { + int cols = (ON4_UNITS + rows - 1) / rows; + if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0) + break; + } + if (rows > ON4_UNITS) + rows = ON4_UNITS; + on4_rows = rows; + on4_cols = (ON4_UNITS + rows - 1) / rows; + if (on4_cols < 1) + on4_cols = 1; + on4_cw = (int)(2.0 * ROAD_MX + (on4_cols - 1) * PATH_SPAC); + on4_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP); + + for (r = 0; r < rows; r++) { + int base = r * on4_cols; + int len = MIN(on4_cols, ON4_UNITS - base); + int fwd = (r % 2) == 0; + for (c = 0; c < len; c++) { + i = base + c; + int cc = fwd ? c : (on4_cols - 1 - c); + on4_cx[i] = ROAD_MX + cc * PATH_SPAC; + on4_cy[i] = ROAD_MY + r * ROAD_GAP + + ROAD_WAVE * sin((double)i * phase); + } + } +} + +static void on4_apply_layout(void) { + if (!on4_fixed) + return; + + if (on4_cols == on4_last_cols && on4_rows == on4_last_rows && + on4_cw == on4_last_cw && on4_ch == on4_last_ch) + return; + + on4_last_cols = on4_cols; + on4_last_rows = on4_rows; + on4_last_cw = on4_cw; + on4_last_ch = on4_ch; + if (on4_rail_cache) { + cairo_surface_destroy(on4_rail_cache); + on4_rail_cache = NULL; + } + + for (int i = 0; i < ON4_UNITS; i++) { + if (!on4_nodes[i] || !on4_labels[i]) + continue; + gtk_fixed_move(GTK_FIXED(on4_fixed), on4_nodes[i], + (int)(on4_cx[i] - NODE_SIZE / 2.0), + (int)(on4_cy[i] - NODE_SIZE / 2.0)); + gtk_fixed_move(GTK_FIXED(on4_fixed), on4_labels[i], + (int)(on4_cx[i] - (PATH_SPAC - 20.0) / 2.0), + (int)(on4_cy[i] + NODE_SIZE / 2.0 + 10.0)); + } + + gtk_widget_set_size_request(on4_fixed, on4_cw, on4_ch); + gtk_widget_set_size_request(on4_rail, on4_cw, on4_ch); + gtk_fixed_move(GTK_FIXED(on4_fixed), on4_rail, 0, 0); + gtk_widget_queue_draw(on4_rail); +} + +static void on4_relayout(void) { + GtkAdjustment *hadj; + double avail; + + if (!on4_scroll) + return; + hadj = gtk_scrolled_window_get_hadjustment( + GTK_SCROLLED_WINDOW(on4_scroll)); + avail = gtk_adjustment_get_page_size(hadj); + if (avail < 1.0) + return; + if (fabs(avail - on4_last_avail) < 1.0) + return; + on4_last_avail = avail; + on4_geometry(avail); + on4_apply_layout(); +} + +static gboolean on4_relayout_idle(gpointer data) { + (void)data; + on4_idle = 0; + on4_relayout(); + return G_SOURCE_REMOVE; +} + +static void on4_relayout_later(void) { + if (on4_idle == 0) + on4_idle = g_idle_add(on4_relayout_idle, NULL); +} + +static void on4_adjust_notify(GtkAdjustment *adj, GParamSpec *ps, + gpointer data) { + (void)adj; + (void)ps; + (void)data; + on4_relayout_later(); +} + +static void on4_render_rail_to(cairo_t *cr) { + const double t_end = (double)(ON4_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); + on4_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); + on4_path(cr, 0.0, t_end); + cairo_stroke(cr); + cairo_set_dash(cr, NULL, 0, 0.0); +} + +static void on4_draw_rail(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + (void)area; + (void)width; + (void)height; + (void)data; + + if (!on4_rail_cache || on4_cache_w != on4_cw || on4_cache_h != on4_ch) { + cairo_t *rcr; + + if (on4_rail_cache) + cairo_surface_destroy(on4_rail_cache); + on4_cache_w = on4_cw; + on4_cache_h = on4_ch; + if (on4_cache_w < 1) + on4_cache_w = 1; + if (on4_cache_h < 1) + on4_cache_h = 1; + on4_rail_cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + on4_cache_w, on4_cache_h); + rcr = cairo_create(on4_rail_cache); + on4_render_rail_to(rcr); + cairo_destroy(rcr); + } + + cairo_set_source_surface(cr, on4_rail_cache, 0, 0); + cairo_paint(cr); +} + +void on4_rail_theme_reset(void) { + if (on4_rail_cache) { + cairo_surface_destroy(on4_rail_cache); + on4_rail_cache = NULL; + } + if (on4_rail) + gtk_widget_queue_draw(on4_rail); +} + +static void on4_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), "on4_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 on4_add_node(GtkFixed *fixed, int index) { + int num = index + 1; + gboolean locked = index >= ON4_LESSONS; + GtkWidget *card; + GtkWidget *vbox; + GtkWidget *number; + GtkWidget *icon; + GtkWidget *name; + char *text; + static char unit_keys[ON4_LESSONS][16]; + static gboolean unit_keys_ready = FALSE; + + if (!unit_keys_ready) { + for (int i = 0; i < ON4_LESSONS; i++) + g_snprintf(unit_keys[i], sizeof unit_keys[i], "on4_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); + on4_lessons[index].done_icon = icon; + g_signal_connect(card, "clicked", G_CALLBACK(on4_open_unit), + &on4_lessons[index]); + } + + on4_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 < ON4_LESSONS) { + i18n_bind(name, unit_keys[index], 0); + } else { + gtk_widget_add_css_class(name, "unit-name-locked"); + } + on4_labels[index] = name; + gtk_fixed_put(fixed, name, 0, 0); +} +GtkWidget *build_on4map_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *wrap; + GtkWidget *fixed; + GtkWidget *rail; + GtkAdjustment *ha; + GtkAdjustment *va; + + on4_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_year4", "on4_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); + on4_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, on4_cw, on4_ch); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); + on4_fixed = fixed; + + rail = gtk_drawing_area_new(); + gtk_widget_set_size_request(rail, on4_cw, on4_ch); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), on4_draw_rail, + NULL, NULL); + gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0); + on4_rail = rail; + + for (int i = 0; i < ON4_UNITS; i++) + on4_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(on4_adjust_notify), NULL); + g_signal_connect(va, "notify::page-size", + G_CALLBACK(on4_adjust_notify), NULL); + + on4_apply_layout(); + on4_relayout_later(); + refresh_on4_completion_ui(); + + return page; +} + +static void on4_slide_apply(NetLesson *L); +static void on4_slide_prev(GtkButton *button, gpointer data); +static void on4_slide_next(GtkButton *button, gpointer data); + +static int on4_note_last_w = -1; + +static void on4_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 < ON4_LESSONS; i++) + net_rescale_lesson_notes(&on4_lessons[i], body, head, kick); +} + +static gboolean on4_note_tick(GtkWidget *w, GdkFrameClock *clock, + gpointer data) { + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + (void)data; + if (width != on4_note_last_w) { + on4_note_last_w = width; + on4_rescale_notes_at(width); + } + return G_SOURCE_CONTINUE; +} + +static GtkWidget *build_on4_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("on4map", 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), "on4_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, on4_note_tick, NULL, NULL); + on4_note_last_w = -1; + on4_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(on4_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(on4_slide_next), L); + gtk_box_append(GTK_BOX(nav), L->next_btn); + + L->idx = 0; + on4_slide_apply(L); + return page; +} + +static void on4_slide_apply(NetLesson *L) { + char name[16]; + + if (!L || !L->stack) + return; + g_snprintf(name, sizeof(name), "on4_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 on4_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + (void)button; + if (!L) return; + if (L->idx > 0) { + L->idx--; + on4_slide_apply(L); + } +} + +static void on4_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + (void)button; + if (!L) return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + on4_slide_apply(L); + } else { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +void on4_lessons_apply_lang(void) { + for (int i = 0; i < ON4_LESSONS; i++) + on4_slide_apply(&on4_lessons[i]); +} + +static void on4_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_on4_done(ctx->lesson_id); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +static GtkWidget *build_on4_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(on4_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_on4_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Cena", "Nabídka a poptávka", + "Tip: cena není rozkaz, je to signál.", + { + "Trh je místo, kde se potkává nabídka s poptávkou.", + "Poptávka je ochota kupujících vzít zboží za určitou cenu.", + "Nabídka je ochota prodávajících zboží prodat.", + "Když cena roste, poptávka obvykle klesá a nabídka obvykle roste.", + NULL, + }, + }, + { + "2 / 2 • Soutěž", "Konkurence a monopol", + "Tip: čtvrtý ročník bere hospodářství, svět a filozofii.", + { + "Rovnovážná cena je tam, kde se nabídka s poptávkou potkají.", + "Konkurence nutí prodávající hlídat cenu i kvalitu.", + "Monopol má na trhu jediný prodávající a cenu si snáze diktuje.", + "Česko má tržní hospodářství. Stát stanoví pravidla, nenahrazuje každý obchod.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[0], "on4_unit1", "on4_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kde se potkává nabídka s poptávkou?", + { + "Jen v parlamentu", + "Na trhu", + "Jen u notáře", + "Jen v jedné rodině", + }, + 4, 1}, + {"Co se s poptávkou obvykle stane, když cena roste?", + { + "Vždycky roste", + "Obvykle klesá", + "Zruší se zákon", + "Zůstane vždy stejná", + }, + 4, 1}, + {"Kdo je monopol?", + { + "Jediný prodávající s velkou mocí nad cenou", + "Každý malý obchod", + "Stát, který vybírá daně", + "Kupující, který srovnává ceny", + }, + 4, 0}, + {"K čemu konkurence prodávající tlačí?", + { + "K libovolně vysoké ceně", + "Aby trh zavřeli", + "Aby hlídali cenu i kvalitu", + "Aby přestali prodávat", + }, + 4, 2}, + }; + static const char *hints[] = { + "Trh není jen kamenný krám", + "Vyšší cena část kupujících odradí", + "Konkurence monopolu chybí", + "Kupující si může vybrat jinde", + }; + return build_on4_mcq_page(0, "on4unit1", "on4_ex1_title", + "on4_quiz1_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Měřítko", "Co HDP počítá", + "Tip: HDP není vysvědčení štěstí.", + { + "Hrubý domácí produkt je tržní hodnota konečných statků a služeb.", + "Počítá se, co se na území státu vytvoří za určité období.", + "Meziprodukt, který už je v ceně hotového výrobku, se znovu nepřičítá.", + "HDP neříká, jak jsou lidé šťastní, a nepočítá neplacenou péči.", + NULL, + }, + }, + { + "2 / 2 • Vlny", "Růst a recese", + "Tip: pokles výroby se lidí dotkne dřív než tabulka.", + { + "Růst znamená, že se za období vytvoří víc.", + "Recese je období, kdy hospodářství klesá.", + "V poklesu obvykle roste nezaměstnanost a firmy odkládají investice.", + "HDP na osobu pomáhá státy srovnat, ale nezměří spravedlnost.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[1], "on4_unit2", "on4_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je hrubý domácí produkt?", + { + "Tržní hodnota konečných statků a služeb vytvořených na území státu", + "Součet poslaneckých platů", + "Počet nezaměstnaných", + "Státní dluh", + }, + 4, 0}, + {"Počítá se meziprodukt do HDP znovu?", + { + "Ano, čím víckrát, tím líp", + "Ne, aby se výroba nepočítala dvakrát", + "Jen o víkendu", + "Jen když jde o dovoz", + }, + 4, 1}, + {"Co je recese?", + { + "Nejrychlejší růst hospodářství", + "Den, kdy se platí daně", + "Období poklesu hospodářství", + "Růst všech mezd ze zákona", + }, + 4, 2}, + {"Měří HDP štěstí lidí?", + { + "Ano, je to měřítko štěstí", + "Ano, počítá i neplacenou péči", + "Ne, měří výrobu, ne štěstí", + "Ano, vyšší HDP znamená spravedlivější stát", + }, + 4, 2}, + }; + static const char *hints[] = { + "Jde o výrobu za určité období", + "V ceně hotového výrobku už je", + "Opak růstu", + "Neplacená péče v něm není", + }; + return build_on4_mcq_page(1, "on4unit2", "on4_ex2_title", + "on4_quiz2_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Směna", "K čemu jsou peníze", + "Tip: eurozóna není totéž co Evropská unie.", + { + "Peníze jsou prostředek směny a společné měřítko cen.", + "Dá se v nich také uložit hodnota na později.", + "Měnou České republiky je koruna.", + "Česko je v Evropské unii, ale eurem se zde neplatí.", + NULL, + }, + }, + { + "2 / 2 • Ceny", "Inflace", + "Tip: zdražení jedné věci ještě není inflace.", + { + "Inflace je všeobecný růst cenové hladiny.", + "Měří ji Český statistický úřad jako růst spotřebitelských cen.", + "Česká národní banka usiluje o inflaci poblíž 2 procent.", + "Vysoká inflace znehodnocuje úspory a pevný příjem koupí míň.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[2], "on4_unit3", "on4_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"K čemu peníze slouží?", + { + "Jsou prostředek směny a společné měřítko cen", + "Nahrazují zákon", + "Jsou totéž co státní dluh", + "Platí jen v jednom obchodě", + }, + 4, 0}, + {"Jaká je měna České republiky?", + { + "Euro", + "Koruna", + "Dolar", + "Měna celé eurozóny", + }, + 4, 1}, + {"Co je inflace?", + { + "Zdražení jedné značky", + "Pokles všech mezd ze zákona", + "Všeobecný růst cenové hladiny", + "Schodek státního rozpočtu", + }, + 4, 2}, + {"O jakou inflaci usiluje Česká národní banka?", + { + "Poblíž nuly za každou cenu", + "Poblíž 10 procent", + "Poblíž 20 procent", + "Poblíž 2 procent", + }, + 4, 3}, + }; + static const char *hints[] = { + "Bez nich se směna srovnává hůř", + "Euro se v Česku nepoužívá", + "Nejde o jednu věc v regálu", + "Cíl je nízká a stabilní inflace", + }; + return build_on4_mcq_page(2, "on4unit3", "on4_ex3_title", + "on4_quiz3_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Stát", "Česká národní banka", + "Tip: ČNB není obchodní banka s pobočkami na náměstí.", + { + "Česká národní banka je ústřední banka a vydává korunu.", + "Je nezávislá. Vláda jí nesmí diktovat úrokové sazby.", + "Hlavním nástrojem měnové politiky jsou právě úrokové sazby.", + "Dohlíží na banky a na další části finančního trhu.", + NULL, + }, + }, + { + "2 / 2 • Pobočka", "Obchodní banka a vklady", + "Tip: limit ochrany vkladů se počítá na osobu a jednu banku.", + { + "Obchodní banka přijímá vklady a poskytuje úvěry.", + "Peníze na účtu nejsou totéž co hotovost v trezoru banky.", + "Vklady chrání garanční systém.", + "Ochrana je zpravidla do ekvivalentu 100 000 eur na osobu a banku.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[3], "on4_unit4", "on4_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je Česká národní banka?", + { + "Obchodní banka s nejvíce pobočkami", + "Ústřední banka, která vydává korunu", + "Úřad práce", + "Ministerstvo financí", + }, + 4, 1}, + {"Čím ČNB hlavně řídí měnovou politiku?", + { + "Školním řádem", + "Počtem poslanců", + "Úrokovými sazbami", + "Sazbou DPH", + }, + 4, 2}, + {"Co dělá obchodní banka?", + { + "Schvaluje zákony", + "Přijímá vklady a poskytuje úvěry", + "Ukládá tresty", + "Stanovuje inflační cíl", + }, + 4, 1}, + {"Jak jsou vklady v bance chráněné?", + { + "Nejsou chráněné vůbec", + "Zpravidla do ekvivalentu 100 000 eur na osobu a banku", + "Jen do 1 000 korun", + "Bez limitu u všech bank dohromady", + }, + 4, 1}, + }; + static const char *hints[] = { + "Neslouží jako běžný účet pro veřejnost", + "Sazby ovlivňují cenu peněz", + "Inflační cíl je věc ČNB", + "Limit se nepočítá na celou zemi najednou", + }; + return build_on4_mcq_page(3, "on4unit4", "on4_ex4_title", + "on4_quiz4_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Výběr", "Přímé a nepřímé daně", + "Tip: DPH je v ceně nákupu, i když ji odvádí prodejce.", + { + "Přímá daň se platí z příjmu nebo z majetku.", + "Nepřímou daň člověk platí v ceně. Taková je daň z přidané hodnoty.", + "Základní sazba DPH je 21 procent, snížená 12 procent.", + "Vybrané knihy jsou od DPH osvobozené. Není to třetí sazba.", + NULL, + }, + }, + { + "2 / 2 • Rok", "Rozpočet, schodek a dluh", + "Tip: schodek je jeden rok, dluh se táhne dál.", + { + "Daně financují školy, zdravotnictví, obranu a další veřejné výdaje.", + "Státní rozpočet srovnává příjmy a výdaje státu na jeden rok.", + "Schodek vznikne, když stát v tom roce utratí víc, než vybere.", + "Dluh je to, co z minulých schodků ještě není splacené. Rozpočet schvaluje Poslanecká sněmovna.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[4], "on4_unit5", "on4_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Čím se DPH liší od daně z příjmu?", + { + "DPH je v ceně nákupu, daň z příjmu se platí z příjmu", + "Jsou totéž", + "DPH lidé v ceně nikdy neplatí", + "Daň z příjmu je přirážka na rohlíku", + }, + 4, 0}, + {"Jaká je základní sazba DPH?", + { + "12 procent", + "21 procent", + "5 procent", + "10 procent", + }, + 4, 1}, + {"Co je schodek státního rozpočtu?", + { + "Přebytek na účtu domácnosti", + "Počet bank", + "Výdaje státu v daném roce převýší příjmy", + "Inflační cíl ČNB", + }, + 4, 2}, + {"Kdo schvaluje státní rozpočet?", + { + "Česká národní banka", + "Obecní úřad", + "Každý živnostník", + "Poslanecká sněmovna", + }, + 4, 3}, + }; + static const char *hints[] = { + "Jedna je nepřímá, druhá přímá", + "Snížená sazba je 12 procent", + "Jde o jeden rozpočtový rok", + "Rozpočet je zákon", + }; + return build_on4_mcq_page(4, "on4unit5", "on4_ex5_title", + "on4_quiz5_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Účet", "Kdo je podnikatel", + "Tip: ne každá výdělečná činnost je živnost.", + { + "Podnikatel jedná soustavně, na vlastní účet a odpovědnost a za účelem zisku.", + "Častou cestou drobného podnikání je živnost.", + "Ohlašovací živnost se ohlásí. Koncesovaná navíc potřebuje povolení.", + "OSVČ podniká jako fyzická osoba. Společnost s ručením omezeným je osoba právnická.", + NULL, + }, + }, + { + "2 / 2 • Ručení", "s.r.o. a akciová společnost", + "Tip: společnost ručí svým majetkem, společník jen omezeně.", + { + "Společník s.r.o. ručí jen do výše nesplaceného vkladu.", + "Minimální vklad společníka s.r.o. je 1 koruna.", + "Akciová společnost má akcie a základní kapitál nejméně 2 miliony korun.", + "Podnikatel platí daně a odvody. Ohlášení živnosti není návod, jak se povinnostem vyhnout.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[5], "on4_unit6", "on4_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co vystihuje podnikání?", + { + "Soustavná činnost na vlastní účet a odpovědnost za účelem zisku", + "Jakákoli brigáda bez jakékoli povinnosti", + "Jen práce pro stát", + "Povinná školní docházka", + }, + 4, 0}, + {"Čím se liší koncesovaná živnost?", + { + "Ničím, stačí stejné ohlášení", + "Potřebuje povolení, nestačí jen ohlášení", + "Smí ji mít jen akciová společnost", + "Je vždy bez podmínek", + }, + 4, 1}, + {"Jaký je minimální vklad společníka s.r.o.?", + { + "2 miliony korun", + "100 000 eur", + "1 koruna", + "Kapitál se skládat nesmí", + }, + 4, 2}, + {"Čím ručí společník s.r.o. za dluhy společnosti?", + { + "Celým osobním majetkem bez omezení", + "Jen do výše nesplaceného vkladu", + "Za dluhy státu", + "Společnost svým majetkem neručí vůbec", + }, + 4, 1}, + }; + static const char *hints[] = { + "Zisk je záměr, ne záruka", + "Ohlašovací živnost se jen ohlašuje", + "Akciová společnost má hranici mnohem výš", + "Majetkem ručí samotná společnost", + }; + return build_on4_mcq_page(5, "on4unit6", "on4_ex6_title", + "on4_quiz6_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Mzda", "Kdo práci hledá", + "Tip: nezaměstnaný není každý, kdo zrovna nesedí v práci.", + { + "Trh práce spojuje zaměstnavatele a lidi, kteří práci hledají.", + "Mzda závisí na tom, co člověk umí, na poptávce a na dohodě.", + "Nezaměstnaný je, kdo práci nemá, chce pracovat a hledá ji.", + "Kdo práci nehledá, není v tomto smyslu nezaměstnaný.", + NULL, + }, + }, + { + "2 / 2 • Úřad", "Pomoc při ztrátě práce", + "Tip: smlouvu a výpověď bere třetí ročník.", + { + "Úřad práce eviduje uchazeče o zaměstnání a pomáhá místo hledat.", + "Podporu v nezaměstnanosti lze dostat jen při splnění zákonných podmínek.", + "Vzdělání a dovednosti šanci na práci zvyšují.", + "Pracovní smlouva, výpověď a zkušební doba jsou učivo třetího ročníku.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[6], "on4_unit7", "on4_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdo je nezaměstnaný?", + { + "Každý, kdo zrovna nemá směnu", + "Kdo práci nemá, chce ji a hledá ji", + "Každý student", + "Kdo práci má, ale nelíbí se mu", + }, + 4, 1}, + {"Kdo eviduje uchazeče o zaměstnání?", + { + "Česká národní banka", + "Úřad práce", + "Ústavní soud", + "Notář", + }, + 4, 1}, + {"Dostane podporu v nezaměstnanosti každý bez práce?", + { + "Ano, hned a bez podmínek", + "Ano, vyplácí ji škola", + "Ne, jen při splnění zákonných podmínek", + "Ano, je to totéž co mzda", + }, + 4, 2}, + {"Kde se bere pracovní smlouva a výpověď?", + { + "U daní ve čtvrtém ročníku", + "U voleb ve druhém ročníku", + "U rodiny v prvním ročníku", + "U pracovního práva ve třetím ročníku", + }, + 4, 3}, + }; + static const char *hints[] = { + "Patří k tomu aktivní hledání", + "Není to banka ani soud", + "Podpora není automatická", + "Čtvrtý ročník smlouvu neopakuje", + }; + return build_on4_mcq_page(6, "on4unit7", "on4_ex7_title", + "on4_quiz7_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Měsíc", "Příjmy a výdaje", + "Tip: rozpočet je porovnání, ne přání.", + { + "Rozpočet domácnosti srovnává, co přijde a co odejde.", + "Příjem je mzda, dávka, kapesné nebo jiný pravidelný přísun.", + "Nejdřív patří bydlení, jídlo, doprava a další nutné výdaje.", + "Až potom zbývá místo na věci, bez kterých se domácnost obejde.", + NULL, + }, + }, + { + "2 / 2 • Zásoba", "Rezerva a schodek", + "Tip: rezerva není totéž co půjčka.", + { + "Rezerva kryje nečekaný výdaj, třeba opravu nebo výpadek příjmu.", + "Krátký schodek se dá vyrovnat z rezervy.", + "Dlouhý schodek znamená škrtat, nebo se zadlužovat.", + "Rozpočet má smysl, jen když se podle něj domácnost opravdu řídí.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[7], "on4_unit8", "on4_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co srovnává rozpočet domácnosti?", + { + "Jen přání", + "Příjmy a výdaje", + "Počet poslanců", + "Inflaci a státní dluh", + }, + 4, 1}, + {"Co má v rozpočtu přednost?", + { + "Nejdřív bydlení, jídlo a další nutné výdaje", + "Nejdřív všechno z reklamy", + "Nejdřív půjčka na dovolenou", + "Rozpočet se nesestavuje", + }, + 4, 0}, + {"K čemu je rezerva?", + { + "Aby smazala státní dluh", + "Na nečekaný výdaj", + "Nahrazuje zdravotní pojištění", + "Je totéž co daň", + }, + 4, 1}, + {"Co znamená dlouhý schodek domácnosti?", + { + "Domácnost šetří víc, než musí", + "Příjmy jsou vyšší než výdaje", + "Výdaje převyšují příjmy a je třeba škrtat, nebo se domácnost zadluží", + "Inflace je nulová", + }, + 4, 2}, + }; + static const char *hints[] = { + "Obě strany musí být vidět", + "Nutné výdaje jsou dřív než přání", + "Patří na výpadek, ne na stát", + "Schodek není přebytek", + }; + return build_on4_mcq_page(7, "on4unit8", "on4_ex8_title", + "on4_quiz8_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit9_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Cena", "Spoření a úvěr", + "Tip: vyšší slíbený výnos obvykle znamená vyšší riziko.", + { + "Úspora je odložená spotřeba. Úvěr jsou půjčené peníze.", + "Úrok je cena půjčených peněz a úvěr se vrací i s ním.", + "RPSN ukazuje roční náklady spotřebitelského úvěru včetně poplatků.", + "Nabídky se porovnávají. Půjčit si lze jen tolik, kolik se dá vracet.", + NULL, + }, + }, + { + "2 / 2 • Krytí", "Pojištění a tíseň", + "Tip: nabídku, která těží z tísně, nepodepisujte.", + { + "Veřejné zdravotní pojištění je v České republice povinné.", + "Sociální pojištění se týká důchodu, nemoci a ztráty zaměstnání, vždy podle zákona.", + "Pojištění bytu nebo odpovědnosti je dobrovolné a kryje jen dohodnuté riziko.", + "Nabídku, která těží z něčí tísně, odmítněte a řekněte o ní dospělému.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[8], "on4_unit9", "on4_unit9_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit9_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je úrok?", + { + "Cena půjčených peněz", + "Daň z přidané hodnoty", + "Pokuta za hluk", + "Státní dluh", + }, + 4, 0}, + {"Co ukazuje RPSN?", + { + "Jen barvu platební karty", + "Roční náklady spotřebitelského úvěru včetně poplatků", + "Výši HDP", + "Počet poboček banky", + }, + 4, 1}, + {"Kolik si má domácnost půjčit?", + { + "Co nejvíc, výnos je jistý", + "Tolik, kolik slíbí první reklama", + "Jen tolik, kolik zvládne vracet", + "Libovolně, úvěr se vracet nemusí", + }, + 4, 2}, + {"Jaké pojištění je v České republice povinné?", + { + "Pojištění dovolené", + "Pojištění mobilu", + "Pojištění bytu", + "Veřejné zdravotní pojištění", + }, + 4, 3}, + }; + static const char *hints[] = { + "Úvěr není zdarma", + "Poplatky do ní patří", + "Jistý vysoký zisk nikdo seriózní neslíbí", + "Pojištění bytu je dobrovolné", + }; + return build_on4_mcq_page(8, "on4unit9", "on4_ex9_title", + "on4_quiz9_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit10_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Členství", "Unie, Schengen a euro", + "Tip: členství v Unii ještě neznamená euro.", + { + "Česká republika je členským státem Evropské unie od 1. května 2004.", + "Unie má 27 států. Společný trh je volný pohyb zboží, služeb, kapitálu a osob.", + "Česko je v Schengenu. Ne každý stát Unie v něm je a některé státy Schengenu v Unii nejsou.", + "Eurozóna je jen část Unie. V Česku se platí korunou.", + NULL, + }, + }, + { + "2 / 2 • Orgány", "Kdo v Unii rozhoduje", + "Tip: Evropská rada není Rada Evropské unie.", + { + "Evropský parlament volí občané přímo.", + "Evropská komise navrhuje unijní předpisy a dohlíží, jak se provádějí.", + "Rada Evropské unie sdružuje ministry členských států.", + "Evropská rada je setkání hlav států a vlád a určuje směr.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[9], "on4_unit10", "on4_unit10_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit10_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Od kdy je Česká republika v Evropské unii?", + { + "Od roku 1993", + "Od 1. května 2004", + "Od 12. března 1999", + "Od roku 1989", + }, + 4, 1}, + {"Platí Česko eurem?", + { + "Ano, od vstupu do Unie", + "Ano, koruna zanikla", + "Ne, je v Unii, ale ne v eurozóně", + "Euro je měna Severoatlantické aliance", + }, + 4, 2}, + {"Je každý stát Evropské unie v Schengenu?", + { + "Ano, je to totéž", + "Ne", + "Ano, a Schengen je totéž co euro", + "Ne, a Česko v něm také není", + }, + 4, 1}, + {"Kterou unijní instituci volí občané přímo?", + { + "Evropskou komisi", + "Evropskou radu", + "Radu Evropské unie", + "Evropský parlament", + }, + 4, 3}, + }; + static const char *hints[] = { + "Rok 1999 patří k NATO", + "Eurozóna je užší než Unie", + "Česko v Schengenu je", + "Ministři v Radě EU voleni přímo nejsou", + }; + return build_on4_mcq_page(9, "on4unit10", "on4_ex10_title", + "on4_quiz10_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit11_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Aliance", "Česko v NATO", + "Tip: společná obrana neruší suverenitu státu.", + { + "Česká republika je svrchovaný stát.", + "NATO je obranná aliance. Česko do něj vstoupilo 12. března 1999.", + "Smlouva bere ozbrojený útok na jednoho člena jako útok na všechny.", + "Ostatní členové pak poskytnou pomoc. Aliance není obchodní smlouva ani soud.", + NULL, + }, + }, + { + "2 / 2 • Mír", "Organizace spojených národů", + "Tip: OSN není totéž co Evropská unie.", + { + "OSN vznikla roku 1945, aby státy chránily mír a řešily spory jednáním.", + "Valné shromáždění sdružuje členské státy.", + "Rada bezpečnosti se zabývá ohrožením míru.", + "Česká republika je členem OSN, stejně jako je členem Evropské unie a NATO.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[10], "on4_unit11", "on4_unit11_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit11_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kdy Česko vstoupilo do NATO?", + { + "1. května 2004", + "12. března 1999", + "Roku 1945", + "Roku 1918", + }, + 4, 1}, + {"Co znamená zásada společné obrany?", + { + "Ozbrojený útok na jednoho člena se bere jako útok na všechny", + "Aliance ruší suverenitu členů", + "Členové si nesmějí pomáhat", + "Jde o smlouvu o sazbě DPH", + }, + 4, 0}, + {"K čemu vznikla OSN?", + { + "Aby vydávala korunu", + "Aby státy chránily mír a řešily spory jednáním", + "Aby vedla živnostenský rejstřík", + "Aby stanovila českou DPH", + }, + 4, 1}, + {"Je Česká republika členem OSN?", + { + "Ne", + "Jen jako pozorovatel bez členství", + "Ano", + "Byla, ale roku 2004 vystoupila", + }, + 4, 2}, + }; + static const char *hints[] = { + "Rok 2004 je vstup do Evropské unie", + "Pomoc není daňový předpis", + "Vznikla roku 1945", + "Členství v Unii členství v OSN neruší", + }; + return build_on4_mcq_page(10, "on4unit11", "on4_ex11_title", + "on4_quiz11_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit12_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Hranice", "Propojený svět", + "Tip: propojení není jen výhoda.", + { + "Globalizace je rostoucí propojení ekonomik, informací a kultur přes hranice.", + "Přináší obchod, cestování a rychlejší šíření poznatků.", + "Nese i závislost na jiných státech, nerovnost a tlak na životní prostředí.", + "Globální problém, třeba změna klimatu nebo pandemie, se na hranici nezastaví.", + NULL, + }, + }, + { + "2 / 2 • Odpověď", "Spolupráce a vlastní volba", + "Tip: sledovat původ zboží je taky rozhodnutí.", + { + "Krize v jedné zemi se může přelít do jiných.", + "Státy proto spolupracují v organizacích a ve smlouvách.", + "Člověk může sledovat, odkud zboží je, a rozhodovat se s rozmyslem.", + "Hlas ve volbách je způsob, jak se k těm otázkám postavit.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[11], "on4_unit12", "on4_unit12_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit12_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je globalizace?", + { + "Zákaz obchodu přes hranice", + "Rostoucí propojení ekonomik, informací a kultur", + "Totéž co státní rozpočet", + "Zánik všech států", + }, + 4, 1}, + {"Co globalizace může přinést?", + { + "Obchod, cestování a rychlejší šíření poznatků", + "Jen izolaci", + "Zrušení všech měn", + "Konec odpovědnosti", + }, + 4, 0}, + {"Co je globální problém?", + { + "Jen hádka dvou sousedů", + "Problém, který se nezastaví na hranici jednoho státu", + "Schodek jedné domácnosti", + "Místní školní řád", + }, + 4, 1}, + {"Proč státy u takových otázek spolupracují?", + { + "Protože krize se může přelít přes hranice", + "Protože hranice už neexistují", + "Protože OSN vybírá české daně", + "Spolupráce není k ničemu", + }, + 4, 0}, + }; + static const char *hints[] = { + "Státy globalizací nemizejí", + "Přínos nevylučuje riziko", + "Klima a pandemie jsou příklady", + "Jedna země to sama neuzavře", + }; + return build_on4_mcq_page(11, "on4unit12", "on4_ex12_title", + "on4_quiz12_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit13_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Otázka", "Co filozofie dělá", + "Tip: filozofie není seznam hotových odpovědí.", + { + "Filozofie se ptá, co je skutečné, co můžeme poznat a jak máme žít.", + "Nestačí jí příkaz ani pouhý zvyk. Chce důvody.", + "Rozlišuje pojmy, aby se lidé nebavili o různých věcech stejným slovem.", + "Nemá jeden školní klíč, který by disputaci ukončil.", + NULL, + }, + }, + { + "2 / 2 • Antika", "Sókratés, Platón a Aristotelés", + "Tip: Sókratés se nevydával za majitele hotových pravd.", + { + "Sókratés zkoumal, co lidé pokládají za jisté.", + "Sám se za vlastníka hotových pravd nevydával.", + "Platón hledal trvalé ideje za proměnlivým světem.", + "Aristotelés vycházel ze zkušenosti a třídil, co lze poznat.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[12], "on4_unit13", "on4_unit13_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit13_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Na co se filozofie ptá?", + { + "Jen na výši DPH", + "Co je skutečné, co lze poznat a jak žít", + "Kolik je poslanců", + "Jak se vyplní daňové přiznání", + }, + 4, 1}, + {"Stačí filozofii pouhý příkaz?", + { + "Ano, příkaz je důkaz", + "Ne, chce důvody", + "Ano, zvyk se nezkoumá", + "Je totéž co zákoník", + }, + 4, 1}, + {"Co Sókratés odmítal o sobě tvrdit?", + { + "Že už vlastní hotové pravdy", + "Že se má ptát", + "Že rozhovor má smysl", + "Že člověk může nevědět", + }, + 4, 0}, + {"Čím se Aristotelés ve škole často liší od Platóna?", + { + "Popíral, že lze cokoli poznat", + "Sepsal zákoník práce", + "Vycházel ze zkušenosti", + "Odmítal třídit pojmy", + }, + 4, 2}, + }; + static const char *hints[] = { + "Jde o otázky, které tabulka neuzavře", + "Bez důvodů to není zkoumání", + "Zkoumal jistoty druhých", + "Platón hledal trvalé ideje", + }; + return build_on4_mcq_page(12, "on4unit13", "on4_ex13_title", + "on4_quiz13_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit14_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Správné", "Norma a svědomí", + "Tip: svědomí může mýlit, proto patří i důvody.", + { + "Etika se ptá, co je správné dělat a proč.", + "Mravní norma není totéž co zákon. Zákon bere třetí ročník.", + "Svědomí je vnitřní smysl pro správné a špatné.", + "Může se mýlit. Proto nestačí pocit bez důvodu.", + NULL, + }, + }, + { + "2 / 2 • Volba", "Svoboda a důstojnost", + "Tip: výhodné jednání ještě nemusí být správné.", + { + "Svoboda znamená, že člověk volí. Odpovědnost znamená, že za volbu stojí.", + "Svoboda bez odpovědnosti je svévole.", + "Lidská důstojnost patří každému, ne jen úspěšným nebo užitečným.", + "Dobré jednání se nepozná jen podle toho, že se vyplatí.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[13], "on4_unit14", "on4_unit14_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit14_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co zkoumá etika?", + { + "Výši úrokových sazeb", + "Co je správné dělat a proč", + "Soustavu soudů", + "Sazbu DPH", + }, + 4, 1}, + {"Je mravní norma totéž co zákon?", + { + "Ano", + "Ne", + "Každá neslušnost je hned trestný čin", + "Morálku vynucuje jen soud", + }, + 4, 1}, + {"Je svědomí neomylné?", + { + "Ano, proto důvody nepotřebujeme", + "Ano, nahrazuje zákon", + "Ne, může se mýlit", + "Svědomí mají jen věřící", + }, + 4, 2}, + {"Komu patří lidská důstojnost?", + { + "Jen úspěšným", + "Jen užitečným", + "Každému člověku", + "Jen těm, kdo se nikdy nezmýlí", + }, + 4, 2}, + }; + static const char *hints[] = { + "Ptá se po důvodech jednání", + "Zákon a mrav se často kryjí, ale nejsou totéž", + "Pocit se má zkoumat", + "Důstojnost se nekupuje výkonem", + }; + return build_on4_mcq_page(13, "on4unit14", "on4_ex14_title", + "on4_quiz14_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit15_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Smysl", "Víra i její odmítnutí", + "Tip: stát nesmí víru vnutit ani zakázat.", + { + "Lidé hledají smysl ve víře, ve filozofii i v pohledu bez náboženství.", + "Křesťanství dlouho utvářelo českou a evropskou kulturu.", + "Vedle něj studenti potkávají judaismus, islám a další tradice.", + "Ateismus a agnosticismus jsou postoje, které svoboda svědomí taky chrání.", + NULL, + }, + }, + { + "2 / 2 • Hranice", "Respekt není totéž co pravda", + "Tip: nesouhlasit lze bez pohrdání.", + { + "Člověk smí věřit, víru změnit, nebo nevěřit.", + "Respekt k člověku neznamená, že každý názor je pravdivý.", + "Náboženství a věda odpovídají na jiný druh otázek.", + "Svoboda svědomí chrání i toho, kdo se víry zřekl.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[14], "on4_unit15", "on4_unit15_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit15_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Smí stát vnutit víru?", + { + "Ano, tu většinovou", + "Ne", + "Ano, ve škole povinně jednu", + "Ano, nevěřící ztrácí práva", + }, + 4, 1}, + {"Chrání svoboda svědomí i ateismus?", + { + "Ne", + "Ano, člověk smí nevěřit", + "Jen mimo Evropu", + "Ateismus je státní náboženství", + }, + 4, 1}, + {"Znamená respekt, že každý názor je pravdivý?", + { + "Ano", + "Ne, nesouhlasit lze slušně", + "Pravda je to, co říká hlasitější", + "Názory se nesmějí vůbec porovnávat", + }, + 4, 1}, + {"Je náboženství totéž co věda?", + { + "Ano", + "Ano, obojí měří inflaci", + "Ne, odpovídají na jiný druh otázek", + "Věda ukládá povinnou víru", + }, + 4, 2}, + }; + static const char *hints[] = { + "Svoboda svědomí nutit nedovolí", + "Patří k ní i odmítnutí víry", + "Respekt patří člověku", + "Každé se ptá jinak", + }; + return build_on4_mcq_page(14, "on4unit15", "on4_ex15_title", + "on4_quiz15_head", qs, hints, 4); +} + +static GtkWidget *build_on4_unit16_page(void) { + static const NetSlide slides[] = { + { + "1 / 2 • Měřítko", "Podle čeho volíme", + "Tip: praktická filozofie chce důvody, ne heslo.", + { + "Hodnoty jsou měřítka volby: důstojnost, pravda, spravedlnost, svoboda, solidarita.", + "Dobrý život není totéž co pohodlí nebo vysoký příjem.", + "Člověk má zkoumat vlastní důvody, ne jen opakovat větu.", + "Společenství, občan, právo a hospodářství k sobě v občance patří.", + NULL, + }, + }, + { + "2 / 2 • Druzí", "Pravda, solidarita, svoboda", + "Tip: svoboda druhého je hranice mé svobody.", + { + "Pravda se nepozná podle toho, kdo křičí hlasitěji.", + "Spravedlnost chce podobným případům podobný přístup.", + "Solidarita je ochota nést část nákladu s druhými, ne jen brát výhody.", + "Svoboda druhého je hranice mé svobody.", + NULL, + }, + }, + }; + + return build_on4_unit_page(&on4_lessons[15], "on4_unit16", "on4_unit16_sub", + slides, G_N_ELEMENTS(slides)); +} +static GtkWidget *build_on4_unit16_exercise_page(void) { + static const ChoiceQ qs[] = { + {"K čemu jsou hodnoty?", + { + "Nahrazují domácí rozpočet", + "Jsou měřítka, podle kterých volíme", + "Ruší odpovědnost", + "Jsou totéž co sazba DPH", + }, + 4, 1}, + {"Je dobrý život totéž co vysoký příjem?", + { + "Ano", + "Ne", + "Ano, HDP ho měří přímo", + "Ano, důstojnost se kupuje", + }, + 4, 1}, + {"Co praktická filozofie od člověka chce?", + { + "Aby opakoval heslo", + "Aby zkoumal vlastní důvody", + "Aby se vzdal svobody", + "Aby nečetl námitky", + }, + 4, 1}, + {"Co ohraničuje mou svobodu?", + { + "Nic", + "Jen výše mzdy", + "Svoboda druhého", + "Počet bank v ulici", + }, + 4, 2}, + }; + static const char *hints[] = { + "Bez nich je volba nahodilá", + "Příjem je prostředek, ne celý život", + "Heslo bez důvodu nestačí", + "Svoboda není svévole", + }; + return build_on4_mcq_page(15, "on4unit16", "on4_ex16_title", + "on4_quiz16_head", qs, hints, 4); +} +void add_on4_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[] = { + {"on4unit1", "on4ex1", build_on4_unit1_page, build_on4_unit1_exercise_page}, + {"on4unit2", "on4ex2", build_on4_unit2_page, build_on4_unit2_exercise_page}, + {"on4unit3", "on4ex3", build_on4_unit3_page, build_on4_unit3_exercise_page}, + {"on4unit4", "on4ex4", build_on4_unit4_page, build_on4_unit4_exercise_page}, + {"on4unit5", "on4ex5", build_on4_unit5_page, build_on4_unit5_exercise_page}, + {"on4unit6", "on4ex6", build_on4_unit6_page, build_on4_unit6_exercise_page}, + {"on4unit7", "on4ex7", build_on4_unit7_page, build_on4_unit7_exercise_page}, + {"on4unit8", "on4ex8", build_on4_unit8_page, build_on4_unit8_exercise_page}, + {"on4unit9", "on4ex9", build_on4_unit9_page, build_on4_unit9_exercise_page}, + {"on4unit10", "on4ex10", build_on4_unit10_page, build_on4_unit10_exercise_page}, + {"on4unit11", "on4ex11", build_on4_unit11_page, build_on4_unit11_exercise_page}, + {"on4unit12", "on4ex12", build_on4_unit12_page, build_on4_unit12_exercise_page}, + {"on4unit13", "on4ex13", build_on4_unit13_page, build_on4_unit13_exercise_page}, + {"on4unit14", "on4ex14", build_on4_unit14_page, build_on4_unit14_exercise_page}, + {"on4unit15", "on4ex15", build_on4_unit15_page, build_on4_unit15_exercise_page}, + {"on4unit16", "on4ex16", build_on4_unit16_page, build_on4_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 76343e2..27ee5b4 100644 --- a/src/progress.c +++ b/src/progress.c @@ -57,6 +57,7 @@ void load_progress(void) { on_load_progress(); on2_load_progress(); on3_load_progress(); + on4_load_progress(); } /* Redraw the exercise rails of every unit that already has a page. */ @@ -426,8 +427,8 @@ void mark_on_done(int lesson_id) { } void progress_for_on(ProgressSum *out) { - out->total_ex += ON_LESSONS + ON2_LESSONS + ON3_LESSONS; - out->open_units += ON_LESSONS + ON2_LESSONS + ON3_LESSONS; + out->total_ex += ON_LESSONS + ON2_LESSONS + ON3_LESSONS + ON4_LESSONS; + out->open_units += ON_LESSONS + ON2_LESSONS + ON3_LESSONS + ON4_LESSONS; for (int i = 0; i < ON_LESSONS; i++) { if (on_lessons[i].done) { out->done_ex++; @@ -446,6 +447,12 @@ void progress_for_on(ProgressSum *out) { out->done_units++; } } + for (int i = 0; i < ON4_LESSONS; i++) { + if (on4_lessons[i].done) { + out->done_ex++; + out->done_units++; + } + } } void on2_load_progress(void) { @@ -607,3 +614,83 @@ void mark_on3_done(int lesson_id) { refresh_on3_completion_ui(); refresh_stats_ui(); } + +void on4_load_progress(void) { + GKeyFile *kf = g_key_file_new(); + GError *err = NULL; + + if (!g_key_file_load_from_file(kf, PROGRESS_ON4, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + + for (int i = 0; i < ON4_LESSONS; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + on4_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + + g_key_file_free(kf); +} + +void on4_save_progress(void) { + GKeyFile *kf = g_key_file_new(); + + for (int i = 0; i < ON4_LESSONS; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + g_key_file_set_boolean(kf, "done", key, on4_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_ON4, data, -1, &err); + if (err) + g_error_free(err); + g_free(data); + } +} + +void refresh_on4_completion_ui(void) { + for (int i = 0; i < ON4_LESSONS; i++) { + GtkWidget *node = on4_nodes[i]; + GtkWidget *icon = on4_lessons[i].done_icon; + + if (!node) + continue; + if (on4_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_on4_done(int lesson_id) { + if (lesson_id < 0 || lesson_id >= ON4_LESSONS) + return; + if (on4_lessons[lesson_id].done) + return; + on4_lessons[lesson_id].done = TRUE; + on4_save_progress(); + refresh_on4_completion_ui(); + refresh_stats_ui(); +} diff --git a/src/search.c b/src/search.c index 17e6be3..f132658 100644 --- a/src/search.c +++ b/src/search.c @@ -205,7 +205,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", "on3map", FALSE, 1, "rocnik"); - catalog_add_key("on_year4", "search_lesson", "onyears", TRUE, 1, "rocnik"); + catalog_add_key("on_year4", "search_lesson", "on4map", FALSE, 1, "rocnik"); for (i = 1; i <= ON_LESSONS; i++) { g_snprintf(key, sizeof(key), "on_unit%d", i); g_snprintf(page, sizeof(page), "onunit%d", i); @@ -227,6 +227,13 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "on3ex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + for (i = 1; i <= ON4_LESSONS; i++) { + g_snprintf(key, sizeof(key), "on4_unit%d", i); + g_snprintf(page, sizeof(page), "on4unit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "obcanka"); + g_snprintf(page, sizeof(page), "on4ex%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 b6e7f67..d408229 100644 --- a/src/theme.c +++ b/src/theme.c @@ -429,6 +429,7 @@ void apply_theme(void) { on_rail_theme_reset(); on2_rail_theme_reset(); on3_rail_theme_reset(); + on4_rail_theme_reset(); czech_rail_theme_reset(); book_rail_theme_reset(); } diff --git a/src/util.c b/src/util.c index 49145d3..db44391 100644 --- a/src/util.c +++ b/src/util.c @@ -23,6 +23,7 @@ char *app_progress_hw; char *app_progress_on; char *app_progress_on2; char *app_progress_on3; +char *app_progress_on4; char *app_progress_mluvnice; char *app_progress_cetba; @@ -164,6 +165,7 @@ static void set_progress_paths(const char *write_root) { g_free(app_progress_on); g_free(app_progress_on2); g_free(app_progress_on3); + g_free(app_progress_on4); g_free(app_progress_mluvnice); g_free(app_progress_cetba); @@ -177,6 +179,7 @@ static void set_progress_paths(const char *write_root) { 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_on4 = g_build_filename(app_progress_dir, "on4.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); }