mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
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:
parent
6fbfed86f8
commit
b324496334
172 changed files with 0 additions and 124855 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) } }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue