Fill in years 2–4 of networks and hardware.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-07 21:07:47 +02:00
commit 10980f24b5
27 changed files with 6345 additions and 64 deletions

View file

@ -176,6 +176,11 @@ final class AppModel: ObservableObject {
refresh()
}
func markCourse(_ course: String, _ id: Int) {
progress.markCourse(course, id)
refresh()
}
func markLit(_ id: Int) {
progress.markLit(id)
refresh()

View file

@ -31,6 +31,12 @@ final class Content {
let netTasks: [J]
let netAnswers: [[J]]
let hw: [J]
let net2: [J]
let net3: [J]
let net4: [J]
let hw2: [J]
let hw3: [J]
let hw4: [J]
let on: [J]
let on2: [J]
let on3: [J]
@ -89,6 +95,12 @@ final class Content {
netAnswers = []
}
hw = raw.arr("hw")
net2 = raw.arr("net2")
net3 = raw.arr("net3")
net4 = raw.arr("net4")
hw2 = raw.arr("hw2")
hw3 = raw.arr("hw3")
hw4 = raw.arr("hw4")
on = raw.arr("on")
on2 = raw.arr("on2")
on3 = raw.arr("on3")
@ -124,6 +136,20 @@ final class Content {
func germanUnit(_ id: Int) -> J? { german.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 itLessons(_ course: String) -> [J] {
switch course {
case "net2": return net2
case "net3": return net3
case "net4": return net4
case "hw2": return hw2
case "hw3": return hw3
case "hw4": return hw4
default: return []
}
}
func itLesson(_ course: String, _ id: Int) -> J? {
itLessons(course).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 } }

View file

@ -50,6 +50,8 @@ final class ProgressStore {
func markEn(_ year: Int, _ id: Int) { defaults.set(true, forKey: "en.\(year).\(id)") }
func deDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "de.\(year).\(id)") }
func markDe(_ year: Int, _ id: Int) { defaults.set(true, forKey: "de.\(year).\(id)") }
func courseDone(_ course: String, _ id: Int) -> Bool { defaults.bool(forKey: "\(course).\(id)") }
func markCourse(_ course: String, _ id: Int) { defaults.set(true, forKey: "\(course).\(id)") }
func litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") }
func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") }
@ -126,6 +128,16 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1
}
}
for course in ["net2", "net3", "net4", "hw2", "hw3", "hw4"] {
for l in content.itLessons(course) {
sum.totalEx += 1
sum.openUnits += 1
if p.courseDone(course, l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
for l in content.on {
sum.totalEx += 1
sum.openUnits += 1

View file

@ -23,10 +23,28 @@ enum Route: Hashable {
case netMap
case netLesson(Int)
case netEx(Int)
case net2Map
case net2Lesson(Int)
case net2Ex(Int)
case net3Map
case net3Lesson(Int)
case net3Ex(Int)
case net4Map
case net4Lesson(Int)
case net4Ex(Int)
case hwYears
case hwMap
case hwLesson(Int)
case hwEx(Int)
case hw2Map
case hw2Lesson(Int)
case hw2Ex(Int)
case hw3Map
case hw3Lesson(Int)
case hw3Ex(Int)
case hw4Map
case hw4Lesson(Int)
case hw4Ex(Int)
case onYears
case onMap
case onLesson(Int)
@ -118,10 +136,28 @@ enum Route: Hashable {
case .netMap: return "netmap"
case .netLesson(let id): return "netunit\(id)"
case .netEx(let id): return "netex\(id)"
case .net2Map: return "net2map"
case .net2Lesson(let id): return "net2unit\(id)"
case .net2Ex(let id): return "net2ex\(id)"
case .net3Map: return "net3map"
case .net3Lesson(let id): return "net3unit\(id)"
case .net3Ex(let id): return "net3ex\(id)"
case .net4Map: return "net4map"
case .net4Lesson(let id): return "net4unit\(id)"
case .net4Ex(let id): return "net4ex\(id)"
case .hwYears: return "hwyears"
case .hwMap: return "hwmap"
case .hwLesson(let id): return "hwunit\(id)"
case .hwEx(let id): return "hwex\(id)"
case .hw2Map: return "hw2map"
case .hw2Lesson(let id): return "hw2unit\(id)"
case .hw2Ex(let id): return "hw2ex\(id)"
case .hw3Map: return "hw3map"
case .hw3Lesson(let id): return "hw3unit\(id)"
case .hw3Ex(let id): return "hw3ex\(id)"
case .hw4Map: return "hw4map"
case .hw4Lesson(let id): return "hw4unit\(id)"
case .hw4Ex(let id): return "hw4ex\(id)"
case .onYears: return "onyears"
case .onMap: return "onmap"
case .onLesson(let id): return "onunit\(id)"
@ -236,6 +272,24 @@ enum Route: Hashable {
return [.netYears, .netMap, .netLesson(id)]
case .netEx(let id):
return [.netYears, .netMap, .netLesson(id), .netEx(id)]
case .net2Map:
return [.netYears, .net2Map]
case .net2Lesson(let id):
return [.netYears, .net2Map, .net2Lesson(id)]
case .net2Ex(let id):
return [.netYears, .net2Map, .net2Lesson(id), .net2Ex(id)]
case .net3Map:
return [.netYears, .net3Map]
case .net3Lesson(let id):
return [.netYears, .net3Map, .net3Lesson(id)]
case .net3Ex(let id):
return [.netYears, .net3Map, .net3Lesson(id), .net3Ex(id)]
case .net4Map:
return [.netYears, .net4Map]
case .net4Lesson(let id):
return [.netYears, .net4Map, .net4Lesson(id)]
case .net4Ex(let id):
return [.netYears, .net4Map, .net4Lesson(id), .net4Ex(id)]
case .hwYears:
return [.hwYears]
case .hwMap:
@ -244,6 +298,24 @@ enum Route: Hashable {
return [.hwYears, .hwMap, .hwLesson(id)]
case .hwEx(let id):
return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)]
case .hw2Map:
return [.hwYears, .hw2Map]
case .hw2Lesson(let id):
return [.hwYears, .hw2Map, .hw2Lesson(id)]
case .hw2Ex(let id):
return [.hwYears, .hw2Map, .hw2Lesson(id), .hw2Ex(id)]
case .hw3Map:
return [.hwYears, .hw3Map]
case .hw3Lesson(let id):
return [.hwYears, .hw3Map, .hw3Lesson(id)]
case .hw3Ex(let id):
return [.hwYears, .hw3Map, .hw3Lesson(id), .hw3Ex(id)]
case .hw4Map:
return [.hwYears, .hw4Map]
case .hw4Lesson(let id):
return [.hwYears, .hw4Map, .hw4Lesson(id)]
case .hw4Ex(let id):
return [.hwYears, .hw4Map, .hw4Lesson(id), .hw4Ex(id)]
case .onYears:
return [.onYears]
case .onMap:
@ -403,6 +475,12 @@ func routeFromPage(_ page: String) -> Route? {
case "dehome": return .deHome
case "netyears": return .netYears
case "netmap": return .netMap
case "net2map": return .net2Map
case "net3map": return .net3Map
case "net4map": return .net4Map
case "hw2map": return .hw2Map
case "hw3map": return .hw3Map
case "hw4map": return .hw4Map
case "hwyears": return .hwYears
case "hwmap": return .hwMap
case "onyears": return .onYears
@ -572,6 +650,34 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /de(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
return .deEx(y, n)
}
if let m = page.wholeMatch(of: /net([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .net3Lesson(n)
case 4: return .net4Lesson(n)
default: return .net2Lesson(n)
}
}
if let m = page.wholeMatch(of: /net([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .net3Ex(n)
case 4: return .net4Ex(n)
default: return .net2Ex(n)
}
}
if let m = page.wholeMatch(of: /hw([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .hw3Lesson(n)
case 4: return .hw4Lesson(n)
default: return .hw2Lesson(n)
}
}
if let m = page.wholeMatch(of: /hw([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
switch y {
case 3: return .hw3Ex(n)
case 4: return .hw4Ex(n)
default: return .hw2Ex(n)
}
}
if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) {
return .enMap(y)
}

View file

@ -896,6 +896,62 @@ private struct MathGrid: View {
}
}
struct ItYearList: View {
@ObservedObject var vm: AppModel
let course: String
let titleKey: String
let subKey: String
let lesson: (Int) -> Route
var body: some View {
LessonListScreen(
vm: vm,
title: vm.tr(titleKey),
subtitle: vm.tr(subKey),
rows: vm.content.itLessons(course).map { l in
let id = l.int("id")
return LessonRow(
id: "\(course)\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.courseDone(course, id),
destination: lesson(id)
)
}
)
}
}
struct ItSlides: View {
@ObservedObject var vm: AppModel
let course: String
let id: Int
let next: Route
var body: some View {
if let l = vm.content.itLesson(course, id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: next)
}
}
}
struct ChoiceLessonScreen: View {
@ObservedObject var vm: AppModel
let lesson: J?
let onDone: () -> Void
var body: some View {
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")), onDone: onDone)
}
}
}
struct FyzQuizScreen: View {
@ObservedObject var vm: AppModel
let year: Int

View file

@ -674,10 +674,18 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
}
case "netyears":
sum.totalEx = vm.content.netLessons.count
+ vm.content.net2.count + vm.content.net3.count + vm.content.net4.count
sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count
+ ["net2", "net3", "net4"].reduce(0) { acc, course in
acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count
}
case "hwyears", "hwmap":
sum.totalEx = vm.content.hw.count
+ vm.content.hw2.count + vm.content.hw3.count + vm.content.hw4.count
sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count
+ ["hw2", "hw3", "hw4"].reduce(0) { acc, course in
acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count
}
case "enyears", "en1map", "en2map", "en3map", "en4map":
sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count }
sum.doneEx = (1...4).reduce(0) { acc, year in
@ -887,14 +895,23 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit\(l.int("id"))", extra: "site")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex\(l.int("id"))")
}
for course in ["net2", "net3", "net4"] {
add(vm.tr("net_year\(course.dropFirst(3))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik site")
for l in vm.content.itLessons(course) {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "site")
}
}
add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra: "rocnik")
add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik")
add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik")
add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik")
for l in vm.content.hw {
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"))")
}
for course in ["hw2", "hw3", "hw4"] {
add(vm.tr("hw_year\(course.dropFirst(2))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik hardware")
for l in vm.content.itLessons(course) {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "hardware")
}
}
add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik")
add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik")
add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
@ -1178,16 +1195,17 @@ struct NetYearsScreen: View {
var body: some View {
#if os(macOS)
let keys = ["net_year1", "net_year2", "net_year3", "net_year4"]
MacFillGrid(count: keys.count, minWidth: 260, minHeight: 180) { i in
if i == 0 {
NavigationLink(value: Route.netMap) {
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("net_sub"), badge: "\(i + 1).")
}
.buttonStyle(.plain)
} else {
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("net_year_locked_sub"), badge: "\(i + 1).", locked: true)
let years: [(String, String, Route)] = [
("net_year1", "net_sub", .netMap),
("net_year2", "net2_sub", .net2Map),
("net_year3", "net3_sub", .net3Map),
("net_year4", "net4_sub", .net4Map),
]
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("net_years_title"))
.navigationSubtitle(vm.tr("net_years_sub"))
@ -1196,13 +1214,14 @@ struct NetYearsScreen: View {
NavigationLink(value: Route.netMap) {
ListRowLabel(title: vm.tr("net_year1"), subtitle: vm.tr("net_sub"))
}
ForEach(["net_year2", "net_year3", "net_year4"], id: \.self) { key in
Label {
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("net_year_locked_sub"))
} icon: {
Image(systemName: "lock.fill")
}
.foregroundStyle(.secondary)
NavigationLink(value: Route.net2Map) {
ListRowLabel(title: vm.tr("net_year2"), subtitle: vm.tr("net2_sub"))
}
NavigationLink(value: Route.net3Map) {
ListRowLabel(title: vm.tr("net_year3"), subtitle: vm.tr("net3_sub"))
}
NavigationLink(value: Route.net4Map) {
ListRowLabel(title: vm.tr("net_year4"), subtitle: vm.tr("net4_sub"))
}
}
.navigationTitle(vm.tr("net_years_title"))
@ -1217,16 +1236,17 @@ struct HwYearsScreen: View {
var body: some View {
#if os(macOS)
let keys = ["hw_year1", "hw_year2", "hw_year3", "hw_year4"]
MacFillGrid(count: keys.count, minWidth: 260, minHeight: 180) { i in
if i == 0 {
NavigationLink(value: Route.hwMap) {
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("hw_sub"), badge: "\(i + 1).")
}
.buttonStyle(.plain)
} else {
MacLinkCard(title: vm.tr(keys[i]), subtitle: vm.tr("hw_year_locked_sub"), badge: "\(i + 1).", locked: true)
let years: [(String, String, Route)] = [
("hw_year1", "hw_sub", .hwMap),
("hw_year2", "hw2_sub", .hw2Map),
("hw_year3", "hw3_sub", .hw3Map),
("hw_year4", "hw4_sub", .hw4Map),
]
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("Technické vybavení"))
.navigationSubtitle(vm.tr("hw_years_sub"))
@ -1235,13 +1255,14 @@ struct HwYearsScreen: View {
NavigationLink(value: Route.hwMap) {
ListRowLabel(title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub"))
}
ForEach(["hw_year2", "hw_year3", "hw_year4"], id: \.self) { key in
Label {
ListRowLabel(title: vm.tr(key), subtitle: vm.tr("hw_year_locked_sub"))
} icon: {
Image(systemName: "lock.fill")
}
.foregroundStyle(.secondary)
NavigationLink(value: Route.hw2Map) {
ListRowLabel(title: vm.tr("hw_year2"), subtitle: vm.tr("hw2_sub"))
}
NavigationLink(value: Route.hw3Map) {
ListRowLabel(title: vm.tr("hw_year3"), subtitle: vm.tr("hw3_sub"))
}
NavigationLink(value: Route.hw4Map) {
ListRowLabel(title: vm.tr("hw_year4"), subtitle: vm.tr("hw4_sub"))
}
}
.navigationTitle(vm.tr("Technické vybavení"))
@ -2256,6 +2277,24 @@ struct RouteDestination: View {
}
case .netEx(let id):
NetQuizScreen(vm: vm, id: id)
case .net2Map:
ItYearList(vm: vm, course: "net2", titleKey: "net_year2", subKey: "net2_sub") { .net2Lesson($0) }
case .net2Lesson(let id):
ItSlides(vm: vm, course: "net2", id: id, next: .net2Ex(id))
case .net2Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net2", id)) { vm.markCourse("net2", id) }
case .net3Map:
ItYearList(vm: vm, course: "net3", titleKey: "net_year3", subKey: "net3_sub") { .net3Lesson($0) }
case .net3Lesson(let id):
ItSlides(vm: vm, course: "net3", id: id, next: .net3Ex(id))
case .net3Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net3", id)) { vm.markCourse("net3", id) }
case .net4Map:
ItYearList(vm: vm, course: "net4", titleKey: "net_year4", subKey: "net4_sub") { .net4Lesson($0) }
case .net4Lesson(let id):
ItSlides(vm: vm, course: "net4", id: id, next: .net4Ex(id))
case .net4Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net4", id)) { vm.markCourse("net4", id) }
case .hwYears:
HwYearsScreen(vm: vm)
case .hwMap:
@ -2279,6 +2318,24 @@ struct RouteDestination: View {
}
case .hwEx(let id):
HwQuizScreen(vm: vm, id: id)
case .hw2Map:
ItYearList(vm: vm, course: "hw2", titleKey: "hw_year2", subKey: "hw2_sub") { .hw2Lesson($0) }
case .hw2Lesson(let id):
ItSlides(vm: vm, course: "hw2", id: id, next: .hw2Ex(id))
case .hw2Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw2", id)) { vm.markCourse("hw2", id) }
case .hw3Map:
ItYearList(vm: vm, course: "hw3", titleKey: "hw_year3", subKey: "hw3_sub") { .hw3Lesson($0) }
case .hw3Lesson(let id):
ItSlides(vm: vm, course: "hw3", id: id, next: .hw3Ex(id))
case .hw3Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw3", id)) { vm.markCourse("hw3", id) }
case .hw4Map:
ItYearList(vm: vm, course: "hw4", titleKey: "hw_year4", subKey: "hw4_sub") { .hw4Lesson($0) }
case .hw4Lesson(let id):
ItSlides(vm: vm, course: "hw4", id: id, next: .hw4Ex(id))
case .hw4Ex(let id):
ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw4", id)) { vm.markCourse("hw4", id) }
case .onYears:
OnYearsScreen(vm: vm)
case .enYears: