Add year-1 civics lessons on life in a community and keep later years locked.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-02 11:44:21 +02:00
commit 515abdc6ce
28 changed files with 2665 additions and 12 deletions

View file

@ -8,7 +8,7 @@ Vzdělávací aplikace k přípravě na maturitu (repo `Graduately`). / An educa
## Česky ## Česky
Rozhraní je v češtině a angličtině. Hotové jsou čtyři předměty; ostatní jsou Rozhraní je v češtině a angličtině. Hotových je pět předmětů; ostatní jsou
zatím zamčené placeholdery. Aplikace se po instalaci aktualizuje sama. zatím zamčené placeholdery. Aplikace se po instalaci aktualizuje sama.
| Úvod | Předměty | Německá cesta | | Úvod | Předměty | Německá cesta |
@ -68,13 +68,15 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
- **Technické vybavení** — 1. ročník, 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení); ročníky 2–4 jsou zamčené - **Technické vybavení** — 1. ročník, 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení); ročníky 2–4 jsou zamčené
- **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba - **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba
(82 knih: zápisky, kvíz, sestavení děje); literární teorie je zatím zamčená (82 knih: zápisky, kvíz, sestavení děje); literární teorie je zatím zamčená
- **Občanská nauka** — 1. ročník, 17 lekcí (člověk v lidském společenství);
ročníky 2–4 jsou zamčené
- statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina - statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina
- ukončení: `Cmd/Super+Q` nebo `Alt+F4` - ukončení: `Cmd/Super+Q` nebo `Alt+F4`
### Použití ### Použití
1. **Pokračuj** otevře mapu předmětů. 1. **Pokračuj** otevře mapu předmětů.
2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka) vede na učební cestu. 2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy) vede na učební cestu.
3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají. 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. 4. **Hledat** (lupa), **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře.
@ -200,7 +202,7 @@ Licence: [GPL-3.0](LICENSE).
## English ## English
The UI is Czech and English. Four subjects are playable; the rest are locked The UI is Czech and English. Five subjects are playable; the rest are locked
placeholders. After install the app updates itself. placeholders. After install the app updates itself.
| Welcome | Subjects | German path | | Welcome | Subjects | German path |
@ -261,13 +263,15 @@ Intel Mac, another architecture, or hacking on the code →
- **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked - **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked
- **Czech language** — grammar (20 exercises) and required reading - **Czech language** — grammar (20 exercises) and required reading
(82 books: notes, quiz, plot ordering); literature theory is still locked (82 books: notes, quiz, plot ordering); literature theory is still locked
- **Civics** — year 1, 17 lessons (the individual and society);
years 2–4 are locked
- statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English - statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English
- quit with `Cmd/Super+Q` or `Alt+F4` - quit with `Cmd/Super+Q` or `Alt+F4`
### Usage ### Usage
1. **Continue** opens the subject map. 1. **Continue** opens the subject map.
2. An open subject (flag, Wi‑Fi, chip, Czech flag) opens its path. 2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales) opens its path.
3. Exercise / lesson nodes open content; locked nodes do nothing. 3. Exercise / lesson nodes open content; locked nodes do nothing.
4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right. 4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right.

View file

@ -126,6 +126,11 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
refresh() refresh()
} }
fun markOn(id: Int) {
progress.markOn(id)
refresh()
}
fun markMluv(n: Int) { fun markMluv(n: Int) {
progress.markMluv(n) progress.markMluv(n)
refresh() refresh()

View file

@ -37,6 +37,8 @@ import org.maturita.maturita.ui.LitQuizScreen
import org.maturita.maturita.ui.MluvExercise import org.maturita.maturita.ui.MluvExercise
import org.maturita.maturita.ui.NetQuizScreen import org.maturita.maturita.ui.NetQuizScreen
import org.maturita.maturita.ui.NetYearsScreen import org.maturita.maturita.ui.NetYearsScreen
import org.maturita.maturita.ui.OnQuizScreen
import org.maturita.maturita.ui.OnYearsScreen
import org.maturita.maturita.ui.PlotScreen import org.maturita.maturita.ui.PlotScreen
import org.maturita.maturita.ui.RoadmapScreen import org.maturita.maturita.ui.RoadmapScreen
import org.maturita.maturita.ui.SearchScreen import org.maturita.maturita.ui.SearchScreen
@ -226,6 +228,21 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.HwEx -> HwQuizScreen(vm, r.id) is Route.HwEx -> HwQuizScreen(vm, r.id)
Route.OnYears -> OnYearsScreen(vm)
Route.OnMap -> LessonListScreen(
vm, vm.tr("on_year1"), vm.tr("on_sub"),
vm.content.on.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.onDone(id), Route.OnLesson(id))
},
)
is Route.OnLesson -> {
val l = vm.content.onLesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.OnEx(r.id))
}
}
is Route.OnEx -> OnQuizScreen(vm, r.id)
Route.CzechMap -> CzechMapScreen(vm) Route.CzechMap -> CzechMapScreen(vm)
Route.Mluvnice -> LessonListScreen( Route.Mluvnice -> LessonListScreen(
vm, vm.tr("Mluvnice"), vm.tr("mluv_sub"), vm, vm.tr("Mluvnice"), vm.tr("mluv_sub"),

View file

@ -17,6 +17,10 @@ sealed class Route(val page: String) {
data object HwMap : Route("hwmap") data object HwMap : Route("hwmap")
data class HwLesson(val id: Int) : Route("hwunit$id") data class HwLesson(val id: Int) : Route("hwunit$id")
data class HwEx(val id: Int) : Route("hwex$id") data class HwEx(val id: Int) : Route("hwex$id")
data object OnYears : Route("onyears")
data object OnMap : Route("onmap")
data class OnLesson(val id: Int) : Route("onunit$id")
data class OnEx(val id: Int) : Route("onex$id")
data object CzechMap : Route("czechmap") data object CzechMap : Route("czechmap")
data object Mluvnice : Route("mluvnice") data object Mluvnice : Route("mluvnice")
data class MluvEx(val n: Int) : Route("mluve$n") data class MluvEx(val n: Int) : Route("mluve$n")
@ -31,7 +35,8 @@ sealed class Route(val page: String) {
/** Screens with their own bottom action bar; the navigation bar stays hidden there. */ /** Screens with their own bottom action bar; the navigation bar stays hidden there. */
val isExercise: Boolean val isExercise: Boolean
get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx || get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx ||
this is MluvEx || this is BookQuiz || this is BookPlot || this is NetLesson || this is HwLesson this is OnEx || this is MluvEx || this is BookQuiz || this is BookPlot ||
this is NetLesson || this is HwLesson || this is OnLesson
/** Full stack from the practice root to this screen, so back always goes up one level. */ /** Full stack from the practice root to this screen, so back always goes up one level. */
fun stack(): List<Route> = when (this) { fun stack(): List<Route> = when (this) {
@ -50,6 +55,10 @@ sealed class Route(val page: String) {
HwMap -> listOf(Subjects, HwYears, HwMap) HwMap -> listOf(Subjects, HwYears, HwMap)
is HwLesson -> listOf(Subjects, HwYears, HwMap, this) is HwLesson -> listOf(Subjects, HwYears, HwMap, this)
is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this) is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this)
OnYears -> listOf(Subjects, OnYears)
OnMap -> listOf(Subjects, OnYears, OnMap)
is OnLesson -> listOf(Subjects, OnYears, OnMap, this)
is OnEx -> listOf(Subjects, OnYears, OnMap, OnLesson(id), this)
CzechMap -> listOf(Subjects, CzechMap) CzechMap -> listOf(Subjects, CzechMap)
Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice)
is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this)
@ -70,6 +79,8 @@ fun routeFromPage(page: String): Route? = when (page) {
"netmap" -> Route.NetMap "netmap" -> Route.NetMap
"hwyears" -> Route.HwYears "hwyears" -> Route.HwYears
"hwmap" -> Route.HwMap "hwmap" -> Route.HwMap
"onyears" -> Route.OnYears
"onmap" -> Route.OnMap
"czechmap" -> Route.CzechMap "czechmap" -> Route.CzechMap
"mluvnice" -> Route.Mluvnice "mluvnice" -> Route.Mluvnice
"readinglist" -> Route.ReadingList "readinglist" -> Route.ReadingList
@ -101,6 +112,12 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""hwex(\d+)""").matchEntire(page)?.let { Regex("""hwex(\d+)""").matchEntire(page)?.let {
return Route.HwEx(it.groupValues[1].toInt()) return Route.HwEx(it.groupValues[1].toInt())
} }
Regex("""onunit(\d+)""").matchEntire(page)?.let {
return Route.OnLesson(it.groupValues[1].toInt())
}
Regex("""onex(\d+)""").matchEntire(page)?.let {
return Route.OnEx(it.groupValues[1].toInt())
}
Regex("""mluve(\d+)""").matchEntire(page)?.let { Regex("""mluve(\d+)""").matchEntire(page)?.let {
return Route.MluvEx(it.groupValues[1].toInt()) return Route.MluvEx(it.groupValues[1].toInt())
} }

View file

@ -29,6 +29,7 @@ class Content(root: JSONObject) {
} }
} }
val hw = raw.arr("hw") val hw = raw.arr("hw")
val on = raw.arr("on")
val mluvnice = raw.arr("mluvnice") val mluvnice = raw.arr("mluvnice")
val books = raw.arr("books") val books = raw.arr("books")
val changelog = raw.arr("changelog") val changelog = raw.arr("changelog")
@ -36,6 +37,7 @@ class Content(root: JSONObject) {
fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id } fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id }
fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id } fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id }
fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id } fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id }
fun onLesson(id: Int) = on.firstOrNull { it.int("id") == id }
fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n } fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n }
fun book(id: String) = books.firstOrNull { it.str("id") == id } fun book(id: String) = books.firstOrNull { it.str("id") == id }

View file

@ -32,6 +32,9 @@ class ProgressStore(context: Context) {
fun hwDone(id: Int) = prefs.getBoolean("hw.$id", false) fun hwDone(id: Int) = prefs.getBoolean("hw.$id", false)
fun markHw(id: Int) { prefs.edit().putBoolean("hw.$id", true).apply() } fun markHw(id: Int) { prefs.edit().putBoolean("hw.$id", true).apply() }
fun onDone(id: Int) = prefs.getBoolean("on.$id", false)
fun markOn(id: Int) { prefs.edit().putBoolean("on.$id", true).apply() }
fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false)
fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() }
@ -70,6 +73,14 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
content.on.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.onDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
sum.totalEx += content.mluvnice.size sum.totalEx += content.mluvnice.size
sum.openUnits += 1 sum.openUnits += 1
val md = content.mluvnice.count { p.mluvDone(it.int("id")) } val md = content.mluvnice.count { p.mluvDone(it.int("id")) }

View file

@ -848,6 +848,18 @@ fun NetQuizScreen(vm: AppViewModel, id: Int) {
DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markNet(id) } DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markNet(id) }
} }
@Composable
fun OnQuizScreen(vm: AppViewModel, id: Int) {
val lesson = vm.content.onLesson(id) ?: return
val spec = J(org.json.JSONObject().apply {
put("type", "choice")
put("title", vm.tr(lesson.str("exTitleKey")))
put("sub", lesson.str("quizHeadKey"))
put("questions", lesson.o.optJSONArray("quiz"))
})
DispatchExercise(vm, spec, vm.tr(lesson.str("exTitleKey"))) { vm.markOn(id) }
}
@Composable @Composable
fun HwQuizScreen(vm: AppViewModel, id: Int) { fun HwQuizScreen(vm: AppViewModel, id: Int) {
val lesson = vm.content.hwLesson(id) ?: return val lesson = vm.content.hwLesson(id) ?: return

View file

@ -29,6 +29,7 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FormatListNumbered import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft 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.Lock
import androidx.compose.material.icons.filled.Quiz import androidx.compose.material.icons.filled.Quiz
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
@ -217,6 +218,7 @@ private fun SubjectIcon(icon: String, open: Boolean) {
"de" -> GermanFlag(28.dp) "de" -> GermanFlag(28.dp)
"wifi" -> WifiIcon(tint, 26.dp) "wifi" -> WifiIcon(tint, 26.dp)
"chip" -> ChipIcon(tint, 26.dp) "chip" -> ChipIcon(tint, 26.dp)
"people" -> Icon(Icons.Filled.Groups, null, tint = tint, modifier = Modifier.size(26.dp))
"cz" -> CzechFlag(28.dp) "cz" -> CzechFlag(28.dp)
else -> Icon(Icons.Filled.Lock, null, tint = tint) else -> Icon(Icons.Filled.Lock, null, tint = tint)
} }
@ -298,6 +300,10 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
sum.totalEx = vm.content.hw.size sum.totalEx = vm.content.hw.size
sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) }
} }
"onyears", "onmap" -> {
sum.totalEx = vm.content.on.size
sum.doneEx = vm.content.on.count { vm.progress.onDone(it.int("id")) }
}
"czechmap" -> { "czechmap" -> {
sum.totalEx = vm.content.mluvnice.size sum.totalEx = vm.content.mluvnice.size
sum.doneEx = vm.content.mluvnice.count { vm.progress.mluvDone(it.int("id")) } sum.doneEx = vm.content.mluvnice.count { vm.progress.mluvDone(it.int("id")) }
@ -485,6 +491,14 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit${l.int("id")}", extra = "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit${l.int("id")}", extra = "hardware")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}")
} }
add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra = "rocnik")
add(vm.tr("on_year2"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik")
add(vm.tr("on_year3"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik")
add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked = true, extra = "rocnik")
vm.content.on.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit${l.int("id")}", extra = "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex${l.int("id")}")
}
add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice") 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")}") } 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") add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
@ -583,6 +597,20 @@ fun HwYearsScreen(vm: AppViewModel) {
} }
} }
@Composable
fun OnYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
item {
NavRow(title = vm.tr("on_year1"), subtitle = vm.tr("on_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.OnMap) }
}
items(listOf("on_year2", "on_year3", "on_year4")) { key ->
NavRow(title = vm.tr(key), subtitle = vm.tr("on_year_locked_sub"), locked = true, onClick = null)
}
}
}
}
@Composable @Composable
fun CzechMapScreen(vm: AppViewModel) { fun CzechMapScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Český jazyk a literatura"), vm.tr("czech_sub")) { inner -> DetailScaffold(vm, vm.tr("Český jazyk a literatura"), vm.tr("czech_sub")) { inner ->

View file

@ -6,6 +6,12 @@
# cs: bullet # cs: bullet
# en: bullet # en: bullet
--- 2026-10-02
cs: Občanská nauka: 1. ročník, 17 lekcí (člověk v lidském společenství)
cs: Občanská nauka: další ročníky jsou zatím zamčené
en: Civics: year 1, 17 lessons (the individual and society)
en: Civics: later years are still locked
--- 2026-09-25 --- 2026-09-25
cs: Technické vybavení: pět lekcí o digitalizaci zvuku (vzorkování, kvantování, PCM) cs: Technické vybavení: pět lekcí o digitalizaci zvuku (vzorkování, kvantování, PCM)
cs: Technické vybavení: skříň, zdroj, deska, CPU, karty, disky a vstupní zařízení cs: Technické vybavení: skříň, zdroj, deska, CPU, karty, disky a vstupní zařízení

View file

@ -128,6 +128,11 @@ final class AppModel: ObservableObject {
refresh() refresh()
} }
func markOn(_ id: Int) {
progress.markOn(id)
refresh()
}
func markMluv(_ n: Int) { func markMluv(_ n: Int) {
progress.markMluv(n) progress.markMluv(n)
refresh() refresh()

View file

@ -31,6 +31,7 @@ final class Content {
let netTasks: [J] let netTasks: [J]
let netAnswers: [[J]] let netAnswers: [[J]]
let hw: [J] let hw: [J]
let on: [J]
let mluvnice: [J] let mluvnice: [J]
let books: [J] let books: [J]
let changelog: [J] let changelog: [J]
@ -62,6 +63,7 @@ final class Content {
netAnswers = [] netAnswers = []
} }
hw = raw.arr("hw") hw = raw.arr("hw")
on = raw.arr("on")
mluvnice = raw.arr("mluvnice") mluvnice = raw.arr("mluvnice")
books = raw.arr("books") books = raw.arr("books")
changelog = raw.arr("changelog") changelog = raw.arr("changelog")
@ -70,6 +72,7 @@ final class Content {
func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } } func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } }
func netLesson(_ id: Int) -> J? { netLessons.first { $0.int("id") == id } } func netLesson(_ id: Int) -> J? { netLessons.first { $0.int("id") == id } }
func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } } func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } }
func onLesson(_ id: Int) -> J? { on.first { $0.int("id") == id } }
func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } } func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
func book(_ id: String) -> J? { books.first { $0.str("id") == id } } func book(_ id: String) -> J? { books.first { $0.str("id") == id } }

View file

@ -34,6 +34,9 @@ final class ProgressStore {
func hwDone(_ id: Int) -> Bool { defaults.bool(forKey: "hw.\(id)") } func hwDone(_ id: Int) -> Bool { defaults.bool(forKey: "hw.\(id)") }
func markHw(_ id: Int) { defaults.set(true, forKey: "hw.\(id)") } func markHw(_ id: Int) { defaults.set(true, forKey: "hw.\(id)") }
func onDone(_ id: Int) -> Bool { defaults.bool(forKey: "on.\(id)") }
func markOn(_ id: Int) { defaults.set(true, forKey: "on.\(id)") }
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") } func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") } func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
@ -77,6 +80,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
for l in content.on {
sum.totalEx += 1
sum.openUnits += 1
if p.onDone(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
sum.totalEx += content.mluvnice.count sum.totalEx += content.mluvnice.count
sum.openUnits += 1 sum.openUnits += 1
let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count

View file

@ -23,6 +23,10 @@ enum Route: Hashable {
case hwMap case hwMap
case hwLesson(Int) case hwLesson(Int)
case hwEx(Int) case hwEx(Int)
case onYears
case onMap
case onLesson(Int)
case onEx(Int)
case czechMap case czechMap
case mluvnice case mluvnice
case mluvEx(Int) case mluvEx(Int)
@ -48,6 +52,10 @@ enum Route: Hashable {
case .hwMap: return "hwmap" case .hwMap: return "hwmap"
case .hwLesson(let id): return "hwunit\(id)" case .hwLesson(let id): return "hwunit\(id)"
case .hwEx(let id): return "hwex\(id)" case .hwEx(let id): return "hwex\(id)"
case .onYears: return "onyears"
case .onMap: return "onmap"
case .onLesson(let id): return "onunit\(id)"
case .onEx(let id): return "onex\(id)"
case .czechMap: return "czechmap" case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice" case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)" case .mluvEx(let n): return "mluve\(n)"
@ -96,6 +104,14 @@ enum Route: Hashable {
return [.hwYears, .hwMap, .hwLesson(id)] return [.hwYears, .hwMap, .hwLesson(id)]
case .hwEx(let id): case .hwEx(let id):
return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)] return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)]
case .onYears:
return [.onYears]
case .onMap:
return [.onYears, .onMap]
case .onLesson(let id):
return [.onYears, .onMap, .onLesson(id)]
case .onEx(let id):
return [.onYears, .onMap, .onLesson(id), .onEx(id)]
case .czechMap: case .czechMap:
return [.czechMap] return [.czechMap]
case .mluvnice: case .mluvnice:
@ -124,6 +140,8 @@ func routeFromPage(_ page: String) -> Route? {
case "netmap": return .netMap case "netmap": return .netMap
case "hwyears": return .hwYears case "hwyears": return .hwYears
case "hwmap": return .hwMap case "hwmap": return .hwMap
case "onyears": return .onYears
case "onmap": return .onMap
case "czechmap": return .czechMap case "czechmap": return .czechMap
case "mluvnice": return .mluvnice case "mluvnice": return .mluvnice
case "readinglist": return .readingList case "readinglist": return .readingList
@ -167,6 +185,12 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) {
return .hwEx(n) return .hwEx(n)
} }
if let m = page.wholeMatch(of: /onunit(\d+)/), let n = Int(m.1) {
return .onLesson(n)
}
if let m = page.wholeMatch(of: /onex(\d+)/), let n = Int(m.1) {
return .onEx(n)
}
if let m = page.wholeMatch(of: /mluve(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /mluve(\d+)/), let n = Int(m.1) {
return .mluvEx(n) return .mluvEx(n)
} }

View file

@ -465,6 +465,7 @@ func subjectSymbol(_ icon: String) -> String {
case "wifi": return "wifi" case "wifi": return "wifi"
case "chip": return "cpu" case "chip": return "cpu"
case "cz": return "text.book.closed.fill" case "cz": return "text.book.closed.fill"
case "people": return "person.2.fill"
default: return "lock.fill" default: return "lock.fill"
} }
} }

View file

@ -625,6 +625,25 @@ struct NetQuizScreen: View {
} }
} }
struct OnQuizScreen: View {
@ObservedObject var vm: AppModel
let id: Int
var body: some View {
if let lesson = vm.content.onLesson(id) {
let spec = J([
"type": "choice",
"title": vm.tr(lesson.str("exTitleKey")),
"sub": lesson.str("quizHeadKey"),
"questions": lesson.rawArray("quiz"),
])
DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
vm.markOn(id)
}
}
}
}
struct HwQuizScreen: View { struct HwQuizScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
let id: Int let id: Int

View file

@ -423,6 +423,9 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
case "hwyears", "hwmap": case "hwyears", "hwmap":
sum.totalEx = vm.content.hw.count sum.totalEx = vm.content.hw.count
sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count
case "onyears", "onmap":
sum.totalEx = vm.content.on.count
sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count
case "czechmap": case "czechmap":
sum.totalEx = vm.content.mluvnice.count sum.totalEx = vm.content.mluvnice.count
sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count
@ -561,6 +564,14 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))")
} }
add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik")
add(vm.tr("on_year2"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik")
add(vm.tr("on_year3"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik")
add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik")
for l in vm.content.on {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex\(l.int("id"))")
}
add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice") add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice")
add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist") add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
for b in vm.content.books { for b in vm.content.books {
@ -777,6 +788,29 @@ struct HwYearsScreen: View {
} }
} }
struct OnYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
List {
NavigationLink(value: Route.onMap) {
ListRowLabel(title: vm.tr("on_year1"), subtitle: vm.tr("on_sub"))
}
ForEach(["on_year2", "on_year3", "on_year4"], id: \.self) { key in
Label {
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("on_year_locked_sub"))
} icon: {
Image(systemName: "lock.fill")
}
.foregroundStyle(.secondary)
}
}
.navigationTitle(vm.tr("Občanská nauka"))
.navigationSubtitle(vm.tr("on_years_sub"))
.appListStyle()
}
}
struct LessonRow: Identifiable { struct LessonRow: Identifiable {
let id: String let id: String
let title: String let title: String
@ -1345,6 +1379,29 @@ struct RouteDestination: View {
} }
case .hwEx(let id): case .hwEx(let id):
HwQuizScreen(vm: vm, id: id) HwQuizScreen(vm: vm, id: id)
case .onYears:
OnYearsScreen(vm: vm)
case .onMap:
LessonListScreen(
vm: vm,
title: vm.tr("on_year1"),
subtitle: vm.tr("on_sub"),
rows: vm.content.on.map { l in
let id = l.int("id")
return LessonRow(
id: "o\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.onDone(id),
destination: .onLesson(id)
)
}
)
case .onLesson(let id):
if let l = vm.content.onLesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .onEx(id))
}
case .onEx(let id):
OnQuizScreen(vm: vm, id: id)
case .czechMap: case .czechMap:
CzechMapScreen(vm: vm) CzechMapScreen(vm: vm)
case .mluvnice: case .mluvnice:

View file

@ -1084,6 +1084,18 @@ def main() -> int:
"quiz": qs, "quiz": qs,
}) })
on_lessons = []
for i in range(1, 18):
on_lessons.append({
"id": i,
"titleKey": f"on_unit{i}",
"subKey": f"on_unit{i}_sub",
"exTitleKey": f"on_ex{i}_title",
"quizHeadKey": f"on_quiz{i}_head",
"slides": slides_items(A(f"build_on_unit{i}_page.slides")),
"quiz": choice_items(A(f"build_on_unit{i}_exercise_page.qs")),
})
hw_lessons = [] hw_lessons = []
for i in range(1, 30): for i in range(1, 30):
hw_lessons.append({ hw_lessons.append({
@ -1481,7 +1493,7 @@ def main() -> int:
{"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"}, {"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"},
{"key": "Technické vybavení", "open": True, "target": "hwyears", "icon": "chip"}, {"key": "Technické vybavení", "open": True, "target": "hwyears", "icon": "chip"},
{"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"}, {"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"},
{"key": "Občanská nauka", "open": False, "target": None, "icon": "lock"}, {"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"},
{"key": "English", "open": False, "target": None, "icon": "lock"}, {"key": "English", "open": False, "target": None, "icon": "lock"},
{"key": "Matematika", "open": False, "target": None, "icon": "lock"}, {"key": "Matematika", "open": False, "target": None, "icon": "lock"},
{"key": "Fyzika", "open": False, "target": None, "icon": "lock"}, {"key": "Fyzika", "open": False, "target": None, "icon": "lock"},
@ -1502,6 +1514,7 @@ def main() -> int:
"answers": net_ans(A("net_ans")), "answers": net_ans(A("net_ans")),
}, },
"hw": hw_lessons, "hw": hw_lessons,
"on": on_lessons,
"mluvnice": mluvnice, "mluvnice": mluvnice,
"books": books, "books": books,
"changelog": parse_changelog(ROOT / "data" / "changelog.txt"), "changelog": parse_changelog(ROOT / "data" / "changelog.txt"),
@ -1536,6 +1549,7 @@ def main() -> int:
print(f" net lessons with slides: {sum(1 for L in net_lessons if L['slides'])}") print(f" net lessons with slides: {sum(1 for L in net_lessons if L['slides'])}")
print(f" net quizzes: {sum(1 for L in net_lessons if L['quiz'])}") print(f" net quizzes: {sum(1 for L in net_lessons if L['quiz'])}")
print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}") print(f" 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" mluvnice: {len(mluvnice)}") print(f" mluvnice: {len(mluvnice)}")
print(f" books quiz items: {[len(b['quiz']) for b in books]}") print(f" books quiz items: {[len(b['quiz']) for b in books]}")
print(f" changelog entries: {len(payload.get('changelog', []))}") print(f" changelog entries: {len(payload.get('changelog', []))}")
@ -1569,6 +1583,11 @@ def main() -> int:
missing.append(f"hw{L['id']} slides") missing.append(f"hw{L['id']} slides")
if not L["quiz"]: if not L["quiz"]:
missing.append(f"hw{L['id']} quiz") missing.append(f"hw{L['id']} quiz")
for L in on_lessons:
if not L["slides"]:
missing.append(f"on{L['id']} slides")
if not L["quiz"]:
missing.append(f"on{L['id']} quiz")
for m in mluvnice: for m in mluvnice:
if m["type"] in ("typed", "reveal") and not m.get("items"): if m["type"] in ("typed", "reveal") and not m.get("items"):
missing.append(f"mluv{m['id']}") missing.append(f"mluv{m['id']}")

View file

@ -19,6 +19,7 @@
#define NET_SUBJ 1 /* index of "Správa počítačových sítí" */ #define NET_SUBJ 1 /* index of "Správa počítačových sítí" */
#define HW_SUBJ 2 /* index of "Technické vybavení" */ #define HW_SUBJ 2 /* index of "Technické vybavení" */
#define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */
#define ON_SUBJ 4 /* index of "Občanská nauka" */
#define CSS_FILE "style.css" #define CSS_FILE "style.css"
#define CHANGELOG_FILE "changelog.txt" #define CHANGELOG_FILE "changelog.txt"
#define APP_ID "org.maturita.Maturita" #define APP_ID "org.maturita.Maturita"
@ -48,6 +49,7 @@ extern char *app_progress_u3;
extern char *app_settings_file; extern char *app_settings_file;
extern char *app_progress_net; extern char *app_progress_net;
extern char *app_progress_hw; extern char *app_progress_hw;
extern char *app_progress_on;
extern char *app_progress_mluvnice; extern char *app_progress_mluvnice;
extern char *app_progress_cetba; extern char *app_progress_cetba;
#define PROGRESS_DIR app_progress_dir #define PROGRESS_DIR app_progress_dir
@ -57,6 +59,7 @@ extern char *app_progress_cetba;
#define SETTINGS_FILE app_settings_file #define SETTINGS_FILE app_settings_file
#define PROGRESS_NET app_progress_net #define PROGRESS_NET app_progress_net
#define PROGRESS_HW app_progress_hw #define PROGRESS_HW app_progress_hw
#define PROGRESS_ON app_progress_on
#define NODE_SIZE 88.0 #define NODE_SIZE 88.0
#define PATH_SPAC 240.0 #define PATH_SPAC 240.0
#define ROAD_MX 150.0 /* horizontal canvas margin */ #define ROAD_MX 150.0 /* horizontal canvas margin */
@ -114,6 +117,8 @@ extern char *app_progress_cetba;
#define NET_LESSONS 27 #define NET_LESSONS 27
#define HW_UNITS 29 #define HW_UNITS 29
#define HW_LESSONS 29 #define HW_LESSONS 29
#define ON_UNITS 17
#define ON_LESSONS 17
#define HW_SLIDES 3 #define HW_SLIDES 3
#define HW2_SLIDES 6 #define HW2_SLIDES 6
#define HW3_SLIDES 2 #define HW3_SLIDES 2
@ -754,6 +759,8 @@ extern NetLesson net_lessons[NET_LESSONS];
extern NetLesson *net_notes_target; extern NetLesson *net_notes_target;
extern NetLesson hw_lessons[HW_LESSONS]; extern NetLesson hw_lessons[HW_LESSONS];
extern GtkWidget *hw_nodes[HW_UNITS]; extern GtkWidget *hw_nodes[HW_UNITS];
extern NetLesson on_lessons[ON_LESSONS];
extern GtkWidget *on_nodes[ON_UNITS];
extern GtkWidget *net_note_host; extern GtkWidget *net_note_host;
extern int net_note_last_w; extern int net_note_last_w;
extern GtkWidget *net_scroll; extern GtkWidget *net_scroll;
@ -841,6 +848,16 @@ void mark_hw_done(int lesson_id);
void refresh_hw_completion_ui(void); void refresh_hw_completion_ui(void);
void progress_for_hw(ProgressSum *out); void progress_for_hw(ProgressSum *out);
void hw_rail_theme_reset(void); void hw_rail_theme_reset(void);
void on_load_progress(void);
void on_save_progress(void);
void mark_on_done(int lesson_id);
void refresh_on_completion_ui(void);
void progress_for_on(ProgressSum *out);
void on_rail_theme_reset(void);
void on_lessons_apply_lang(void);
GtkWidget *build_onyears_page(void);
GtkWidget *build_onmap_page(void);
void add_on_pages(GtkStack *stack);
void czech_rail_theme_reset(void); void czech_rail_theme_reset(void);
void hw_lessons_apply_lang(void); void hw_lessons_apply_lang(void);
GtkWidget *build_hwyears_page(void); GtkWidget *build_hwyears_page(void);
@ -927,6 +944,8 @@ GtkWidget *build_hw_unit29_page(void);
GtkWidget *build_hw_unit29_exercise_page(void); GtkWidget *build_hw_unit29_exercise_page(void);
void draw_chip_icon(GtkDrawingArea *area, cairo_t *cr, void draw_chip_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_civics_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data);
void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_book_icon(GtkDrawingArea *area, cairo_t *cr, void draw_book_icon(GtkDrawingArea *area, cairo_t *cr,

View file

@ -700,6 +700,110 @@ const TrEntry tr_ui[] = {
"Quiz: game controls and touch"}, "Quiz: game controls and touch"},
{"hw_quiz29_head", "Kvíz k herním ovladačům a dotyku", {"hw_quiz29_head", "Kvíz k herním ovladačům a dotyku",
"Quiz on game controls and touch"}, "Quiz on game controls and touch"},
{"on_sub", "Člověk v lidském společenství. Vyberte lekci na cestě.",
"The individual and society. Pick a lesson on the path."},
{"on_years_sub", "Vyberte ročník a začněte procvičovat.",
"Pick a school year and start practicing."},
{"on_year1", "1. ročník", "Year 1"},
{"on_year2", "2. ročník", "Year 2"},
{"on_year3", "3. ročník", "Year 3"},
{"on_year4", "4. ročník", "Year 4"},
{"on_year_locked_sub", "Zatím bez obsahu", "No content yet"},
{"on_unit1", "Výchova k občanství", "Civics"},
{"on_unit1_sub", "2 snímky o smyslu předmětu a pak kvíz.",
"2 slides on what the subject is for, then a quiz."},
{"on_ex1_title", "Kvíz: výchova k občanství", "Quiz: civics"},
{"on_quiz1_head", "Kvíz k výchově k občanství", "Quiz on civics"},
{"on_unit2", "Učení a volný čas", "Learning and free time"},
{"on_unit2_sub", "2 snímky o učení a volném čase a pak kvíz.",
"2 slides on learning and free time, then a quiz."},
{"on_ex2_title", "Kvíz: učení a volný čas", "Quiz: learning and free time"},
{"on_quiz2_head", "Kvíz k učení a volnému času",
"Quiz on learning and free time"},
{"on_unit3", "Osobnost člověka", "Personality"},
{"on_unit3_sub", "2 snímky o osobnosti a socializaci a pak kvíz.",
"2 slides on personality and socialisation, then a quiz."},
{"on_ex3_title", "Kvíz: osobnost člověka", "Quiz: personality"},
{"on_quiz3_head", "Kvíz k osobnosti", "Quiz on personality"},
{"on_unit4", "Etapy lidského života", "Stages of life"},
{"on_unit4_sub", "2 snímky o etapách života a pak kvíz.",
"2 slides on the stages of life, then a quiz."},
{"on_ex4_title", "Kvíz: etapy života", "Quiz: stages of life"},
{"on_quiz4_head", "Kvíz k etapám života", "Quiz on the stages of life"},
{"on_unit5", "Psychohygiena", "Mental hygiene"},
{"on_unit5_sub", "2 snímky o náročných situacích a psychohygieně a pak kvíz.",
"2 slides on hard situations and mental hygiene, then a quiz."},
{"on_ex5_title", "Kvíz: psychohygiena", "Quiz: mental hygiene"},
{"on_quiz5_head", "Kvíz k psychohygieně", "Quiz on mental hygiene"},
{"on_unit6", "Slušné chování", "Good manners"},
{"on_unit6_sub", "2 snímky o etiketě a vztazích a pak kvíz.",
"2 slides on manners and relationships, then a quiz."},
{"on_ex6_title", "Kvíz: slušné chování", "Quiz: good manners"},
{"on_quiz6_head", "Kvíz ke slušnému chování", "Quiz on good manners"},
{"on_unit7", "Komunikace a konflikty", "Communication and conflict"},
{"on_unit7_sub", "2 snímky o komunikaci a konfliktech a pak kvíz.",
"2 slides on communication and conflict, then a quiz."},
{"on_ex7_title", "Kvíz: komunikace a konflikty",
"Quiz: communication and conflict"},
{"on_quiz7_head", "Kvíz ke komunikaci a konfliktům",
"Quiz on communication and conflict"},
{"on_unit8", "Zdraví a životní styl", "Health and lifestyle"},
{"on_unit8_sub", "2 snímky o zdraví a životním stylu a pak kvíz.",
"2 slides on health and lifestyle, then a quiz."},
{"on_ex8_title", "Kvíz: zdraví a životní styl", "Quiz: health and lifestyle"},
{"on_quiz8_head", "Kvíz ke zdraví a životnímu stylu",
"Quiz on health and lifestyle"},
{"on_unit9", "Závislosti", "Addictions"},
{"on_unit9_sub", "2 snímky o závislostech a pomoci a pak kvíz.",
"2 slides on addictions and where to get help, then a quiz."},
{"on_ex9_title", "Kvíz: závislosti", "Quiz: addictions"},
{"on_quiz9_head", "Kvíz k závislostem", "Quiz on addictions"},
{"on_unit10", "Rodina a sociální skupiny", "Family and social groups"},
{"on_unit10_sub", "2 snímky o skupinách a rodině a pak kvíz.",
"2 slides on groups and the family, then a quiz."},
{"on_ex10_title", "Kvíz: rodina a skupiny", "Quiz: family and groups"},
{"on_quiz10_head", "Kvíz k rodině a sociálním skupinám",
"Quiz on the family and social groups"},
{"on_unit11", "Komunita, dav a veřejnost", "Community, crowd and the public"},
{"on_unit11_sub", "2 snímky o komunitě, davu a veřejnosti a pak kvíz.",
"2 slides on community, crowds and the public, then a quiz."},
{"on_ex11_title", "Kvíz: komunita a dav", "Quiz: community and crowds"},
{"on_quiz11_head", "Kvíz ke komunitě, davu a veřejnosti",
"Quiz on community, crowds and the public"},
{"on_unit12", "Vrstevníci a migrace", "Peers and migration"},
{"on_unit12_sub", "2 snímky o vrstevnících, solidaritě a migraci a pak kvíz.",
"2 slides on peers, solidarity and migration, then a quiz."},
{"on_ex12_title", "Kvíz: vrstevníci a migrace", "Quiz: peers and migration"},
{"on_quiz12_head", "Kvíz k vrstevníkům a migraci",
"Quiz on peers and migration"},
{"on_unit13", "Společnost a nerovnost", "Society and inequality"},
{"on_unit13_sub", "2 snímky o společnosti, vrstvách a chudobě a pak kvíz.",
"2 slides on society, strata and poverty, then a quiz."},
{"on_ex13_title", "Kvíz: společnost a nerovnost",
"Quiz: society and inequality"},
{"on_quiz13_head", "Kvíz ke společnosti a nerovnosti",
"Quiz on society and inequality"},
{"on_unit14", "Postavení mužů a žen", "Men, women and equality"},
{"on_unit14_sub", "2 snímky o rovnosti a genderu a pak kvíz.",
"2 slides on equality and gender, then a quiz."},
{"on_ex14_title", "Kvíz: postavení mužů a žen", "Quiz: men, women and equality"},
{"on_quiz14_head", "Kvíz k postavení mužů a žen",
"Quiz on men, women and equality"},
{"on_unit15", "Kultura", "Culture"},
{"on_unit15_sub", "2 snímky o hmotné a duchovní kultuře a pak kvíz.",
"2 slides on material and non-material culture, then a quiz."},
{"on_ex15_title", "Kvíz: kultura", "Quiz: culture"},
{"on_quiz15_head", "Kvíz ke kultuře", "Quiz on culture"},
{"on_unit16", "Víra a náboženství", "Faith and religion"},
{"on_unit16_sub", "2 snímky o víře, církvích a sektách a pak kvíz.",
"2 slides on faith, churches and sects, then a quiz."},
{"on_ex16_title", "Kvíz: víra a náboženství", "Quiz: faith and religion"},
{"on_quiz16_head", "Kvíz k víře a náboženství", "Quiz on faith and religion"},
{"on_unit17", "Rozpočet domácnosti", "Household budget"},
{"on_unit17_sub", "2 snímky o rozpočtu a finanční tísni a pak kvíz.",
"2 slides on the household budget and money trouble, then a quiz."},
{"on_ex17_title", "Kvíz: rozpočet domácnosti", "Quiz: household budget"},
{"on_quiz17_head", "Kvíz k rozpočtu domácnosti", "Quiz on the household budget"},
{"czech_sub", "Vyberte oblast a začněte procvičovat.", {"czech_sub", "Vyberte oblast a začněte procvičovat.",
"Pick an area and start practicing."}, "Pick an area and start practicing."},
{"Literatura", "Literatura", "Literature"}, {"Literatura", "Literatura", "Literature"},
@ -1723,6 +1827,7 @@ void refresh_welcome_heading(void) {
void refresh_stats_ui(void); void refresh_stats_ui(void);
void net_lessons_apply_lang(void); void net_lessons_apply_lang(void);
void hw_lessons_apply_lang(void); void hw_lessons_apply_lang(void);
void on_lessons_apply_lang(void);
void net_rail_theme_reset(void); void net_rail_theme_reset(void);
void apply_language(void) { void apply_language(void) {
@ -1737,5 +1842,6 @@ void apply_language(void) {
changelog_apply_lang(); changelog_apply_lang();
net_lessons_apply_lang(); net_lessons_apply_lang();
hw_lessons_apply_lang(); hw_lessons_apply_lang();
on_lessons_apply_lang();
search_apply_lang(); search_apply_lang();
} }

View file

@ -142,6 +142,52 @@ void draw_chip_icon(GtkDrawingArea *area, cairo_t *cr,
} }
} }
/* Balance scale for Občanská nauka. */
void draw_civics_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.34;
const double beam_y = top + s * 0.22;
const double half = s * 0.28;
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_width(cr, MAX(1.6, s * 0.07));
cairo_move_to(cr, cx, top);
cairo_line_to(cr, cx, top + s * 0.62);
cairo_stroke(cr);
cairo_move_to(cr, cx - half, beam_y);
cairo_line_to(cr, cx + half, beam_y);
cairo_stroke(cr);
cairo_set_line_width(cr, MAX(1.4, s * 0.05));
cairo_move_to(cr, cx - half, beam_y);
cairo_line_to(cr, cx - half, beam_y + s * 0.16);
cairo_move_to(cr, cx + half, beam_y);
cairo_line_to(cr, cx + half, beam_y + s * 0.16);
cairo_stroke(cr);
cairo_new_path(cr);
cairo_arc(cr, cx - half, beam_y + s * 0.24, s * 0.11, 0.15 * G_PI, 0.85 * G_PI);
cairo_stroke(cr);
cairo_new_path(cr);
cairo_arc(cr, cx + half, beam_y + s * 0.24, s * 0.11, 0.15 * G_PI, 0.85 * G_PI);
cairo_stroke(cr);
cairo_set_line_width(cr, MAX(1.8, s * 0.08));
cairo_move_to(cr, cx - s * 0.16, top + s * 0.66);
cairo_line_to(cr, cx + s * 0.16, top + s * 0.66);
cairo_stroke(cr);
}
/* Open book glyph, painted in the given colour. */ /* Open book glyph, painted in the given colour. */
static void book_paint(cairo_t *cr, int width, int height, Rgb c) { static void book_paint(cairo_t *cr, int width, int height, Rgb c) {
const double w = width; const double w = width;

View file

@ -385,7 +385,8 @@ static gboolean shot_later(gpointer data) {
static gboolean run_smoke_test(gpointer data) { static gboolean run_smoke_test(gpointer data) {
static const char *pages[] = { static const char *pages[] = {
"welcome", "subjects", "roadmap", "stats", "welcome", "subjects", "roadmap", "stats",
"netyears", "netmap", "hwyears", "hwmap", "czechmap", "netyears", "netmap", "hwyears", "hwmap", "onyears", "onmap",
"czechmap",
"mluvnice", "readinglist", "mluvnice", "readinglist",
"cetba1984", "cetba1984quiz", "cetba1984dej", "cetba1984", "cetba1984quiz", "cetba1984dej",
"cetbaFuks", "cetbaFuksQuiz", "cetbaFuksDej", "cetbaFuks", "cetbaFuksQuiz", "cetbaFuksDej",
@ -396,6 +397,7 @@ static gboolean run_smoke_test(gpointer data) {
"hwunit10", "hwex10", "hwunit15", "hwex15", "hwunit10", "hwex10", "hwunit15", "hwex15",
"hwunit20", "hwex20", "hwunit25", "hwex25", "hwunit20", "hwex20", "hwunit25", "hwex25",
"hwunit29", "hwex29", "hwunit29", "hwex29",
"onunit1", "onex1", "onunit17", "onex17",
"unit1", "unit2", "unit3", "unit1", "unit2", "unit3",
"u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15", "u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15",
"u1vocab", "u2vocab", "u3vocab", "u1vocab", "u2vocab", "u3vocab",
@ -506,6 +508,9 @@ void activate(GtkApplication *app, gpointer user_data) {
gtk_stack_add_named(main_stack, build_netmap_page(), "netmap"); gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");
gtk_stack_add_named(main_stack, build_hwyears_page(), "hwyears"); gtk_stack_add_named(main_stack, build_hwyears_page(), "hwyears");
gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap"); gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap");
gtk_stack_add_named(main_stack, build_onyears_page(), "onyears");
gtk_stack_add_named(main_stack, build_onmap_page(), "onmap");
add_on_pages(main_stack);
gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap"); gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap");
gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice"); gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice");
for (int n = 1; n <= 20; n++) { for (int n = 1; n <= 20; n++) {

2114
src/obcanka.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -54,6 +54,7 @@ void load_progress(void) {
unit_load_progress(&units[i]); unit_load_progress(&units[i]);
net_load_progress(); net_load_progress();
hw_load_progress(); hw_load_progress();
on_load_progress();
} }
/* Redraw the exercise rails of every unit that already has a page. */ /* Redraw the exercise rails of every unit that already has a page. */
@ -341,3 +342,94 @@ void progress_for_hw(ProgressSum *out) {
} }
} }
} }
void on_load_progress(void) {
GKeyFile *kf = g_key_file_new();
GError *err = NULL;
if (!g_key_file_load_from_file(kf, PROGRESS_ON, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
return;
}
for (int i = 0; i < ON_LESSONS; i++) {
gchar key[8];
g_snprintf(key, sizeof(key), "%d", i + 1);
on_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
g_key_file_free(kf);
}
void on_save_progress(void) {
GKeyFile *kf = g_key_file_new();
for (int i = 0; i < ON_LESSONS; i++) {
gchar key[8];
g_snprintf(key, sizeof(key), "%d", i + 1);
g_key_file_set_boolean(kf, "done", key, on_lessons[i].done);
}
if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
g_key_file_free(kf);
return;
}
gchar *data = g_key_file_to_data(kf, NULL, NULL);
g_key_file_free(kf);
if (data) {
GError *err = NULL;
g_file_set_contents(PROGRESS_ON, data, -1, &err);
if (err)
g_error_free(err);
g_free(data);
}
}
void refresh_on_completion_ui(void) {
for (int i = 0; i < ON_LESSONS; i++) {
GtkWidget *node = on_nodes[i];
GtkWidget *icon = on_lessons[i].done_icon;
if (!node)
continue;
if (on_lessons[i].done) {
gtk_widget_remove_css_class(node, "current");
gtk_widget_add_css_class(node, "done");
if (icon)
gtk_widget_set_visible(icon, TRUE);
} else {
gtk_widget_remove_css_class(node, "done");
gtk_widget_add_css_class(node, "current");
if (icon)
gtk_widget_set_visible(icon, FALSE);
}
}
}
void mark_on_done(int lesson_id) {
if (lesson_id < 0 || lesson_id >= ON_LESSONS)
return;
if (on_lessons[lesson_id].done)
return;
on_lessons[lesson_id].done = TRUE;
on_save_progress();
refresh_on_completion_ui();
refresh_stats_ui();
}
void progress_for_on(ProgressSum *out) {
out->total_ex += ON_LESSONS;
out->open_units += ON_LESSONS;
for (int i = 0; i < ON_LESSONS; i++) {
if (on_lessons[i].done) {
out->done_ex++;
out->done_units++;
}
}
}

View file

@ -22,7 +22,8 @@ static GtkWidget *search_empty;
static GPtrArray *search_items; static GPtrArray *search_items;
static gboolean subject_open(int i) { static gboolean subject_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ; return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ
|| i == ON_SUBJ;
} }
static const char *subject_page(int i) { static const char *subject_page(int i) {
@ -34,6 +35,8 @@ static const char *subject_page(int i) {
return "hwyears"; return "hwyears";
if (i == CZ_SUBJ) if (i == CZ_SUBJ)
return "czechmap"; return "czechmap";
if (i == ON_SUBJ)
return "onyears";
return "subjects"; return "subjects";
} }
@ -199,6 +202,18 @@ static void catalog_rebuild(void) {
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
} }
catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik");
catalog_add_key("on_year2", "search_lesson", "onyears", TRUE, 1, "rocnik");
catalog_add_key("on_year3", "search_lesson", "onyears", TRUE, 1, "rocnik");
catalog_add_key("on_year4", "search_lesson", "onyears", TRUE, 1, "rocnik");
for (i = 1; i <= ON_LESSONS; i++) {
g_snprintf(key, sizeof(key), "on_unit%d", i);
g_snprintf(page, sizeof(page), "onunit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "obcanka");
g_snprintf(page, sizeof(page), "onex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
}
catalog_add_key("Český jazyk a literatura", "search_subject", catalog_add_key("Český jazyk a literatura", "search_subject",
"czechmap", FALSE, 0, "cestina czech"); "czechmap", FALSE, 0, "cestina czech");
catalog_add_key("Literatura", "search_lesson", "czechmap", TRUE, 1, NULL); catalog_add_key("Literatura", "search_lesson", "czechmap", TRUE, 1, NULL);

View file

@ -69,6 +69,8 @@ void refresh_stats_ui(void) {
progress_for_net(&all); progress_for_net(&all);
else if (s == HW_SUBJ) else if (s == HW_SUBJ)
progress_for_hw(&all); progress_for_hw(&all);
else if (s == ON_SUBJ)
progress_for_on(&all);
else if (s == CZ_SUBJ) else if (s == CZ_SUBJ)
progress_for_mluvnice(&all); progress_for_mluvnice(&all);
else else
@ -100,7 +102,8 @@ void refresh_stats_ui(void) {
GtkWidget *bar = stats_ui.subj[s].bar; GtkWidget *bar = stats_ui.subj[s].bar;
ProgressSum sp = {0}; ProgressSum sp = {0};
gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ) gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ)
|| (s == CZ_SUBJ) || sub_unit_count[s] > 0; || (s == ON_SUBJ) || (s == CZ_SUBJ)
|| sub_unit_count[s] > 0;
if (!count || !bar) if (!count || !bar)
continue; continue;
@ -108,6 +111,8 @@ void refresh_stats_ui(void) {
progress_for_net(&sp); progress_for_net(&sp);
else if (s == HW_SUBJ) else if (s == HW_SUBJ)
progress_for_hw(&sp); progress_for_hw(&sp);
else if (s == ON_SUBJ)
progress_for_on(&sp);
else if (s == CZ_SUBJ) else if (s == CZ_SUBJ)
progress_for_mluvnice(&sp); progress_for_mluvnice(&sp);
else else

View file

@ -4,8 +4,8 @@
/* Subjects page — icon grid, one tile per subject */ /* Subjects page — icon grid, one tile per subject */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware) and CZ_SUBJ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ
* (Czech) are unlocked. */ * (Czech) and ON_SUBJ (civics) are unlocked. */
const char *sub_keys[NUM_SUBJECTS] = { const char *sub_keys[NUM_SUBJECTS] = {
"Deutsch", "Deutsch",
"Správa počítačových sítí", "Správa počítačových sítí",
@ -153,7 +153,8 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
} }
static gboolean subject_is_open(int i) { static gboolean subject_is_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ; return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ
|| i == ON_SUBJ;
} }
static GtkWidget *subject_icon(int i) { static GtkWidget *subject_icon(int i) {
@ -173,6 +174,9 @@ static GtkWidget *subject_icon(int i) {
else if (i == CZ_SUBJ) else if (i == CZ_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_czech_flag, NULL, NULL); draw_czech_flag, NULL, NULL);
else if (i == ON_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_civics_icon, NULL, NULL);
else else
return icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 22); return icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 22);
return area; return area;
@ -187,6 +191,8 @@ static const char *subject_target(int i) {
return "hwyears"; return "hwyears";
if (i == CZ_SUBJ) if (i == CZ_SUBJ)
return "czechmap"; return "czechmap";
if (i == ON_SUBJ)
return "onyears";
return NULL; return NULL;
} }

View file

@ -426,6 +426,7 @@ void apply_theme(void) {
} }
net_rail_theme_reset(); net_rail_theme_reset();
hw_rail_theme_reset(); hw_rail_theme_reset();
on_rail_theme_reset();
czech_rail_theme_reset(); czech_rail_theme_reset();
book_rail_theme_reset(); book_rail_theme_reset();
} }

View file

@ -20,6 +20,7 @@ char *app_progress_u3;
char *app_settings_file; char *app_settings_file;
char *app_progress_net; char *app_progress_net;
char *app_progress_hw; char *app_progress_hw;
char *app_progress_on;
char *app_progress_mluvnice; char *app_progress_mluvnice;
char *app_progress_cetba; char *app_progress_cetba;
@ -158,6 +159,7 @@ static void set_progress_paths(const char *write_root) {
g_free(app_settings_file); g_free(app_settings_file);
g_free(app_progress_net); g_free(app_progress_net);
g_free(app_progress_hw); g_free(app_progress_hw);
g_free(app_progress_on);
g_free(app_progress_mluvnice); g_free(app_progress_mluvnice);
g_free(app_progress_cetba); g_free(app_progress_cetba);
@ -168,6 +170,7 @@ static void set_progress_paths(const char *write_root) {
app_settings_file = g_build_filename(app_progress_dir, "settings.conf", NULL); app_settings_file = g_build_filename(app_progress_dir, "settings.conf", NULL);
app_progress_net = g_build_filename(app_progress_dir, "net.conf", NULL); app_progress_net = g_build_filename(app_progress_dir, "net.conf", NULL);
app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL); app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL);
app_progress_on = g_build_filename(app_progress_dir, "on.conf", NULL);
app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL); app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL);
app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL); app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL);
} }