mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Stop Android from crashing on subject maps and ship a new installable version.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
db0422a952
commit
cc36881bb9
6 changed files with 177 additions and 206 deletions
|
|
@ -13,8 +13,8 @@ android {
|
||||||
applicationId = "org.maturita.maturita"
|
applicationId = "org.maturita.maturita"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 3
|
||||||
versionName = "android"
|
versionName = "android-3"
|
||||||
buildConfigField("String", "COMMIT", "\"$commit\"")
|
buildConfigField("String", "COMMIT", "\"$commit\"")
|
||||||
buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"")
|
buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"")
|
||||||
buildConfigField("String", "UPDATE_BRANCH", "\"builds\"")
|
buildConfigField("String", "UPDATE_BRANCH", "\"builds\"")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
package org.maturita.maturita
|
package org.maturita.maturita
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
|
@ -60,9 +66,61 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
val crashPrefs = getSharedPreferences("crash", MODE_PRIVATE)
|
||||||
|
val prev = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, error ->
|
||||||
|
try {
|
||||||
|
crashPrefs.edit().putString("last", error.stackTraceToString()).commit()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
prev?.uncaughtException(thread, error)
|
||||||
|
}
|
||||||
|
val lastCrash = crashPrefs.getString("last", null)
|
||||||
|
if (lastCrash != null) {
|
||||||
|
setContentView(crashView(lastCrash) {
|
||||||
|
crashPrefs.edit().remove("last").apply()
|
||||||
|
recreate()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent { MaturitaApp(vm) }
|
setContent { MaturitaApp(vm) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun crashView(dump: String, retry: () -> Unit): LinearLayout {
|
||||||
|
val pad = (20 * resources.displayMetrics.density).toInt()
|
||||||
|
val title = TextView(this).apply {
|
||||||
|
text = "maturita.c spadla"
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f)
|
||||||
|
setPadding(0, 0, 0, pad / 2)
|
||||||
|
}
|
||||||
|
val hint = TextView(this).apply {
|
||||||
|
text = "Pošli tenhle text, ať jde pád opravit."
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
|
||||||
|
setPadding(0, 0, 0, pad / 2)
|
||||||
|
}
|
||||||
|
val body = TextView(this).apply {
|
||||||
|
text = dump
|
||||||
|
setTextIsSelectable(true)
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||||
|
typeface = android.graphics.Typeface.MONOSPACE
|
||||||
|
}
|
||||||
|
val scroll = ScrollView(this).apply {
|
||||||
|
addView(body, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||||
|
}
|
||||||
|
val retryBtn = Button(this).apply {
|
||||||
|
text = "Zkusit znovu"
|
||||||
|
setOnClickListener { retry() }
|
||||||
|
}
|
||||||
|
return LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(pad, pad, pad, pad)
|
||||||
|
addView(title, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||||
|
addView(hint, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||||
|
addView(scroll, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f))
|
||||||
|
addView(retryBtn, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class Content(root: JSONObject) {
|
||||||
}
|
}
|
||||||
val subjects = raw.arr("subjects")
|
val subjects = raw.arr("subjects")
|
||||||
val german = raw.arr("german")
|
val german = raw.arr("german")
|
||||||
val net = raw.obj("net")!!
|
val net = raw.obj("net") ?: J(JSONObject())
|
||||||
val netLessons = net.arr("lessons")
|
val netLessons = net.arr("lessons")
|
||||||
val netTasks = net.arr("tasks")
|
val netTasks = net.arr("tasks")
|
||||||
val netAnswers: List<List<J>> = run {
|
val netAnswers: List<List<J>> = run {
|
||||||
|
|
@ -59,8 +59,12 @@ class Content(root: JSONObject) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun load(context: Context): Content {
|
fun load(context: Context): Content {
|
||||||
val text = context.assets.open("content.json").bufferedReader().use { it.readText() }
|
return try {
|
||||||
return Content(JSONObject(text))
|
val text = context.assets.open("content.json").bufferedReader().use { it.readText() }
|
||||||
|
Content(JSONObject(text))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Content(JSONObject())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
|
@ -191,7 +190,8 @@ private fun AssemblyEx(vm: AppViewModel, title: String, sub: String?, spec: J, o
|
||||||
FlowRow(Modifier.fillMaxWidth().padding(bottom = 6.dp).clip(RoundedCornerShape(14.dp)).background(p.surface0).padding(8.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
FlowRow(Modifier.fillMaxWidth().padding(bottom = 6.dp).clip(RoundedCornerShape(14.dp)).background(p.surface0).padding(8.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
if (placed[i].isEmpty()) Text("…", color = p.overlay)
|
if (placed[i].isEmpty()) Text("…", color = p.overlay)
|
||||||
placed[i].forEach { idx ->
|
placed[i].forEach { idx ->
|
||||||
Chip(item.strs("words")[idx], p) {
|
val word = item.strs("words").getOrNull(idx) ?: return@forEach
|
||||||
|
Chip(word, p) {
|
||||||
placed[i].remove(idx)
|
placed[i].remove(idx)
|
||||||
pools[i].add(idx)
|
pools[i].add(idx)
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +199,8 @@ private fun AssemblyEx(vm: AppViewModel, title: String, sub: String?, spec: J, o
|
||||||
}
|
}
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
pools[i].toList().forEach { idx ->
|
pools[i].toList().forEach { idx ->
|
||||||
Chip(item.strs("words")[idx], p) {
|
val word = item.strs("words").getOrNull(idx) ?: return@forEach
|
||||||
|
Chip(word, p) {
|
||||||
pools[i].remove(idx)
|
pools[i].remove(idx)
|
||||||
placed[i].add(idx)
|
placed[i].add(idx)
|
||||||
}
|
}
|
||||||
|
|
@ -663,26 +664,43 @@ private fun ComboLine(
|
||||||
val p = vm.palette
|
val p = vm.palette
|
||||||
var open by remember { mutableStateOf(false) }
|
var open by remember { mutableStateOf(false) }
|
||||||
val box = @Composable {
|
val box = @Composable {
|
||||||
Box {
|
Column {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(p.mantle)
|
.background(p.mantle)
|
||||||
.border(1.dp, if (revealed && picks.getOrNull(idx).orEmpty().isNotEmpty()) {
|
.border(1.dp, if (revealed && picks.getOrNull(idx).orEmpty().isNotEmpty()) {
|
||||||
if (answerAccepts(normalizeAnswer(picks[idx]), answer)) p.success else p.error
|
if (answerAccepts(normalizeAnswer(picks.getOrNull(idx).orEmpty()), answer)) p.success else p.error
|
||||||
} else p.text.copy(0.1f), RoundedCornerShape(12.dp))
|
} else p.text.copy(0.1f), RoundedCornerShape(12.dp))
|
||||||
.clickable { open = true }
|
.clickable { open = !open }
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
val pick = picks.getOrNull(idx).orEmpty()
|
val pick = picks.getOrNull(idx).orEmpty()
|
||||||
Text(pick.ifEmpty { "—" }, color = if (pick.isEmpty()) p.overlay else p.text)
|
Text(pick.ifEmpty { "—" }, color = if (pick.isEmpty()) p.overlay else p.text)
|
||||||
}
|
}
|
||||||
DropdownMenu(open, { open = false }) {
|
if (open) {
|
||||||
pool.forEach { w ->
|
Column(
|
||||||
DropdownMenuItem({ Text(w) }, {
|
Modifier
|
||||||
if (idx in picks.indices) picks[idx] = w
|
.padding(top = 4.dp)
|
||||||
open = false
|
.heightIn(max = 220.dp)
|
||||||
})
|
.verticalScroll(rememberScrollState())
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(p.surface0)
|
||||||
|
.border(1.dp, p.text.copy(0.08f), RoundedCornerShape(12.dp)),
|
||||||
|
) {
|
||||||
|
pool.forEach { w ->
|
||||||
|
Text(
|
||||||
|
w,
|
||||||
|
color = p.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
if (idx in picks.indices) picks[idx] = w
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -950,13 +968,28 @@ private fun VlsmEx(vm: AppViewModel, @Suppress("UNUSED_PARAMETER") lesson: J) {
|
||||||
@Composable
|
@Composable
|
||||||
private fun PrefixMenu(value: String, p: Palette, onPick: (String) -> Unit) {
|
private fun PrefixMenu(value: String, p: Palette, onPick: (String) -> Unit) {
|
||||||
var open by remember { mutableStateOf(false) }
|
var open by remember { mutableStateOf(false) }
|
||||||
Box(Modifier.padding(vertical = 4.dp)) {
|
Column(Modifier.padding(vertical = 4.dp)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.clip(RoundedCornerShape(12.dp)).background(p.mantle).clickable { open = true }.padding(10.dp),
|
Modifier.clip(RoundedCornerShape(12.dp)).background(p.mantle).clickable { open = !open }.padding(10.dp),
|
||||||
) { Text(value.ifEmpty { "/…" }, color = p.text) }
|
) { Text(value.ifEmpty { "/…" }, color = p.text) }
|
||||||
DropdownMenu(open, { open = false }) {
|
if (open) {
|
||||||
(8..30).forEach { n ->
|
Column(
|
||||||
DropdownMenuItem({ Text("/$n") }, { onPick("/$n"); open = false })
|
Modifier
|
||||||
|
.heightIn(max = 220.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(p.surface0),
|
||||||
|
) {
|
||||||
|
(8..30).forEach { n ->
|
||||||
|
Text(
|
||||||
|
"/$n",
|
||||||
|
color = p.text,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onPick("/$n"); open = false }
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,30 @@
|
||||||
package org.maturita.maturita.ui
|
package org.maturita.maturita.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Path
|
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.graphics.StrokeJoin
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import org.maturita.maturita.data.Palette
|
import org.maturita.maturita.data.Palette
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
data class MapNode(
|
data class MapNode(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
|
@ -47,191 +36,73 @@ data class MapNode(
|
||||||
val onClick: (() -> Unit)? = null,
|
val onClick: (() -> Unit)? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class NodePos(val x: Float, val y: Float)
|
|
||||||
|
|
||||||
data class Serpentine(
|
|
||||||
val points: List<NodePos>,
|
|
||||||
val width: Float,
|
|
||||||
val height: Float,
|
|
||||||
val litUntil: Float,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun serpentine(
|
|
||||||
n: Int,
|
|
||||||
avail: Float,
|
|
||||||
mx: Float,
|
|
||||||
my: Float,
|
|
||||||
spac: Float,
|
|
||||||
gap: Float,
|
|
||||||
wave: Float,
|
|
||||||
lift: Float = 0f,
|
|
||||||
minScale: Float = 0.55f,
|
|
||||||
): Serpentine {
|
|
||||||
if (n < 1) return Serpentine(emptyList(), avail, my * 2, 0f)
|
|
||||||
val scale = min(1f, maxOf(minScale, avail / (2 * mx + maxOf(n - 1, 0) * spac)))
|
|
||||||
val smx = mx * scale
|
|
||||||
val smy = my * scale
|
|
||||||
val sspac = spac * scale
|
|
||||||
val sgap = gap * scale
|
|
||||||
val swave = wave * scale
|
|
||||||
val slift = lift * scale
|
|
||||||
var rows = 1
|
|
||||||
while (rows <= n) {
|
|
||||||
val cols = (n + rows - 1) / rows
|
|
||||||
if (2 * smx + (cols - 1) * sspac <= avail + 1) break
|
|
||||||
rows++
|
|
||||||
}
|
|
||||||
rows = min(rows, n)
|
|
||||||
val cols = maxOf(1, (n + rows - 1) / rows)
|
|
||||||
val cw = 2 * smx + (cols - 1) * sspac
|
|
||||||
val ch = 2 * smy + (rows - 1) * sgap + slift
|
|
||||||
val phase = (2.0 * PI * 1.7 / maxOf(n - 1, 1)).toFloat()
|
|
||||||
val pts = MutableList(n) { NodePos(0f, 0f) }
|
|
||||||
for (r in 0 until rows) {
|
|
||||||
val base = r * cols
|
|
||||||
val len = min(cols, n - base)
|
|
||||||
val fwd = r % 2 == 0
|
|
||||||
for (c in 0 until len) {
|
|
||||||
val i = base + c
|
|
||||||
val cc = if (fwd) c else cols - 1 - c
|
|
||||||
pts[i] = NodePos(
|
|
||||||
smx + cc * sspac,
|
|
||||||
smy + slift + r * sgap + swave * sin(i * phase),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Serpentine(pts, cw, ch, 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun catmull(pts: List<NodePos>, t: Float): NodePos {
|
|
||||||
val n = pts.size
|
|
||||||
if (n == 1) return pts[0]
|
|
||||||
var k = t.toInt()
|
|
||||||
var u = t - k
|
|
||||||
if (k < 0) { k = 0; u = 0f }
|
|
||||||
if (k >= n - 1) { k = n - 2; u = 1f }
|
|
||||||
val p1 = pts[k]
|
|
||||||
val p2 = pts[k + 1]
|
|
||||||
val p0 = if (k - 1 >= 0) pts[k - 1] else NodePos(p1.x - (p2.x - p1.x), p1.y - (p2.y - p1.y))
|
|
||||||
val p3 = if (k + 2 < n) pts[k + 2] else NodePos(p2.x + (p2.x - p1.x), p2.y + (p2.y - p1.y))
|
|
||||||
val u2 = u * u
|
|
||||||
val u3 = u2 * u
|
|
||||||
val x = 0.5f * (2 * p1.x + (-p0.x + p2.x) * u + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * u2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * u3)
|
|
||||||
val y = 0.5f * (2 * p1.y + (-p0.y + p2.y) * u + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * u2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * u3)
|
|
||||||
return NodePos(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PathMap(
|
fun PathMap(
|
||||||
nodes: List<MapNode>,
|
nodes: List<MapNode>,
|
||||||
palette: Palette,
|
palette: Palette,
|
||||||
litUntil: Float,
|
@Suppress("UNUSED_PARAMETER") litUntil: Float,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
nodeSize: Float = 72f,
|
@Suppress("UNUSED_PARAMETER") nodeSize: Float = 72f,
|
||||||
mx: Float = 90f,
|
@Suppress("UNUSED_PARAMETER") mx: Float = 90f,
|
||||||
my: Float = 110f,
|
@Suppress("UNUSED_PARAMETER") my: Float = 110f,
|
||||||
spac: Float = 170f,
|
@Suppress("UNUSED_PARAMETER") spac: Float = 170f,
|
||||||
gap: Float = 170f,
|
@Suppress("UNUSED_PARAMETER") gap: Float = 170f,
|
||||||
wave: Float = 28f,
|
@Suppress("UNUSED_PARAMETER") wave: Float = 28f,
|
||||||
lift: Float = 0f,
|
@Suppress("UNUSED_PARAMETER") lift: Float = 0f,
|
||||||
extra: List<Pair<NodePos, @Composable () -> Unit>> = emptyList(),
|
|
||||||
extraPoints: List<NodePos> = emptyList(),
|
|
||||||
) {
|
) {
|
||||||
if (nodes.isEmpty()) return
|
Column(
|
||||||
val density = LocalDensity.current
|
modifier
|
||||||
fun Float.toDp(): androidx.compose.ui.unit.Dp = with(density) { this@toDp.toDp() }
|
.fillMaxSize()
|
||||||
BoxWithConstraints(modifier.fillMaxSize()) {
|
.verticalScroll(rememberScrollState())
|
||||||
val avail = constraints.maxWidth.toFloat().coerceAtLeast(1f)
|
.padding(bottom = 24.dp),
|
||||||
val geo = serpentine(nodes.size, avail, mx, my, spac, gap, wave, lift)
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
Box(
|
) {
|
||||||
Modifier
|
nodes.forEachIndexed { i, node ->
|
||||||
.horizontalScroll(rememberScrollState())
|
val click = node.onClick
|
||||||
.verticalScroll(rememberScrollState()),
|
CardBox(
|
||||||
) {
|
palette,
|
||||||
Box(Modifier.width(geo.width.toDp()).height((geo.height + 80f).toDp())) {
|
Modifier
|
||||||
Canvas(Modifier.fillMaxSize()) {
|
.fillMaxWidth()
|
||||||
if (geo.points.size < 2) return@Canvas
|
.alpha(if (node.locked) 0.48f else 1f)
|
||||||
fun pathBetween(t0: Float, t1: Float): Path {
|
.then(
|
||||||
val p = Path()
|
if (!node.locked && click != null) Modifier.clickable { click() }
|
||||||
val steps = maxOf(1, ((t1 - t0) * 48).toInt())
|
else Modifier,
|
||||||
val start = catmull(geo.points, t0)
|
),
|
||||||
p.moveTo(start.x, start.y)
|
) {
|
||||||
for (s in 1..steps) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
val t = t0 + (t1 - t0) * s / steps
|
val bg = when {
|
||||||
val pt = catmull(geo.points, t)
|
node.locked -> palette.lockedBg
|
||||||
p.lineTo(pt.x, pt.y)
|
node.current || node.done -> palette.accent
|
||||||
}
|
else -> palette.node
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
val end = (nodes.size - 1).toFloat()
|
val fg = when {
|
||||||
drawPath(pathBetween(0f, end), palette.rail, style = Stroke(12f, cap = StrokeCap.Round, join = StrokeJoin.Round))
|
node.locked -> palette.overlay
|
||||||
if (litUntil > 0f) {
|
node.current || node.done -> palette.onAccent
|
||||||
drawPath(pathBetween(0f, litUntil), palette.accent, style = Stroke(12f, cap = StrokeCap.Round, join = StrokeJoin.Round))
|
else -> palette.text
|
||||||
}
|
}
|
||||||
extraPoints.forEach { }
|
|
||||||
}
|
|
||||||
nodes.forEachIndexed { i, node ->
|
|
||||||
val pt = geo.points[i]
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.offset { IntOffset((pt.x - nodeSize / 2).toInt(), (pt.y - nodeSize / 2).toInt()) }
|
.size(44.dp)
|
||||||
.size(nodeSize.toDp()),
|
.clip(CircleShape)
|
||||||
|
.background(bg),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val bg = when {
|
when {
|
||||||
node.locked -> palette.lockedBg
|
node.locked -> LockIcon(palette.overlay, 16.dp)
|
||||||
node.current || node.done -> palette.accent
|
node.done -> CheckIcon(palette.onAccent, 16.dp)
|
||||||
else -> palette.node
|
else -> Text("${i + 1}", color = fg, fontWeight = FontWeight.Black, fontSize = 16.sp)
|
||||||
}
|
|
||||||
val fg = when {
|
|
||||||
node.locked -> palette.overlay
|
|
||||||
node.current || node.done -> palette.onAccent
|
|
||||||
else -> palette.text
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(bg)
|
|
||||||
.then(
|
|
||||||
if (node.locked) Modifier.border(1.dp, palette.lockedBorder, CircleShape)
|
|
||||||
else Modifier
|
|
||||||
)
|
|
||||||
.then(if (!node.locked && node.onClick != null) Modifier.clickable { node.onClick.invoke() } else Modifier),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
if (node.finish) {
|
|
||||||
FinishFlag(palette, "${i + 1}", nodeSize.toDp())
|
|
||||||
} else {
|
|
||||||
androidx.compose.foundation.layout.Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text("${i + 1}", color = fg, fontWeight = FontWeight.Black, fontSize = 18.sp)
|
|
||||||
if (node.locked) LockIcon(palette.overlay, 14.dp)
|
|
||||||
else if (node.done) CheckIcon(palette.onAccent, 14.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
node.label,
|
node.label,
|
||||||
color = if (node.locked) palette.overlay else palette.text,
|
color = if (node.locked) palette.overlay else palette.text,
|
||||||
fontSize = 12.sp,
|
fontWeight = if (node.current) FontWeight.Bold else FontWeight.SemiBold,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontSize = 16.sp,
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier
|
|
||||||
.offset { IntOffset((pt.x - 70).toInt(), (pt.y + nodeSize / 2 + 8).toInt()) }
|
|
||||||
.width(140.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
extra.forEach { (pt, content) ->
|
|
||||||
Box(Modifier.offset { IntOffset(pt.x.toInt(), pt.y.toInt()) }) { content() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun branchPoint(points: List<NodePos>, index: Int = 1, dx: Float = 40f, lift: Float = 110f): NodePos? {
|
|
||||||
if (index !in points.indices) return null
|
|
||||||
val p = points[index]
|
|
||||||
return NodePos(p.x + dx, p.y - lift)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ fun WelcomeScreen(vm: AppViewModel) {
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
) {
|
) {
|
||||||
Text("maturita.c", color = p.text, fontWeight = FontWeight.Bold, fontSize = 15.sp)
|
Text("maturita.c", color = p.text, fontWeight = FontWeight.Bold, fontSize = 15.sp)
|
||||||
|
Text(
|
||||||
|
"sestavení ${org.maturita.maturita.BuildConfig.VERSION_NAME} · ${org.maturita.maturita.BuildConfig.COMMIT.take(7)}",
|
||||||
|
color = p.overlay,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
|
|
@ -184,7 +189,7 @@ fun SettingsSheet(vm: AppViewModel) {
|
||||||
Text(vm.tr("theme"), color = p.subtext, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
Text(vm.tr("theme"), color = p.subtext, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
themeNames.forEachIndexed { i, name ->
|
themeNames.forEachIndexed { i, name ->
|
||||||
val id = ThemeId.entries[i]
|
val id = ThemeId.entries.getOrNull(i) ?: return@forEachIndexed
|
||||||
val sw = themePalette(id, vm.mode)
|
val sw = themePalette(id, vm.mode)
|
||||||
val sel = vm.themeId == id
|
val sel = vm.themeId == id
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue