Move shared Kotlin code to android/shared, remove GTK app and old data packs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-09 20:39:42 +02:00
commit b324496334
172 changed files with 0 additions and 124855 deletions

View file

@ -1,286 +0,0 @@
package org.maturita.maturita
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import org.maturita.maturita.data.ColorMode
import org.maturita.maturita.data.Content
import org.maturita.maturita.data.ProgressStore
import org.maturita.maturita.data.ThemeId
import org.maturita.maturita.data.UiLang
import org.maturita.maturita.data.themePalette
import org.maturita.maturita.update.UpdateState
import org.maturita.maturita.update.Updater
class AppViewModel(app: Application) : AndroidViewModel(app) {
val content = Content.load(app)
val progress = ProgressStore(app)
val updater = Updater(app)
var themeId by mutableStateOf(progress.themeId)
private set
var mode by mutableStateOf(progress.mode)
private set
var lang by mutableStateOf(progress.lang)
private set
var searchOpen by mutableStateOf(false)
var searchQuery by mutableStateOf("")
var tick by mutableStateOf(0)
private set
var update by mutableStateOf(UpdateState())
private set
var showChangelog by mutableStateOf(shouldShowChangelog())
private set
/** True when the latest navigation moved deeper; false for back and tab switches. */
var navigatingForward by mutableStateOf(true)
private set
private val stack = mutableStateListOf<Route>(Route.Subjects)
val route: Route get() = stack.last()
val root: Route get() = stack.first()
val canGoBack: Boolean get() = stack.size > 1 || root != Route.Subjects
val palette get() = themePalette(themeId, mode)
fun tr(key: String?) = content.tr(key, lang)
fun fmt(key: String, vararg args: Any) = content.fmt(key, lang, *args)
/** Roots switch the bottom tab; everything else is pushed on the practice stack. */
fun go(r: Route) {
searchOpen = false
if (stack.last() == r) return
if (r.isRoot) {
navigatingForward = false
val target = if (r == Route.Welcome) Route.Subjects else r
stack.clear()
stack.add(target)
return
}
navigatingForward = true
if (root != Route.Subjects) {
stack.clear()
stack.addAll(r.stack())
return
}
stack.add(r)
}
fun goPage(page: String?) {
val r = page?.let { routeFromPage(it) } ?: return
go(r)
}
/** Jumping from search rebuilds the whole path so back walks up level by level. */
fun openPage(page: String) {
val r = routeFromPage(page) ?: return
searchOpen = false
navigatingForward = true
stack.clear()
stack.addAll(r.stack())
}
/** Replace the current screen (lesson slides -> its exercise). */
fun replace(r: Route) {
navigatingForward = true
if (stack.size > 1) stack.removeAt(stack.lastIndex)
stack.add(r)
}
fun back() {
navigatingForward = false
when {
stack.size > 1 -> stack.removeAt(stack.lastIndex)
root != Route.Subjects -> go(Route.Subjects)
}
}
fun setTheme(id: ThemeId) {
themeId = id
progress.themeId = id
}
fun applyMode(m: ColorMode) {
mode = m
progress.mode = m
}
fun applyLang(l: UiLang) {
lang = l
progress.lang = l
}
fun refresh() { tick++ }
fun markGerman(unit: Int, ex: Int) {
progress.markGerman(unit, ex)
refresh()
}
fun markVocab(unit: Int) {
progress.markVocab(unit)
refresh()
}
fun markNet(id: Int) {
progress.markNet(id)
refresh()
}
fun markHw(id: Int) {
progress.markHw(id)
refresh()
}
fun markOn(id: Int) {
progress.markOn(id)
refresh()
}
fun markOn2(id: Int) {
progress.markOn2(id)
refresh()
}
fun markOn3(id: Int) {
progress.markOn3(id)
refresh()
}
fun markOn4(id: Int) {
progress.markOn4(id)
refresh()
}
fun markEn(year: Int, id: Int) {
progress.markEn(year, id)
refresh()
}
fun markDe(year: Int, id: Int) {
progress.markDe(year, id)
refresh()
}
fun markCourse(course: String, id: Int) {
progress.markCourse(course, id)
refresh()
}
fun markLit(id: Int) {
progress.markLit(id)
refresh()
}
fun markLit2(id: Int) {
progress.markLit2(id)
refresh()
}
fun markLit3(id: Int) {
progress.markLit3(id)
refresh()
}
fun markLit4(id: Int) {
progress.markLit4(id)
refresh()
}
fun markChem(id: Int) {
progress.markChem(id)
refresh()
}
fun markBio(id: Int) {
progress.markBio(id)
refresh()
}
fun markFyz(id: Int) {
progress.markFyz(id)
refresh()
}
fun markFyz2(id: Int) {
progress.markFyz2(id)
refresh()
}
fun markFyz3(id: Int) {
progress.markFyz3(id)
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) {
progress.markFyz4(id)
refresh()
}
fun markMluv(n: Int) {
progress.markMluv(n)
refresh()
}
fun markBookQuiz(id: String) {
progress.markBookQuiz(id)
refresh()
}
fun markBookPlot(id: String) {
progress.markBookPlot(id)
refresh()
}
fun checkUpdate(interactive: Boolean) {
updater.check(interactive) { update = it }
}
fun installUpdate() {
updater.install { update = it }
}
fun dismissChangelog() {
progress.seenCommit = BuildConfig.COMMIT
showChangelog = false
}
private fun shouldShowChangelog(): Boolean {
if (content.changelog.isEmpty()) return false
return !sameBuild(BuildConfig.COMMIT, progress.seenCommit)
}
}
internal fun sameBuild(a: String?, b: String?): Boolean {
if (a.isNullOrEmpty() || b.isNullOrEmpty()) return false
if (a.equals(b, ignoreCase = true)) return true
val n = minOf(a.length, b.length)
return n >= 7 && a.regionMatches(0, b, 0, n, ignoreCase = true)
}

View file

@ -1,496 +0,0 @@
package org.maturita.maturita
sealed class Route(val page: String) {
data object Welcome : Route("welcome")
data object Subjects : Route("subjects")
data object Stats : Route("stats")
data object Settings : Route("settings")
data object Roadmap : Route("roadmap")
data object DeHome : Route("dehome")
data class DeMap(val year: Int) : Route("de${year}map")
data class DeLesson(val year: Int, val id: Int) : Route("de${year}unit$id")
data class DeEx(val year: Int, val id: Int) : Route("de${year}ex$id")
data class UnitMap(val unitId: Int) : Route("unit${unitId + 1}")
data class GermanEx(val unitId: Int, val ex: Int) : Route("u${unitId + 1}e$ex")
data class Vocab(val unitId: Int) : Route("u${unitId + 1}vocab")
data object NetYears : Route("netyears")
data object NetMap : Route("netmap")
data class NetLesson(val id: Int) : Route("netunit$id")
data class NetEx(val id: Int) : Route("netex$id")
data object Net2Map : Route("net2map")
data class Net2Lesson(val id: Int) : Route("net2unit$id")
data class Net2Ex(val id: Int) : Route("net2ex$id")
data object Net3Map : Route("net3map")
data class Net3Lesson(val id: Int) : Route("net3unit$id")
data class Net3Ex(val id: Int) : Route("net3ex$id")
data object Net4Map : Route("net4map")
data class Net4Lesson(val id: Int) : Route("net4unit$id")
data class Net4Ex(val id: Int) : Route("net4ex$id")
data object HwYears : Route("hwyears")
data object HwMap : Route("hwmap")
data class HwLesson(val id: Int) : Route("hwunit$id")
data class HwEx(val id: Int) : Route("hwex$id")
data object Hw2Map : Route("hw2map")
data class Hw2Lesson(val id: Int) : Route("hw2unit$id")
data class Hw2Ex(val id: Int) : Route("hw2ex$id")
data object Hw3Map : Route("hw3map")
data class Hw3Lesson(val id: Int) : Route("hw3unit$id")
data class Hw3Ex(val id: Int) : Route("hw3ex$id")
data object Hw4Map : Route("hw4map")
data class Hw4Lesson(val id: Int) : Route("hw4unit$id")
data class Hw4Ex(val id: Int) : Route("hw4ex$id")
data object OnYears : Route("onyears")
data object OnMap : Route("onmap")
data class OnLesson(val id: Int) : Route("onunit$id")
data class OnEx(val id: Int) : Route("onex$id")
data object On2Map : Route("on2map")
data class On2Lesson(val id: Int) : Route("on2unit$id")
data class On2Ex(val id: Int) : Route("on2ex$id")
data object On3Map : Route("on3map")
data class On3Lesson(val id: Int) : Route("on3unit$id")
data class On3Ex(val id: Int) : Route("on3ex$id")
data object On4Map : Route("on4map")
data class On4Lesson(val id: Int) : Route("on4unit$id")
data class On4Ex(val id: Int) : Route("on4ex$id")
data object EnYears : Route("enyears")
data class EnMap(val year: Int) : Route("en${year}map")
data class EnLesson(val year: Int, val id: Int) : Route("en${year}unit$id")
data class EnEx(val year: Int, val id: Int) : Route("en${year}ex$id")
data object LitYears : Route("lityears")
data object LitMap : Route("litmap")
data class LitLesson(val id: Int) : Route("litunit$id")
data class LitEx(val id: Int) : Route("litex$id")
data object Lit2Map : Route("lit2map")
data class Lit2Lesson(val id: Int) : Route("lit2unit$id")
data class Lit2Ex(val id: Int) : Route("lit2ex$id")
data object Lit3Map : Route("lit3map")
data class Lit3Lesson(val id: Int) : Route("lit3unit$id")
data class Lit3Ex(val id: Int) : Route("lit3ex$id")
data object Lit4Map : Route("lit4map")
data class Lit4Lesson(val id: Int) : Route("lit4unit$id")
data class Lit4Ex(val id: Int) : Route("lit4ex$id")
data object SciMap : Route("scimap")
data object ChemMap : Route("chemmap")
data class ChemLesson(val id: Int) : Route("chemunit$id")
data class ChemEx(val id: Int) : Route("chemex$id")
data object BioMap : Route("biomap")
data class BioLesson(val id: Int) : Route("biounit$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 FyzMap : Route("fyzmap")
data class FyzLesson(val id: Int) : Route("fyzunit$id")
data class FyzEx(val id: Int) : Route("fyzex$id")
data object Fyz2Map : Route("fyz2map")
data class Fyz2Lesson(val id: Int) : Route("fyz2unit$id")
data class Fyz2Ex(val id: Int) : Route("fyz2ex$id")
data object Fyz3Map : Route("fyz3map")
data class Fyz3Lesson(val id: Int) : Route("fyz3unit$id")
data class Fyz3Ex(val id: Int) : Route("fyz3ex$id")
data object Fyz4Map : Route("fyz4map")
data class Fyz4Lesson(val id: Int) : Route("fyz4unit$id")
data class Fyz4Ex(val id: Int) : Route("fyz4ex$id")
data object CzechMap : Route("czechmap")
data object Mluvnice : Route("mluvnice")
data class MluvEx(val n: Int) : Route("mluve$n")
data object ReadingList : Route("readinglist")
data class Book(val id: String) : Route(if (id == "1984") "cetba1984" else "cetbaFuks")
data class BookQuiz(val id: String) : Route(if (id == "1984") "cetba1984quiz" else "cetbaFuksQuiz")
data class BookPlot(val id: String) : Route(if (id == "1984") "cetba1984dej" else "cetbaFuksDej")
/** Bottom-navigation roots; everything else is pushed on top of [Subjects]. */
val isRoot: Boolean get() = this is Welcome || this is Subjects || this is Stats || this is Settings
/** Screens with their own bottom action bar; the navigation bar stays hidden there. */
val isExercise: Boolean
get() = this is GermanEx || this is Vocab || this is NetEx || this is HwEx ||
this is OnEx || this is On2Ex || this is On3Ex || this is On4Ex || this is EnLesson || this is EnEx || this is DeLesson || this is DeEx || this is MluvEx || this is BookQuiz || this is BookPlot ||
this is Net2Lesson || this is Net2Ex || this is Net3Lesson || this is Net3Ex || this is Net4Lesson || this is Net4Ex ||
this is Hw2Lesson || this is Hw2Ex || this is Hw3Lesson || this is Hw3Ex || this is Hw4Lesson || this is Hw4Ex ||
this is NetLesson || this is HwLesson || this is OnLesson || this is On2Lesson || this is On3Lesson || this is On4Lesson ||
this is LitLesson || this is LitEx || this is Lit2Lesson || this is Lit2Ex ||
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 FyzLesson || this is FyzEx || this is Fyz2Lesson || this is Fyz2Ex ||
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. */
fun stack(): List<Route> = when (this) {
Welcome, Subjects -> listOf(Subjects)
Stats -> listOf(Stats)
Settings -> listOf(Settings)
DeHome -> listOf(Subjects, DeHome)
is DeMap -> listOf(Subjects, DeHome, this)
is DeLesson -> listOf(Subjects, DeHome, DeMap(year), this)
is DeEx -> listOf(Subjects, DeHome, DeMap(year), DeLesson(year, id), this)
Roadmap -> listOf(Subjects, DeHome, Roadmap)
is UnitMap -> listOf(Subjects, DeHome, Roadmap, this)
is GermanEx -> listOf(Subjects, DeHome, Roadmap, UnitMap(unitId), this)
is Vocab -> listOf(Subjects, DeHome, Roadmap, UnitMap(unitId), this)
NetYears -> listOf(Subjects, NetYears)
NetMap -> listOf(Subjects, NetYears, NetMap)
is NetLesson -> listOf(Subjects, NetYears, NetMap, this)
is NetEx -> listOf(Subjects, NetYears, NetMap, NetLesson(id), this)
Net2Map -> listOf(Subjects, NetYears, Net2Map)
is Net2Lesson -> listOf(Subjects, NetYears, Net2Map, this)
is Net2Ex -> listOf(Subjects, NetYears, Net2Map, Net2Lesson(id), this)
Net3Map -> listOf(Subjects, NetYears, Net3Map)
is Net3Lesson -> listOf(Subjects, NetYears, Net3Map, this)
is Net3Ex -> listOf(Subjects, NetYears, Net3Map, Net3Lesson(id), this)
Net4Map -> listOf(Subjects, NetYears, Net4Map)
is Net4Lesson -> listOf(Subjects, NetYears, Net4Map, this)
is Net4Ex -> listOf(Subjects, NetYears, Net4Map, Net4Lesson(id), this)
HwYears -> listOf(Subjects, HwYears)
HwMap -> listOf(Subjects, HwYears, HwMap)
is HwLesson -> listOf(Subjects, HwYears, HwMap, this)
is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this)
Hw2Map -> listOf(Subjects, HwYears, Hw2Map)
is Hw2Lesson -> listOf(Subjects, HwYears, Hw2Map, this)
is Hw2Ex -> listOf(Subjects, HwYears, Hw2Map, Hw2Lesson(id), this)
Hw3Map -> listOf(Subjects, HwYears, Hw3Map)
is Hw3Lesson -> listOf(Subjects, HwYears, Hw3Map, this)
is Hw3Ex -> listOf(Subjects, HwYears, Hw3Map, Hw3Lesson(id), this)
Hw4Map -> listOf(Subjects, HwYears, Hw4Map)
is Hw4Lesson -> listOf(Subjects, HwYears, Hw4Map, this)
is Hw4Ex -> listOf(Subjects, HwYears, Hw4Map, Hw4Lesson(id), this)
OnYears -> listOf(Subjects, OnYears)
OnMap -> listOf(Subjects, OnYears, OnMap)
is OnLesson -> listOf(Subjects, OnYears, OnMap, this)
is OnEx -> listOf(Subjects, OnYears, OnMap, OnLesson(id), this)
On2Map -> listOf(Subjects, OnYears, On2Map)
is On2Lesson -> listOf(Subjects, OnYears, On2Map, this)
is On2Ex -> listOf(Subjects, OnYears, On2Map, On2Lesson(id), this)
On3Map -> listOf(Subjects, OnYears, On3Map)
is On3Lesson -> listOf(Subjects, OnYears, On3Map, this)
is On3Ex -> listOf(Subjects, OnYears, On3Map, On3Lesson(id), this)
On4Map -> listOf(Subjects, OnYears, On4Map)
is On4Lesson -> listOf(Subjects, OnYears, On4Map, this)
is On4Ex -> listOf(Subjects, OnYears, On4Map, On4Lesson(id), this)
EnYears -> listOf(Subjects, EnYears)
is EnMap -> listOf(Subjects, EnYears, this)
is EnLesson -> listOf(Subjects, EnYears, EnMap(year), this)
is EnEx -> listOf(Subjects, EnYears, EnMap(year), EnLesson(year, id), this)
LitYears -> listOf(Subjects, CzechMap, LitYears)
LitMap -> listOf(Subjects, CzechMap, LitYears, LitMap)
is LitLesson -> listOf(Subjects, CzechMap, LitYears, LitMap, this)
is LitEx -> listOf(Subjects, CzechMap, LitYears, LitMap, LitLesson(id), this)
Lit2Map -> listOf(Subjects, CzechMap, LitYears, Lit2Map)
is Lit2Lesson -> listOf(Subjects, CzechMap, LitYears, Lit2Map, this)
is Lit2Ex -> listOf(Subjects, CzechMap, LitYears, Lit2Map, Lit2Lesson(id), this)
Lit3Map -> listOf(Subjects, CzechMap, LitYears, Lit3Map)
is Lit3Lesson -> listOf(Subjects, CzechMap, LitYears, Lit3Map, this)
is Lit3Ex -> listOf(Subjects, CzechMap, LitYears, Lit3Map, Lit3Lesson(id), this)
Lit4Map -> listOf(Subjects, CzechMap, LitYears, Lit4Map)
is Lit4Lesson -> listOf(Subjects, CzechMap, LitYears, Lit4Map, this)
is Lit4Ex -> listOf(Subjects, CzechMap, LitYears, Lit4Map, Lit4Lesson(id), this)
SciMap -> listOf(Subjects, SciMap)
ChemMap -> listOf(Subjects, SciMap, ChemMap)
is ChemLesson -> listOf(Subjects, SciMap, ChemMap, this)
is ChemEx -> listOf(Subjects, SciMap, ChemMap, ChemLesson(id), this)
BioMap -> listOf(Subjects, SciMap, BioMap)
is BioLesson -> listOf(Subjects, SciMap, BioMap, 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)
FyzMap -> listOf(Subjects, FyzYears, FyzMap)
is FyzLesson -> listOf(Subjects, FyzYears, FyzMap, this)
is FyzEx -> listOf(Subjects, FyzYears, FyzMap, FyzLesson(id), this)
Fyz2Map -> listOf(Subjects, FyzYears, Fyz2Map)
is Fyz2Lesson -> listOf(Subjects, FyzYears, Fyz2Map, this)
is Fyz2Ex -> listOf(Subjects, FyzYears, Fyz2Map, Fyz2Lesson(id), this)
Fyz3Map -> listOf(Subjects, FyzYears, Fyz3Map)
is Fyz3Lesson -> listOf(Subjects, FyzYears, Fyz3Map, this)
is Fyz3Ex -> listOf(Subjects, FyzYears, Fyz3Map, Fyz3Lesson(id), this)
Fyz4Map -> listOf(Subjects, FyzYears, Fyz4Map)
is Fyz4Lesson -> listOf(Subjects, FyzYears, Fyz4Map, this)
is Fyz4Ex -> listOf(Subjects, FyzYears, Fyz4Map, Fyz4Lesson(id), this)
CzechMap -> listOf(Subjects, CzechMap)
Mluvnice -> listOf(Subjects, CzechMap, Mluvnice)
is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this)
ReadingList -> listOf(Subjects, CzechMap, ReadingList)
is Book -> listOf(Subjects, CzechMap, ReadingList, this)
is BookQuiz -> listOf(Subjects, CzechMap, ReadingList, Book(id), this)
is BookPlot -> listOf(Subjects, CzechMap, ReadingList, Book(id), this)
}
}
fun routeFromPage(page: String): Route? = when (page) {
"welcome" -> Route.Welcome
"subjects" -> Route.Subjects
"stats" -> Route.Stats
"settings" -> Route.Settings
"roadmap" -> Route.Roadmap
"dehome" -> Route.DeHome
"netyears" -> Route.NetYears
"netmap" -> Route.NetMap
"net2map" -> Route.Net2Map
"net3map" -> Route.Net3Map
"net4map" -> Route.Net4Map
"hw2map" -> Route.Hw2Map
"hw3map" -> Route.Hw3Map
"hw4map" -> Route.Hw4Map
"hwyears" -> Route.HwYears
"hwmap" -> Route.HwMap
"onyears" -> Route.OnYears
"onmap" -> Route.OnMap
"on2map" -> Route.On2Map
"on3map" -> Route.On3Map
"on4map" -> Route.On4Map
"enyears" -> Route.EnYears
"lityears" -> Route.LitYears
"litmap" -> Route.LitMap
"lit2map" -> Route.Lit2Map
"lit3map" -> Route.Lit3Map
"lit4map" -> Route.Lit4Map
"scimap" -> Route.SciMap
"chemmap" -> Route.ChemMap
"biomap" -> Route.BioMap
"matyears" -> Route.MatYears
"mat0map" -> Route.Mat0Map
"matmap" -> Route.MatMap
"mat2map" -> Route.Mat2Map
"mat3map" -> Route.Mat3Map
"mat4map" -> Route.Mat4Map
"fyzyears" -> Route.FyzYears
"fyzmap" -> Route.FyzMap
"fyz2map" -> Route.Fyz2Map
"fyz3map" -> Route.Fyz3Map
"fyz4map" -> Route.Fyz4Map
"czechmap" -> Route.CzechMap
"mluvnice" -> Route.Mluvnice
"readinglist" -> Route.ReadingList
"cetba1984" -> Route.Book("1984")
"cetba1984quiz" -> Route.BookQuiz("1984")
"cetba1984dej" -> Route.BookPlot("1984")
"cetbaFuks" -> Route.Book("fuks")
"cetbaFuksQuiz" -> Route.BookQuiz("fuks")
"cetbaFuksDej" -> Route.BookPlot("fuks")
else -> {
Regex("""unit(\d+)""").matchEntire(page)?.let {
return Route.UnitMap(it.groupValues[1].toInt() - 1)
}
Regex("""u(\d+)e(\d+)""").matchEntire(page)?.let {
return Route.GermanEx(it.groupValues[1].toInt() - 1, it.groupValues[2].toInt())
}
Regex("""u(\d+)vocab""").matchEntire(page)?.let {
return Route.Vocab(it.groupValues[1].toInt() - 1)
}
Regex("""netunit(\d+)""").matchEntire(page)?.let {
return Route.NetLesson(it.groupValues[1].toInt())
}
Regex("""netex(\d+)""").matchEntire(page)?.let {
return Route.NetEx(it.groupValues[1].toInt())
}
Regex("""hwunit(\d+)""").matchEntire(page)?.let {
return Route.HwLesson(it.groupValues[1].toInt())
}
Regex("""hwex(\d+)""").matchEntire(page)?.let {
return Route.HwEx(it.groupValues[1].toInt())
}
Regex("""chemunit(\d+)""").matchEntire(page)?.let {
return Route.ChemLesson(it.groupValues[1].toInt())
}
Regex("""chemex(\d+)""").matchEntire(page)?.let {
return Route.ChemEx(it.groupValues[1].toInt())
}
Regex("""biounit(\d+)""").matchEntire(page)?.let {
return Route.BioLesson(it.groupValues[1].toInt())
}
Regex("""bioex(\d+)""").matchEntire(page)?.let {
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 {
return Route.Fyz4Lesson(it.groupValues[1].toInt())
}
Regex("""fyz4ex(\d+)""").matchEntire(page)?.let {
return Route.Fyz4Ex(it.groupValues[1].toInt())
}
Regex("""fyz3unit(\d+)""").matchEntire(page)?.let {
return Route.Fyz3Lesson(it.groupValues[1].toInt())
}
Regex("""fyz3ex(\d+)""").matchEntire(page)?.let {
return Route.Fyz3Ex(it.groupValues[1].toInt())
}
Regex("""fyz2unit(\d+)""").matchEntire(page)?.let {
return Route.Fyz2Lesson(it.groupValues[1].toInt())
}
Regex("""fyz2ex(\d+)""").matchEntire(page)?.let {
return Route.Fyz2Ex(it.groupValues[1].toInt())
}
Regex("""fyzunit(\d+)""").matchEntire(page)?.let {
return Route.FyzLesson(it.groupValues[1].toInt())
}
Regex("""fyzex(\d+)""").matchEntire(page)?.let {
return Route.FyzEx(it.groupValues[1].toInt())
}
Regex("""lit4unit(\d+)""").matchEntire(page)?.let {
return Route.Lit4Lesson(it.groupValues[1].toInt())
}
Regex("""lit4ex(\d+)""").matchEntire(page)?.let {
return Route.Lit4Ex(it.groupValues[1].toInt())
}
Regex("""lit3unit(\d+)""").matchEntire(page)?.let {
return Route.Lit3Lesson(it.groupValues[1].toInt())
}
Regex("""lit3ex(\d+)""").matchEntire(page)?.let {
return Route.Lit3Ex(it.groupValues[1].toInt())
}
Regex("""lit2unit(\d+)""").matchEntire(page)?.let {
return Route.Lit2Lesson(it.groupValues[1].toInt())
}
Regex("""lit2ex(\d+)""").matchEntire(page)?.let {
return Route.Lit2Ex(it.groupValues[1].toInt())
}
Regex("""litunit(\d+)""").matchEntire(page)?.let {
return Route.LitLesson(it.groupValues[1].toInt())
}
Regex("""litex(\d+)""").matchEntire(page)?.let {
return Route.LitEx(it.groupValues[1].toInt())
}
Regex("""de(\d)map""").matchEntire(page)?.let {
return Route.DeMap(it.groupValues[1].toInt())
}
Regex("""de(\d)unit(\d+)""").matchEntire(page)?.let {
return Route.DeLesson(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""de(\d)ex(\d+)""").matchEntire(page)?.let {
return Route.DeEx(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""net([234])unit(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Net3Lesson(id)
4 -> Route.Net4Lesson(id)
else -> Route.Net2Lesson(id)
}
}
Regex("""net([234])ex(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Net3Ex(id)
4 -> Route.Net4Ex(id)
else -> Route.Net2Ex(id)
}
}
Regex("""hw([234])unit(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Hw3Lesson(id)
4 -> Route.Hw4Lesson(id)
else -> Route.Hw2Lesson(id)
}
}
Regex("""hw([234])ex(\d+)""").matchEntire(page)?.let {
val year = it.groupValues[1].toInt()
val id = it.groupValues[2].toInt()
return when (year) {
3 -> Route.Hw3Ex(id)
4 -> Route.Hw4Ex(id)
else -> Route.Hw2Ex(id)
}
}
Regex("""en(\d)map""").matchEntire(page)?.let {
return Route.EnMap(it.groupValues[1].toInt())
}
Regex("""en(\d)unit(\d+)""").matchEntire(page)?.let {
return Route.EnLesson(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""en(\d)ex(\d+)""").matchEntire(page)?.let {
return Route.EnEx(it.groupValues[1].toInt(), it.groupValues[2].toInt())
}
Regex("""on4unit(\d+)""").matchEntire(page)?.let {
return Route.On4Lesson(it.groupValues[1].toInt())
}
Regex("""on4ex(\d+)""").matchEntire(page)?.let {
return Route.On4Ex(it.groupValues[1].toInt())
}
Regex("""on3unit(\d+)""").matchEntire(page)?.let {
return Route.On3Lesson(it.groupValues[1].toInt())
}
Regex("""on3ex(\d+)""").matchEntire(page)?.let {
return Route.On3Ex(it.groupValues[1].toInt())
}
Regex("""on2unit(\d+)""").matchEntire(page)?.let {
return Route.On2Lesson(it.groupValues[1].toInt())
}
Regex("""on2ex(\d+)""").matchEntire(page)?.let {
return Route.On2Ex(it.groupValues[1].toInt())
}
Regex("""onunit(\d+)""").matchEntire(page)?.let {
return Route.OnLesson(it.groupValues[1].toInt())
}
Regex("""onex(\d+)""").matchEntire(page)?.let {
return Route.OnEx(it.groupValues[1].toInt())
}
Regex("""mluve(\d+)""").matchEntire(page)?.let {
return Route.MluvEx(it.groupValues[1].toInt())
}
null
}
}

View file

@ -1,718 +0,0 @@
package org.maturita.maturita.data
import java.text.Normalizer
fun normalizeAnswer(input: String?): String {
val src = (input ?: "").lowercase()
val pre = StringBuilder()
src.codePoints().forEach { ch ->
when (ch) {
0x00e4 -> pre.append("ae")
0x00f6 -> pre.append("oe")
0x00fc -> pre.append("ue")
0x00df -> pre.append("ss")
else -> pre.appendCodePoint(ch)
}
}
val decomp = Normalizer.normalize(pre.toString(), Normalizer.Form.NFD)
val out = StringBuilder()
var prevSpace = false
decomp.codePoints().forEach { ch ->
val type = Character.getType(ch)
if (type == Character.NON_SPACING_MARK.toInt() ||
type == Character.COMBINING_SPACING_MARK.toInt() ||
type == Character.ENCLOSING_MARK.toInt()
) {
return@forEach
}
if (Character.isWhitespace(ch)) {
if (out.isNotEmpty() && !prevSpace) out.append(' ')
prevSpace = true
return@forEach
}
if (!Character.isLetterOrDigit(ch)) return@forEach
prevSpace = false
out.appendCodePoint(ch)
}
return out.toString().trimEnd()
}
fun answerAccepts(selNorm: String, ans: String?): Boolean {
if (ans.isNullOrEmpty()) return false
return ans.split('|').any { normalizeAnswer(it) == selNorm }
}
fun answerIncomplete(selNorm: String, ans: String?): Boolean {
if (selNorm.isEmpty() || ans.isNullOrEmpty()) return false
for (part in ans.split('|')) {
val a = normalizeAnswer(part)
if (a.startsWith("$selNorm ") && a.length > selNorm.length + 1) return true
}
return false
}
fun netTxtEq(a: String?, b: String?): Boolean {
if (a == null || b == null) return a == b
val x = a.filterNot { it.isWhitespace() }.lowercase()
val y = b.filterNot { it.isWhitespace() }.lowercase()
return x == y
}
fun hasUmlaut(s: String?): Boolean {
if (s == null) return false
return s.any { 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

@ -1,147 +0,0 @@
package org.maturita.maturita.data
import android.content.Context
import org.json.JSONObject
class Content(root: JSONObject) {
val raw = J(root)
val i18n: Map<String, Pair<String?, String?>> = run {
val obj = root.optJSONObject("i18n") ?: JSONObject()
obj.keys().asSequence().associateWith { key ->
val e = obj.optJSONObject(key)
val cs = e?.optString("cs")?.takeIf { it.isNotEmpty() && it != "null" }
val en = e?.optString("en")?.takeIf { it.isNotEmpty() && it != "null" }
cs to en
}
}
val subjects = raw.arr("subjects")
val german = raw.arr("german")
val net = raw.obj("net") ?: J(JSONObject())
val netLessons = net.arr("lessons")
val netTasks = net.arr("tasks")
val netAnswers: List<List<J>> = run {
val a = net.o.optJSONArray("answers") ?: return@run emptyList()
(0 until a.length()).map { i ->
val block = a.optJSONArray(i) ?: org.json.JSONArray()
(0 until block.length()).mapNotNull { j ->
block.optJSONObject(j)?.let { J(it) }
}
}
}
val hw = raw.arr("hw")
val net2 = raw.arr("net2")
val net3 = raw.arr("net3")
val net4 = raw.arr("net4")
val hw2 = raw.arr("hw2")
val hw3 = raw.arr("hw3")
val hw4 = raw.arr("hw4")
val on = raw.arr("on")
val on2 = raw.arr("on2")
val on3 = raw.arr("on3")
val on4 = raw.arr("on4")
val en = raw.arr("en")
val en2 = raw.arr("en2")
val en3 = raw.arr("en3")
val en4 = raw.arr("en4")
val de = raw.arr("de")
val de2 = raw.arr("de2")
val de3 = raw.arr("de3")
val de4 = raw.arr("de4")
val lit = raw.arr("lit")
val lit2 = raw.arr("lit2")
val lit3 = raw.arr("lit3")
val lit4 = raw.arr("lit4")
val chem = raw.arr("chem")
val bio = raw.arr("bio")
val fyz = raw.arr("fyz")
val fyz2 = raw.arr("fyz2")
val fyz3 = raw.arr("fyz3")
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 books = raw.arr("books")
val changelog = raw.arr("changelog")
fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id }
fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id }
fun hwLesson(id: Int) = hw.firstOrNull { it.int("id") == id }
fun itLessons(course: String) = when (course) {
"net2" -> net2
"net3" -> net3
"net4" -> net4
"hw2" -> hw2
"hw3" -> hw3
"hw4" -> hw4
else -> emptyList()
}
fun itLesson(course: String, id: Int) = itLessons(course).firstOrNull { it.int("id") == id }
fun onLesson(id: Int) = on.firstOrNull { it.int("id") == id }
fun on2Lesson(id: Int) = on2.firstOrNull { it.int("id") == id }
fun on3Lesson(id: Int) = on3.firstOrNull { it.int("id") == id }
fun on4Lesson(id: Int) = on4.firstOrNull { it.int("id") == id }
fun enYear(year: Int) = when (year) {
2 -> en2
3 -> en3
4 -> en4
else -> en
}
fun enLesson(year: Int, id: Int) = enYear(year).firstOrNull { it.int("id") == id }
fun deYear(year: Int) = when (year) {
2 -> de2
3 -> de3
4 -> de4
else -> de
}
fun deLesson(year: Int, id: Int) = deYear(year).firstOrNull { it.int("id") == id }
fun litLesson(id: Int) = lit.firstOrNull { it.int("id") == id }
fun lit2Lesson(id: Int) = lit2.firstOrNull { it.int("id") == id }
fun lit3Lesson(id: Int) = lit3.firstOrNull { it.int("id") == id }
fun lit4Lesson(id: Int) = lit4.firstOrNull { it.int("id") == id }
fun chemLesson(id: Int) = chem.firstOrNull { it.int("id") == id }
fun bioLesson(id: Int) = bio.firstOrNull { it.int("id") == id }
fun fyzLesson(id: Int) = fyz.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 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 book(id: String) = books.firstOrNull { it.str("id") == id }
fun tr(key: String?, lang: UiLang): String {
if (key.isNullOrEmpty()) return ""
val hit = i18n[key]
return if (lang == UiLang.En) {
hit?.second ?: key
} else {
hit?.first ?: key
}
}
fun fmt(key: String, lang: UiLang, vararg args: Any): String {
val template = tr(key, lang)
return try {
String.format(template, *args)
} catch (_: Exception) {
template
}
}
companion object {
fun load(context: Context): Content {
return try {
val text = context.assets.open("content.json").bufferedReader().use { it.readText() }
Content(JSONObject(text))
} catch (_: Exception) {
Content(JSONObject())
}
}
}
}

View file

@ -1,52 +0,0 @@
package org.maturita.maturita.data
import org.json.JSONArray
import org.json.JSONObject
class J(val o: JSONObject) {
fun has(key: String) = o.has(key) && !o.isNull(key)
fun str(key: String, fallback: String = "") =
if (!has(key)) fallback else o.optString(key, fallback)
fun strOrNull(key: String): String? =
if (!has(key)) null else o.optString(key).takeIf { it.isNotEmpty() && it != "null" }
fun bool(key: String, fallback: Boolean = false) = o.optBoolean(key, fallback)
fun int(key: String, fallback: Int = 0) = o.optInt(key, fallback)
fun obj(key: String): J? = o.optJSONObject(key)?.let { J(it) }
fun exercise(n: Int): J? = o.optJSONObject("exercises")?.optJSONObject(n.toString())?.let { J(it) }
fun arr(key: String): List<J> {
val a = o.optJSONArray(key) ?: return emptyList()
return (0 until a.length()).mapNotNull { i -> a.optJSONObject(i)?.let { J(it) } }
}
fun strs(key: String): List<String> {
val a = o.optJSONArray(key) ?: return emptyList()
return (0 until a.length()).mapNotNull { i ->
if (a.isNull(i)) null else a.optString(i).takeIf { it.isNotEmpty() }
}
}
fun strsOrEmpty(key: String): List<String?> {
val a = o.optJSONArray(key) ?: return emptyList()
return (0 until a.length()).map { i ->
if (a.isNull(i)) null else a.optString(i)
}
}
fun intRows(key: String): List<List<String>> {
val a = o.optJSONArray(key) ?: return emptyList()
return (0 until a.length()).map { i ->
val row = a.optJSONArray(i) ?: JSONArray()
(0 until row.length()).map { j -> row.optString(j) }
}
}
fun answerList(): List<String> {
val a = o.optJSONArray("answers")
if (a != null) {
return (0 until a.length()).mapNotNull { i ->
if (a.isNull(i)) null else a.optString(i).takeIf { it.isNotEmpty() }
}
}
return listOfNotNull(strOrNull("answers"))
}
}
fun JSONArray.objects(): List<J> =
(0 until length()).mapNotNull { i -> optJSONObject(i)?.let { J(it) } }

View file

@ -1,310 +0,0 @@
package org.maturita.maturita.data
import android.content.Context
class ProgressStore(context: Context) {
private val prefs = context.getSharedPreferences("maturita", Context.MODE_PRIVATE)
var themeId: ThemeId
get() = ThemeId.entries.getOrElse(prefs.getInt("theme", 0)) { ThemeId.Catppuccin }
set(v) { prefs.edit().putInt("theme", v.ordinal).apply() }
var mode: ColorMode
get() = if (prefs.getString("mode", "light") == "dark") ColorMode.Dark else ColorMode.Light
set(v) { prefs.edit().putString("mode", if (v == ColorMode.Light) "light" else "dark").apply() }
var lang: UiLang
get() = if (prefs.getString("lang", "cs") == "en") UiLang.En else UiLang.Cs
set(v) { prefs.edit().putString("lang", if (v == UiLang.En) "en" else "cs").apply() }
var seenCommit: String
get() = prefs.getString("seen_commit", "") ?: ""
set(v) { prefs.edit().putString("seen_commit", v).apply() }
fun germanDone(unit: Int, ex: Int) = prefs.getBoolean("g.$unit.$ex", false)
fun markGerman(unit: Int, ex: Int) { prefs.edit().putBoolean("g.$unit.$ex", true).apply() }
fun vocabDone(unit: Int) = prefs.getBoolean("g.$unit.vocab", false)
fun markVocab(unit: Int) { prefs.edit().putBoolean("g.$unit.vocab", true).apply() }
fun netDone(id: Int) = prefs.getBoolean("net.$id", false)
fun markNet(id: Int) { prefs.edit().putBoolean("net.$id", true).apply() }
fun hwDone(id: Int) = prefs.getBoolean("hw.$id", false)
fun markHw(id: Int) { prefs.edit().putBoolean("hw.$id", true).apply() }
fun onDone(id: Int) = prefs.getBoolean("on.$id", false)
fun markOn(id: Int) { prefs.edit().putBoolean("on.$id", true).apply() }
fun on2Done(id: Int) = prefs.getBoolean("on2.$id", false)
fun markOn2(id: Int) { prefs.edit().putBoolean("on2.$id", true).apply() }
fun on3Done(id: Int) = prefs.getBoolean("on3.$id", false)
fun markOn3(id: Int) { prefs.edit().putBoolean("on3.$id", true).apply() }
fun on4Done(id: Int) = prefs.getBoolean("on4.$id", false)
fun markOn4(id: Int) { prefs.edit().putBoolean("on4.$id", true).apply() }
fun enDone(year: Int, id: Int) = prefs.getBoolean("en.$year.$id", false)
fun markEn(year: Int, id: Int) { prefs.edit().putBoolean("en.$year.$id", true).apply() }
fun deDone(year: Int, id: Int) = prefs.getBoolean("de.$year.$id", false)
fun markDe(year: Int, id: Int) { prefs.edit().putBoolean("de.$year.$id", true).apply() }
fun courseDone(course: String, id: Int) = prefs.getBoolean("$course.$id", false)
fun markCourse(course: String, id: Int) { prefs.edit().putBoolean("$course.$id", true).apply() }
fun litDone(id: Int) = prefs.getBoolean("lit.$id", false)
fun markLit(id: Int) { prefs.edit().putBoolean("lit.$id", true).apply() }
fun lit2Done(id: Int) = prefs.getBoolean("lit2.$id", false)
fun markLit2(id: Int) { prefs.edit().putBoolean("lit2.$id", true).apply() }
fun lit3Done(id: Int) = prefs.getBoolean("lit3.$id", false)
fun markLit3(id: Int) { prefs.edit().putBoolean("lit3.$id", true).apply() }
fun lit4Done(id: Int) = prefs.getBoolean("lit4.$id", false)
fun markLit4(id: Int) { prefs.edit().putBoolean("lit4.$id", true).apply() }
fun chemDone(id: Int) = prefs.getBoolean("chem.$id", false)
fun markChem(id: Int) { prefs.edit().putBoolean("chem.$id", true).apply() }
fun bioDone(id: Int) = prefs.getBoolean("bio.$id", false)
fun markBio(id: Int) { prefs.edit().putBoolean("bio.$id", true).apply() }
fun fyzDone(id: Int) = prefs.getBoolean("fyz.$id", false)
fun markFyz(id: Int) { prefs.edit().putBoolean("fyz.$id", true).apply() }
fun fyz2Done(id: Int) = prefs.getBoolean("fyz2.$id", false)
fun markFyz2(id: Int) { prefs.edit().putBoolean("fyz2.$id", true).apply() }
fun fyz3Done(id: Int) = prefs.getBoolean("fyz3.$id", false)
fun markFyz3(id: Int) { prefs.edit().putBoolean("fyz3.$id", true).apply() }
fun fyz4Done(id: Int) = prefs.getBoolean("fyz4.$id", false)
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 markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() }
fun bookQuiz(id: String) = prefs.getBoolean("book.$id.quiz", false)
fun markBookQuiz(id: String) { prefs.edit().putBoolean("book.$id.quiz", true).apply() }
fun bookPlot(id: String) = prefs.getBoolean("book.$id.plot", false)
fun markBookPlot(id: String) { prefs.edit().putBoolean("book.$id.plot", true).apply() }
}
data class ProgressSum(var doneEx: Int = 0, var totalEx: Int = 0, var doneUnits: Int = 0, var openUnits: Int = 0)
fun summarize(content: Content, p: ProgressStore): ProgressSum {
val sum = ProgressSum()
content.german.filter { it.bool("unlocked") }.forEach { u ->
val id = u.int("id")
val names = u.strs("names")
sum.totalEx += names.size
sum.openUnits += 1
val done = names.indices.count { p.germanDone(id, it + 1) }
sum.doneEx += done
if (names.isNotEmpty() && done == names.size) sum.doneUnits += 1
}
content.netLessons.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.netDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.hw.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.hwDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
listOf("net2", "net3", "net4", "hw2", "hw3", "hw4").forEach { course ->
content.itLessons(course).forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.courseDone(course, l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
content.on.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.onDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.on2.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.on2Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.on3.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.on3Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.on4.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.on4Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for (year in 1..4) {
content.enYear(year).forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.enDone(year, l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.deYear(year).forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.deDone(year, l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
}
content.lit.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.litDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.lit2.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.lit2Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.lit3.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.lit3Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.lit4.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.lit4Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.chem.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.chemDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.bio.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.bioDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.fyz.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.fyzDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.fyz2.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.fyz2Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.fyz3.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.fyz3Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.fyz4.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.fyz4Done(l.int("id"))) {
sum.doneEx += 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.openUnits += 1
val md = content.mluvnice.count { p.mluvDone(it.int("id")) }
sum.doneEx += md
if (md == content.mluvnice.size && content.mluvnice.isNotEmpty()) sum.doneUnits += 1
return sum
}

View file

@ -1,196 +0,0 @@
package org.maturita.maturita.data
import androidx.compose.ui.graphics.Color
enum class ThemeId { Catppuccin, Nord, Dracula, RosePine, Ocean, Gruvbox, Solarized, Everforest, Monokai, OneDark }
enum class ColorMode { Dark, Light }
enum class UiLang { Cs, En }
val themeNames = listOf(
"Catppuccin", "Nord", "Dracula", "Rose Pine", "Ocean",
"Gruvbox", "Solarized", "Everforest", "Monokai", "One Dark",
)
data class Palette(
val crust: Color,
val base: Color,
val mantle: Color,
val surface0: Color,
val surface1: Color,
val surface2: Color,
val overlay: Color,
val text: Color,
val subtext: Color,
val accent: Color,
val accent2: Color,
val accent3: Color,
val success: Color,
val success2: Color,
val warning: Color,
val error: Color,
val onAccent: Color,
val node: Color,
val lockedBg: Color,
val lockedBorder: Color,
val rail: Color,
val finishDark: Color,
val finishLight: Color,
val finishStroke: Color,
/** Accent that stays readable as text and icon tint on [base]. */
val tint: Color,
/** Text colour that reads on [tint]. */
val onTint: Color,
)
private fun hx(v: Long) = Color(0xFF000000L or v)
private fun luminance(v: Long): Double {
fun channel(c: Long): Double {
val s = c / 255.0
return if (s <= 0.03928) s / 12.92 else Math.pow((s + 0.055) / 1.055, 2.4)
}
return 0.2126 * channel((v shr 16) and 0xFF) + 0.7152 * channel((v shr 8) and 0xFF) + 0.0722 * channel(v and 0xFF)
}
private fun contrast(a: Long, b: Long): Double {
val la = luminance(a)
val lb = luminance(b)
return (maxOf(la, lb) + 0.05) / (minOf(la, lb) + 0.05)
}
/** Yellow on white is unreadable as link text; fall back to the next accent. */
private fun readableTint(c: LongArray): Long {
val background = c[1]
for (candidate in longArrayOf(c[9], c[10], c[11])) {
if (contrast(candidate, background) >= 3.0) return candidate
}
return c[7]
}
private fun pal(vararg c: Long): Palette {
val tint = readableTint(c)
return Palette(
hx(c[0]), hx(c[1]), hx(c[2]), hx(c[3]), hx(c[4]), hx(c[5]), hx(c[6]),
hx(c[7]), hx(c[8]), hx(c[9]), hx(c[10]), hx(c[11]), hx(c[12]), hx(c[13]),
hx(c[14]), hx(c[15]), hx(c[16]), hx(c[17]), hx(c[18]), hx(c[19]), hx(c[20]),
hx(c[21]), hx(c[22]), hx(c[23]),
tint = hx(tint),
onTint = if (luminance(tint) > 0.4) Color(0xFF111111) else Color.White,
)
}
fun themePalette(id: ThemeId, mode: ColorMode): Palette {
val light = mode == ColorMode.Light
return when (id) {
ThemeId.Catppuccin -> if (light) pal(
0xf3eee6, 0xfaf7f2, 0xffffff, 0xffffff, 0xf0ebe3, 0xe5dfd4,
0x8a8378, 0x1a1a1a, 0x5c574f, 0xffcc00, 0x0071e3, 0x2997ff,
0x1f8a4c, 0x1a7f4a, 0xe6a800, 0xd32f2f, 0x1a1a1a,
0xffffff, 0xf0ebe3, 0xd5cfc4, 0xe5dfd4, 0xf3eee6, 0xd5cfc4, 0x8a8378,
) else pal(
0x000000, 0x000000, 0x111111, 0x1d1d1f, 0x2c2c2e, 0x3a3a3c,
0x86868b, 0xf5f5f7, 0xa1a1a6, 0xffd60a, 0x2997ff, 0x64d2ff,
0x30d158, 0x64d2ff, 0xffd60a, 0xff453a, 0x1d1d1f,
0x1d1d1f, 0x111111, 0x3a3a3c, 0x2c2c2e, 0x000000, 0x3a3a3c, 0x86868b,
)
ThemeId.Nord -> if (light) pal(
0xd8dee9, 0xeceff4, 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xb0b8c8,
0x7b88a1, 0x2e3440, 0x4c566a, 0x5e81ac, 0x81a1c1, 0x5e81ac,
0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0xeceff4,
0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xd8dee9, 0xd8dee9, 0xc7ceda, 0x7b88a1,
) else pal(
0x2e3440, 0x3b4252, 0x2e3440, 0x434c5e, 0x4c566a, 0x616e88,
0x7b88a1, 0xeceff4, 0xd8dee9, 0x88c0d0, 0x81a1c1, 0x5e81ac,
0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0x2e3440,
0x434c5e, 0x3b4252, 0x4c566a, 0x2e3440, 0x2e3440, 0x434c5e, 0x4c566a,
)
ThemeId.Dracula -> if (light) pal(
0xd5d7e0, 0xf8f8f2, 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xbfbfb2,
0x9a9a8c, 0x282a36, 0x44475a, 0x7c3aed, 0xd946a6, 0x0891b2,
0x16a34a, 0x0e7490, 0xca8a04, 0xdc2626, 0xf8f8f2,
0xefefea, 0xe2e2d8, 0xd0d0c4, 0xe2e2d8, 0xd5d7e0, 0xd0d0c4, 0x9a9a8c,
) else pal(
0x191a21, 0x282a36, 0x21222c, 0x44475a, 0x6272a4, 0x707eb0,
0x6272a4, 0xf8f8f2, 0xbfbfb2, 0xbd93f9, 0xff79c6, 0x8be9fd,
0x50fa7b, 0x8be9fd, 0xf1fa8c, 0xff5555, 0x282a36,
0x383a4a, 0x2f3240, 0x44475a, 0x21222c, 0x191a21, 0x383a4a, 0x6272a4,
)
ThemeId.RosePine -> if (light) pal(
0xf2e9e1, 0xfaf4ed, 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xcecacd,
0x9893a5, 0x575279, 0x797593, 0x907aa9, 0xd7827e, 0x56949f,
0x286983, 0x56949f, 0xea9d34, 0xb4637a, 0xfaf4ed,
0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xf2e9e1, 0xf2e9e1, 0xdfdad9, 0x9893a5,
) else pal(
0x191724, 0x1f1d2e, 0x1f1d2e, 0x26233a, 0x403d52, 0x524f67,
0x6e6a86, 0xe0def4, 0x908caa, 0xc4a7e7, 0xebbcba, 0x9ccfd8,
0x31748f, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x191724,
0x26233a, 0x21202e, 0x403d52, 0x191724, 0x191724, 0x26233a, 0x524f67,
)
ThemeId.Ocean -> if (light) pal(
0xd6dae8, 0xe1e2ec, 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0x9aa0b5,
0x7a8199, 0x2e3446, 0x4a5168, 0x2e7de9, 0x7847bd, 0x007197,
0x387068, 0x007197, 0x8c6c3e, 0xc64343, 0xe1e2ec,
0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0xc8c9d6, 0xd6dae8, 0xb4b6c5, 0x7a8199,
) else pal(
0x16161e, 0x1a1b26, 0x16161e, 0x24283b, 0x414868, 0x565f89,
0x565f89, 0xc0caf5, 0xa9b1d6, 0x7aa2f7, 0xbb9af7, 0x7dcfff,
0x9ece6a, 0x73daca, 0xe0af68, 0xf7768e, 0x1a1b26,
0x24283b, 0x1f2335, 0x3b4261, 0x16161e, 0x16161e, 0x24283b, 0x414868,
)
ThemeId.Gruvbox -> if (light) pal(
0xebdbb2, 0xfbf1c7, 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xbdae93,
0x928374, 0x3c3836, 0x504945, 0xd65d0e, 0xb16286, 0x076678,
0x79740e, 0x427b58, 0xb57614, 0x9d0006, 0xfbf1c7,
0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xebdbb2, 0xebdbb2, 0xd5c4a1, 0x928374,
) else pal(
0x1d2021, 0x282828, 0x1d2021, 0x3c3836, 0x504945, 0x665c54,
0x928374, 0xebdbb2, 0xa89984, 0xfe8019, 0xd3869b, 0x83a598,
0xb8bb26, 0x8ec07c, 0xfabd2f, 0xfb4934, 0x1d2021,
0x3c3836, 0x32302f, 0x504945, 0x1d2021, 0x1d2021, 0x3c3836, 0x504945,
)
ThemeId.Solarized -> if (light) pal(
0xeee8d5, 0xfdf6e3, 0xeee8d5, 0xeee8d5, 0xe2dbc6, 0xc9c0a6,
0x657b83, 0x586e75, 0x657b83, 0x268bd2, 0x6c71c4, 0x2aa198,
0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0xfdf6e3,
0xeee8d5, 0xe6dfc8, 0x93a1a1, 0xeee8d5, 0xeee8d5, 0x93a1a1, 0x586e75,
) else pal(
0x002b36, 0x073642, 0x002b36, 0x586e75, 0x657b83, 0x839496,
0x93a1a1, 0xfdf6e3, 0x93a1a1, 0x268bd2, 0x6c71c4, 0x2aa198,
0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0x002b36,
0x073642, 0x002b36, 0x586e75, 0x002b36, 0x002b36, 0x073642, 0x586e75,
)
ThemeId.Everforest -> if (light) pal(
0xe6e2cc, 0xfdf6e3, 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xa6b0a0,
0x939f91, 0x5c6a72, 0x829181, 0x8da101, 0xdf69ba, 0x35a77c,
0x8da101, 0x35a77c, 0xdfa000, 0xf85552, 0xfdf6e3,
0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xe6e2cc, 0xe6e2cc, 0xe0dcc7, 0x939f91,
) else pal(
0x1e2326, 0x272e33, 0x2d353b, 0x374145, 0x4f5b58, 0x7a8478,
0x859289, 0xd3c6aa, 0xa7c080, 0xa7c080, 0xd699b6, 0x7fbbb3,
0xa7c080, 0x83c092, 0xdbbc7f, 0xe67e80, 0x272e33,
0x374145, 0x2d353b, 0x4f5b58, 0x1e2326, 0x1e2326, 0x374145, 0x4f5b58,
)
ThemeId.Monokai -> if (light) pal(
0xe6e3d4, 0xfaf8f0, 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xb8b49e,
0x8a8674, 0x2d2a2e, 0x5b5854, 0xe14775, 0x7c5cbf, 0x1c7d9b,
0x5b8c2a, 0x1c7d9b, 0xb08900, 0xe14775, 0xfaf8f0,
0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xe6e3d4, 0xe6e3d4, 0xd6d2c0, 0x8a8674,
) else pal(
0x1d1e19, 0x272822, 0x1e1f1a, 0x3e3d32, 0x49483e, 0x75715e,
0x75715e, 0xf8f8f2, 0xa59f85, 0xf92672, 0xae81ff, 0x66d9ef,
0xa6e22e, 0xa1efe4, 0xe6db74, 0xf92672, 0x272822,
0x3e3d32, 0x2e2f29, 0x49483e, 0x1d1e19, 0x1d1e19, 0x3e3d32, 0x49483e,
)
ThemeId.OneDark -> if (light) pal(
0xe5e5e6, 0xfafafa, 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
0xa0a1a7, 0x383a42, 0x696c77, 0xa626a4, 0x4078f2, 0x0184bc,
0x50a14f, 0x0184bc, 0xc18401, 0xe45649, 0xfafafa,
0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xe5e5e6, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
) else pal(
0x21252b, 0x282c34, 0x21252b, 0x3b4048, 0x4b5263, 0x5c6370,
0x5c6370, 0xabb2bf, 0x7f848e, 0xc678dd, 0x61afef, 0x56b6c2,
0x98c379, 0x56b6c2, 0xe5c07b, 0xe06c75, 0x282c34,
0x3b4048, 0x2c313a, 0x4b5263, 0x21252b, 0x21252b, 0x3b4048, 0x4b5263,
)
}
}

View file

@ -1,519 +0,0 @@
package org.maturita.maturita.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.Typography
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.maturita.maturita.AppViewModel
import org.maturita.maturita.Route
/** Bold is capped at Medium: synthetic bold doubled glyphs on some devices. */
fun appTextStyle(
fontSize: TextUnit,
fontWeight: FontWeight = FontWeight.Normal,
textAlign: TextAlign = TextAlign.Unspecified,
color: Color = Color.Unspecified,
): TextStyle {
val weight = if (fontWeight >= FontWeight.Bold) FontWeight.Medium else fontWeight
return TextStyle(
color = color,
fontSize = fontSize,
fontWeight = weight,
fontFamily = FontFamily.SansSerif,
fontSynthesis = FontSynthesis.None,
lineHeight = fontSize * 1.35f,
letterSpacing = 0.sp,
textAlign = textAlign,
)
}
fun appTypography(): Typography = Typography(
displaySmall = appTextStyle(34.sp, FontWeight.Medium),
headlineLarge = appTextStyle(30.sp, FontWeight.Medium),
headlineMedium = appTextStyle(26.sp, FontWeight.Medium),
headlineSmall = appTextStyle(22.sp, FontWeight.Medium),
titleLarge = appTextStyle(20.sp, FontWeight.Medium),
titleMedium = appTextStyle(16.sp, FontWeight.Medium),
titleSmall = appTextStyle(14.sp, FontWeight.Medium),
bodyLarge = appTextStyle(16.sp),
bodyMedium = appTextStyle(14.sp),
bodySmall = appTextStyle(12.sp),
labelLarge = appTextStyle(14.sp, FontWeight.Medium),
labelMedium = appTextStyle(12.sp, FontWeight.Medium),
labelSmall = appTextStyle(11.sp, FontWeight.Medium),
)
// ---------------------------------------------------------------------------
// Chrome
@Composable
fun AppNavBar(vm: AppViewModel) {
val root = vm.root
NavigationBar {
NavigationBarItem(
selected = root == Route.Subjects,
onClick = { vm.go(Route.Subjects) },
icon = { SubjectsIcon(navIconColor(root == Route.Subjects)) },
label = { Text(vm.tr("subjects_title"), maxLines = 1, overflow = TextOverflow.Ellipsis) },
)
NavigationBarItem(
selected = root == Route.Stats,
onClick = { vm.go(Route.Stats) },
icon = { StatsIcon(navIconColor(root == Route.Stats), 22.dp) },
label = { Text(vm.tr("stats"), maxLines = 1, overflow = TextOverflow.Ellipsis) },
)
NavigationBarItem(
selected = root == Route.Settings,
onClick = { vm.go(Route.Settings) },
icon = { SettingsIcon(navIconColor(root == Route.Settings), 22.dp) },
label = { Text(vm.tr("settings"), maxLines = 1, overflow = TextOverflow.Ellipsis) },
)
}
}
@Composable
private fun navIconColor(selected: Boolean): Color =
if (selected) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
/** Tab root: large collapsing title, search action, bottom navigation. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RootScaffold(
vm: AppViewModel,
title: String,
showSearch: Boolean = true,
content: LazyListScope.() -> Unit,
) {
val scroll = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier.nestedScroll(scroll.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
actions = {
if (showSearch) {
IconButton(onClick = { vm.searchOpen = true }) {
Icon(Icons.Filled.Search, contentDescription = vm.tr("search_placeholder"))
}
}
},
scrollBehavior = scroll,
)
},
bottomBar = { AppNavBar(vm) },
) { inner ->
LazyColumn(
contentPadding = PaddingValues(
top = inner.calculateTopPadding(),
bottom = inner.calculateBottomPadding() + 16.dp,
),
) { content() }
}
}
/** Pushed screen: back arrow, title with optional subtitle, optional bottom action bar. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScaffold(
vm: AppViewModel,
title: String,
subtitle: String? = null,
bottomBar: (@Composable () -> Unit)? = null,
content: @Composable (PaddingValues) -> Unit,
) {
val scroll = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier.nestedScroll(scroll.nestedScrollConnection),
topBar = {
TopAppBar(
title = {
Column {
Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleLarge)
if (!subtitle.isNullOrBlank()) {
Text(
subtitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
navigationIcon = {
IconButton(onClick = { vm.back() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = vm.tr("back"))
}
},
scrollBehavior = scroll,
)
},
bottomBar = { bottomBar?.invoke() },
content = content,
)
}
/** Feedback line plus the primary action, anchored at the bottom of an exercise. */
@Composable
fun ActionBar(
label: String,
feedback: String = "",
kind: String = "",
enabled: Boolean = true,
leading: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
) {
Surface(color = MaterialTheme.colorScheme.surfaceContainer, tonalElevation = 2.dp) {
Column(
Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
AnimatedVisibility(
visible = feedback.isNotEmpty(),
enter = fadeIn(tween(180)) + expandVertically(),
exit = fadeOut(tween(120)) + shrinkVertically(),
) {
FeedbackLine(feedback, kind, animate = false)
Spacer(Modifier.height(10.dp))
}
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
leading?.invoke()
Button(onClick = onClick, enabled = enabled, modifier = Modifier.weight(1f).height(50.dp)) {
Text(label, style = MaterialTheme.typography.titleMedium)
}
}
}
}
}
// ---------------------------------------------------------------------------
// Lists
@Composable
fun SectionHeader(text: String) {
Text(
text,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 20.dp, bottom = 6.dp),
)
}
/** One navigable row: done/pending/locked state on the left, chevron on the right. */
@Composable
fun NavRow(
title: String,
subtitle: String? = null,
done: Boolean = false,
locked: Boolean = false,
leading: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)?,
) {
val enabled = !locked && onClick != null
val textColor = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
val rowModifier = if (enabled) Modifier.clickable(onClick = onClick!!) else Modifier
ListItem(
headlineContent = { Text(title, color = textColor) },
supportingContent = subtitle?.takeIf { it.isNotBlank() }?.let {
{ Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant) }
},
leadingContent = {
when {
leading != null -> leading()
locked -> Icon(Icons.Filled.Lock, null, tint = MaterialTheme.colorScheme.outline)
done -> Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary)
else -> Icon(Icons.Filled.RadioButtonUnchecked, null, tint = MaterialTheme.colorScheme.outline)
}
},
trailingContent = if (enabled) {
{ Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null, tint = MaterialTheme.colorScheme.outline) }
} else null,
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = rowModifier,
)
}
// ---------------------------------------------------------------------------
// Exercise widgets
@Composable
fun PrimaryButton(label: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
Button(onClick = onClick, modifier = modifier) { Text(label) }
}
@Composable
fun PillButton(label: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
FilledTonalButton(onClick = onClick, modifier = modifier) { Text(label) }
}
@Composable
fun SmoothProgress(
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary,
trackColor: Color = MaterialTheme.colorScheme.surfaceVariant,
) {
val animated by animateFloatAsState(
targetValue = progress.coerceIn(0f, 1f),
animationSpec = tween(durationMillis = 380),
label = "progress",
)
LinearProgressIndicator(
progress = { animated },
modifier = modifier,
color = color,
trackColor = trackColor,
)
}
@Composable
fun FeedbackLine(text: String, kind: String, animate: Boolean = true) {
val color = when (kind) {
"ok" -> MaterialTheme.colorScheme.primary
"warn" -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.error
}
val icon = when (kind) {
"ok" -> Icons.Filled.CheckCircle
"warn" -> Icons.Filled.Warning
else -> Icons.Filled.Cancel
}
if (!animate) {
if (text.isEmpty()) return
FeedbackRow(text, color, icon)
return
}
AnimatedVisibility(
visible = text.isNotEmpty(),
enter = fadeIn(tween(180)) + expandVertically(),
exit = fadeOut(tween(120)) + shrinkVertically(),
) {
FeedbackRow(text, color, icon)
}
}
@Composable
private fun FeedbackRow(text: String, color: Color, icon: ImageVector) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(icon, null, tint = color, modifier = Modifier.size(20.dp))
Spacer(Modifier.width(8.dp))
Text(text, color = color, style = MaterialTheme.typography.titleSmall)
}
}
@Composable
fun Meaning(text: String, visible: Boolean) {
AnimatedVisibility(
visible = visible && text.isNotEmpty(),
enter = fadeIn(tween(180)),
exit = fadeOut(tween(120)),
) {
Text(
text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 8.dp),
)
}
}
@Composable
fun Hint(text: String) {
Text(
text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp),
)
}
@Composable
fun Prompt(text: String) {
Text(text, style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 4.dp, bottom = 4.dp))
}
@Composable
fun WordField(
value: String,
onValue: (String) -> Unit,
placeholder: String,
mark: Boolean? = null,
modifier: Modifier = Modifier,
numeric: Boolean = false,
) {
OutlinedTextField(
value = value,
onValueChange = onValue,
placeholder = if (placeholder.isNotEmpty()) ({ Text(placeholder) }) else null,
singleLine = true,
isError = mark == false,
trailingIcon = when (mark) {
true -> ({ Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary) })
false -> ({ Icon(Icons.Filled.Cancel, null, tint = MaterialTheme.colorScheme.error) })
null -> null
},
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
keyboardType = if (numeric) KeyboardType.Decimal else KeyboardType.Text,
),
modifier = modifier.fillMaxWidth().padding(vertical = 4.dp),
)
}
/** Tappable word in a bank or a built sentence. */
@Composable
fun Chip(text: String, onClick: () -> Unit) {
SuggestionChip(onClick = onClick, label = { Text(text) })
}
@Composable
fun SegChip(label: String, selected: Boolean, onClick: () -> Unit) {
FilterChip(selected = selected, onClick = onClick, label = { Text(label) })
}
/** Multiple-choice answer with radio state and, after checking, a correct/wrong mark. */
@Composable
fun OptionRow(text: String, selected: Boolean, mark: Boolean?, enabled: Boolean = true, onClick: () -> Unit) {
val target = when (mark) {
true -> MaterialTheme.colorScheme.primaryContainer
false -> MaterialTheme.colorScheme.errorContainer
null -> if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh
}
val container by animateColorAsState(target, tween(180), label = "option")
Surface(
onClick = onClick,
enabled = enabled,
color = container,
shape = MaterialTheme.shapes.large,
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
) {
Row(Modifier.padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
when (mark) {
true -> Icon(Icons.Filled.CheckCircle, null, tint = MaterialTheme.colorScheme.primary)
false -> Icon(Icons.Filled.Cancel, null, tint = MaterialTheme.colorScheme.error)
null -> RadioButton(selected = selected, onClick = null)
}
Spacer(Modifier.width(12.dp))
Text(text, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
}
}
}
/** Drop zone for word ordering; visible even without words. */
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun WordSlot(empty: Boolean, content: @Composable () -> Unit) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.large,
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
) {
FlowRow(
Modifier.fillMaxWidth().heightIn(min = 52.dp).padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (empty) Text("…", color = MaterialTheme.colorScheme.outline, modifier = Modifier.padding(8.dp))
content()
}
}
}
/** Bottom sheet with the word bank for combo/dialogue blanks. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WordPicker(title: String, pool: List<String>, onPick: (String?) -> Unit) {
ModalBottomSheet(onDismissRequest = { onPick(null) }) {
Text(
title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
LazyColumn(contentPadding = PaddingValues(bottom = 24.dp)) {
items(pool.size) { i ->
ListItem(
headlineContent = { Text(pool[i]) },
modifier = Modifier.clickable { onPick(pool[i]) },
)
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,296 +0,0 @@
package org.maturita.maturita.ui
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.maturita.maturita.data.Palette
@Composable
fun DrawnIcon(iconSize: Dp, modifier: Modifier = Modifier, onDraw: androidx.compose.ui.graphics.drawscope.DrawScope.() -> Unit) {
Canvas(modifier.size(iconSize)) { onDraw() }
}
@Composable
fun BackIcon(color: Color, iconSize: Dp = 16.dp) {
DrawnIcon(iconSize) {
val p = Path().apply {
moveTo(size.width * 0.68f, size.height * 0.18f)
lineTo(size.width * 0.28f, size.height * 0.5f)
lineTo(size.width * 0.68f, size.height * 0.82f)
}
drawPath(p, color, style = Stroke(width = size.minDimension * 0.16f, cap = StrokeCap.Round))
}
}
@Composable
fun SearchIcon(color: Color, iconSize: Dp = 18.dp) {
DrawnIcon(iconSize) {
val r = size.minDimension * 0.28f
drawCircle(color, r, Offset(size.width * 0.42f, size.height * 0.42f), style = Stroke(size.minDimension * 0.12f))
drawLine(color, Offset(size.width * 0.62f, size.height * 0.62f), Offset(size.width * 0.82f, size.height * 0.82f), size.minDimension * 0.12f, StrokeCap.Round)
}
}
@Composable
fun StatsIcon(color: Color, iconSize: Dp = 18.dp) {
DrawnIcon(iconSize) {
val w = size.minDimension * 0.16f
drawRoundRect(color, Offset(size.width * 0.18f, size.height * 0.48f), Size(w, size.height * 0.36f), androidx.compose.ui.geometry.CornerRadius(w))
drawRoundRect(color, Offset(size.width * 0.42f, size.height * 0.22f), Size(w, size.height * 0.62f), androidx.compose.ui.geometry.CornerRadius(w))
drawRoundRect(color, Offset(size.width * 0.66f, size.height * 0.36f), Size(w, size.height * 0.48f), androidx.compose.ui.geometry.CornerRadius(w))
}
}
@Composable
fun SubjectsIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
val gap = size.minDimension * 0.08f
val cell = (size.minDimension - gap) / 2f
val r = androidx.compose.ui.geometry.CornerRadius(cell * 0.28f)
for (iy in 0 until 2) for (ix in 0 until 2) {
drawRoundRect(color, Offset(ix * (cell + gap), iy * (cell + gap)), Size(cell, cell), r)
}
}
}
@Composable
fun SettingsIcon(color: Color, iconSize: Dp = 18.dp) {
DrawnIcon(iconSize) {
val cx = size.width / 2f
val cy = size.height / 2f
val s = size.minDimension
val rOuter = s * 0.48f
val rInner = s * 0.34f
val rHole = s * 0.16f
val teeth = 8
val toothHalf = (Math.PI / teeth).toFloat()
val gear = Path().apply {
fillType = PathFillType.EvenOdd
for (i in 0 until teeth) {
val a = (-Math.PI / 2.0 + 2.0 * Math.PI * i / teeth).toFloat()
val a0 = a - toothHalf * 0.55f
val a1 = a - toothHalf * 0.28f
val a2 = a + toothHalf * 0.28f
val a3 = a + toothHalf * 0.55f
val p0 = Offset(cx + kotlin.math.cos(a0) * rInner, cy + kotlin.math.sin(a0) * rInner)
if (i == 0) moveTo(p0.x, p0.y) else lineTo(p0.x, p0.y)
lineTo(cx + kotlin.math.cos(a1) * rOuter, cy + kotlin.math.sin(a1) * rOuter)
lineTo(cx + kotlin.math.cos(a2) * rOuter, cy + kotlin.math.sin(a2) * rOuter)
lineTo(cx + kotlin.math.cos(a3) * rInner, cy + kotlin.math.sin(a3) * rInner)
}
close()
addOval(
androidx.compose.ui.geometry.Rect(
cx - rHole, cy - rHole, cx + rHole, cy + rHole,
),
)
}
drawPath(gear, color)
}
}
@Composable
fun LockIcon(color: Color, iconSize: Dp = 18.dp) {
DrawnIcon(iconSize) {
val body = Path().apply {
addRoundRect(
androidx.compose.ui.geometry.RoundRect(
size.width * 0.28f, size.height * 0.46f,
size.width * 0.72f, size.height * 0.82f,
androidx.compose.ui.geometry.CornerRadius(size.minDimension * 0.08f),
),
)
}
drawPath(body, color)
drawArc(
color,
200f, 140f, false,
Offset(size.width * 0.32f, size.height * 0.18f),
Size(size.width * 0.36f, size.height * 0.4f),
style = Stroke(size.minDimension * 0.1f, cap = StrokeCap.Round),
)
}
}
@Composable
fun CheckIcon(color: Color, iconSize: Dp = 16.dp) {
DrawnIcon(iconSize) {
val p = Path().apply {
moveTo(size.width * 0.18f, size.height * 0.52f)
lineTo(size.width * 0.4f, size.height * 0.74f)
lineTo(size.width * 0.84f, size.height * 0.26f)
}
drawPath(p, color, style = Stroke(size.minDimension * 0.16f, cap = StrokeCap.Round))
}
}
@Composable
fun WifiIcon(color: Color, iconSize: Dp = 28.dp) {
DrawnIcon(iconSize) {
val c = Offset(size.width / 2, size.height * 0.72f)
drawCircle(color, size.minDimension * 0.06f, c)
for (i in 1..3) {
val r = size.minDimension * (0.16f + i * 0.16f)
drawArc(color, 210f, 120f, false, Offset(c.x - r, c.y - r), Size(r * 2, r * 2), style = Stroke(size.minDimension * 0.08f, cap = StrokeCap.Round))
}
}
}
@Composable
fun ChipIcon(color: Color, iconSize: Dp = 28.dp) {
DrawnIcon(iconSize) {
val m = size.minDimension * 0.22f
drawRoundRect(color, Offset(m, m), Size(size.width - 2 * m, size.height - 2 * m), androidx.compose.ui.geometry.CornerRadius(6f), style = Stroke(size.minDimension * 0.08f))
val pin = size.minDimension * 0.08f
for (i in 0 until 3) {
val t = m + (size.height - 2 * m) * (i + 1) / 4f
drawLine(color, Offset(m - pin, t), Offset(m, t), size.minDimension * 0.06f)
drawLine(color, Offset(size.width - m, t), Offset(size.width - m + pin, t), size.minDimension * 0.06f)
}
}
}
@Composable
fun GermanFlag(iconSize: Dp = 48.dp) {
DrawnIcon(iconSize) {
val w = this.size.width
val h = this.size.height
val clip = Path().apply { addOval(androidx.compose.ui.geometry.Rect(0f, 0f, w, h)) }
clipPath(clip) {
val band = h / 3f
drawRect(Color(0xFF2E2E30), size = Size(w, band))
drawRect(Color(0xFFB8453D), topLeft = Offset(0f, band), size = Size(w, band))
drawRect(Color(0xFFD6B352), topLeft = Offset(0f, 2 * band), size = Size(w, band))
}
}
}
@Composable
fun CzechFlag(iconSize: Dp = 48.dp) {
DrawnIcon(iconSize) {
val w = this.size.width
val h = this.size.height
val clip = Path().apply { addOval(androidx.compose.ui.geometry.Rect(0f, 0f, w, h)) }
clipPath(clip) {
drawRect(Color(0xFFFAFAFA), size = Size(w, h))
drawRect(Color(0xFFD4292E), topLeft = Offset(0f, h / 2), size = Size(w, h / 2))
val tri = Path().apply {
moveTo(0f, 0f)
lineTo(w * 0.55f, h / 2)
lineTo(0f, h)
close()
}
drawPath(tri, Color(0xFF123D8C))
}
}
}
@Composable
fun UkFlag(iconSize: Dp = 48.dp) {
DrawnIcon(iconSize) {
val w = this.size.width
val h = this.size.height
val clip = Path().apply { addOval(androidx.compose.ui.geometry.Rect(0f, 0f, w, h)) }
clipPath(clip) {
drawRect(Color(0xFF012466), size = Size(w, h))
drawLine(Color.White, Offset(0f, 0f), Offset(w, h), strokeWidth = w * 0.16f)
drawLine(Color.White, Offset(w, 0f), Offset(0f, h), strokeWidth = w * 0.16f)
drawLine(Color(0xFFCC0D26), Offset(0f, 0f), Offset(w, h), strokeWidth = w * 0.055f)
drawLine(Color(0xFFCC0D26), Offset(w, 0f), Offset(0f, h), strokeWidth = w * 0.055f)
drawRect(Color.White, topLeft = Offset(0f, h * 0.38f), size = Size(w, h * 0.24f))
drawRect(Color.White, topLeft = Offset(w * 0.38f, 0f), size = Size(w * 0.24f, h))
drawRect(Color(0xFFCC0D26), topLeft = Offset(0f, h * 0.43f), size = Size(w, h * 0.14f))
drawRect(Color(0xFFCC0D26), topLeft = Offset(w * 0.43f, 0f), size = Size(w * 0.14f, h))
}
}
}
@Composable
fun BookIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
drawRoundRect(color, Offset(size.width * 0.22f, size.height * 0.18f), Size(size.width * 0.56f, size.height * 0.64f), androidx.compose.ui.geometry.CornerRadius(4f), style = Stroke(size.minDimension * 0.08f))
drawLine(color, Offset(size.width * 0.38f, size.height * 0.18f), Offset(size.width * 0.38f, size.height * 0.82f), size.minDimension * 0.08f)
}
}
@Composable
fun QuizIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
drawCircle(color, size.minDimension * 0.36f, Offset(size.width / 2, size.height / 2), style = Stroke(size.minDimension * 0.08f))
drawCircle(color, size.minDimension * 0.06f, Offset(size.width / 2, size.height * 0.68f))
drawLine(color, Offset(size.width / 2, size.height * 0.32f), Offset(size.width / 2, size.height * 0.52f), size.minDimension * 0.08f, StrokeCap.Round)
}
}
@Composable
fun OrderIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
for (i in 0 until 3) {
val y = size.height * (0.28f + i * 0.22f)
drawLine(color, Offset(size.width * 0.22f, y), Offset(size.width * 0.78f, y), size.minDimension * 0.08f, StrokeCap.Round)
}
}
}
@Composable
fun GlobeIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
drawCircle(color, size.minDimension * 0.34f, center, style = Stroke(size.minDimension * 0.08f))
drawOval(color, Offset(size.width * 0.32f, size.height * 0.18f), Size(size.width * 0.36f, size.height * 0.64f), style = Stroke(size.minDimension * 0.06f))
}
}
@Composable
fun PeopleIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
drawCircle(color, size.minDimension * 0.12f, Offset(size.width * 0.35f, size.height * 0.32f))
drawCircle(color, size.minDimension * 0.12f, Offset(size.width * 0.65f, size.height * 0.32f))
drawArc(color, 200f, 140f, false, Offset(size.width * 0.18f, size.height * 0.42f), Size(size.width * 0.34f, size.height * 0.4f), style = Stroke(size.minDimension * 0.08f, cap = StrokeCap.Round))
drawArc(color, 200f, 140f, false, Offset(size.width * 0.48f, size.height * 0.42f), Size(size.width * 0.34f, size.height * 0.4f), style = Stroke(size.minDimension * 0.08f, cap = StrokeCap.Round))
}
}
@Composable
fun BulbIcon(color: Color, iconSize: Dp = 22.dp) {
DrawnIcon(iconSize) {
drawCircle(color, size.minDimension * 0.22f, Offset(size.width / 2, size.height * 0.4f), style = Stroke(size.minDimension * 0.08f))
drawRoundRect(color, Offset(size.width * 0.4f, size.height * 0.58f), Size(size.width * 0.2f, size.height * 0.2f), androidx.compose.ui.geometry.CornerRadius(3f))
}
}
@Composable
fun FinishFlag(p: Palette, number: String, iconSize: Dp) {
DrawnIcon(iconSize) {
val r = size.minDimension / 2f - 2f
drawCircle(p.finishDark, r)
drawCircle(p.finishStroke, r, style = Stroke(2f))
val cell = 6f
val fx = center.x - 1.5f * cell
val fy = center.y + 8f
for (iy in 0 until 2) for (ix in 0 until 3) {
val c = if ((ix + iy) and 1 == 1) p.finishLight else p.finishStroke
drawRect(c, Offset(fx + ix * cell, fy + iy * cell), Size(cell, cell))
}
}
}
fun noteIcon(name: String, color: Color): @Composable () -> Unit = {
when (name) {
"globe" -> GlobeIcon(color)
"people" -> PeopleIcon(color)
"bulb" -> BulbIcon(color)
"quiz" -> QuizIcon(color)
"order" -> OrderIcon(color)
else -> BookIcon(color)
}
}

File diff suppressed because it is too large Load diff