Add year-3 civics lessons on the individual and the law.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-02 22:52:39 +02:00
commit 0ee2f8e960
26 changed files with 2321 additions and 23 deletions

View file

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

View file

@ -33,6 +33,7 @@ final class Content {
let hw: [J]
let on: [J]
let on2: [J]
let on3: [J]
let mluvnice: [J]
let books: [J]
let changelog: [J]
@ -66,6 +67,7 @@ final class Content {
hw = raw.arr("hw")
on = raw.arr("on")
on2 = raw.arr("on2")
on3 = raw.arr("on3")
mluvnice = raw.arr("mluvnice")
books = raw.arr("books")
changelog = raw.arr("changelog")
@ -76,6 +78,7 @@ final class Content {
func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } }
func onLesson(_ id: Int) -> J? { on.first { $0.int("id") == id } }
func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } }
func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } }
func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
func book(_ id: String) -> J? { books.first { $0.str("id") == id } }

View file

@ -40,6 +40,9 @@ final class ProgressStore {
func on2Done(_ id: Int) -> Bool { defaults.bool(forKey: "on2.\(id)") }
func markOn2(_ id: Int) { defaults.set(true, forKey: "on2.\(id)") }
func on3Done(_ id: Int) -> Bool { defaults.bool(forKey: "on3.\(id)") }
func markOn3(_ id: Int) { defaults.set(true, forKey: "on3.\(id)") }
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
@ -99,6 +102,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1
}
}
for l in content.on3 {
sum.totalEx += 1
sum.openUnits += 1
if p.on3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
sum.totalEx += content.mluvnice.count
sum.openUnits += 1
let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count

View file

@ -30,6 +30,9 @@ enum Route: Hashable {
case on2Map
case on2Lesson(Int)
case on2Ex(Int)
case on3Map
case on3Lesson(Int)
case on3Ex(Int)
case czechMap
case mluvnice
case mluvEx(Int)
@ -62,6 +65,9 @@ enum Route: Hashable {
case .on2Map: return "on2map"
case .on2Lesson(let id): return "on2unit\(id)"
case .on2Ex(let id): return "on2ex\(id)"
case .on3Map: return "on3map"
case .on3Lesson(let id): return "on3unit\(id)"
case .on3Ex(let id): return "on3ex\(id)"
case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)"
@ -124,6 +130,12 @@ enum Route: Hashable {
return [.onYears, .on2Map, .on2Lesson(id)]
case .on2Ex(let id):
return [.onYears, .on2Map, .on2Lesson(id), .on2Ex(id)]
case .on3Map:
return [.onYears, .on3Map]
case .on3Lesson(let id):
return [.onYears, .on3Map, .on3Lesson(id)]
case .on3Ex(let id):
return [.onYears, .on3Map, .on3Lesson(id), .on3Ex(id)]
case .czechMap:
return [.czechMap]
case .mluvnice:
@ -155,6 +167,7 @@ func routeFromPage(_ page: String) -> Route? {
case "onyears": return .onYears
case "onmap": return .onMap
case "on2map": return .on2Map
case "on3map": return .on3Map
case "czechmap": return .czechMap
case "mluvnice": return .mluvnice
case "readinglist": return .readingList
@ -198,6 +211,12 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) {
return .hwEx(n)
}
if let m = page.wholeMatch(of: /on3unit(\d+)/), let n = Int(m.1) {
return .on3Lesson(n)
}
if let m = page.wholeMatch(of: /on3ex(\d+)/), let n = Int(m.1) {
return .on3Ex(n)
}
if let m = page.wholeMatch(of: /on2unit(\d+)/), let n = Int(m.1) {
return .on2Lesson(n)
}

View file

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

View file

@ -423,10 +423,11 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
case "hwyears", "hwmap":
sum.totalEx = vm.content.hw.count
sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count
case "onyears", "onmap", "on2map":
sum.totalEx = vm.content.on.count + vm.content.on2.count
case "onyears", "onmap", "on2map", "on3map":
sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count
sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count
+ vm.content.on2.filter { vm.progress.on2Done($0.int("id")) }.count
+ vm.content.on3.filter { vm.progress.on3Done($0.int("id")) }.count
case "czechmap":
sum.totalEx = vm.content.mluvnice.count
sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count
@ -567,7 +568,7 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
}
add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik")
add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik")
add(vm.tr("on_year3"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik")
add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
add(vm.tr("on_year4"), vm.tr("search_lesson"), "onyears", locked: true, extra: "rocnik")
for l in vm.content.on {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka")
@ -577,6 +578,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex\(l.int("id"))")
}
for l in vm.content.on3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on3unit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on3ex\(l.int("id"))")
}
add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice")
add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
for b in vm.content.books {
@ -804,7 +809,10 @@ struct OnYearsScreen: View {
NavigationLink(value: Route.on2Map) {
ListRowLabel(title: vm.tr("on_year2"), subtitle: vm.tr("on2_sub"))
}
ForEach(["on_year3", "on_year4"], id: \.self) { key in
NavigationLink(value: Route.on3Map) {
ListRowLabel(title: vm.tr("on_year3"), subtitle: vm.tr("on3_sub"))
}
ForEach(["on_year4"], id: \.self) { key in
Label {
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("on_year_locked_sub"))
} icon: {
@ -1431,6 +1439,27 @@ struct RouteDestination: View {
}
case .on2Ex(let id):
On2QuizScreen(vm: vm, id: id)
case .on3Map:
LessonListScreen(
vm: vm,
title: vm.tr("on_year3"),
subtitle: vm.tr("on3_sub"),
rows: vm.content.on3.map { l in
let id = l.int("id")
return LessonRow(
id: "o3\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.on3Done(id),
destination: .on3Lesson(id)
)
}
)
case .on3Lesson(let id):
if let l = vm.content.on3Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on3Ex(id))
}
case .on3Ex(let id):
On3QuizScreen(vm: vm, id: id)
case .czechMap:
CzechMapScreen(vm: vm)
case .mluvnice: