Add physics lessons across four secondary-school years.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-06 10:55:27 +02:00
commit 524269edd7
32 changed files with 4152 additions and 13 deletions

View file

@ -178,6 +178,26 @@ final class AppModel: ObservableObject {
refresh()
}
func markFyz(_ id: Int) {
progress.markFyz(id)
refresh()
}
func markFyz2(_ id: Int) {
progress.markFyz2(id)
refresh()
}
func markFyz3(_ id: Int) {
progress.markFyz3(id)
refresh()
}
func markFyz4(_ id: Int) {
progress.markFyz4(id)
refresh()
}
func markMluv(_ n: Int) {
progress.markMluv(n)
refresh()

View file

@ -41,6 +41,10 @@ final class Content {
let lit4: [J]
let chem: [J]
let bio: [J]
let fyz: [J]
let fyz2: [J]
let fyz3: [J]
let fyz4: [J]
let mluvnice: [J]
let books: [J]
let changelog: [J]
@ -82,6 +86,10 @@ final class Content {
lit4 = raw.arr("lit4")
chem = raw.arr("chem")
bio = raw.arr("bio")
fyz = raw.arr("fyz")
fyz2 = raw.arr("fyz2")
fyz3 = raw.arr("fyz3")
fyz4 = raw.arr("fyz4")
mluvnice = raw.arr("mluvnice")
books = raw.arr("books")
changelog = raw.arr("changelog")
@ -100,6 +108,10 @@ final class Content {
func lit4Lesson(_ id: Int) -> J? { lit4.first { $0.int("id") == id } }
func chemLesson(_ id: Int) -> J? { chem.first { $0.int("id") == id } }
func bioLesson(_ id: Int) -> J? { bio.first { $0.int("id") == id } }
func fyzLesson(_ id: Int) -> J? { fyz.first { $0.int("id") == id } }
func fyz2Lesson(_ id: Int) -> J? { fyz2.first { $0.int("id") == id } }
func fyz3Lesson(_ id: Int) -> J? { fyz3.first { $0.int("id") == id } }
func fyz4Lesson(_ id: Int) -> J? { fyz4.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

@ -59,6 +59,14 @@ final class ProgressStore {
func markChem(_ id: Int) { defaults.set(true, forKey: "chem.\(id)") }
func bioDone(_ id: Int) -> Bool { defaults.bool(forKey: "bio.\(id)") }
func markBio(_ id: Int) { defaults.set(true, forKey: "bio.\(id)") }
func fyzDone(_ id: Int) -> Bool { defaults.bool(forKey: "fyz.\(id)") }
func markFyz(_ id: Int) { defaults.set(true, forKey: "fyz.\(id)") }
func fyz2Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz2.\(id)") }
func markFyz2(_ id: Int) { defaults.set(true, forKey: "fyz2.\(id)") }
func fyz3Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz3.\(id)") }
func markFyz3(_ id: Int) { defaults.set(true, forKey: "fyz3.\(id)") }
func fyz4Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz4.\(id)") }
func markFyz4(_ id: Int) { defaults.set(true, forKey: "fyz4.\(id)") }
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
@ -183,6 +191,38 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1
}
}
for l in content.fyz {
sum.totalEx += 1
sum.openUnits += 1
if p.fyzDone(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.fyz2 {
sum.totalEx += 1
sum.openUnits += 1
if p.fyz2Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.fyz3 {
sum.totalEx += 1
sum.openUnits += 1
if p.fyz3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.fyz4 {
sum.totalEx += 1
sum.openUnits += 1
if p.fyz4Done(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

@ -56,6 +56,19 @@ enum Route: Hashable {
case bioMap
case bioLesson(Int)
case bioEx(Int)
case fyzYears
case fyzMap
case fyzLesson(Int)
case fyzEx(Int)
case fyz2Map
case fyz2Lesson(Int)
case fyz2Ex(Int)
case fyz3Map
case fyz3Lesson(Int)
case fyz3Ex(Int)
case fyz4Map
case fyz4Lesson(Int)
case fyz4Ex(Int)
case czechMap
case mluvnice
case mluvEx(Int)
@ -114,6 +127,19 @@ enum Route: Hashable {
case .bioMap: return "biomap"
case .bioLesson(let id): return "biounit\(id)"
case .bioEx(let id): return "bioex\(id)"
case .fyzYears: return "fyzyears"
case .fyzMap: return "fyzmap"
case .fyzLesson(let id): return "fyzunit\(id)"
case .fyzEx(let id): return "fyzex\(id)"
case .fyz2Map: return "fyz2map"
case .fyz2Lesson(let id): return "fyz2unit\(id)"
case .fyz2Ex(let id): return "fyz2ex\(id)"
case .fyz3Map: return "fyz3map"
case .fyz3Lesson(let id): return "fyz3unit\(id)"
case .fyz3Ex(let id): return "fyz3ex\(id)"
case .fyz4Map: return "fyz4map"
case .fyz4Lesson(let id): return "fyz4unit\(id)"
case .fyz4Ex(let id): return "fyz4ex\(id)"
case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)"
@ -228,6 +254,32 @@ enum Route: Hashable {
return [.sciMap, .bioMap, .bioLesson(id)]
case .bioEx(let id):
return [.sciMap, .bioMap, .bioLesson(id), .bioEx(id)]
case .fyzYears:
return [.fyzYears]
case .fyzMap:
return [.fyzYears, .fyzMap]
case .fyzLesson(let id):
return [.fyzYears, .fyzMap, .fyzLesson(id)]
case .fyzEx(let id):
return [.fyzYears, .fyzMap, .fyzLesson(id), .fyzEx(id)]
case .fyz2Map:
return [.fyzYears, .fyz2Map]
case .fyz2Lesson(let id):
return [.fyzYears, .fyz2Map, .fyz2Lesson(id)]
case .fyz2Ex(let id):
return [.fyzYears, .fyz2Map, .fyz2Lesson(id), .fyz2Ex(id)]
case .fyz3Map:
return [.fyzYears, .fyz3Map]
case .fyz3Lesson(let id):
return [.fyzYears, .fyz3Map, .fyz3Lesson(id)]
case .fyz3Ex(let id):
return [.fyzYears, .fyz3Map, .fyz3Lesson(id), .fyz3Ex(id)]
case .fyz4Map:
return [.fyzYears, .fyz4Map]
case .fyz4Lesson(let id):
return [.fyzYears, .fyz4Map, .fyz4Lesson(id)]
case .fyz4Ex(let id):
return [.fyzYears, .fyz4Map, .fyz4Lesson(id), .fyz4Ex(id)]
case .czechMap:
return [.czechMap]
case .mluvnice:
@ -269,6 +321,11 @@ func routeFromPage(_ page: String) -> Route? {
case "scimap": return .sciMap
case "chemmap": return .chemMap
case "biomap": return .bioMap
case "fyzyears": return .fyzYears
case "fyzmap": return .fyzMap
case "fyz2map": return .fyz2Map
case "fyz3map": return .fyz3Map
case "fyz4map": return .fyz4Map
case "czechmap": return .czechMap
case "mluvnice": return .mluvnice
case "readinglist": return .readingList
@ -324,6 +381,30 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /bioex(\d+)/), let n = Int(m.1) {
return .bioEx(n)
}
if let m = page.wholeMatch(of: /fyz4unit(\d+)/), let n = Int(m.1) {
return .fyz4Lesson(n)
}
if let m = page.wholeMatch(of: /fyz4ex(\d+)/), let n = Int(m.1) {
return .fyz4Ex(n)
}
if let m = page.wholeMatch(of: /fyz3unit(\d+)/), let n = Int(m.1) {
return .fyz3Lesson(n)
}
if let m = page.wholeMatch(of: /fyz3ex(\d+)/), let n = Int(m.1) {
return .fyz3Ex(n)
}
if let m = page.wholeMatch(of: /fyz2unit(\d+)/), let n = Int(m.1) {
return .fyz2Lesson(n)
}
if let m = page.wholeMatch(of: /fyz2ex(\d+)/), let n = Int(m.1) {
return .fyz2Ex(n)
}
if let m = page.wholeMatch(of: /fyzunit(\d+)/), let n = Int(m.1) {
return .fyzLesson(n)
}
if let m = page.wholeMatch(of: /fyzex(\d+)/), let n = Int(m.1) {
return .fyzEx(n)
}
if let m = page.wholeMatch(of: /lit4unit(\d+)/), let n = Int(m.1) {
return .lit4Lesson(n)
}

View file

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

View file

@ -646,6 +646,39 @@ struct SciQuizScreen: View {
}
}
struct FyzQuizScreen: 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.fyz2Lesson(id)
case 3: return vm.content.fyz3Lesson(id)
case 4: return vm.content.fyz4Lesson(id)
default: return vm.content.fyzLesson(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.markFyz2(id)
case 3: vm.markFyz3(id)
case 4: vm.markFyz4(id)
default: vm.markFyz(id)
}
}
}
}
}
struct LitYearQuizScreen: View {
@ObservedObject var vm: AppModel
let year: Int

View file

@ -699,6 +699,12 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
sum.totalEx = vm.content.chem.count + vm.content.bio.count
sum.doneEx = vm.content.chem.filter { vm.progress.chemDone($0.int("id")) }.count
+ vm.content.bio.filter { vm.progress.bioDone($0.int("id")) }.count
case "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map":
sum.totalEx = vm.content.fyz.count + vm.content.fyz2.count + vm.content.fyz3.count + vm.content.fyz4.count
sum.doneEx = vm.content.fyz.filter { vm.progress.fyzDone($0.int("id")) }.count
+ vm.content.fyz2.filter { vm.progress.fyz2Done($0.int("id")) }.count
+ vm.content.fyz3.filter { vm.progress.fyz3Done($0.int("id")) }.count
+ vm.content.fyz4.filter { vm.progress.fyz4Done($0.int("id")) }.count
case "czechmap":
let litCount = vm.content.lit.count + vm.content.lit2.count + vm.content.lit3.count + vm.content.lit4.count
let litDone = vm.content.lit.filter { vm.progress.litDone($0.int("id")) }.count
@ -929,6 +935,27 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit\(l.int("id"))", extra: "literatura")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex\(l.int("id"))")
}
add(vm.tr("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra: "fyzika")
add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra: "rocnik")
add(vm.tr("fyz_year2"), vm.tr("search_lesson"), "fyz2map", extra: "rocnik")
add(vm.tr("fyz_year3"), vm.tr("search_lesson"), "fyz3map", extra: "rocnik")
add(vm.tr("fyz_year4"), vm.tr("search_lesson"), "fyz4map", extra: "rocnik")
for l in vm.content.fyz {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyzunit\(l.int("id"))", extra: "fyzika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyzex\(l.int("id"))")
}
for l in vm.content.fyz2 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz2unit\(l.int("id"))", extra: "fyzika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz2ex\(l.int("id"))")
}
for l in vm.content.fyz3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz3unit\(l.int("id"))", extra: "fyzika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz3ex\(l.int("id"))")
}
for l in vm.content.fyz4 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz4unit\(l.int("id"))", extra: "fyzika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz4ex\(l.int("id"))")
}
add(vm.tr("Chemie"), vm.tr("search_lesson"), "chemmap", extra: "chemie")
add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra: "biologie")
for l in vm.content.chem {
@ -1485,6 +1512,47 @@ struct LitYearsScreen: View {
}
}
struct FyzYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let years: [(String, String, Route)] = [
("fyz_year1", "fyz_sub", .fyzMap),
("fyz_year2", "fyz2_sub", .fyz2Map),
("fyz_year3", "fyz3_sub", .fyz3Map),
("fyz_year4", "fyz4_sub", .fyz4Map),
]
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("Fyzika"))
.navigationSubtitle(vm.tr("fyz_years_sub"))
#else
List {
NavigationLink(value: Route.fyzMap) {
ListRowLabel(title: vm.tr("fyz_year1"), subtitle: vm.tr("fyz_sub"))
}
NavigationLink(value: Route.fyz2Map) {
ListRowLabel(title: vm.tr("fyz_year2"), subtitle: vm.tr("fyz2_sub"))
}
NavigationLink(value: Route.fyz3Map) {
ListRowLabel(title: vm.tr("fyz_year3"), subtitle: vm.tr("fyz3_sub"))
}
NavigationLink(value: Route.fyz4Map) {
ListRowLabel(title: vm.tr("fyz_year4"), subtitle: vm.tr("fyz4_sub"))
}
}
.navigationTitle(vm.tr("Fyzika"))
.navigationSubtitle(vm.tr("fyz_years_sub"))
.appListStyle()
#endif
}
}
struct SciMapScreen: View {
@ObservedObject var vm: AppModel
@ -2253,6 +2321,92 @@ struct RouteDestination: View {
}
case .bioEx(let id):
SciQuizScreen(vm: vm, area: "bio", id: id)
case .fyzYears:
FyzYearsScreen(vm: vm)
case .fyzMap:
LessonListScreen(
vm: vm,
title: vm.tr("fyz_year1"),
subtitle: vm.tr("fyz_sub"),
rows: vm.content.fyz.map { l in
let id = l.int("id")
return LessonRow(
id: "f\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.fyzDone(id),
destination: .fyzLesson(id)
)
}
)
case .fyzLesson(let id):
if let l = vm.content.fyzLesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyzEx(id))
}
case .fyzEx(let id):
FyzQuizScreen(vm: vm, year: 1, id: id)
case .fyz2Map:
LessonListScreen(
vm: vm,
title: vm.tr("fyz_year2"),
subtitle: vm.tr("fyz2_sub"),
rows: vm.content.fyz2.map { l in
let id = l.int("id")
return LessonRow(
id: "f2\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.fyz2Done(id),
destination: .fyz2Lesson(id)
)
}
)
case .fyz2Lesson(let id):
if let l = vm.content.fyz2Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyz2Ex(id))
}
case .fyz2Ex(let id):
FyzQuizScreen(vm: vm, year: 2, id: id)
case .fyz3Map:
LessonListScreen(
vm: vm,
title: vm.tr("fyz_year3"),
subtitle: vm.tr("fyz3_sub"),
rows: vm.content.fyz3.map { l in
let id = l.int("id")
return LessonRow(
id: "f3\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.fyz3Done(id),
destination: .fyz3Lesson(id)
)
}
)
case .fyz3Lesson(let id):
if let l = vm.content.fyz3Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyz3Ex(id))
}
case .fyz3Ex(let id):
FyzQuizScreen(vm: vm, year: 3, id: id)
case .fyz4Map:
LessonListScreen(
vm: vm,
title: vm.tr("fyz_year4"),
subtitle: vm.tr("fyz4_sub"),
rows: vm.content.fyz4.map { l in
let id = l.int("id")
return LessonRow(
id: "f4\(id)",
title: vm.tr(l.str("titleKey")),
done: vm.progress.fyz4Done(id),
destination: .fyz4Lesson(id)
)
}
)
case .fyz4Lesson(let id):
if let l = vm.content.fyz4Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyz4Ex(id))
}
case .fyz4Ex(let id):
FyzQuizScreen(vm: vm, year: 4, id: id)
case .mluvnice:
LessonListScreen(
vm: vm,