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

@ -69,6 +69,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
- **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov - **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov
středních škol), mluvnice (20 cvičení) a maturitní četba středních škol), mluvnice (20 cvičení) a maturitní četba
(82 knih: zápisky, kvíz, sestavení děje) (82 knih: zápisky, kvíz, sestavení děje)
- **Matematika** — opakování základní školy a 4 ročníky, 40 lekcí;
příklady se píšou a kontrolují, u geometrie jde i rýsovat na mřížku
- **Fyzika** — 4 ročníky, 32 lekcí podle osnov středních škol - **Fyzika** — 4 ročníky, 32 lekcí podle osnov středních škol
(mechanika, teplo a vlny, elektřina a magnetismus, optika až astrofyzika) (mechanika, teplo a vlny, elektřina a magnetismus, optika až astrofyzika)
- **Základy přírodopisných věd** — chemie a biologie, v každé 8 lekcí - **Základy přírodopisných věd** — chemie a biologie, v každé 8 lekcí
@ -82,7 +84,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
### Použití ### Použití
1. **Pokračuj** otevře mapu předmětů. 1. **Pokračuj** otevře mapu předmětů.
2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy, atom, baňka) vede na učební cestu. 2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka, váhy, funkce, atom, baňka) vede na učební cestu.
3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají. 3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají.
4. **Hledat** (lupa), **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře. 4. **Hledat** (lupa), **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře.
@ -270,6 +272,8 @@ Intel Mac, another architecture, or hacking on the code →
- **Czech language** — literature (4 years, 32 lessons from the secondary-school - **Czech language** — literature (4 years, 32 lessons from the secondary-school
syllabus), grammar (20 exercises) and required reading syllabus), grammar (20 exercises) and required reading
(82 books: notes, quiz, plot ordering) (82 books: notes, quiz, plot ordering)
- **Mathematics** — elementary-school review and 4 years, 40 lessons;
examples are typed and checked, and some geometry is drawn on a grid
- **Physics** — 4 years, 32 lessons from the secondary-school syllabus - **Physics** — 4 years, 32 lessons from the secondary-school syllabus
(mechanics, heat and waves, electricity and magnetism, optics through astrophysics) (mechanics, heat and waves, electricity and magnetism, optics through astrophysics)
- **Natural sciences** — chemistry and biology, eight lessons each - **Natural sciences** — chemistry and biology, eight lessons each
@ -284,7 +288,7 @@ Intel Mac, another architecture, or hacking on the code →
### Usage ### Usage
1. **Continue** opens the subject map. 1. **Continue** opens the subject map.
2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales, atom, flask) opens its path. 2. An open subject (flag, Wi‑Fi, chip, Czech flag, scales, function, atom, flask) opens its path.
3. Exercise / lesson nodes open content; locked nodes do nothing. 3. Exercise / lesson nodes open content; locked nodes do nothing.
4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right. 4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right.

View file

@ -191,6 +191,31 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
refresh() refresh()
} }
fun markMat0(id: Int) {
progress.markMat0(id)
refresh()
}
fun markMat(id: Int) {
progress.markMat(id)
refresh()
}
fun markMat2(id: Int) {
progress.markMat2(id)
refresh()
}
fun markMat3(id: Int) {
progress.markMat3(id)
refresh()
}
fun markMat4(id: Int) {
progress.markMat4(id)
refresh()
}
fun markFyz4(id: Int) { fun markFyz4(id: Int) {
progress.markFyz4(id) progress.markFyz4(id)
refresh() refresh()

View file

@ -33,6 +33,8 @@ import org.maturita.maturita.ui.HwQuizScreen
import org.maturita.maturita.ui.HwYearsScreen import org.maturita.maturita.ui.HwYearsScreen
import org.maturita.maturita.ui.LessonListScreen import org.maturita.maturita.ui.LessonListScreen
import org.maturita.maturita.ui.FyzQuizScreen import org.maturita.maturita.ui.FyzQuizScreen
import org.maturita.maturita.ui.MathPracticeScreen
import org.maturita.maturita.ui.MatYearsScreen
import org.maturita.maturita.ui.FyzYearsScreen import org.maturita.maturita.ui.FyzYearsScreen
import org.maturita.maturita.ui.LitYearsScreen import org.maturita.maturita.ui.LitYearsScreen
import org.maturita.maturita.ui.LessonRow import org.maturita.maturita.ui.LessonRow
@ -350,6 +352,77 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.Lit4Ex -> LitQuizScreen(vm, 4, r.id) is Route.Lit4Ex -> LitQuizScreen(vm, 4, r.id)
Route.MatYears -> MatYearsScreen(vm)
Route.Mat0Map -> LessonListScreen(
vm, vm.tr("mat0_year"), vm.tr("mat0_sub"),
vm.content.mat0.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat0Done(id), Route.Mat0Lesson(id))
},
)
is Route.Mat0Lesson -> {
val l = vm.content.mat0Lesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Mat0Ex(r.id))
}
}
is Route.Mat0Ex -> MathPracticeScreen(vm, 0, r.id)
Route.MatMap -> LessonListScreen(
vm, vm.tr("mat_year1"), vm.tr("mat_sub"),
vm.content.mat.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.matDone(id), Route.MatLesson(id))
},
)
is Route.MatLesson -> {
val l = vm.content.matLesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.MatEx(r.id))
}
}
is Route.MatEx -> MathPracticeScreen(vm, 1, r.id)
Route.Mat2Map -> LessonListScreen(
vm, vm.tr("mat_year2"), vm.tr("mat2_sub"),
vm.content.mat2.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat2Done(id), Route.Mat2Lesson(id))
},
)
is Route.Mat2Lesson -> {
val l = vm.content.mat2Lesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Mat2Ex(r.id))
}
}
is Route.Mat2Ex -> MathPracticeScreen(vm, 2, r.id)
Route.Mat3Map -> LessonListScreen(
vm, vm.tr("mat_year3"), vm.tr("mat3_sub"),
vm.content.mat3.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat3Done(id), Route.Mat3Lesson(id))
},
)
is Route.Mat3Lesson -> {
val l = vm.content.mat3Lesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Mat3Ex(r.id))
}
}
is Route.Mat3Ex -> MathPracticeScreen(vm, 3, r.id)
Route.Mat4Map -> LessonListScreen(
vm, vm.tr("mat_year4"), vm.tr("mat4_sub"),
vm.content.mat4.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat4Done(id), Route.Mat4Lesson(id))
},
)
is Route.Mat4Lesson -> {
val l = vm.content.mat4Lesson(r.id)
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
vm.replace(Route.Mat4Ex(r.id))
}
}
is Route.Mat4Ex -> MathPracticeScreen(vm, 4, r.id)
Route.FyzYears -> FyzYearsScreen(vm) Route.FyzYears -> FyzYearsScreen(vm)
Route.FyzMap -> LessonListScreen( Route.FyzMap -> LessonListScreen(
vm, vm.tr("fyz_year1"), vm.tr("fyz_sub"), vm, vm.tr("fyz_year1"), vm.tr("fyz_sub"),

View file

@ -50,6 +50,22 @@ sealed class Route(val page: String) {
data object BioMap : Route("biomap") data object BioMap : Route("biomap")
data class BioLesson(val id: Int) : Route("biounit$id") data class BioLesson(val id: Int) : Route("biounit$id")
data class BioEx(val id: Int) : Route("bioex$id") data class BioEx(val id: Int) : Route("bioex$id")
data object MatYears : Route("matyears")
data object Mat0Map : Route("mat0map")
data class Mat0Lesson(val id: Int) : Route("mat0unit$id")
data class Mat0Ex(val id: Int) : Route("mat0ex$id")
data object MatMap : Route("matmap")
data class MatLesson(val id: Int) : Route("matunit$id")
data class MatEx(val id: Int) : Route("matex$id")
data object Mat2Map : Route("mat2map")
data class Mat2Lesson(val id: Int) : Route("mat2unit$id")
data class Mat2Ex(val id: Int) : Route("mat2ex$id")
data object Mat3Map : Route("mat3map")
data class Mat3Lesson(val id: Int) : Route("mat3unit$id")
data class Mat3Ex(val id: Int) : Route("mat3ex$id")
data object Mat4Map : Route("mat4map")
data class Mat4Lesson(val id: Int) : Route("mat4unit$id")
data class Mat4Ex(val id: Int) : Route("mat4ex$id")
data object FyzYears : Route("fyzyears") data object FyzYears : Route("fyzyears")
data object FyzMap : Route("fyzmap") data object FyzMap : Route("fyzmap")
data class FyzLesson(val id: Int) : Route("fyzunit$id") data class FyzLesson(val id: Int) : Route("fyzunit$id")
@ -83,7 +99,10 @@ sealed class Route(val page: String) {
this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex || this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex ||
this is ChemLesson || this is ChemEx || this is BioLesson || this is BioEx || this is ChemLesson || this is ChemEx || this is BioLesson || this is BioEx ||
this is FyzLesson || this is FyzEx || this is Fyz2Lesson || this is Fyz2Ex || this is FyzLesson || this is FyzEx || this is Fyz2Lesson || this is Fyz2Ex ||
this is Fyz3Lesson || this is Fyz3Ex || this is Fyz4Lesson || this is Fyz4Ex this is Fyz3Lesson || this is Fyz3Ex || this is Fyz4Lesson || this is Fyz4Ex ||
this is Mat0Lesson || this is Mat0Ex || this is MatLesson || this is MatEx ||
this is Mat2Lesson || this is Mat2Ex || this is Mat3Lesson || this is Mat3Ex ||
this is Mat4Lesson || this is Mat4Ex
/** Full stack from the practice root to this screen, so back always goes up one level. */ /** Full stack from the practice root to this screen, so back always goes up one level. */
fun stack(): List<Route> = when (this) { fun stack(): List<Route> = when (this) {
@ -135,6 +154,22 @@ sealed class Route(val page: String) {
BioMap -> listOf(Subjects, SciMap, BioMap) BioMap -> listOf(Subjects, SciMap, BioMap)
is BioLesson -> listOf(Subjects, SciMap, BioMap, this) is BioLesson -> listOf(Subjects, SciMap, BioMap, this)
is BioEx -> listOf(Subjects, SciMap, BioMap, BioLesson(id), this) is BioEx -> listOf(Subjects, SciMap, BioMap, BioLesson(id), this)
MatYears -> listOf(Subjects, MatYears)
Mat0Map -> listOf(Subjects, MatYears, Mat0Map)
is Mat0Lesson -> listOf(Subjects, MatYears, Mat0Map, this)
is Mat0Ex -> listOf(Subjects, MatYears, Mat0Map, Mat0Lesson(id), this)
MatMap -> listOf(Subjects, MatYears, MatMap)
is MatLesson -> listOf(Subjects, MatYears, MatMap, this)
is MatEx -> listOf(Subjects, MatYears, MatMap, MatLesson(id), this)
Mat2Map -> listOf(Subjects, MatYears, Mat2Map)
is Mat2Lesson -> listOf(Subjects, MatYears, Mat2Map, this)
is Mat2Ex -> listOf(Subjects, MatYears, Mat2Map, Mat2Lesson(id), this)
Mat3Map -> listOf(Subjects, MatYears, Mat3Map)
is Mat3Lesson -> listOf(Subjects, MatYears, Mat3Map, this)
is Mat3Ex -> listOf(Subjects, MatYears, Mat3Map, Mat3Lesson(id), this)
Mat4Map -> listOf(Subjects, MatYears, Mat4Map)
is Mat4Lesson -> listOf(Subjects, MatYears, Mat4Map, this)
is Mat4Ex -> listOf(Subjects, MatYears, Mat4Map, Mat4Lesson(id), this)
FyzYears -> listOf(Subjects, FyzYears) FyzYears -> listOf(Subjects, FyzYears)
FyzMap -> listOf(Subjects, FyzYears, FyzMap) FyzMap -> listOf(Subjects, FyzYears, FyzMap)
is FyzLesson -> listOf(Subjects, FyzYears, FyzMap, this) is FyzLesson -> listOf(Subjects, FyzYears, FyzMap, this)
@ -181,6 +216,12 @@ fun routeFromPage(page: String): Route? = when (page) {
"scimap" -> Route.SciMap "scimap" -> Route.SciMap
"chemmap" -> Route.ChemMap "chemmap" -> Route.ChemMap
"biomap" -> Route.BioMap "biomap" -> Route.BioMap
"matyears" -> Route.MatYears
"mat0map" -> Route.Mat0Map
"matmap" -> Route.MatMap
"mat2map" -> Route.Mat2Map
"mat3map" -> Route.Mat3Map
"mat4map" -> Route.Mat4Map
"fyzyears" -> Route.FyzYears "fyzyears" -> Route.FyzYears
"fyzmap" -> Route.FyzMap "fyzmap" -> Route.FyzMap
"fyz2map" -> Route.Fyz2Map "fyz2map" -> Route.Fyz2Map
@ -229,6 +270,36 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""bioex(\d+)""").matchEntire(page)?.let { Regex("""bioex(\d+)""").matchEntire(page)?.let {
return Route.BioEx(it.groupValues[1].toInt()) return Route.BioEx(it.groupValues[1].toInt())
} }
Regex("""mat0unit(\d+)""").matchEntire(page)?.let {
return Route.Mat0Lesson(it.groupValues[1].toInt())
}
Regex("""mat0ex(\d+)""").matchEntire(page)?.let {
return Route.Mat0Ex(it.groupValues[1].toInt())
}
Regex("""mat4unit(\d+)""").matchEntire(page)?.let {
return Route.Mat4Lesson(it.groupValues[1].toInt())
}
Regex("""mat4ex(\d+)""").matchEntire(page)?.let {
return Route.Mat4Ex(it.groupValues[1].toInt())
}
Regex("""mat3unit(\d+)""").matchEntire(page)?.let {
return Route.Mat3Lesson(it.groupValues[1].toInt())
}
Regex("""mat3ex(\d+)""").matchEntire(page)?.let {
return Route.Mat3Ex(it.groupValues[1].toInt())
}
Regex("""mat2unit(\d+)""").matchEntire(page)?.let {
return Route.Mat2Lesson(it.groupValues[1].toInt())
}
Regex("""mat2ex(\d+)""").matchEntire(page)?.let {
return Route.Mat2Ex(it.groupValues[1].toInt())
}
Regex("""matunit(\d+)""").matchEntire(page)?.let {
return Route.MatLesson(it.groupValues[1].toInt())
}
Regex("""matex(\d+)""").matchEntire(page)?.let {
return Route.MatEx(it.groupValues[1].toInt())
}
Regex("""fyz4unit(\d+)""").matchEntire(page)?.let { Regex("""fyz4unit(\d+)""").matchEntire(page)?.let {
return Route.Fyz4Lesson(it.groupValues[1].toInt()) return Route.Fyz4Lesson(it.groupValues[1].toInt())
} }

View file

@ -63,3 +63,656 @@ fun hasUmlaut(s: String?): Boolean {
return s.any { it == 'ä' || it == 'ö' || it == 'ü' || it == 'ß' || return s.any { it == 'ä' || it == 'ö' || it == 'ü' || it == 'ß' ||
it == 'Ä' || it == 'Ö' || it == 'Ü' } it == 'Ä' || it == 'Ö' || it == 'Ü' }
} }
// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer.
private data class MFrac(val n: Long = 0, val d: Long = 1)
private class MTerm(
var ok: Boolean = true,
var a: MFrac = MFrac(),
var b: MFrac = MFrac(),
var rad: Long = 0,
var p: MFrac = MFrac(),
)
private fun mGcd(a0: Long, b0: Long): Long {
var a = if (a0 < 0) -a0 else a0
var b = if (b0 < 0) -b0 else b0
while (b != 0L) {
val t = a % b
a = b
b = t
}
return if (a == 0L) 1 else a
}
private fun mMul(a: Long, b: Long): Long? = try {
Math.multiplyExact(a, b)
} catch (_: ArithmeticException) {
null
}
private fun mAddL(a: Long, b: Long): Long? = try {
Math.addExact(a, b)
} catch (_: ArithmeticException) {
null
}
private fun mSet(n0: Long, d0: Long): MFrac? {
if (d0 == 0L) return null
var n = n0
var d = d0
if (d < 0) {
n = -n
d = -d
}
val g = mGcd(n, d)
return MFrac(n / g, d / g)
}
private fun mAddF(x: MFrac, y: MFrac): MFrac? {
val xn = mMul(x.n, y.d) ?: return null
val yn = mMul(y.n, x.d) ?: return null
val d = mMul(x.d, y.d) ?: return null
val s = mAddL(xn, yn) ?: return null
return mSet(s, d)
}
private fun mMulF(x: MFrac, y: MFrac): MFrac? {
val n = mMul(x.n, y.n) ?: return null
val d = mMul(x.d, y.d) ?: return null
return mSet(n, d)
}
private fun mDivF(x: MFrac, y: MFrac): MFrac? {
if (y.n == 0L) return null
return mMulF(x, MFrac(y.d, y.n))
}
private fun mSameF(x: MFrac, y: MFrac) = x.n == y.n && x.d == y.d
private fun mPow(b: Long, e: Int): Long {
var r = 1L
var i = 0
while (i < e) {
r *= b
i++
}
return r
}
private fun mSqrtReduce(n0: Long): Pair<Long, Long>? {
if (n0 < 0 || n0 > 1_000_000) return null
var n = n0
var out = 1L
var f = 2L
while (f * f <= n) {
var c = 0
while (n % f == 0L) {
n /= f
c++
}
if (c >= 2) out *= mPow(f, c / 2)
if (c % 2 == 1) n *= f
f = if (f == 2L) 3 else f + 2
if (f > 10000) break
}
return out to n
}
private fun mNorm(t: MTerm) {
if (!t.ok) return
if (t.rad > 1) {
val s = mSqrtReduce(t.rad) ?: run { t.ok = false; return }
t.b = mMulF(t.b, MFrac(s.first, 1)) ?: run { t.ok = false; return }
t.rad = s.second
}
if (t.rad <= 1) {
t.a = mAddF(t.a, t.b) ?: run { t.ok = false; return }
t.b = MFrac()
t.rad = 0
}
if (t.b.n == 0L) t.rad = 0
}
private fun mFail() = MTerm(ok = false)
private fun mAdd(x: MTerm, y: MTerm): MTerm {
if (!x.ok || !y.ok) return mFail()
val o = MTerm()
o.a = mAddF(x.a, y.a) ?: return mFail()
o.p = mAddF(x.p, y.p) ?: return mFail()
if (x.rad == 0L && y.rad == 0L) {
o.b = MFrac()
o.rad = 0
} else if (x.rad == 0L) {
o.b = y.b
o.rad = y.rad
} else if (y.rad == 0L || x.rad == y.rad) {
o.b = mAddF(x.b, y.b) ?: return mFail()
o.rad = x.rad
} else return mFail()
mNorm(o)
return o
}
private fun mScale(x: MTerm, f: MFrac): MTerm {
if (!x.ok) return mFail()
val o = MTerm()
o.a = mMulF(x.a, f) ?: return mFail()
o.b = mMulF(x.b, f) ?: return mFail()
o.p = mMulF(x.p, f) ?: return mFail()
o.rad = x.rad
mNorm(o)
return o
}
private fun mMulT(x: MTerm, y: MTerm): MTerm {
if (!x.ok || !y.ok) return mFail()
if ((x.p.n != 0L && (y.p.n != 0L || y.rad != 0L)) ||
(y.p.n != 0L && (x.p.n != 0L || x.rad != 0L))
) return mFail()
if (x.rad != 0L && y.rad != 0L && x.rad != y.rad) return mFail()
val o = MTerm()
o.a = mMulF(x.a, y.a) ?: return mFail()
if (x.rad == 0L && y.rad == 0L) {
o.b = MFrac()
o.rad = 0
} else if (x.rad == 0L) {
o.b = mMulF(x.a, y.b) ?: return mFail()
o.rad = y.rad
} else if (y.rad == 0L) {
o.b = mMulF(y.a, x.b) ?: return mFail()
o.rad = x.rad
} else {
val ab = mMulF(x.a, y.b) ?: return mFail()
val ba = mMulF(x.b, y.a) ?: return mFail()
val cross = mAddF(ab, ba) ?: return mFail()
val sq = mMul(x.rad, y.rad) ?: return mFail()
val bb = mMulF(x.b, y.b) ?: return mFail()
val extra = mMulF(bb, MFrac(sq, 1)) ?: return mFail()
o.a = mAddF(o.a, extra) ?: return mFail()
o.b = cross
o.rad = x.rad
}
o.p = if (x.p.n != 0L) x.p else y.p
if ((y.a.n != 1L || y.a.d != 1L || y.b.n != 0L || y.rad != 0L || y.p.n != 0L) && x.p.n != 0L) {
o.p = mMulF(x.p, y.a) ?: return mFail()
}
if ((x.a.n != 1L || x.a.d != 1L || x.b.n != 0L || x.rad != 0L || x.p.n != 0L) &&
y.p.n != 0L && x.p.n == 0L
) {
o.p = mMulF(y.p, x.a) ?: return mFail()
}
mNorm(o)
return o
}
private fun mDivT(x: MTerm, y: MTerm): MTerm {
if (!x.ok || !y.ok || y.p.n != 0L) return mFail()
if (y.rad == 0L) {
val f = mDivF(MFrac(1, 1), y.a) ?: return mFail()
return mScale(x, f)
}
if (x.p.n != 0L) return mFail()
val yb = mMulF(y.b, MFrac(y.rad, 1)) ?: return mFail()
val bb = mMulF(y.b, yb) ?: return mFail()
val aa = mMulF(y.a, y.a) ?: return mFail()
val den = mAddF(aa, MFrac(-bb.n, bb.d)) ?: return mFail()
val num = MTerm()
num.a = y.a
num.b = MFrac(-y.b.n, y.b.d)
num.rad = y.rad
val inv = mDivF(MFrac(1, 1), den) ?: return mFail()
return mMulT(x, mScale(num, inv))
}
private sealed class MTok {
class Num(val n: Long, val d: Long) : MTok()
object Sqrt : MTok()
object Pi : MTok()
object Add : MTok()
object Sub : MTok()
object Mul : MTok()
object Div : MTok()
object Lp : MTok()
object Rp : MTok()
}
private fun mLex(s: String): List<MTok>? {
val toks = ArrayList<MTok>()
var i = 0
fun peek(k: Int) = if (i + k < s.length) s[i + k] else null
while (i < s.length) {
while (i < s.length && s[i].isWhitespace()) i++
if (i >= s.length) break
val ch = s[i]
if (ch.isDigit() || ((ch == '.' || ch == ',') && peek(1)?.isDigit() == true)) {
var whole = 0L
var saw = false
while (i < s.length && s[i].isDigit()) {
saw = true
whole = whole * 10 + (s[i] - '0')
i++
}
if (i < s.length && (s[i] == '.' || s[i] == ',') && i + 1 < s.length && s[i + 1].isDigit()) {
i++
var frac = 0L
var div = 1L
while (i < s.length && s[i].isDigit()) {
frac = frac * 10 + (s[i] - '0')
div *= 10
i++
}
val n = mAddL(mMul(whole, div) ?: return null, frac) ?: return null
toks.add(MTok.Num(n, div))
} else if (saw) {
toks.add(MTok.Num(whole, 1))
} else return null
continue
}
when (ch) {
'+' -> toks.add(MTok.Add)
'-', '\u2212', '\u2013' -> toks.add(MTok.Sub)
'*', '\u00D7', '\u00B7' -> toks.add(MTok.Mul)
'/', ':' -> toks.add(MTok.Div)
'(' -> toks.add(MTok.Lp)
')' -> toks.add(MTok.Rp)
'\u03C0' -> toks.add(MTok.Pi)
'\u221A' -> toks.add(MTok.Sqrt)
else -> {
if (ch in 'A'..'Z' || ch in 'a'..'z') {
val start = i
while (i < s.length && (s[i] in 'A'..'Z' || s[i] in 'a'..'z')) i++
when (s.substring(start, i).lowercase()) {
"pi" -> toks.add(MTok.Pi)
"sqrt" -> toks.add(MTok.Sqrt)
else -> return null
}
continue
}
return null
}
}
i++
}
return toks
}
private class MParse(val t: List<MTok>) {
var i = 0
fun peek() = if (i < t.size) t[i] else null
fun take(): MTok? = if (i < t.size) t[i++] else null
fun at(k: Int) = if (k in t.indices) t[k] else null
fun expr(): MTerm? {
var left = term() ?: return null
while (true) {
val p = peek()
if (p !is MTok.Add && p !is MTok.Sub) break
take()
val r = term() ?: return null
left = if (p is MTok.Sub) mAdd(left, mScale(r, MFrac(-1, 1))) else mAdd(left, r)
if (!left.ok) return null
}
return left
}
fun term(): MTerm? {
var left = unary() ?: return null
while (true) {
val p = peek()
if (p !is MTok.Mul && p !is MTok.Div) break
take()
val r = unary() ?: return null
left = if (p is MTok.Div) mDivT(left, r) else mMulT(left, r)
if (!left.ok) return null
}
return left
}
fun unary(): MTerm? {
if (peek() is MTok.Sub) {
take()
val v = unary() ?: return null
return mScale(v, MFrac(-1, 1))
}
if (peek() is MTok.Add) {
take()
return unary()
}
return factor()
}
fun factor(): MTerm? {
var left = primary() ?: return null
while (true) {
when (peek()) {
is MTok.Sqrt, is MTok.Pi, is MTok.Lp -> {
val r = primary() ?: return null
left = mMulT(left, r)
if (!left.ok) return null
}
is MTok.Num -> return null
else -> return left
}
}
}
fun primary(): MTerm? {
when (val p = take()) {
is MTok.Num -> {
val n = p.n
val d = p.d
if (peek() is MTok.Div && at(i + 1) is MTok.Num) {
take()
val n2 = take() as? MTok.Num ?: return null
if (n2.n <= 0 || n2.d != 1L || d != 1L) return null
val o = MTerm()
o.a = mSet(n, n2.n) ?: return null
return o
}
if (d == 1L && n >= 0 && peek() is MTok.Num) {
val saved = i
val n2 = take() as MTok.Num
val n3tok = at(i + 1)
if (peek() is MTok.Div && n3tok is MTok.Num && n2.d == 1L && n3tok.d == 1L && n2.n >= 0 && n3tok.n > 0) {
take()
take()
val frac = mSet(n2.n, n3tok.n) ?: return null
val whole = MTerm()
whole.a = MFrac(n, 1)
val part = MTerm()
part.a = frac
return mAdd(whole, part)
}
i = saved
}
val o = MTerm()
o.a = mSet(n, d) ?: return null
return o
}
is MTok.Pi -> {
val o = MTerm()
o.p = MFrac(1, 1)
return o
}
is MTok.Sqrt -> {
if (peek() is MTok.Lp) {
take()
val inner = expr() ?: return null
if (take() !is MTok.Rp) return null
return mSqrtTerm(inner)
}
val num = peek()
if (num is MTok.Num && num.d == 1L && num.n >= 0) {
take()
val inner = MTerm()
inner.a = MFrac(num.n, 1)
return mSqrtTerm(inner)
}
return null
}
is MTok.Lp -> {
val inner = expr() ?: return null
if (take() !is MTok.Rp) return null
return inner
}
else -> return null
}
}
}
private fun mSqrtTerm(inner: MTerm): MTerm? {
if (!inner.ok || inner.p.n != 0L || inner.rad != 0L || inner.b.n != 0L) return null
if (inner.a.n < 0 || inner.a.d != 1L) return null
val s = mSqrtReduce(inner.a.n) ?: return null
val o = MTerm()
o.b = MFrac(s.first, 1)
o.rad = s.second
mNorm(o)
return if (o.ok) o else null
}
private fun mStripUnits(raw: String): String {
var s = raw.trim()
val units = listOf("procenta", "procent", "cm", "mm", "dm", "km", "m")
var cut = true
while (cut) {
cut = false
s = s.trimEnd()
if (s.endsWith("°") || s.endsWith("%") || s.endsWith(".")) {
s = s.dropLast(1)
cut = true
continue
}
for (u in units) {
if (s.length <= u.length) continue
if (s.lowercase().endsWith(u)) {
val keep = s.dropLast(u.length)
if (keep.lastOrNull()?.isLetter() == true) continue
s = keep
cut = true
break
}
}
}
return s.trim()
}
private fun mPrep(raw: String): String {
var s = raw.trim()
val pairs = listOf('(' to ')', '[' to ']', '{' to '}')
while (s.length >= 2 && pairs.any { it.first == s.first() && it.second == s.last() }) {
s = s.substring(1, s.length - 1).trim()
}
return mStripUnits(s).trim()
}
private fun mParseTerm(s: String): MTerm? {
val text = mPrep(s)
if (text.isEmpty()) return null
val toks = mLex(text) ?: return null
if (toks.isEmpty()) return null
val p = MParse(toks)
val v = p.expr() ?: return null
if (p.i != toks.size || !v.ok) return null
return v
}
private fun mExact(x: MTerm, y: MTerm) =
x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p)
private fun mDouble(t: MTerm): Double {
var v = t.a.n.toDouble() / t.a.d + t.p.n.toDouble() / t.p.d * Math.PI
if (t.b.n != 0L && t.rad > 0) v += t.b.n.toDouble() / t.b.d * Math.sqrt(t.rad.toDouble())
return v
}
private fun mClose(x: MTerm, y: MTerm): Boolean {
if (!x.ok || !y.ok) return false
if (mExact(x, y)) return true
if (x.p.n != 0L || y.p.n != 0L) return false
val diff = Math.abs(mDouble(x) - mDouble(y))
return if (x.b.n != 0L || y.b.n != 0L) diff <= 0.001 else diff <= 0.0005
}
private fun mFold(s: String): String {
val out = StringBuilder()
for (ch in s.lowercase()) {
when (ch) {
'á', 'à', 'ä', 'â' -> out.append('a')
'č', 'ć' -> out.append('c')
'ď' -> out.append('d')
'é', 'ě', 'ë', 'ê' -> out.append('e')
'í', 'ï' -> out.append('i')
'ň' -> out.append('n')
'ó', 'ö', 'ô' -> out.append('o')
'ř' -> out.append('r')
'š', 'ś' -> out.append('s')
'ť' -> out.append('t')
'ú', 'ů', 'ü' -> out.append('u')
'ý' -> out.append('y')
'ž', 'ź' -> out.append('z')
else -> if (ch.isLetter() || ch.isDigit()) out.append(ch)
}
}
return out.toString()
}
private fun mSplit(raw: String): List<String>? {
val s = raw.trim()
if (s.isEmpty()) return null
val semi = s.contains(';')
val parts = ArrayList<String>()
var rest = s
while (rest.isNotEmpty()) {
val idx = if (semi) rest.indexOf(';') else rest.indexOf(", ")
val step = if (semi) 1 else 2
val piece: String
val next: String
if (idx < 0) {
piece = rest.trim()
next = ""
} else {
piece = rest.substring(0, idx).trim()
next = rest.substring(idx + step)
}
if (piece.isEmpty()) return null
parts.add(piece)
if (idx < 0) break
rest = next
}
return parts.ifEmpty { null }
}
fun mathAnswerOk(spec: String?, user: String?): Boolean {
if (spec == null || user == null) return false
val bodySpec = spec.trim()
if (bodySpec.startsWith("word:")) {
val opts = bodySpec.removePrefix("word:").split('|').map { mFold(it) }
return opts.contains(mFold(user))
}
if (bodySpec.startsWith("set:") || bodySpec.startsWith("pair:")) {
val ordered = bodySpec.startsWith("pair:")
val body = bodySpec.removePrefix(if (ordered) "pair:" else "set:")
val want = mSplit(body) ?: return false
val got = mSplit(user) ?: return false
if (want.size != got.size || want.isEmpty()) return false
val wt = want.map { mParseTerm(it) ?: return false }
val gt = got.map { mParseTerm(it) ?: return false }
if (ordered) return wt.indices.all { mClose(wt[it], gt[it]) }
val used = BooleanArray(gt.size)
for (w in wt) {
var hit = gt.indices.firstOrNull { !used[it] && mExact(w, gt[it]) }
if (hit == null) hit = gt.indices.firstOrNull { !used[it] && mClose(w, gt[it]) }
if (hit == null) return false
used[hit] = true
}
return true
}
if (!bodySpec.startsWith("num:")) return false
val tw = mParseTerm(bodySpec.removePrefix("num:")) ?: return false
val tg = mParseTerm(user) ?: return false
return mClose(tw, tg)
}
fun mathIsDraw(spec: String?): Boolean {
if (spec == null) return false
return spec.startsWith("square:") || spec.startsWith("mid:") || spec.startsWith("right:") ||
spec.startsWith("para:") || spec.startsWith("points:")
}
fun mathDrawNeed(spec: String?): Int {
if (spec == null) return 0
if (spec.startsWith("square:")) return 2
if (spec.startsWith("mid:") || spec.startsWith("right:") || spec.startsWith("para:")) return 1
if (spec.startsWith("points:")) {
return spec.removePrefix("points:").split(';').count { !it.startsWith("fix:") }
}
return 0
}
private fun mParseXY(s: String): Pair<Int, Int>? {
val bits = s.trim().split(',')
if (bits.size != 2) return null
val x = bits[0].trim().toIntOrNull() ?: return null
val y = bits[1].trim().toIntOrNull() ?: return null
return x to y
}
fun mathDrawGiven(spec: String?): List<Pair<Int, Int>> {
if (spec == null) return emptyList()
if (spec.startsWith("points:")) {
return spec.removePrefix("points:").split(';').mapNotNull { part ->
if (!part.startsWith("fix:")) null else mParseXY(part.removePrefix("fix:"))
}
}
val body: String
val n: Int
when {
spec.startsWith("para:") -> { body = spec.removePrefix("para:"); n = 3 }
spec.startsWith("square:") -> { body = spec.removePrefix("square:"); n = 2 }
spec.startsWith("right:") -> { body = spec.removePrefix("right:"); n = 2 }
spec.startsWith("mid:") -> { body = spec.removePrefix("mid:"); n = 2 }
else -> return emptyList()
}
return body.split(';').take(n).mapNotNull { mParseXY(it) }
}
private fun mOnBoard(p: Pair<Int, Int>) = p.first in 0..10 && p.second in 0..8
fun mathDrawOk(spec: String?, pts: List<Pair<Int, Int>>): Boolean {
if (spec == null || mathDrawNeed(spec) != pts.size) return false
if (pts.any { !mOnBoard(it) }) return false
if (pts.map { "${it.first},${it.second}" }.toSet().size != pts.size) return false
val g = mathDrawGiven(spec)
if (spec.startsWith("mid:")) {
if (g.size != 2 || (g[0].first + g[1].first) % 2 != 0 || (g[0].second + g[1].second) % 2 != 0) return false
return pts[0] == (g[0].first + g[1].first) / 2 to (g[0].second + g[1].second) / 2
}
if (spec.startsWith("para:")) {
if (g.size != 3) return false
return pts[0] == g[1].first + g[2].first - g[0].first to g[1].second + g[2].second - g[0].second
}
if (spec.startsWith("right:")) {
if (g.size != 2) return false
val c = pts[0]
if (c == g[0] || c == g[1]) return false
val ux = g[0].first - c.first
val uy = g[0].second - c.second
val vx = g[1].first - c.first
val vy = g[1].second - c.second
return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0)
}
if (spec.startsWith("square:")) {
if (g.size != 2 || pts.size != 2) return false
val a = g[0]
val b = g[1]
val dx = b.first - a.first
val dy = b.second - a.second
if (dx == 0 && dy == 0) return false
val c1 = b.first - dy to b.second + dx
val d1 = a.first - dy to a.second + dx
val c2 = b.first + dy to b.second - dx
val d2 = a.first + dy to a.second - dx
fun same(p: Pair<Int, Int>, q: Pair<Int, Int>, r: Pair<Int, Int>, s: Pair<Int, Int>) =
(p == r && q == s) || (p == s && q == r)
if ((!mOnBoard(c1) || !mOnBoard(d1)) && (!mOnBoard(c2) || !mOnBoard(d2))) return false
return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2)
}
if (spec.startsWith("points:")) {
val want = spec.removePrefix("points:").split(';').mapNotNull { part ->
if (part.startsWith("fix:")) null else mParseXY(part)
}
if (want.size != pts.size) return false
val used = BooleanArray(pts.size)
for (w in want) {
val j = pts.indices.firstOrNull { !used[it] && pts[it] == w } ?: return false
used[j] = true
}
return true
}
return false
}

View file

@ -43,6 +43,11 @@ class Content(root: JSONObject) {
val fyz2 = raw.arr("fyz2") val fyz2 = raw.arr("fyz2")
val fyz3 = raw.arr("fyz3") val fyz3 = raw.arr("fyz3")
val fyz4 = raw.arr("fyz4") val fyz4 = raw.arr("fyz4")
val mat0 = raw.arr("mat0")
val mat = raw.arr("mat")
val mat2 = raw.arr("mat2")
val mat3 = raw.arr("mat3")
val mat4 = raw.arr("mat4")
val mluvnice = raw.arr("mluvnice") val mluvnice = raw.arr("mluvnice")
val books = raw.arr("books") val books = raw.arr("books")
val changelog = raw.arr("changelog") val changelog = raw.arr("changelog")
@ -64,6 +69,11 @@ class Content(root: JSONObject) {
fun fyz2Lesson(id: Int) = fyz2.firstOrNull { it.int("id") == id } fun fyz2Lesson(id: Int) = fyz2.firstOrNull { it.int("id") == id }
fun fyz3Lesson(id: Int) = fyz3.firstOrNull { it.int("id") == id } fun fyz3Lesson(id: Int) = fyz3.firstOrNull { it.int("id") == id }
fun fyz4Lesson(id: Int) = fyz4.firstOrNull { it.int("id") == id } fun fyz4Lesson(id: Int) = fyz4.firstOrNull { it.int("id") == id }
fun mat0Lesson(id: Int) = mat0.firstOrNull { it.int("id") == id }
fun matLesson(id: Int) = mat.firstOrNull { it.int("id") == id }
fun mat2Lesson(id: Int) = mat2.firstOrNull { it.int("id") == id }
fun mat3Lesson(id: Int) = mat3.firstOrNull { it.int("id") == id }
fun mat4Lesson(id: Int) = mat4.firstOrNull { it.int("id") == id }
fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n } fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n }
fun book(id: String) = books.firstOrNull { it.str("id") == id } fun book(id: String) = books.firstOrNull { it.str("id") == id }

View file

@ -65,6 +65,16 @@ class ProgressStore(context: Context) {
fun markFyz3(id: Int) { prefs.edit().putBoolean("fyz3.$id", true).apply() } fun markFyz3(id: Int) { prefs.edit().putBoolean("fyz3.$id", true).apply() }
fun fyz4Done(id: Int) = prefs.getBoolean("fyz4.$id", false) fun fyz4Done(id: Int) = prefs.getBoolean("fyz4.$id", false)
fun markFyz4(id: Int) { prefs.edit().putBoolean("fyz4.$id", true).apply() } fun markFyz4(id: Int) { prefs.edit().putBoolean("fyz4.$id", true).apply() }
fun mat0Done(id: Int) = prefs.getBoolean("mat0.$id", false)
fun markMat0(id: Int) { prefs.edit().putBoolean("mat0.$id", true).apply() }
fun matDone(id: Int) = prefs.getBoolean("mat.$id", false)
fun markMat(id: Int) { prefs.edit().putBoolean("mat.$id", true).apply() }
fun mat2Done(id: Int) = prefs.getBoolean("mat2.$id", false)
fun markMat2(id: Int) { prefs.edit().putBoolean("mat2.$id", true).apply() }
fun mat3Done(id: Int) = prefs.getBoolean("mat3.$id", false)
fun markMat3(id: Int) { prefs.edit().putBoolean("mat3.$id", true).apply() }
fun mat4Done(id: Int) = prefs.getBoolean("mat4.$id", false)
fun markMat4(id: Int) { prefs.edit().putBoolean("mat4.$id", true).apply() }
fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false) fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false)
fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() } fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() }
@ -216,6 +226,46 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
content.mat0.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.mat0Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.mat.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.matDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.mat2.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.mat2Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.mat3.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.mat3Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.mat4.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.mat4Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
sum.totalEx += content.mluvnice.size sum.totalEx += content.mluvnice.size
sum.openUnits += 1 sum.openUnits += 1
val md = content.mluvnice.count { p.mluvDone(it.int("id")) } val md = content.mluvnice.count { p.mluvDone(it.int("id")) }

View file

@ -1,6 +1,8 @@
package org.maturita.maturita.ui package org.maturita.maturita.ui
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
@ -45,18 +48,28 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.maturita.maturita.AppViewModel import org.maturita.maturita.AppViewModel
import org.maturita.maturita.data.J import org.maturita.maturita.data.J
import org.maturita.maturita.data.answerAccepts import org.maturita.maturita.data.answerAccepts
import org.maturita.maturita.data.mathAnswerOk
import org.maturita.maturita.data.mathDrawGiven
import org.maturita.maturita.data.mathDrawNeed
import org.maturita.maturita.data.mathDrawOk
import org.maturita.maturita.data.mathIsDraw
import org.maturita.maturita.data.answerIncomplete import org.maturita.maturita.data.answerIncomplete
import org.maturita.maturita.data.hasUmlaut import org.maturita.maturita.data.hasUmlaut
import org.maturita.maturita.data.netTxtEq import org.maturita.maturita.data.netTxtEq
@ -863,6 +876,198 @@ fun SciQuizScreen(vm: AppViewModel, area: String, id: Int) {
} }
} }
@Composable
fun MathPracticeScreen(vm: AppViewModel, year: Int, id: Int) {
val lesson = when (year) {
0 -> vm.content.mat0Lesson(id)
2 -> vm.content.mat2Lesson(id)
3 -> vm.content.mat3Lesson(id)
4 -> vm.content.mat4Lesson(id)
else -> vm.content.matLesson(id)
} ?: return
val problems = lesson.arr("problems")
var texts by remember(year, id) { mutableStateOf(List(problems.size) { "" }) }
var placed by remember(year, id) { mutableStateOf(List(problems.size) { emptyList<Pair<Int, Int>>() }) }
var marks by remember(year, id) { mutableStateOf(List<Boolean?>(problems.size) { null }) }
var revealed by remember(year, id) { mutableStateOf(false) }
var feedback by remember(year, id) { mutableStateOf("") }
var kind by remember(year, id) { mutableStateOf("") }
ExerciseScaffold(
vm,
vm.tr(lesson.str("exTitleKey")),
vm.tr(lesson.str("quizHeadKey")),
vm.tr("check"),
{
var all = problems.isNotEmpty()
val next = MutableList<Boolean?>(problems.size) { null }
problems.forEachIndexed { i, problem ->
val spec = problem.str("answer")
val good = if (mathIsDraw(spec)) mathDrawOk(spec, placed[i]) else mathAnswerOk(spec, texts[i])
next[i] = good
if (!good) all = false
}
marks = next
revealed = true
if (all) {
feedback = vm.tr("feedback_ok")
kind = "ok"
when (year) {
0 -> vm.markMat0(id)
2 -> vm.markMat2(id)
3 -> vm.markMat3(id)
4 -> vm.markMat4(id)
else -> vm.markMat(id)
}
} else {
feedback = vm.tr("feedback_retry_short")
kind = "bad"
}
},
feedback,
kind,
) {
Text(
vm.tr("math_quiz_intro"),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(8.dp))
problems.forEachIndexed { i, problem ->
val spec = problem.str("answer")
val border = when (marks.getOrNull(i)) {
true -> Color(0xFF2E7D32)
false -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.outlineVariant
}
Column(
Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
.border(1.5.dp, border, MaterialTheme.shapes.medium)
.padding(12.dp),
) {
Text(problem.str("prompt"), style = MaterialTheme.typography.bodyLarge)
Spacer(Modifier.height(8.dp))
if (mathIsDraw(spec)) {
Text(
vm.tr("math_draw_hint"),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(6.dp))
MathBoard(mathDrawGiven(spec), placed[i], mathDrawNeed(spec), marks.getOrNull(i)) { next ->
placed = placed.toMutableList().also { it[i] = next }
}
Row {
OutlinedButton(onClick = {
val next = placed[i].dropLast(1)
placed = placed.toMutableList().also { it[i] = next }
}) { Text(vm.tr("math_undo")) }
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = {
placed = placed.toMutableList().also { it[i] = emptyList() }
}) { Text(vm.tr("math_clear")) }
}
} else {
OutlinedTextField(
value = texts[i],
onValueChange = { value -> texts = texts.toMutableList().also { it[i] = value } },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(vm.tr("math_answer_ph")) },
singleLine = true,
)
}
if (revealed) {
Spacer(Modifier.height(6.dp))
Text(
problem.str("hint"),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
@Composable
private fun MathBoard(
given: List<Pair<Int, Int>>,
user: List<Pair<Int, Int>>,
need: Int,
mark: Boolean?,
onChange: (List<Pair<Int, Int>>) -> Unit,
) {
val measurer = rememberTextMeasurer()
val accent = MaterialTheme.colorScheme.primary
val ink = MaterialTheme.colorScheme.onSurface
val grid = MaterialTheme.colorScheme.onSurfaceVariant
val paper = MaterialTheme.colorScheme.surfaceVariant
val ok = Color(0xFF2E7D32)
val bad = MaterialTheme.colorScheme.error
Canvas(
Modifier
.size(316.dp, 260.dp)
.pointerInput(given, user, need) {
detectTapGestures { pos ->
val cell = size.width.toFloat() / (10f + 36f / 28f)
val pad = cell * 18f / 28f
val cx = (pos.x - pad) / cell
val cy = (size.height.toFloat() - pad - pos.y) / cell
val ix = kotlin.math.round(cx)
val iy = kotlin.math.round(cy)
if (kotlin.math.abs(cx - ix) > 0.45f || kotlin.math.abs(cy - iy) > 0.45f) return@detectTapGestures
val x = ix.toInt()
val y = iy.toInt()
if (x !in 0..10 || y !in 0..8) return@detectTapGestures
val hit = x to y
if (given.any { it == hit }) return@detectTapGestures
val next = user.toMutableList()
val idx = next.indexOf(hit)
if (idx >= 0) next.removeAt(idx)
else if (next.size < need) next.add(hit)
else return@detectTapGestures
onChange(next)
}
},
) {
val cell = size.width / (10f + 36f / 28f)
val pad = cell * 18f / 28f
fun at(x: Int, y: Int) = Offset(pad + x * cell, size.height - pad - y * cell)
drawRect(paper)
for (i in 0..10) {
val x = pad + i * cell
drawLine(grid.copy(alpha = 0.55f), Offset(x, pad), Offset(x, size.height - pad), strokeWidth = 1f)
}
for (i in 0..8) {
val y = pad + i * cell
drawLine(grid.copy(alpha = 0.55f), Offset(pad, y), Offset(size.width - pad, y), strokeWidth = 1f)
}
drawLine(ink, Offset(pad, size.height - pad), Offset(size.width - pad, size.height - pad), strokeWidth = 2.4f)
drawLine(ink, Offset(pad, size.height - pad), Offset(pad, pad), strokeWidth = 2.4f)
if (given.size >= 2) {
drawLine(accent, at(given[0].first, given[0].second), at(given[1].first, given[1].second), strokeWidth = 3.5f)
if (given.size >= 3) {
drawLine(accent, at(given[0].first, given[0].second), at(given[2].first, given[2].second), strokeWidth = 3.5f)
}
}
val names = listOf("A", "B", "C", "D")
val label = TextStyle(color = ink, fontSize = 13.sp)
given.forEachIndexed { i, g ->
if (i >= names.size) return@forEachIndexed
val pt = at(g.first, g.second)
drawCircle(accent, radius = 7f, center = pt)
drawText(measurer, names[i], topLeft = Offset(pt.x + 8f, pt.y - 22f), style = label)
}
val dot = if (mark == false) bad else ok
user.forEachIndexed { i, u ->
val pt = at(u.first, u.second)
drawCircle(dot, radius = 7f, center = pt)
drawText(measurer, "${i + 1}", topLeft = Offset(pt.x + 8f, pt.y - 22f), style = label)
}
}
}
@Composable @Composable
fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) { fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) {
val lesson = when (year) { val lesson = when (year) {

View file

@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Functions
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FormatListNumbered import androidx.compose.material.icons.filled.FormatListNumbered
@ -224,6 +225,7 @@ private fun SubjectIcon(icon: String, open: Boolean) {
"cz" -> CzechFlag(28.dp) "cz" -> CzechFlag(28.dp)
"flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp)) "flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp))
"bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp)) "bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp))
"function" -> Icon(Icons.Filled.Functions, null, tint = tint, modifier = Modifier.size(26.dp))
else -> Icon(Icons.Filled.Lock, null, tint = tint) else -> Icon(Icons.Filled.Lock, null, tint = tint)
} }
} }
@ -316,6 +318,14 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
sum.doneEx = vm.content.chem.count { vm.progress.chemDone(it.int("id")) } + sum.doneEx = vm.content.chem.count { vm.progress.chemDone(it.int("id")) } +
vm.content.bio.count { vm.progress.bioDone(it.int("id")) } vm.content.bio.count { vm.progress.bioDone(it.int("id")) }
} }
"matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map" -> {
sum.totalEx = vm.content.mat0.size + vm.content.mat.size + vm.content.mat2.size + vm.content.mat3.size + vm.content.mat4.size
sum.doneEx = vm.content.mat0.count { vm.progress.mat0Done(it.int("id")) } +
vm.content.mat.count { vm.progress.matDone(it.int("id")) } +
vm.content.mat2.count { vm.progress.mat2Done(it.int("id")) } +
vm.content.mat3.count { vm.progress.mat3Done(it.int("id")) } +
vm.content.mat4.count { vm.progress.mat4Done(it.int("id")) }
}
"fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map" -> { "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map" -> {
sum.totalEx = vm.content.fyz.size + vm.content.fyz2.size + vm.content.fyz3.size + vm.content.fyz4.size sum.totalEx = vm.content.fyz.size + vm.content.fyz2.size + vm.content.fyz3.size + vm.content.fyz4.size
sum.doneEx = vm.content.fyz.count { vm.progress.fyzDone(it.int("id")) } + sum.doneEx = vm.content.fyz.count { vm.progress.fyzDone(it.int("id")) } +
@ -556,6 +566,32 @@ private fun buildSearch(vm: AppViewModel): List<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_lesson"), "lit4unit${l.int("id")}", extra = "literatura")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex${l.int("id")}") 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")
vm.content.mat0.forEach { l ->
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")}")
}
vm.content.mat.forEach { l ->
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")}")
}
vm.content.mat2.forEach { l ->
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")}")
}
vm.content.mat3.forEach { l ->
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")}")
}
vm.content.mat4.forEach { l ->
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("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra = "fyzika")
add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra = "rocnik") 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_year2"), vm.tr("search_lesson"), "fyz2map", extra = "rocnik")
@ -713,6 +749,21 @@ fun LitYearsScreen(vm: AppViewModel) {
} }
} }
@Composable
fun MatYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Matematika"), vm.tr("mat_years_sub")) { inner ->
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
item {
NavRow(title = vm.tr("mat0_year"), subtitle = vm.tr("mat0_sub"), leading = { Text("ZŠ", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat0Map) }
NavRow(title = vm.tr("mat_year1"), subtitle = vm.tr("mat_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.MatMap) }
NavRow(title = vm.tr("mat_year2"), subtitle = vm.tr("mat2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat2Map) }
NavRow(title = vm.tr("mat_year3"), subtitle = vm.tr("mat3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat3Map) }
NavRow(title = vm.tr("mat_year4"), subtitle = vm.tr("mat4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat4Map) }
}
}
}
}
@Composable @Composable
fun FyzYearsScreen(vm: AppViewModel) { fun FyzYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Fyzika"), vm.tr("fyz_years_sub")) { inner -> DetailScaffold(vm, vm.tr("Fyzika"), vm.tr("fyz_years_sub")) { inner ->

View file

@ -6,6 +6,12 @@
# cs: bullet # cs: bullet
# en: bullet # en: bullet
--- 2026-10-06
cs: Matematika: opakování základní školy a čtyři ročníky, 40 lekcí
cs: Příklady se píšou a kontrolují, u geometrie se rýsuje na mřížku
en: Mathematics: elementary review and four years, 40 lessons
en: Examples are typed and checked, and some geometry is drawn on a grid
--- 2026-10-06 --- 2026-10-06
cs: Fyzika: čtyři ročníky, 32 lekcí podle osnov středních škol cs: Fyzika: čtyři ročníky, 32 lekcí podle osnov středních škol
cs: Od mechaniky přes teplo a elektřinu až po optiku, jádro a astrofyziku cs: Od mechaniky přes teplo a elektřinu až po optiku, jádro a astrofyziku

View file

@ -198,6 +198,31 @@ final class AppModel: ObservableObject {
refresh() 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) { func markMluv(_ n: Int) {
progress.markMluv(n) progress.markMluv(n)
refresh() refresh()

View file

@ -58,3 +58,658 @@ func hasUmlaut(_ s: String?) -> Bool {
guard let s else { return false } guard let s else { return false }
return s.contains(where: { "äöüÄÖÜß".contains($0) }) 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 fyz2: [J]
let fyz3: [J] let fyz3: [J]
let fyz4: [J] let fyz4: [J]
let mat0: [J]
let mat: [J]
let mat2: [J]
let mat3: [J]
let mat4: [J]
let mluvnice: [J] let mluvnice: [J]
let books: [J] let books: [J]
let changelog: [J] let changelog: [J]
@ -90,6 +95,11 @@ final class Content {
fyz2 = raw.arr("fyz2") fyz2 = raw.arr("fyz2")
fyz3 = raw.arr("fyz3") fyz3 = raw.arr("fyz3")
fyz4 = raw.arr("fyz4") 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") mluvnice = raw.arr("mluvnice")
books = raw.arr("books") books = raw.arr("books")
changelog = raw.arr("changelog") changelog = raw.arr("changelog")
@ -112,6 +122,11 @@ final class Content {
func fyz2Lesson(_ id: Int) -> J? { fyz2.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 fyz3Lesson(_ id: Int) -> J? { fyz3.first { $0.int("id") == id } }
func fyz4Lesson(_ id: Int) -> J? { fyz4.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 mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
func book(_ id: String) -> J? { books.first { $0.str("id") == id } } 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 markFyz3(_ id: Int) { defaults.set(true, forKey: "fyz3.\(id)") }
func fyz4Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz4.\(id)") } func fyz4Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz4.\(id)") }
func markFyz4(_ id: Int) { defaults.set(true, 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 mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
func markMluv(_ n: Int) { defaults.set(true, 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 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.totalEx += content.mluvnice.count
sum.openUnits += 1 sum.openUnits += 1
let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count

View file

@ -56,6 +56,22 @@ enum Route: Hashable {
case bioMap case bioMap
case bioLesson(Int) case bioLesson(Int)
case bioEx(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 fyzYears
case fyzMap case fyzMap
case fyzLesson(Int) case fyzLesson(Int)
@ -127,6 +143,22 @@ enum Route: Hashable {
case .bioMap: return "biomap" case .bioMap: return "biomap"
case .bioLesson(let id): return "biounit\(id)" case .bioLesson(let id): return "biounit\(id)"
case .bioEx(let id): return "bioex\(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 .fyzYears: return "fyzyears"
case .fyzMap: return "fyzmap" case .fyzMap: return "fyzmap"
case .fyzLesson(let id): return "fyzunit\(id)" case .fyzLesson(let id): return "fyzunit\(id)"
@ -254,6 +286,38 @@ enum Route: Hashable {
return [.sciMap, .bioMap, .bioLesson(id)] return [.sciMap, .bioMap, .bioLesson(id)]
case .bioEx(let id): case .bioEx(let id):
return [.sciMap, .bioMap, .bioLesson(id), .bioEx(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: case .fyzYears:
return [.fyzYears] return [.fyzYears]
case .fyzMap: case .fyzMap:
@ -321,6 +385,12 @@ func routeFromPage(_ page: String) -> Route? {
case "scimap": return .sciMap case "scimap": return .sciMap
case "chemmap": return .chemMap case "chemmap": return .chemMap
case "biomap": return .bioMap 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 "fyzyears": return .fyzYears
case "fyzmap": return .fyzMap case "fyzmap": return .fyzMap
case "fyz2map": return .fyz2Map 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) { if let m = page.wholeMatch(of: /bioex(\d+)/), let n = Int(m.1) {
return .bioEx(n) 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) { if let m = page.wholeMatch(of: /fyz4unit(\d+)/), let n = Int(m.1) {
return .fyz4Lesson(n) return .fyz4Lesson(n)
} }

View file

@ -609,6 +609,7 @@ func subjectSymbol(_ icon: String) -> String {
case "people": return "person.2.fill" case "people": return "person.2.fill"
case "flask": return "flask.fill" case "flask": return "flask.fill"
case "bolt": return "bolt.fill" case "bolt": return "bolt.fill"
case "function": return "function"
default: return "lock.fill" 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 { struct FyzQuizScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
let year: Int 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.totalEx = vm.content.chem.count + vm.content.bio.count
sum.doneEx = vm.content.chem.filter { vm.progress.chemDone($0.int("id")) }.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 + 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": 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.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 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_lesson"), "lit4unit\(l.int("id"))", extra: "literatura")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex\(l.int("id"))") 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("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra: "fyzika")
add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra: "rocnik") 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_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 { struct FyzYearsScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
@ -2321,6 +2399,88 @@ struct RouteDestination: View {
} }
case .bioEx(let id): case .bioEx(let id):
SciQuizScreen(vm: vm, area: "bio", id: 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: case .fyzYears:
FyzYearsScreen(vm: vm) FyzYearsScreen(vm: vm)
case .fyzMap: case .fyzMap:

View file

@ -455,6 +455,20 @@ def profile_items(rows) -> list[dict]:
return out return out
def math_items(rows) -> list[dict]:
out = []
for row in as_list(rows):
row = as_list(row)
if len(row) < 3:
continue
out.append({
"prompt": as_str(row[0]) or "",
"answer": as_str(row[1]) or "",
"hint": as_str(row[2]) or "",
})
return out
def slides_items(rows) -> list[dict]: def slides_items(rows) -> list[dict]:
out = [] out = []
for row in as_list(rows): for row in as_list(rows):
@ -1116,6 +1130,28 @@ def main() -> int:
fyz3_lessons = lit_pack("fyz3") fyz3_lessons = lit_pack("fyz3")
fyz4_lessons = lit_pack("fyz4") fyz4_lessons = lit_pack("fyz4")
def math_pack(prefix):
lessons = []
for i in range(1, 9):
fn_u = f"build_{prefix}_unit{i}_page"
fn_e = f"build_{prefix}_unit{i}_exercise_page"
lessons.append({
"id": i,
"titleKey": f"{prefix}_unit{i}",
"subKey": f"{prefix}_unit{i}_sub",
"exTitleKey": f"{prefix}_ex{i}_title",
"quizHeadKey": f"{prefix}_quiz{i}_head",
"slides": slides_items(A(f"{fn_u}.slides")),
"problems": math_items(A(f"{fn_e}.qs")),
})
return lessons
mat0_lessons = math_pack("mat0")
mat_lessons = math_pack("mat")
mat2_lessons = math_pack("mat2")
mat3_lessons = math_pack("mat3")
mat4_lessons = math_pack("mat4")
on_lessons = [] on_lessons = []
for i in range(1, 18): for i in range(1, 18):
on_lessons.append({ on_lessons.append({
@ -1563,7 +1599,7 @@ def main() -> int:
{"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"}, {"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"},
{"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"}, {"key": "Občanská nauka", "open": True, "target": "onyears", "icon": "people"},
{"key": "English", "open": False, "target": None, "icon": "lock"}, {"key": "English", "open": False, "target": None, "icon": "lock"},
{"key": "Matematika", "open": False, "target": None, "icon": "lock"}, {"key": "Matematika", "open": True, "target": "matyears", "icon": "function"},
{"key": "Fyzika", "open": True, "target": "fyzyears", "icon": "bolt"}, {"key": "Fyzika", "open": True, "target": "fyzyears", "icon": "bolt"},
{"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"}, {"key": "Základy Přírodopisných věd", "open": True, "target": "scimap", "icon": "flask"},
{"key": "Technická grafika", "open": False, "target": None, "icon": "lock"}, {"key": "Technická grafika", "open": False, "target": None, "icon": "lock"},
@ -1593,6 +1629,11 @@ def main() -> int:
"fyz2": fyz2_lessons, "fyz2": fyz2_lessons,
"fyz3": fyz3_lessons, "fyz3": fyz3_lessons,
"fyz4": fyz4_lessons, "fyz4": fyz4_lessons,
"mat0": mat0_lessons,
"mat": mat_lessons,
"mat2": mat2_lessons,
"mat3": mat3_lessons,
"mat4": mat4_lessons,
"on2": on2_lessons, "on2": on2_lessons,
"on3": on3_lessons, "on3": on3_lessons,
"on4": on4_lessons, "on4": on4_lessons,
@ -1635,6 +1676,7 @@ def main() -> int:
print(f" chem lessons: {sum(1 for L in chem_lessons if L['slides'])}") print(f" chem lessons: {sum(1 for L in chem_lessons if L['slides'])}")
print(f" bio lessons: {sum(1 for L in bio_lessons if L['slides'])}") print(f" bio lessons: {sum(1 for L in bio_lessons if L['slides'])}")
print(f" fyz lessons: {sum(1 for L in fyz_lessons + fyz2_lessons + fyz3_lessons + fyz4_lessons if L['slides'])}") print(f" fyz lessons: {sum(1 for L in fyz_lessons + fyz2_lessons + fyz3_lessons + fyz4_lessons if L['slides'])}")
print(f" math lessons: {sum(1 for L in mat0_lessons + mat_lessons + mat2_lessons + mat3_lessons + mat4_lessons if L['slides'])}")
print(f" on2 lessons: {sum(1 for L in on2_lessons if L['slides'])}") print(f" on2 lessons: {sum(1 for L in on2_lessons if L['slides'])}")
print(f" on3 lessons: {sum(1 for L in on3_lessons if L['slides'])}") print(f" on3 lessons: {sum(1 for L in on3_lessons if L['slides'])}")
print(f" on4 lessons: {sum(1 for L in on4_lessons if L['slides'])}") print(f" on4 lessons: {sum(1 for L in on4_lessons if L['slides'])}")
@ -1705,6 +1747,17 @@ def main() -> int:
missing.append(f"{label}{L['id']} quiz") missing.append(f"{label}{L['id']} quiz")
if not all(q.get("expl") for q in L["quiz"]): if not all(q.get("expl") for q in L["quiz"]):
missing.append(f"{label}{L['id']} expl") missing.append(f"{label}{L['id']} expl")
for label, block in (
("mat0", mat0_lessons), ("mat", mat_lessons),
("mat2", mat2_lessons), ("mat3", mat3_lessons), ("mat4", mat4_lessons),
):
for L in block:
if len(L["slides"]) != 3:
missing.append(f"{label}{L['id']} slides")
if not L["problems"]:
missing.append(f"{label}{L['id']} problems")
if not all(p.get("prompt") and p.get("answer") and p.get("hint") for p in L["problems"]):
missing.append(f"{label}{L['id']} answer")
for m in mluvnice: for m in mluvnice:
if m["type"] in ("typed", "reveal") and not m.get("items"): if m["type"] in ("typed", "reveal") and not m.get("items"):
missing.append(f"mluv{m['id']}") missing.append(f"mluv{m['id']}")

2040
scripts/gen-math.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@
#define HW_SUBJ 2 /* index of "Technické vybavení" */ #define HW_SUBJ 2 /* index of "Technické vybavení" */
#define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */
#define ON_SUBJ 4 /* index of "Občanská nauka" */ #define ON_SUBJ 4 /* index of "Občanská nauka" */
#define MATH_SUBJ 6 /* index of "Matematika" */
#define PHY_SUBJ 7 /* index of "Fyzika" */ #define PHY_SUBJ 7 /* index of "Fyzika" */
#define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */ #define SCI_SUBJ 8 /* index of "Základy Přírodopisných věd" */
#define CSS_FILE "style.css" #define CSS_FILE "style.css"
@ -67,6 +68,11 @@ extern char *app_progress_fyz;
extern char *app_progress_fyz2; extern char *app_progress_fyz2;
extern char *app_progress_fyz3; extern char *app_progress_fyz3;
extern char *app_progress_fyz4; extern char *app_progress_fyz4;
extern char *app_progress_mat0;
extern char *app_progress_mat;
extern char *app_progress_mat2;
extern char *app_progress_mat3;
extern char *app_progress_mat4;
#define PROGRESS_DIR app_progress_dir #define PROGRESS_DIR app_progress_dir
#define PROGRESS_U1 app_progress_u1 #define PROGRESS_U1 app_progress_u1
#define PROGRESS_U2 app_progress_u2 #define PROGRESS_U2 app_progress_u2
@ -146,6 +152,7 @@ extern char *app_progress_fyz4;
#define LIT_N 8 #define LIT_N 8
#define SCI_N 8 #define SCI_N 8
#define FYZ_N 8 #define FYZ_N 8
#define MATH_N 8
#define HW_SLIDES 3 #define HW_SLIDES 3
#define HW2_SLIDES 6 #define HW2_SLIDES 6
#define HW3_SLIDES 2 #define HW3_SLIDES 2
@ -345,6 +352,11 @@ typedef struct {
int n_options; int n_options;
int correct; int correct;
} ChoiceQ; } ChoiceQ;
typedef struct {
const char *prompt;
const char *answer;
const char *hint;
} MathItem;
typedef struct { typedef struct {
const ChoiceQ *qs; const ChoiceQ *qs;
int n; int n;
@ -979,6 +991,37 @@ extern NetLesson fyz_lessons[FYZ_N];
extern NetLesson fyz2_lessons[FYZ_N]; extern NetLesson fyz2_lessons[FYZ_N];
extern NetLesson fyz3_lessons[FYZ_N]; extern NetLesson fyz3_lessons[FYZ_N];
extern NetLesson fyz4_lessons[FYZ_N]; extern NetLesson fyz4_lessons[FYZ_N];
gboolean math_answer_ok(const char *spec, const char *user);
gboolean math_is_draw(const char *spec);
int math_draw_need(const char *spec);
int math_draw_fixed(const char *spec, int *xs, int *ys, int cap);
gboolean math_draw_ok(const char *spec, const int *xs, const int *ys, int n);
void math_load_progress(void);
void math_rail_theme_reset(void);
void math_lessons_apply_lang(void);
void progress_for_math(ProgressSum *out);
GtkWidget *math_unit_page(int year, NetLesson *L, const char *title_key,
const char *sub_key, const NetSlide *slides,
guint n_slides);
GtkWidget *math_practice_page(int year, int lesson_id, const char *back_page,
const char *title_key, const char *heading_key,
const MathItem *items, int n);
GtkWidget *build_matyears_page(void);
GtkWidget *build_mat0map_page(void);
GtkWidget *build_matmap_page(void);
GtkWidget *build_mat2map_page(void);
GtkWidget *build_mat3map_page(void);
GtkWidget *build_mat4map_page(void);
void add_mat0_pages(GtkStack *stack);
void add_mat_pages(GtkStack *stack);
void add_mat2_pages(GtkStack *stack);
void add_mat3_pages(GtkStack *stack);
void add_mat4_pages(GtkStack *stack);
extern NetLesson mat0_lessons[MATH_N];
extern NetLesson mat_lessons[MATH_N];
extern NetLesson mat2_lessons[MATH_N];
extern NetLesson mat3_lessons[MATH_N];
extern NetLesson mat4_lessons[MATH_N];
void hw_lessons_apply_lang(void); void hw_lessons_apply_lang(void);
GtkWidget *build_hwyears_page(void); GtkWidget *build_hwyears_page(void);
GtkWidget *build_hwmap_page(void); GtkWidget *build_hwmap_page(void);
@ -1068,6 +1111,8 @@ void draw_civics_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_science_icon(GtkDrawingArea *area, cairo_t *cr, void draw_science_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_math_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data);
void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr, void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr, void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,

View file

@ -90,12 +90,12 @@ const TrEntry tr_ui[] = {
"Cvičení, nápovědy a vokabeltraining. Aplikace si pamatuje, kde jste skončili.", "Cvičení, nápovědy a vokabeltraining. Aplikace si pamatuje, kde jste skončili.",
"Exercises, hints and vocabulary drills. The app remembers where you left off."}, "Exercises, hints and vocabulary drills. The app remembers where you left off."},
{"welcome_feat1_link", "Začít procvičovat", "Start practicing"}, {"welcome_feat1_link", "Začít procvičovat", "Start practicing"},
{"welcome_feat2_title", "Sedm otevřených předmětů", "Seven open subjects"}, {"welcome_feat2_title", "Osm otevřených předmětů", "Eight open subjects"},
{"welcome_feat2_body", {"welcome_feat2_body",
"Deutsch, sítě, hardware, čeština, občanská nauka, fyzika a přírodní vědy. Ostatní předměty čekají na svůj obsah.", "Deutsch, sítě, hardware, čeština, občanská nauka, matematika, fyzika a přírodní vědy. Ostatní předměty čekají na svůj obsah.",
"German, networks, hardware, Czech, civics, physics and natural sciences. The other subjects are still locked."}, "German, networks, hardware, Czech, civics, mathematics, physics and natural sciences. The other subjects are still locked."},
{"welcome_feat2_link", "Jaké předměty jsou otevřené?", "Which subjects are open?"}, {"welcome_feat2_link", "Jaké předměty jsou otevřené?", "Which subjects are open?"},
{"welcome_stat_value", "7", "7"}, {"welcome_stat_value", "8", "8"},
{"welcome_stat_label", "otevřené\npředměty", "open\nsubjects"}, {"welcome_stat_label", "otevřené\npředměty", "open\nsubjects"},
{"continue", "Pokračuj", "Continue"}, {"continue", "Pokračuj", "Continue"},
{"roadmap_title", "Učební plán", "Learning path"}, {"roadmap_title", "Učební plán", "Learning path"},
@ -1619,6 +1619,7 @@ const TrEntry tr_ui[] = {
{"lit_fuks_plot_title", "Sestavte děj", "Order the plot"}, {"lit_fuks_plot_title", "Sestavte děj", "Order the plot"},
{"lit_fuks_plot_sub", "Přetáhněte části příběhu do správného pořadí.", {"lit_fuks_plot_sub", "Přetáhněte části příběhu do správného pořadí.",
"Drag the story parts into the correct order."}, "Drag the story parts into the correct order."},
#include "math_i18n.inc"
{NULL, NULL, NULL} {NULL, NULL, NULL}
}; };
@ -2603,6 +2604,7 @@ void on4_lessons_apply_lang(void);
void lit_lessons_apply_lang(void); void lit_lessons_apply_lang(void);
void sci_lessons_apply_lang(void); void sci_lessons_apply_lang(void);
void fyz_lessons_apply_lang(void); void fyz_lessons_apply_lang(void);
void math_lessons_apply_lang(void);
void net_rail_theme_reset(void); void net_rail_theme_reset(void);
void apply_language(void) { void apply_language(void) {
@ -2624,5 +2626,6 @@ void apply_language(void) {
lit_lessons_apply_lang(); lit_lessons_apply_lang();
sci_lessons_apply_lang(); sci_lessons_apply_lang();
fyz_lessons_apply_lang(); fyz_lessons_apply_lang();
math_lessons_apply_lang();
search_apply_lang(); search_apply_lang();
} }

View file

@ -261,6 +261,34 @@ void draw_physics_icon(GtkDrawingArea *area, cairo_t *cr,
cairo_stroke(cr); cairo_stroke(cr);
} }
/* Axes and a rising curve for Matematika. */
void draw_math_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data) {
const double s = MIN(width, height);
const double left = width * 0.22;
const double right = width * 0.82;
const double bottom = height * 0.78;
const double top = height * 0.22;
const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent),
color_from_hex(app_theme.accent2), 0.22);
(void)area;
(void)data;
cairo_set_source_rgb(cr, c.r, c.g, c.b);
cairo_set_line_width(cr, MAX(1.6, s * 0.07));
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
cairo_move_to(cr, left, top);
cairo_line_to(cr, left, bottom);
cairo_line_to(cr, right, bottom);
cairo_stroke(cr);
cairo_move_to(cr, left + s * 0.06, bottom - s * 0.08);
cairo_curve_to(cr, left + s * 0.28, bottom - s * 0.02,
width * 0.48, height * 0.62,
right - s * 0.04, top + s * 0.06);
cairo_stroke(cr);
}
/* Open book glyph, painted in the given colour. */ /* Open book glyph, painted in the given colour. */
static void book_paint(cairo_t *cr, int width, int height, Rgb c) { static void book_paint(cairo_t *cr, int width, int height, Rgb c) {
const double w = width; const double w = width;

View file

@ -390,6 +390,9 @@ static gboolean run_smoke_test(gpointer data) {
"czechmap", "czechmap",
"lityears", "litmap", "lit2map", "lit3map", "lit4map", "lityears", "litmap", "lit2map", "lit3map", "lit4map",
"scimap", "chemmap", "biomap", "scimap", "chemmap", "biomap",
"matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map",
"mat0unit1", "mat0ex1", "matunit1", "matex1",
"mat4unit8", "mat4ex8",
"fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map", "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map",
"fyzunit1", "fyzex1", "fyzunit8", "fyzex8", "fyzunit1", "fyzex1", "fyzunit8", "fyzex8",
"fyz2unit1", "fyz2ex1", "fyz3unit1", "fyz4unit1", "fyz2unit1", "fyz2ex1", "fyz3unit1", "fyz4unit1",
@ -532,6 +535,17 @@ void activate(GtkApplication *app, gpointer user_data) {
add_on3_pages(main_stack); add_on3_pages(main_stack);
gtk_stack_add_named(main_stack, build_on4map_page(), "on4map"); gtk_stack_add_named(main_stack, build_on4map_page(), "on4map");
add_on4_pages(main_stack); add_on4_pages(main_stack);
gtk_stack_add_named(main_stack, build_matyears_page(), "matyears");
gtk_stack_add_named(main_stack, build_mat0map_page(), "mat0map");
add_mat0_pages(main_stack);
gtk_stack_add_named(main_stack, build_matmap_page(), "matmap");
add_mat_pages(main_stack);
gtk_stack_add_named(main_stack, build_mat2map_page(), "mat2map");
add_mat2_pages(main_stack);
gtk_stack_add_named(main_stack, build_mat3map_page(), "mat3map");
add_mat3_pages(main_stack);
gtk_stack_add_named(main_stack, build_mat4map_page(), "mat4map");
add_mat4_pages(main_stack);
gtk_stack_add_named(main_stack, build_fyzyears_page(), "fyzyears"); gtk_stack_add_named(main_stack, build_fyzyears_page(), "fyzyears");
gtk_stack_add_named(main_stack, build_fyzmap_page(), "fyzmap"); gtk_stack_add_named(main_stack, build_fyzmap_page(), "fyzmap");
add_fyz_pages(main_stack); add_fyz_pages(main_stack);

469
src/mat_year0.c Normal file
View file

@ -0,0 +1,469 @@
#include "graduately.h"
NetLesson mat0_lessons[MATH_N] = {
{ .n_slides = 3, .unit_page = "mat0unit1", .ex_page = "mat0ex1" },
{ .n_slides = 3, .unit_page = "mat0unit2", .ex_page = "mat0ex2" },
{ .n_slides = 3, .unit_page = "mat0unit3", .ex_page = "mat0ex3" },
{ .n_slides = 3, .unit_page = "mat0unit4", .ex_page = "mat0ex4" },
{ .n_slides = 3, .unit_page = "mat0unit5", .ex_page = "mat0ex5" },
{ .n_slides = 3, .unit_page = "mat0unit6", .ex_page = "mat0ex6" },
{ .n_slides = 3, .unit_page = "mat0unit7", .ex_page = "mat0ex7" },
{ .n_slides = 3, .unit_page = "mat0unit8", .ex_page = "mat0ex8" },
};
static GtkWidget *build_mat0_unit1_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Pravidlo", "Co se počítá dřív",
"Tip: násobení a dělení jsou stejně silné a jdou zleva doprava.",
{
"Nejprve se vyřeší závorky, zevnitř ven.",
"Pak jsou mocniny a odmocniny.",
"Násobení a dělení mají přednost před sčítáním a odčítáním.",
"Sčítání a odčítání se také provádějí zleva doprava.",
NULL,
},
},
{
"2 / 3 • Bez závorek", "Násobení má přednost",
"Tip: 2 + 3 · 4 není 20.",
{
"Ve výrazu 2 + 3 · 4 se nejdřív násobí: 3 · 4 = 12.",
"Teprve potom se přičtou dvě: 2 + 12 = 14.",
"Ve výrazu 20 − 6 / 2 se nejdřív dělí: 6 / 2 = 3.",
"Pak 20 − 3 = 17.",
NULL,
},
},
{
"3 / 3 • Závorky", "Závorka změní pořadí",
"Tip: závorka se chová jako jeden počet.",
{
"(8 − 3) · 2 začíná v závorce: 5 · 2 = 10.",
"3 · (4 + 1) je 3 · 5 = 15.",
"Bez závorky by 3 · 4 + 1 vyšlo 13.",
"Závorku proto pište vždy, když má sčítání předběhnout násobení.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[0], "mat0_unit1", "mat0_unit1_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit1_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte 2 + 3 · 4.", "num:14", "Násobení má přednost: 3 · 4 = 12 a 2 + 12 = 14. Výsledek je 14."},
{"Spočítejte (8 − 3) · 2.", "num:10", "Nejdřív závorka: 8 − 3 = 5, potom 5 · 2 = 10. Výsledek je 10."},
{"Spočítejte 20 − 6 / 2.", "num:17", "Nejdřív dělení: 6 / 2 = 3, potom 20 − 3 = 17. Výsledek je 17."},
{"Spočítejte 3 · (4 + 1).", "num:15", "Nejdřív závorka: 4 + 1 = 5, potom 3 · 5 = 15. Výsledek je 15."},
};
return math_practice_page(0, 0, "mat0unit1",
"mat0_ex1_title", "mat0_quiz1_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit2_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Součet", "Společný jmenovatel",
"Tip: zlomek v základním tvaru má nesoudělný čitatel a jmenovatel.",
{
"Sčítat a odčítat lze zlomky se stejným jmenovatelem.",
"Jinak se jmenovatelé rozšíří na nejmenší společný násobek.",
"1/2 + 1/3 má společný jmenovatel 6: 3/6 + 2/6 = 5/6.",
"3/4 − 1/4 = 2/4 = 1/2.",
NULL,
},
},
{
"2 / 3 • Součin", "Čitatel s čitatelem",
"Tip: před násobením jde často krátit křížem.",
{
"Zlomky se násobí čitatel čitatelem a jmenovatel jmenovatelem.",
"2/3 · 3/5 = 6/15 = 2/5.",
"Krátit se smí jen čitatel se jmenovatelem, ne dvě horní čísla mezi sebou.",
"Smíšené číslo se před počtem převede na zlomek.",
NULL,
},
},
{
"3 / 3 • Podíl", "Dělit znamená násobit převráceným",
"Tip: převrácený zlomek k a/b je b/a.",
{
"Dělení zlomkem je násobení jeho převrácenou hodnotou.",
"(3/4) : (1/2) = 3/4 · 2/1 = 6/4 = 3/2.",
"Výsledek se uvede v základním tvaru.",
"Nulou se dělit nesmí, proto jmenovatel nesmí vyjít 0.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[1], "mat0_unit2", "mat0_unit2_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit2_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte 1/2 + 1/3. Zlomek v základním tvaru.", "num:5/6", "Společný jmenovatel je 6: 3/6 + 2/6 = 5/6. Výsledek je 5/6."},
{"Spočítejte 3/4 − 1/4.", "num:1/2", "Jmenovatel už je stejný: 2/4 = 1/2. Výsledek je 1/2."},
{"Spočítejte 2/3 · 3/5.", "num:2/5", "6/15 se zkrátí třemi na 2/5. Výsledek je 2/5."},
{"Spočítejte (3/4) : (1/2).", "num:3/2", "Násobí se převráceným zlomkem: 3/4 · 2/1 = 3/2. Výsledek je 3/2."},
};
return math_practice_page(0, 1, "mat0unit2",
"mat0_ex2_title", "mat0_quiz2_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit3_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Zápis", "Čárka i tečka",
"Tip: 0,5 je totéž co 1/2.",
{
"Desetinná čárka odděluje celou část od desetin, setin a tisícin.",
"0,1 je jedna desetina, 0,01 je jedna setina.",
"Při sčítání se čárky píšou pod sebe.",
"2,5 + 1,5 = 4.",
NULL,
},
},
{
"2 / 3 • Násobení", "Čárka se posouvá",
"Tip: násobení deseti posune čárku o jedno místo doprava.",
{
"Počet desetinných míst součinu je součet počtů míst činitelů.",
"0,2 · 5 = 1, protože dvě desetiny pětkrát jsou jedna celá.",
"1,25 − 0,5 = 0,75, což je 3/4.",
"Výsledek lze psát desetinně i zlomkem.",
NULL,
},
},
{
"3 / 3 • Dělení", "Dělit desetinným číslem",
"Tip: dělitel se nejdřív upraví na celé číslo.",
{
"3,6 : 0,3 se rozšíří deseti na 36 : 3 = 12.",
"Čárka se v děliteli i děleném posune o stejný počet míst.",
"Dělení deseti posune čárku o jedno místo doleva.",
"Nulou se ani u desetinných čísel dělit nesmí.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[2], "mat0_unit3", "mat0_unit3_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit3_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte 2,5 + 1,5.", "num:4", "Pět desetin a pět desetin je jedna celá, celkem 4. Výsledek je 4."},
{"Spočítejte 0,2 · 5.", "num:1", "Dvě desetiny pětkrát jsou 10 desetin, tedy 1. Výsledek je 1."},
{"Spočítejte 3,6 : 0,3.", "num:12", "Posun čárky dá 36 : 3 = 12. Výsledek je 12."},
{"Spočítejte 1,25 − 0,5.", "num:3/4", "1,25 − 0,50 = 0,75, tedy 3/4. Výsledek je 3/4."},
};
return math_practice_page(0, 2, "mat0unit3",
"mat0_ex3_title", "mat0_quiz3_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit4_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Základ", "Jedno procento je setina",
"Tip: pište jen číslo, znak procenta ne.",
{
"1 % ze základu je jedna setina základu.",
"p % ze základu z je (p/100) · z.",
"20 % ze 150 je 0,2 · 150 = 30.",
"Základ, procentová část a počet procent jsou tři veličiny.",
NULL,
},
},
{
"2 / 3 • Kolik procent", "Část dělená základem",
"Tip: výsledek 0,25 znamená 25 %.",
{
"Počet procent je (část / základ) · 100.",
"15 ze 60 je 15/60 = 1/4, tedy 25 %.",
"Do odpovědi se píše 25, ne 0,25 a ne 25 %.",
"Zlomek se na procenta násobí stem.",
NULL,
},
},
{
"3 / 3 • Sleva a základ", "Dopočítání celku",
"Tip: sleva 10 % nechá z ceny 90 %.",
{
"Po slevě 10 % zbývá 90 % původní ceny.",
"400 − 10 % je 0,9 · 400 = 360.",
"Když 20 % základu je 18, je 1 % rovno 18/20.",
"Celý základ je pak 100 takových dílů: 90.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[3], "mat0_unit4", "mat0_unit4_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit4_exercise_page(void) {
static const MathItem qs[] = {
{"Kolik je 20 % ze 150? Napište jen číslo.", "num:30", "20 % je 0,2 a 0,2 · 150 = 30. Výsledek je 30."},
{"15 je kolik procent z 60? Napište jen číslo, bez znaku %.", "num:25", "15/60 = 1/4 = 25 %. Výsledek je 25."},
{"Cena 400 se sníží o 10 %. Jaká bude nová cena?", "num:360", "Zůstane 90 %: 0,9 · 400 = 360. Výsledek je 360."},
{"20 % základu je 18. Jak velký je základ?", "num:90", "1 % je 18/20 = 0,9 a 100 % je 90. Výsledek je 90."},
};
return math_practice_page(0, 3, "mat0unit4",
"mat0_ex4_title", "mat0_quiz4_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit5_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Poměr", "Díly celku",
"Tip: poměr 2 : 3 má dohromady 5 dílů.",
{
"Poměr a : b říká, na kolik stejných dílů se celek rozdělí.",
"Počet dílů je a + b.",
"První část je a/(a + b) z celku.",
"Z 20 v poměru 2 : 3 připadá na první část 8.",
NULL,
},
},
{
"2 / 3 • Úměra", "Součin vnějších a vnitřních členů",
"Tip: 3 : 5 = x : 20 znamená 3/5 = x/20.",
{
"V úměře a : b = c : d platí a · d = b · c.",
"Z 3/5 = x/20 vyjde x = 3/5 · 20 = 12.",
"Stejný poměr lze rozšiřovat i krátit.",
"Neznámý člen se dopočítá jedním násobením a jedním dělením.",
NULL,
},
},
{
"3 / 3 • Měřítko", "Mapa a skutečnost",
"Tip: 1 : 1000 znamená, že 1 cm na mapě je 10 m ve skutečnosti.",
{
"Měřítko 1 : k zmenšuje: skutečnost je k-krát větší než plán.",
"5 cm na mapě 1 : 1000 je 5000 cm, tedy 50 m.",
"Měřítko 5 : 1 je zvětšení, obraz je pětkrát větší.",
"4 cm v měřítku 5 : 1 odpovídá obrazu 20 cm.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[4], "mat0_unit5", "mat0_unit5_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit5_exercise_page(void) {
static const MathItem qs[] = {
{"Číslo 20 rozdělte v poměru 2 : 3. Jak velká je první část?", "num:8", "Dílů je 5 a první část jsou 2 z nich: 2/5 · 20 = 8. Výsledek je 8."},
{"Na mapě 1 : 1000 jsou 5 cm. Kolik metrů je to ve skutečnosti?", "num:50", "5 cm · 1000 = 5000 cm = 50 m. Výsledek je 50."},
{"Platí 3 : 5 = x : 20. Kolik je x?", "num:12", "x = 3/5 · 20 = 12. Výsledek je 12."},
{"Úsečka 4 cm se zobrazí v měřítku 5 : 1. Jak dlouhý je obraz v centimetrech?", "num:20", "Měřítko 5 : 1 zvětšuje pětkrát: 4 · 5 = 20. Výsledek je 20."},
};
return math_practice_page(0, 4, "mat0unit5",
"mat0_ex5_title", "mat0_quiz5_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit6_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Číselná osa", "Vpravo roste, vlevo klesá",
"Tip: přičíst kladné číslo znamená posun doprava.",
{
"Kladná čísla leží vpravo od nuly, záporná vlevo.",
"−3 + 8 je posun z −3 o 8 kroků doprava, tedy na 5.",
"Odčítání záporného čísla je totéž co přičítání kladného.",
"6 − (−2) = 6 + 2 = 8.",
NULL,
},
},
{
"2 / 3 • Násobení", "Znaménka",
"Tip: dvě záporná znaménka dají kladný součin.",
{
"Součin kladného a záporného čísla je záporný.",
"Součin dvou záporných čísel je kladný.",
"(−4) · (−5) = 20.",
"Stejné pravidlo platí pro dělení.",
NULL,
},
},
{
"3 / 3 • Absolutní hodnota", "Vzdálenost od nuly",
"Tip: absolutní hodnota nikdy není záporná.",
{
"|a| je vzdálenost čísla a od nuly na ose.",
"|−7| = 7 a |7| = 7.",
"|0| = 0.",
"Absolutní hodnota smaže znaménko, ale číslo jinak nezmění.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[5], "mat0_unit6", "mat0_unit6_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit6_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte −3 + 8.", "num:5", "Z −3 se jde o 8 doprava, na 5. Výsledek je 5."},
{"Spočítejte (−4) · (−5).", "num:20", "Součin dvou záporných čísel je kladný: 20. Výsledek je 20."},
{"Spočítejte |−7|.", "num:7", "Vzdálenost −7 od nuly je 7. Výsledek je 7."},
{"Spočítejte 6 − (−2).", "num:8", "Odčítat −2 znamená přičíst 2: 6 + 2 = 8. Výsledek je 8."},
};
return math_practice_page(0, 5, "mat0unit6",
"mat0_ex6_title", "mat0_quiz6_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit7_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Rovnováha", "Stejně na obě strany",
"Tip: co uděláte vlevo, udělejte i vpravo.",
{
"Rovnice říká, že levá strana se rovná pravé.",
"K oběma stranám lze přičíst stejné číslo.",
"Obě strany lze vynásobit stejným nenulovým číslem.",
"Cílem je osamostatnit neznámou.",
NULL,
},
},
{
"2 / 3 • Kroky", "Nejdřív číslo, pak koeficient",
"Tip: 2x − 4 = 10 se nejdřív zbaví čtyřky.",
{
"x + 5 = 12 se odečtením pěti změní na x = 7.",
"3x = 15 se vydělí třemi: x = 5.",
"2x − 4 = 10 dá po přičtení čtyř 2x = 14, tedy x = 7.",
"Zkouška dosadí výsledek do původní rovnice.",
NULL,
},
},
{
"3 / 3 • Dělení neznámé", "Zlomek s x",
"Tip: x/2 = 6 se násobí dvěma.",
{
"x/2 je totéž co (1/2) · x.",
"Násobení obou stran dvěma dá x = 12.",
"Násobit nulou se nesmí, rovnice by ztratila význam.",
"U lineární rovnice s nenulovým koeficientem je řešení jedno.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[6], "mat0_unit7", "mat0_unit7_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit7_exercise_page(void) {
static const MathItem qs[] = {
{"Vyřešte x + 5 = 12. Napište x.", "num:7", "Od obou stran se odečte 5. Výsledek je 7."},
{"Vyřešte 3x = 15. Napište x.", "num:5", "Obě strany se vydělí 3. Výsledek je 5."},
{"Vyřešte 2x − 4 = 10. Napište x.", "num:7", "2x = 14, takže x = 7. Výsledek je 7."},
{"Vyřešte x/2 = 6. Napište x.", "num:12", "Obě strany se násobí 2. Výsledek je 12."},
};
return math_practice_page(0, 6, "mat0unit7",
"mat0_ex7_title", "mat0_quiz7_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat0_unit8_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Obdélník", "Obvod a obsah",
"Tip: obvod je součet všech stran, obsah je součin dvou sousedních.",
{
"Obdélník a × b má obvod 2 · (a + b).",
"Obdélník 5 × 3 má obvod 2 · 8 = 16.",
"Obsah je a · b. Obdélník 6 × 4 má obsah 24.",
"Jednotky obvodu jsou délkové, jednotky obsahu čtvereční.",
NULL,
},
},
{
"2 / 3 • Trojúhelník", "Základna a výška",
"Tip: výška je kolmá na základnu, ne délka šikmé strany.",
{
"Součet vnitřních úhlů trojúhelníku je 180°.",
"Třetí úhel je 180° minus součet dvou známých.",
"Úhly 60° a 50° doplňuje 70°.",
"Obsah je (základna · výška) / 2. Pro 8 a 5 je to 20.",
NULL,
},
},
{
"3 / 3 • Čtverec", "Doplnění na mřížce",
"Tip: osa y míří nahoru. Klepnutím na bod ho odeberete.",
{
"Čtverec má čtyři shodné strany a čtyři pravé úhly.",
"Druhé dva vrcholy vzniknou posunutím strany o stejnou délku kolmo.",
"U vodorovné strany leží zbývající vrcholy přímo nad ní, nebo pod ní.",
"Na mřížce jsou platné jen oba vrcholy téže strany.",
NULL,
},
},
};
return math_unit_page(0, &mat0_lessons[7], "mat0_unit8", "mat0_unit8_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat0_unit8_exercise_page(void) {
static const MathItem qs[] = {
{"Obdélník má strany 5 a 3. Jaký je jeho obvod?", "num:16", "2 · (5 + 3) = 16. Výsledek je 16."},
{"Obdélník má strany 6 a 4. Jaký je jeho obsah?", "num:24", "Obsah je 6 · 4 = 24. Výsledek je 24."},
{"Trojúhelník má základnu 8 a výšku 5. Jaký je jeho obsah?", "num:20", "Obsah je (8 · 5) / 2 = 20. Výsledek je 20."},
{"Dva úhly trojúhelníku jsou 60° a 50°. Jak velký je třetí úhel ve stupních?", "num:70", "Součet je 180°, takže třetí úhel je 70°. Výsledek je 70."},
{"Na mřížce je strana čtverce od A = [2; 3] k B = [5; 3]. Doplňte zbývající dva vrcholy.", "square:2,3;5,3", "Strana má délku 3. Druhé dva vrcholy jsou [2; 6] a [5; 6], nebo [2; 0] a [5; 0]."},
};
return math_practice_page(0, 7, "mat0unit8",
"mat0_ex8_title", "mat0_quiz8_head",
qs, G_N_ELEMENTS(qs));
}
void add_mat0_pages(GtkStack *stack) {
typedef GtkWidget *(*MathBuilder)(void);
static const struct {
const char *unit_name;
const char *ex_name;
MathBuilder build_unit;
MathBuilder build_ex;
} pages[] = {
{"mat0unit1", "mat0ex1", build_mat0_unit1_page, build_mat0_unit1_exercise_page},
{"mat0unit2", "mat0ex2", build_mat0_unit2_page, build_mat0_unit2_exercise_page},
{"mat0unit3", "mat0ex3", build_mat0_unit3_page, build_mat0_unit3_exercise_page},
{"mat0unit4", "mat0ex4", build_mat0_unit4_page, build_mat0_unit4_exercise_page},
{"mat0unit5", "mat0ex5", build_mat0_unit5_page, build_mat0_unit5_exercise_page},
{"mat0unit6", "mat0ex6", build_mat0_unit6_page, build_mat0_unit6_exercise_page},
{"mat0unit7", "mat0ex7", build_mat0_unit7_page, build_mat0_unit7_exercise_page},
{"mat0unit8", "mat0ex8", build_mat0_unit8_page, build_mat0_unit8_exercise_page},
};
for (guint i = 0; i < G_N_ELEMENTS(pages); i++) {
gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name);
gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name);
}
}

468
src/mat_year1.c Normal file
View file

@ -0,0 +1,468 @@
#include "graduately.h"
NetLesson mat_lessons[MATH_N] = {
{ .n_slides = 3, .unit_page = "matunit1", .ex_page = "matex1" },
{ .n_slides = 3, .unit_page = "matunit2", .ex_page = "matex2" },
{ .n_slides = 3, .unit_page = "matunit3", .ex_page = "matex3" },
{ .n_slides = 3, .unit_page = "matunit4", .ex_page = "matex4" },
{ .n_slides = 3, .unit_page = "matunit5", .ex_page = "matex5" },
{ .n_slides = 3, .unit_page = "matunit6", .ex_page = "matex6" },
{ .n_slides = 3, .unit_page = "matunit7", .ex_page = "matex7" },
{ .n_slides = 3, .unit_page = "matunit8", .ex_page = "matex8" },
};
static GtkWidget *build_mat_unit1_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Dělitel", "Čísla, která dělí beze zbytku",
"Tip: prvočíslo má právě dva dělitele, jedničku a sebe.",
{
"Dělitel čísla a je celé číslo, kterým a dělí beze zbytku.",
"Číslo 12 má dělitele 1, 2, 3, 4, 6 a 12, tedy šest dělitelů.",
"Prvočíslo nelze rozložit na součin dvou menších celých čísel větších než 1.",
"Jednička prvočíslo není.",
NULL,
},
},
{
"2 / 3 • NSD a NSN", "Společný dělitel a násobek",
"Tip: NSD · NSN dvou čísel je jejich součin.",
{
"Největší společný dělitel je největší číslo, které dělí obě čísla.",
"NSD(24, 36) = 12.",
"Nejmenší společný násobek je nejmenší kladné číslo, které je násobkem obou.",
"NSN(6, 8) = 24.",
NULL,
},
},
{
"3 / 3 • Rozklad", "Součin mocnin prvočísel",
"Tip: společné prvočinitele berte v nejmenší mocnině pro NSD.",
{
"Každé celé číslo větší než 1 má jediný rozklad na prvočísla.",
"72 = 2³ · 3².",
"V NSD se bere nižší exponent, v NSN vyšší.",
"Rozklad usnadní krácení zlomků i hledání společného jmenovatele.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[0], "mat_unit1", "mat_unit1_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit1_exercise_page(void) {
static const MathItem qs[] = {
{"Jaký je největší společný dělitel čísel 24 a 36?", "num:12", "Společní dělitelé jsou 1, 2, 3, 4, 6 a 12. Výsledek je 12."},
{"Jaký je nejmenší společný násobek čísel 6 a 8?", "num:24", "Násobky 6 jsou 6, 12, 18, 24, … a 24 je první z nich dělitelné osmi. Výsledek je 24."},
{"Spočítejte 2³ · 3².", "num:72", "8 · 9 = 72. Výsledek je 72."},
{"Kolik kladných dělitelů má číslo 12?", "num:6", "Jsou to 1, 2, 3, 4, 6 a 12. Výsledek je 6."},
};
return math_practice_page(1, 0, "matunit1",
"mat_ex1_title", "mat_quiz1_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit2_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Mocnina", "Opakované násobení",
"Tip: aⁿ je a násobené sebou n-krát.",
{
"2⁵ = 2 · 2 · 2 · 2 · 2 = 32.",
"10³ = 1000, tři nuly za jedničkou.",
"Každé číslo na první je ono samo a a⁰ = 1 pro a ≠ 0.",
"Záporný exponent znamená převrácenou hodnotu mocniny.",
NULL,
},
},
{
"2 / 3 • Pravidla", "Stejný základ",
"Tip: (aᵐ)ⁿ = aᵐ·ⁿ.",
{
"aᵐ · aⁿ = aᵐ⁺ⁿ.",
"aᵐ : aⁿ = aᵐ⁻ⁿ pro a ≠ 0.",
"(a · b)ⁿ = aⁿ · bⁿ.",
"(2³)² = 2⁶ = 64.",
NULL,
},
},
{
"3 / 3 • Odmocnina", "Druhá odmocnina",
"Tip: √a je nezáporné číslo, jehož druhá mocnina je a.",
{
"√81 = 9, protože 9² = 81.",
"√0 = 0 a √1 = 1.",
"Ze záporného čísla se v reálných číslech druhá odmocnina nedělá.",
"√(a²) = |a|, ne vždy a.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[1], "mat_unit2", "mat_unit2_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit2_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte 2⁵.", "num:32", "Pět dvojkových činitelů je 32. Výsledek je 32."},
{"Spočítejte 10³.", "num:1000", "10 · 10 · 10 = 1000. Výsledek je 1000."},
{"Spočítejte √81.", "num:9", "9 · 9 = 81. Výsledek je 9."},
{"Spočítejte (2³)².", "num:64", "(8)² = 64, nebo 2⁶ = 64. Výsledek je 64."},
};
return math_practice_page(1, 1, "matunit2",
"mat_ex2_title", "mat_quiz2_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit3_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Dosazení", "Písmeno je číslo",
"Tip: nejdřív závorky, potom mocniny a násobení.",
{
"Do výrazu se za proměnnou dosadí dané číslo.",
"2(x + 3) při x = 4 je 2 · 7 = 14.",
"3x − 2x + 5 se nejdřív sloučí na x + 5.",
"Při x = 2 je x + 5 = 7.",
NULL,
},
},
{
"2 / 3 • Vzorce", "Druhá mocnina dvojčlenu",
"Tip: (a + b)² není a² + b².",
{
"(a + b)² = a² + 2ab + b².",
"(a − b)² = a² − 2ab + b².",
"a² − b² = (a − b)(a + b).",
"Pro a = 3 a b = 1 je (a + b)² = 16.",
NULL,
},
},
{
"3 / 3 • Zkrácení", "Rozdíl čtverců",
"Tip: krátit lze jen pro x, která nejsou kořenem jmenovatele.",
{
"x² − 9 = (x − 3)(x + 3).",
"(x² − 9)/(x − 3) = x + 3 pro x ≠ 3.",
"Při x = 5 je hodnota 8.",
"Dosazení x = 3 by dělilo nulou, výraz tam není definován.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[2], "mat_unit3", "mat_unit3_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit3_exercise_page(void) {
static const MathItem qs[] = {
{"Do výrazu 2(x + 3) dosažte x = 4.", "num:14", "2 · (4 + 3) = 14. Výsledek je 14."},
{"Do výrazu (a + b)² dosažte a = 3 a b = 1.", "num:16", "4² = 16. Rozepsáno 9 + 6 + 1 = 16. Výsledek je 16."},
{"Do výrazu 3x − 2x + 5 dosažte x = 2.", "num:7", "Výraz je x + 5, při x = 2 je 7. Výsledek je 7."},
{"Do výrazu (x² − 9)/(x − 3) dosažte x = 5.", "num:8", "Pro x ≠ 3 je výraz roven x + 3, tedy 8. Výsledek je 8."},
};
return math_practice_page(1, 2, "matunit3",
"mat_ex3_title", "mat_quiz3_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit4_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Převod", "Neznámé na jednu stranu",
"Tip: člen s x se převádí s opačným znaménkem.",
{
"2x + 1 = 9 se odečtením jedné změní na 2x = 8.",
"x = 4.",
"Členy s x patří obvykle vlevo, čísla vpravo.",
"Po každé úpravě zůstává rovnost zachovaná.",
NULL,
},
},
{
"2 / 3 • Obě strany", "Stejný člen vlevo i vpravo",
"Tip: 5x − 3 = 2x + 9 se zbaví 2x odečtením.",
{
"5x − 2x = 9 + 3.",
"3x = 12 a x = 4.",
"Zkouška: levá strana 5 · 4 − 3 = 17, pravá 8 + 9 = 17.",
"Když koeficient u x vyjde 0, rovnice buď nemá řešení, nebo jich má nekonečně mnoho.",
NULL,
},
},
{
"3 / 3 • Závorka a zlomek", "Nejdřív roznásobit",
"Tip: x/2 + 1 = 5 se nejdřív zbaví jedničky.",
{
"x/2 + 1 = 5 dá x/2 = 4, tedy x = 8.",
"3(x − 2) = 12 se vydělí třemi: x − 2 = 4.",
"Nebo se závorka roznásobí: 3x − 6 = 12.",
"Obě cesty dají x = 6.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[3], "mat_unit4", "mat_unit4_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit4_exercise_page(void) {
static const MathItem qs[] = {
{"Vyřešte 2x + 1 = 9. Napište x.", "num:4", "2x = 8, takže x = 4. Výsledek je 4."},
{"Vyřešte 5x − 3 = 2x + 9. Napište x.", "num:4", "3x = 12, takže x = 4. Výsledek je 4."},
{"Vyřešte x/2 + 1 = 5. Napište x.", "num:8", "x/2 = 4, takže x = 8. Výsledek je 8."},
{"Vyřešte 3(x − 2) = 12. Napište x.", "num:6", "x − 2 = 4, takže x = 6. Výsledek je 6."},
};
return math_practice_page(1, 3, "matunit4",
"mat_ex4_title", "mat_quiz4_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit5_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Dráha", "Rychlost krát čas",
"Tip: jednotky času a rychlosti musí patřit k sobě.",
{
"Při stálé rychlosti je dráha s = v · t.",
"60 km/h po dobu 2,5 h urazí 150 km.",
"2,5 h je dvě a půl hodiny, ne dvě hodiny a padesát minut.",
"Z dráhy a času se rychlost dopočítá jako s/t.",
NULL,
},
},
{
"2 / 3 • Práce a směs", "Sčítají se výkony, ne časy",
"Tip: kdo práci udělá za 3 hodiny, udělá za hodinu třetinu.",
{
"První pracovník udělá za hodinu 1/6 práce, druhý 1/3.",
"Společně udělají 1/6 + 2/6 = 1/2 práce za hodinu, tedy celou za 2 hodiny.",
"U směsi se sčítá množství čisté látky.",
"2 kg dvacetiprocentní a 3 kg desetiprocentní směsi obsahují 0,4 + 0,3 = 0,7 kg, tedy 14 %.",
NULL,
},
},
{
"3 / 3 • Sleva", "Nová cena je část základu",
"Tip: po slevě 20 % zbývá 80 % původní ceny.",
{
"Když je nová cena 160 a sleva byla 20 %, je 160 rovno 80 % původní ceny.",
"Původní cena je 160 / 0,8 = 200.",
"Sleva v korunách je pak 40.",
"Nepleťte si slevu 20 % s tím, že se k nové ceně přičte 20 %.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[4], "mat_unit5", "mat_unit5_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit5_exercise_page(void) {
static const MathItem qs[] = {
{"Auto jede 2,5 hodiny rychlostí 60 km/h. Kolik kilometrů ujede?", "num:150", "s = v · t = 60 · 2,5 = 150. Výsledek je 150."},
{"První pracovník práci udělá za 6 hodin, druhý za 3 hodiny. Za kolik hodin ji udělají společně?", "num:2", "Za hodinu spolu udělají 1/2 práce, celou tedy za 2 hodiny. Výsledek je 2."},
{"Smíchají se 2 kg směsi s 20 % látky a 3 kg směsi s 10 % látky. Kolik procent látky má směs? Napište jen číslo.", "num:14", "Čisté látky je 0,7 kg z 5 kg, tedy 14 %. Výsledek je 14."},
{"Po slevě 20 % stojí zboží 160. Jaká byla původní cena?", "num:200", "160 je 80 % původní ceny, základ je 200. Výsledek je 200."},
};
return math_practice_page(1, 4, "matunit5",
"mat_ex5_title", "mat_quiz5_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit6_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Trojúhelník", "Součet 180°",
"Tip: rovnoramenný trojúhelník má dvě ramena a dva úhly u základny shodné.",
{
"Součet vnitřních úhlů každého trojúhelníku je 180°.",
"V rovnoramenném trojúhelníku jsou úhly při základně shodné.",
"Když jsou úhly při základně 70°, je úhel proti základně 40°.",
"Rovnostranný trojúhelník má všechny strany shodné a každý úhel 60°.",
NULL,
},
},
{
"2 / 3 • Vnější úhel", "Součet dvou vzdálených vnitřních",
"Tip: vnější úhel a sousední vnitřní úhel dávají 180°.",
{
"Vnější úhel vznikne prodloužením jedné strany.",
"Je roven součtu dvou vnitřních úhlů, které s ním nesousedí.",
"Vnitřní úhly 40° a 60° dávají vnější úhel 100° u zbývajícího vrcholu.",
"Součet vnějšího úhlu a přilehlého vnitřního je 180°.",
NULL,
},
},
{
"3 / 3 • Mnohoúhelník", "Pravidelný šestiúhelník",
"Tip: součet vnitřních úhlů n-úhelníku je (n − 2) · 180°.",
{
"Trojúhelník má součet 180°, čtyřúhelník 360°.",
"Obecný vzorec je (n − 2) · 180°.",
"Pravidelný mnohoúhelník má všechny strany i úhly shodné.",
"Pravidelný šestiúhelník má vnitřní úhel 720° / 6 = 120°.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[5], "mat_unit6", "mat_unit6_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit6_exercise_page(void) {
static const MathItem qs[] = {
{"Vnější úhel trojúhelníku je součet vnitřních úhlů 40° a 60°. Jak je velký ve stupních?", "num:100", "Vnější úhel se rovná součtu dvou nesousedních vnitřních úhlů: 100°. Výsledek je 100."},
{"Rovnoramenný trojúhelník má úhly při základně 70°. Jak velký je úhel proti základně?", "num:40", "180° − 140° = 40°. Výsledek je 40."},
{"Rovnostranný trojúhelník má stranu 6. Jaký je jeho obvod?", "num:18", "Tři shodné strany: 3 · 6 = 18. Výsledek je 18."},
{"Jak velký je jeden vnitřní úhel pravidelného šestiúhelníku ve stupních?", "num:120", "Součet je (6 − 2) · 180° = 720° a jeden úhel je 120°. Výsledek je 120."},
};
return math_practice_page(1, 5, "matunit6",
"mat_ex6_title", "mat_quiz6_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit7_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Kružnice", "Obvod a obsah",
"Tip: u výsledku s π nechte π ve výrazu a pište ho jako pi.",
{
"Kružnice s poloměrem r má obvod 2πr.",
"Obsah kruhu je πr².",
"Pro r = 7 je obvod 14π.",
"Pro r = 3 je obsah 9π.",
NULL,
},
},
{
"2 / 3 • Lichoběžník", "Dvě základny a výška",
"Tip: obsah je průměr základen krát výška.",
{
"Lichoběžník má právě jednu dvojici rovnoběžných stran, základny a a c.",
"Obsah je (a + c) · v / 2.",
"Základny 4 a 6 a výška 3 dávají obsah 15.",
"Výška je kolmá vzdálenost základen.",
NULL,
},
},
{
"3 / 3 • Pythagoras", "Pravoúhlý trojúhelník",
"Tip: přepona je nejdelší strana a leží proti pravému úhlu.",
{
"V pravoúhlém trojúhelníku je a² + b² = c².",
"c je přepona, a a b jsou odvěsny.",
"Trojúhelník 5, 12, 13 je pravoúhlý, protože 25 + 144 = 169.",
"Přepona je 13.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[6], "mat_unit7", "mat_unit7_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit7_exercise_page(void) {
static const MathItem qs[] = {
{"Jaký je obvod kružnice s poloměrem 7? Nechte výsledek v násobcích π.", "num:14*pi", "Obvod je 2πr = 14π. Výsledek je 14π."},
{"Jaký je obsah kruhu s poloměrem 3? Nechte výsledek v násobcích π.", "num:9*pi", "Obsah je πr² = 9π. Výsledek je 9π."},
{"Lichoběžník má základny 4 a 6 a výšku 3. Jaký je jeho obsah?", "num:15", "(4 + 6) · 3 / 2 = 15. Výsledek je 15."},
{"Pravoúhlý trojúhelník má odvěsny 5 a 12. Jak dlouhá je přepona?", "num:13", "5² + 12² = 25 + 144 = 169 = 13². Výsledek je 13."},
};
return math_practice_page(1, 6, "matunit7",
"mat_ex7_title", "mat_quiz7_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat_unit8_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Body", "Vzdálenost na mřížce",
"Tip: u svislé nebo vodorovné úsečky stačí rozdíl souřadnic.",
{
"Bod v rovině má souřadnice [x; y].",
"Vzdálenost [x₁; y₁] a [x₂; y₂] je √((x₂ − x₁)² + (y₂ − y₁)²).",
"A = [1; 1] a B = [5; 1] mají vzdálenost 4.",
"Osa y na naší mřížce míří nahoru.",
NULL,
},
},
{
"2 / 3 • SSS", "Trojúhelník ze tří stran",
"Tip: třetí vrchol je průsečík dvou kružnic.",
{
"Konstrukce SSS použije tři strany.",
"Z bodu A se rýsuje kružnice o poloměru |AC|, z bodu B o poloměru |BC|.",
"Kružnice se protnou ve dvou bodech, pokud trojúhelník existuje.",
"Trojúhelníková nerovnost: součet dvou stran je větší než třetí.",
NULL,
},
},
{
"3 / 3 • Příklad", "Strany 4, 3 a 5",
"Tip: 3² + 4² = 5², pravý úhel je u bodu A.",
{
"A = [1; 1], B = [5; 1], takže |AB| = 4.",
"Hledáme C tak, aby |AC| = 3 a |BC| = 5.",
"Na mřížce od 0 do 10 a od 0 do 8 leží jen C = [1; 4].",
"Druhý průsečík [1; −2] je pod mřížkou.",
NULL,
},
},
};
return math_unit_page(1, &mat_lessons[7], "mat_unit8", "mat_unit8_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat_unit8_exercise_page(void) {
static const MathItem qs[] = {
{"Jaká je vzdálenost bodů A = [1; 1] a B = [5; 1]?", "num:4", "Body mají stejné y, vzdálenost je 5 − 1 = 4. Výsledek je 4."},
{"Jaký je obsah pravoúhlého trojúhelníku s odvěsnami 3 a 4?", "num:6", "Obsah je (3 · 4) / 2 = 6. Výsledek je 6."},
{"Jaký je obvod trojúhelníku se stranami 3, 4 a 5?", "num:12", "3 + 4 + 5 = 12. Výsledek je 12."},
{"A = [1; 1] a B = [5; 1] jsou dané. Zakreslete C, pro které je |AC| = 3 a |BC| = 5.", "points:1,4;fix:1,1;fix:5,1", "Kružnice se na mřížce protnou v [1; 4]. Bod [1; −2] už na ní není."},
};
return math_practice_page(1, 7, "matunit8",
"mat_ex8_title", "mat_quiz8_head",
qs, G_N_ELEMENTS(qs));
}
void add_mat_pages(GtkStack *stack) {
typedef GtkWidget *(*MathBuilder)(void);
static const struct {
const char *unit_name;
const char *ex_name;
MathBuilder build_unit;
MathBuilder build_ex;
} pages[] = {
{"matunit1", "matex1", build_mat_unit1_page, build_mat_unit1_exercise_page},
{"matunit2", "matex2", build_mat_unit2_page, build_mat_unit2_exercise_page},
{"matunit3", "matex3", build_mat_unit3_page, build_mat_unit3_exercise_page},
{"matunit4", "matex4", build_mat_unit4_page, build_mat_unit4_exercise_page},
{"matunit5", "matex5", build_mat_unit5_page, build_mat_unit5_exercise_page},
{"matunit6", "matex6", build_mat_unit6_page, build_mat_unit6_exercise_page},
{"matunit7", "matex7", build_mat_unit7_page, build_mat_unit7_exercise_page},
{"matunit8", "matex8", build_mat_unit8_page, build_mat_unit8_exercise_page},
};
for (guint i = 0; i < G_N_ELEMENTS(pages); i++) {
gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name);
gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name);
}
}

468
src/mat_year2.c Normal file
View file

@ -0,0 +1,468 @@
#include "graduately.h"
NetLesson mat2_lessons[MATH_N] = {
{ .n_slides = 3, .unit_page = "mat2unit1", .ex_page = "mat2ex1" },
{ .n_slides = 3, .unit_page = "mat2unit2", .ex_page = "mat2ex2" },
{ .n_slides = 3, .unit_page = "mat2unit3", .ex_page = "mat2ex3" },
{ .n_slides = 3, .unit_page = "mat2unit4", .ex_page = "mat2ex4" },
{ .n_slides = 3, .unit_page = "mat2unit5", .ex_page = "mat2ex5" },
{ .n_slides = 3, .unit_page = "mat2unit6", .ex_page = "mat2ex6" },
{ .n_slides = 3, .unit_page = "mat2unit7", .ex_page = "mat2ex7" },
{ .n_slides = 3, .unit_page = "mat2unit8", .ex_page = "mat2ex8" },
};
static GtkWidget *build_mat2_unit1_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Předpis", "y = ax + b",
"Tip: a je směrnice, b je úsek na ose y.",
{
"Lineární funkce má předpis f(x) = ax + b.",
"Grafem je přímka.",
"b je hodnota f(0), tedy průsečík s osou y.",
"Pro f(x) = 2x − 3 je průsečík s osou y číslo −3.",
NULL,
},
},
{
"2 / 3 • Směrnice", "O kolik y vzroste",
"Tip: směrnice je podíl změny y a změny x.",
{
"Mezi body [x₁; y₁] a [x₂; y₂] je a = (y₂ − y₁)/(x₂ − x₁).",
"Body [1; 2] a [3; 8] dávají směrnici (8 − 2)/(3 − 1) = 3.",
"Kladná směrnice znamená růst, záporná pokles.",
"Nulová směrnice je vodorovná přímka.",
NULL,
},
},
{
"3 / 3 • Hodnota a kořen", "Dosazení a rovnice",
"Tip: nulový bod je x, pro které je f(x) = 0.",
{
"f(4) u funkce 2x − 3 je 8 − 3 = 5.",
"Rovnice 2x − 6 = 0 má řešení x = 3.",
"To je průsečík přímky y = 2x − 6 s osou x.",
"Lineární funkce s a ≠ 0 má právě jeden nulový bod.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[0], "mat2_unit1", "mat2_unit1_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit1_exercise_page(void) {
static const MathItem qs[] = {
{"Funkce f(x) = 2x − 3. Kolik je f(4)?", "num:5", "2 · 4 − 3 = 5. Výsledek je 5."},
{"Jaký je průsečík funkce f(x) = 2x − 3 s osou y?", "num:-3", "f(0) = −3. Výsledek je -3."},
{"Jaká je směrnice přímky body [1; 2] a [3; 8]?", "num:3", "(8 − 2)/(3 − 1) = 3. Výsledek je 3."},
{"Vyřešte 2x − 6 = 0. Napište x.", "num:3", "2x = 6, takže x = 3. Výsledek je 3."},
};
return math_practice_page(2, 0, "mat2unit1",
"mat2_ex1_title", "mat2_quiz1_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit2_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Dvojice", "Jedno x a jedno y",
"Tip: výsledek pište x;y, nejdřív x.",
{
"Soustava dvou lineárních rovnic hledá dvojici [x; y], která vyhovuje oběma.",
"Řešením je průsečík dvou přímek.",
"Sčítací metoda sečte rovnice tak, aby jedna neznámá zmizela.",
"Dosazovací metoda vyjádří jednu neznámou a dosadí ji do druhé rovnice.",
NULL,
},
},
{
"2 / 3 • Sčítání", "Opačné koeficienty",
"Tip: x + y = 10 a x − y = 2 se sčítají na 2x = 12.",
{
"Součet dá 2x = 12, tedy x = 6.",
"Z x + y = 10 je y = 4.",
"Zkouška ve druhé rovnici: 6 − 4 = 2.",
"Odečtení rovnic se použije, když má neznámá v obou stejný koeficient.",
NULL,
},
},
{
"3 / 3 • Odečtení", "Stejný koeficient",
"Tip: rovnice lze před odečtením násobit.",
{
"2x + y = 8 a x + y = 5 se odečtou na x = 3.",
"Pak y = 2.",
"3x + y = 10 a x + y = 6 dají 2x = 4, tedy x = 2 a y = 4.",
"x + y = 7 a x − y = 1 dají x = 4 a y = 3.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[1], "mat2_unit2", "mat2_unit2_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit2_exercise_page(void) {
static const MathItem qs[] = {
{"Vyřešte soustavu x + y = 10 a x − y = 2. Napište x a y oddělené středníkem.", "pair:6;4", "Sečtení dá 2x = 12, x = 6 a y = 4. Napište 6;4."},
{"Vyřešte soustavu 2x + y = 8 a x + y = 5. Napište x a y oddělené středníkem.", "pair:3;2", "Odečtení dá x = 3 a y = 2. Napište 3;2."},
{"Vyřešte soustavu x + y = 7 a x − y = 1. Napište x a y oddělené středníkem.", "pair:4;3", "Sečtení dá 2x = 8, x = 4 a y = 3. Napište 4;3."},
{"Vyřešte soustavu 3x + y = 10 a x + y = 6. Napište x a y oddělené středníkem.", "pair:2;4", "Odečtení dá 2x = 4, x = 2 a y = 4. Napište 2;4."},
};
return math_practice_page(2, 1, "mat2unit2",
"mat2_ex2_title", "mat2_quiz2_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit3_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Tvar", "ax² + bx + c = 0",
"Tip: kořeny pište oddělené středníkem, pořadí nehraje roli.",
{
"Kvadratická rovnice má a ≠ 0.",
"Kořeny rovnice x² − 5x + 6 = 0 jsou 2 a 3, protože (x − 2)(x − 3) = 0.",
"x² − 4 = 0 má kořeny −2 a 2.",
"Součin je nula, právě když je nula aspoň jeden činitel.",
NULL,
},
},
{
"2 / 3 • Diskriminant", "D = b² − 4ac",
"Tip: D > 0 znamená dva různé reálné kořeny.",
{
"Pro ax² + bx + c = 0 je D = b² − 4ac.",
"U x² − 2x − 3 je a = 1, b = −2, c = −3 a D = 4 + 12 = 16.",
"D = 0 znamená jeden dvojnásobný kořen.",
"D < 0 znamená, že v reálných číslech řešení není.",
NULL,
},
},
{
"3 / 3 • Vzorec", "Kořen je (−b ± √D) / (2a)",
"Tip: dvojnásobný kořen x² + 6x + 9 = 0 je −3.",
{
"x = (−b ± √D) / (2a).",
"x² + 6x + 9 = (x + 3)², takže kořen −3 je dvojnásobný.",
"U dvojnásobného kořene stačí napsat číslo jednou.",
"Zkouška dosadí kořen zpět do rovnice.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[2], "mat2_unit3", "mat2_unit3_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit3_exercise_page(void) {
static const MathItem qs[] = {
{"Najděte kořeny rovnice x² − 5x + 6 = 0. Oddělte je středníkem.", "set:2;3", "(x − 2)(x − 3) = 0. Napište 2;3."},
{"Najděte kořeny rovnice x² − 4 = 0. Oddělte je středníkem.", "set:-2;2", "x² = 4, takže x = −2 nebo x = 2. Napište -2;2."},
{"Jaký je diskriminant rovnice x² − 2x − 3 = 0?", "num:16", "D = (−2)² − 4 · 1 · (−3) = 4 + 12 = 16. Výsledek je 16."},
{"Jaký je dvojnásobný kořen rovnice x² + 6x + 9 = 0?", "num:-3", "(x + 3)² = 0, kořen je −3. Výsledek je -3."},
};
return math_practice_page(2, 2, "mat2unit3",
"mat2_ex3_title", "mat2_quiz3_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit4_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Obor", "Jmenovatel nesmí být nula",
"Tip: podmínku pište dřív, než krátíte.",
{
"Lomený výraz je podíl dvou mnohočlenů.",
"Není definován tam, kde je jmenovatel nula.",
"(x + 2)/(x − 2) nemá smysl pro x = 2.",
"Po krácení se vyloučené hodnoty do oboru nevracejí.",
NULL,
},
},
{
"2 / 3 • Krácení", "Rozdíl čtverců",
"Tip: x² − 1 = (x − 1)(x + 1).",
{
"(x² − 1)/(x − 1) = x + 1 pro x ≠ 1.",
"Při x = 4 je hodnota 5.",
"(x² − 4)/(x + 2) = x − 2 pro x ≠ −2.",
"Při x = 5 je hodnota 3.",
NULL,
},
},
{
"3 / 3 • Násobení", "Čitatel s čitatelem",
"Tip: x ve jmenovateli se zkrátí jen pro x ≠ 0.",
{
"Zlomky se násobí stejně jako číselné.",
"(2x/4) · (6/x) = 12x/(4x) = 3 pro x ≠ 0.",
"6x/(3x) = 2 pro x ≠ 0.",
"Než se zkrátí písmeno, ověří se, že není nula.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[3], "mat2_unit4", "mat2_unit4_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit4_exercise_page(void) {
static const MathItem qs[] = {
{"Do výrazu (x² − 1)/(x − 1) dosažte x = 4.", "num:5", "Pro x ≠ 1 je výraz x + 1, tedy 5. Výsledek je 5."},
{"Pro které x nemá výraz (x + 2)/(x − 2) smysl?", "num:2", "Jmenovatel x − 2 je nula právě při x = 2. Výsledek je 2."},
{"Upravte (2x/4) · (6/x) pro x ≠ 0.", "num:3", "12x/(4x) = 3. Výsledek je 3."},
{"Do výrazu (x² − 4)/(x + 2) dosažte x = 5.", "num:3", "Pro x ≠ −2 je výraz x − 2, tedy 3. Výsledek je 3."},
};
return math_practice_page(2, 3, "mat2unit4",
"mat2_ex4_title", "mat2_quiz4_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit5_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Odmocnina jako exponent", "a^(1/n)",
"Tip: a^(1/2) je druhá odmocnina z a.",
{
"Pro a > 0 je a^(1/n) n-tá odmocnina z a.",
"16^(1/2) = 4.",
"a^(m/n) je n-tá odmocnina z aᵐ, nebo m-tá mocnina odmocniny.",
"8^(2/3) = (8^(1/3))² = 2² = 4.",
NULL,
},
},
{
"2 / 3 • Stejný základ", "Exponenty se sčítají",
"Tip: 3² · 3³ = 3⁵.",
{
"aᵐ · aⁿ = aᵐ⁺ⁿ.",
"3² · 3³ = 3⁵ = 243.",
"(aᵐ)ⁿ = aᵐ·ⁿ.",
"Základ musí být při sčítání exponentů stejný.",
NULL,
},
},
{
"3 / 3 • Záporný exponent", "Převrácená hodnota",
"Tip: a⁻ⁿ = 1/aⁿ pro a ≠ 0.",
{
"10⁻² = 1/100.",
"10⁻¹ = 0,1 a 10⁰ = 1.",
"Záporný exponent nepřevrací znaménko základu, ale celou mocninu.",
"(−2)⁻² = 1/4, protože (−2)² = 4.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[4], "mat2_unit5", "mat2_unit5_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit5_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte 16^(1/2).", "num:4", "Druhá odmocnina z 16 je 4. Výsledek je 4."},
{"Spočítejte 8^(2/3).", "num:4", "Třetí odmocnina z 8 je 2 a 2² = 4. Výsledek je 4."},
{"Spočítejte 3² · 3³.", "num:243", "3⁵ = 243. Výsledek je 243."},
{"Spočítejte 10⁻².", "num:1/100", "10⁻² = 1/10² = 1/100. Výsledek je 1/100."},
};
return math_practice_page(2, 4, "mat2unit5",
"mat2_ex5_title", "mat2_quiz5_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit6_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Poměry", "Odvěsny a přepona",
"Tip: sinus je protilehlá odvěsna ku přeponě.",
{
"V pravoúhlém trojúhelníku je sin α = protilehlá / přepona.",
"cos α = přilehlá / přepona.",
"tan α = protilehlá / přilehlá.",
"Přepona je strana proti pravému úhlu.",
NULL,
},
},
{
"2 / 3 • Tabulkové úhly", "30°, 45° a 60°",
"Tip: sinus 30° je 1/2.",
{
"sin 30° = 1/2 a cos 60° = 1/2.",
"sin 60° = cos 30° = √3/2.",
"sin 45° = cos 45° = √2/2.",
"tan 45° = 1.",
NULL,
},
},
{
"3 / 3 • Dopočet strany", "Přepona a úhel",
"Tip: proti úhlu 30° je polovina přepony.",
{
"Známe-li přeponu a úhel, protilehlá odvěsna je přepona · sin α.",
"Proti 30° při přeponě 10 je odvěsna 5.",
"Přilehlá odvěsna by byla 10 · cos 30°.",
"Tangens se hodí, když jsou známé dvě odvěsny a ne přepona.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[5], "mat2_unit6", "mat2_unit6_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit6_exercise_page(void) {
static const MathItem qs[] = {
{"Kolik je sin 30°?", "num:1/2", "Sinus 30° je 1/2. Výsledek je 1/2."},
{"Kolik je cos 60°?", "num:1/2", "Kosinus 60° je 1/2. Výsledek je 1/2."},
{"Kolik je tan 45°?", "num:1", "Protilehlá a přilehlá odvěsna jsou u 45° stejně dlouhé. Výsledek je 1."},
{"Přepona pravoúhlého trojúhelníku je 10 a jeden úhel je 30°. Jak dlouhá je odvěsna proti tomuto úhlu?", "num:5", "10 · sin 30° = 10 · 1/2 = 5. Výsledek je 5."},
};
return math_practice_page(2, 5, "mat2unit6",
"mat2_ex6_title", "mat2_quiz6_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit7_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Pythagoras", "a² + b² = c²",
"Tip: 8² + 15² = 64 + 225 = 289 = 17².",
{
"V pravoúhlém trojúhelníku se součet čtverců nad odvěsnami rovná čtverci nad přeponou.",
"Trojúhelník 8, 15, 17 je pravoúhlý a přepona je 17.",
"Čtverec nad přeponou trojúhelníku 3, 4, 5 má obsah 25.",
"Věta platí jen v pravoúhlém trojúhelníku.",
NULL,
},
},
{
"2 / 3 • Eukleides", "Úsek přepony",
"Tip: a² = c · a_c, kde a_c je úsek přepony přilehlý k odvěsně a.",
{
"Odvěsna je geometrický průměr přepony a svého úseku.",
"a² = c · a_c a b² = c · b_c.",
"Úseky a_c a b_c dají dohromady přeponu c.",
"U odvěsen 6 a 8 je přepona 10 a úsek u odvěsny 6 je 36/10 = 18/5.",
NULL,
},
},
{
"3 / 3 • Výška", "v² = a_c · b_c",
"Tip: také platí a · b = c · v.",
{
"Výška k přeponě je geometrický průměr obou úseků.",
"Z a · b = c · v plyne v = ab/c.",
"Pro odvěsny 6 a 8 a přeponu 10 je v = 48/10 = 24/5.",
"Obsah trojúhelníku je pak (c · v) / 2, stejně jako (a · b) / 2.",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[6], "mat2_unit7", "mat2_unit7_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit7_exercise_page(void) {
static const MathItem qs[] = {
{"Pravoúhlý trojúhelník má odvěsny 8 a 15. Jak dlouhá je přepona?", "num:17", "64 + 225 = 289 = 17². Výsledek je 17."},
{"Jaký obsah má čtverec sestrojený nad přeponou pravoúhlého trojúhelníku se stranami 3, 4 a 5?", "num:25", "Přepona je 5 a 5² = 25. Výsledek je 25."},
{"Odvěsny jsou 6 a 8, přepona je 10. Jak dlouhý je úsek přepony přilehlý k odvěsně 6?", "num:18/5", "a² = c · a_c, takže a_c = 36/10 = 18/5. Výsledek je 18/5."},
{"Odvěsny jsou 6 a 8, přepona je 10. Jak dlouhá je výška k přeponě?", "num:24/5", "v = ab/c = 48/10 = 24/5. Výsledek je 24/5."},
};
return math_practice_page(2, 6, "mat2unit7",
"mat2_ex7_title", "mat2_quiz7_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat2_unit8_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Úhly", "Obvodový je polovina středového",
"Tip: oba úhly musí být nad stejným obloukem.",
{
"Středový úhel má vrchol ve středu kružnice.",
"Obvodový úhel má vrchol na kružnici.",
"Nad stejným obloukem je obvodový úhel polovinou středového.",
"Středový úhel 80° patří k obvodovému úhlu 40°.",
NULL,
},
},
{
"2 / 3 • Oblouk", "Část obvodu",
"Tip: délka oblouku je (α/360°) · 2πr.",
{
"Celý obvod je 2πr.",
"Oblouk 60° je šestina obvodu.",
"Při r = 6 je jeho délka 2π.",
"Výsledek nechte v násobcích π.",
NULL,
},
},
{
"3 / 3 • Thales", "Pravý úhel nad průměrem",
"Tip: vrchol pravého úhlu leží na kružnici s průměrem AB.",
{
"Thaletova věta: úhel nad průměrem je pravý.",
"Je-li AB průměr, je úhel ACB pravý pro každé C na kružnici různé od A a B.",
"Na mřížce má průměr od [2; 4] do [8; 4] střed [5; 4] a poloměr 3.",
"Vyhovují body [5; 7] a [5; 1].",
NULL,
},
},
};
return math_unit_page(2, &mat2_lessons[7], "mat2_unit8", "mat2_unit8_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat2_unit8_exercise_page(void) {
static const MathItem qs[] = {
{"Středový úhel je 80°. Jak velký je obvodový úhel nad stejným obloukem ve stupních?", "num:40", "Obvodový úhel je polovina středového. Výsledek je 40."},
{"Kružnice má poloměr 6. Jak dlouhý je oblouk 60°? Nechte π ve výsledku.", "num:2*pi", "(60/360) · 2π · 6 = 2π. Výsledek je 2π."},
{"Obvodový úhel je 35°. Jak velký je středový úhel nad stejným obloukem ve stupních?", "num:70", "Středový úhel je dvojnásobek obvodového. Výsledek je 70."},
{"AB je průměr od [2; 4] do [8; 4]. Zakreslete bod C na kružnici, ve kterém je úhel ACB pravý.", "right:2,4;8,4", "Thaletova kružnice má střed [5; 4] a poloměr 3. Na mřížce vyhovuje [5; 7] nebo [5; 1]."},
};
return math_practice_page(2, 7, "mat2unit8",
"mat2_ex8_title", "mat2_quiz8_head",
qs, G_N_ELEMENTS(qs));
}
void add_mat2_pages(GtkStack *stack) {
typedef GtkWidget *(*MathBuilder)(void);
static const struct {
const char *unit_name;
const char *ex_name;
MathBuilder build_unit;
MathBuilder build_ex;
} pages[] = {
{"mat2unit1", "mat2ex1", build_mat2_unit1_page, build_mat2_unit1_exercise_page},
{"mat2unit2", "mat2ex2", build_mat2_unit2_page, build_mat2_unit2_exercise_page},
{"mat2unit3", "mat2ex3", build_mat2_unit3_page, build_mat2_unit3_exercise_page},
{"mat2unit4", "mat2ex4", build_mat2_unit4_page, build_mat2_unit4_exercise_page},
{"mat2unit5", "mat2ex5", build_mat2_unit5_page, build_mat2_unit5_exercise_page},
{"mat2unit6", "mat2ex6", build_mat2_unit6_page, build_mat2_unit6_exercise_page},
{"mat2unit7", "mat2ex7", build_mat2_unit7_page, build_mat2_unit7_exercise_page},
{"mat2unit8", "mat2ex8", build_mat2_unit8_page, build_mat2_unit8_exercise_page},
};
for (guint i = 0; i < G_N_ELEMENTS(pages); i++) {
gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name);
gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name);
}
}

468
src/mat_year3.c Normal file
View file

@ -0,0 +1,468 @@
#include "graduately.h"
NetLesson mat3_lessons[MATH_N] = {
{ .n_slides = 3, .unit_page = "mat3unit1", .ex_page = "mat3ex1" },
{ .n_slides = 3, .unit_page = "mat3unit2", .ex_page = "mat3ex2" },
{ .n_slides = 3, .unit_page = "mat3unit3", .ex_page = "mat3ex3" },
{ .n_slides = 3, .unit_page = "mat3unit4", .ex_page = "mat3ex4" },
{ .n_slides = 3, .unit_page = "mat3unit5", .ex_page = "mat3ex5" },
{ .n_slides = 3, .unit_page = "mat3unit6", .ex_page = "mat3ex6" },
{ .n_slides = 3, .unit_page = "mat3unit7", .ex_page = "mat3ex7" },
{ .n_slides = 3, .unit_page = "mat3unit8", .ex_page = "mat3ex8" },
};
static GtkWidget *build_mat3_unit1_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Parabola", "f(x) = ax² + bx + c",
"Tip: pro a > 0 má parabola minimum, pro a < 0 maximum.",
{
"Grafem kvadratické funkce je parabola.",
"Průsečík s osou y je bod [0; c].",
"U f(x) = x² − 4x + 3 je f(0) = 3.",
"Osa paraboly je svislá.",
NULL,
},
},
{
"2 / 3 • Vrchol", "x = −b/(2a)",
"Tip: souřadnice vrcholu pište x;y.",
{
"x-ová souřadnice vrcholu je −b/(2a).",
"U x² − 4x + 3 je to −(−4)/2 = 2.",
"f(2) = 4 − 8 + 3 = −1.",
"Vrchol je tedy [2; −1].",
NULL,
},
},
{
"3 / 3 • Kořeny", "Průsečíky s osou x",
"Tip: kořeny jsou řešení f(x) = 0.",
{
"x² − 4x + 3 = (x − 1)(x − 3).",
"Kořeny jsou 1 a 3.",
"Leží symetricky kolem osy paraboly.",
"f(4) = 16 − 16 + 3 = 3, stejně jako f(0).",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[0], "mat3_unit1", "mat3_unit1_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit1_exercise_page(void) {
static const MathItem qs[] = {
{"Najděte vrchol funkce f(x) = x² − 4x + 3. Napište x a y oddělené středníkem.", "pair:2;-1", "x = 4/2 = 2 a f(2) = −1. Napište 2;-1."},
{"Najděte kořeny funkce f(x) = x² − 4x + 3. Oddělte je středníkem.", "set:1;3", "(x − 1)(x − 3) = 0. Napište 1;3."},
{"Kolik je f(0) u funkce f(x) = x² − 4x + 3?", "num:3", "f(0) = 3, to je absolutní člen. Výsledek je 3."},
{"Kolik je f(4) u funkce f(x) = x² − 4x + 3?", "num:3", "16 − 16 + 3 = 3. Výsledek je 3."},
};
return math_practice_page(3, 0, "mat3unit1",
"mat3_ex1_title", "mat3_quiz1_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit2_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Definice", "Logaritmus je exponent",
"Tip: log_b(x) = y právě tehdy, když bʸ = x.",
{
"Základ logaritmu je kladný a různý od 1.",
"Argument logaritmu je kladný.",
"log₁₀(1000) = 3, protože 10³ = 1000.",
"log₂(32) = 5, protože 2⁵ = 32.",
NULL,
},
},
{
"2 / 3 • Rovnice", "Stejný základ",
"Tip: 2ˣ = 16 se přepíše jako 2ˣ = 2⁴.",
{
"Exponentialní rovnice se stejným základem porovná exponenty.",
"2ˣ = 16 má řešení x = 4.",
"log₃(81) = 4, protože 3⁴ = 81.",
"log_b(b) = 1 a log_b(1) = 0.",
NULL,
},
},
{
"3 / 3 • Pravidla", "Součin a podíl",
"Tip: logaritmus součinu je součet logaritmů.",
{
"log_b(xy) = log_b(x) + log_b(y).",
"log_b(x/y) = log_b(x) − log_b(y).",
"log_b(xⁿ) = n · log_b(x).",
"Těmito pravidly se argument zjednodušuje, ne základ.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[1], "mat3_unit2", "mat3_unit2_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit2_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte logaritmus o základu 10 z 1000.", "num:3", "10³ = 1000, takže logaritmus je 3. Výsledek je 3."},
{"Spočítejte logaritmus o základu 2 z 32.", "num:5", "2⁵ = 32. Výsledek je 5."},
{"Vyřešte 2ˣ = 16. Napište x.", "num:4", "16 = 2⁴, takže x = 4. Výsledek je 4."},
{"Spočítejte logaritmus o základu 3 z 81.", "num:4", "3⁴ = 81. Výsledek je 4."},
};
return math_practice_page(3, 1, "mat3unit2",
"mat3_ex2_title", "mat3_quiz2_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit3_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Jednotková kružnice", "Úhel od kladné poloosy x",
"Tip: sinus je y-ová souřadnice, kosinus x-ová.",
{
"Bod na jednotkové kružnici pod úhlem α má souřadnice [cos α; sin α].",
"cos 0° = 1 a sin 0° = 0.",
"sin 90° = 1 a cos 90° = 0.",
"cos 180° = −1 a sin 180° = 0.",
NULL,
},
},
{
"2 / 3 • 45°", "Polovina pravého úhlu",
"Tip: sin 45° = cos 45° = √2/2.",
{
"Úhel 45° půlí čtvrtkružnici.",
"Souřadnice bodu splňují x = y a x² + y² = 1.",
"Kladné řešení je √2/2.",
"Odpověď lze psát jako sqrt(2)/2 nebo 1/sqrt(2).",
NULL,
},
},
{
"3 / 3 • Znaménka", "Čtyři kvadranty",
"Tip: v prvním kvadrantu jsou sinus i kosinus kladné.",
{
"Ve druhém kvadrantu je sinus kladný a kosinus záporný.",
"Ve třetím jsou oba záporné.",
"Ve čtvrtém je kosinus kladný a sinus záporný.",
"Tangens je sinus dělený kosinem a není definován při cos α = 0.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[2], "mat3_unit3", "mat3_unit3_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit3_exercise_page(void) {
static const MathItem qs[] = {
{"Kolik je sin 90°?", "num:1", "Bod [0; 1] má y-ovou souřadnici 1. Výsledek je 1."},
{"Kolik je cos 180°?", "num:-1", "Bod [−1; 0] má x-ovou souřadnici −1. Výsledek je -1."},
{"Kolik je sin 45°? Můžete psát sqrt(2)/2.", "num:sqrt(2)/2", "sin 45° = √2/2. Stejnou hodnotu má 1/√2."},
{"Kolik je cos 0°?", "num:1", "Úhel 0° je bod [1; 0]. Výsledek je 1."},
};
return math_practice_page(3, 2, "mat3unit3",
"mat3_ex3_title", "mat3_quiz3_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit4_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Sinová věta", "a/sin α = 2R",
"Tip: poměr strany a sinu protilehlého úhlu je pro všechny strany stejný.",
{
"a/sin α = b/sin β = c/sin γ = 2R.",
"R je poloměr kružnice opsané.",
"Když a = 10 a α = 30°, je 10 / (1/2) = 20 = 2R.",
"Poloměr R je 10.",
NULL,
},
},
{
"2 / 3 • Kosinová věta", "c² = a² + b² − 2ab cos γ",
"Tip: pro pravý úhel je cos 90° = 0 a věta přejde v Pythagora.",
{
"Kosinová věta dopočítá stranu proti známému úhlu.",
"Jsou-li dvě strany 2 a sevřený úhel 60°, je třetí strana také 2.",
"a² = 4 + 4 − 2 · 2 · 2 · cos 60° = 8 − 8 · 1/2 = 4.",
"Rovnostranný trojúhelník má všechny úhly 60°.",
NULL,
},
},
{
"3 / 3 • Obsah", "Dvě strany a sevřený úhel",
"Tip: S = (1/2) · b · c · sin α.",
{
"Obsah je polovina součinu dvou stran a sinu sevřeného úhlu.",
"(1/2) · 4 · 6 · sin 30° = 12 · 1/2 = 6.",
"Sinová věta také dopočítá další stranu: b = a · sin β / sin α.",
"Pro a = 8, α = 30° a β = 45° je b = 8√2.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[3], "mat3_unit4", "mat3_unit4_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit4_exercise_page(void) {
static const MathItem qs[] = {
{"Strana a = 10 leží proti úhlu 30°. Jaký je poloměr kružnice opsané?", "num:10", "a/sin 30° = 20 = 2R, takže R = 10. Výsledek je 10."},
{"Dvě strany trojúhelníku mají délku 2 a svírají úhel 60°. Jak dlouhá je třetí strana?", "num:2", "a² = 4 + 4 − 8 · 1/2 = 4, takže a = 2. Výsledek je 2."},
{"Dvě strany mají délky 4 a 6 a svírají úhel 30°. Jaký je obsah trojúhelníku?", "num:6", "(1/2) · 4 · 6 · 1/2 = 6. Výsledek je 6."},
{"Strana a = 8 je proti 30° a úhel β je 45°. Jak dlouhá je strana b? Sin 45° je √2/2.", "num:8*sqrt(2)", "b = 8 · sin 45° / sin 30° = 8 · (√2/2) / (1/2) = 8√2."},
};
return math_practice_page(3, 3, "mat3unit4",
"mat3_ex4_title", "mat3_quiz4_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit5_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Aritmetická", "Stálý rozdíl",
"Tip: aₙ = a₁ + (n − 1)d.",
{
"Aritmetická posloupnost přičítá stále stejný rozdíl d.",
"Pro a₁ = 3 a d = 2 je a₅ = 3 + 4 · 2 = 11.",
"Součet prvních n členů je sₙ = n/2 · (2a₁ + (n − 1)d).",
"s₅ = 5/2 · (6 + 8) = 35.",
NULL,
},
},
{
"2 / 3 • Geometrická", "Stálý kvocient",
"Tip: aₙ = a₁ · qⁿ⁻¹.",
{
"Geometrická posloupnost násobí stále stejným kvocientem q.",
"Pro a₁ = 2 a q = 3 je a₄ = 2 · 3³ = 54.",
"q = 1 znamená, že se členy nemění.",
"q = 0 je po prvním členu samá nula, pokud je první člen definován.",
NULL,
},
},
{
"3 / 3 • Nekonečná řada", "Součet pro |q| < 1",
"Tip: s = a₁ / (1 − q).",
{
"Nekonečná geometrická řada má součet jen pro |q| < 1.",
"Pro a₁ = 6 a q = 1/2 je s = 6 / (1/2) = 12.",
"Pro |q| ≥ 1 se součet členů neblíží ke konečnému číslu, kromě případu q = 1 a a₁ = 0.",
"Řada 6 + 3 + 3/2 + … se tedy blíží ke 12.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[4], "mat3_unit5", "mat3_unit5_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit5_exercise_page(void) {
static const MathItem qs[] = {
{"Aritmetická posloupnost má a₁ = 3 a d = 2. Kolik je a₅?", "num:11", "a₅ = 3 + 4 · 2 = 11. Výsledek je 11."},
{"Aritmetická posloupnost má a₁ = 3 a d = 2. Kolik je součet prvních pěti členů?", "num:35", "s₅ = 5/2 · (6 + 8) = 35. Výsledek je 35."},
{"Geometrická posloupnost má a₁ = 2 a q = 3. Kolik je a₄?", "num:54", "a₄ = 2 · 27 = 54. Výsledek je 54."},
{"Nekonečná geometrická řada má a₁ = 6 a q = 1/2. Jaký je její součet?", "num:12", "s = 6 / (1/2) = 12. Výsledek je 12."},
};
return math_practice_page(3, 4, "mat3unit5",
"mat3_ex5_title", "mat3_quiz5_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit6_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Permutace", "Pořadí všech prvků",
"Tip: n! = 1 · 2 · … · n a 0! = 1.",
{
"Permutace je uspořádání všech n různých prvků.",
"Počet je n!.",
"4! = 24 a 3! = 6.",
"Na prvním místě je n možností, na dalším n − 1, a tak dál.",
NULL,
},
},
{
"2 / 3 • Variace", "Pořadí vybraných prvků",
"Tip: variace bez opakování V(n, k) = n! / (n − k)!.",
{
"Variace vybírá k prvků z n a záleží na pořadí.",
"V(5, 2) = 5 · 4 = 20.",
"Opakování by dovolilo vybrat stejný prvek víckrát.",
"Permutace je variace, která vybírá všech n prvků.",
NULL,
},
},
{
"3 / 3 • Kombinace", "Pořadí nehraje roli",
"Tip: C(n, k) = n! / (k! · (n − k)!).",
{
"Kombinace vybírá k prvků a dvě vybrané skupiny se stejným obsahem jsou jedna.",
"C(5, 2) = 10.",
"C(n, k) = C(n, n − k).",
"Počet variací je k! krát větší než počet kombinací.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[5], "mat3_unit6", "mat3_unit6_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit6_exercise_page(void) {
static const MathItem qs[] = {
{"Kolik je permutací čtyř různých prvků?", "num:24", "4! = 24. Výsledek je 24."},
{"Kolik je kombinací 2 prvků z 5?", "num:10", "C(5, 2) = (5 · 4) / 2 = 10. Výsledek je 10."},
{"Kolik je variací 2 prvků z 5 bez opakování?", "num:20", "V(5, 2) = 5 · 4 = 20. Výsledek je 20."},
{"Kolik je 3!?", "num:6", "3 · 2 · 1 = 6. Výsledek je 6."},
};
return math_practice_page(3, 5, "mat3unit6",
"mat3_ex6_title", "mat3_quiz6_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit7_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Definice", "Příznivé ku všem",
"Tip: všechny výsledky musí být stejně možné.",
{
"Klasická pravděpodobnost je počet příznivých výsledků dělený počtem všech.",
"Hodnota leží od 0 do 1.",
"Jev jistý má pravděpodobnost 1, jev nemožný 0.",
"Na kostce je sudé číslo ve třech případech ze šesti, tedy 1/2.",
NULL,
},
},
{
"2 / 3 • Nezávislost", "Součin pravděpodobností",
"Tip: dvě mince mají čtyři stejně možné dvojice výsledků.",
{
"U dvou nezávislých mincí jsou možnosti PP, PO, OP a OO.",
"Dva panny jsou jedna možnost ze čtyř, tedy 1/4.",
"U dvou kostek je 36 stejně možných dvojic.",
"Součet 7 nastane u šesti dvojic: 1+6, 2+5, 3+4, 4+3, 5+2 a 6+1.",
NULL,
},
},
{
"3 / 3 • Karty", "Čtyři esa v balíčku 32 karet",
"Tip: mariášový balíček má 32 karet.",
{
"Pravděpodobnost esa je 4/32 = 1/8.",
"Tažení jedné karty považuje každou kartu za stejně možnou.",
"Doplňkový jev má pravděpodobnost 1 − p.",
"Výsledek se krátí do základního tvaru.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[6], "mat3_unit7", "mat3_unit7_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit7_exercise_page(void) {
static const MathItem qs[] = {
{"Jaká je pravděpodobnost sudého čísla při hodu hrací kostkou?", "num:1/2", "Sudá jsou 2, 4 a 6, tedy 3 ze 6, čili 1/2. Výsledek je 1/2."},
{"Jaká je pravděpodobnost, že na dvou mincích padnou dvě panny?", "num:1/4", "Ze čtyř dvojic je příznivá jedna. Výsledek je 1/4."},
{"V balíčku 32 karet jsou 4 esa. Jaká je pravděpodobnost, že tažená karta je eso?", "num:1/8", "4/32 = 1/8. Výsledek je 1/8."},
{"Jaká je pravděpodobnost, že součet na dvou hracích kostkách je 7?", "num:1/6", "Příznivých dvojic je 6 z 36, tedy 1/6. Výsledek je 1/6."},
};
return math_practice_page(3, 6, "mat3unit7",
"mat3_ex7_title", "mat3_quiz7_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat3_unit8_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Krychle", "Objem a povrch",
"Tip: krychle o hraně a má objem a³ a povrch 6a².",
{
"Objem je počet jednotkových krychlí, které se do tělesa vejdou.",
"Krychle o hraně 3 má objem 27.",
"Povrch je součet obsahů všech stěn: 6 · 9 = 54.",
"Jednotky objemu jsou krychlové, jednotky povrchu čtvereční.",
NULL,
},
},
{
"2 / 3 • Válec a koule", "Vzorce s π",
"Tip: výsledek nechte v násobcích π.",
{
"Objem válce je πr²v.",
"Pro r = 2 a v = 5 je objem 20π.",
"Objem koule je (4/3)πr³.",
"Pro r = 3 je objem (4/3)π · 27 = 36π.",
NULL,
},
},
{
"3 / 3 • Jehlan", "Třetina hranolu",
"Tip: objem jehlanu je (1/3) · obsah podstavy · výška.",
{
"Jehlan má stejný objem jako třetina hranolu se stejnou podstavou a výškou.",
"Podstava o obsahu 12 a výška 4 dávají objem 16.",
"Výška je kolmá vzdálenost vrcholu od roviny podstavy.",
"Stejný vztah třetiny platí i pro kužel vůči válci.",
NULL,
},
},
};
return math_unit_page(3, &mat3_lessons[7], "mat3_unit8", "mat3_unit8_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat3_unit8_exercise_page(void) {
static const MathItem qs[] = {
{"Krychle má hranu 3. Jaký je její objem?", "num:27", "3³ = 27. Výsledek je 27."},
{"Krychle má hranu 3. Jaký je její povrch?", "num:54", "Šest stěn o obsahu 9 dává 54. Výsledek je 54."},
{"Válec má poloměr 2 a výšku 5. Jaký je jeho objem? Nechte π ve výsledku.", "num:20*pi", "πr²v = π · 4 · 5 = 20π. Výsledek je 20π."},
{"Koule má poloměr 3. Jaký je její objem? Nechte π ve výsledku.", "num:36*pi", "(4/3)π · 27 = 36π. Výsledek je 36π."},
};
return math_practice_page(3, 7, "mat3unit8",
"mat3_ex8_title", "mat3_quiz8_head",
qs, G_N_ELEMENTS(qs));
}
void add_mat3_pages(GtkStack *stack) {
typedef GtkWidget *(*MathBuilder)(void);
static const struct {
const char *unit_name;
const char *ex_name;
MathBuilder build_unit;
MathBuilder build_ex;
} pages[] = {
{"mat3unit1", "mat3ex1", build_mat3_unit1_page, build_mat3_unit1_exercise_page},
{"mat3unit2", "mat3ex2", build_mat3_unit2_page, build_mat3_unit2_exercise_page},
{"mat3unit3", "mat3ex3", build_mat3_unit3_page, build_mat3_unit3_exercise_page},
{"mat3unit4", "mat3ex4", build_mat3_unit4_page, build_mat3_unit4_exercise_page},
{"mat3unit5", "mat3ex5", build_mat3_unit5_page, build_mat3_unit5_exercise_page},
{"mat3unit6", "mat3ex6", build_mat3_unit6_page, build_mat3_unit6_exercise_page},
{"mat3unit7", "mat3ex7", build_mat3_unit7_page, build_mat3_unit7_exercise_page},
{"mat3unit8", "mat3ex8", build_mat3_unit8_page, build_mat3_unit8_exercise_page},
};
for (guint i = 0; i < G_N_ELEMENTS(pages); i++) {
gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name);
gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name);
}
}

472
src/mat_year4.c Normal file
View file

@ -0,0 +1,472 @@
#include "graduately.h"
NetLesson mat4_lessons[MATH_N] = {
{ .n_slides = 3, .unit_page = "mat4unit1", .ex_page = "mat4ex1" },
{ .n_slides = 3, .unit_page = "mat4unit2", .ex_page = "mat4ex2" },
{ .n_slides = 3, .unit_page = "mat4unit3", .ex_page = "mat4ex3" },
{ .n_slides = 3, .unit_page = "mat4unit4", .ex_page = "mat4ex4" },
{ .n_slides = 3, .unit_page = "mat4unit5", .ex_page = "mat4ex5" },
{ .n_slides = 3, .unit_page = "mat4unit6", .ex_page = "mat4ex6" },
{ .n_slides = 3, .unit_page = "mat4unit7", .ex_page = "mat4ex7" },
{ .n_slides = 3, .unit_page = "mat4unit8", .ex_page = "mat4ex8" },
};
static GtkWidget *build_mat4_unit1_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Souřadnice", "Velikost a součet",
"Tip: velikost vektoru (a; b) je √(a² + b²).",
{
"Vektor má velikost a směr.",
"|(3; 4)| = 5, protože 9 + 16 = 25.",
"Vektory se sčítají po souřadnicích.",
"(1; 2) + (3; −1) = (4; 1).",
NULL,
},
},
{
"2 / 3 • Skalární součin", "Číslo, ne vektor",
"Tip: (a; b) · (c; d) = ac + bd.",
{
"Skalární součin dvou vektorů je číslo.",
"(1; 2) · (3; 4) = 3 + 8 = 11.",
"Nulový skalární součin znamená kolmé vektory.",
"Velikost lze psát jako odmocninu ze skalárního součinu vektoru se sebou.",
NULL,
},
},
{
"3 / 3 • Vzdálenost", "Velikost rozdílu",
"Tip: na mřížce osa y míří nahoru.",
{
"Vzdálenost bodů je velikost vektoru mezi nimi.",
"Od [1; 1] do [4; 5] je vektor (3; 4) a vzdálenost 5.",
"Koncový bod vektoru (3; 2) umístěného do [1; 1] je [4; 3].",
"Pořadí souřadnic je x a potom y.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[0], "mat4_unit1", "mat4_unit1_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit1_exercise_page(void) {
static const MathItem qs[] = {
{"Jaká je velikost vektoru (3; 4)?", "num:5", "√(9 + 16) = √25 = 5. Výsledek je 5."},
{"Sečtěte vektory (1; 2) a (3; −1). Napište souřadnice oddělené středníkem.", "pair:4;1", "(1 + 3; 2 + (−1)) = (4; 1). Napište 4;1."},
{"Spočítejte skalární součin vektorů (1; 2) a (3; 4).", "num:11", "1 · 3 + 2 · 4 = 11. Výsledek je 11."},
{"Jaká je vzdálenost bodů [1; 1] a [4; 5]?", "num:5", "Rozdíl je (3; 4) a jeho velikost je 5. Výsledek je 5."},
{"Z bodu A = [1; 1] naneste vektor (3; 2). Zakreslete koncový bod.", "points:4,3;fix:1,1", "Ke každé souřadnici se přičte složka vektoru: [1 + 3; 1 + 2] = [4; 3]."},
};
return math_practice_page(4, 0, "mat4unit1",
"mat4_ex1_title", "mat4_quiz1_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit2_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Směrnice", "y = ax + b",
"Tip: a = (y₂ − y₁)/(x₂ − x₁).",
{
"Body [1; 2] a [4; 8] určují směrnici (8 − 2)/(4 − 1) = 2.",
"Přímka procházející bodem [0; 1] se směrnicí 2 má rovnici y = 2x + 1.",
"Úsek na ose y je 1.",
"Svislá přímka směrnici tohoto tvaru nemá.",
NULL,
},
},
{
"2 / 3 • Parametr", "Bod a směrový vektor",
"Tip: X = A + t · u.",
{
"Parametrické vyjádření je x = x₀ + t · u₁, y = y₀ + t · u₂.",
"Přímka (1; 3) + t(2; 0) má při t = 2 bod [5; 3].",
"Každé t dá právě jeden bod přímky.",
"Směrový vektor (2; 0) je vodorovný.",
NULL,
},
},
{
"3 / 3 • Vzdálenost", "Bod od přímky",
"Tip: pro ax + by + c = 0 je vzdálenost |ax₀ + by₀ + c| / √(a² + b²).",
{
"Přímka 3x + 4y − 10 = 0 má a = 3, b = 4 a c = −10.",
"Vzdálenost počátku je |−10| / √(9 + 16) = 10/5 = 2.",
"Čitatel je absolutní hodnota, vzdálenost není záporná.",
"Čtvrtý vrchol rovnoběžníku k bodům A, B a C je B + C − A.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[1], "mat4_unit2", "mat4_unit2_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit2_exercise_page(void) {
static const MathItem qs[] = {
{"Jaká je směrnice přímky body [1; 2] a [4; 8]?", "num:2", "(8 − 2)/(4 − 1) = 2. Výsledek je 2."},
{"Přímka má rovnici y = 2x + 1. Jaký je její úsek na ose y?", "num:1", "Úsek je absolutní člen, tedy 1. Výsledek je 1."},
{"Bod přímky je (1; 3) + t(2; 0). Kam přímka dojde pro t = 2? Napište x a y středníkem.", "pair:5;3", "(1; 3) + 2 · (2; 0) = (5; 3). Napište 5;3."},
{"Jaká je vzdálenost bodu [0; 0] od přímky 3x + 4y − 10 = 0?", "num:2", "|−10| / √(9 + 16) = 10/5 = 2. Výsledek je 2."},
{"A = [1; 1], B = [4; 1] a C = [2; 3]. Zakreslete čtvrtý vrchol rovnoběžníku, ve kterém jsou AB a AC sousední strany.", "para:1,1;4,1;2,3", "D = B + C − A = [4 + 2 − 1; 1 + 3 − 1] = [5; 3]."},
};
return math_practice_page(4, 1, "mat4unit2",
"mat4_ex2_title", "mat4_quiz2_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit3_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Kružnice", "Střed a poloměr",
"Tip: (x − m)² + (y − n)² = r² má střed [m; n].",
{
"Kružnice je množina bodů stejně vzdálených od středu.",
"(x − 1)² + (y + 2)² = 9 má střed [1; −2].",
"y + 2 je totéž co y − (−2).",
"Poloměr je √9 = 3, ne 9.",
NULL,
},
},
{
"2 / 3 • Elipsa", "Dvě poloosy",
"Tip: ve tvaru x²/a² + y²/b² = 1 je a poloosa na ose x.",
{
"Elipsa v osové poloze se středem v počátku má rovnici x²/a² + y²/b² = 1.",
"U x²/25 + y²/9 = 1 je a = 5 a b = 3.",
"Delší poloosa je hlavní, kratší vedlejší.",
"Vrcholy na ose x jsou [±a; 0], na ose y [0; ±b].",
NULL,
},
},
{
"3 / 3 • Excentricita", "Vzdálenost ohniska od středu",
"Tip: pro a > b je e = √(a² − b²).",
{
"Lineární výstřednost e splňuje e² = a² − b², když je a hlavní poloosa.",
"√(25 − 9) = √16 = 4.",
"Ohniska elipsy jsou [±e; 0].",
"Součet vzdáleností bodu elipsy od obou ohnisek je 2a.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[2], "mat4_unit3", "mat4_unit3_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit3_exercise_page(void) {
static const MathItem qs[] = {
{"Kružnice má rovnici (x − 1)² + (y + 2)² = 9. Napište souřadnice středu jako x;y.", "pair:1;-2", "Střed je [1; −2]. Napište 1;-2."},
{"Jaký je poloměr kružnice (x − 1)² + (y + 2)² = 9?", "num:3", "Pravá strana je r², takže r = 3. Výsledek je 3."},
{"Elipsa má rovnici x²/25 + y²/9 = 1. Jak dlouhá je poloosa a na ose x?", "num:5", "a² = 25, takže a = 5. Výsledek je 5."},
{"Elipsa má rovnici x²/25 + y²/9 = 1. Jak dlouhá je poloosa b na ose y?", "num:3", "b² = 9, takže b = 3. Výsledek je 3."},
{"Elipsa má a = 5 a b = 3. Jaká je její lineární výstřednost e?", "num:4", "e = √(25 − 9) = √16 = 4. Výsledek je 4."},
};
return math_practice_page(4, 2, "mat4unit3",
"mat4_ex3_title", "mat4_quiz3_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit4_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Tvar", "a + bi",
"Tip: i² = −1. Reálnou a imaginární část pište oddělené středníkem.",
{
"Komplexní číslo a + bi má reálnou část a a imaginární část b.",
"i je imaginární jednotka a i² = −1.",
"Čísla se sčítají po složkách.",
"(2 + 3i) + (1 − i) = 3 + 2i.",
NULL,
},
},
{
"2 / 3 • Násobení", "Jako dvojčlen",
"Tip: (1 + i)(1 − i) je rozdíl čtverců.",
{
"(a + bi)(c + di) = ac + adi + bci + bdi².",
"bdi² = −bd, proto se imaginární jednotka ve výsledku nemusí objevit.",
"(1 + i)(1 − i) = 1 − (i)² = 1 − (−1) = 2.",
"Komplexní čísla lze násobit i v goniometrickém tvaru sčítáním argumentů.",
NULL,
},
},
{
"3 / 3 • Absolutní hodnota", "Vzdálenost od nuly",
"Tip: |a + bi| = √(a² + b²).",
{
"Absolutní hodnota je vzdálenost obrazu čísla od počátku Gaussovy roviny.",
"|3 + 4i| = 5.",
"|z| je vždy reálné nezáporné číslo.",
"i² = −1, i³ = −i a i⁴ = 1.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[3], "mat4_unit4", "mat4_unit4_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit4_exercise_page(void) {
static const MathItem qs[] = {
{"Sečtěte 2 + 3i a 1 − i. Napište reálnou a imaginární část středníkem.", "pair:3;2", "(2 + 1) + (3 − 1)i = 3 + 2i. Napište 3;2."},
{"Spočítejte (1 + i)(1 − i).", "num:2", "1 − i² = 1 − (−1) = 2. Výsledek je 2."},
{"Jaká je absolutní hodnota čísla 3 + 4i?", "num:5", "√(9 + 16) = 5. Výsledek je 5."},
{"Kolik je i²?", "num:-1", "Imaginární jednotka splňuje i² = −1. Výsledek je -1."},
};
return math_practice_page(4, 3, "mat4unit4",
"mat4_ex4_title", "mat4_quiz4_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit5_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Mocnina", "(xⁿ)' = n xⁿ⁻¹",
"Tip: derivace je směrnice tečny.",
{
"Derivace funkce v bodě je směrnice tečny grafu v tom bodě.",
"(x³)' = 3x². V bodě x = 2 je 3 · 4 = 12.",
"(5x²)' = 10x. V bodě x = 1 je 10.",
"Derivace konstanty je 0 a konstanta se vytýká.",
NULL,
},
},
{
"2 / 3 • Tečna", "Směrnice v bodě",
"Tip: u f(x) = x² je f'(x) = 2x.",
{
"Tečna ke grafu y = x² v bodě x = 1 má směrnici 2.",
"Rovnice tečny používá bod [1; 1] a tuto směrnici.",
"Kladná derivace znamená, že funkce v bodě roste.",
"Záporná derivace znamená, že funkce klesá.",
NULL,
},
},
{
"3 / 3 • Sinus", "Derivace sinu je kosinus",
"Tip: (sin x)' = cos x a cos 0 = 1.",
{
"Derivace funkce sin x je cos x.",
"Derivace funkce cos x je −sin x.",
"V bodě x = 0 je derivace sinu rovna cos 0 = 1.",
"Úhel v tomto vzorci je v radiánech, ale nula je nula v obou jednotkách.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[4], "mat4_unit5", "mat4_unit5_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit5_exercise_page(void) {
static const MathItem qs[] = {
{"Jaká je derivace funkce x³ v bodě x = 2?", "num:12", "(x³)' = 3x² a 3 · 4 = 12. Výsledek je 12."},
{"Jaká je derivace funkce 5x² v bodě x = 1?", "num:10", "(5x²)' = 10x a v jedničce je 10. Výsledek je 10."},
{"Jakou směrnici má tečna ke grafu y = x² v bodě x = 1?", "num:2", "Derivace 2x je v bodě 1 rovna 2. Výsledek je 2."},
{"Jaká je derivace funkce sin x v bodě x = 0? Derivace sinu je kosinus.", "num:1", "(sin x)' = cos x a cos 0 = 1. Výsledek je 1."},
};
return math_practice_page(4, 4, "mat4unit5",
"mat4_ex5_title", "mat4_quiz5_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit6_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Stacionární bod", "Derivace je nula",
"Tip: f'(x) = 0 je vodorovná tečna.",
{
"V lokálním extrému diferencovatelné funkce je derivace nulová.",
"U f(x) = x³ − 3x je f'(x) = 3x² − 3.",
"3(x² − 1) = 0 dává x = −1 a x = 1.",
"To jsou jediní kandidáti na lokální extrém.",
NULL,
},
},
{
"2 / 3 • Maximum a minimum", "Znaménko druhé derivace",
"Tip: f''(x) = 6x. Záporná druhá derivace znamená lokální maximum.",
{
"V x = −1 je f''(−1) = −6 < 0, takže jde o lokální maximum.",
"f(−1) = −1 + 3 = 2.",
"V x = 1 je f''(1) = 6 > 0, takže jde o lokální minimum.",
"f(1) = 1 − 3 = −2.",
NULL,
},
},
{
"3 / 3 • Jiný příklad", "Lineární derivace",
"Tip: g'(x) = 2x − 4 je nula při x = 2.",
{
"Kde je derivace nula, má graf vodorovnou tečnu.",
"Rovnice 2x − 4 = 0 má řešení x = 2.",
"Pro x < 2 je derivace 2x − 4 záporná, funkce klesá.",
"Pro x > 2 je derivace kladná, funkce roste.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[5], "mat4_unit6", "mat4_unit6_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit6_exercise_page(void) {
static const MathItem qs[] = {
{"Funkce f(x) = x³ − 3x. Ve kterém x má lokální maximum?", "num:-1", "f'(x) = 3x² − 3 = 0 pro x = ±1 a v −1 je maximum. Výsledek je -1."},
{"Funkce f(x) = x³ − 3x. Jaká je hodnota lokálního maxima?", "num:2", "f(−1) = −1 + 3 = 2. Výsledek je 2."},
{"Funkce f(x) = x³ − 3x. Ve kterém x má lokální minimum?", "num:1", "Druhá derivace 6x je v x = 1 kladná. Výsledek je 1."},
{"Derivace funkce je g'(x) = 2x − 4. Pro které x je derivace nulová?", "num:2", "2x − 4 = 0 dává x = 2. Výsledek je 2."},
};
return math_practice_page(4, 5, "mat4unit6",
"mat4_ex6_title", "mat4_quiz6_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit7_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Primitivní funkce", "Opačný postup k derivaci",
"Tip: primitivní funkce k xⁿ je xⁿ⁺¹/(n + 1) pro n ≠ −1.",
{
"Neurčitý integrál je množina primitivních funkcí.",
"Liší se o konstantu, protože derivace konstanty je nula.",
"Primitivní funkce k 2x je x².",
"Primitivní funkce k x² je x³/3.",
NULL,
},
},
{
"2 / 3 • Určitý integrál", "Newtonův vzorec",
"Tip: ∫ od a do b z f je F(b) − F(a).",
{
"Určitý integrál nezáporné funkce je obsah plochy pod grafem.",
"∫ od 0 do 3 z 2x dx = [x²] od 0 do 3 = 9.",
"∫ od 0 do 2 z x² dx = [x³/3] od 0 do 2 = 8/3.",
"Dolní mez 0 u těchto mocnin často vynuluje první člen.",
NULL,
},
},
{
"3 / 3 • Konstanta", "Násobek lze vytknout",
"Tip: integrál z c · f je c krát integrál z f.",
{
"∫ od 0 do 1 z 3x² dx = [x³] od 0 do 1 = 1.",
"∫ od 0 do 2 z 6x dx = [3x²] od 0 do 2 = 12.",
"Záporná funkce dává záporný integrál, nejde vždy o obsah beze znaménka.",
"Součet funkcí se integruje člen po členu.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[6], "mat4_unit7", "mat4_unit7_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit7_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte integrál od 0 do 3 z funkce 2x.", "num:9", "Primitivní funkce je x² a 9 − 0 = 9. Výsledek je 9."},
{"Spočítejte integrál od 0 do 2 z funkce x².", "num:8/3", "Primitivní funkce je x³/3 a 8/3 − 0 = 8/3. Výsledek je 8/3."},
{"Spočítejte integrál od 0 do 1 z funkce 3x².", "num:1", "Primitivní funkce je x³ a 1 − 0 = 1. Výsledek je 1."},
{"Spočítejte integrál od 0 do 2 z funkce 6x.", "num:12", "Primitivní funkce je 3x² a 12 − 0 = 12. Výsledek je 12."},
};
return math_practice_page(4, 6, "mat4unit7",
"mat4_ex7_title", "mat4_quiz7_head",
qs, G_N_ELEMENTS(qs));
}
static GtkWidget *build_mat4_unit8_page(void) {
static const NetSlide slides[] = {
{
"1 / 3 • Číslo", "Logaritmus a zlomek",
"Tip: log₂(8) = 3, protože 2³ = 8.",
{
"Logaritmus vrací exponent.",
"1/2 + 1/3 = 5/6.",
"Společný jmenovatel se hledá i v maturitním příkladu stejně jako na základní škole.",
"Výsledek se krátí.",
NULL,
},
},
{
"2 / 3 • Počet a vektor", "Kombinace a velikost",
"Tip: C(6, 2) = 15 a |(3; 4)| = 5.",
{
"C(6, 2) = (6 · 5) / 2 = 15.",
"Pořadí vybrané dvojice se nepočítá dvakrát.",
"Velikost vektoru (3; 4) je přepona trojúhelníku 3-4-5.",
"Skalární součin by u kolmých vektorů vyšel nula.",
NULL,
},
},
{
"3 / 3 • Rovnice", "x² − 1 = 0",
"Tip: rozdíl čtverců (x − 1)(x + 1).",
{
"x² − 1 = 0 má kořeny −1 a 1.",
"Oba se napíšou, oddělené středníkem.",
"Diskriminant je 0 − 4 · 1 · (−1) = 4.",
"Zkouška: 1 − 1 = 0 a 1 − 1 = 0.",
NULL,
},
},
};
return math_unit_page(4, &mat4_lessons[7], "mat4_unit8", "mat4_unit8_sub",
slides, G_N_ELEMENTS(slides));
}
static GtkWidget *build_mat4_unit8_exercise_page(void) {
static const MathItem qs[] = {
{"Spočítejte logaritmus o základu 2 z 8.", "num:3", "2³ = 8. Výsledek je 3."},
{"Kolik je kombinací 2 prvků ze 6?", "num:15", "C(6, 2) = 15. Výsledek je 15."},
{"Jaká je velikost vektoru (3; 4)?", "num:5", "√(9 + 16) = 5. Výsledek je 5."},
{"Spočítejte 1/2 + 1/3.", "num:5/6", "3/6 + 2/6 = 5/6. Výsledek je 5/6."},
{"Najděte kořeny rovnice x² − 1 = 0. Oddělte je středníkem.", "set:-1;1", "(x − 1)(x + 1) = 0. Napište -1;1."},
};
return math_practice_page(4, 7, "mat4unit8",
"mat4_ex8_title", "mat4_quiz8_head",
qs, G_N_ELEMENTS(qs));
}
void add_mat4_pages(GtkStack *stack) {
typedef GtkWidget *(*MathBuilder)(void);
static const struct {
const char *unit_name;
const char *ex_name;
MathBuilder build_unit;
MathBuilder build_ex;
} pages[] = {
{"mat4unit1", "mat4ex1", build_mat4_unit1_page, build_mat4_unit1_exercise_page},
{"mat4unit2", "mat4ex2", build_mat4_unit2_page, build_mat4_unit2_exercise_page},
{"mat4unit3", "mat4ex3", build_mat4_unit3_page, build_mat4_unit3_exercise_page},
{"mat4unit4", "mat4ex4", build_mat4_unit4_page, build_mat4_unit4_exercise_page},
{"mat4unit5", "mat4ex5", build_mat4_unit5_page, build_mat4_unit5_exercise_page},
{"mat4unit6", "mat4ex6", build_mat4_unit6_page, build_mat4_unit6_exercise_page},
{"mat4unit7", "mat4ex7", build_mat4_unit7_page, build_mat4_unit7_exercise_page},
{"mat4unit8", "mat4ex8", build_mat4_unit8_page, build_mat4_unit8_exercise_page},
};
for (guint i = 0; i < G_N_ELEMENTS(pages); i++) {
gtk_stack_add_named(stack, pages[i].build_unit(), pages[i].unit_name);
gtk_stack_add_named(stack, pages[i].build_ex(), pages[i].ex_name);
}
}

955
src/matematika.c Normal file
View file

@ -0,0 +1,955 @@
#include "graduately.h"
#include <string.h>
/* Mathematics: elementary review plus four gymnasium years.
* Lessons are slides, then typed examples. A few lessons also
* ask for a point snapped to the grid. */
#define MATH_CELL 28.0
#define MATH_PAD 18.0
#define MATH_GX 10
#define MATH_GY 8
#define MATH_PTS 16
typedef struct {
const char *map_page;
const char *back_page;
const char *title_key;
const char *sub_key;
char **progress_path;
NetLesson *lessons;
const char *unit_keys[MATH_N];
const char *sub_keys[MATH_N];
GtkWidget *nodes[MATH_N];
int note_last_w;
} MathTrack;
typedef struct {
const char *spec;
int ux[MATH_PTS];
int uy[MATH_PTS];
int n;
int fx[8];
int fy[8];
int nf;
int need;
int checked;
int correct;
GtkWidget *area;
} MathBoard;
typedef struct {
const MathItem *items;
int n;
MathTrack *track;
int lesson_id;
GtkWidget *feedback;
GtkWidget **entries;
MathBoard **boards;
GtkWidget **hints;
} MathExCtx;
static MathTrack math_tracks[5];
static gboolean math_ready;
static char math_key_store[5][MATH_N][32];
static char math_sub_store[5][MATH_N][40];
static void math_ensure(void) {
static const char *prefixes[] = {"mat0", "mat", "mat2", "mat3", "mat4"};
static const char *maps[] = {
"mat0map", "matmap", "mat2map", "mat3map", "mat4map",
};
static const char *titles[] = {
"mat0_year", "mat_year1", "mat_year2", "mat_year3", "mat_year4",
};
static const char *subs[] = {
"mat0_sub", "mat_sub", "mat2_sub", "mat3_sub", "mat4_sub",
};
NetLesson *lessons[5] = {
mat0_lessons, mat_lessons, mat2_lessons, mat3_lessons, mat4_lessons,
};
char **paths[5] = {
&app_progress_mat0, &app_progress_mat, &app_progress_mat2,
&app_progress_mat3, &app_progress_mat4,
};
int y, i;
if (math_ready)
return;
for (y = 0; y < 5; y++) {
MathTrack *T = &math_tracks[y];
T->map_page = maps[y];
T->back_page = "matyears";
T->title_key = titles[y];
T->sub_key = subs[y];
T->progress_path = paths[y];
T->lessons = lessons[y];
T->note_last_w = -1;
for (i = 0; i < MATH_N; i++) {
g_snprintf(math_key_store[y][i], sizeof math_key_store[y][i],
"%s_unit%d", prefixes[y], i + 1);
g_snprintf(math_sub_store[y][i], sizeof math_sub_store[y][i],
"%s_unit%d_sub", prefixes[y], i + 1);
T->unit_keys[i] = math_key_store[y][i];
T->sub_keys[i] = math_sub_store[y][i];
}
}
math_ready = TRUE;
}
static void math_save(MathTrack *T) {
GKeyFile *kf;
gchar *data;
if (!T->progress_path || !*T->progress_path)
return;
kf = g_key_file_new();
for (int i = 0; i < MATH_N; i++) {
gchar key[8];
g_snprintf(key, sizeof key, "%d", i + 1);
g_key_file_set_boolean(kf, "done", key, T->lessons[i].done);
}
if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
g_key_file_free(kf);
return;
}
data = g_key_file_to_data(kf, NULL, NULL);
g_key_file_free(kf);
if (data) {
GError *err = NULL;
g_file_set_contents(*T->progress_path, data, -1, &err);
if (err)
g_error_free(err);
g_free(data);
}
}
static void math_load_one(MathTrack *T) {
GKeyFile *kf;
GError *err = NULL;
if (!T->progress_path || !*T->progress_path)
return;
kf = g_key_file_new();
if (!g_key_file_load_from_file(kf, *T->progress_path, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
return;
}
for (int i = 0; i < MATH_N; i++) {
gchar key[8];
g_snprintf(key, sizeof key, "%d", i + 1);
T->lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
g_key_file_free(kf);
}
void math_load_progress(void) {
math_ensure();
for (int y = 0; y < 5; y++)
math_load_one(&math_tracks[y]);
}
void math_rail_theme_reset(void) {
}
static void math_refresh(MathTrack *T) {
for (int i = 0; i < MATH_N; i++) {
GtkWidget *node = T->nodes[i];
GtkWidget *icon = T->lessons[i].done_icon;
if (!node)
continue;
if (T->lessons[i].done) {
gtk_widget_remove_css_class(node, "current");
gtk_widget_add_css_class(node, "done");
if (icon)
gtk_widget_set_visible(icon, TRUE);
} else {
gtk_widget_remove_css_class(node, "done");
gtk_widget_add_css_class(node, "current");
if (icon)
gtk_widget_set_visible(icon, FALSE);
}
}
}
static void math_mark_done(MathTrack *T, int lesson_id) {
if (lesson_id < 0 || lesson_id >= MATH_N)
return;
if (T->lessons[lesson_id].done)
return;
T->lessons[lesson_id].done = TRUE;
math_save(T);
math_refresh(T);
refresh_stats_ui();
}
void progress_for_math(ProgressSum *out) {
if (!out)
return;
math_ensure();
for (int y = 0; y < 5; y++) {
MathTrack *T = &math_tracks[y];
out->total_ex += MATH_N;
out->open_units += MATH_N;
for (int i = 0; i < MATH_N; i++) {
if (T->lessons[i].done) {
out->done_ex++;
out->done_units++;
}
}
}
}
static void math_open_unit(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (!L)
return;
L->idx = 0;
if (L->stack) {
char name[16];
g_snprintf(name, sizeof name, "s%u", L->idx);
gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name);
if (L->prev_btn)
gtk_widget_set_sensitive(L->prev_btn, FALSE);
if (L->next_btn)
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
}
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
}
static GtkWidget *math_lesson_button(MathTrack *T, int index) {
GtkWidget *btn;
GtkWidget *row;
GtkWidget *num;
GtkWidget *texts;
GtkWidget *title;
GtkWidget *sub;
GtkWidget *icon;
char *num_text;
btn = gtk_button_new();
gtk_widget_add_css_class(btn, "unit-node");
gtk_widget_add_css_class(btn, "current");
gtk_widget_set_can_focus(btn, FALSE);
gtk_widget_set_hexpand(btn, TRUE);
gtk_widget_set_size_request(btn, -1, 84);
row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_widget_set_valign(row, GTK_ALIGN_CENTER);
gtk_widget_set_margin_start(row, 18);
gtk_widget_set_margin_end(row, 18);
gtk_button_set_child(GTK_BUTTON(btn), row);
num_text = g_strdup_printf("%d", index + 1);
num = gtk_label_new(num_text);
g_free(num_text);
gtk_widget_add_css_class(num, "unit-number");
gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), num);
texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_halign(texts, GTK_ALIGN_START);
gtk_widget_set_hexpand(texts, TRUE);
gtk_box_append(GTK_BOX(row), texts);
title = gtk_label_new(NULL);
i18n_bind(title, T->unit_keys[index], 0);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_add_css_class(title, "unit-name");
gtk_box_append(GTK_BOX(texts), title);
sub = gtk_label_new(NULL);
i18n_bind(sub, T->sub_keys[index], 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_label_set_xalign(GTK_LABEL(sub), 0.0);
gtk_widget_add_css_class(sub, "hint");
gtk_box_append(GTK_BOX(texts), sub);
icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18);
gtk_widget_set_visible(icon, FALSE);
gtk_widget_set_valign(icon, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), icon);
T->lessons[index].done_icon = icon;
T->nodes[index] = btn;
g_signal_connect(btn, "clicked", G_CALLBACK(math_open_unit),
&T->lessons[index]);
return btn;
}
static GtkWidget *math_build_list(MathTrack *T) {
GtkWidget *page;
GtkWidget *scroll;
GtkWidget *list;
math_ensure();
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_hexpand(page, TRUE);
gtk_widget_set_vexpand(page, TRUE);
gtk_widget_set_margin_start(page, 32);
gtk_widget_set_margin_end(page, 32);
gtk_widget_set_margin_top(page, 24);
gtk_widget_set_margin_bottom(page, 24);
gtk_box_append(GTK_BOX(page),
top_bar(T->back_page, T->title_key, T->sub_key));
scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 18);
gtk_box_append(GTK_BOX(page), scroll);
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_halign(list, GTK_ALIGN_CENTER);
gtk_widget_set_valign(list, GTK_ALIGN_START);
gtk_widget_set_size_request(list, 520, -1);
gtk_widget_set_margin_bottom(list, 12);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
for (int i = 0; i < MATH_N; i++)
gtk_box_append(GTK_BOX(list), math_lesson_button(T, i));
math_refresh(T);
return page;
}
GtkWidget *build_mat0map_page(void) { return math_build_list(&math_tracks[0]); }
GtkWidget *build_matmap_page(void) { return math_build_list(&math_tracks[1]); }
GtkWidget *build_mat2map_page(void) { return math_build_list(&math_tracks[2]); }
GtkWidget *build_mat3map_page(void) { return math_build_list(&math_tracks[3]); }
GtkWidget *build_mat4map_page(void) { return math_build_list(&math_tracks[4]); }
static GtkWidget *math_year_button(const char *badge, const char *target,
const char *title_key, const char *sub_key) {
GtkWidget *btn;
GtkWidget *row;
GtkWidget *num;
GtkWidget *texts;
GtkWidget *title;
GtkWidget *sub;
btn = gtk_button_new();
gtk_widget_add_css_class(btn, "unit-node");
gtk_widget_add_css_class(btn, "current");
gtk_widget_set_can_focus(btn, FALSE);
gtk_widget_set_hexpand(btn, TRUE);
gtk_widget_set_size_request(btn, -1, 84);
row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_widget_set_valign(row, GTK_ALIGN_CENTER);
gtk_widget_set_margin_start(row, 18);
gtk_widget_set_margin_end(row, 18);
gtk_button_set_child(GTK_BUTTON(btn), row);
num = gtk_label_new(badge);
gtk_widget_add_css_class(num, "unit-number");
gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), num);
texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_halign(texts, GTK_ALIGN_START);
gtk_widget_set_hexpand(texts, TRUE);
gtk_box_append(GTK_BOX(row), texts);
title = gtk_label_new(NULL);
i18n_bind(title, title_key, 0);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_add_css_class(title, "unit-name");
gtk_box_append(GTK_BOX(texts), title);
sub = gtk_label_new(NULL);
i18n_bind(sub, sub_key, 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_label_set_xalign(GTK_LABEL(sub), 0.0);
gtk_widget_add_css_class(sub, "hint");
gtk_box_append(GTK_BOX(texts), sub);
g_object_set_data_full(G_OBJECT(btn), "target", g_strdup(target), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
return btn;
}
GtkWidget *build_matyears_page(void) {
GtkWidget *page;
GtkWidget *scroll;
GtkWidget *list;
static const char *badges[] = {"ZŠ", "1.", "2.", "3.", "4."};
static const char *targets[] = {
"mat0map", "matmap", "mat2map", "mat3map", "mat4map",
};
static const char *titles[] = {
"mat0_year", "mat_year1", "mat_year2", "mat_year3", "mat_year4",
};
static const char *subs[] = {
"mat0_sub", "mat_sub", "mat2_sub", "mat3_sub", "mat4_sub",
};
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_hexpand(page, TRUE);
gtk_widget_set_vexpand(page, TRUE);
gtk_widget_set_margin_start(page, 32);
gtk_widget_set_margin_end(page, 32);
gtk_widget_set_margin_top(page, 24);
gtk_widget_set_margin_bottom(page, 24);
gtk_box_append(GTK_BOX(page),
top_bar("subjects", "Matematika", "mat_years_sub"));
scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 18);
gtk_box_append(GTK_BOX(page), scroll);
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_halign(list, GTK_ALIGN_CENTER);
gtk_widget_set_valign(list, GTK_ALIGN_CENTER);
gtk_widget_set_size_request(list, 480, -1);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
for (int y = 0; y < 5; y++)
gtk_box_append(GTK_BOX(list),
math_year_button(badges[y], targets[y], titles[y], subs[y]));
return page;
}
static void math_slide_apply(NetLesson *L) {
char name[16];
if (!L || !L->stack)
return;
g_snprintf(name, sizeof name, "s%u", L->idx);
gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name);
if (L->prev_btn) {
gtk_button_set_label(GTK_BUTTON(L->prev_btn), tr("net_slide_prev"));
gtk_widget_set_sensitive(L->prev_btn, L->idx > 0);
}
if (L->next_btn) {
gtk_button_set_label(
GTK_BUTTON(L->next_btn),
L->idx + 1 >= L->n_slides ? tr("net_slide_start")
: tr("net_slide_next"));
}
}
static void math_slide_prev(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (!L || L->idx == 0)
return;
L->idx--;
math_slide_apply(L);
}
static void math_slide_next(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (!L)
return;
if (L->idx + 1 < L->n_slides) {
L->idx++;
math_slide_apply(L);
} else {
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
}
}
static void math_rescale(MathTrack *T, int w) {
int body, head, kick;
if (w < 160)
w = 160;
body = w / 46;
if (body < 14)
body = 14;
if (body > 26)
body = 26;
head = body + 14;
if (head > 46)
head = 46;
kick = body - 3;
if (kick < 11)
kick = 11;
for (int i = 0; i < MATH_N; i++)
net_rescale_lesson_notes(&T->lessons[i], body, head, kick);
}
static gboolean math_note_tick(GtkWidget *w, GdkFrameClock *clock, gpointer data) {
MathTrack *T = data;
int width = gtk_widget_get_allocated_width(w);
(void)clock;
if (width != T->note_last_w) {
T->note_last_w = width;
math_rescale(T, width);
}
return G_SOURCE_CONTINUE;
}
GtkWidget *math_unit_page(int year, NetLesson *L, const char *title_key,
const char *sub_key, const NetSlide *slides,
guint n_slides) {
MathTrack *T;
GtkWidget *page;
GtkWidget *scroll;
GtkWidget *nav;
math_ensure();
if (year < 0 || year > 4)
year = 0;
T = &math_tracks[year];
net_lesson_notes_ensure(L);
net_notes_target = L;
L->n_slides = n_slides;
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_margin_start(page, 28);
gtk_widget_set_margin_end(page, 28);
gtk_widget_set_margin_top(page, 20);
gtk_widget_set_margin_bottom(page, 20);
gtk_box_append(GTK_BOX(page), top_bar(T->map_page, title_key, sub_key));
scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 10);
gtk_box_append(GTK_BOX(page), scroll);
L->stack = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(L->stack),
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack);
for (guint s = 0; s < n_slides; s++) {
GtkWidget *stage = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *card;
char name[16];
gtk_widget_set_vexpand(stage, TRUE);
g_snprintf(name, sizeof name, "s%u", s);
gtk_stack_add_named(GTK_STACK(L->stack), stage, name);
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
gtk_widget_add_css_class(card, "notes-card");
gtk_widget_set_valign(card, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(stage), card);
net_note_kicker(card, slides[s].kicker);
net_note_title(card, slides[s].title);
for (int i = 0; i < 8 && slides[s].line[i]; i++)
net_note_line(card, slides[s].line[i], FALSE);
if (slides[s].tip)
net_note_line(card, slides[s].tip, TRUE);
}
gtk_widget_add_tick_callback(L->stack, math_note_tick, T, NULL);
T->note_last_w = -1;
math_rescale(T, 0);
net_notes_target = NULL;
nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_box_set_homogeneous(GTK_BOX(nav), TRUE);
gtk_widget_set_margin_top(nav, 10);
gtk_box_append(GTK_BOX(page), nav);
L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev"));
gtk_widget_add_css_class(L->prev_btn, "btn-primary");
g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(math_slide_prev), L);
gtk_box_append(GTK_BOX(nav), L->prev_btn);
L->next_btn = gtk_button_new_with_label(tr("net_slide_next"));
gtk_widget_add_css_class(L->next_btn, "btn-primary");
g_signal_connect(L->next_btn, "clicked", G_CALLBACK(math_slide_next), L);
gtk_box_append(GTK_BOX(nav), L->next_btn);
L->idx = 0;
math_slide_apply(L);
return page;
}
void math_lessons_apply_lang(void) {
math_ensure();
for (int y = 0; y < 5; y++) {
for (int i = 0; i < MATH_N; i++)
math_slide_apply(&math_tracks[y].lessons[i]);
}
}
static void paint_hex(cairo_t *cr, unsigned hex) {
cairo_set_source_rgb(cr, ((hex >> 16) & 255) / 255.0,
((hex >> 8) & 255) / 255.0, (hex & 255) / 255.0);
}
static int board_px_w(void) {
return (int)(MATH_PAD * 2 + MATH_GX * MATH_CELL);
}
static int board_px_h(void) {
return (int)(MATH_PAD * 2 + MATH_GY * MATH_CELL);
}
static void board_to_screen(int gx, int gy, double *sx, double *sy) {
*sx = MATH_PAD + gx * MATH_CELL;
*sy = board_px_h() - MATH_PAD - gy * MATH_CELL;
}
static gboolean board_snap(double x, double y, int *gx, int *gy) {
double cx = (x - MATH_PAD) / MATH_CELL;
double cy = (board_px_h() - MATH_PAD - y) / MATH_CELL;
int ix = (int)lround(cx);
int iy = (int)lround(cy);
if (ix < 0 || ix > MATH_GX || iy < 0 || iy > MATH_GY)
return FALSE;
if (fabs(cx - ix) > 0.45 || fabs(cy - iy) > 0.45)
return FALSE;
*gx = ix;
*gy = iy;
return TRUE;
}
static void board_draw(GtkDrawingArea *area, cairo_t *cr, int width, int height,
gpointer data) {
MathBoard *b = data;
int i;
static const char *names[] = {"A", "B", "C", "D"};
(void)area;
(void)width;
(void)height;
paint_hex(cr, app_theme.surface1);
cairo_rectangle(cr, 0, 0, board_px_w(), board_px_h());
cairo_fill(cr);
cairo_set_line_width(cr, 1);
cairo_set_source_rgba(cr,
((app_theme.subtext >> 16) & 255) / 255.0,
((app_theme.subtext >> 8) & 255) / 255.0,
(app_theme.subtext & 255) / 255.0, 0.55);
for (i = 0; i <= MATH_GX; i++) {
double x = MATH_PAD + i * MATH_CELL;
cairo_move_to(cr, x, MATH_PAD);
cairo_line_to(cr, x, board_px_h() - MATH_PAD);
}
for (i = 0; i <= MATH_GY; i++) {
double y = MATH_PAD + i * MATH_CELL;
cairo_move_to(cr, MATH_PAD, y);
cairo_line_to(cr, board_px_w() - MATH_PAD, y);
}
cairo_stroke(cr);
paint_hex(cr, app_theme.text);
cairo_set_line_width(cr, 1.6);
cairo_move_to(cr, MATH_PAD, board_px_h() - MATH_PAD);
cairo_line_to(cr, board_px_w() - MATH_PAD, board_px_h() - MATH_PAD);
cairo_move_to(cr, MATH_PAD, board_px_h() - MATH_PAD);
cairo_line_to(cr, MATH_PAD, MATH_PAD);
cairo_stroke(cr);
if (b->nf >= 2) {
double x0, y0, x1, y1;
paint_hex(cr, app_theme.accent);
cairo_set_line_width(cr, 2);
board_to_screen(b->fx[0], b->fy[0], &x0, &y0);
board_to_screen(b->fx[1], b->fy[1], &x1, &y1);
cairo_move_to(cr, x0, y0);
cairo_line_to(cr, x1, y1);
if (b->nf >= 3) {
double x2, y2;
board_to_screen(b->fx[2], b->fy[2], &x2, &y2);
cairo_move_to(cr, x0, y0);
cairo_line_to(cr, x2, y2);
}
cairo_stroke(cr);
}
for (i = 0; i < b->nf && i < 4; i++) {
double x, y;
board_to_screen(b->fx[i], b->fy[i], &x, &y);
paint_hex(cr, app_theme.accent);
cairo_arc(cr, x, y, 5.5, 0, 2 * G_PI);
cairo_fill(cr);
paint_hex(cr, app_theme.text);
cairo_set_font_size(cr, 13);
cairo_move_to(cr, x + 7, y - 7);
cairo_show_text(cr, names[i]);
}
for (i = 0; i < b->n; i++) {
double x, y;
char label[8];
board_to_screen(b->ux[i], b->uy[i], &x, &y);
if (b->checked)
paint_hex(cr, b->correct ? app_theme.success : app_theme.error);
else
paint_hex(cr, app_theme.success);
cairo_arc(cr, x, y, 5.5, 0, 2 * G_PI);
cairo_fill(cr);
paint_hex(cr, app_theme.text);
g_snprintf(label, sizeof label, "%d", i + 1);
cairo_set_font_size(cr, 13);
cairo_move_to(cr, x + 7, y + 14);
cairo_show_text(cr, label);
}
}
static int board_has_fixed(MathBoard *b, int x, int y) {
for (int i = 0; i < b->nf; i++)
if (b->fx[i] == x && b->fy[i] == y)
return 1;
return 0;
}
static void board_pressed(GtkGestureClick *gesture, int n_press, double x, double y,
gpointer data) {
MathBoard *b = data;
int gx, gy, i;
(void)gesture;
(void)n_press;
if (!board_snap(x, y, &gx, &gy) || board_has_fixed(b, gx, gy))
return;
for (i = 0; i < b->n; i++) {
if (b->ux[i] == gx && b->uy[i] == gy) {
memmove(&b->ux[i], &b->ux[i + 1], (b->n - i - 1) * sizeof(int));
memmove(&b->uy[i], &b->uy[i + 1], (b->n - i - 1) * sizeof(int));
b->n--;
b->checked = 0;
gtk_widget_queue_draw(b->area);
return;
}
}
if (b->n >= b->need || b->n >= MATH_PTS)
return;
b->ux[b->n] = gx;
b->uy[b->n] = gy;
b->n++;
b->checked = 0;
gtk_widget_queue_draw(b->area);
}
static void board_undo(GtkButton *button, gpointer data) {
MathBoard *b = data;
(void)button;
if (b->n <= 0)
return;
b->n--;
b->checked = 0;
gtk_widget_queue_draw(b->area);
}
static void board_clear(GtkButton *button, gpointer data) {
MathBoard *b = data;
(void)button;
b->n = 0;
b->checked = 0;
gtk_widget_queue_draw(b->area);
}
static void math_ex_check(GtkButton *button, gpointer data) {
MathExCtx *ctx = data;
int ok = 0;
(void)button;
for (int i = 0; i < ctx->n; i++) {
int good = 0;
if (ctx->boards[i]) {
MathBoard *b = ctx->boards[i];
good = math_draw_ok(b->spec, b->ux, b->uy, b->n);
b->checked = 1;
b->correct = good;
gtk_widget_queue_draw(b->area);
} else if (ctx->entries[i]) {
const char *text = gtk_editable_get_text(GTK_EDITABLE(ctx->entries[i]));
gtk_widget_remove_css_class(ctx->entries[i], "ok");
gtk_widget_remove_css_class(ctx->entries[i], "wrong");
good = math_answer_ok(ctx->items[i].answer, text);
gtk_widget_add_css_class(ctx->entries[i], good ? "ok" : "wrong");
}
if (good)
ok++;
if (ctx->hints[i])
gtk_widget_set_visible(ctx->hints[i], TRUE);
}
if (ok == ctx->n) {
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
math_mark_done(ctx->track, ctx->lesson_id);
} else {
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
}
}
GtkWidget *math_practice_page(int year, int lesson_id, const char *back_page,
const char *title_key, const char *heading_key,
const MathItem *items, int n) {
GtkWidget *page;
GtkWidget *scroll;
GtkWidget *body;
GtkWidget *check;
GtkWidget *head;
GtkWidget *intro;
MathExCtx *ctx = g_new0(MathExCtx, 1);
math_ensure();
if (year < 0 || year > 4)
year = 0;
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_margin_start(page, 32);
gtk_widget_set_margin_end(page, 32);
gtk_widget_set_margin_top(page, 24);
gtk_widget_set_margin_bottom(page, 24);
gtk_box_append(GTK_BOX(page), top_bar(back_page, title_key, NULL));
scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 12);
gtk_box_append(GTK_BOX(page), scroll);
body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
gtk_widget_set_hexpand(body, TRUE);
gtk_widget_set_margin_start(body, 4);
gtk_widget_set_margin_end(body, 4);
gtk_widget_set_margin_bottom(body, 12);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
head = gtk_label_new(NULL);
i18n_bind(head, heading_key, 0);
gtk_widget_set_halign(head, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(head), TRUE);
gtk_widget_add_css_class(head, "quiz-heading");
gtk_box_append(GTK_BOX(body), head);
intro = gtk_label_new(NULL);
i18n_bind(intro, "math_quiz_intro", 0);
gtk_widget_set_halign(intro, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(intro), TRUE);
gtk_label_set_xalign(GTK_LABEL(intro), 0);
gtk_widget_add_css_class(intro, "quiz-intro");
gtk_box_append(GTK_BOX(body), intro);
ctx->items = items;
ctx->n = n;
ctx->track = &math_tracks[year];
ctx->lesson_id = lesson_id;
ctx->entries = g_new0(GtkWidget *, n);
ctx->boards = g_new0(MathBoard *, n);
ctx->hints = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) {
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
GtkWidget *prompt = gtk_label_new(items[i].prompt);
GtkWidget *hint = gtk_label_new(items[i].hint);
gtk_widget_set_halign(card, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), card);
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
gtk_label_set_xalign(GTK_LABEL(prompt), 0);
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(card), prompt);
if (math_is_draw(items[i].answer)) {
MathBoard *b = g_new0(MathBoard, 1);
GtkWidget *area = gtk_drawing_area_new();
GtkGesture *click = gtk_gesture_click_new();
GtkWidget *tools = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget *undo = gtk_button_new();
GtkWidget *clear = gtk_button_new();
GtkWidget *note = gtk_label_new(NULL);
b->spec = items[i].answer;
b->need = math_draw_need(items[i].answer);
if (b->need < 1)
b->need = 1;
b->nf = math_draw_fixed(items[i].answer, b->fx, b->fy, 8);
b->area = area;
ctx->boards[i] = b;
gtk_widget_set_size_request(area, board_px_w(), board_px_h());
gtk_widget_set_halign(area, GTK_ALIGN_START);
gtk_drawing_area_set_content_width(GTK_DRAWING_AREA(area), board_px_w());
gtk_drawing_area_set_content_height(GTK_DRAWING_AREA(area), board_px_h());
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), board_draw, b, NULL);
gtk_widget_add_controller(area, GTK_EVENT_CONTROLLER(click));
g_signal_connect(click, "pressed", G_CALLBACK(board_pressed), b);
gtk_box_append(GTK_BOX(card), area);
i18n_bind(note, "math_draw_hint", 0);
gtk_label_set_wrap(GTK_LABEL(note), TRUE);
gtk_widget_add_css_class(note, "hint");
gtk_widget_set_halign(note, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(card), note);
i18n_bind(undo, "math_undo", 1);
i18n_bind(clear, "math_clear", 1);
g_signal_connect(undo, "clicked", G_CALLBACK(board_undo), b);
g_signal_connect(clear, "clicked", G_CALLBACK(board_clear), b);
gtk_box_append(GTK_BOX(tools), undo);
gtk_box_append(GTK_BOX(tools), clear);
gtk_box_append(GTK_BOX(card), tools);
} else {
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), tr("math_answer_ph"));
gtk_widget_set_hexpand(entry, TRUE);
gtk_widget_set_size_request(entry, 280, -1);
gtk_box_append(GTK_BOX(card), entry);
ctx->entries[i] = entry;
}
gtk_label_set_wrap(GTK_LABEL(hint), TRUE);
gtk_label_set_xalign(GTK_LABEL(hint), 0);
gtk_widget_set_halign(hint, GTK_ALIGN_START);
gtk_widget_add_css_class(hint, "hint");
gtk_widget_set_visible(hint, FALSE);
gtk_box_append(GTK_BOX(card), hint);
ctx->hints[i] = hint;
}
check = gtk_button_new();
i18n_bind(check, "check", 1);
gtk_widget_add_css_class(check, "btn-primary");
gtk_widget_set_halign(check, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), check);
g_signal_connect(check, "clicked", G_CALLBACK(math_ex_check), ctx);
ctx->feedback = gtk_label_new("");
gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), ctx->feedback);
return page;
}

1362
src/math_check.c Normal file

File diff suppressed because it is too large Load diff

176
src/math_i18n.inc Normal file
View file

@ -0,0 +1,176 @@
{"mat_years_sub", "Opakování základní školy a čtyři ročníky gymnázia.", "Elementary review and four years of gymnasium mathematics."},
{"math_quiz_intro", "Napište výsledek. Zlomky jako 3/4, desetinnou čárku i tečku, π jako pi, odmocninu jako sqrt(2) nebo √2. Více čísel oddělte středníkem.", "Write the result. Fractions as 3/4, a decimal comma or point, π as pi, a root as sqrt(2) or √2. Separate several numbers with a semicolon."},
{"math_answer_ph", "výsledek", "result"},
{"math_undo", "Zpět", "Undo"},
{"math_clear", "Smazat body", "Clear points"},
{"math_draw_hint", "Klepněte na mřížku. Osa y směřuje nahoru.", "Tap the grid. The y-axis points up."},
{"mat0_year", "Opakování základní školy", "Elementary-school review"},
{"mat0_sub", "Pořadí operací, zlomky, procenta, rovnice a rýsování.", "Order of operations, fractions, percentages, equations and a construction."},
{"mat0_unit1", "Pořadí operací", "Order of operations"},
{"mat0_unit1_sub", "Závorky, násobení a dělení, sčítání a odčítání.", "Brackets, then multiplication and division, then addition and subtraction."},
{"mat0_ex1_title", "Příklady", "Practice"},
{"mat0_quiz1_head", "Pořadí operací", "Order of operations"},
{"mat0_unit2", "Zlomky", "Fractions"},
{"mat0_unit2_sub", "Sčítání, odčítání, násobení a dělení zlomků.", "Adding, subtracting, multiplying and dividing fractions."},
{"mat0_ex2_title", "Příklady", "Practice"},
{"mat0_quiz2_head", "Zlomky", "Fractions"},
{"mat0_unit3", "Desetinná čísla", "Decimals"},
{"mat0_unit3_sub", "Sčítání, násobení a dělení desetinných čísel.", "Adding, multiplying and dividing decimals."},
{"mat0_ex3_title", "Příklady", "Practice"},
{"mat0_quiz3_head", "Desetinná čísla", "Decimals"},
{"mat0_unit4", "Procenta", "Percentages"},
{"mat0_unit4_sub", "Procento ze základu, počet procent a cena po slevě.", "A percentage of a base, the rate, and a price after a discount."},
{"mat0_ex4_title", "Příklady", "Practice"},
{"mat0_quiz4_head", "Procenta", "Percentages"},
{"mat0_unit5", "Poměr a měřítko", "Ratio and scale"},
{"mat0_unit5_sub", "Dělení v poměru, úměra a měřítko mapy.", "Sharing in a ratio, proportion and the scale of a map."},
{"mat0_ex5_title", "Příklady", "Practice"},
{"mat0_quiz5_head", "Poměr a měřítko", "Ratio and scale"},
{"mat0_unit6", "Záporná čísla", "Negative numbers"},
{"mat0_unit6_sub", "Sčítání, násobení a absolutní hodnota.", "Addition, multiplication and absolute value."},
{"mat0_ex6_title", "Příklady", "Practice"},
{"mat0_quiz6_head", "Záporná čísla", "Negative numbers"},
{"mat0_unit7", "Lineární rovnice", "Linear equations"},
{"mat0_unit7_sub", "Ekvivalentní úpravy a jednoduché rovnice.", "Equivalent steps and simple equations."},
{"mat0_ex7_title", "Příklady", "Practice"},
{"mat0_quiz7_head", "Lineární rovnice", "Linear equations"},
{"mat0_unit8", "Obvod, obsah a čtverec", "Perimeter, area and a square"},
{"mat0_unit8_sub", "Obdélník, trojúhelník a doplnění čtverce na mřížce.", "A rectangle, a triangle, and completing a square on the grid."},
{"mat0_ex8_title", "Příklady", "Practice"},
{"mat0_quiz8_head", "Obvod, obsah a čtverec", "Perimeter, area and a square"},
{"mat_year1", "1. ročník", "Year 1"},
{"mat_sub", "Dělitelnost, výrazy, rovnice, planimetrie a konstrukce.", "Divisibility, expressions, equations, plane figures and a construction."},
{"mat_unit1", "Dělitelnost a prvočísla", "Divisibility and primes"},
{"mat_unit1_sub", "Největší společný dělitel, násobek a rozklad.", "Highest common factor, lowest common multiple and factorisation."},
{"mat_ex1_title", "Příklady", "Practice"},
{"mat_quiz1_head", "Dělitelnost a prvočísla", "Divisibility and primes"},
{"mat_unit2", "Mocniny a odmocniny", "Powers and roots"},
{"mat_unit2_sub", "Mocniny se stejným základem a druhá odmocnina.", "Powers with the same base and the square root."},
{"mat_ex2_title", "Příklady", "Practice"},
{"mat_quiz2_head", "Mocniny a odmocniny", "Powers and roots"},
{"mat_unit3", "Algebraické výrazy", "Algebraic expressions"},
{"mat_unit3_sub", "Dosazení, druhá mocnina součtu a rozklad rozdílu čtverců.", "Substitution, the square of a sum and a difference of squares."},
{"mat_ex3_title", "Příklady", "Practice"},
{"mat_quiz3_head", "Algebraické výrazy", "Algebraic expressions"},
{"mat_unit4", "Lineární rovnice s úpravami", "Linear equations"},
{"mat_unit4_sub", "Rovnice s koeficientem, závorkou a zlomkem.", "Equations with a coefficient, a bracket and a fraction."},
{"mat_ex4_title", "Příklady", "Practice"},
{"mat_quiz4_head", "Lineární rovnice s úpravami", "Linear equations"},
{"mat_unit5", "Slovní úlohy", "Word problems"},
{"mat_unit5_sub", "Dráha, společná práce, směs a sleva.", "Distance, joint work, a mixture and a discount."},
{"mat_ex5_title", "Příklady", "Practice"},
{"mat_quiz5_head", "Slovní úlohy", "Word problems"},
{"mat_unit6", "Planimetrie", "Plane figures"},
{"mat_unit6_sub", "Úhly trojúhelníku, vnější úhel a pravidelný mnohoúhelník.", "Angles of a triangle, the exterior angle and a regular polygon."},
{"mat_ex6_title", "Příklady", "Practice"},
{"mat_quiz6_head", "Planimetrie", "Plane figures"},
{"mat_unit7", "Obvody a obsahy", "Perimeter and area"},
{"mat_unit7_sub", "Kružnice, lichoběžník a Pythagorova věta.", "The circle, the trapezium and Pythagoras."},
{"mat_ex7_title", "Příklady", "Practice"},
{"mat_quiz7_head", "Obvody a obsahy", "Perimeter and area"},
{"mat_unit8", "Konstrukce trojúhelníku", "Constructing a triangle"},
{"mat_unit8_sub", "Vzdálenost bodů a trojúhelník daný třemi stranami.", "The distance between points and a triangle given by three sides."},
{"mat_ex8_title", "Příklady", "Practice"},
{"mat_quiz8_head", "Konstrukce trojúhelníku", "Constructing a triangle"},
{"mat_year2", "2. ročník", "Year 2"},
{"mat2_sub", "Funkce, soustavy, kvadratické rovnice, goniometrie a kružnice.", "Functions, systems, quadratic equations, trigonometry and circles."},
{"mat2_unit1", "Lineární funkce", "Linear functions"},
{"mat2_unit1_sub", "Předpis, průsečík s osou y, směrnice a nulový bod.", "The formula, the y-intercept, the slope and a root."},
{"mat2_ex1_title", "Příklady", "Practice"},
{"mat2_quiz1_head", "Lineární funkce", "Linear functions"},
{"mat2_unit2", "Soustavy rovnic", "Systems of equations"},
{"mat2_unit2_sub", "Dvě lineární rovnice o dvou neznámých.", "Two linear equations in two unknowns."},
{"mat2_ex2_title", "Příklady", "Practice"},
{"mat2_quiz2_head", "Soustavy rovnic", "Systems of equations"},
{"mat2_unit3", "Kvadratické rovnice", "Quadratic equations"},
{"mat2_unit3_sub", "Rozklad, diskriminant a dvojnásobný kořen.", "Factoring, the discriminant and a double root."},
{"mat2_ex3_title", "Příklady", "Practice"},
{"mat2_quiz3_head", "Kvadratické rovnice", "Quadratic equations"},
{"mat2_unit4", "Lomené výrazy", "Rational expressions"},
{"mat2_unit4_sub", "Krácení, definiční obor a násobení zlomků s proměnnou.", "Cancelling, the domain and multiplying algebraic fractions."},
{"mat2_ex4_title", "Příklady", "Practice"},
{"mat2_quiz4_head", "Lomené výrazy", "Rational expressions"},
{"mat2_unit5", "Mocniny s racionálním exponentem", "Rational exponents"},
{"mat2_unit5_sub", "Odmocniny, součin mocnin a záporný exponent.", "Roots, a product of powers and a negative exponent."},
{"mat2_ex5_title", "Příklady", "Practice"},
{"mat2_quiz5_head", "Mocniny s racionálním exponentem", "Rational exponents"},
{"mat2_unit6", "Goniometrie pravoúhlého trojúhelníku", "Right-triangle trigonometry"},
{"mat2_unit6_sub", "Sinus, kosinus a tangens ostrého úhlu.", "Sine, cosine and tangent of an acute angle."},
{"mat2_ex6_title", "Příklady", "Practice"},
{"mat2_quiz6_head", "Goniometrie pravoúhlého trojúhelníku", "Right-triangle trigonometry"},
{"mat2_unit7", "Pythagorova a Eukleidovy věty", "Pythagoras and Euclid"},
{"mat2_unit7_sub", "Přepona, obsah čtverce nad ní a úseky přepony.", "The hypotenuse, the square on it, and the projections on it."},
{"mat2_ex7_title", "Příklady", "Practice"},
{"mat2_quiz7_head", "Pythagorova a Eukleidovy věty", "Pythagoras and Euclid"},
{"mat2_unit8", "Kružnice a Thaletova věta", "Circles and Thales"},
{"mat2_unit8_sub", "Obvodový a středový úhel, délka oblouku a pravý úhel nad průměrem.", "An inscribed and a central angle, arc length, and the right angle in a semicircle."},
{"mat2_ex8_title", "Příklady", "Practice"},
{"mat2_quiz8_head", "Kružnice a Thaletova věta", "Circles and Thales"},
{"mat_year3", "3. ročník", "Year 3"},
{"mat3_sub", "Logaritmus, posloupnosti, pravděpodobnost a stereometrie.", "Logarithms, sequences, probability and solid geometry."},
{"mat3_unit1", "Kvadratická funkce", "The quadratic function"},
{"mat3_unit1_sub", "Vrchol, kořeny a hodnota paraboly.", "The vertex, the roots and a value of a parabola."},
{"mat3_ex1_title", "Příklady", "Practice"},
{"mat3_quiz1_head", "Kvadratická funkce", "The quadratic function"},
{"mat3_unit2", "Exponenciální funkce a logaritmus", "Exponentials and logarithms"},
{"mat3_unit2_sub", "Mocnina se základem a logaritmus jako její opak.", "A power and the logarithm as its inverse."},
{"mat3_ex2_title", "Příklady", "Practice"},
{"mat3_quiz2_head", "Exponenciální funkce a logaritmus", "Exponentials and logarithms"},
{"mat3_unit3", "Goniometrické funkce", "Trigonometric functions"},
{"mat3_unit3_sub", "Sinus a kosinus na jednotkové kružnici.", "Sine and cosine on the unit circle."},
{"mat3_ex3_title", "Příklady", "Practice"},
{"mat3_quiz3_head", "Goniometrické funkce", "Trigonometric functions"},
{"mat3_unit4", "Sinová a kosinová věta", "The sine and cosine rules"},
{"mat3_unit4_sub", "Obecný trojúhelník, poloměr kružnice a obsah.", "A general triangle, the circumradius and the area."},
{"mat3_ex4_title", "Příklady", "Practice"},
{"mat3_quiz4_head", "Sinová a kosinová věta", "The sine and cosine rules"},
{"mat3_unit5", "Posloupnosti a řady", "Sequences and series"},
{"mat3_unit5_sub", "Aritmetická a geometrická posloupnost a nekonečná řada.", "An arithmetic and a geometric sequence, and an infinite series."},
{"mat3_ex5_title", "Příklady", "Practice"},
{"mat3_quiz5_head", "Posloupnosti a řady", "Sequences and series"},
{"mat3_unit6", "Kombinatorika", "Combinatorics"},
{"mat3_unit6_sub", "Permutace, variace a kombinace.", "Permutations, variations and combinations."},
{"mat3_ex6_title", "Příklady", "Practice"},
{"mat3_quiz6_head", "Kombinatorika", "Combinatorics"},
{"mat3_unit7", "Pravděpodobnost", "Probability"},
{"mat3_unit7_sub", "Klasická pravděpodobnost, kostka, mince a karty.", "Classical probability with a die, a coin and cards."},
{"mat3_ex7_title", "Příklady", "Practice"},
{"mat3_quiz7_head", "Pravděpodobnost", "Probability"},
{"mat3_unit8", "Stereometrie", "Solid geometry"},
{"mat3_unit8_sub", "Krychle, válec, koule a jehlan.", "The cube, the cylinder, the sphere and a pyramid."},
{"mat3_ex8_title", "Příklady", "Practice"},
{"mat3_quiz8_head", "Stereometrie", "Solid geometry"},
{"mat_year4", "4. ročník", "Year 4"},
{"mat4_sub", "Analytická geometrie, derivace, integrál a maturitní opakování.", "Coordinate geometry, derivatives, integrals and a maturita review."},
{"mat4_unit1", "Vektory", "Vectors"},
{"mat4_unit1_sub", "Velikost, součet, skalární součin a koncový bod.", "Magnitude, a sum, the dot product and an endpoint."},
{"mat4_ex1_title", "Příklady", "Practice"},
{"mat4_quiz1_head", "Vektory", "Vectors"},
{"mat4_unit2", "Analytická přímka", "The line in the plane"},
{"mat4_unit2_sub", "Směrnice, úsek, parametrické vyjádření a vzdálenost od přímky.", "Slope, intercept, a parametric equation and the distance from a line."},
{"mat4_ex2_title", "Příklady", "Practice"},
{"mat4_quiz2_head", "Analytická přímka", "The line in the plane"},
{"mat4_unit3", "Kuželosečky", "Conic sections"},
{"mat4_unit3_sub", "Kružnice a elipsa v osové poloze.", "A circle and an ellipse aligned with the axes."},
{"mat4_ex3_title", "Příklady", "Practice"},
{"mat4_quiz3_head", "Kuželosečky", "Conic sections"},
{"mat4_unit4", "Komplexní čísla", "Complex numbers"},
{"mat4_unit4_sub", "Součet, součin, absolutní hodnota a mocnina i.", "A sum, a product, the modulus and a power of i."},
{"mat4_ex4_title", "Příklady", "Practice"},
{"mat4_quiz4_head", "Komplexní čísla", "Complex numbers"},
{"mat4_unit5", "Derivace", "The derivative"},
{"mat4_unit5_sub", "Derivace mocniny, tečna a derivace sinu.", "The derivative of a power, a tangent and the derivative of sine."},
{"mat4_ex5_title", "Příklady", "Practice"},
{"mat4_quiz5_head", "Derivace", "The derivative"},
{"mat4_unit6", "Průběh funkce", "The shape of a function"},
{"mat4_unit6_sub", "Stacionární body, lokální extrémy a nulová derivace.", "Stationary points, local extrema and a zero derivative."},
{"mat4_ex6_title", "Příklady", "Practice"},
{"mat4_quiz6_head", "Průběh funkce", "The shape of a function"},
{"mat4_unit7", "Integrál", "The integral"},
{"mat4_unit7_sub", "Určitý integrál mocniny jako obsah pod grafem.", "The definite integral of a power as the area under the graph."},
{"mat4_ex7_title", "Příklady", "Practice"},
{"mat4_quiz7_head", "Integrál", "The integral"},
{"mat4_unit8", "Maturitní mix", "Maturita mix"},
{"mat4_unit8_sub", "Logaritmus, kombinace, vektor, zlomek a kvadratická rovnice.", "A logarithm, a combination, a vector, a fraction and a quadratic equation."},
{"mat4_ex8_title", "Příklady", "Practice"},
{"mat4_quiz8_head", "Maturitní mix", "Maturita mix"},

View file

@ -61,6 +61,7 @@ void load_progress(void) {
lit_load_progress(); lit_load_progress();
sci_load_progress(); sci_load_progress();
fyz_load_progress(); fyz_load_progress();
math_load_progress();
} }
/* Redraw the exercise rails of every unit that already has a page. */ /* Redraw the exercise rails of every unit that already has a page. */

View file

@ -23,7 +23,7 @@ static GPtrArray *search_items;
static gboolean subject_open(int i) { static gboolean subject_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ
|| i == ON_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; || i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ;
} }
static const char *subject_page(int i) { static const char *subject_page(int i) {
@ -37,6 +37,8 @@ static const char *subject_page(int i) {
return "czechmap"; return "czechmap";
if (i == ON_SUBJ) if (i == ON_SUBJ)
return "onyears"; return "onyears";
if (i == MATH_SUBJ)
return "matyears";
if (i == PHY_SUBJ) if (i == PHY_SUBJ)
return "fyzyears"; return "fyzyears";
if (i == SCI_SUBJ) if (i == SCI_SUBJ)
@ -275,6 +277,47 @@ static void catalog_rebuild(void) {
g_snprintf(page, sizeof(page), "lit4ex%d", i); g_snprintf(page, sizeof(page), "lit4ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
} }
catalog_add_key("Matematika", "search_lesson", "matyears", FALSE, 1, "matematika");
catalog_add_key("mat0_year", "search_lesson", "mat0map", FALSE, 1, "matematika zakladka");
catalog_add_key("mat_year1", "search_lesson", "matmap", FALSE, 1, "matematika rocnik");
catalog_add_key("mat_year2", "search_lesson", "mat2map", FALSE, 1, "matematika rocnik");
catalog_add_key("mat_year3", "search_lesson", "mat3map", FALSE, 1, "matematika rocnik");
catalog_add_key("mat_year4", "search_lesson", "mat4map", FALSE, 1, "matematika rocnik");
for (i = 1; i <= MATH_N; i++) {
g_snprintf(key, sizeof(key), "mat0_unit%d", i);
g_snprintf(page, sizeof(page), "mat0unit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika");
g_snprintf(page, sizeof(page), "mat0ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady");
}
for (i = 1; i <= MATH_N; i++) {
g_snprintf(key, sizeof(key), "mat_unit%d", i);
g_snprintf(page, sizeof(page), "matunit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika");
g_snprintf(page, sizeof(page), "matex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady");
}
for (i = 1; i <= MATH_N; i++) {
g_snprintf(key, sizeof(key), "mat2_unit%d", i);
g_snprintf(page, sizeof(page), "mat2unit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika");
g_snprintf(page, sizeof(page), "mat2ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady");
}
for (i = 1; i <= MATH_N; i++) {
g_snprintf(key, sizeof(key), "mat3_unit%d", i);
g_snprintf(page, sizeof(page), "mat3unit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika");
g_snprintf(page, sizeof(page), "mat3ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady");
}
for (i = 1; i <= MATH_N; i++) {
g_snprintf(key, sizeof(key), "mat4_unit%d", i);
g_snprintf(page, sizeof(page), "mat4unit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "matematika");
g_snprintf(page, sizeof(page), "mat4ex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "priklady");
}
catalog_add_key("fyz_year1", "search_lesson", "fyzmap", FALSE, 1, "fyzika rocnik"); catalog_add_key("fyz_year1", "search_lesson", "fyzmap", FALSE, 1, "fyzika rocnik");
catalog_add_key("fyz_year2", "search_lesson", "fyz2map", FALSE, 1, "fyzika rocnik"); catalog_add_key("fyz_year2", "search_lesson", "fyz2map", FALSE, 1, "fyzika rocnik");
catalog_add_key("fyz_year3", "search_lesson", "fyz3map", FALSE, 1, "fyzika rocnik"); catalog_add_key("fyz_year3", "search_lesson", "fyz3map", FALSE, 1, "fyzika rocnik");

View file

@ -79,6 +79,8 @@ void refresh_stats_ui(void) {
progress_for_sci(&all); progress_for_sci(&all);
else if (s == PHY_SUBJ) else if (s == PHY_SUBJ)
progress_for_fyz(&all); progress_for_fyz(&all);
else if (s == MATH_SUBJ)
progress_for_math(&all);
else else
progress_for_units(sub_unit_start[s], sub_unit_count[s], &all); progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
} }
@ -110,6 +112,7 @@ void refresh_stats_ui(void) {
gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ) gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ)
|| (s == ON_SUBJ) || (s == CZ_SUBJ) || (s == ON_SUBJ) || (s == CZ_SUBJ)
|| (s == SCI_SUBJ) || (s == PHY_SUBJ) || (s == SCI_SUBJ) || (s == PHY_SUBJ)
|| (s == MATH_SUBJ)
|| sub_unit_count[s] > 0; || sub_unit_count[s] > 0;
if (!count || !bar) if (!count || !bar)
@ -128,6 +131,8 @@ void refresh_stats_ui(void) {
progress_for_sci(&sp); progress_for_sci(&sp);
else if (s == PHY_SUBJ) else if (s == PHY_SUBJ)
progress_for_fyz(&sp); progress_for_fyz(&sp);
else if (s == MATH_SUBJ)
progress_for_math(&sp);
else else
progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp); progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
if (has_content) { if (has_content) {
@ -399,6 +404,7 @@ GtkWidget *build_stats_page(void) {
gboolean has_units = (s == NET_SUBJ) || (s == HW_SUBJ) gboolean has_units = (s == NET_SUBJ) || (s == HW_SUBJ)
|| (s == CZ_SUBJ) || (s == ON_SUBJ) || (s == CZ_SUBJ) || (s == ON_SUBJ)
|| (s == SCI_SUBJ) || (s == PHY_SUBJ) || (s == SCI_SUBJ) || (s == PHY_SUBJ)
|| (s == MATH_SUBJ)
|| sub_unit_count[s] > 0; || sub_unit_count[s] > 0;
gboolean has_deutsch_units = sub_unit_count[s] > 0; gboolean has_deutsch_units = sub_unit_count[s] > 0;

View file

@ -5,7 +5,8 @@
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware), CZ_SUBJ
* (Czech), ON_SUBJ (civics), PHY_SUBJ (physics) and SCI_SUBJ * (Czech), ON_SUBJ (civics), MATH_SUBJ (mathematics), PHY_SUBJ
* (physics) and SCI_SUBJ
* (natural sciences) are unlocked. */ * (natural sciences) are unlocked. */
const char *sub_keys[NUM_SUBJECTS] = { const char *sub_keys[NUM_SUBJECTS] = {
"Deutsch", "Deutsch",
@ -155,7 +156,7 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
static gboolean subject_is_open(int i) { static gboolean subject_is_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ
|| i == ON_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ; || i == ON_SUBJ || i == MATH_SUBJ || i == PHY_SUBJ || i == SCI_SUBJ;
} }
static GtkWidget *subject_icon(int i) { static GtkWidget *subject_icon(int i) {
@ -178,6 +179,9 @@ static GtkWidget *subject_icon(int i) {
else if (i == ON_SUBJ) else if (i == ON_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_civics_icon, NULL, NULL); draw_civics_icon, NULL, NULL);
else if (i == MATH_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_math_icon, NULL, NULL);
else if (i == PHY_SUBJ) else if (i == PHY_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area), gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_physics_icon, NULL, NULL); draw_physics_icon, NULL, NULL);
@ -200,6 +204,8 @@ static const char *subject_target(int i) {
return "czechmap"; return "czechmap";
if (i == ON_SUBJ) if (i == ON_SUBJ)
return "onyears"; return "onyears";
if (i == MATH_SUBJ)
return "matyears";
if (i == PHY_SUBJ) if (i == PHY_SUBJ)
return "fyzyears"; return "fyzyears";
if (i == SCI_SUBJ) if (i == SCI_SUBJ)

View file

@ -434,5 +434,6 @@ void apply_theme(void) {
lit_rail_theme_reset(); lit_rail_theme_reset();
sci_rail_theme_reset(); sci_rail_theme_reset();
fyz_rail_theme_reset(); fyz_rail_theme_reset();
math_rail_theme_reset();
book_rail_theme_reset(); book_rail_theme_reset();
} }

View file

@ -36,6 +36,11 @@ char *app_progress_fyz;
char *app_progress_fyz2; char *app_progress_fyz2;
char *app_progress_fyz3; char *app_progress_fyz3;
char *app_progress_fyz4; char *app_progress_fyz4;
char *app_progress_mat0;
char *app_progress_mat;
char *app_progress_mat2;
char *app_progress_mat3;
char *app_progress_mat4;
static char *dup_dirname(const char *path) { static char *dup_dirname(const char *path) {
char *tmp = g_strdup(path); char *tmp = g_strdup(path);
@ -188,6 +193,11 @@ static void set_progress_paths(const char *write_root) {
g_free(app_progress_fyz2); g_free(app_progress_fyz2);
g_free(app_progress_fyz3); g_free(app_progress_fyz3);
g_free(app_progress_fyz4); g_free(app_progress_fyz4);
g_free(app_progress_mat0);
g_free(app_progress_mat);
g_free(app_progress_mat2);
g_free(app_progress_mat3);
g_free(app_progress_mat4);
app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL); app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL);
app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL); app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL);
@ -212,6 +222,11 @@ static void set_progress_paths(const char *write_root) {
app_progress_fyz2 = g_build_filename(app_progress_dir, "fyz2.conf", NULL); app_progress_fyz2 = g_build_filename(app_progress_dir, "fyz2.conf", NULL);
app_progress_fyz3 = g_build_filename(app_progress_dir, "fyz3.conf", NULL); app_progress_fyz3 = g_build_filename(app_progress_dir, "fyz3.conf", NULL);
app_progress_fyz4 = g_build_filename(app_progress_dir, "fyz4.conf", NULL); app_progress_fyz4 = g_build_filename(app_progress_dir, "fyz4.conf", NULL);
app_progress_mat0 = g_build_filename(app_progress_dir, "mat0.conf", NULL);
app_progress_mat = g_build_filename(app_progress_dir, "mat.conf", NULL);
app_progress_mat2 = g_build_filename(app_progress_dir, "mat2.conf", NULL);
app_progress_mat3 = g_build_filename(app_progress_dir, "mat3.conf", NULL);
app_progress_mat4 = g_build_filename(app_progress_dir, "mat4.conf", NULL);
} }
/* Resolve read-only asset root, chdir there, and point progress at a writable /* Resolve read-only asset root, chdir there, and point progress at a writable