mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 07:04:35 +00:00
Add four years of German listening, reading and writing beside the textbook.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3b4e7b283a
commit
b822c57875
31 changed files with 3589 additions and 102 deletions
|
|
@ -171,6 +171,11 @@ final class AppModel: ObservableObject {
|
|||
refresh()
|
||||
}
|
||||
|
||||
func markDe(_ year: Int, _ id: Int) {
|
||||
progress.markDe(year, id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markLit(_ id: Int) {
|
||||
progress.markLit(id)
|
||||
refresh()
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ final class Content {
|
|||
let en2: [J]
|
||||
let en3: [J]
|
||||
let en4: [J]
|
||||
let de: [J]
|
||||
let de2: [J]
|
||||
let de3: [J]
|
||||
let de4: [J]
|
||||
let lit: [J]
|
||||
let lit2: [J]
|
||||
let lit3: [J]
|
||||
|
|
@ -93,6 +97,10 @@ final class Content {
|
|||
en2 = raw.arr("en2")
|
||||
en3 = raw.arr("en3")
|
||||
en4 = raw.arr("en4")
|
||||
de = raw.arr("de")
|
||||
de2 = raw.arr("de2")
|
||||
de3 = raw.arr("de3")
|
||||
de4 = raw.arr("de4")
|
||||
lit = raw.arr("lit")
|
||||
lit2 = raw.arr("lit2")
|
||||
lit3 = raw.arr("lit3")
|
||||
|
|
@ -129,6 +137,15 @@ final class Content {
|
|||
}
|
||||
}
|
||||
func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).first { $0.int("id") == id } }
|
||||
func deYear(_ year: Int) -> [J] {
|
||||
switch year {
|
||||
case 2: return de2
|
||||
case 3: return de3
|
||||
case 4: return de4
|
||||
default: return de
|
||||
}
|
||||
}
|
||||
func deLesson(_ year: Int, _ id: Int) -> J? { deYear(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 } }
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ final class ProgressStore {
|
|||
|
||||
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 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 litDone(_ id: Int) -> Bool { defaults.bool(forKey: "lit.\(id)") }
|
||||
func markLit(_ id: Int) { defaults.set(true, forKey: "lit.\(id)") }
|
||||
|
|
@ -165,6 +167,14 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
|
|||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for l in content.deYear(year) {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.deDone(year, l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
for l in content.lit {
|
||||
sum.totalEx += 1
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ enum Route: Hashable {
|
|||
case subjects
|
||||
case stats
|
||||
case roadmap
|
||||
case deHome
|
||||
case deMap(Int)
|
||||
case deLesson(Int, Int)
|
||||
case deEx(Int, Int)
|
||||
case unitMap(Int)
|
||||
case germanEx(Int, Int)
|
||||
case vocab(Int)
|
||||
|
|
@ -103,6 +107,10 @@ enum Route: Hashable {
|
|||
case .subjects: return "subjects"
|
||||
case .stats: return "stats"
|
||||
case .roadmap: return "roadmap"
|
||||
case .deHome: return "dehome"
|
||||
case .deMap(let year): return "de\(year)map"
|
||||
case .deLesson(let year, let id): return "de\(year)unit\(id)"
|
||||
case .deEx(let year, let id): return "de\(year)ex\(id)"
|
||||
case .unitMap(let unitId): return "unit\(unitId + 1)"
|
||||
case .germanEx(let unitId, let ex): return "u\(unitId + 1)e\(ex)"
|
||||
case .vocab(let unitId): return "u\(unitId + 1)vocab"
|
||||
|
|
@ -204,14 +212,22 @@ enum Route: Hashable {
|
|||
switch self {
|
||||
case .welcome, .subjects, .stats:
|
||||
return []
|
||||
case .deHome:
|
||||
return [.deHome]
|
||||
case .deMap(let year):
|
||||
return [.deHome, .deMap(year)]
|
||||
case .deLesson(let year, let id):
|
||||
return [.deHome, .deMap(year), .deLesson(year, id)]
|
||||
case .deEx(let year, let id):
|
||||
return [.deHome, .deMap(year), .deLesson(year, id), .deEx(year, id)]
|
||||
case .roadmap:
|
||||
return [.roadmap]
|
||||
return [.deHome, .roadmap]
|
||||
case .unitMap(let id):
|
||||
return [.roadmap, .unitMap(id)]
|
||||
return [.deHome, .roadmap, .unitMap(id)]
|
||||
case .germanEx(let unit, let ex):
|
||||
return [.roadmap, .unitMap(unit), .germanEx(unit, ex)]
|
||||
return [.deHome, .roadmap, .unitMap(unit), .germanEx(unit, ex)]
|
||||
case .vocab(let unit):
|
||||
return [.roadmap, .unitMap(unit), .vocab(unit)]
|
||||
return [.deHome, .roadmap, .unitMap(unit), .vocab(unit)]
|
||||
case .netYears:
|
||||
return [.netYears]
|
||||
case .netMap:
|
||||
|
|
@ -384,6 +400,7 @@ func routeFromPage(_ page: String) -> Route? {
|
|||
case "subjects": return .subjects
|
||||
case "stats": return .stats
|
||||
case "roadmap": return .roadmap
|
||||
case "dehome": return .deHome
|
||||
case "netyears": return .netYears
|
||||
case "netmap": return .netMap
|
||||
case "hwyears": return .hwYears
|
||||
|
|
@ -546,6 +563,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: /de(\d)map/), let y = Int(m.1) {
|
||||
return .deMap(y)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /de(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
|
||||
return .deLesson(y, n)
|
||||
}
|
||||
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: /en(\d)map/), let y = Int(m.1) {
|
||||
return .enMap(y)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1359,6 +1359,7 @@ private final class EnSpeaker: NSObject, ObservableObject, AVSpeechSynthesizerDe
|
|||
@Published var rate: Float = 0.9
|
||||
@Published var volume: Float = 1
|
||||
@Published var phase: Phase = .idle
|
||||
var language = "en-GB"
|
||||
var onFinish: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
|
|
@ -1408,8 +1409,8 @@ private final class EnSpeaker: NSObject, ObservableObject, AVSpeechSynthesizerDe
|
|||
let rest = ns.substring(from: start)
|
||||
let utterance = AVSpeechUtterance(string: rest.isEmpty ? text : rest)
|
||||
if rest.isEmpty { base = 0 }
|
||||
utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
|
||||
?? AVSpeechSynthesisVoice(language: "en-US")
|
||||
utterance.voice = AVSpeechSynthesisVoice(language: language)
|
||||
?? AVSpeechSynthesisVoice(language: language.hasPrefix("de") ? "de-DE" : "en-US")
|
||||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * rate
|
||||
utterance.volume = volume
|
||||
synth.speak(utterance)
|
||||
|
|
@ -1443,6 +1444,9 @@ struct EngExScreen: View {
|
|||
@ObservedObject var vm: AppModel
|
||||
let year: Int
|
||||
let id: Int
|
||||
let voice: String
|
||||
let lessonOf: (AppModel, Int, Int) -> J?
|
||||
let mark: (AppModel, Int, Int) -> Void
|
||||
@StateObject private var speaker: EnSpeaker
|
||||
@State private var readPicks: [Int]
|
||||
@State private var listenPicks: [Int]
|
||||
|
|
@ -1455,20 +1459,27 @@ struct EngExScreen: View {
|
|||
@State private var showModel = false
|
||||
@State private var fb = ("", "")
|
||||
|
||||
init(vm: AppModel, year: Int, id: Int) {
|
||||
init(vm: AppModel, year: Int, id: Int, voice: String = "en-GB",
|
||||
lesson: @escaping (AppModel, Int, Int) -> J? = { $0.content.enLesson($1, $2) },
|
||||
mark: @escaping (AppModel, Int, Int) -> Void = { $0.markEn($1, $2) }) {
|
||||
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())
|
||||
self.voice = voice
|
||||
self.lessonOf = lesson
|
||||
self.mark = mark
|
||||
let row = lesson(vm, year, id)
|
||||
_readPicks = State(initialValue: Array(repeating: -1, count: row?.arr("readQuiz").count ?? 0))
|
||||
_listenPicks = State(initialValue: Array(repeating: -1, count: row?.arr("listenQuiz").count ?? 0))
|
||||
_quizPicks = State(initialValue: Array(repeating: -1, count: row?.arr("quiz").count ?? 0))
|
||||
_gaps = State(initialValue: Array(repeating: "", count: row?.arr("gaps").count ?? 0))
|
||||
let speaker = EnSpeaker()
|
||||
speaker.language = voice
|
||||
_speaker = StateObject(wrappedValue: speaker)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let lesson = vm.content.enLesson(year, id) {
|
||||
if let lesson = lessonOf(vm, year, id) {
|
||||
let p = vm.palette
|
||||
ExerciseScaffold(vm: vm, title: vm.tr(lesson.str("titleKey")), sub: nil, action: vm.tr("check"), onAction: {
|
||||
check(lesson)
|
||||
|
|
@ -1619,7 +1630,7 @@ struct EngExScreen: View {
|
|||
if all {
|
||||
fb = (vm.tr("feedback_ok"), "ok")
|
||||
showModel = true
|
||||
vm.markEn(year, id)
|
||||
mark(vm, year, id)
|
||||
} else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count {
|
||||
fb = (vm.tr("en_write_short"), "err")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -662,12 +662,16 @@ struct StatsScreen: View {
|
|||
private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
|
||||
var sum = ProgressSum()
|
||||
switch s.str("target") {
|
||||
case "roadmap":
|
||||
case "dehome":
|
||||
for u in vm.content.german where u.bool("unlocked") {
|
||||
let n = u.strs("names").count
|
||||
sum.totalEx += n
|
||||
sum.doneEx += (1...n).filter { vm.progress.germanDone(u.int("id"), $0) }.count
|
||||
}
|
||||
sum.totalEx += (1...4).reduce(0) { $0 + vm.content.deYear($1).count }
|
||||
sum.doneEx += (1...4).reduce(0) { acc, year in
|
||||
acc + vm.content.deYear(year).filter { vm.progress.deDone(year, $0.int("id")) }.count
|
||||
}
|
||||
case "netyears":
|
||||
sum.totalEx = vm.content.netLessons.count
|
||||
sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count
|
||||
|
|
@ -900,6 +904,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
|
|||
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")
|
||||
}
|
||||
add(vm.tr("de_year\(year)"), vm.tr("search_lesson"), "de\(year)map", extra: "nemcina deutsch rocnik")
|
||||
for l in vm.content.deYear(year) {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "de\(year)unit\(l.int("id"))", extra: "nemcina deutsch")
|
||||
}
|
||||
}
|
||||
for l in vm.content.on {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka")
|
||||
|
|
@ -1243,6 +1251,43 @@ struct HwYearsScreen: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct DeHomeScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
let rows: [(String, String, String, Route)] = [
|
||||
("U", "de_book_title", "de_book_sub", .roadmap),
|
||||
("1", "de_year1", "de_y1_sub", .deMap(1)),
|
||||
("2", "de_year2", "de_y2_sub", .deMap(2)),
|
||||
("3", "de_year3", "de_y3_sub", .deMap(3)),
|
||||
("4", "de_year4", "de_y4_sub", .deMap(4)),
|
||||
]
|
||||
MacFillGrid(count: rows.count, minWidth: 260, minHeight: 180) { i in
|
||||
NavigationLink(value: rows[i].3) {
|
||||
MacLinkCard(title: vm.tr(rows[i].1), subtitle: vm.tr(rows[i].2), badge: rows[i].0)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.navigationTitle(vm.tr("Deutsch"))
|
||||
.navigationSubtitle(vm.tr("de_home_sub"))
|
||||
#else
|
||||
List {
|
||||
NavigationLink(value: Route.roadmap) {
|
||||
ListRowLabel(title: vm.tr("de_book_title"), subtitle: vm.tr("de_book_sub"))
|
||||
}
|
||||
ForEach(1...4, id: \.self) { year in
|
||||
NavigationLink(value: Route.deMap(year)) {
|
||||
ListRowLabel(title: vm.tr("de_year\(year)"), subtitle: vm.tr("de_y\(year)_sub"))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("Deutsch"))
|
||||
.navigationSubtitle(vm.tr("de_home_sub"))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct EnYearsScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
|
|
@ -2157,6 +2202,29 @@ struct RouteDestination: View {
|
|||
PracticeHome(vm: vm)
|
||||
case .stats:
|
||||
StatsScreen(vm: vm)
|
||||
case .deHome:
|
||||
DeHomeScreen(vm: vm)
|
||||
case .deMap(let year):
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("de_year\(year)"),
|
||||
subtitle: vm.tr("de_y\(year)_sub"),
|
||||
rows: vm.content.deYear(year).map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(
|
||||
id: "d\(year)\(id)",
|
||||
title: vm.tr(l.str("titleKey")),
|
||||
done: vm.progress.deDone(year, id),
|
||||
destination: .deLesson(year, id)
|
||||
)
|
||||
}
|
||||
)
|
||||
case .deLesson(let year, let id):
|
||||
if let l = vm.content.deLesson(year, id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .deEx(year, id))
|
||||
}
|
||||
case .deEx(let year, let id):
|
||||
EngExScreen(vm: vm, year: year, id: id, voice: "de-DE", lesson: { $0.content.deLesson($1, $2) }, mark: { $0.markDe($1, $2) })
|
||||
case .roadmap:
|
||||
RoadmapScreen(vm: vm)
|
||||
case .unitMap(let id):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue