Add Czech literature lessons across four secondary-school years.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-06 08:38:49 +02:00
commit 09312d8c93
31 changed files with 4395 additions and 35 deletions

View file

@ -148,6 +148,26 @@ final class AppModel: ObservableObject {
refresh()
}
func markLit(_ id: Int) {
progress.markLit(id)
refresh()
}
func markLit2(_ id: Int) {
progress.markLit2(id)
refresh()
}
func markLit3(_ id: Int) {
progress.markLit3(id)
refresh()
}
func markLit4(_ id: Int) {
progress.markLit4(id)
refresh()
}
func markMluv(_ n: Int) {
progress.markMluv(n)
refresh()

View file

@ -35,6 +35,10 @@ final class Content {
let on2: [J]
let on3: [J]
let on4: [J]
let lit: [J]
let lit2: [J]
let lit3: [J]
let lit4: [J]
let mluvnice: [J]
let books: [J]
let changelog: [J]
@ -70,6 +74,10 @@ final class Content {
on2 = raw.arr("on2")
on3 = raw.arr("on3")
on4 = raw.arr("on4")
lit = raw.arr("lit")
lit2 = raw.arr("lit2")
lit3 = raw.arr("lit3")
lit4 = raw.arr("lit4")
mluvnice = raw.arr("mluvnice")
books = raw.arr("books")
changelog = raw.arr("changelog")
@ -82,6 +90,10 @@ final class Content {
func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } }
func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } }
func on4Lesson(_ id: Int) -> J? { on4.first { $0.int("id") == id } }
func litLesson(_ id: Int) -> J? { lit.first { $0.int("id") == id } }
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 mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
func book(_ id: String) -> J? { books.first { $0.str("id") == id } }

View file

@ -46,6 +46,15 @@ final class ProgressStore {
func on4Done(_ id: Int) -> Bool { defaults.bool(forKey: "on4.\(id)") }
func markOn4(_ id: Int) { defaults.set(true, forKey: "on4.\(id)") }
func litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") }
func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") }
func lit2Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit2.\(id)") }
func markLit2(_ id: Int) { defaults.set(true, forKey: "lit2.\(id)") }
func lit3Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit3.\(id)") }
func markLit3(_ id: Int) { defaults.set(true, forKey: "lit3.\(id)") }
func lit4Done(_ id: Int) -> Bool { defaults.bool(forKey: "lit4.\(id)") }
func markLit4(_ id: Int) { defaults.set(true, forKey: "lit4.\(id)") }
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
@ -121,6 +130,38 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1
}
}
for l in content.lit {
sum.totalEx += 1
sum.openUnits += 1
if p.litDone(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.lit2 {
sum.totalEx += 1
sum.openUnits += 1
if p.lit2Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.lit3 {
sum.totalEx += 1
sum.openUnits += 1
if p.lit3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.lit4 {
sum.totalEx += 1
sum.openUnits += 1
if p.lit4Done(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

@ -36,6 +36,19 @@ enum Route: Hashable {
case on4Map
case on4Lesson(Int)
case on4Ex(Int)
case litYears
case litMap
case litLesson(Int)
case litEx(Int)
case lit2Map
case lit2Lesson(Int)
case lit2Ex(Int)
case lit3Map
case lit3Lesson(Int)
case lit3Ex(Int)
case lit4Map
case lit4Lesson(Int)
case lit4Ex(Int)
case czechMap
case mluvnice
case mluvEx(Int)
@ -74,6 +87,19 @@ enum Route: Hashable {
case .on4Map: return "on4map"
case .on4Lesson(let id): return "on4unit\(id)"
case .on4Ex(let id): return "on4ex\(id)"
case .litYears: return "lityears"
case .litMap: return "litmap"
case .litLesson(let id): return "litunit\(id)"
case .litEx(let id): return "litex\(id)"
case .lit2Map: return "lit2map"
case .lit2Lesson(let id): return "lit2unit\(id)"
case .lit2Ex(let id): return "lit2ex\(id)"
case .lit3Map: return "lit3map"
case .lit3Lesson(let id): return "lit3unit\(id)"
case .lit3Ex(let id): return "lit3ex\(id)"
case .lit4Map: return "lit4map"
case .lit4Lesson(let id): return "lit4unit\(id)"
case .lit4Ex(let id): return "lit4ex\(id)"
case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)"
@ -148,6 +174,32 @@ enum Route: Hashable {
return [.onYears, .on4Map, .on4Lesson(id)]
case .on4Ex(let id):
return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)]
case .litYears:
return [.czechMap, .litYears]
case .litMap:
return [.czechMap, .litYears, .litMap]
case .litLesson(let id):
return [.czechMap, .litYears, .litMap, .litLesson(id)]
case .litEx(let id):
return [.czechMap, .litYears, .litMap, .litLesson(id), .litEx(id)]
case .lit2Map:
return [.czechMap, .litYears, .lit2Map]
case .lit2Lesson(let id):
return [.czechMap, .litYears, .lit2Map, .lit2Lesson(id)]
case .lit2Ex(let id):
return [.czechMap, .litYears, .lit2Map, .lit2Lesson(id), .lit2Ex(id)]
case .lit3Map:
return [.czechMap, .litYears, .lit3Map]
case .lit3Lesson(let id):
return [.czechMap, .litYears, .lit3Map, .lit3Lesson(id)]
case .lit3Ex(let id):
return [.czechMap, .litYears, .lit3Map, .lit3Lesson(id), .lit3Ex(id)]
case .lit4Map:
return [.czechMap, .litYears, .lit4Map]
case .lit4Lesson(let id):
return [.czechMap, .litYears, .lit4Map, .lit4Lesson(id)]
case .lit4Ex(let id):
return [.czechMap, .litYears, .lit4Map, .lit4Lesson(id), .lit4Ex(id)]
case .czechMap:
return [.czechMap]
case .mluvnice:
@ -181,6 +233,11 @@ func routeFromPage(_ page: String) -> Route? {
case "on2map": return .on2Map
case "on3map": return .on3Map
case "on4map": return .on4Map
case "lityears": return .litYears
case "litmap": return .litMap
case "lit2map": return .lit2Map
case "lit3map": return .lit3Map
case "lit4map": return .lit4Map
case "czechmap": return .czechMap
case "mluvnice": return .mluvnice
case "readinglist": return .readingList
@ -224,6 +281,30 @@ 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: /lit4unit(\d+)/), let n = Int(m.1) {
return .lit4Lesson(n)
}
if let m = page.wholeMatch(of: /lit4ex(\d+)/), let n = Int(m.1) {
return .lit4Ex(n)
}
if let m = page.wholeMatch(of: /lit3unit(\d+)/), let n = Int(m.1) {
return .lit3Lesson(n)
}
if let m = page.wholeMatch(of: /lit3ex(\d+)/), let n = Int(m.1) {
return .lit3Ex(n)
}
if let m = page.wholeMatch(of: /lit2unit(\d+)/), let n = Int(m.1) {
return .lit2Lesson(n)
}
if let m = page.wholeMatch(of: /lit2ex(\d+)/), let n = Int(m.1) {
return .lit2Ex(n)
}
if let m = page.wholeMatch(of: /litunit(\d+)/), let n = Int(m.1) {
return .litLesson(n)
}
if let m = page.wholeMatch(of: /litex(\d+)/), let n = Int(m.1) {
return .litEx(n)
}
if let m = page.wholeMatch(of: /on4unit(\d+)/), let n = Int(m.1) {
return .on4Lesson(n)
}

View file

@ -149,6 +149,9 @@ private struct ChoiceEx: View {
if revealed {
if i < meanings.count { Meaning(text: vm.tr(meanings[i]), palette: p, visible: true) }
if i < expls.count { Meaning(text: expls[i], palette: p, visible: true) }
if let expl = qs[i].strOrNull("expl") {
Meaning(text: expl, palette: p, visible: true)
}
}
Spacer().frame(height: 10)
}

View file

@ -625,6 +625,39 @@ struct NetQuizScreen: View {
}
}
struct LitYearQuizScreen: View {
@ObservedObject var vm: AppModel
let year: Int
let id: Int
var body: some View {
let lesson: J? = {
switch year {
case 2: return vm.content.lit2Lesson(id)
case 3: return vm.content.lit3Lesson(id)
case 4: return vm.content.lit4Lesson(id)
default: return vm.content.litLesson(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"))) {
switch year {
case 2: vm.markLit2(id)
case 3: vm.markLit3(id)
case 4: vm.markLit4(id)
default: vm.markLit(id)
}
}
}
}
}
struct OnQuizScreen: View {
@ObservedObject var vm: AppModel
let id: Int

View file

@ -696,8 +696,13 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
+ vm.content.on3.filter { vm.progress.on3Done($0.int("id")) }.count
+ vm.content.on4.filter { vm.progress.on4Done($0.int("id")) }.count
case "czechmap":
sum.totalEx = vm.content.mluvnice.count
sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count
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
+ vm.content.lit2.filter { vm.progress.lit2Done($0.int("id")) }.count
+ vm.content.lit3.filter { vm.progress.lit3Done($0.int("id")) }.count
+ vm.content.lit4.filter { vm.progress.lit4Done($0.int("id")) }.count
sum.totalEx = vm.content.mluvnice.count + litCount
sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count + litDone
default:
break
}
@ -899,6 +904,27 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on4unit\(l.int("id"))", extra: "obcanka")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex\(l.int("id"))")
}
add(vm.tr("Literatura"), vm.tr("search_lesson"), "lityears", extra: "literatura")
add(vm.tr("lit_year1"), vm.tr("search_lesson"), "litmap", extra: "rocnik")
add(vm.tr("lit_year2"), vm.tr("search_lesson"), "lit2map", extra: "rocnik")
add(vm.tr("lit_year3"), vm.tr("search_lesson"), "lit3map", extra: "rocnik")
add(vm.tr("lit_year4"), vm.tr("search_lesson"), "lit4map", extra: "rocnik")
for l in vm.content.lit {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "litunit\(l.int("id"))", extra: "literatura")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "litex\(l.int("id"))")
}
for l in vm.content.lit2 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit2unit\(l.int("id"))", extra: "literatura")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit2ex\(l.int("id"))")
}
for l in vm.content.lit3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit3unit\(l.int("id"))", extra: "literatura")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit3ex\(l.int("id"))")
}
for l in vm.content.lit4 {
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("Mluvnice"), vm.tr("search_lesson"), "mluvnice")
add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
for b in vm.content.books {
@ -1404,6 +1430,47 @@ struct SlidesScreen: View {
}
}
struct LitYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let years: [(String, String, Route)] = [
("lit_year1", "lit_sub", .litMap),
("lit_year2", "lit2_sub", .lit2Map),
("lit_year3", "lit3_sub", .lit3Map),
("lit_year4", "lit4_sub", .lit4Map),
]
MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
NavigationLink(value: years[i].2) {
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
}
.buttonStyle(.plain)
}
.navigationTitle(vm.tr("Literatura"))
.navigationSubtitle(vm.tr("lit_years_sub"))
#else
List {
NavigationLink(value: Route.litMap) {
ListRowLabel(title: vm.tr("lit_year1"), subtitle: vm.tr("lit_sub"))
}
NavigationLink(value: Route.lit2Map) {
ListRowLabel(title: vm.tr("lit_year2"), subtitle: vm.tr("lit2_sub"))
}
NavigationLink(value: Route.lit3Map) {
ListRowLabel(title: vm.tr("lit_year3"), subtitle: vm.tr("lit3_sub"))
}
NavigationLink(value: Route.lit4Map) {
ListRowLabel(title: vm.tr("lit_year4"), subtitle: vm.tr("lit4_sub"))
}
}
.navigationTitle(vm.tr("Literatura"))
.navigationSubtitle(vm.tr("lit_years_sub"))
.appListStyle()
#endif
}
}
struct CzechMapScreen: View {
@ObservedObject var vm: AppModel
@ -1422,19 +1489,23 @@ struct CzechMapScreen: View {
}
.buttonStyle(.plain)
default:
MacLinkCard(title: vm.tr("Literatura"), systemImage: "book.closed", locked: true)
NavigationLink(value: Route.litYears) {
MacLinkCard(title: vm.tr("Literatura"), systemImage: "book.closed")
}
.buttonStyle(.plain)
}
}
.navigationTitle(vm.tr("Český jazyk a literatura"))
.navigationSubtitle(vm.tr("czech_sub"))
#else
List {
Label {
Text(vm.tr("Literatura"))
} icon: {
Image(systemName: "lock.fill")
NavigationLink(value: Route.litYears) {
Label {
Text(vm.tr("Literatura"))
} icon: {
Image(systemName: "book.closed")
}
}
.foregroundStyle(.secondary)
NavigationLink(value: Route.mluvnice) {
Label {
Text(vm.tr("Mluvnice"))
@ -1994,6 +2065,92 @@ struct RouteDestination: View {
On4QuizScreen(vm: vm, id: id)
case .czechMap:
CzechMapScreen(vm: vm)
case .litYears:
LitYearsScreen(vm: vm)
case .litMap:
LessonListScreen(
vm: vm,
title: vm.tr("lit_year1"),
subtitle: vm.tr("lit_sub"),
rows: vm.content.lit.map { l in
let id = l.int("id")
return LessonRow(
id: "l\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.litDone(id),
destination: .litLesson(id)
)
}
)
case .litLesson(let id):
if let l = vm.content.litLesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .litEx(id))
}
case .litEx(let id):
LitYearQuizScreen(vm: vm, year: 1, id: id)
case .lit2Map:
LessonListScreen(
vm: vm,
title: vm.tr("lit_year2"),
subtitle: vm.tr("lit2_sub"),
rows: vm.content.lit2.map { l in
let id = l.int("id")
return LessonRow(
id: "l2\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.lit2Done(id),
destination: .lit2Lesson(id)
)
}
)
case .lit2Lesson(let id):
if let l = vm.content.lit2Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .lit2Ex(id))
}
case .lit2Ex(let id):
LitYearQuizScreen(vm: vm, year: 2, id: id)
case .lit3Map:
LessonListScreen(
vm: vm,
title: vm.tr("lit_year3"),
subtitle: vm.tr("lit3_sub"),
rows: vm.content.lit3.map { l in
let id = l.int("id")
return LessonRow(
id: "l3\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.lit3Done(id),
destination: .lit3Lesson(id)
)
}
)
case .lit3Lesson(let id):
if let l = vm.content.lit3Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .lit3Ex(id))
}
case .lit3Ex(let id):
LitYearQuizScreen(vm: vm, year: 3, id: id)
case .lit4Map:
LessonListScreen(
vm: vm,
title: vm.tr("lit_year4"),
subtitle: vm.tr("lit4_sub"),
rows: vm.content.lit4.map { l in
let id = l.int("id")
return LessonRow(
id: "l4\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.lit4Done(id),
destination: .lit4Lesson(id)
)
}
)
case .lit4Lesson(let id):
if let l = vm.content.lit4Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .lit4Ex(id))
}
case .lit4Ex(let id):
LitYearQuizScreen(vm: vm, year: 4, id: id)
case .mluvnice:
LessonListScreen(
vm: vm,