mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 07:04:35 +00:00
Add four years of English from the gymnasium syllabus.
Each year mixes listening, reading, writing, gap-fill, quizzes, and a year test so the subject is usable through the maturita. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c561c0acb5
commit
bb93ec76ec
32 changed files with 6607 additions and 14 deletions
|
|
@ -166,6 +166,11 @@ final class AppModel: ObservableObject {
|
|||
refresh()
|
||||
}
|
||||
|
||||
func markEn(_ year: Int, _ id: Int) {
|
||||
progress.markEn(year, id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markLit(_ id: Int) {
|
||||
progress.markLit(id)
|
||||
refresh()
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ final class Content {
|
|||
let on2: [J]
|
||||
let on3: [J]
|
||||
let on4: [J]
|
||||
let en: [J]
|
||||
let en2: [J]
|
||||
let en3: [J]
|
||||
let en4: [J]
|
||||
let lit: [J]
|
||||
let lit2: [J]
|
||||
let lit3: [J]
|
||||
|
|
@ -85,6 +89,10 @@ final class Content {
|
|||
on2 = raw.arr("on2")
|
||||
on3 = raw.arr("on3")
|
||||
on4 = raw.arr("on4")
|
||||
en = raw.arr("en")
|
||||
en2 = raw.arr("en2")
|
||||
en3 = raw.arr("en3")
|
||||
en4 = raw.arr("en4")
|
||||
lit = raw.arr("lit")
|
||||
lit2 = raw.arr("lit2")
|
||||
lit3 = raw.arr("lit3")
|
||||
|
|
@ -112,6 +120,15 @@ 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 enYear(_ year: Int) -> [J] {
|
||||
switch year {
|
||||
case 2: return en2
|
||||
case 3: return en3
|
||||
case 4: return en4
|
||||
default: return en
|
||||
}
|
||||
}
|
||||
func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).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 } }
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ final class ProgressStore {
|
|||
func on4Done(_ id: Int) -> Bool { defaults.bool(forKey: "on4.\(id)") }
|
||||
func markOn4(_ id: Int) { defaults.set(true, forKey: "on4.\(id)") }
|
||||
|
||||
func enDone(_ year: Int, _ id: Int) -> Bool { defaults.bool(forKey: "en.\(year).\(id)") }
|
||||
func markEn(_ year: Int, _ id: Int) { defaults.set(true, forKey: "en.\(year).\(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)") }
|
||||
|
|
@ -153,6 +156,16 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
|
|||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for year in 1...4 {
|
||||
for l in content.enYear(year) {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.enDone(year, l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
for l in content.lit {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ enum Route: Hashable {
|
|||
case on4Map
|
||||
case on4Lesson(Int)
|
||||
case on4Ex(Int)
|
||||
case enYears
|
||||
case enMap(Int)
|
||||
case enLesson(Int, Int)
|
||||
case enEx(Int, Int)
|
||||
case litYears
|
||||
case litMap
|
||||
case litLesson(Int)
|
||||
|
|
@ -123,6 +127,10 @@ enum Route: Hashable {
|
|||
case .on4Map: return "on4map"
|
||||
case .on4Lesson(let id): return "on4unit\(id)"
|
||||
case .on4Ex(let id): return "on4ex\(id)"
|
||||
case .enYears: return "enyears"
|
||||
case .enMap(let year): return "en\(year)map"
|
||||
case .enLesson(let year, let id): return "en\(year)unit\(id)"
|
||||
case .enEx(let year, let id): return "en\(year)ex\(id)"
|
||||
case .litYears: return "lityears"
|
||||
case .litMap: return "litmap"
|
||||
case .litLesson(let id): return "litunit\(id)"
|
||||
|
|
@ -246,6 +254,14 @@ enum Route: Hashable {
|
|||
return [.onYears, .on4Map, .on4Lesson(id)]
|
||||
case .on4Ex(let id):
|
||||
return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)]
|
||||
case .enYears:
|
||||
return [.enYears]
|
||||
case .enMap(let year):
|
||||
return [.enYears, .enMap(year)]
|
||||
case .enLesson(let year, let id):
|
||||
return [.enYears, .enMap(year), .enLesson(year, id)]
|
||||
case .enEx(let year, let id):
|
||||
return [.enYears, .enMap(year), .enLesson(year, id), .enEx(year, id)]
|
||||
case .litYears:
|
||||
return [.czechMap, .litYears]
|
||||
case .litMap:
|
||||
|
|
@ -377,6 +393,7 @@ func routeFromPage(_ page: String) -> Route? {
|
|||
case "on2map": return .on2Map
|
||||
case "on3map": return .on3Map
|
||||
case "on4map": return .on4Map
|
||||
case "enyears": return .enYears
|
||||
case "lityears": return .litYears
|
||||
case "litmap": return .litMap
|
||||
case "lit2map": return .lit2Map
|
||||
|
|
@ -529,6 +546,15 @@ func routeFromPage(_ page: String) -> Route? {
|
|||
if let m = page.wholeMatch(of: /litex(\d+)/), let n = Int(m.1) {
|
||||
return .litEx(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) {
|
||||
return .enMap(y)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /en(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
|
||||
return .enLesson(y, n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /en(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
|
||||
return .enEx(y, n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /on4unit(\d+)/), let n = Int(m.1) {
|
||||
return .on4Lesson(n)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -625,6 +625,7 @@ struct SubjectIcon: View {
|
|||
switch icon {
|
||||
case "de": GermanFlag(size: size)
|
||||
case "cz": CzechFlag(size: size)
|
||||
case "uk": UkFlag(size: size)
|
||||
default:
|
||||
Image(systemName: subjectSymbol(icon))
|
||||
.font(.system(size: size * 0.72, weight: .semibold))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
struct DialogEx: View {
|
||||
|
|
@ -1345,3 +1346,188 @@ struct PlotScreen: View {
|
|||
}
|
||||
}
|
||||
|
||||
private final class EnSpeaker: ObservableObject {
|
||||
private let synth = AVSpeechSynthesizer()
|
||||
|
||||
func speak(_ text: String) {
|
||||
if synth.isSpeaking { synth.stopSpeaking(at: .immediate) }
|
||||
let utterance = AVSpeechUtterance(string: text)
|
||||
utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
|
||||
?? AVSpeechSynthesisVoice(language: "en-US")
|
||||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.9
|
||||
synth.speak(utterance)
|
||||
}
|
||||
}
|
||||
|
||||
struct EngExScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
let year: Int
|
||||
let id: Int
|
||||
@StateObject private var speaker: EnSpeaker
|
||||
@State private var readPicks: [Int]
|
||||
@State private var listenPicks: [Int]
|
||||
@State private var quizPicks: [Int]
|
||||
@State private var gaps: [String]
|
||||
@State private var essay = ""
|
||||
@State private var played = false
|
||||
@State private var showScript = false
|
||||
@State private var revealed = false
|
||||
@State private var showModel = false
|
||||
@State private var fb = ("", "")
|
||||
|
||||
init(vm: AppModel, year: Int, id: Int) {
|
||||
self.vm = vm
|
||||
self.year = year
|
||||
self.id = id
|
||||
let lesson = vm.content.enLesson(year, id)
|
||||
_readPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("readQuiz").count ?? 0))
|
||||
_listenPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("listenQuiz").count ?? 0))
|
||||
_quizPicks = State(initialValue: Array(repeating: -1, count: lesson?.arr("quiz").count ?? 0))
|
||||
_gaps = State(initialValue: Array(repeating: "", count: lesson?.arr("gaps").count ?? 0))
|
||||
_speaker = StateObject(wrappedValue: EnSpeaker())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let lesson = vm.content.enLesson(year, id) {
|
||||
let p = vm.palette
|
||||
ExerciseScaffold(vm: vm, title: vm.tr(lesson.str("titleKey")), sub: nil, action: vm.tr("check"), onAction: {
|
||||
check(lesson)
|
||||
}, feedback: fb.0, kind: fb.1) {
|
||||
if let reading = lesson.strOrNull("reading") {
|
||||
section("en_sec_read")
|
||||
Text(reading).textSelection(.enabled)
|
||||
choiceBlock(lesson.arr("readQuiz"), picks: $readPicks)
|
||||
}
|
||||
if let listening = lesson.strOrNull("listening") {
|
||||
section("en_sec_listen")
|
||||
Text(vm.tr("en_listen_note"))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
PillButton(label: vm.tr("en_play")) {
|
||||
played = true
|
||||
showScript = false
|
||||
speaker.speak(listening)
|
||||
let wait = max(2.5, Double(listening.split(separator: " ").count) * 0.42)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
|
||||
showScript = true
|
||||
}
|
||||
}
|
||||
if showScript {
|
||||
Text(listening).textSelection(.enabled)
|
||||
}
|
||||
choiceBlock(lesson.arr("listenQuiz"), picks: $listenPicks)
|
||||
}
|
||||
let gapRows = lesson.arr("gaps")
|
||||
if !gapRows.isEmpty {
|
||||
section("en_sec_gap")
|
||||
ForEach(gapRows.indices, id: \.self) { i in
|
||||
Text("\(i + 1). \(gapRows[i].str("prompt"))")
|
||||
.font(.body.weight(.semibold))
|
||||
WordField(
|
||||
value: Binding(get: { gaps[i] }, set: { gaps[i] = $0 }),
|
||||
placeholder: "",
|
||||
mark: revealed ? answerAccepts(normalizeAnswer(gaps[i]), gapRows[i].str("answers")) : nil,
|
||||
palette: p
|
||||
)
|
||||
if revealed, let hint = gapRows[i].strOrNull("meaning") {
|
||||
Meaning(text: hint, palette: p, visible: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let writing = lesson.obj("writing") {
|
||||
section("en_sec_write")
|
||||
Text(writing.str("prompt"))
|
||||
TextEditor(text: $essay)
|
||||
.frame(minHeight: 140)
|
||||
.padding(8)
|
||||
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 12))
|
||||
if showModel {
|
||||
Text(vm.tr("en_model"))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(writing.str("model")).textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
let quiz = lesson.arr("quiz")
|
||||
if !quiz.isEmpty {
|
||||
section("en_sec_quiz")
|
||||
choiceBlock(quiz, picks: $quizPicks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func section(_ key: String) -> some View {
|
||||
Text(vm.tr(key))
|
||||
.font(.title3.weight(.bold))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
private func choiceBlock(_ questions: [J], picks: Binding<[Int]>) -> some View {
|
||||
let p = vm.palette
|
||||
return ForEach(questions.indices, id: \.self) { i in
|
||||
let q = questions[i]
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("\(i + 1). \(q.str("prompt"))")
|
||||
.font(.body.weight(.semibold))
|
||||
ForEach(Array(q.strs("options").enumerated()), id: \.offset) { o, opt in
|
||||
let selected = picks.wrappedValue[i] == o
|
||||
let mark: Bool? = revealed && selected ? picks.wrappedValue[i] == q.int("correct") : nil
|
||||
OptionChip(text: opt, selected: selected, mark: mark) {
|
||||
picks.wrappedValue[i] = o
|
||||
}
|
||||
}
|
||||
if revealed, let expl = q.strOrNull("expl") {
|
||||
Meaning(text: expl, palette: p, visible: true)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
|
||||
private func check(_ lesson: J) {
|
||||
if lesson.strOrNull("listening") != nil && !played {
|
||||
fb = (vm.tr("en_listen_first"), "err")
|
||||
return
|
||||
}
|
||||
let readOk = score(lesson.arr("readQuiz"), readPicks)
|
||||
let listenOk = score(lesson.arr("listenQuiz"), listenPicks)
|
||||
let quizOk = score(lesson.arr("quiz"), quizPicks)
|
||||
let gapRows = lesson.arr("gaps")
|
||||
let gapOk = gapRows.indices.filter {
|
||||
answerAccepts(normalizeAnswer(gaps[$0]), gapRows[$0].str("answers"))
|
||||
}.count
|
||||
var writeOk = true
|
||||
if let writing = lesson.obj("writing") {
|
||||
let keys = writing.str("keys").split(separator: "|").map(String.init)
|
||||
let enough = wordCount(essay) >= writing.int("minWords", 20)
|
||||
writeOk = enough && keys.allSatisfy { hasWord(essay, $0) }
|
||||
}
|
||||
revealed = true
|
||||
let all = readOk && listenOk && quizOk && gapOk == gapRows.count && writeOk
|
||||
if all {
|
||||
fb = (vm.tr("feedback_ok"), "ok")
|
||||
showModel = true
|
||||
vm.markEn(year, id)
|
||||
} else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count {
|
||||
fb = (vm.tr("en_write_short"), "err")
|
||||
} else {
|
||||
fb = (vm.tr("feedback_retry_short"), "err")
|
||||
}
|
||||
}
|
||||
|
||||
private func score(_ questions: [J], _ picks: [Int]) -> Bool {
|
||||
questions.indices.allSatisfy { picks[$0] == questions[$0].int("correct") }
|
||||
}
|
||||
|
||||
private func wordCount(_ text: String) -> Int {
|
||||
let n = normalizeAnswer(text)
|
||||
return n.isEmpty ? 0 : n.split(separator: " ").count
|
||||
}
|
||||
|
||||
private func hasWord(_ text: String, _ word: String) -> Bool {
|
||||
let n = " \(normalizeAnswer(text)) "
|
||||
let w = normalizeAnswer(word)
|
||||
return !w.isEmpty && n.contains(" \(w) ")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,3 +33,27 @@ struct CzechFlag: View {
|
|||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
|
||||
struct UkFlag: View {
|
||||
var size: CGFloat = 48
|
||||
var body: some View {
|
||||
Canvas { ctx, sz in
|
||||
let w = sz.width
|
||||
let h = sz.height
|
||||
ctx.clip(to: Path(ellipseIn: CGRect(origin: .zero, size: sz)))
|
||||
ctx.fill(Path(CGRect(origin: .zero, size: sz)), with: .color(Color(red: 0.01, green: 0.14, blue: 0.40)))
|
||||
var saltire = Path()
|
||||
saltire.move(to: .zero)
|
||||
saltire.addLine(to: CGPoint(x: w, y: h))
|
||||
saltire.move(to: CGPoint(x: w, y: 0))
|
||||
saltire.addLine(to: CGPoint(x: 0, y: h))
|
||||
ctx.stroke(saltire, with: .color(.white), lineWidth: w * 0.16)
|
||||
ctx.stroke(saltire, with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)), lineWidth: w * 0.055)
|
||||
ctx.fill(Path(CGRect(x: 0, y: h * 0.38, width: w, height: h * 0.24)), with: .color(.white))
|
||||
ctx.fill(Path(CGRect(x: w * 0.38, y: 0, width: w * 0.24, height: h)), with: .color(.white))
|
||||
ctx.fill(Path(CGRect(x: 0, y: h * 0.43, width: w, height: h * 0.14)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)))
|
||||
ctx.fill(Path(CGRect(x: w * 0.43, y: 0, width: w * 0.14, height: h)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)))
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -689,6 +689,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 "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
|
||||
acc + vm.content.enYear(year).filter { vm.progress.enDone(year, $0.int("id")) }.count
|
||||
}
|
||||
case "onyears", "onmap", "on2map", "on3map", "on4map":
|
||||
sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + vm.content.on4.count
|
||||
sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count
|
||||
|
|
@ -905,6 +910,12 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
|
|||
add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik")
|
||||
add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
|
||||
add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra: "rocnik")
|
||||
for year in 1...4 {
|
||||
add(vm.tr("en_year\(year)"), vm.tr("search_lesson"), "en\(year)map", extra: "anglictina english rocnik")
|
||||
for l in vm.content.enYear(year) {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en\(year)unit\(l.int("id"))", extra: "anglictina english")
|
||||
}
|
||||
}
|
||||
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"))")
|
||||
|
|
@ -1247,6 +1258,40 @@ struct HwYearsScreen: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct EnYearsScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
let years: [(String, String, Route)] = [
|
||||
("en_year1", "en_y1_sub", .enMap(1)),
|
||||
("en_year2", "en_y2_sub", .enMap(2)),
|
||||
("en_year3", "en_y3_sub", .enMap(3)),
|
||||
("en_year4", "en_y4_sub", .enMap(4)),
|
||||
]
|
||||
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("English"))
|
||||
.navigationSubtitle(vm.tr("en_years_sub"))
|
||||
#else
|
||||
List {
|
||||
ForEach(1...4, id: \.self) { year in
|
||||
NavigationLink(value: Route.enMap(year)) {
|
||||
ListRowLabel(title: vm.tr("en_year\(year)"), subtitle: vm.tr("en_y\(year)_sub"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("English"))
|
||||
.navigationSubtitle(vm.tr("en_years_sub"))
|
||||
.appListStyle()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct OnYearsScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
|
|
@ -2183,6 +2228,29 @@ struct RouteDestination: View {
|
|||
HwQuizScreen(vm: vm, id: id)
|
||||
case .onYears:
|
||||
OnYearsScreen(vm: vm)
|
||||
case .enYears:
|
||||
EnYearsScreen(vm: vm)
|
||||
case .enMap(let year):
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("en_year\(year)"),
|
||||
subtitle: vm.tr("en_y\(year)_sub"),
|
||||
rows: vm.content.enYear(year).map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(
|
||||
id: "e\(year)\(id)",
|
||||
title: vm.tr(l.str("titleKey")),
|
||||
done: vm.progress.enDone(year, id),
|
||||
destination: .enLesson(year, id)
|
||||
)
|
||||
}
|
||||
)
|
||||
case .enLesson(let year, let id):
|
||||
if let l = vm.content.enLesson(year, id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .enEx(year, id))
|
||||
}
|
||||
case .enEx(let year, let id):
|
||||
EngExScreen(vm: vm, year: year, id: id)
|
||||
case .onMap:
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue