From bd7ab09b1d3f78e2853d193c17f543f0870252d7 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Tue, 6 Oct 2026 09:09:07 +0200 Subject: [PATCH] Add chemistry and biology lessons for natural sciences. Co-authored-by: Cursor --- README.md | 8 +- .../org/maturita/maturita/AppViewModel.kt | 10 + .../org/maturita/maturita/MainActivity.kt | 31 + .../main/java/org/maturita/maturita/Route.kt | 32 +- .../org/maturita/maturita/data/Content.kt | 4 + .../maturita/maturita/data/ProgressStore.kt | 21 + .../org/maturita/maturita/ui/Exercises.kt | 14 + .../java/org/maturita/maturita/ui/Screens.kt | 35 + data/changelog.txt | 4 + ios/Graduately/AppModel.swift | 10 + ios/Graduately/Data/Content.swift | 6 + ios/Graduately/Data/ProgressStore.swift | 21 + ios/Graduately/Route.swift | 43 ++ ios/Graduately/UI/Components.swift | 1 + ios/Graduately/UI/ExercisesMore.swift | 21 + ios/Graduately/UI/Screens.swift | 102 +++ scripts/extract-android-content.py | 9 +- src/biologie.c | 617 +++++++++++++++ src/chemie.c | 604 +++++++++++++++ src/graduately.h | 23 + src/i18n.c | 102 ++- src/icons.c | 40 + src/main.c | 8 + src/priroda.c | 720 ++++++++++++++++++ src/progress.c | 1 + src/search.c | 20 +- src/stats.c | 9 +- src/subjects.c | 9 +- src/theme.c | 1 + src/util.c | 6 + 30 files changed, 2519 insertions(+), 13 deletions(-) create mode 100644 src/biologie.c create mode 100644 src/chemie.c create mode 100644 src/priroda.c diff --git a/README.md b/README.md index 4f10f28..31c1d8e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses - **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov středních škol), mluvnice (20 cvičení) a maturitní četba (82 knih: zápisky, kvíz, sestavení děje) +- **Základy přírodopisných věd** — chemie a biologie, v každé 8 lekcí + (od atomu a buňky po biochemii a ekologii) s výkladem a kvízem - **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, 16 lekcí (hospodářství, svět a filozofie) @@ -78,7 +80,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses ### Použití 1. **Pokračuj** otevře mapu předmětů. -2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy) vede na učební cestu. +2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy, baňka) vede na učební cestu. 3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají. 4. **Hledat** (lupa), **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře. @@ -266,6 +268,8 @@ Intel Mac, another architecture, or hacking on the code → - **Czech language** — literature (4 years, 32 lessons from the secondary-school syllabus), grammar (20 exercises) and required reading (82 books: notes, quiz, plot ordering) +- **Natural sciences** — chemistry and biology, eight lessons each + (from the atom and the cell to biochemistry and ecology), with an overview and a quiz - **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); @@ -276,7 +280,7 @@ Intel Mac, another architecture, or hacking on the code → ### Usage 1. **Continue** opens the subject map. -2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales) opens its path. +2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales, flask) opens its path. 3. Exercise / lesson nodes open content; locked nodes do nothing. 4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right. 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 fe5a0a9..ba1b79f 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -166,6 +166,16 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { refresh() } + fun markChem(id: Int) { + progress.markChem(id) + refresh() + } + + fun markBio(id: Int) { + progress.markBio(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 68585c2..ecb2ddc 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -35,6 +35,8 @@ import org.maturita.maturita.ui.LessonListScreen import org.maturita.maturita.ui.LitYearsScreen import org.maturita.maturita.ui.LessonRow import org.maturita.maturita.ui.LitQuizScreen +import org.maturita.maturita.ui.SciMapScreen +import org.maturita.maturita.ui.SciQuizScreen import org.maturita.maturita.ui.MluvExercise import org.maturita.maturita.ui.NetQuizScreen import org.maturita.maturita.ui.NetYearsScreen @@ -346,6 +348,35 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.Lit4Ex -> LitQuizScreen(vm, 4, r.id) + Route.SciMap -> SciMapScreen(vm) + Route.ChemMap -> LessonListScreen( + vm, vm.tr("Chemie"), vm.tr("chem_sub"), + vm.content.chem.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.chemDone(id), Route.ChemLesson(id)) + }, + ) + is Route.ChemLesson -> { + val l = vm.content.chemLesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.ChemEx(r.id)) + } + } + is Route.ChemEx -> SciQuizScreen(vm, "chem", r.id) + Route.BioMap -> LessonListScreen( + vm, vm.tr("Biologie"), vm.tr("bio_sub"), + vm.content.bio.map { l -> + val id = l.int("id") + LessonRow(vm.tr(l.str("titleKey")), vm.progress.bioDone(id), Route.BioLesson(id)) + }, + ) + is Route.BioLesson -> { + val l = vm.content.bioLesson(r.id) + if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) { + vm.replace(Route.BioEx(r.id)) + } + } + is Route.BioEx -> SciQuizScreen(vm, "bio", 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 79bf595..2e0a5d2 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -43,6 +43,13 @@ sealed class Route(val page: String) { data object Lit4Map : Route("lit4map") data class Lit4Lesson(val id: Int) : Route("lit4unit$id") data class Lit4Ex(val id: Int) : Route("lit4ex$id") + data object SciMap : Route("scimap") + data object ChemMap : Route("chemmap") + data class ChemLesson(val id: Int) : Route("chemunit$id") + data class ChemEx(val id: Int) : Route("chemex$id") + data object BioMap : Route("biomap") + data class BioLesson(val id: Int) : Route("biounit$id") + data class BioEx(val id: Int) : Route("bioex$id") data object CzechMap : Route("czechmap") data object Mluvnice : Route("mluvnice") data class MluvEx(val n: Int) : Route("mluve$n") @@ -60,7 +67,8 @@ sealed class Route(val page: String) { 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 || this is LitLesson || this is LitEx || this is Lit2Lesson || this is Lit2Ex || - this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex + this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex || + this is ChemLesson || this is ChemEx || this is BioLesson || this is BioEx /** Full stack from the practice root to this screen, so back always goes up one level. */ fun stack(): List = when (this) { @@ -105,6 +113,13 @@ sealed class Route(val page: String) { Lit4Map -> listOf(Subjects, CzechMap, LitYears, Lit4Map) is Lit4Lesson -> listOf(Subjects, CzechMap, LitYears, Lit4Map, this) is Lit4Ex -> listOf(Subjects, CzechMap, LitYears, Lit4Map, Lit4Lesson(id), this) + SciMap -> listOf(Subjects, SciMap) + ChemMap -> listOf(Subjects, SciMap, ChemMap) + is ChemLesson -> listOf(Subjects, SciMap, ChemMap, this) + is ChemEx -> listOf(Subjects, SciMap, ChemMap, ChemLesson(id), this) + BioMap -> listOf(Subjects, SciMap, BioMap) + is BioLesson -> listOf(Subjects, SciMap, BioMap, this) + is BioEx -> listOf(Subjects, SciMap, BioMap, BioLesson(id), this) CzechMap -> listOf(Subjects, CzechMap) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) @@ -135,6 +150,9 @@ fun routeFromPage(page: String): Route? = when (page) { "lit2map" -> Route.Lit2Map "lit3map" -> Route.Lit3Map "lit4map" -> Route.Lit4Map + "scimap" -> Route.SciMap + "chemmap" -> Route.ChemMap + "biomap" -> Route.BioMap "czechmap" -> Route.CzechMap "mluvnice" -> Route.Mluvnice "readinglist" -> Route.ReadingList @@ -166,6 +184,18 @@ fun routeFromPage(page: String): Route? = when (page) { Regex("""hwex(\d+)""").matchEntire(page)?.let { return Route.HwEx(it.groupValues[1].toInt()) } + Regex("""chemunit(\d+)""").matchEntire(page)?.let { + return Route.ChemLesson(it.groupValues[1].toInt()) + } + Regex("""chemex(\d+)""").matchEntire(page)?.let { + return Route.ChemEx(it.groupValues[1].toInt()) + } + Regex("""biounit(\d+)""").matchEntire(page)?.let { + return Route.BioLesson(it.groupValues[1].toInt()) + } + Regex("""bioex(\d+)""").matchEntire(page)?.let { + return Route.BioEx(it.groupValues[1].toInt()) + } Regex("""lit4unit(\d+)""").matchEntire(page)?.let { return Route.Lit4Lesson(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 0772f7e..c12900e 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 @@ -37,6 +37,8 @@ class Content(root: JSONObject) { val lit2 = raw.arr("lit2") val lit3 = raw.arr("lit3") val lit4 = raw.arr("lit4") + val chem = raw.arr("chem") + val bio = raw.arr("bio") val mluvnice = raw.arr("mluvnice") val books = raw.arr("books") val changelog = raw.arr("changelog") @@ -52,6 +54,8 @@ class Content(root: JSONObject) { fun lit2Lesson(id: Int) = lit2.firstOrNull { it.int("id") == id } fun lit3Lesson(id: Int) = lit3.firstOrNull { it.int("id") == id } fun lit4Lesson(id: Int) = lit4.firstOrNull { it.int("id") == id } + fun chemLesson(id: Int) = chem.firstOrNull { it.int("id") == id } + fun bioLesson(id: Int) = bio.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 7ab7485..80a6d7d 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 @@ -53,6 +53,11 @@ class ProgressStore(context: Context) { fun lit4Done(id: Int) = prefs.getBoolean("lit4.$id", false) fun markLit4(id: Int) { prefs.edit().putBoolean("lit4.$id", true).apply() } + fun chemDone(id: Int) = prefs.getBoolean("chem.$id", false) + fun markChem(id: Int) { prefs.edit().putBoolean("chem.$id", true).apply() } + fun bioDone(id: Int) = prefs.getBoolean("bio.$id", false) + fun markBio(id: Int) { prefs.edit().putBoolean("bio.$id", true).apply() } + fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } @@ -155,6 +160,22 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum { sum.doneUnits += 1 } } + content.chem.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.chemDone(l.int("id"))) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + content.bio.forEach { l -> + sum.totalEx += 1 + sum.openUnits += 1 + if (p.bioDone(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 5b1db1b..2e87f55 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 @@ -849,6 +849,20 @@ fun NetQuizScreen(vm: AppViewModel, id: Int) { DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markNet(id) } } +@Composable +fun SciQuizScreen(vm: AppViewModel, area: String, id: Int) { + val lesson = (if (area == "bio") vm.content.bioLesson(id) else vm.content.chemLesson(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"))) { + if (area == "bio") vm.markBio(id) else vm.markChem(id) + } +} + @Composable fun LitQuizScreen(vm: AppViewModel, year: Int, id: Int) { val lesson = when (year) { 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 a37e246..5dd9789 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 @@ -32,6 +32,7 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.filled.Groups import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Quiz +import androidx.compose.material.icons.filled.Science import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.Lightbulb import androidx.compose.material3.Card @@ -220,6 +221,7 @@ private fun SubjectIcon(icon: String, open: Boolean) { "chip" -> ChipIcon(tint, 26.dp) "people" -> Icon(Icons.Filled.Groups, null, tint = tint, modifier = Modifier.size(26.dp)) "cz" -> CzechFlag(28.dp) + "flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp)) else -> Icon(Icons.Filled.Lock, null, tint = tint) } } @@ -307,6 +309,11 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { vm.content.on3.count { vm.progress.on3Done(it.int("id")) } + vm.content.on4.count { vm.progress.on4Done(it.int("id")) } } + "scimap" -> { + sum.totalEx = vm.content.chem.size + vm.content.bio.size + sum.doneEx = vm.content.chem.count { vm.progress.chemDone(it.int("id")) } + + vm.content.bio.count { vm.progress.bioDone(it.int("id")) } + } "czechmap" -> { val litDone = vm.content.lit.count { vm.progress.litDone(it.int("id")) } + vm.content.lit2.count { vm.progress.lit2Done(it.int("id")) } + @@ -540,6 +547,16 @@ private fun buildSearch(vm: AppViewModel): List { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit${l.int("id")}", extra = "literatura") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex${l.int("id")}") } + add(vm.tr("Chemie"), vm.tr("search_lesson"), "chemmap", extra = "chemie") + add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra = "biologie") + vm.content.chem.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "chemunit${l.int("id")}", extra = "chemie") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "chemex${l.int("id")}") + } + vm.content.bio.forEach { l -> + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "biounit${l.int("id")}", extra = "biologie") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "bioex${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") @@ -666,6 +683,24 @@ fun LitYearsScreen(vm: AppViewModel) { } } +@Composable +fun SciMapScreen(vm: AppViewModel) { + DetailScaffold(vm, vm.tr("Základy Přírodopisných věd"), vm.tr("sci_sub")) { inner -> + LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { + item { + NavRow( + title = vm.tr("Chemie"), + subtitle = vm.tr("chem_sub"), + leading = { Icon(Icons.Filled.Science, null, tint = MaterialTheme.colorScheme.primary) }, + ) { vm.go(Route.ChemMap) } + } + item { + NavRow(title = vm.tr("Biologie"), subtitle = vm.tr("bio_sub")) { vm.go(Route.BioMap) } + } + } + } +} + @Composable fun CzechMapScreen(vm: AppViewModel) { DetailScaffold(vm, vm.tr("Český jazyk a literatura"), vm.tr("czech_sub")) { inner -> diff --git a/data/changelog.txt b/data/changelog.txt index f9970d0..29ffc1c 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -7,6 +7,10 @@ # en: bullet --- 2026-10-06 +cs: Základy přírodopisných věd: chemie a biologie, v každé 8 lekcí podle osnov +cs: Každá lekce má výklad a kvíz se čtyřmi otázkami +en: Natural sciences: chemistry and biology, eight lessons each, from the syllabus +en: Every lesson has an overview and a four-question quiz cs: Český jazyk: literatura ve čtyřech ročnících podle osnov středních škol cs: 32 lekcí od starověku po současnost, každá s výkladem a kvízem en: Czech: literature across four years, following the secondary-school syllabus diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift index 1bae8c9..a46f6fe 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -168,6 +168,16 @@ final class AppModel: ObservableObject { refresh() } + func markChem(_ id: Int) { + progress.markChem(id) + refresh() + } + + func markBio(_ id: Int) { + progress.markBio(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 8f93c38..5a80bd7 100644 --- a/ios/Graduately/Data/Content.swift +++ b/ios/Graduately/Data/Content.swift @@ -39,6 +39,8 @@ final class Content { let lit2: [J] let lit3: [J] let lit4: [J] + let chem: [J] + let bio: [J] let mluvnice: [J] let books: [J] let changelog: [J] @@ -78,6 +80,8 @@ final class Content { lit2 = raw.arr("lit2") lit3 = raw.arr("lit3") lit4 = raw.arr("lit4") + chem = raw.arr("chem") + bio = raw.arr("bio") mluvnice = raw.arr("mluvnice") books = raw.arr("books") changelog = raw.arr("changelog") @@ -94,6 +98,8 @@ final class Content { func lit2Lesson(_ id: Int) -> J? { lit2.first { $0.int("id") == id } } func lit3Lesson(_ id: Int) -> J? { lit3.first { $0.int("id") == id } } func lit4Lesson(_ id: Int) -> J? { lit4.first { $0.int("id") == id } } + func chemLesson(_ id: Int) -> J? { chem.first { $0.int("id") == id } } + func bioLesson(_ id: Int) -> J? { bio.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 d84eec7..e806a67 100644 --- a/ios/Graduately/Data/ProgressStore.swift +++ b/ios/Graduately/Data/ProgressStore.swift @@ -55,6 +55,11 @@ final class ProgressStore { func lit4Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit4.\(id)") } func markLit4(_ id: Int) { defaults.set(true, forKey: "lit4.\(id)") } + func chemDone(_ id: Int) -> Bool { defaults.bool(forKey: "chem.\(id)") } + func markChem(_ id: Int) { defaults.set(true, forKey: "chem.\(id)") } + func bioDone(_ id: Int) -> Bool { defaults.bool(forKey: "bio.\(id)") } + func markBio(_ id: Int) { defaults.set(true, forKey: "bio.\(id)") } + func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } @@ -162,6 +167,22 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum { sum.doneUnits += 1 } } + for l in content.chem { + sum.totalEx += 1 + sum.openUnits += 1 + if p.chemDone(l.int("id")) { + sum.doneEx += 1 + sum.doneUnits += 1 + } + } + for l in content.bio { + sum.totalEx += 1 + sum.openUnits += 1 + if p.bioDone(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 607f529..2ef7701 100644 --- a/ios/Graduately/Route.swift +++ b/ios/Graduately/Route.swift @@ -49,6 +49,13 @@ enum Route: Hashable { case lit4Map case lit4Lesson(Int) case lit4Ex(Int) + case sciMap + case chemMap + case chemLesson(Int) + case chemEx(Int) + case bioMap + case bioLesson(Int) + case bioEx(Int) case czechMap case mluvnice case mluvEx(Int) @@ -100,6 +107,13 @@ enum Route: Hashable { case .lit4Map: return "lit4map" case .lit4Lesson(let id): return "lit4unit\(id)" case .lit4Ex(let id): return "lit4ex\(id)" + case .sciMap: return "scimap" + case .chemMap: return "chemmap" + case .chemLesson(let id): return "chemunit\(id)" + case .chemEx(let id): return "chemex\(id)" + case .bioMap: return "biomap" + case .bioLesson(let id): return "biounit\(id)" + case .bioEx(let id): return "bioex\(id)" case .czechMap: return "czechmap" case .mluvnice: return "mluvnice" case .mluvEx(let n): return "mluve\(n)" @@ -200,6 +214,20 @@ enum Route: Hashable { return [.czechMap, .litYears, .lit4Map, .lit4Lesson(id)] case .lit4Ex(let id): return [.czechMap, .litYears, .lit4Map, .lit4Lesson(id), .lit4Ex(id)] + case .sciMap: + return [.sciMap] + case .chemMap: + return [.sciMap, .chemMap] + case .chemLesson(let id): + return [.sciMap, .chemMap, .chemLesson(id)] + case .chemEx(let id): + return [.sciMap, .chemMap, .chemLesson(id), .chemEx(id)] + case .bioMap: + return [.sciMap, .bioMap] + case .bioLesson(let id): + return [.sciMap, .bioMap, .bioLesson(id)] + case .bioEx(let id): + return [.sciMap, .bioMap, .bioLesson(id), .bioEx(id)] case .czechMap: return [.czechMap] case .mluvnice: @@ -238,6 +266,9 @@ func routeFromPage(_ page: String) -> Route? { case "lit2map": return .lit2Map case "lit3map": return .lit3Map case "lit4map": return .lit4Map + case "scimap": return .sciMap + case "chemmap": return .chemMap + case "biomap": return .bioMap case "czechmap": return .czechMap case "mluvnice": return .mluvnice case "readinglist": return .readingList @@ -281,6 +312,18 @@ 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: /chemunit(\d+)/), let n = Int(m.1) { + return .chemLesson(n) + } + if let m = page.wholeMatch(of: /chemex(\d+)/), let n = Int(m.1) { + return .chemEx(n) + } + if let m = page.wholeMatch(of: /biounit(\d+)/), let n = Int(m.1) { + return .bioLesson(n) + } + if let m = page.wholeMatch(of: /bioex(\d+)/), let n = Int(m.1) { + return .bioEx(n) + } if let m = page.wholeMatch(of: /lit4unit(\d+)/), let n = Int(m.1) { return .lit4Lesson(n) } diff --git a/ios/Graduately/UI/Components.swift b/ios/Graduately/UI/Components.swift index 0ea5c4e..4aba45b 100644 --- a/ios/Graduately/UI/Components.swift +++ b/ios/Graduately/UI/Components.swift @@ -607,6 +607,7 @@ func subjectSymbol(_ icon: String) -> String { case "chip": return "cpu" case "cz": return "text.book.closed.fill" case "people": return "person.2.fill" + case "flask": return "flask.fill" default: return "lock.fill" } } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index a33db07..bd32f71 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -625,6 +625,27 @@ struct NetQuizScreen: View { } } +struct SciQuizScreen: View { + @ObservedObject var vm: AppModel + let area: String + let id: Int + + var body: some View { + let lesson = area == "bio" ? vm.content.bioLesson(id) : vm.content.chemLesson(id) + if let lesson { + let spec = J([ + "type": "choice", + "title": vm.tr(lesson.str("exTitleKey")), + "sub": lesson.str("quizHeadKey"), + "questions": lesson.rawArray("quiz"), + ]) + DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) { + if area == "bio" { vm.markBio(id) } else { vm.markChem(id) } + } + } + } +} + struct LitYearQuizScreen: View { @ObservedObject var vm: AppModel let year: Int diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index c1c064b..4afc829 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -695,6 +695,10 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { + 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 "scimap": + sum.totalEx = vm.content.chem.count + vm.content.bio.count + sum.doneEx = vm.content.chem.filter { vm.progress.chemDone($0.int("id")) }.count + + vm.content.bio.filter { vm.progress.bioDone($0.int("id")) }.count case "czechmap": let litCount = vm.content.lit.count + vm.content.lit2.count + vm.content.lit3.count + vm.content.lit4.count let litDone = vm.content.lit.filter { vm.progress.litDone($0.int("id")) }.count @@ -925,6 +929,16 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit\(l.int("id"))", extra: "literatura") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex\(l.int("id"))") } + add(vm.tr("Chemie"), vm.tr("search_lesson"), "chemmap", extra: "chemie") + add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra: "biologie") + for l in vm.content.chem { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "chemunit\(l.int("id"))", extra: "chemie") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "chemex\(l.int("id"))") + } + for l in vm.content.bio { + add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "biounit\(l.int("id"))", extra: "biologie") + add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "bioex\(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 { @@ -1471,6 +1485,50 @@ struct LitYearsScreen: View { } } +struct SciMapScreen: View { + @ObservedObject var vm: AppModel + + var body: some View { + #if os(macOS) + MacFillGrid(count: 2, minWidth: 260, minHeight: 180) { i in + if i == 0 { + NavigationLink(value: Route.chemMap) { + MacLinkCard(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub"), systemImage: "flask.fill") + } + .buttonStyle(.plain) + } else { + NavigationLink(value: Route.bioMap) { + MacLinkCard(title: vm.tr("Biologie"), subtitle: vm.tr("bio_sub"), systemImage: "leaf.fill") + } + .buttonStyle(.plain) + } + } + .navigationTitle(vm.tr("Základy Přírodopisných věd")) + .navigationSubtitle(vm.tr("sci_sub")) + #else + List { + NavigationLink(value: Route.chemMap) { + Label { + ListRowLabel(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub")) + } icon: { + Image(systemName: "flask.fill") + } + } + NavigationLink(value: Route.bioMap) { + Label { + ListRowLabel(title: vm.tr("Biologie"), subtitle: vm.tr("bio_sub")) + } icon: { + Image(systemName: "leaf.fill") + } + } + } + .navigationTitle(vm.tr("Základy Přírodopisných věd")) + .navigationSubtitle(vm.tr("sci_sub")) + .appListStyle() + #endif + } +} + struct CzechMapScreen: View { @ObservedObject var vm: AppModel @@ -2151,6 +2209,50 @@ struct RouteDestination: View { } case .lit4Ex(let id): LitYearQuizScreen(vm: vm, year: 4, id: id) + case .sciMap: + SciMapScreen(vm: vm) + case .chemMap: + LessonListScreen( + vm: vm, + title: vm.tr("Chemie"), + subtitle: vm.tr("chem_sub"), + rows: vm.content.chem.map { l in + let id = l.int("id") + return LessonRow( + id: "c\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.chemDone(id), + destination: .chemLesson(id) + ) + } + ) + case .chemLesson(let id): + if let l = vm.content.chemLesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .chemEx(id)) + } + case .chemEx(let id): + SciQuizScreen(vm: vm, area: "chem", id: id) + case .bioMap: + LessonListScreen( + vm: vm, + title: vm.tr("Biologie"), + subtitle: vm.tr("bio_sub"), + rows: vm.content.bio.map { l in + let id = l.int("id") + return LessonRow( + id: "b\(id)", + title: vm.tr(l.str("titleKey")), + done: vm.progress.bioDone(id), + destination: .bioLesson(id) + ) + } + ) + case .bioLesson(let id): + if let l = vm.content.bioLesson(id) { + SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .bioEx(id)) + } + case .bioEx(let id): + SciQuizScreen(vm: vm, area: "bio", id: id) case .mluvnice: LessonListScreen( vm: vm, diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index 9a111d9..4584dd6 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -1109,6 +1109,8 @@ def main() -> int: lit2_lessons = lit_pack("lit2") lit3_lessons = lit_pack("lit3") lit4_lessons = lit_pack("lit4") + chem_lessons = lit_pack("chem") + bio_lessons = lit_pack("bio") on_lessons = [] for i in range(1, 18): @@ -1559,7 +1561,7 @@ def main() -> int: {"key": "English", "open": False, "target": None, "icon": "lock"}, {"key": "Matematika", "open": False, "target": None, "icon": "lock"}, {"key": "Fyzika", "open": False, "target": None, "icon": "lock"}, - {"key": "Základy Přírodopisných věd", "open": False, "target": None, "icon": "lock"}, + {"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"}, {"key": "Technická grafika", "open": False, "target": None, "icon": "lock"}, {"key": "Prezentační grafika", "open": False, "target": None, "icon": "lock"}, {"key": "Programování", "open": False, "target": None, "icon": "lock"}, @@ -1581,6 +1583,8 @@ def main() -> int: "lit2": lit2_lessons, "lit3": lit3_lessons, "lit4": lit4_lessons, + "chem": chem_lessons, + "bio": bio_lessons, "on2": on2_lessons, "on3": on3_lessons, "on4": on4_lessons, @@ -1620,6 +1624,8 @@ def main() -> int: print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}") print(f" on lessons: {sum(1 for L in on_lessons if L['slides'])}") print(f" lit lessons: {sum(1 for L in lit_lessons + lit2_lessons + lit3_lessons + lit4_lessons if L['slides'])}") + print(f" chem lessons: {sum(1 for L in chem_lessons if L['slides'])}") + print(f" bio lessons: {sum(1 for L in bio_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'])}") @@ -1679,6 +1685,7 @@ def main() -> int: for label, block in ( ("lit", lit_lessons), ("lit2", lit2_lessons), ("lit3", lit3_lessons), ("lit4", lit4_lessons), + ("chem", chem_lessons), ("bio", bio_lessons), ): for L in block: if not L["slides"]: diff --git a/src/biologie.c b/src/biologie.c new file mode 100644 index 0000000..8612458 --- /dev/null +++ b/src/biologie.c @@ -0,0 +1,617 @@ +#include "graduately.h" + +/* Biology for Základy přírodopisných věd. + * Follows the secondary-school outline: general biology, a survey of + * organisms, human biology, genetics and ecology. */ + +NetLesson bio_lessons[SCI_N] = { + { .n_slides = 3, .unit_page = "biounit1", .ex_page = "bioex1" }, + { .n_slides = 3, .unit_page = "biounit2", .ex_page = "bioex2" }, + { .n_slides = 3, .unit_page = "biounit3", .ex_page = "bioex3" }, + { .n_slides = 3, .unit_page = "biounit4", .ex_page = "bioex4" }, + { .n_slides = 3, .unit_page = "biounit5", .ex_page = "bioex5" }, + { .n_slides = 3, .unit_page = "biounit6", .ex_page = "bioex6" }, + { .n_slides = 3, .unit_page = "biounit7", .ex_page = "bioex7" }, + { .n_slides = 3, .unit_page = "biounit8", .ex_page = "bioex8" }, +}; + +static GtkWidget *build_bio_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Život", "Čím se živé liší", + "Tip: metabolismus je přeměna látek a energie, ne jen dýchání.", + { + "Živé soustavy mají metabolismus, dráždivost, růst a vývin.", + "Rozmnožují se a předávají dědičnou informaci.", + "Udržují stálé vnitřní prostředí, homeostázu.", + "Bez příjmu energie a látek z okolí život nepokračuje.", + NULL, + }, + }, + { + "2 / 3 • Řada", "Od buňky k ekosystému", + "Tip: populace je jeden druh, společenstvo je víc druhů pohromadě.", + { + "Buňka je základní stavební a funkční jednotka živého.", + "Tkáně tvoří orgány, orgány soustavy a soustavy organismus.", + "Populace jsou jedinci téhož druhu na jednom místě.", + "Společenstvo s neživým prostředím tvoří ekosystém.", + NULL, + }, + }, + { + "3 / 3 • Výživa", "Autotrof a heterotrof", + "Tip: zelená rostlina je autotrof, živočich heterotrof.", + { + "Autotrof tvoří organické látky z anorganických, hlavně fotosyntézou.", + "Heterotrof přijímá hotové organické látky.", + "Život na Zemi je vázaný na vodu a na zdroj energie.", + "Buňka vznikla dřív než mnohobuněčné organismy.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[0], "bio_unit1", "bio_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je základní jednotka živých organismů?", + {"Atom", "Buňka", "Ekosystém", "Orgán"}, + 4, 1}, + {"Co je metabolismus?", + {"Jen pohyb živočicha", + "Přeměna látek a energie v organismu", + "Počet chromozomů", + "Rozmnožování viru mimo buňku"}, + 4, 1}, + {"Co je populace?", + {"Jedinci jednoho druhu na určitém místě", + "Všechny druhy v ekosystému", + "Jedna buňka", + "Neživé složky prostředí"}, + 4, 0}, + {"Čím se autotrof liší od heterotrofa?", + {"Nemá buňky", + "Sám tvoří organické látky z anorganických", + "Nepotřebuje energii", + "Žije jen bez vody"}, + 4, 1}, + }; + static const char *hints[] = { + "Buněčná teorie: nová buňka vzniká z buňky.", + "Metabolismus zahrnuje výstavbu i rozklad látek.", + "Společenstvo už sdružuje populace různých druhů.", + "Rostliny fotosyntetizují. Živočichové a houby jsou heterotrofní.", + }; + + return sci_mcq_page(1, 0, "biounit1", "bio_ex1_title", "bio_quiz1_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Dva typy", "Prokaryota a eukaryota", + "Tip: bakterie nemají jádro, živočišná a rostlinná buňka ano.", + { + "Prokaryotní buňka nemá pravé jádro. Patří sem bakterie.", + "Eukaryotní buňka má jádro oddělené membránou a má organely.", + "Plazmatická membrána ohraničuje každou buňku.", + "Ribozomy vyrábějí bílkoviny v obou typech buněk.", + NULL, + }, + }, + { + "2 / 3 • Organely", "Co je v eukaryotní buňce", + "Tip: chloroplast mají rostliny, mitochondrii mají i živočichové.", + { + "Jádro obsahuje DNA a řídí buňku.", + "Mitochondrie uvolňují energii buněčným dýcháním.", + "Chloroplasty provádějí fotosyntézu a jsou v rostlinách.", + "Rostlinná buňka má navíc buněčnou stěnu a vakuolu.", + NULL, + }, + }, + { + "3 / 3 • Dělení", "Mitóza a meióza", + "Tip: člověk má 46 chromozomů, pohlavní buňka 23.", + { + "Mitóza dává dvě tělní buňky se stejnou genetickou informací.", + "Slouží k růstu, obnově a nepohlavnímu rozmnožování.", + "Meióza tvoří pohlavní buňky s polovičním počtem chromozomů.", + "Oplozením se počet chromozomů vrací na původní.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[1], "bio_unit2", "bio_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Která buňka je prokaryotní?", + {"Bakterie", "Rostlinná buňka", "Živočišná buňka", "Buňka houby s jádrem"}, + 4, 0}, + {"Kde probíhá fotosyntéza v rostlinné buňce?", + {"V mitochondrii", "V chloroplastu", "V ribozomu", "Jen v jádře"}, + 4, 1}, + {"K čemu slouží mitóza?", + {"Ke vzniku pohlavních buněk s poloviční sadou", + "Ke vzniku dvou geneticky stejných tělních buněk", + "K tvorbě viru", + "K odstranění jádra"}, + 4, 1}, + {"Kolik chromozomů má lidská pohlavní buňka?", + {"46", "23", "92", "2"}, + 4, 1}, + }; + static const char *hints[] = { + "Prokaryota nemají pravé jádro. Eukaryota ano.", + "Chloroplast je zelený a je v rostlinách a některých protistech.", + "Meióza, ne mitóza, snižuje počet chromozomů na polovinu.", + "Tělní buňka má 46, spermie a vajíčko po 23.", + }; + + return sci_mcq_page(1, 1, "biounit2", "bio_ex2_title", "bio_quiz2_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Virus", "Na hranici živého", + "Tip: virus není buňka a sám se mimo hostitele nemnoží.", + { + "Virus má nukleovou kyselinu v bílkovinném obalu.", + "Nemá vlastní metabolismus a množí se jen v hostitelské buňce.", + "Způsobuje třeba chřipku. Antibiotika na virus nepůsobí.", + "Očkování učí imunitu poznat virus dřív, než nemoc propukne.", + NULL, + }, + }, + { + "2 / 3 • Bakterie a houby", "Rozkladači a původci", + "Tip: houba není rostlina, živí se hotovými organickými látkami.", + { + "Bakterie jsou prokaryota. Některé jsou symbionti, jiné patogeny.", + "V průmyslu kvasí jogurt, v přírodě rozkládají organické zbytky.", + "Houby mají buněčnou stěnu z chitinu a nemají chlorofyl.", + "Kvasinky, plísně i kloboukaté houby patří mezi houby.", + NULL, + }, + }, + { + "3 / 3 • Rostliny", "Kořen, stonek, list", + "Tip: fotosyntéza běží hlavně v listech, v chloroplastech.", + { + "Rostliny jsou autotrofní a mají buněčnou stěnu z celulózy.", + "Kořen přijímá vodu a minerály a upevňuje rostlinu.", + "Stonek nese listy a vede látky, list je hlavní místo fotosyntézy.", + "Květ slouží rozmnožování, semeno obsahuje zárodek.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[2], "bio_unit3", "bio_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Proč se virus množí jen v hostiteli?", + {"Má vlastní chloroplast, ale bez světla", + "Nemá buněčný metabolismus", + "Je to prokaryotní buňka", + "Má jádro a mitochondrie"}, + 4, 1}, + {"Čím se houby liší od rostlin?", + {"Nemají buňky", + "Jsou heterotrofní a nemají chlorofyl", + "Mají chloroplasty navíc", + "Jsou vždy jednobuněčné prokaryota"}, + 4, 1}, + {"Kde v rostlině probíhá většina fotosyntézy?", + {"V kořeni", "V listu", "Jen v květu", "Ve dřevu bez chloroplastů"}, + 4, 1}, + {"Z čeho je buněčná stěna rostlin?", + {"Z chitinu", "Z celulózy", "Z cholesterolu", "Z peptidoglykanu jako u všech hub"}, + 4, 1}, + }; + static const char *hints[] = { + "Virus je nukleová kyselina v obalu. Buňku si půjčuje.", + "Chitin je stěna hub. Rostliny mají celulózu a chlorofyl.", + "List má nejvíc chloroplastů. Kořen je většinou pod zemí.", + "Rostliny: celulóza. Houby: chitin. Bakterie: peptidoglykan.", + }; + + return sci_mcq_page(1, 2, "biounit3", "bio_ex3_title", "bio_quiz3_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Bezobratlí", "Hmyz a další", + "Tip: hmyz má tři páry nohou a tělo ze tří částí.", + { + "Bezobratlí nemají vnitřní kostru z obratlů.", + "Kroužkovci, třeba žížala, mají článkované tělo.", + "Měkkýši mají měkké tělo, často schránku. Patří sem hlemýžď.", + "Hmyz má hlavu, hruď a zadeček a šest nohou.", + NULL, + }, + }, + { + "2 / 3 • Obratlovci", "Pět tříd, které škola probírá", + "Tip: ptáci i savci mají stálou tělesnou teplotu.", + { + "Ryby dýchají žábrami a žijí ve vodě.", + "Obojživelníci mají larvu ve vodě a vlhkou kůži, třeba žába.", + "Plazi mají suchou kůži se šupinami a vejce s obalem.", + "Ptáci mají peří, savci srst a mláďata krmí mlékem.", + NULL, + }, + }, + { + "3 / 3 • Vývoj", "Jak číst příbuznost", + "Tip: vývoj není žebřík od horšího k dokonalému člověku.", + { + "Druhy se mění v čase. Společný předek vysvětluje podobnost.", + "Obratlovci jsou jedna větev, ne vrchol všech živočichů.", + "Hmyz je druhově nejbohatší skupina živočichů.", + "Přizpůsobení prostředí se týká stavby těla i chování.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[3], "bio_unit4", "bio_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kolik nohou má hmyz?", + {"Čtyři", "Šest", "Osm", "Deset"}, + 4, 1}, + {"Čím dýchají ryby?", + {"Plicemi jako savci", "Žábrami", "Jen kůží bez cév", "Vzdušnicemi jako hmyz"}, + 4, 1}, + {"Který znak patří ptákům?", + {"Mléčné žlázy", "Peří", "Žábry u dospělce", "Tělo bez kostry"}, + 4, 1}, + {"Čím savci živí mláďata?", + {"Vždy jen žloutkem mimo tělo", "Mlékem", + "Fotosyntézou mláděte", "Filtrací vody žábrami"}, + 4, 1}, + }; + static const char *hints[] = { + "Tři páry nohou jsou šest. Pavouci mají osm a nejsou hmyz.", + "Žábry berou kyslík z vody. Dospělá žába už dýchá plícemi a kůží.", + "Peří je ptačí znak. Srst a mléko patří savcům.", + "Mléčné žlázy jsou znak savců. Ptáci vejce zahřívají.", + }; + + return sci_mcq_page(1, 3, "biounit4", "bio_ex4_title", "bio_quiz4_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Pohyb", "Kostra a svaly", + "Tip: kost je živá tkáň, ne jen minerál.", + { + "Kostra dává oporu, chrání orgány a je zásobárna vápníku.", + "Sval se upíná na kost šlachou a pohyb vzniká jeho stahem.", + "Kloub spojuje kosti pohyblivě.", + "Páteř chrání míchu.", + NULL, + }, + }, + { + "2 / 3 • Oběh", "Srdce a krev", + "Tip: malý oběh vede krev do plic, velký do těla.", + { + "Srdce člověka má dvě síně a dvě komory.", + "Pravá komora žene krev do plic, levá do těla.", + "Tepny vedou krev ze srdce, žíly do srdce.", + "V plicních sklípcích se do krve dostává kyslík a odchází oxid uhličitý.", + NULL, + }, + }, + { + "3 / 3 • Trávení", "Od úst k ledvinám", + "Tip: živiny se vstřebávají hlavně v tenkém střevě.", + { + "V ústech začíná mechanické a chemické zpracování potravy.", + "Žaludek kyselinou a enzymy pokračuje v trávení bílkovin.", + "Tenké střevo živiny vstřebá, tlusté hlavně vodu.", + "Ledviny filtrují krev a tvoří moč, játra zpracovávají živiny.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[4], "bio_unit5", "bio_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kolik dutin má srdce člověka?", + {"Dvě", "Tři", "Čtyři", "Jednu"}, + 4, 2}, + {"Kam vede malý krevní oběh?", + {"Z levé komory do celého těla", "Z pravé komory do plic", + "Z jater do žaludku", "Z ledvin do střeva"}, + 4, 1}, + {"Kde se vstřebává většina živin?", + {"V jícnu", "V tenkém střevě", "V tlustém střevě jako cukry", "Ve slezině"}, + 4, 1}, + {"Co tvoří ledviny?", + {"Žluč", "Moč", "Insulin", "Slinu"}, + 4, 1}, + }; + static const char *hints[] = { + "Dvě síně a dvě komory. Mezi pravou a levou polovinou je přepážka.", + "Malý oběh okysličuje krev. Velký ji rozvádí tělem.", + "Klky tenkého střeva zvětšují plochu. Tlusté střevo hlavně zahušťuje.", + "Játra tvoří žluč. Slinivka insulin. Ledviny moč.", + }; + + return sci_mcq_page(1, 4, "biounit5", "bio_ex5_title", "bio_quiz5_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Nervy", "Řízení organismu", + "Tip: mozek a mícha jsou ústřední nervová soustava.", + { + "Nervová soustava vede vzruchy rychle a cíleně.", + "Mozek a mícha tvoří ústřední část, nervy obvodovou.", + "Reflex je odpověď na podnět, třeba ucuknutí od horka.", + "Smysly převádějí podněty z okolí na vzruch.", + NULL, + }, + }, + { + "2 / 3 • Hormony", "Pomalejší řízení", + "Tip: insulin ze slinivky snižuje hladinu glukózy v krvi.", + { + "Hormony jsou signální látky přenášené krví.", + "Štítná žláza ovlivňuje metabolismus, nadledviny stresovou odpověď.", + "Insulin snižuje glykemii, glukagon ji zvyšuje.", + "Pohlavní hormony řídí dospívání a rozmnožování.", + NULL, + }, + }, + { + "3 / 3 • Obrana", "Rozmnožování a imunita", + "Tip: očkování připraví imunitu, aniž by člověk nemoc prodělal.", + { + "Spermie a vajíčko mají po 23 chromozomech, zygota 46.", + "Oplození spojí genetickou informaci obou rodičů.", + "Bílé krvinky a protilátky patří k imunitě.", + "Imunita rozliší vlastní buňky od cizích a od patogenů.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[5], "bio_unit6", "bio_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co tvoří ústřední nervovou soustavu?", + {"Jen obvodové nervy", "Mozek a mícha", + "Jen štítná žláza", "Srdce a plíce"}, + 4, 1}, + {"Co dělá insulin?", + {"Zvyšuje množství glukózy v krvi", + "Snižuje hladinu glukózy v krvi", + "Tvoří moč", + "Nahrazuje červené krvinky"}, + 4, 1}, + {"Kolik chromozomů má lidská zygota?", + {"23", "46", "92 v každé běžné tělní buňce navíc", "2"}, + 4, 1}, + {"K čemu je očkování?", + {"Aby virus získal buňku", + "Aby se imunita naučila poznat patogen předem", + "Aby nahradilo všechny bílé krvinky natrvalo", + "Aby zastavilo mitózu"}, + 4, 1}, + }; + static const char *hints[] = { + "Obvodové nervy spojují ústředí se smysly a svaly.", + "Insulin vyrábí slinivka. Jeho nedostatek je podstatou cukrovky 1. typu.", + "23 + 23 = 46. Pohlavní buňky jsou haploidní, zygota diploidní.", + "Vakcína je antigen, který imunitní paměť připraví.", + }; + + return sci_mcq_page(1, 5, "biounit6", "bio_ex6_title", "bio_quiz6_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • DNA", "Gen a alela", + "Tip: gen je úsek DNA, alela je jeho konkrétní forma.", + { + "DNA nese dědičnou informaci a je v chromozomech.", + "Gen je úsek DNA, který ovlivňuje určitý znak.", + "Alely jsou varianty téhož genu.", + "Dominantní alela se projeví i v jedné kopii, recesivní až ve dvou.", + NULL, + }, + }, + { + "2 / 3 • Mendel", "Genotyp a fenotyp", + "Tip: fenotyp je to, co vidíme, genotyp je zápis alel.", + { + "Johann Gregor Mendel sledoval dědičnost na hrachu.", + "Při křížení se alely rozcházejí do pohlavních buněk.", + "Genotyp je sestava alel, fenotyp je jejich projev.", + "Ne každý znak řídí jediný gen a prostředí projev také mění.", + NULL, + }, + }, + { + "3 / 3 • Změna", "Mutace a pohlaví", + "Tip: u člověka je typická žena XX a typický muž XY.", + { + "Mutace je změna DNA. Může být neutrální, škodlivá, nebo vzácně užitečná.", + "Mutace v pohlavní buňce se může přenést na potomka.", + "Pohlavní chromozomy člověka jsou X a Y.", + "Kombinace XX odpovídá ženě, XY muži.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[6], "bio_unit7", "bio_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je gen?", + {"Celá buňka", "Úsek DNA ovlivňující znak", + "Druh bílkoviny v potravě", "Hormon štítné žlázy"}, + 4, 1}, + {"Co je fenotyp?", + {"Sestava alel v buňce", "Pozorovatelný projev znaku", + "Počet ribozomů", "Vzorec glukózy"}, + 4, 1}, + {"Kdy se projeví recesivní alela?", + {"Vždycky, i když je v páru s dominantní", + "Až když jsou recesivní obě alely", + "Jen u bakterií", + "Jen jako mutace viru"}, + 4, 1}, + {"Která sestava chromozomů odpovídá muži?", + {"XX", "XY", "YY jako jediná možnost u savců", "Jen autozomy bez X a Y"}, + 4, 1}, + }; + static const char *hints[] = { + "Alela je varianta genu. Chromozom nese mnoho genů.", + "Genotyp je zápis, fenotyp je výsledek genů a prostředí.", + "Heterozygot s dominantní alelou má dominantní fenotyp.", + "XX je žena, XY muž. Y nese gen pro vývoj varlat.", + }; + + return sci_mcq_page(1, 6, "biounit7", "bio_ex7_title", "bio_quiz7_head", + qs, hints, 4); +} + +static GtkWidget *build_bio_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Úrovně", "Od jedince k biosféře", + "Tip: ekosystém zahrnuje živé i neživé.", + { + "Ekologie zkoumá vztahy organismů mezi sebou a k prostředí.", + "Populace, společenstvo a ekosystém jsou tři navazující úrovně.", + "Biosféra je všechny ekosystémy Země.", + "Abiotické faktory jsou světlo, voda, teplota, vzduch a půda.", + NULL, + }, + }, + { + "2 / 3 • Potravní vztahy", "Kdo koho živí", + "Tip: na každý další článek řetězce zbývá méně energie.", + { + "Producenti, hlavně rostliny, vážou energii fotosyntézou.", + "Konzumenti se živí jinými organismy.", + "Rozkladači, bakterie a houby, vracejí látky do koloběhu.", + "V potravním řetězci se většina energie ztrácí jako teplo.", + NULL, + }, + }, + { + "3 / 3 • Člověk", "Prostředí a ochrana", + "Tip: fotosyntéza váže CO2, dýchání a spalování ho vracejí.", + { + "Člověk mění krajinu, klima i biodiverzitu.", + "Znečištění, kácení a nadměrný lov snižují rozmanitost druhů.", + "Koloběh uhlíku spojuje fotosyntézu, dýchání a spalování.", + "Ochrana přírody drží životní podmínky i pro člověka.", + NULL, + }, + }, + }; + + return sci_unit_page(1, &bio_lessons[7], "bio_unit8", "bio_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_bio_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je ekosystém?", + {"Jen jedinci jednoho druhu", + "Společenstvo spolu s neživým prostředím", + "Jedna buňka", + "Jen atmosféra bez organismů"}, + 4, 1}, + {"Kdo je v ekosystému producent?", + {"Dravec", "Zelená rostlina", "Houba rozkladač", "Virus"}, + 4, 1}, + {"Co dělají rozkladači?", + {"Vyrábějí kyslík fotosyntézou jako hlavní producenti", + "Rozkládají organické zbytky a vracejí látky do koloběhu", + "Jsou vždy obratlovci", + "Nahrazují producenty v moři i na souši jako rostliny"}, + 4, 1}, + {"Proč je na vrcholu potravního řetězce málo energie?", + {"Protože se na každém článku část energie ztratí", + "Protože producenti energii nevytvářejí", + "Protože rozkladači energii zdvojnásobí", + "Protože slunce ekosystém neovlivňuje"}, + 4, 0}, + }; + static const char *hints[] = { + "Společenstvo jsou živé složky. Ekosystém k nim přidává neživé.", + "Producent je autotrof. Konzument je heterotrof, který někoho žere.", + "Bakterie a houby mineralizují zbytky. Rostliny je znovu využijí.", + "Škola často uvádí, že na další hladinu přejde jen malá část energie.", + }; + + return sci_mcq_page(1, 7, "biounit8", "bio_ex8_title", "bio_quiz8_head", + qs, hints, 4); +} + +void add_bio_pages(GtkStack *stack) { + typedef GtkWidget *(*SciBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + SciBuilder build_unit; + SciBuilder build_ex; + } pages[] = { + {"biounit1", "bioex1", build_bio_unit1_page, build_bio_unit1_exercise_page}, + {"biounit2", "bioex2", build_bio_unit2_page, build_bio_unit2_exercise_page}, + {"biounit3", "bioex3", build_bio_unit3_page, build_bio_unit3_exercise_page}, + {"biounit4", "bioex4", build_bio_unit4_page, build_bio_unit4_exercise_page}, + {"biounit5", "bioex5", build_bio_unit5_page, build_bio_unit5_exercise_page}, + {"biounit6", "bioex6", build_bio_unit6_page, build_bio_unit6_exercise_page}, + {"biounit7", "bioex7", build_bio_unit7_page, build_bio_unit7_exercise_page}, + {"biounit8", "bioex8", build_bio_unit8_page, build_bio_unit8_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/chemie.c b/src/chemie.c new file mode 100644 index 0000000..5169e35 --- /dev/null +++ b/src/chemie.c @@ -0,0 +1,604 @@ +#include "graduately.h" + +/* Chemistry for Základy přírodopisných věd. + * Follows the secondary-school outline (RVP for vocational schools, + * přírodovědné vzdělávání, and the gymnasium general-chemistry track): + * general chemistry, inorganic chemistry, organic chemistry, biochemistry. */ + +NetLesson chem_lessons[SCI_N] = { + { .n_slides = 3, .unit_page = "chemunit1", .ex_page = "chemex1" }, + { .n_slides = 3, .unit_page = "chemunit2", .ex_page = "chemex2" }, + { .n_slides = 3, .unit_page = "chemunit3", .ex_page = "chemex3" }, + { .n_slides = 3, .unit_page = "chemunit4", .ex_page = "chemex4" }, + { .n_slides = 3, .unit_page = "chemunit5", .ex_page = "chemex5" }, + { .n_slides = 3, .unit_page = "chemunit6", .ex_page = "chemex6" }, + { .n_slides = 3, .unit_page = "chemunit7", .ex_page = "chemex7" }, + { .n_slides = 3, .unit_page = "chemunit8", .ex_page = "chemex8" }, +}; + +static GtkWidget *build_chem_unit1_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Atom", "Z čeho je látka", + "Tip: protonové číslo je počet protonů, ne elektronů.", + { + "Atom má jádro z protonů a neutronů a obal z elektronů.", + "Proton má kladný náboj, elektron záporný, neutron je bez náboje.", + "Protonové číslo Z udává počet protonů a určuje prvek.", + "Nukleonové číslo A je součet protonů a neutronů.", + NULL, + }, + }, + { + "2 / 3 • Tabulka", "Periody a skupiny", + "Tip: perioda je řádek, skupina je sloupec.", + { + "Periodická soustava řadí prvky podle rostoucího protonového čísla.", + "Prvky v jedné skupině mají podobné chemické vlastnosti.", + "Číslo periody souvisí s počtem obsazených slupek.", + "Vlevo jsou kovy, vpravo nekovy, mezi nimi polokovy.", + NULL, + }, + }, + { + "3 / 3 • Částice", "Atom, molekula, ion", + "Tip: kation vzniká ztrátou elektronů, anion jejich přijetím.", + { + "Prvek tvoří atomy se stejným protonovým číslem.", + "Molekula je částice ze dvou nebo více atomů spojených vazbou.", + "Ion je nabitá částice: kation je kladný, anion záporný.", + "Izotopy jednoho prvku se liší počtem neutronů, ne protonů.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[0], "chem_unit1", "chem_unit1_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit1_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co udává protonové číslo?", + {"Počet protonů v jádře", "Počet neutronů v jádře", + "Součet protonů a elektronů v molekule", "Hmotnost atomu v gramech"}, + 4, 0}, + {"Jaký náboj má elektron?", + {"Kladný", "Záporný", "Žádný", "Stejný jako neutron"}, + 4, 1}, + {"Co mají společného prvky v jedné skupině?", + {"Stejné nukleonové číslo", "Podobné chemické vlastnosti", + "Stejný počet neutronů", "Stejné skupenství za pokojové teploty"}, + 4, 1}, + {"Co je v periodické soustavě perioda?", + {"Sloupec prvků", "Řádek prvků", + "Jen skupina vzácných plynů", "Rozdíl protonů a neutronů"}, + 4, 1}, + }; + static const char *hints[] = { + "Z je počet protonů a zároveň pořadí prvku v tabulce.", + "Proton je kladný, elektron záporný, neutron neutrální.", + "Skupina je sloupec. Podobné vlastnosti plynou z podobného obalu.", + "Perioda je řádek. Číslo periody souvisí s obsazenými slupkami.", + }; + + return sci_mcq_page(0, 0, "chemunit1", "chem_ex1_title", "chem_quiz1_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit2_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Ionty", "Iontová vazba", + "Tip: NaCl je typická iontová sloučenina.", + { + "Iontová vazba vzniká přenosem elektronů mezi atomy.", + "Kov odevzdá elektrony a stane se kationtem, nekov je přijme jako anion.", + "Opačné náboje se přitahují a drží krystal pohromadě.", + "Iontové látky mají vysoké teploty tání a v tavenině vedou proud.", + NULL, + }, + }, + { + "2 / 3 • Sdílení", "Kovalentní vazba", + "Tip: ve vodě atomy elektrony sdílejí, nepředávají si je natrvalo.", + { + "Kovalentní vazba vzniká sdílením elektronového páru.", + "Jednoduchá vazba je jeden pár, dvojná dva, trojná tři.", + "Nepolární vazba spojuje atomy se stejnou elektronegativitou, třeba H2.", + "Polární vazba vzniká mezi atomy s různou elektronegativitou, třeba v H2O.", + NULL, + }, + }, + { + "3 / 3 • Kovy", "Kovová vazba", + "Tip: volné elektrony vysvětlují lesk i vodivost kovů.", + { + "V kovu jsou kationty obklopené sdílenými elektrony.", + "Elektrony se mohou posouvat, proto kov vede proud a teplo.", + "Elektronegativita je schopnost atomu přitahovat elektrony vazby.", + "Velký rozdíl elektronegativit vede spíš k iontové vazbě.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[1], "chem_unit2", "chem_unit2_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit2_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Jaká vazba drží krystal chloridu sodného?", + {"Kovalentní nepolární", "Iontová", "Kovová", "Vodíkový můstek jako jediná"}, + 4, 1}, + {"Co je kovalentní vazba?", + {"Přenos elektronu z kovu na nekov", + "Sdílení elektronového páru", + "Pohyb volných elektronů mezi kationty kovu", + "Přitažlivost jen mezi molekulami vody"}, + 4, 1}, + {"Proč kovy vedou elektrický proud?", + {"Mají pevné jádro bez elektronů", + "Obsahují pohyblivé elektrony kovové vazby", + "Jsou vždy kapalné", + "Nemají protony"}, + 4, 1}, + {"Co je elektronegativita?", + {"Počet neutronů v jádře", + "Schopnost atomu přitahovat elektrony vazby", + "Hmotnost jednoho molu", + "Teplota varu kovu"}, + 4, 1}, + }; + static const char *hints[] = { + "Sodík předá elektron chloru. Vznikne Na+ a Cl−.", + "Kovalentní vazba je společný elektronový pár, ne trvalý přenos.", + "Kovová vazba má volné elektrony, které přenášejí náboj.", + "Čím větší rozdíl elektronegativit, tím polárnější nebo iontovější vazba.", + }; + + return sci_mcq_page(0, 1, "chemunit2", "chem_ex2_title", "chem_quiz2_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit3_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Rovnice", "Co se při reakci zachová", + "Tip: vyčíslit znamená srovnat počty atomů, ne měnit vzorce.", + { + "Při chemické reakci se atomy přeskupují, ale neztrácejí.", + "Zákon zachování hmotnosti: hmotnost reaktantů se rovná hmotnosti produktů.", + "V rovnici musí být na obou stranách stejný počet atomů každého prvku.", + "Koeficient před vzorcem násobí celou částici.", + NULL, + }, + }, + { + "2 / 3 • Mol", "Kolik je částic", + "Tip: molární hmotnost vody je přibližně 18 g/mol.", + { + "Mol je jednotka látkového množství.", + "Jeden mol obsahuje 6,022 × 10^23 částic, to je Avogadrova konstanta.", + "Molární hmotnost M je hmotnost jednoho molu v g/mol.", + "Látkové množství se počítá jako n = m / M.", + NULL, + }, + }, + { + "3 / 3 • Výpočet", "Od hmotnosti k molům", + "Tip: nejdřív sečti relativní atomové hmotnosti ve vzorci.", + { + "Relativní atomová hmotnost vodíku je 1, kyslíku 16, uhlíku 12.", + "Voda H2O má molární hmotnost 18 g/mol, oxid uhličitý CO2 má 44 g/mol.", + "18 g vody je 1 mol, tedy 6,022 × 10^23 molekul.", + "Stechiometrie porovnává moly podle koeficientů v rovnici.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[2], "chem_unit3", "chem_unit3_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit3_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co říká zákon zachování hmotnosti?", + {"Hmotnost produktů je vždy menší", + "Hmotnost reaktantů se rovná hmotnosti produktů", + "Atomy se při reakci ničí", + "Molární hmotnost se během reakce mění"}, + 4, 1}, + {"Kolik částic je v jednom molu?", + {"18", "44", "6,022 × 10^23", "1000"}, + 4, 2}, + {"Jak se spočítá látkové množství?", + {"n = m × M", "n = m / M", "n = M / m", "n = Z + A"}, + 4, 1}, + {"Jaká je přibližně molární hmotnost vody?", + {"2 g/mol", "16 g/mol", "18 g/mol", "44 g/mol"}, + 4, 2}, + }; + static const char *hints[] = { + "Atomy se přeskupují. Jejich celková hmotnost zůstává.", + "Avogadrova konstanta je počet částic v jednom molu.", + "Hmotnost dělíme molární hmotností. Výsledek je v molech.", + "H2O: 1 + 1 + 16 = 18. CO2 je 12 + 16 + 16 = 44.", + }; + + return sci_mcq_page(0, 2, "chemunit3", "chem_ex3_title", "chem_quiz3_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit4_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Směsi", "Čistá látka a směs", + "Tip: roztok je homogenní směs, suspenze heterogenní.", + { + "Čistá látka má stálé složení a stálé vlastnosti.", + "Směs obsahuje dvě nebo více látek, které nejsou chemicky vázané.", + "Homogenní směs, třeba roztok, vypadá v celém objemu stejně.", + "Heterogenní směs má rozlišitelné složky, třeba suspenze nebo emulze.", + NULL, + }, + }, + { + "2 / 3 • Roztok", "Složení roztoku", + "Tip: hmotnostní zlomek je podíl hmotnosti složky a hmotnosti směsi.", + { + "Rozpouštědlo je látka, v níž se rozpouští rozpuštěná látka.", + "Hmotnostní zlomek w = hmotnost složky / hmotnost celé směsi.", + "Nasycený roztok už při dané teplotě další látku nerozpustí.", + "Rozpustnost většiny pevných látek s teplotou roste.", + NULL, + }, + }, + { + "3 / 3 • pH", "Kyseliny a zásady", + "Tip: neutralizace kyseliny hydroxidem dává sůl a vodu.", + { + "Kyselina ve vodě zvyšuje množství H+, zásada množství OH−.", + "pH menší než 7 je kyselé, 7 neutrální, větší než 7 zásadité.", + "Silné kyseliny jsou třeba HCl a H2SO4, silná zásada je NaOH.", + "Neutralizace: kyselina + hydroxid → sůl + voda.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[3], "chem_unit4", "chem_unit4_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit4_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je roztok?", + {"Heterogenní směs dvou kovů", "Homogenní směs", + "Čistý prvek", "Jen sraženina na dně"}, + 4, 1}, + {"Jaké pH má kyselý roztok?", + {"Přesně 7", "Menší než 7", "Větší než 7", "Vždy 14"}, + 4, 1}, + {"Co vzniká neutralizací kyseliny hydroxidem?", + {"Jen vodík a kyslík jako prvky", "Sůl a voda", + "Uhlovodík", "Vzácný plyn"}, + 4, 1}, + {"Co je hmotnostní zlomek?", + {"Podíl hmotnosti složky a hmotnosti směsi", + "Počet molů v litru", + "Rozdíl pH a 7", + "Hmotnost jednoho protonu"}, + 4, 0}, + }; + static const char *hints[] = { + "Roztok je stejnorodý. Suspenze a emulze jsou různorodé.", + "Neutrální pH je 7. Pod ním je kyselina, nad ním zásada.", + "H+ z kyseliny a OH− ze zásady dají vodu, zbytek iontů sůl.", + "w = m(složky) / m(směsi). Nemá jednotku, nebo se uvádí v procentech.", + }; + + return sci_mcq_page(0, 3, "chemunit4", "chem_ex4_title", "chem_quiz4_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit5_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Názvy", "Čtyři skupiny sloučenin", + "Tip: oxid uhličitý je CO2, oxid uhelnatý je CO.", + { + "Oxidy jsou sloučeniny prvku s kyslíkem.", + "Kyseliny ve vzorci obsahují vodík, který mohou odštěpit jako H+.", + "Hydroxidy obsahují skupinu OH, soli vznikají z kyseliny náhradou vodíku.", + "NaCl je chlorid sodný, NaOH hydroxid sodný, HCl kyselina chlorovodíková.", + NULL, + }, + }, + { + "2 / 3 • Nekovy", "Halogeny, kyslík, dusík", + "Tip: halogeny jsou ve 17. skupině a jsou velmi reaktivní.", + { + "Kyslík O2 tvoří asi pětinu vzduchu a je nutný k dýchání i hoření.", + "Halogeny fluor, chlor, brom a jod tvoří soli, halogenidy.", + "Dusík N2 tvoří většinu vzduchu a je v bílkovinách i v hnojivech.", + "Síra je v kyselině sírové H2SO4, důležité průmyslové kyselině.", + NULL, + }, + }, + { + "3 / 3 • Kovy", "Sodík, vápník, železo", + "Tip: alkalické kovy reagují s vodou a musí se uchovávat pod petrolejem.", + { + "Alkalické kovy lithium, sodík a draslík jsou měkké a velmi reaktivní.", + "Vápník a hořčík patří ke kovům alkalických zemin a jsou v kostech i chlorofylu.", + "Železo je přechodný kov, základ oceli, a v těle je v hemoglobinu.", + "Ušlechtilé kovy, třeba zlato, vzdorují korozi víc než železo.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[4], "chem_unit5", "chem_unit5_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit5_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Který vzorec patří oxidu uhličitému?", + {"CO", "CO2", "H2CO3", "CaCO3"}, + 4, 1}, + {"Co je hydroxid sodný?", + {"NaCl", "NaOH", "HCl", "H2SO4"}, + 4, 1}, + {"Které prvky jsou halogeny?", + {"Li, Na, K", "F, Cl, Br, I", "Fe, Cu, Zn", "He, Ne, Ar"}, + 4, 1}, + {"Kam patří sodík a draslík?", + {"Mezi vzácné plyny", "Mezi alkalické kovy", + "Mezi halogeny", "Mezi polokovy"}, + 4, 1}, + }; + static const char *hints[] = { + "CO je oxid uhelnatý, CO2 oxid uhličitý. H2CO3 je kyselina uhličitá.", + "Hydroxidy končí na OH. NaOH je silná zásada.", + "Halogeny jsou 17. skupina. Alkalické kovy jsou 1. skupina.", + "Li, Na a K jsou alkalické kovy. S vodou reagují bouřlivě.", + }; + + return sci_mcq_page(0, 4, "chemunit5", "chem_ex5_title", "chem_quiz5_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit6_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Uhlík", "Proč existuje organická chemie", + "Tip: organické sloučeniny staví řetězce a kruhy z uhlíku.", + { + "Organická chemie zkoumá sloučeniny uhlíku, kromě oxidů a uhličitanů.", + "Uhlík tvoří čtyři vazby a může se řetězit.", + "Uhlovodíky obsahují jen uhlík a vodík.", + "Nasycené mají jen jednoduché vazby, nenasycené i dvojné nebo trojné.", + NULL, + }, + }, + { + "2 / 3 • Řady", "Alkany, alkeny a alkyny", + "Tip: obecný vzorec alkanů je CnH2n+2.", + { + "Alkany jsou nasycené. Methan CH4 je nejjednodušší.", + "Alkeny mají dvojnou vazbu. Ethen C2H4 je surovina pro plasty.", + "Alkyny mají trojnou vazbu. Ethyn, acetylen, se používá ke svařování.", + "Hořením uhlovodíků vzniká oxid uhličitý a voda.", + NULL, + }, + }, + { + "3 / 3 • Areny", "Benzen", + "Tip: benzen není alken se třemi obyčejnými dvojnými vazbami.", + { + "Areny jsou aromatické uhlovodíky s zvláštně stálým kruhem.", + "Benzen má vzorec C6H6.", + "Aromatický kruh je stálejší než obyčejné střídání dvojných vazeb.", + "Z arenů se vyrábějí plasty, léčiva i barviva.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[5], "chem_unit6", "chem_unit6_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit6_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Který uhlovodík je alkan?", + {"Ethen", "Ethyn", "Methan", "Benzen"}, + 4, 2}, + {"Jaký je obecný vzorec alkanů?", + {"CnH2n", "CnH2n+2", "CnH2n−2", "CnH2n−6"}, + 4, 1}, + {"Čím se ethen liší od ethanu?", + {"Nemá uhlík", "Má dvojnou vazbu", + "Je to kov", "Má trojnou vazbu"}, + 4, 1}, + {"Jaký vzorec má benzen?", + {"CH4", "C2H4", "C2H2", "C6H6"}, + 4, 3}, + }; + static const char *hints[] = { + "Methan a ethan jsou alkany. Ethen je alken, ethyn alkyn, benzen aren.", + "Alkany CnH2n+2, alkeny CnH2n, alkyny CnH2n−2.", + "Ethan je nasycený. Ethen má jednu dvojnou vazbu.", + "Benzen C6H6 je základní aren. Methan je CH4.", + }; + + return sci_mcq_page(0, 5, "chemunit6", "chem_ex6_title", "chem_quiz6_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit7_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Skupiny", "Deriváty uhlovodíků", + "Tip: funkční skupina rozhoduje o vlastnostech víc než délka řetězce.", + { + "Derivát vzniká náhradou vodíku jinou skupinou atomů.", + "Alkoholy mají hydroxyl −OH. Ethanol je v alkoholických nápojích.", + "Karboxylové kyseliny mají −COOH. Kyselina octová je v octu.", + "Aldehydy a ketony obsahují karbonylovou skupinu C=O.", + NULL, + }, + }, + { + "2 / 3 • Cukry", "Sacharidy", + "Tip: glukóza je monosacharid, škrob a celulóza jsou její polymery.", + { + "Sacharidy jsou přírodní látky z uhlíku, vodíku a kyslíku.", + "Glukóza C6H12O6 je základní cukr a palivo buněk.", + "Sacharosa je řepný a třtinový cukr, škrob je zásoba rostlin.", + "Celulóza tvoří buněčné stěny rostlin a člověk ji netráví.", + NULL, + }, + }, + { + "3 / 3 • Tuky a bílkoviny", "Z čeho je tělo", + "Tip: bílkovina je řetězec aminokyselin, tuk je ester glycerolu.", + { + "Lipidy jsou tuky a oleje, estery glycerolu a mastných kyselin.", + "Tuky jsou zásoba energie a součást membrán.", + "Bílkoviny vznikají spojením aminokyselin peptidovou vazbou.", + "Pořadí aminokyselin určuje tvar a funkci bílkoviny.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[6], "chem_unit7", "chem_unit7_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit7_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Kterou skupinu mají alkoholy?", + {"−COOH", "−OH", "−NH2 jako jedinou v alkanech", "Jen dvojnou vazbu uhlík–uhlík"}, + 4, 1}, + {"Co je kyselina octová?", + {"Alkan", "Karboxylová kyselina", "Halogen", "Hydroxid"}, + 4, 1}, + {"Která látka je sacharid?", + {"Škrob", "Chlorid sodný", "Ethyn", "Oxid vápenatý"}, + 4, 0}, + {"Z čeho jsou bílkoviny?", + {"Z glycerolu a nic jiného", "Z aminokyselin", + "Jen z glukózy", "Z vzácných plynů"}, + 4, 1}, + }; + static const char *hints[] = { + "Hydroxyl −OH je alkohol. Karboxyl −COOH je kyselina.", + "Octová kyselina CH3COOH má karboxyl. V octu je její zředěný roztok.", + "Glukóza, sacharosa, škrob a celulóza jsou sacharidy.", + "Aminokyseliny se pojí peptidovou vazbou do bílkoviny.", + }; + + return sci_mcq_page(0, 6, "chemunit7", "chem_ex7_title", "chem_quiz7_head", + qs, hints, 4); +} + +static GtkWidget *build_chem_unit8_page(void) { + static const NetSlide slides[] = { + { + "1 / 3 • Enzymy", "Biokatalyzátory", + "Tip: enzym je bílkovina a urychluje jednu určitou reakci.", + { + "Biochemie zkoumá chemické děje v živých organismech.", + "Enzym je bílkovinný katalyzátor. Snižuje aktivační energii.", + "Každý enzym působí na určitý substrát, jako klíč do zámku.", + "Teplo a nevhodné pH enzym denaturují a reakce se zpomalí.", + NULL, + }, + }, + { + "2 / 3 • Energie", "Fotosyntéza a dýchání", + "Tip: fotosyntéza váže energii, buněčné dýchání ji uvolňuje.", + { + "Fotosyntéza: 6 CO2 + 6 H2O → C6H12O6 + 6 O2, potřebuje světlo.", + "Probíhá v chloroplastech a je zdrojem kyslíku i organických látek.", + "Buněčné dýchání glukózu oxiduje a uvolňuje energii pro buňku.", + "Zjednodušeně je dýchání opačný směr než fotosyntéza.", + NULL, + }, + }, + { + "3 / 3 • DNA", "Chemický zápis dědičnosti", + "Tip: v RNA je uracil tam, kde má DNA thymin.", + { + "Nukleové kyseliny jsou DNA a RNA.", + "DNA nese genetickou informaci. Báze jsou adenin, thymin, guanin a cytosin.", + "Adenin se páruje s thyminem, guanin s cytosinem.", + "RNA má místo thyminu uracil a v buňce pomáhá číst geny.", + NULL, + }, + }, + }; + + return sci_unit_page(0, &chem_lessons[7], "chem_unit8", "chem_unit8_sub", + slides, G_N_ELEMENTS(slides)); +} + +static GtkWidget *build_chem_unit8_exercise_page(void) { + static const ChoiceQ qs[] = { + {"Co je enzym?", + {"Anorganická sůl", "Bílkovinný biokatalyzátor", + "Vzácný plyn", "Nasycený uhlovodík"}, + 4, 1}, + {"Co při fotosyntéze vzniká spolu s glukózou?", + {"Dusík", "Kyslík", "Chlorid sodný", "Železo"}, + 4, 1}, + {"Která báze je v RNA místo thyminu?", + {"Adenin", "Uracil", "Guanin navíc bez cytosinu", "Sodík"}, + 4, 1}, + {"Co dělá buněčné dýchání s glukózou?", + {"Ukládá ji beze změny do jádra", + "Oxiduje ji a uvolňuje energii", + "Mění ji na vzácný plyn", + "Zastavuje všechny enzymy"}, + 4, 1}, + }; + static const char *hints[] = { + "Enzym je bílkovina. Katalyzátor reakci urychlí a sám se nespotřebuje.", + "Zjednodušená rovnice končí glukózou a kyslíkem.", + "DNA: A, T, G, C. RNA má U místo T.", + "Dýchání je opak fotosyntézy v tom smyslu, že energii z glukózy uvolní.", + }; + + return sci_mcq_page(0, 7, "chemunit8", "chem_ex8_title", "chem_quiz8_head", + qs, hints, 4); +} + +void add_chem_pages(GtkStack *stack) { + typedef GtkWidget *(*SciBuilder)(void); + static const struct { + const char *unit_name; + const char *ex_name; + SciBuilder build_unit; + SciBuilder build_ex; + } pages[] = { + {"chemunit1", "chemex1", build_chem_unit1_page, build_chem_unit1_exercise_page}, + {"chemunit2", "chemex2", build_chem_unit2_page, build_chem_unit2_exercise_page}, + {"chemunit3", "chemex3", build_chem_unit3_page, build_chem_unit3_exercise_page}, + {"chemunit4", "chemex4", build_chem_unit4_page, build_chem_unit4_exercise_page}, + {"chemunit5", "chemex5", build_chem_unit5_page, build_chem_unit5_exercise_page}, + {"chemunit6", "chemex6", build_chem_unit6_page, build_chem_unit6_exercise_page}, + {"chemunit7", "chemex7", build_chem_unit7_page, build_chem_unit7_exercise_page}, + {"chemunit8", "chemex8", build_chem_unit8_page, build_chem_unit8_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/graduately.h b/src/graduately.h index 264e614..405a8ac 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -20,6 +20,7 @@ #define HW_SUBJ 2 /* index of "Technické vybavení" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define ON_SUBJ 4 /* index of "Občanská nauka" */ +#define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */ #define CSS_FILE "style.css" #define CHANGELOG_FILE "changelog.txt" #define APP_ID "org.maturita.Maturita" @@ -59,6 +60,8 @@ extern char *app_progress_lit; extern char *app_progress_lit2; extern char *app_progress_lit3; extern char *app_progress_lit4; +extern char *app_progress_chem; +extern char *app_progress_bio; #define PROGRESS_DIR app_progress_dir #define PROGRESS_U1 app_progress_u1 #define PROGRESS_U2 app_progress_u2 @@ -136,6 +139,7 @@ extern char *app_progress_lit4; #define ON4_UNITS 16 #define ON4_LESSONS 16 #define LIT_N 8 +#define SCI_N 8 #define HW_SLIDES 3 #define HW2_SLIDES 6 #define HW3_SLIDES 2 @@ -929,6 +933,23 @@ extern NetLesson lit_lessons[LIT_N]; extern NetLesson lit2_lessons[LIT_N]; extern NetLesson lit3_lessons[LIT_N]; extern NetLesson lit4_lessons[LIT_N]; +void sci_load_progress(void); +void sci_rail_theme_reset(void); +void sci_lessons_apply_lang(void); +void progress_for_sci(ProgressSum *out); +GtkWidget *sci_unit_page(int area, NetLesson *L, const char *title_key, + const char *sub_key, const NetSlide *slides, + guint n_slides); +GtkWidget *sci_mcq_page(int area, int lesson_id, const char *back_page, + const char *title_key, const char *heading_key, + const ChoiceQ *qs, const char **hints, int n); +GtkWidget *build_scimap_page(void); +GtkWidget *build_chemmap_page(void); +GtkWidget *build_biomap_page(void); +void add_chem_pages(GtkStack *stack); +void add_bio_pages(GtkStack *stack); +extern NetLesson chem_lessons[SCI_N]; +extern NetLesson bio_lessons[SCI_N]; void hw_lessons_apply_lang(void); GtkWidget *build_hwyears_page(void); GtkWidget *build_hwmap_page(void); @@ -1016,6 +1037,8 @@ void draw_chip_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void draw_civics_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); +void draw_science_icon(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data); void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void draw_book_icon(GtkDrawingArea *area, cairo_t *cr, diff --git a/src/i18n.c b/src/i18n.c index 117a2c4..82e16a1 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -90,12 +90,12 @@ const TrEntry tr_ui[] = { "Cvičení, nápovědy a vokabeltraining. Aplikace si pamatuje, kde jste skončili.", "Exercises, hints and vocabulary drills. The app remembers where you left off."}, {"welcome_feat1_link", "Začít procvičovat", "Start practicing"}, - {"welcome_feat2_title", "Čtyři otevřené předměty", "Four open subjects"}, + {"welcome_feat2_title", "Šest otevřených předmětů", "Six open subjects"}, {"welcome_feat2_body", - "Deutsch, sítě, hardware a čeština. Ostatní předměty čekají na svůj obsah.", - "German, networks, hardware and Czech. The other subjects are still locked."}, + "Deutsch, sítě, hardware, čeština, občanská nauka a přírodní vědy. Ostatní předměty čekají na svůj obsah.", + "German, networks, hardware, Czech, civics and natural sciences. The other subjects are still locked."}, {"welcome_feat2_link", "Jaké předměty jsou otevřené?", "Which subjects are open?"}, - {"welcome_stat_value", "4", "4"}, + {"welcome_stat_value", "6", "6"}, {"welcome_stat_label", "otevřené\npředměty", "open\nsubjects"}, {"continue", "Pokračuj", "Continue"}, {"roadmap_title", "Učební plán", "Learning path"}, @@ -1304,6 +1304,98 @@ const TrEntry tr_ui[] = { {"lit4_ex8_title", "Kvíz: interpretace", "Quiz: interpretation"}, {"lit4_quiz8_head", "Ověřte si postup rozboru textu", "Check how to read a text"}, + {"sci_sub", "Vyberte chemii nebo biologii.", "Pick chemistry or biology."}, + {"Chemie", "Chemie", "Chemistry"}, + {"Biologie", "Biologie", "Biology"}, + {"chem_sub", "Osm lekcí od atomu po biochemii.", + "Eight lessons from the atom to biochemistry."}, + {"bio_sub", "Osm lekcí od buňky po ekologii.", + "Eight lessons from the cell to ecology."}, + {"chem_unit1", "Částice a periodická soustava", "Particles and the periodic table"}, + {"chem_unit1_sub", "Atom, protonové číslo, periody a skupiny.", + "The atom, atomic number, periods and groups."}, + {"chem_ex1_title", "Kvíz: částice", "Quiz: particles"}, + {"chem_quiz1_head", "Ověřte si stavbu atomu", "Check the structure of the atom"}, + {"chem_unit2", "Chemická vazba", "Chemical bonding"}, + {"chem_unit2_sub", "Iontová, kovalentní a kovová vazba.", + "Ionic, covalent and metallic bonds."}, + {"chem_ex2_title", "Kvíz: vazba", "Quiz: bonding"}, + {"chem_quiz2_head", "Ověřte si chemickou vazbu", "Check chemical bonding"}, + {"chem_unit3", "Reakce a výpočty", "Reactions and calculations"}, + {"chem_unit3_sub", "Zákon zachování hmotnosti, mol a molární hmotnost.", + "Conservation of mass, the mole and molar mass."}, + {"chem_ex3_title", "Kvíz: výpočty", "Quiz: calculations"}, + {"chem_quiz3_head", "Ověřte si mol a reakce", "Check the mole and reactions"}, + {"chem_unit4", "Směsi, roztoky a pH", "Mixtures, solutions and pH"}, + {"chem_unit4_sub", "Homogenní a heterogenní směsi, neutralizace.", + "Homogeneous and heterogeneous mixtures, neutralization."}, + {"chem_ex4_title", "Kvíz: roztoky", "Quiz: solutions"}, + {"chem_quiz4_head", "Ověřte si směsi a pH", "Check mixtures and pH"}, + {"chem_unit5", "Anorganické látky", "Inorganic substances"}, + {"chem_unit5_sub", "Oxidy, kyseliny, hydroxidy, soli a vybrané prvky.", + "Oxides, acids, hydroxides, salts and selected elements."}, + {"chem_ex5_title", "Kvíz: anorganika", "Quiz: inorganic chemistry"}, + {"chem_quiz5_head", "Ověřte si anorganické látky", "Check inorganic substances"}, + {"chem_unit6", "Uhlovodíky", "Hydrocarbons"}, + {"chem_unit6_sub", "Alkany, alkeny, alkyny a benzen.", + "Alkanes, alkenes, alkynes and benzene."}, + {"chem_ex6_title", "Kvíz: uhlovodíky", "Quiz: hydrocarbons"}, + {"chem_quiz6_head", "Ověřte si uhlovodíky", "Check hydrocarbons"}, + {"chem_unit7", "Deriváty a přírodní látky", "Derivatives and natural substances"}, + {"chem_unit7_sub", "Alkoholy, kyseliny, sacharidy a bílkoviny.", + "Alcohols, acids, carbohydrates and proteins."}, + {"chem_ex7_title", "Kvíz: deriváty", "Quiz: derivatives"}, + {"chem_quiz7_head", "Ověřte si deriváty a přírodní látky", + "Check derivatives and natural substances"}, + {"chem_unit8", "Biochemie", "Biochemistry"}, + {"chem_unit8_sub", "Enzymy, fotosyntéza, dýchání a nukleové kyseliny.", + "Enzymes, photosynthesis, respiration and nucleic acids."}, + {"chem_ex8_title", "Kvíz: biochemie", "Quiz: biochemistry"}, + {"chem_quiz8_head", "Ověřte si biochemii", "Check biochemistry"}, + {"bio_unit1", "Živé soustavy", "Living systems"}, + {"bio_unit1_sub", "Metabolismus a organizace od buňky k ekosystému.", + "Metabolism and organisation from the cell to the ecosystem."}, + {"bio_ex1_title", "Kvíz: živé soustavy", "Quiz: living systems"}, + {"bio_quiz1_head", "Ověřte si znaky života", "Check the features of life"}, + {"bio_unit2", "Buňka", "The cell"}, + {"bio_unit2_sub", "Prokaryota, eukaryota, organely a dělení.", + "Prokaryotes, eukaryotes, organelles and division."}, + {"bio_ex2_title", "Kvíz: buňka", "Quiz: the cell"}, + {"bio_quiz2_head", "Ověřte si stavbu buňky", "Check the structure of the cell"}, + {"bio_unit3", "Viry, bakterie, houby a rostliny", + "Viruses, bacteria, fungi and plants"}, + {"bio_unit3_sub", "Nebuněčné viry, prokaryota a autotrofní rostliny.", + "Acellular viruses, prokaryotes and autotrophic plants."}, + {"bio_ex3_title", "Kvíz: organismy", "Quiz: organisms"}, + {"bio_quiz3_head", "Ověřte si viry, houby a rostliny", + "Check viruses, fungi and plants"}, + {"bio_unit4", "Živočichové", "Animals"}, + {"bio_unit4_sub", "Bezobratlí a pět tříd obratlovců.", + "Invertebrates and five classes of vertebrates."}, + {"bio_ex4_title", "Kvíz: živočichové", "Quiz: animals"}, + {"bio_quiz4_head", "Ověřte si skupiny živočichů", "Check the animal groups"}, + {"bio_unit5", "Soustavy člověka", "Human body systems"}, + {"bio_unit5_sub", "Kostra, oběh, dýchání a trávení.", + "Skeleton, circulation, breathing and digestion."}, + {"bio_ex5_title", "Kvíz: člověk", "Quiz: the human body"}, + {"bio_quiz5_head", "Ověřte si soustavy člověka", "Check the human body systems"}, + {"bio_unit6", "Řízení, rozmnožování a imunita", + "Control, reproduction and immunity"}, + {"bio_unit6_sub", "Nervy, hormony, oplození a obrana organismu.", + "Nerves, hormones, fertilisation and the body's defence."}, + {"bio_ex6_title", "Kvíz: řízení", "Quiz: control"}, + {"bio_quiz6_head", "Ověřte si řízení a imunitu", "Check control and immunity"}, + {"bio_unit7", "Genetika", "Genetics"}, + {"bio_unit7_sub", "Gen, alela, Mendel a pohlavní chromozomy.", + "The gene, the allele, Mendel and the sex chromosomes."}, + {"bio_ex7_title", "Kvíz: genetika", "Quiz: genetics"}, + {"bio_quiz7_head", "Ověřte si dědičnost", "Check inheritance"}, + {"bio_unit8", "Ekologie", "Ecology"}, + {"bio_unit8_sub", "Ekosystém, potravní vztahy a člověk v prostředí.", + "The ecosystem, feeding relationships and humans in the environment."}, + {"bio_ex8_title", "Kvíz: ekologie", "Quiz: ecology"}, + {"bio_quiz8_head", "Ověřte si vztahy v ekosystému", + "Check relationships in an ecosystem"}, {"czech_sub", "Vyberte oblast a začněte procvičovat.", "Pick an area and start practicing."}, {"Literatura", "Literatura", "Literature"}, @@ -2332,6 +2424,7 @@ void on2_lessons_apply_lang(void); void on3_lessons_apply_lang(void); void on4_lessons_apply_lang(void); void lit_lessons_apply_lang(void); +void sci_lessons_apply_lang(void); void net_rail_theme_reset(void); void apply_language(void) { @@ -2351,5 +2444,6 @@ void apply_language(void) { on3_lessons_apply_lang(); on4_lessons_apply_lang(); lit_lessons_apply_lang(); + sci_lessons_apply_lang(); search_apply_lang(); } diff --git a/src/icons.c b/src/icons.c index 73e9b82..b14c376 100644 --- a/src/icons.c +++ b/src/icons.c @@ -188,6 +188,46 @@ void draw_civics_icon(GtkDrawingArea *area, cairo_t *cr, cairo_stroke(cr); } +/* Round flask for Základy přírodopisných věd. */ +void draw_science_icon(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + const double s = MIN(width, height); + const double cx = width / 2.0; + const double top = height / 2.0 - s * 0.38; + const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent), + color_from_hex(app_theme.accent2), 0.22); + + (void)area; + (void)data; + + cairo_set_source_rgb(cr, c.r, c.g, c.b); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + cairo_set_line_width(cr, MAX(1.6, s * 0.07)); + + cairo_move_to(cr, cx - s * 0.08, top); + cairo_line_to(cr, cx - s * 0.08, top + s * 0.22); + cairo_line_to(cr, cx - s * 0.28, top + s * 0.58); + cairo_curve_to(cr, cx - s * 0.34, top + s * 0.78, + cx - s * 0.16, top + s * 0.82, + cx, top + s * 0.82); + cairo_curve_to(cr, cx + s * 0.16, top + s * 0.82, + cx + s * 0.34, top + s * 0.78, + cx + s * 0.28, top + s * 0.58); + cairo_line_to(cr, cx + s * 0.08, top + s * 0.22); + cairo_line_to(cr, cx + s * 0.08, top); + cairo_stroke(cr); + + cairo_move_to(cr, cx - s * 0.14, top); + cairo_line_to(cr, cx + s * 0.14, top); + cairo_stroke(cr); + + cairo_set_line_width(cr, MAX(1.4, s * 0.055)); + cairo_move_to(cr, cx - s * 0.16, top + s * 0.62); + cairo_line_to(cr, cx + s * 0.16, top + s * 0.62); + cairo_stroke(cr); +} + /* Open book glyph, painted in the given colour. */ static void book_paint(cairo_t *cr, int width, int height, Rgb c) { const double w = width; diff --git a/src/main.c b/src/main.c index 49f5f70..16df5c2 100644 --- a/src/main.c +++ b/src/main.c @@ -389,6 +389,9 @@ static gboolean run_smoke_test(gpointer data) { "on3map", "on4map", "czechmap", "lityears", "litmap", "lit2map", "lit3map", "lit4map", + "scimap", "chemmap", "biomap", + "chemunit1", "chemex1", "chemunit8", "chemex8", + "biounit1", "bioex1", "biounit8", "bioex8", "litunit1", "litex1", "litunit8", "litex8", "lit2unit1", "lit2ex1", "lit3unit1", "lit3ex1", "lit4unit1", "lit4ex1", "lit4unit8", "lit4ex8", @@ -525,6 +528,11 @@ void activate(GtkApplication *app, gpointer user_data) { 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_scimap_page(), "scimap"); + gtk_stack_add_named(main_stack, build_chemmap_page(), "chemmap"); + add_chem_pages(main_stack); + gtk_stack_add_named(main_stack, build_biomap_page(), "biomap"); + add_bio_pages(main_stack); gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap"); gtk_stack_add_named(main_stack, build_lityears_page(), "lityears"); gtk_stack_add_named(main_stack, build_litmap_page(), "litmap"); diff --git a/src/priroda.c b/src/priroda.c new file mode 100644 index 0000000..9a1ef94 --- /dev/null +++ b/src/priroda.c @@ -0,0 +1,720 @@ +#include "graduately.h" + +/* Základy přírodopisných věd: chemistry and biology share one subject. + * Each area is a vertical list of lessons, then slides and a quiz. */ + +typedef struct { + const char *map_page; + const char *back_page; + const char *title_key; + const char *sub_key; + char **progress_path; + NetLesson *lessons; + const char *unit_keys[SCI_N]; + const char *sub_keys[SCI_N]; + GtkWidget *nodes[SCI_N]; + int note_last_w; +} SciTrack; + +static SciTrack sci_tracks[2]; +static GtkWidget *sci_rail; +static gboolean sci_ready; + +static void sci_ensure(void) { + static const char *chem_units[] = { + "chem_unit1", "chem_unit2", "chem_unit3", "chem_unit4", + "chem_unit5", "chem_unit6", "chem_unit7", "chem_unit8", + }; + static const char *chem_subs[] = { + "chem_unit1_sub", "chem_unit2_sub", "chem_unit3_sub", "chem_unit4_sub", + "chem_unit5_sub", "chem_unit6_sub", "chem_unit7_sub", "chem_unit8_sub", + }; + static const char *bio_units[] = { + "bio_unit1", "bio_unit2", "bio_unit3", "bio_unit4", + "bio_unit5", "bio_unit6", "bio_unit7", "bio_unit8", + }; + static const char *bio_subs[] = { + "bio_unit1_sub", "bio_unit2_sub", "bio_unit3_sub", "bio_unit4_sub", + "bio_unit5_sub", "bio_unit6_sub", "bio_unit7_sub", "bio_unit8_sub", + }; + + if (sci_ready) + return; + sci_tracks[0].map_page = "chemmap"; + sci_tracks[0].back_page = "scimap"; + sci_tracks[0].title_key = "Chemie"; + sci_tracks[0].sub_key = "chem_sub"; + sci_tracks[0].progress_path = &app_progress_chem; + sci_tracks[0].lessons = chem_lessons; + sci_tracks[0].note_last_w = -1; + sci_tracks[1].map_page = "biomap"; + sci_tracks[1].back_page = "scimap"; + sci_tracks[1].title_key = "Biologie"; + sci_tracks[1].sub_key = "bio_sub"; + sci_tracks[1].progress_path = &app_progress_bio; + sci_tracks[1].lessons = bio_lessons; + sci_tracks[1].note_last_w = -1; + for (int i = 0; i < SCI_N; i++) { + sci_tracks[0].unit_keys[i] = chem_units[i]; + sci_tracks[0].sub_keys[i] = chem_subs[i]; + sci_tracks[1].unit_keys[i] = bio_units[i]; + sci_tracks[1].sub_keys[i] = bio_subs[i]; + } + sci_ready = TRUE; +} + +static void sci_save(SciTrack *T) { + GKeyFile *kf; + char *path; + gchar *data; + + if (!T->progress_path || !*T->progress_path) + return; + path = *T->progress_path; + kf = g_key_file_new(); + for (int i = 0; i < SCI_N; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + g_key_file_set_boolean(kf, "done", key, T->lessons[i].done); + } + if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) { + g_key_file_free(kf); + return; + } + data = g_key_file_to_data(kf, NULL, NULL); + g_key_file_free(kf); + if (data) { + GError *err = NULL; + + g_file_set_contents(path, data, -1, &err); + if (err) + g_error_free(err); + g_free(data); + } +} + +static void sci_load_one(SciTrack *T) { + GKeyFile *kf; + GError *err = NULL; + const char *path; + + if (!T->progress_path || !*T->progress_path) + return; + path = *T->progress_path; + kf = g_key_file_new(); + if (!g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + for (int i = 0; i < SCI_N; i++) { + gchar key[8]; + + g_snprintf(key, sizeof(key), "%d", i + 1); + T->lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL); + } + g_key_file_free(kf); +} + +void sci_load_progress(void) { + sci_ensure(); + sci_load_one(&sci_tracks[0]); + sci_load_one(&sci_tracks[1]); +} + +static void sci_refresh(SciTrack *T) { + for (int i = 0; i < SCI_N; i++) { + GtkWidget *node = T->nodes[i]; + GtkWidget *icon = T->lessons[i].done_icon; + + if (!node) + continue; + if (T->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); + } + } +} + +static void sci_mark_done(SciTrack *T, int lesson_id) { + if (lesson_id < 0 || lesson_id >= SCI_N) + return; + if (T->lessons[lesson_id].done) + return; + T->lessons[lesson_id].done = TRUE; + sci_save(T); + sci_refresh(T); + refresh_stats_ui(); +} + +void progress_for_sci(ProgressSum *out) { + if (!out) + return; + sci_ensure(); + for (int a = 0; a < 2; a++) { + SciTrack *T = &sci_tracks[a]; + + out->total_ex += SCI_N; + out->open_units += SCI_N; + for (int i = 0; i < SCI_N; i++) { + if (T->lessons[i].done) { + out->done_ex++; + out->done_units++; + } + } + } +} + +static void sci_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), "s%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 GtkWidget *sci_lesson_button(SciTrack *T, int index) { + GtkWidget *btn; + GtkWidget *row; + GtkWidget *num; + GtkWidget *texts; + GtkWidget *title; + GtkWidget *sub; + GtkWidget *icon; + char *num_text; + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_add_css_class(btn, "current"); + gtk_widget_set_can_focus(btn, FALSE); + gtk_widget_set_hexpand(btn, TRUE); + gtk_widget_set_size_request(btn, -1, 84); + + row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_widget_set_valign(row, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(row, 18); + gtk_widget_set_margin_end(row, 18); + gtk_button_set_child(GTK_BUTTON(btn), row); + + num_text = g_strdup_printf("%d", index + 1); + num = gtk_label_new(num_text); + g_free(num_text); + gtk_widget_add_css_class(num, "unit-number"); + gtk_widget_set_valign(num, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), num); + + texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(texts, GTK_ALIGN_START); + gtk_widget_set_hexpand(texts, TRUE); + gtk_box_append(GTK_BOX(row), texts); + + title = gtk_label_new(NULL); + i18n_bind(title, T->unit_keys[index], 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_add_css_class(title, "unit-name"); + gtk_box_append(GTK_BOX(texts), title); + + sub = gtk_label_new(NULL); + i18n_bind(sub, T->sub_keys[index], 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(sub), TRUE); + gtk_label_set_xalign(GTK_LABEL(sub), 0.0); + gtk_widget_add_css_class(sub, "hint"); + gtk_box_append(GTK_BOX(texts), sub); + + icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18); + gtk_widget_set_visible(icon, FALSE); + gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), icon); + T->lessons[index].done_icon = icon; + T->nodes[index] = btn; + g_signal_connect(btn, "clicked", G_CALLBACK(sci_open_unit), + &T->lessons[index]); + return btn; +} + +static GtkWidget *sci_build_list(SciTrack *T) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *list; + + sci_ensure(); + 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(T->back_page, T->title_key, T->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, 18); + gtk_box_append(GTK_BOX(page), scroll); + + list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_halign(list, GTK_ALIGN_CENTER); + gtk_widget_set_valign(list, GTK_ALIGN_START); + gtk_widget_set_size_request(list, 520, -1); + gtk_widget_set_margin_bottom(list, 12); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); + + for (int i = 0; i < SCI_N; i++) + gtk_box_append(GTK_BOX(list), sci_lesson_button(T, i)); + sci_refresh(T); + return page; +} + +GtkWidget *build_chemmap_page(void) { return sci_build_list(&sci_tracks[0]); } +GtkWidget *build_biomap_page(void) { return sci_build_list(&sci_tracks[1]); } + +#define SCI_BUBBLE SUB_BUBBLE +#define SCI_LABEL_W 180.0 +#define SCI_CW 560 +#define SCI_CH 280 + +static double sci_cx[2]; +static double sci_cy[2]; + +static void draw_sci_rail(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data) { + const Rgb rail = color_from_hex(app_theme.rail); + const Rgb slate = color_from_hex(app_theme.surface1); + + (void)area; + (void)width; + (void)height; + (void)data; + + 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_move_to(cr, sci_cx[0], sci_cy[0]); + cairo_line_to(cr, sci_cx[1], sci_cy[1]); + cairo_set_line_width(cr, 14); + cairo_set_source_rgb(cr, rail.r, rail.g, rail.b); + cairo_stroke(cr); + + cairo_move_to(cr, sci_cx[0], sci_cy[0]); + cairo_line_to(cr, sci_cx[1], sci_cy[1]); + cairo_set_line_width(cr, 9); + cairo_set_source_rgb(cr, slate.r, slate.g, slate.b); + cairo_stroke(cr); +} + +void sci_rail_theme_reset(void) { + if (sci_rail) + gtk_widget_queue_draw(sci_rail); +} + +GtkWidget *build_scimap_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *wrap; + GtkWidget *fixed; + GtkWidget *rail; + static const char *keys[] = {"Chemie", "Biologie"}; + static const char *initials[] = {"C", "B"}; + static const char *targets[] = {"chemmap", "biomap"}; + + sci_cx[0] = 160.0; + sci_cy[0] = 110.0; + sci_cx[1] = 400.0; + sci_cy[1] = 110.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("subjects", "Základy Přírodopisných věd", "sci_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); + + 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, SCI_CW, SCI_CH); + gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); + + rail = gtk_drawing_area_new(); + gtk_widget_set_size_request(rail, SCI_CW, SCI_CH); + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_sci_rail, + NULL, NULL); + gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0); + sci_rail = rail; + + for (int i = 0; i < 2; i++) { + GtkWidget *btn = gtk_button_new(); + GtkWidget *letter = gtk_label_new(initials[i]); + GtkWidget *name = gtk_label_new(NULL); + + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_add_css_class(btn, "current"); + gtk_widget_set_can_focus(btn, FALSE); + gtk_widget_set_size_request(btn, (int)SCI_BUBBLE, (int)SCI_BUBBLE); + gtk_widget_add_css_class(letter, "unit-number"); + gtk_button_set_child(GTK_BUTTON(btn), letter); + g_object_set_data_full(G_OBJECT(btn), "target", + g_strdup(targets[i]), g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); + + i18n_bind(name, keys[i], 0); + gtk_widget_set_size_request(name, (int)SCI_LABEL_W, -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"); + + gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0); + gtk_fixed_put(GTK_FIXED(fixed), name, 0, 0); + gtk_fixed_move(GTK_FIXED(fixed), btn, + (int)(sci_cx[i] - SCI_BUBBLE / 2.0), + (int)(sci_cy[i] - SCI_BUBBLE / 2.0)); + gtk_fixed_move(GTK_FIXED(fixed), name, + (int)(sci_cx[i] - SCI_LABEL_W / 2.0), + (int)(sci_cy[i] + SCI_BUBBLE / 2.0 + 10.0)); + } + return page; +} + +static void sci_slide_apply(NetLesson *L) { + char name[16]; + + if (!L || !L->stack) + return; + g_snprintf(name, sizeof(name), "s%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 sci_slide_prev(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L || L->idx == 0) + return; + L->idx--; + sci_slide_apply(L); +} + +static void sci_slide_next(GtkButton *button, gpointer data) { + NetLesson *L = data; + + (void)button; + if (!L) + return; + if (L->idx + 1 < L->n_slides) { + L->idx++; + sci_slide_apply(L); + } else { + gtk_stack_set_visible_child_name(main_stack, L->ex_page); + } +} + +static void sci_rescale(SciTrack *T, 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 < SCI_N; i++) + net_rescale_lesson_notes(&T->lessons[i], body, head, kick); +} + +static gboolean sci_note_tick(GtkWidget *w, GdkFrameClock *clock, + gpointer data) { + SciTrack *T = data; + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + if (width != T->note_last_w) { + T->note_last_w = width; + sci_rescale(T, width); + } + return G_SOURCE_CONTINUE; +} + +GtkWidget *sci_unit_page(int area, NetLesson *L, const char *title_key, + const char *sub_key, const NetSlide *slides, + guint n_slides) { + SciTrack *T; + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *nav; + + sci_ensure(); + if (area < 0 || area > 1) + area = 0; + T = &sci_tracks[area]; + + 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(T->map_page, 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), "s%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, sci_note_tick, T, NULL); + T->note_last_w = -1; + sci_rescale(T, 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(sci_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(sci_slide_next), L); + gtk_box_append(GTK_BOX(nav), L->next_btn); + + L->idx = 0; + sci_slide_apply(L); + return page; +} + +void sci_lessons_apply_lang(void) { + sci_ensure(); + for (int a = 0; a < 2; a++) { + for (int i = 0; i < SCI_N; i++) + sci_slide_apply(&sci_tracks[a].lessons[i]); + } +} + +typedef struct { + const ChoiceQ *qs; + int n; + int n_opts; + SciTrack *track; + int lesson_id; + GtkWidget *feedback; + GtkToggleButton **toggles; + GtkWidget **hints; +} SciMcqCtx; + +static void sci_mcq_check(GtkButton *button, gpointer data) { + SciMcqCtx *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")); + sci_mark_done(ctx->track, ctx->lesson_id); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +GtkWidget *sci_mcq_page(int area, 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; + SciMcqCtx *ctx = g_new0(SciMcqCtx, 1); + int n_opts = qs[0].n_options; + + sci_ensure(); + if (area < 0 || area > 1) + area = 0; + + 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->track = &sci_tracks[area]; + 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(sci_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; +} diff --git a/src/progress.c b/src/progress.c index c0059cf..508b0ed 100644 --- a/src/progress.c +++ b/src/progress.c @@ -59,6 +59,7 @@ void load_progress(void) { on3_load_progress(); on4_load_progress(); lit_load_progress(); + sci_load_progress(); } /* Redraw the exercise rails of every unit that already has a page. */ diff --git a/src/search.c b/src/search.c index 8240e7c..1a47354 100644 --- a/src/search.c +++ b/src/search.c @@ -23,7 +23,7 @@ static GPtrArray *search_items; static gboolean subject_open(int i) { return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ - || i == ON_SUBJ; + || i == ON_SUBJ || i == SCI_SUBJ; } static const char *subject_page(int i) { @@ -37,6 +37,8 @@ static const char *subject_page(int i) { return "czechmap"; if (i == ON_SUBJ) return "onyears"; + if (i == SCI_SUBJ) + return "scimap"; return "subjects"; } @@ -271,6 +273,22 @@ static void catalog_rebuild(void) { g_snprintf(page, sizeof(page), "lit4ex%d", i); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + catalog_add_key("Chemie", "search_lesson", "chemmap", FALSE, 1, "chemie"); + catalog_add_key("Biologie", "search_lesson", "biomap", FALSE, 1, "biologie"); + for (i = 1; i <= SCI_N; i++) { + g_snprintf(key, sizeof(key), "chem_unit%d", i); + g_snprintf(page, sizeof(page), "chemunit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "chemie"); + g_snprintf(page, sizeof(page), "chemex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); + } + for (i = 1; i <= SCI_N; i++) { + g_snprintf(key, sizeof(key), "bio_unit%d", i); + g_snprintf(page, sizeof(page), "biounit%d", i); + catalog_add_key(key, "search_lesson", page, FALSE, 2, "biologie"); + g_snprintf(page, sizeof(page), "bioex%d", i); + catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); + } catalog_add_key("Mluvnice", "search_lesson", "mluvnice", FALSE, 1, "gramatika grammar"); catalog_add_key("Maturitní četba", "search_book", "readinglist", FALSE, 1, diff --git a/src/stats.c b/src/stats.c index 101b9ff..8a5acf9 100644 --- a/src/stats.c +++ b/src/stats.c @@ -75,6 +75,8 @@ void refresh_stats_ui(void) { progress_for_mluvnice(&all); progress_for_lit(&all); } + else if (s == SCI_SUBJ) + progress_for_sci(&all); else progress_for_units(sub_unit_start[s], sub_unit_count[s], &all); } @@ -105,7 +107,7 @@ void refresh_stats_ui(void) { ProgressSum sp = {0}; gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ) || (s == ON_SUBJ) || (s == CZ_SUBJ) - || sub_unit_count[s] > 0; + || (s == SCI_SUBJ) || sub_unit_count[s] > 0; if (!count || !bar) continue; @@ -119,6 +121,8 @@ void refresh_stats_ui(void) { progress_for_mluvnice(&sp); progress_for_lit(&sp); } + else if (s == SCI_SUBJ) + progress_for_sci(&sp); else progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp); if (has_content) { @@ -388,7 +392,8 @@ GtkWidget *build_stats_page(void) { GtkWidget *count; GtkWidget *bar; gboolean has_units = (s == NET_SUBJ) || (s == HW_SUBJ) - || (s == CZ_SUBJ) || sub_unit_count[s] > 0; + || (s == CZ_SUBJ) || (s == ON_SUBJ) + || (s == SCI_SUBJ) || sub_unit_count[s] > 0; gboolean has_deutsch_units = sub_unit_count[s] > 0; subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); diff --git a/src/subjects.c b/src/subjects.c index ca330c5..ab50e7d 100644 --- a/src/subjects.c +++ b/src/subjects.c @@ -5,7 +5,7 @@ /* ------------------------------------------------------------------ */ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ - * (Czech) and ON_SUBJ (civics) are unlocked. */ + * (Czech), ON_SUBJ (civics) and SCI_SUBJ (natural sciences) are unlocked. */ const char *sub_keys[NUM_SUBJECTS] = { "Deutsch", "Správa počítačových sítí", @@ -154,7 +154,7 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, static gboolean subject_is_open(int i) { return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ - || i == ON_SUBJ; + || i == ON_SUBJ || i == SCI_SUBJ; } static GtkWidget *subject_icon(int i) { @@ -177,6 +177,9 @@ static GtkWidget *subject_icon(int i) { else if (i == ON_SUBJ) gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), draw_civics_icon, NULL, NULL); + else if (i == SCI_SUBJ) + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), + draw_science_icon, NULL, NULL); else return icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 22); return area; @@ -193,6 +196,8 @@ static const char *subject_target(int i) { return "czechmap"; if (i == ON_SUBJ) return "onyears"; + if (i == SCI_SUBJ) + return "scimap"; return NULL; } diff --git a/src/theme.c b/src/theme.c index 746db2f..0ee8901 100644 --- a/src/theme.c +++ b/src/theme.c @@ -432,5 +432,6 @@ void apply_theme(void) { on4_rail_theme_reset(); czech_rail_theme_reset(); lit_rail_theme_reset(); + sci_rail_theme_reset(); book_rail_theme_reset(); } diff --git a/src/util.c b/src/util.c index 847d7e3..b248635 100644 --- a/src/util.c +++ b/src/util.c @@ -30,6 +30,8 @@ char *app_progress_lit; char *app_progress_lit2; char *app_progress_lit3; char *app_progress_lit4; +char *app_progress_chem; +char *app_progress_bio; static char *dup_dirname(const char *path) { char *tmp = g_strdup(path); @@ -176,6 +178,8 @@ static void set_progress_paths(const char *write_root) { g_free(app_progress_lit2); g_free(app_progress_lit3); g_free(app_progress_lit4); + g_free(app_progress_chem); + g_free(app_progress_bio); app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL); app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL); @@ -194,6 +198,8 @@ static void set_progress_paths(const char *write_root) { app_progress_lit2 = g_build_filename(app_progress_dir, "lit2.conf", NULL); app_progress_lit3 = g_build_filename(app_progress_dir, "lit3.conf", NULL); app_progress_lit4 = g_build_filename(app_progress_dir, "lit4.conf", NULL); + app_progress_chem = g_build_filename(app_progress_dir, "chem.conf", NULL); + app_progress_bio = g_build_filename(app_progress_dir, "bio.conf", NULL); } /* Resolve read-only asset root, chdir there, and point progress at a writable