Add interactive mathematics for elementary review and four secondary-school years.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-06 14:49:25 +02:00
commit 0e24e8c649
38 changed files with 9507 additions and 11 deletions

View file

@ -198,6 +198,31 @@ final class AppModel: ObservableObject {
refresh()
}
func markMat0(_ id: Int) {
progress.markMat0(id)
refresh()
}
func markMat(_ id: Int) {
progress.markMat(id)
refresh()
}
func markMat2(_ id: Int) {
progress.markMat2(id)
refresh()
}
func markMat3(_ id: Int) {
progress.markMat3(id)
refresh()
}
func markMat4(_ id: Int) {
progress.markMat4(id)
refresh()
}
func markMluv(_ n: Int) {
progress.markMluv(n)
refresh()

View file

@ -58,3 +58,658 @@ func hasUmlaut(_ s: String?) -> Bool {
guard let s else { return false }
return s.contains(where: { "äöüÄÖÜß".contains($0) })
}
// MARK: - Mathematics
// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer.
private struct MFrac { var n: Int64 = 0; var d: Int64 = 1 }
private struct MTerm {
var ok = true
var a = MFrac()
var b = MFrac()
var rad: Int64 = 0
var p = MFrac()
}
private func mGcd(_ a0: Int64, _ b0: Int64) -> Int64 {
var a = a0 < 0 ? -a0 : a0
var b = b0 < 0 ? -b0 : b0
while b != 0 {
let t = a % b
a = b
b = t
}
return a == 0 ? 1 : a
}
private func mMul(_ a: Int64, _ b: Int64) -> Int64? {
let r = a.multipliedReportingOverflow(by: b)
return r.overflow ? nil : r.partialValue
}
private func mSet(_ f: inout MFrac, _ n: Int64, _ d: Int64) -> Bool {
if d == 0 { return false }
var n = n, d = d
if d < 0 { n = -n; d = -d }
let g = mGcd(n, d)
f.n = n / g
f.d = d / g
return true
}
private func mAddF(_ x: MFrac, _ y: MFrac) -> MFrac? {
guard let xn = mMul(x.n, y.d), let yn = mMul(y.n, x.d),
let d = mMul(x.d, y.d) else { return nil }
let s = xn.addingReportingOverflow(yn)
if s.overflow { return nil }
var o = MFrac()
return mSet(&o, s.partialValue, d) ? o : nil
}
private func mMulF(_ x: MFrac, _ y: MFrac) -> MFrac? {
guard let n = mMul(x.n, y.n), let d = mMul(x.d, y.d) else { return nil }
var o = MFrac()
return mSet(&o, n, d) ? o : nil
}
private func mDivF(_ x: MFrac, _ y: MFrac) -> MFrac? {
if y.n == 0 { return nil }
return mMulF(x, MFrac(n: y.d, d: y.n))
}
private func mSameF(_ x: MFrac, _ y: MFrac) -> Bool { x.n == y.n && x.d == y.d }
private func mSqrtReduce(_ n0: Int64) -> (out: Int64, rad: Int64)? {
if n0 < 0 { return nil }
if n0 > 1_000_000 { return nil }
var n = n0
var out: Int64 = 1
var f: Int64 = 2
while f * f <= n {
var c = 0
while n % f == 0 { n /= f; c += 1 }
if c >= 2 { out *= mPow(f, c / 2) }
if c % 2 == 1 { n *= f }
if f == 2 { f = 3 } else { f += 2 }
if f > 10000 { break }
}
return (out, n)
}
private func mPow(_ b: Int64, _ e: Int) -> Int64 {
var r: Int64 = 1
var i = 0
while i < e { r *= b; i += 1 }
return r
}
private func mNorm(_ t: inout MTerm) {
if !t.ok { return }
if t.rad > 1, let s = mSqrtReduce(t.rad) {
guard let nb = mMulF(t.b, MFrac(n: s.out, d: 1)) else { t.ok = false; return }
t.b = nb
t.rad = s.rad
}
if t.rad <= 1 {
guard let na = mAddF(t.a, t.b) else { t.ok = false; return }
t.a = na
t.b = MFrac()
t.rad = 0
}
if t.b.n == 0 { t.rad = 0 }
}
private func mAdd(_ x: MTerm, _ y: MTerm) -> MTerm {
if !x.ok || !y.ok { return MTerm(ok: false) }
var o = MTerm()
guard let a = mAddF(x.a, y.a), let p = mAddF(x.p, y.p) else { return MTerm(ok: false) }
o.a = a
o.p = p
if x.rad == 0 && y.rad == 0 {
o.b = MFrac(); o.rad = 0
} else if x.rad == 0 {
o.b = y.b; o.rad = y.rad
} else if y.rad == 0 || x.rad == y.rad {
guard let b = mAddF(x.b, y.b) else { return MTerm(ok: false) }
o.b = b; o.rad = x.rad
} else { return MTerm(ok: false) }
mNorm(&o)
return o
}
private func mScale(_ x: MTerm, _ f: MFrac) -> MTerm {
if !x.ok { return MTerm(ok: false) }
var o = MTerm()
guard let a = mMulF(x.a, f), let b = mMulF(x.b, f), let p = mMulF(x.p, f) else {
return MTerm(ok: false)
}
o.a = a; o.b = b; o.rad = x.rad; o.p = p
mNorm(&o)
return o
}
private func mMulT(_ x: MTerm, _ y: MTerm) -> MTerm {
if !x.ok || !y.ok { return MTerm(ok: false) }
if (x.p.n != 0 && (y.p.n != 0 || y.rad != 0)) ||
(y.p.n != 0 && (x.p.n != 0 || x.rad != 0)) { return MTerm(ok: false) }
if x.rad != 0 && y.rad != 0 && x.rad != y.rad { return MTerm(ok: false) }
var o = MTerm()
guard let aa = mMulF(x.a, y.a) else { return MTerm(ok: false) }
o.a = aa
if x.rad == 0 && y.rad == 0 {
o.b = MFrac(); o.rad = 0
} else if x.rad == 0 {
guard let b = mMulF(x.a, y.b) else { return MTerm(ok: false) }
o.b = b; o.rad = y.rad
} else if y.rad == 0 {
guard let b = mMulF(y.a, x.b) else { return MTerm(ok: false) }
o.b = b; o.rad = x.rad
} else {
guard let ab = mMulF(x.a, y.b), let ba = mMulF(x.b, y.a),
let cross = mAddF(ab, ba),
let sq = mMul(x.rad, y.rad) else { return MTerm(ok: false) }
guard let bb = mMulF(x.b, y.b), let extra = mMulF(bb, MFrac(n: sq, d: 1)),
let na = mAddF(o.a, extra) else { return MTerm(ok: false) }
o.a = na; o.b = cross; o.rad = x.rad
}
if x.p.n != 0 { o.p = x.p } else { o.p = y.p }
if y.a.n != 1 || y.a.d != 1 || y.b.n != 0 || y.rad != 0 || y.p.n != 0 {
if x.p.n != 0 {
guard let p = mMulF(x.p, y.a) else { return MTerm(ok: false) }
o.p = p
}
}
if x.a.n != 1 || x.a.d != 1 || x.b.n != 0 || x.rad != 0 || x.p.n != 0 {
if y.p.n != 0 && x.p.n == 0 {
guard let p = mMulF(y.p, x.a) else { return MTerm(ok: false) }
o.p = p
}
}
mNorm(&o)
return o
}
private func mDivT(_ x: MTerm, _ y: MTerm) -> MTerm {
if !x.ok || !y.ok || y.p.n != 0 { return MTerm(ok: false) }
if y.rad == 0 {
guard let f = mDivF(MFrac(n: 1, d: 1), y.a) else { return MTerm(ok: false) }
return mScale(x, f)
}
if x.p.n != 0 { return MTerm(ok: false) }
guard let yb = mMulF(y.b, MFrac(n: y.rad, d: 1)),
let bb = mMulF(y.b, yb),
let aa = mMulF(y.a, y.a),
let den = mAddF(aa, MFrac(n: -bb.n, d: bb.d)) else { return MTerm(ok: false) }
var num = MTerm()
num.a = y.a
num.b = MFrac(n: -y.b.n, d: y.b.d)
num.rad = y.rad
guard let inv = mDivF(MFrac(n: 1, d: 1), den) else { return MTerm(ok: false) }
return mMulT(x, mScale(num, inv))
}
private enum MTok {
case num(Int64, Int64)
case sqrt, pi, add, sub, mul, div, lp, rp
}
private func mLex(_ s: String) -> [MTok]? {
var toks: [MTok] = []
var i = s.startIndex
func peek(_ k: Int) -> Character? {
var j = i
var n = 0
while n < k && j < s.endIndex { j = s.index(after: j); n += 1 }
return j < s.endIndex ? s[j] : nil
}
while i < s.endIndex {
while i < s.endIndex && s[i].isWhitespace { i = s.index(after: i) }
if i >= s.endIndex { break }
let ch = s[i]
if ch.isNumber || ((ch == "." || ch == ",") && (peek(1)?.isNumber == true)) {
var whole: Int64 = 0
var saw = false
while i < s.endIndex && s[i].isNumber {
saw = true
whole = whole * 10 + Int64(s[i].wholeNumberValue ?? 0)
i = s.index(after: i)
}
if i < s.endIndex && (s[i] == "." || s[i] == ",") && (s.index(after: i) < s.endIndex && s[s.index(after: i)].isNumber) {
i = s.index(after: i)
var frac: Int64 = 0
var div: Int64 = 1
while i < s.endIndex && s[i].isNumber {
frac = frac * 10 + Int64(s[i].wholeNumberValue ?? 0)
div *= 10
i = s.index(after: i)
}
let n = whole * div + frac
toks.append(.num(n, div))
} else if saw {
toks.append(.num(whole, 1))
} else { return nil }
continue
}
if ch == "+" { toks.append(.add); i = s.index(after: i); continue }
if ch == "-" || ch == "−" || ch == "–" { toks.append(.sub); i = s.index(after: i); continue }
if ch == "*" || ch == "×" || ch == "·" { toks.append(.mul); i = s.index(after: i); continue }
if ch == "/" || ch == ":" { toks.append(.div); i = s.index(after: i); continue }
if ch == "(" { toks.append(.lp); i = s.index(after: i); continue }
if ch == ")" { toks.append(.rp); i = s.index(after: i); continue }
if ch == "π" { toks.append(.pi); i = s.index(after: i); continue }
if ch == "√" { toks.append(.sqrt); i = s.index(after: i); continue }
if ch.isLetter {
var j = i
while j < s.endIndex && s[j].isLetter { j = s.index(after: j) }
let w = s[i..<j].lowercased()
if w == "pi" { toks.append(.pi) }
else if w == "sqrt" { toks.append(.sqrt) }
else { return nil }
i = j
continue
}
return nil
}
return toks
}
private struct MParse {
let t: [MTok]
var i = 0
func peek() -> MTok? { i < t.count ? t[i] : nil }
mutating func take() -> MTok? {
if i >= t.count { return nil }
defer { i += 1 }
return t[i]
}
mutating func expr() -> MTerm? {
guard var left = term() else { return nil }
while let p = peek() {
if case .add = p { _ = take() }
else if case .sub = p { _ = take() }
else { break }
guard let r = term() else { return nil }
if case .sub = p { left = mAdd(left, mScale(r, MFrac(n: -1, d: 1))) }
else { left = mAdd(left, r) }
if !left.ok { return nil }
}
return left
}
mutating func term() -> MTerm? {
guard var left = unary() else { return nil }
while let p = peek() {
if case .mul = p { _ = take() }
else if case .div = p { _ = take() }
else { break }
guard let r = unary() else { return nil }
if case .div = p { left = mDivT(left, r) }
else { left = mMulT(left, r) }
if !left.ok { return nil }
}
return left
}
mutating func unary() -> MTerm? {
if case .sub = peek() {
_ = take()
guard let v = unary() else { return nil }
return mScale(v, MFrac(n: -1, d: 1))
}
if case .add = peek() {
_ = take()
return unary()
}
return factor()
}
mutating func factor() -> MTerm? {
guard var left = primary() else { return nil }
while true {
guard let p = peek() else { break }
switch p {
case .sqrt, .pi, .lp:
guard let r = primary() else { return nil }
left = mMulT(left, r)
if !left.ok { return nil }
case .num:
return nil
default:
return left
}
}
return left
}
mutating func primary() -> MTerm? {
guard let p = take() else { return nil }
switch p {
case .num(let n, let d):
if case .div = peek(), case .num = t[safe: i + 1] {
_ = take()
if case .num(let n2, let d2) = take() {
if n2 <= 0 || d2 != 1 || d != 1 { return nil }
var o = MTerm()
if !mSet(&o.a, n, n2) { return nil }
return o
}
return nil
}
if d == 1, case .num(let n2, let d2) = peek(), n >= 0 {
_ = take()
if case .div = peek(), case .num(let n3, let d3) = t[safe: i + 1], d2 == 1, d3 == 1, n2 >= 0, n3 > 0 {
_ = take()
_ = take()
var frac = MFrac()
if !mSet(&frac, n2, n3) { return nil }
var whole = MTerm()
whole.a = MFrac(n: n, d: 1)
var part = MTerm()
part.a = frac
return mAdd(whole, part)
}
i -= 1
}
var o = MTerm()
if !mSet(&o.a, n, d) { return nil }
return o
case .pi:
var o = MTerm()
o.p = MFrac(n: 1, d: 1)
return o
case .sqrt:
if case .lp = peek() {
_ = take()
guard let inner = expr(), case .rp = take() else { return nil }
return mSqrtTerm(inner)
}
if case .num(let n, let d) = peek(), d == 1, n >= 0 {
_ = take()
return mSqrtTerm(MTerm(a: MFrac(n: n, d: 1)))
}
return nil
case .lp:
guard let inner = expr(), case .rp = take() else { return nil }
return inner
default:
return nil
}
}
}
private extension Array {
subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil }
}
private func mSqrtTerm(_ inner: MTerm) -> MTerm? {
if !inner.ok || inner.p.n != 0 || inner.rad != 0 || inner.b.n != 0 { return nil }
if inner.a.n < 0 || inner.a.d != 1 { return nil }
guard let s = mSqrtReduce(inner.a.n) else { return nil }
var o = MTerm()
o.b = MFrac(n: s.out, d: 1)
o.rad = s.rad
mNorm(&o)
return o.ok ? o : nil
}
private func mPrep(_ raw: String) -> String {
var s = raw.trimmingCharacters(in: .whitespaces)
var again = true
while again {
again = false
if s.count >= 2 {
let pairs: [(Character, Character)] = [("(", ")"), ("[", "]"), ("{", "}")]
if let last = s.last, pairs.contains(where: { $0.0 == s.first && $0.1 == last }) {
s = String(s.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces)
again = true
continue
}
}
}
s = mStripUnits(s).trimmingCharacters(in: .whitespaces)
return s
}
private func mParseTerm(_ s: String) -> MTerm? {
let s = mPrep(s)
if s.isEmpty { return nil }
guard let toks = mLex(s) else { return nil }
if toks.isEmpty { return nil }
var p = MParse(t: toks)
guard let v = p.expr(), p.i == toks.count, v.ok else { return nil }
return v
}
private func mExact(_ x: MTerm, _ y: MTerm) -> Bool {
x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p)
}
private func mDouble(_ t: MTerm) -> Double {
var v = Double(t.a.n) / Double(t.a.d) + Double(t.p.n) / Double(t.p.d) * Double.pi
if t.b.n != 0 && t.rad > 0 {
v += Double(t.b.n) / Double(t.b.d) * (Double(t.rad)).squareRoot()
}
return v
}
private func mClose(_ x: MTerm, _ y: MTerm) -> Bool {
if !x.ok || !y.ok { return false }
if mExact(x, y) { return true }
if x.p.n != 0 || y.p.n != 0 { return false }
let diff = abs(mDouble(x) - mDouble(y))
if x.b.n != 0 || y.b.n != 0 { return diff <= 0.001 }
return diff <= 0.0005
}
private func mFold(_ s: String) -> String {
var out = ""
for ch in s.lowercased() {
switch ch {
case "á", "à", "ä", "â": out.append("a")
case "č", "ć": out.append("c")
case "ď": out.append("d")
case "é", "ě", "ë", "ê": out.append("e")
case "í", "ï": out.append("i")
case "ň": out.append("n")
case "ó", "ö", "ô": out.append("o")
case "ř": out.append("r")
case "š", "ś": out.append("s")
case "ť": out.append("t")
case "ú", "ů", "ü": out.append("u")
case "ý": out.append("y")
case "ž", "ź": out.append("z")
default:
if ch.isLetter || ch.isNumber { out.append(ch) }
}
}
return out
}
private func mStripUnits(_ raw: String) -> String {
var s = raw.trimmingCharacters(in: .whitespaces)
let units = ["procenta", "procent", "cm", "mm", "dm", "km", "m"]
var cut = true
while cut {
cut = false
while s.last?.isWhitespace == true { s.removeLast() }
if s.hasSuffix("°") || s.hasSuffix("%") || s.hasSuffix(".") {
s.removeLast()
cut = true
continue
}
for u in units {
guard s.count > u.count else { continue }
if s.lowercased().hasSuffix(u) {
let keep = s.dropLast(u.count)
if keep.last?.isLetter == true { continue }
s = String(keep)
cut = true
break
}
}
}
return s.trimmingCharacters(in: .whitespaces)
}
private func mSplit(_ s: String) -> [String]? {
let s = s.trimmingCharacters(in: .whitespaces)
if s.isEmpty { return nil }
let semi = s.contains(";")
var parts: [String] = []
var rest = Substring(s)
while !rest.isEmpty {
let end: Substring.Index
let step: Int
if semi {
if let i = rest.firstIndex(of: ";") { end = i; step = 1 }
else { end = rest.endIndex; step = 0 }
} else if let r = rest.range(of: ", ") {
end = r.lowerBound
step = 2
} else {
end = rest.endIndex
step = 0
}
let piece = rest[..<end].trimmingCharacters(in: .whitespaces)
if piece.isEmpty { return nil }
parts.append(piece)
if step == 0 { break }
rest = rest[rest.index(end, offsetBy: step)...]
}
return parts.isEmpty ? nil : parts
}
func mathAnswerOk(_ spec: String?, _ user: String?) -> Bool {
guard var spec, let user else { return false }
spec = spec.trimmingCharacters(in: .whitespaces)
if spec.hasPrefix("word:") {
let opts = spec.dropFirst(5).split(separator: "|").map { mFold(String($0)) }
return opts.contains(mFold(user))
}
if spec.hasPrefix("set:") || spec.hasPrefix("pair:") {
let ordered = spec.hasPrefix("pair:")
let body = String(spec.dropFirst(ordered ? 5 : 4))
guard let want = mSplit(body), let got = mSplit(user) else { return false }
if want.count != got.count || want.isEmpty { return false }
var wt: [MTerm] = []
var gt: [MTerm] = []
for w in want {
guard let t = mParseTerm(w) else { return false }
wt.append(t)
}
for g in got {
guard let t = mParseTerm(g) else { return false }
gt.append(t)
}
if ordered {
for i in wt.indices where !mClose(wt[i], gt[i]) { return false }
return true
}
var used = Array(repeating: false, count: gt.count)
for w in wt {
var hit = gt.indices.first { !used[$0] && mExact(w, gt[$0]) }
if hit == nil { hit = gt.indices.first { !used[$0] && mClose(w, gt[$0]) } }
guard let hit else { return false }
used[hit] = true
}
return true
}
let body = spec.hasPrefix("num:") ? String(spec.dropFirst(4)) : spec
guard let tw = mParseTerm(body), let tg = mParseTerm(user) else { return false }
return mClose(tw, tg)
}
func mathIsDraw(_ spec: String?) -> Bool {
guard let spec else { return false }
return spec.hasPrefix("square:") || spec.hasPrefix("mid:") || spec.hasPrefix("right:")
|| spec.hasPrefix("para:") || spec.hasPrefix("points:")
}
func mathDrawNeed(_ spec: String?) -> Int {
guard let spec else { return 0 }
if spec.hasPrefix("square:") { return 2 }
if spec.hasPrefix("mid:") || spec.hasPrefix("right:") || spec.hasPrefix("para:") { return 1 }
if spec.hasPrefix("points:") {
let body = spec.dropFirst(7)
return body.split(separator: ";").filter { !$0.hasPrefix("fix:") }.count
}
return 0
}
private func mParseXY(_ s: Substring) -> (Int, Int)? {
let t = s.trimmingCharacters(in: .whitespaces)
let bits = t.split(separator: ",")
guard bits.count == 2, let x = Int(bits[0].trimmingCharacters(in: .whitespaces)),
let y = Int(bits[1].trimmingCharacters(in: .whitespaces)) else { return nil }
return (x, y)
}
func mathDrawGiven(_ spec: String?) -> [(Int, Int)] {
guard let spec else { return [] }
if spec.hasPrefix("points:") {
return spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in
guard part.hasPrefix("fix:") else { return nil }
return mParseXY(part.dropFirst(4))
}
}
let body: Substring
let n: Int
if spec.hasPrefix("para:") { body = spec.dropFirst(5); n = 3 }
else if spec.hasPrefix("square:") { body = spec.dropFirst(7); n = 2 }
else if spec.hasPrefix("right:") { body = spec.dropFirst(6); n = 2 }
else if spec.hasPrefix("mid:") { body = spec.dropFirst(4); n = 2 }
else { return [] }
return body.split(separator: ";").prefix(n).compactMap { mParseXY($0) }
}
private func mOnBoard(_ p: (Int, Int)) -> Bool {
p.0 >= 0 && p.0 <= 10 && p.1 >= 0 && p.1 <= 8
}
func mathDrawOk(_ spec: String?, _ pts: [(Int, Int)]) -> Bool {
guard let spec, mathDrawNeed(spec) == pts.count else { return false }
if pts.contains(where: { !mOnBoard($0) }) { return false }
if Set(pts.map { "\($0.0),\($0.1)" }).count != pts.count { return false }
let g = mathDrawGiven(spec)
if spec.hasPrefix("mid:") {
guard g.count == 2, (g[0].0 + g[1].0) % 2 == 0, (g[0].1 + g[1].1) % 2 == 0 else { return false }
return pts[0] == ((g[0].0 + g[1].0) / 2, (g[0].1 + g[1].1) / 2)
}
if spec.hasPrefix("para:") {
guard g.count == 3 else { return false }
return pts[0] == (g[1].0 + g[2].0 - g[0].0, g[1].1 + g[2].1 - g[0].1)
}
if spec.hasPrefix("right:") {
guard g.count == 2 else { return false }
let c = pts[0]
if c == g[0] || c == g[1] { return false }
let ux = g[0].0 - c.0, uy = g[0].1 - c.1
let vx = g[1].0 - c.0, vy = g[1].1 - c.1
return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0)
}
if spec.hasPrefix("square:") {
guard g.count == 2, pts.count == 2 else { return false }
let a = g[0], b = g[1]
let dx = b.0 - a.0, dy = b.1 - a.1
if dx == 0 && dy == 0 { return false }
let c1 = (b.0 - dy, b.1 + dx), d1 = (a.0 - dy, a.1 + dx)
let c2 = (b.0 + dy, b.1 - dx), d2 = (a.0 + dy, a.1 - dx)
func same(_ p: (Int, Int), _ q: (Int, Int), _ r: (Int, Int), _ s: (Int, Int)) -> Bool {
(p == r && q == s) || (p == s && q == r)
}
if !mOnBoard(c1) || !mOnBoard(d1) {
if !mOnBoard(c2) || !mOnBoard(d2) { return false }
}
return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2)
}
if spec.hasPrefix("points:") {
let want = spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in
if part.hasPrefix("fix:") { return nil }
return mParseXY(part)
}
if want.count != pts.count { return false }
var used = Array(repeating: false, count: pts.count)
for w in want {
guard let j = pts.indices.first(where: { !used[$0] && pts[$0] == w }) else { return false }
used[j] = true
}
return true
}
return false
}

View file

@ -45,6 +45,11 @@ final class Content {
let fyz2: [J]
let fyz3: [J]
let fyz4: [J]
let mat0: [J]
let mat: [J]
let mat2: [J]
let mat3: [J]
let mat4: [J]
let mluvnice: [J]
let books: [J]
let changelog: [J]
@ -90,6 +95,11 @@ final class Content {
fyz2 = raw.arr("fyz2")
fyz3 = raw.arr("fyz3")
fyz4 = raw.arr("fyz4")
mat0 = raw.arr("mat0")
mat = raw.arr("mat")
mat2 = raw.arr("mat2")
mat3 = raw.arr("mat3")
mat4 = raw.arr("mat4")
mluvnice = raw.arr("mluvnice")
books = raw.arr("books")
changelog = raw.arr("changelog")
@ -112,6 +122,11 @@ final class Content {
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 mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } }
func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } }
func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } }
func mat3Lesson(_ id: Int) -> J? { mat3.first { $0.int("id") == id } }
func mat4Lesson(_ id: Int) -> J? { mat4.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

@ -67,6 +67,16 @@ final class ProgressStore {
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 mat0Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat0.\(id)") }
func markMat0(_ id: Int) { defaults.set(true, forKey: "mat0.\(id)") }
func matDone(_ id: Int) -> Bool { defaults.bool(forKey: "mat.\(id)") }
func markMat(_ id: Int) { defaults.set(true, forKey: "mat.\(id)") }
func mat2Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat2.\(id)") }
func markMat2(_ id: Int) { defaults.set(true, forKey: "mat2.\(id)") }
func mat3Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat3.\(id)") }
func markMat3(_ id: Int) { defaults.set(true, forKey: "mat3.\(id)") }
func mat4Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat4.\(id)") }
func markMat4(_ id: Int) { defaults.set(true, forKey: "mat4.\(id)") }
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
@ -223,6 +233,46 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1
}
}
for l in content.mat0 {
sum.totalEx += 1
sum.openUnits += 1
if p.mat0Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.mat {
sum.totalEx += 1
sum.openUnits += 1
if p.matDone(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.mat2 {
sum.totalEx += 1
sum.openUnits += 1
if p.mat2Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.mat3 {
sum.totalEx += 1
sum.openUnits += 1
if p.mat3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.mat4 {
sum.totalEx += 1
sum.openUnits += 1
if p.mat4Done(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,22 @@ enum Route: Hashable {
case bioMap
case bioLesson(Int)
case bioEx(Int)
case matYears
case mat0Map
case mat0Lesson(Int)
case mat0Ex(Int)
case matMap
case matLesson(Int)
case matEx(Int)
case mat2Map
case mat2Lesson(Int)
case mat2Ex(Int)
case mat3Map
case mat3Lesson(Int)
case mat3Ex(Int)
case mat4Map
case mat4Lesson(Int)
case mat4Ex(Int)
case fyzYears
case fyzMap
case fyzLesson(Int)
@ -127,6 +143,22 @@ enum Route: Hashable {
case .bioMap: return "biomap"
case .bioLesson(let id): return "biounit\(id)"
case .bioEx(let id): return "bioex\(id)"
case .matYears: return "matyears"
case .mat0Map: return "mat0map"
case .mat0Lesson(let id): return "mat0unit\(id)"
case .mat0Ex(let id): return "mat0ex\(id)"
case .matMap: return "matmap"
case .matLesson(let id): return "matunit\(id)"
case .matEx(let id): return "matex\(id)"
case .mat2Map: return "mat2map"
case .mat2Lesson(let id): return "mat2unit\(id)"
case .mat2Ex(let id): return "mat2ex\(id)"
case .mat3Map: return "mat3map"
case .mat3Lesson(let id): return "mat3unit\(id)"
case .mat3Ex(let id): return "mat3ex\(id)"
case .mat4Map: return "mat4map"
case .mat4Lesson(let id): return "mat4unit\(id)"
case .mat4Ex(let id): return "mat4ex\(id)"
case .fyzYears: return "fyzyears"
case .fyzMap: return "fyzmap"
case .fyzLesson(let id): return "fyzunit\(id)"
@ -254,6 +286,38 @@ enum Route: Hashable {
return [.sciMap, .bioMap, .bioLesson(id)]
case .bioEx(let id):
return [.sciMap, .bioMap, .bioLesson(id), .bioEx(id)]
case .matYears:
return [.matYears]
case .mat0Map:
return [.matYears, .mat0Map]
case .mat0Lesson(let id):
return [.matYears, .mat0Map, .mat0Lesson(id)]
case .mat0Ex(let id):
return [.matYears, .mat0Map, .mat0Lesson(id), .mat0Ex(id)]
case .matMap:
return [.matYears, .matMap]
case .matLesson(let id):
return [.matYears, .matMap, .matLesson(id)]
case .matEx(let id):
return [.matYears, .matMap, .matLesson(id), .matEx(id)]
case .mat2Map:
return [.matYears, .mat2Map]
case .mat2Lesson(let id):
return [.matYears, .mat2Map, .mat2Lesson(id)]
case .mat2Ex(let id):
return [.matYears, .mat2Map, .mat2Lesson(id), .mat2Ex(id)]
case .mat3Map:
return [.matYears, .mat3Map]
case .mat3Lesson(let id):
return [.matYears, .mat3Map, .mat3Lesson(id)]
case .mat3Ex(let id):
return [.matYears, .mat3Map, .mat3Lesson(id), .mat3Ex(id)]
case .mat4Map:
return [.matYears, .mat4Map]
case .mat4Lesson(let id):
return [.matYears, .mat4Map, .mat4Lesson(id)]
case .mat4Ex(let id):
return [.matYears, .mat4Map, .mat4Lesson(id), .mat4Ex(id)]
case .fyzYears:
return [.fyzYears]
case .fyzMap:
@ -321,6 +385,12 @@ func routeFromPage(_ page: String) -> Route? {
case "scimap": return .sciMap
case "chemmap": return .chemMap
case "biomap": return .bioMap
case "matyears": return .matYears
case "mat0map": return .mat0Map
case "matmap": return .matMap
case "mat2map": return .mat2Map
case "mat3map": return .mat3Map
case "mat4map": return .mat4Map
case "fyzyears": return .fyzYears
case "fyzmap": return .fyzMap
case "fyz2map": return .fyz2Map
@ -381,6 +451,36 @@ 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: /mat0unit(\d+)/), let n = Int(m.1) {
return .mat0Lesson(n)
}
if let m = page.wholeMatch(of: /mat0ex(\d+)/), let n = Int(m.1) {
return .mat0Ex(n)
}
if let m = page.wholeMatch(of: /mat4unit(\d+)/), let n = Int(m.1) {
return .mat4Lesson(n)
}
if let m = page.wholeMatch(of: /mat4ex(\d+)/), let n = Int(m.1) {
return .mat4Ex(n)
}
if let m = page.wholeMatch(of: /mat3unit(\d+)/), let n = Int(m.1) {
return .mat3Lesson(n)
}
if let m = page.wholeMatch(of: /mat3ex(\d+)/), let n = Int(m.1) {
return .mat3Ex(n)
}
if let m = page.wholeMatch(of: /mat2unit(\d+)/), let n = Int(m.1) {
return .mat2Lesson(n)
}
if let m = page.wholeMatch(of: /mat2ex(\d+)/), let n = Int(m.1) {
return .mat2Ex(n)
}
if let m = page.wholeMatch(of: /matunit(\d+)/), let n = Int(m.1) {
return .matLesson(n)
}
if let m = page.wholeMatch(of: /matex(\d+)/), let n = Int(m.1) {
return .matEx(n)
}
if let m = page.wholeMatch(of: /fyz4unit(\d+)/), let n = Int(m.1) {
return .fyz4Lesson(n)
}

View file

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

View file

@ -646,6 +646,255 @@ struct SciQuizScreen: View {
}
}
private func mathLesson(_ vm: AppModel, year: Int, id: Int) -> J? {
switch year {
case 0: return vm.content.mat0Lesson(id)
case 2: return vm.content.mat2Lesson(id)
case 3: return vm.content.mat3Lesson(id)
case 4: return vm.content.mat4Lesson(id)
default: return vm.content.matLesson(id)
}
}
private func markMathLesson(_ vm: AppModel, year: Int, id: Int) {
switch year {
case 0: vm.markMat0(id)
case 2: vm.markMat2(id)
case 3: vm.markMat3(id)
case 4: vm.markMat4(id)
default: vm.markMat(id)
}
}
struct MathPracticeScreen: View {
@ObservedObject var vm: AppModel
let year: Int
let id: Int
@State private var text: [Int: String] = [:]
@State private var points: [Int: [(Int, Int)]] = [:]
@State private var marks: [Int: Bool] = [:]
@State private var revealed = false
@State private var feedback = ""
@State private var kind = ""
var body: some View {
if let lesson = mathLesson(vm, year: year, id: id) {
let problems = lesson.arr("problems")
ExerciseScaffold(
vm: vm,
title: vm.tr(lesson.str("exTitleKey")),
sub: vm.tr(lesson.str("quizHeadKey")),
action: vm.tr("check"),
onAction: { check(problems) },
feedback: feedback,
kind: kind
) {
Text(vm.tr("math_quiz_intro"))
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.bottom, 4)
ForEach(problems.indices, id: \.self) { i in
mathCard(problems[i], index: i)
}
}
.onChange(of: year) { _, _ in reset() }
.onChange(of: id) { _, _ in reset() }
}
}
private func reset() {
text = [:]
points = [:]
marks = [:]
revealed = false
feedback = ""
kind = ""
}
private func check(_ problems: [J]) {
var all = !problems.isEmpty
var next: [Int: Bool] = [:]
for i in problems.indices {
let spec = problems[i].str("answer")
let good = mathIsDraw(spec)
? mathDrawOk(spec, points[i] ?? [])
: mathAnswerOk(spec, text[i] ?? "")
next[i] = good
if !good { all = false }
}
marks = next
revealed = true
if all {
feedback = vm.tr("feedback_ok")
kind = "ok"
markMathLesson(vm, year: year, id: id)
} else {
feedback = vm.tr("feedback_retry_short")
kind = "bad"
}
}
@ViewBuilder
private func mathCard(_ problem: J, index i: Int) -> some View {
let spec = problem.str("answer")
let p = vm.palette
let border: Color = {
guard let mark = marks[i] else { return p.surface2 }
return mark ? p.success : p.error
}()
VStack(alignment: .leading, spacing: 8) {
Text(problem.str("prompt"))
.frame(maxWidth: .infinity, alignment: .leading)
if mathIsDraw(spec) {
Text(vm.tr("math_draw_hint"))
.font(.caption)
.foregroundStyle(.secondary)
MathGrid(
given: mathDrawGiven(spec),
user: points[i] ?? [],
need: mathDrawNeed(spec),
mark: marks[i],
palette: p
) { next in
points[i] = next
}
HStack {
Button(vm.tr("math_undo")) {
var pts = points[i] ?? []
if !pts.isEmpty {
pts.removeLast()
points[i] = pts
}
}
Button(vm.tr("math_clear")) { points[i] = [] }
}
.buttonStyle(.bordered)
} else {
TextField(vm.tr("math_answer_ph"), text: Binding(
get: { text[i] ?? "" },
set: { text[i] = $0 }
))
.textFieldStyle(.roundedBorder)
#if os(iOS)
.textInputAutocapitalization(.never)
.keyboardType(.numbersAndPunctuation)
#endif
.autocorrectionDisabled()
}
if revealed {
Text(problem.str("hint"))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.padding(12)
.background(p.surface0, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(border, lineWidth: 1.5))
}
}
private struct MathGrid: View {
let given: [(Int, Int)]
let user: [(Int, Int)]
let need: Int
let mark: Bool?
let palette: Palette
var onChange: ([(Int, Int)]) -> Void
private let cell: CGFloat = 28
private let pad: CGFloat = 18
private let gx = 10
private let gy = 8
private var width: CGFloat { pad * 2 + CGFloat(gx) * cell }
private var height: CGFloat { pad * 2 + CGFloat(gy) * cell }
private func screen(_ x: Int, _ y: Int) -> CGPoint {
CGPoint(x: pad + CGFloat(x) * cell, y: height - pad - CGFloat(y) * cell)
}
private func snap(_ loc: CGPoint) -> (Int, Int)? {
let cx = (loc.x - pad) / cell
let cy = (height - pad - loc.y) / cell
let ix = Int(cx.rounded())
let iy = Int(cy.rounded())
if ix < 0 || ix > gx || iy < 0 || iy > gy { return nil }
if abs(cx - CGFloat(ix)) > 0.45 || abs(cy - CGFloat(iy)) > 0.45 { return nil }
return (ix, iy)
}
var body: some View {
Canvas { ctx, _ in
let rect = CGRect(x: 0, y: 0, width: width, height: height)
ctx.fill(Path(rect), with: .color(palette.surface1))
var grid = Path()
for i in 0...gx {
let x = pad + CGFloat(i) * cell
grid.move(to: CGPoint(x: x, y: pad))
grid.addLine(to: CGPoint(x: x, y: height - pad))
}
for i in 0...gy {
let y = pad + CGFloat(i) * cell
grid.move(to: CGPoint(x: pad, y: y))
grid.addLine(to: CGPoint(x: width - pad, y: y))
}
ctx.stroke(grid, with: .color(palette.subtext.opacity(0.55)), lineWidth: 1)
var axes = Path()
axes.move(to: CGPoint(x: pad, y: height - pad))
axes.addLine(to: CGPoint(x: width - pad, y: height - pad))
axes.move(to: CGPoint(x: pad, y: height - pad))
axes.addLine(to: CGPoint(x: pad, y: pad))
ctx.stroke(axes, with: .color(palette.text), lineWidth: 1.6)
if given.count >= 2 {
var seg = Path()
seg.move(to: screen(given[0].0, given[0].1))
seg.addLine(to: screen(given[1].0, given[1].1))
if given.count >= 3 {
seg.move(to: screen(given[0].0, given[0].1))
seg.addLine(to: screen(given[2].0, given[2].1))
}
ctx.stroke(seg, with: .color(palette.accent), lineWidth: 2)
}
let names = ["A", "B", "C", "D"]
for (i, g) in given.enumerated() where i < names.count {
let pt = screen(g.0, g.1)
ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(palette.accent))
ctx.draw(
Text(names[i]).font(.system(size: 13, weight: .semibold)).foregroundStyle(palette.text),
at: CGPoint(x: pt.x + 8, y: pt.y - 8),
anchor: .bottomLeading
)
}
let dot = mark == false ? palette.error : palette.success
for (i, u) in user.enumerated() {
let pt = screen(u.0, u.1)
ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(dot))
ctx.draw(
Text("\(i + 1)").font(.system(size: 12, weight: .bold)).foregroundStyle(palette.text),
at: CGPoint(x: pt.x + 8, y: pt.y - 8),
anchor: .bottomLeading
)
}
}
.frame(width: width, height: height)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0).onEnded { value in
guard let hit = snap(value.location) else { return }
if given.contains(where: { $0.0 == hit.0 && $0.1 == hit.1 }) { return }
var next = user
if let idx = next.firstIndex(where: { $0.0 == hit.0 && $0.1 == hit.1 }) {
next.remove(at: idx)
} else if next.count < need {
next.append(hit)
} else {
return
}
onChange(next)
}
)
}
}
struct FyzQuizScreen: View {
@ObservedObject var vm: AppModel
let year: Int

View file

@ -699,6 +699,13 @@ 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 "matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map":
sum.totalEx = vm.content.mat0.count + vm.content.mat.count + vm.content.mat2.count + vm.content.mat3.count + vm.content.mat4.count
sum.doneEx = vm.content.mat0.filter { vm.progress.mat0Done($0.int("id")) }.count
+ vm.content.mat.filter { vm.progress.matDone($0.int("id")) }.count
+ vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count
+ vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count
+ vm.content.mat4.filter { vm.progress.mat4Done($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
@ -935,6 +942,32 @@ 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("Matematika"), vm.tr("search_lesson"), "matyears", extra: "matematika")
add(vm.tr("mat0_year"), vm.tr("search_lesson"), "mat0map", extra: "zakladka")
add(vm.tr("mat_year1"), vm.tr("search_lesson"), "matmap", extra: "rocnik")
add(vm.tr("mat_year2"), vm.tr("search_lesson"), "mat2map", extra: "rocnik")
add(vm.tr("mat_year3"), vm.tr("search_lesson"), "mat3map", extra: "rocnik")
add(vm.tr("mat_year4"), vm.tr("search_lesson"), "mat4map", extra: "rocnik")
for l in vm.content.mat0 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat0unit\(l.int("id"))", extra: "matematika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat0ex\(l.int("id"))")
}
for l in vm.content.mat {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "matunit\(l.int("id"))", extra: "matematika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "matex\(l.int("id"))")
}
for l in vm.content.mat2 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat2unit\(l.int("id"))", extra: "matematika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat2ex\(l.int("id"))")
}
for l in vm.content.mat3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat3unit\(l.int("id"))", extra: "matematika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat3ex\(l.int("id"))")
}
for l in vm.content.mat4 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat4unit\(l.int("id"))", extra: "matematika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat4ex\(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")
@ -1512,6 +1545,51 @@ struct LitYearsScreen: View {
}
}
struct MatYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let years: [(String, String, Route, String)] = [
("mat0_year", "mat0_sub", .mat0Map, "ZŠ"),
("mat_year1", "mat_sub", .matMap, "1."),
("mat_year2", "mat2_sub", .mat2Map, "2."),
("mat_year3", "mat3_sub", .mat3Map, "3."),
("mat_year4", "mat4_sub", .mat4Map, "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: years[i].3)
}
.buttonStyle(.plain)
}
.navigationTitle(vm.tr("Matematika"))
.navigationSubtitle(vm.tr("mat_years_sub"))
#else
List {
NavigationLink(value: Route.mat0Map) {
ListRowLabel(title: vm.tr("mat0_year"), subtitle: vm.tr("mat0_sub"))
}
NavigationLink(value: Route.matMap) {
ListRowLabel(title: vm.tr("mat_year1"), subtitle: vm.tr("mat_sub"))
}
NavigationLink(value: Route.mat2Map) {
ListRowLabel(title: vm.tr("mat_year2"), subtitle: vm.tr("mat2_sub"))
}
NavigationLink(value: Route.mat3Map) {
ListRowLabel(title: vm.tr("mat_year3"), subtitle: vm.tr("mat3_sub"))
}
NavigationLink(value: Route.mat4Map) {
ListRowLabel(title: vm.tr("mat_year4"), subtitle: vm.tr("mat4_sub"))
}
}
.navigationTitle(vm.tr("Matematika"))
.navigationSubtitle(vm.tr("mat_years_sub"))
.appListStyle()
#endif
}
}
struct FyzYearsScreen: View {
@ObservedObject var vm: AppModel
@ -2321,6 +2399,88 @@ struct RouteDestination: View {
}
case .bioEx(let id):
SciQuizScreen(vm: vm, area: "bio", id: id)
case .matYears:
MatYearsScreen(vm: vm)
case .mat0Map:
LessonListScreen(
vm: vm,
title: vm.tr("mat0_year"),
subtitle: vm.tr("mat0_sub"),
rows: vm.content.mat0.map { l in
let id = l.int("id")
return LessonRow(id: "m0\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat0Done(id), destination: .mat0Lesson(id))
}
)
case .mat0Lesson(let id):
if let l = vm.content.mat0Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat0Ex(id))
}
case .mat0Ex(let id):
MathPracticeScreen(vm: vm, year: 0, id: id)
case .matMap:
LessonListScreen(
vm: vm,
title: vm.tr("mat_year1"),
subtitle: vm.tr("mat_sub"),
rows: vm.content.mat.map { l in
let id = l.int("id")
return LessonRow(id: "m1\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.matDone(id), destination: .matLesson(id))
}
)
case .matLesson(let id):
if let l = vm.content.matLesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .matEx(id))
}
case .matEx(let id):
MathPracticeScreen(vm: vm, year: 1, id: id)
case .mat2Map:
LessonListScreen(
vm: vm,
title: vm.tr("mat_year2"),
subtitle: vm.tr("mat2_sub"),
rows: vm.content.mat2.map { l in
let id = l.int("id")
return LessonRow(id: "m2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat2Done(id), destination: .mat2Lesson(id))
}
)
case .mat2Lesson(let id):
if let l = vm.content.mat2Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat2Ex(id))
}
case .mat2Ex(let id):
MathPracticeScreen(vm: vm, year: 2, id: id)
case .mat3Map:
LessonListScreen(
vm: vm,
title: vm.tr("mat_year3"),
subtitle: vm.tr("mat3_sub"),
rows: vm.content.mat3.map { l in
let id = l.int("id")
return LessonRow(id: "m3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat3Done(id), destination: .mat3Lesson(id))
}
)
case .mat3Lesson(let id):
if let l = vm.content.mat3Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat3Ex(id))
}
case .mat3Ex(let id):
MathPracticeScreen(vm: vm, year: 3, id: id)
case .mat4Map:
LessonListScreen(
vm: vm,
title: vm.tr("mat_year4"),
subtitle: vm.tr("mat4_sub"),
rows: vm.content.mat4.map { l in
let id = l.int("id")
return LessonRow(id: "m4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat4Done(id), destination: .mat4Lesson(id))
}
)
case .mat4Lesson(let id):
if let l = vm.content.mat4Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat4Ex(id))
}
case .mat4Ex(let id):
MathPracticeScreen(vm: vm, year: 4, id: id)
case .fyzYears:
FyzYearsScreen(vm: vm)
case .fyzMap: