mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Polish the UI on all platforms with progress and a two-column code editor.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
333085ee53
commit
0047bd74f0
9 changed files with 1288 additions and 594 deletions
|
|
@ -40,6 +40,8 @@ fun main() {
|
|||
val info = Properties().apply { resourceText("build-info.properties")?.let { load(it.reader()) } }
|
||||
val platform = DesktopPlatform(info.getProperty("commit") ?: "dev", prefs, install)
|
||||
val vm = AppViewModel(Content.parse(resourceText("content.json")), platform)
|
||||
// `GRADUATELY_PAGE=u1e3 make run` opens a page, for screenshots.
|
||||
System.getenv("GRADUATELY_PAGE")?.takeIf { it.isNotBlank() }?.let { vm.openPage(it) }
|
||||
val icon = Thread.currentThread().contextClassLoader.getResourceAsStream("icon.png")
|
||||
?.use { ImageIO.read(it) }?.let { BitmapPainter(it.toComposeImageBitmap()) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,59 @@
|
|||
package org.maturita.maturita.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
import androidx.compose.ui.unit.min
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.maturita.maturita.AppViewModel
|
||||
import org.maturita.maturita.data.J
|
||||
|
|
@ -34,18 +61,21 @@ import org.maturita.maturita.data.J
|
|||
/** Explanation on one side, the program and its output on the other. */
|
||||
@Composable
|
||||
fun CodeLessonScreen(vm: AppViewModel, lesson: J, onSuccess: () -> Unit) {
|
||||
val key = lesson.str("key")
|
||||
val starter = lesson.str("code")
|
||||
var code by remember(lesson.str("key")) { mutableStateOf(starter) }
|
||||
var stdin by remember(lesson.str("key")) { mutableStateOf("") }
|
||||
var output by remember(lesson.str("key")) { mutableStateOf("") }
|
||||
var running by remember(lesson.str("key")) { mutableStateOf(false) }
|
||||
var code by remember(key) { mutableStateOf(starter) }
|
||||
var stdin by remember(key) { mutableStateOf("") }
|
||||
var output by remember(key) { mutableStateOf("") }
|
||||
var status by remember(key) { mutableStateOf("") }
|
||||
val running = status == "running"
|
||||
val needsInput = "ReadLine" in code
|
||||
|
||||
fun run() {
|
||||
if (running) return
|
||||
running = true
|
||||
status = "running"
|
||||
output = vm.tr("cs_running")
|
||||
vm.platform.runCSharp(code, stdin) { result ->
|
||||
running = false
|
||||
vm.platform.runCSharp(code, if (needsInput) stdin else "") { result ->
|
||||
status = result.status
|
||||
output = when (result.status) {
|
||||
"unavailable" -> vm.tr("cs_unavailable")
|
||||
"timeout" -> (result.output + "\n\n" + vm.tr("cs_timeout")).trim()
|
||||
|
|
@ -55,19 +85,39 @@ fun CodeLessonScreen(vm: AppViewModel, lesson: J, onSuccess: () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
DetailScaffold(vm, vm.tr(lesson.str("titleKey")), vm.tr(lesson.str("subKey"))) { inner ->
|
||||
BoxWithConstraints(Modifier.fillMaxSize().padding(inner).padding(12.dp)) {
|
||||
if (maxWidth >= 840.dp) {
|
||||
Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Explanation(vm, lesson, Modifier.weight(0.9f).fillMaxHeight())
|
||||
EditorColumn(vm, code, { code = it }, stdin, { stdin = it }, expand = true, Modifier.weight(1.15f).fillMaxHeight())
|
||||
OutputColumn(vm, output, running, { run() }, { code = starter }, expand = true, Modifier.weight(1f).fillMaxHeight())
|
||||
fun reset() {
|
||||
code = starter
|
||||
output = ""
|
||||
status = ""
|
||||
}
|
||||
|
||||
DetailScaffold(vm, vm.tr(lesson.str("titleKey")), vm.tr(lesson.str("subKey")), maxContentWidth = 1640.dp) { inner ->
|
||||
BoxWithConstraints(Modifier.fillMaxSize().padding(inner)) {
|
||||
val width = maxWidth
|
||||
val height = maxHeight
|
||||
if (width >= 900.dp) {
|
||||
Row(
|
||||
Modifier.fillMaxSize().padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Column(Modifier.width(min(560.dp, width * 0.4f)).fillMaxHeight().verticalScroll(rememberScrollState())) {
|
||||
Explanation(lesson)
|
||||
}
|
||||
Column(Modifier.weight(1f).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
EditorCard(vm, code, { code = it }, running, ::run, ::reset, compact = false, Modifier.weight(1f))
|
||||
if (needsInput) InputField(vm, stdin) { stdin = it }
|
||||
OutputCard(vm, output, status, Modifier.height(max(180.dp, height * 0.32f)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Explanation(vm, lesson, Modifier.fillMaxWidth())
|
||||
EditorColumn(vm, code, { code = it }, stdin, { stdin = it }, expand = false, Modifier.fillMaxWidth())
|
||||
OutputColumn(vm, output, running, { run() }, { code = starter }, expand = false, Modifier.fillMaxWidth())
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Explanation(lesson)
|
||||
EditorCard(vm, code, { code = it }, running, ::run, ::reset, compact = true, Modifier.height(320.dp))
|
||||
if (needsInput) InputField(vm, stdin) { stdin = it }
|
||||
OutputCard(vm, output, status, Modifier.height(220.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,84 +125,179 @@ fun CodeLessonScreen(vm: AppViewModel, lesson: J, onSuccess: () -> Unit) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Explanation(vm: AppViewModel, lesson: J, modifier: Modifier) {
|
||||
Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
lesson.arr("slides").forEach { slide ->
|
||||
Text(slide.str("kicker"), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelLarge)
|
||||
Text(slide.str("title"), style = MaterialTheme.typography.titleLarge)
|
||||
slide.strs("lines").forEach { line ->
|
||||
Text("• $line", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
slide.strOrNull("tip")?.let { tip ->
|
||||
Text(tip, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
private fun Explanation(lesson: J) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(28.dp)) {
|
||||
lesson.arr("slides").forEach { SlideContent(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditorColumn(
|
||||
private fun EditorCard(
|
||||
vm: AppViewModel,
|
||||
code: String,
|
||||
onCode: (String) -> Unit,
|
||||
stdin: String,
|
||||
onStdin: (String) -> Unit,
|
||||
expand: Boolean,
|
||||
running: Boolean,
|
||||
onRun: () -> Unit,
|
||||
onReset: () -> Unit,
|
||||
compact: Boolean,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(vm.tr("cs_code"), style = MaterialTheme.typography.titleMedium)
|
||||
BasicTextField(
|
||||
value = code,
|
||||
onValueChange = onCode,
|
||||
textStyle = codeStyle().copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.then(if (expand) Modifier.weight(1f) else Modifier.height(240.dp))
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(12.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
val dark = MaterialTheme.colorScheme.background.luminance() < 0.5f
|
||||
val highlight = remember(dark) { CSharpHighlight(dark) }
|
||||
Surface(modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surfaceContainerHigh) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.Code, null, tint = MaterialTheme.colorScheme.primary)
|
||||
Text("Program.cs", style = MaterialTheme.typography.titleSmall.copy(fontFamily = FontFamily.Monospace))
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (compact) {
|
||||
IconButton(onClick = onReset, enabled = !running) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = vm.tr("cs_reset"))
|
||||
}
|
||||
} else {
|
||||
OutlinedButton(onClick = onReset, enabled = !running) {
|
||||
Icon(Icons.Filled.Refresh, null, Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(vm.tr("cs_reset"))
|
||||
}
|
||||
}
|
||||
Button(onClick = onRun, enabled = !running) {
|
||||
Icon(Icons.Filled.PlayArrow, null, Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(vm.tr("cs_run"))
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
BasicTextField(
|
||||
value = code,
|
||||
onValueChange = onCode,
|
||||
textStyle = codeStyle().copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
visualTransformation = highlight,
|
||||
modifier = Modifier.fillMaxSize().padding(14.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InputField(vm: AppViewModel, stdin: String, onStdin: (String) -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(vm.tr("cs_stdin"), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
BasicTextField(
|
||||
OutlinedTextField(
|
||||
value = stdin,
|
||||
onValueChange = onStdin,
|
||||
textStyle = codeStyle().copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceContainer).padding(12.dp),
|
||||
textStyle = codeStyle(),
|
||||
minLines = 1,
|
||||
maxLines = 4,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OutputColumn(
|
||||
vm: AppViewModel,
|
||||
output: String,
|
||||
running: Boolean,
|
||||
onRun: () -> Unit,
|
||||
onReset: () -> Unit,
|
||||
expand: Boolean,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FilledTonalButton(onClick = onRun, enabled = !running) { Text(vm.tr("cs_run")) }
|
||||
OutlinedButton(onClick = onReset, enabled = !running) { Text(vm.tr("cs_reset")) }
|
||||
private fun OutputCard(vm: AppViewModel, output: String, status: String, modifier: Modifier) {
|
||||
val muted = output.isEmpty() || status == "running"
|
||||
Surface(modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surfaceContainerHigh) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(48.dp).padding(horizontal = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(Icons.Filled.Terminal, null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(vm.tr("cs_output"), style = MaterialTheme.typography.titleSmall)
|
||||
Spacer(Modifier.weight(1f))
|
||||
StatusBadge(vm, status)
|
||||
}
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Text(
|
||||
output.ifEmpty { vm.tr("cs_empty") },
|
||||
style = codeStyle(),
|
||||
color = if (muted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(14.dp),
|
||||
)
|
||||
}
|
||||
Text(vm.tr("cs_output"), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
output.ifEmpty { vm.tr("cs_empty") },
|
||||
style = codeStyle(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.then(if (expand) Modifier.weight(1f) else Modifier.height(180.dp))
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(12.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun codeStyle() = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 14.sp, lineHeight = 20.sp)
|
||||
@Composable
|
||||
private fun StatusBadge(vm: AppViewModel, status: String) {
|
||||
when (status) {
|
||||
"running" -> CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
"ok" -> Badge(vm.tr("cs_done"), Icons.Filled.CheckCircle, MaterialTheme.colorScheme.primary)
|
||||
"fail", "timeout" -> Badge(vm.tr("cs_failed"), Icons.Filled.Cancel, MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Badge(text: String, icon: androidx.compose.ui.graphics.vector.ImageVector, color: Color) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, null, tint = color, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(text, style = MaterialTheme.typography.labelLarge, color = color)
|
||||
}
|
||||
}
|
||||
|
||||
private fun codeStyle() = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 15.sp, lineHeight = 22.sp)
|
||||
|
||||
/** Colours C# keywords, types, numbers, strings and comments without changing the text. */
|
||||
private class CSharpHighlight(dark: Boolean) : VisualTransformation {
|
||||
private val palette = listOf(
|
||||
Color(if (dark) 0xFFFF7AB2 else 0xFF9B2393),
|
||||
Color(if (dark) 0xFF5DD8FF else 0xFF0B4F79),
|
||||
Color(if (dark) 0xFFD9C97C else 0xFF1C00CF),
|
||||
Color(if (dark) 0xFFFC6A5D else 0xFFC41A16),
|
||||
Color(if (dark) 0xFF7F8C98 else 0xFF5D6C79),
|
||||
)
|
||||
|
||||
override fun filter(text: AnnotatedString): TransformedText =
|
||||
TransformedText(highlight(text.text), OffsetMapping.Identity)
|
||||
|
||||
private fun highlight(code: String): AnnotatedString {
|
||||
val kind = IntArray(code.length) { -1 }
|
||||
RULES.forEachIndexed { index, rule ->
|
||||
rule.findAll(code).forEach { m -> for (i in m.range) kind[i] = index }
|
||||
}
|
||||
return buildAnnotatedString {
|
||||
var start = 0
|
||||
while (start < code.length) {
|
||||
var end = start
|
||||
while (end < code.length && kind[end] == kind[start]) end++
|
||||
val k = kind[start]
|
||||
if (k >= 0) {
|
||||
pushStyle(SpanStyle(color = palette[k]))
|
||||
append(code, start, end)
|
||||
pop()
|
||||
} else {
|
||||
append(code, start, end)
|
||||
}
|
||||
start = end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEYWORDS = listOf(
|
||||
"abstract", "as", "async", "await", "base", "bool", "break", "case", "catch", "char", "class",
|
||||
"const", "continue", "decimal", "default", "do", "double", "else", "enum", "false", "finally",
|
||||
"float", "for", "foreach", "get", "if", "in", "int", "interface", "internal", "is", "long",
|
||||
"namespace", "new", "null", "object", "out", "override", "private", "protected", "public",
|
||||
"readonly", "record", "ref", "return", "sealed", "set", "static", "string", "struct", "switch",
|
||||
"this", "throw", "true", "try", "using", "var", "virtual", "void", "while",
|
||||
)
|
||||
|
||||
/** Later rules win, so strings and comments override keywords inside them. */
|
||||
private val RULES = listOf(
|
||||
Regex("\\b(?:" + KEYWORDS.joinToString("|") + ")\\b"),
|
||||
Regex("\\b(?:Console|Math|List|Dictionary|File|Task|Exception|Func|Action|String|DateTime|Random)\\b"),
|
||||
Regex("\\b\\d+(?:\\.\\d+)?\\b"),
|
||||
Regex("\\$?@?\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])'"),
|
||||
Regex("//[^\\n]*|/\\*[\\s\\S]*?\\*/"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,22 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
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.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
|
|
@ -24,7 +31,17 @@ 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.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
|
@ -44,7 +61,6 @@ 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
|
||||
|
|
@ -150,7 +166,19 @@ fun AppNavBar(vm: AppViewModel) {
|
|||
private fun navIconColor(selected: Boolean): Color =
|
||||
if (selected) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
/** Tab root: large collapsing title, search action, bottom navigation. */
|
||||
/** Lists and exercises stop growing here so they keep their shape in wide desktop windows. */
|
||||
val ContentMaxWidth = 880.dp
|
||||
val ActionMaxWidth = 560.dp
|
||||
|
||||
/** Caps the width of [content] and centres it; phones are narrower than the cap anyway. */
|
||||
@Composable
|
||||
fun CenteredColumn(maxWidth: Dp = ContentMaxWidth, content: @Composable () -> Unit) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
Box(Modifier.widthIn(max = maxWidth).fillMaxSize()) { content() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Tab root: large collapsing title on phones, a compact one in wide windows. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RootScaffold(
|
||||
|
|
@ -159,30 +187,48 @@ fun RootScaffold(
|
|||
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() }
|
||||
BoxWithConstraints {
|
||||
val wide = maxWidth >= 600.dp
|
||||
val scroll = if (wide) {
|
||||
TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
} else {
|
||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||
}
|
||||
val actions: @Composable RowScope.() -> Unit = {
|
||||
if (showSearch) {
|
||||
IconButton(onClick = { vm.searchOpen = true }) {
|
||||
Icon(Icons.Filled.Search, contentDescription = vm.tr("search_placeholder"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scroll.nestedScrollConnection),
|
||||
topBar = {
|
||||
if (wide) {
|
||||
TopAppBar(
|
||||
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.headlineSmall) },
|
||||
actions = actions,
|
||||
scrollBehavior = scroll,
|
||||
)
|
||||
} else {
|
||||
LargeTopAppBar(
|
||||
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||
actions = actions,
|
||||
scrollBehavior = scroll,
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = { AppNavBar(vm) },
|
||||
) { inner ->
|
||||
CenteredColumn {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = inner.calculateTopPadding(),
|
||||
bottom = inner.calculateBottomPadding() + 16.dp,
|
||||
),
|
||||
) { content() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,6 +240,7 @@ fun DetailScaffold(
|
|||
title: String,
|
||||
subtitle: String? = null,
|
||||
bottomBar: (@Composable () -> Unit)? = null,
|
||||
maxContentWidth: Dp = ContentMaxWidth,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
val scroll = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
|
@ -224,8 +271,9 @@ fun DetailScaffold(
|
|||
)
|
||||
},
|
||||
bottomBar = { bottomBar?.invoke() },
|
||||
content = content,
|
||||
)
|
||||
) { inner ->
|
||||
CenteredColumn(maxContentWidth) { content(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Feedback line plus the primary action, anchored at the bottom of an exercise. */
|
||||
|
|
@ -242,6 +290,8 @@ fun ActionBar(
|
|||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.widthIn(max = ActionMaxWidth)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
|
|
@ -276,6 +326,27 @@ fun SectionHeader(text: String) {
|
|||
)
|
||||
}
|
||||
|
||||
/** Number or short label in a tinted square, so rows line up whatever the label's width. */
|
||||
@Composable
|
||||
fun LeadingBadge(text: String) {
|
||||
Box(
|
||||
Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LeadingIcon(icon: ImageVector) {
|
||||
Box(
|
||||
Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(icon, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
/** One navigable row: done/pending/locked state on the left, chevron on the right. */
|
||||
@Composable
|
||||
fun NavRow(
|
||||
|
|
@ -284,15 +355,33 @@ fun NavRow(
|
|||
done: Boolean = false,
|
||||
locked: Boolean = false,
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
progress: Pair<Int, Int>? = 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
|
||||
val shownSubtitle = subtitle?.takeIf { it.isNotBlank() }
|
||||
val shownProgress = progress?.takeIf { it.second > 0 }
|
||||
ListItem(
|
||||
headlineContent = { Text(title, color = textColor) },
|
||||
supportingContent = subtitle?.takeIf { it.isNotBlank() }?.let {
|
||||
{ Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||
supportingContent = if (shownSubtitle == null && shownProgress == null) null else {
|
||||
{
|
||||
Column {
|
||||
shownSubtitle?.let { Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||
shownProgress?.let { (doneCount, total) ->
|
||||
Row(Modifier.padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
SmoothProgress(doneCount / total.toFloat(), Modifier.weight(1f).height(6.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(
|
||||
"$doneCount / $total",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
when {
|
||||
|
|
@ -328,19 +417,38 @@ fun SmoothProgress(
|
|||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.primary,
|
||||
trackColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
trackColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||
) {
|
||||
val animated by animateFloatAsState(
|
||||
targetValue = progress.coerceIn(0f, 1f),
|
||||
animationSpec = tween(durationMillis = 380),
|
||||
label = "progress",
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
progress = { animated },
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
)
|
||||
Box(modifier.heightIn(min = 4.dp).clip(CircleShape).background(trackColor)) {
|
||||
if (animated > 0f) {
|
||||
Box(Modifier.fillMaxHeight().fillMaxWidth(animated).clip(CircleShape).background(color))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Circular progress with a label in the middle. */
|
||||
@Composable
|
||||
fun ProgressRing(progress: Float, modifier: Modifier = Modifier, stroke: Dp = 10.dp, label: @Composable () -> Unit) {
|
||||
val animated by animateFloatAsState(progress.coerceIn(0f, 1f), tween(durationMillis = 380), label = "ring")
|
||||
val track = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
val fill = MaterialTheme.colorScheme.primary
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
val width = stroke.toPx()
|
||||
val inset = width / 2
|
||||
val arcSize = Size(size.width - width, size.height - width)
|
||||
drawArc(track, 0f, 360f, false, Offset(inset, inset), arcSize, style = Stroke(width))
|
||||
if (animated > 0f) {
|
||||
drawArc(fill, -90f, 360f * animated, false, Offset(inset, inset), arcSize, style = Stroke(width, cap = StrokeCap.Round))
|
||||
}
|
||||
}
|
||||
label()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.Architecture
|
||||
import androidx.compose.material.icons.filled.AutoAwesome
|
||||
import androidx.compose.material.icons.filled.AutoStories
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Eco
|
||||
import androidx.compose.material.icons.filled.Functions
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
|
|
@ -101,50 +104,61 @@ fun HomeScreen(vm: AppViewModel) {
|
|||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
) {
|
||||
Column(Modifier.padding(20.dp)) {
|
||||
Text(
|
||||
vm.tr("welcome_title"),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
vm.tr(vm.platform.welcomeKey),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SmoothProgress(
|
||||
progress = pct,
|
||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.18f),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||
)
|
||||
Row(Modifier.padding(20.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(vm.tr("welcome_title"), style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
vm.tr(vm.platform.welcomeKey),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
ProgressRing(pct, Modifier.size(104.dp)) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(vm.fmt("stats_pct_fmt", (pct * 100).toInt()), style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vm.update.canInstall || vm.update.status == "downloading" || vm.update.status == "staged") {
|
||||
item { UpdateBanner(vm) }
|
||||
}
|
||||
val open = vm.content.subjects.filter { it.bool("open") }
|
||||
val soon = vm.content.subjects.filter { !it.bool("open") }
|
||||
item { SectionHeader(vm.tr("subjects_title")) }
|
||||
items(vm.content.subjects) { s ->
|
||||
val open = s.bool("open")
|
||||
items(open) { s ->
|
||||
val part = subjectSum(vm, s)
|
||||
NavRow(
|
||||
title = vm.tr(s.str("key")),
|
||||
subtitle = if (open) null else vm.tr("stats_locked"),
|
||||
locked = !open,
|
||||
leading = { SubjectIcon(s.str("icon"), open) },
|
||||
onClick = if (open) ({ vm.goPage(s.str("target")) }) else null,
|
||||
leading = { SubjectIcon(s.str("icon"), true) },
|
||||
progress = part.doneEx to part.totalEx,
|
||||
onClick = { vm.goPage(s.str("target")) },
|
||||
)
|
||||
}
|
||||
if (soon.isNotEmpty()) {
|
||||
item { SectionHeader(vm.tr("subjects_soon")) }
|
||||
items(soon) { s ->
|
||||
NavRow(
|
||||
title = vm.tr(s.str("key")),
|
||||
locked = true,
|
||||
leading = { SubjectIcon(s.str("icon"), false) },
|
||||
onClick = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,14 +168,18 @@ private fun ChangelogCard(vm: AppViewModel) {
|
|||
val blocks = vm.content.changelog.take(3)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
) {
|
||||
Column(Modifier.padding(20.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Filled.AutoAwesome, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
vm.tr("changelog_title"),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
val date = blocks.firstOrNull()?.str("date").orEmpty()
|
||||
|
|
@ -169,7 +187,7 @@ private fun ChangelogCard(vm: AppViewModel) {
|
|||
Text(
|
||||
date,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.7f),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -180,7 +198,6 @@ private fun ChangelogCard(vm: AppViewModel) {
|
|||
Text(
|
||||
"• $line",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -293,6 +310,67 @@ private fun Metric(label: String, value: String, modifier: Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
/** Done and total exercises behind one row of a years screen, or null when there is no count. */
|
||||
fun routeProgress(vm: AppViewModel, r: Route): Pair<Int, Int>? {
|
||||
val c = vm.content
|
||||
val p = vm.progress
|
||||
fun tally(lessons: List<J>, done: (Int) -> Boolean) = lessons.count { done(it.int("id")) } to lessons.size
|
||||
return when (r) {
|
||||
Route.NetMap -> tally(c.netLessons, p::netDone)
|
||||
Route.Net2Map -> tally(c.itLessons("net2")) { p.courseDone("net2", it) }
|
||||
Route.Net3Map -> tally(c.itLessons("net3")) { p.courseDone("net3", it) }
|
||||
Route.Net4Map -> tally(c.itLessons("net4")) { p.courseDone("net4", it) }
|
||||
Route.HwMap -> tally(c.hw, p::hwDone)
|
||||
Route.Hw2Map -> tally(c.itLessons("hw2")) { p.courseDone("hw2", it) }
|
||||
Route.Hw3Map -> tally(c.itLessons("hw3")) { p.courseDone("hw3", it) }
|
||||
Route.Hw4Map -> tally(c.itLessons("hw4")) { p.courseDone("hw4", it) }
|
||||
Route.OnMap -> tally(c.on, p::onDone)
|
||||
Route.On2Map -> tally(c.on2, p::on2Done)
|
||||
Route.On3Map -> tally(c.on3, p::on3Done)
|
||||
Route.On4Map -> tally(c.on4, p::on4Done)
|
||||
is Route.EnMap -> tally(c.enYear(r.year)) { p.enDone(r.year, it) }
|
||||
is Route.DeMap -> tally(c.deYear(r.year)) { p.deDone(r.year, it) }
|
||||
Route.Roadmap -> {
|
||||
var done = 0
|
||||
var total = 0
|
||||
c.german.filter { it.bool("unlocked") }.forEach { u ->
|
||||
val n = u.strs("names").size
|
||||
total += n
|
||||
done += (1..n).count { p.germanDone(u.int("id"), it) }
|
||||
}
|
||||
done to total
|
||||
}
|
||||
Route.LitYears -> listOf(
|
||||
tally(c.lit, p::litDone), tally(c.lit2, p::lit2Done), tally(c.lit3, p::lit3Done), tally(c.lit4, p::lit4Done),
|
||||
).let { parts -> parts.sumOf { it.first } to parts.sumOf { it.second } }
|
||||
Route.LitMap -> tally(c.lit, p::litDone)
|
||||
Route.Lit2Map -> tally(c.lit2, p::lit2Done)
|
||||
Route.Lit3Map -> tally(c.lit3, p::lit3Done)
|
||||
Route.Lit4Map -> tally(c.lit4, p::lit4Done)
|
||||
Route.Mluvnice -> tally(c.mluvnice, p::mluvDone)
|
||||
Route.ChemMap -> tally(c.chem, p::chemDone)
|
||||
Route.BioMap -> tally(c.bio, p::bioDone)
|
||||
Route.Mat0Map -> tally(c.mat0, p::mat0Done)
|
||||
Route.MatMap -> tally(c.mat, p::matDone)
|
||||
Route.Mat2Map -> tally(c.mat2, p::mat2Done)
|
||||
Route.Mat3Map -> tally(c.mat3, p::mat3Done)
|
||||
Route.Mat4Map -> tally(c.mat4, p::mat4Done)
|
||||
Route.FyzMap -> tally(c.fyz, p::fyzDone)
|
||||
Route.Fyz2Map -> tally(c.fyz2, p::fyz2Done)
|
||||
Route.Fyz3Map -> tally(c.fyz3, p::fyz3Done)
|
||||
Route.Fyz4Map -> tally(c.fyz4, p::fyz4Done)
|
||||
Route.TgMap -> tally(c.tg, p::tgDone)
|
||||
Route.Tg2Map -> tally(c.tg2, p::tg2Done)
|
||||
Route.Tg3Map -> tally(c.tg3, p::tg3Done)
|
||||
Route.Tg4Map -> tally(c.tg4, p::tg4Done)
|
||||
Route.CsMap -> tally(c.cs, p::csDone)
|
||||
Route.Cs2Map -> tally(c.cs2, p::cs2Done)
|
||||
Route.Cs3Map -> tally(c.cs3, p::cs3Done)
|
||||
Route.Cs4Map -> tally(c.cs4, p::cs4Done)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
|
||||
val sum = ProgressSum()
|
||||
when (s.str("target")) {
|
||||
|
|
@ -801,10 +879,10 @@ fun NetYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("net_years_title"), vm.tr("net_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("net_year1"), subtitle = vm.tr("net_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.NetMap) }
|
||||
NavRow(title = vm.tr("net_year2"), subtitle = vm.tr("net2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net2Map) }
|
||||
NavRow(title = vm.tr("net_year3"), subtitle = vm.tr("net3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net3Map) }
|
||||
NavRow(title = vm.tr("net_year4"), subtitle = vm.tr("net4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Net4Map) }
|
||||
NavRow(title = vm.tr("net_year1"), subtitle = vm.tr("net_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.NetMap)) { vm.go(Route.NetMap) }
|
||||
NavRow(title = vm.tr("net_year2"), subtitle = vm.tr("net2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Net2Map)) { vm.go(Route.Net2Map) }
|
||||
NavRow(title = vm.tr("net_year3"), subtitle = vm.tr("net3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Net3Map)) { vm.go(Route.Net3Map) }
|
||||
NavRow(title = vm.tr("net_year4"), subtitle = vm.tr("net4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Net4Map)) { vm.go(Route.Net4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -815,10 +893,10 @@ fun HwYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Technické vybavení"), vm.tr("hw_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("hw_year1"), subtitle = vm.tr("hw_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.HwMap) }
|
||||
NavRow(title = vm.tr("hw_year2"), subtitle = vm.tr("hw2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw2Map) }
|
||||
NavRow(title = vm.tr("hw_year3"), subtitle = vm.tr("hw3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw3Map) }
|
||||
NavRow(title = vm.tr("hw_year4"), subtitle = vm.tr("hw4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Hw4Map) }
|
||||
NavRow(title = vm.tr("hw_year1"), subtitle = vm.tr("hw_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.HwMap)) { vm.go(Route.HwMap) }
|
||||
NavRow(title = vm.tr("hw_year2"), subtitle = vm.tr("hw2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Hw2Map)) { vm.go(Route.Hw2Map) }
|
||||
NavRow(title = vm.tr("hw_year3"), subtitle = vm.tr("hw3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Hw3Map)) { vm.go(Route.Hw3Map) }
|
||||
NavRow(title = vm.tr("hw_year4"), subtitle = vm.tr("hw4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Hw4Map)) { vm.go(Route.Hw4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -829,7 +907,7 @@ fun DeHomeScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Deutsch"), vm.tr("de_home_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("de_book_title"), subtitle = vm.tr("de_book_sub"), leading = { Text("U", style = MaterialTheme.typography.titleMedium) }) {
|
||||
NavRow(title = vm.tr("de_book_title"), subtitle = vm.tr("de_book_sub"), leading = { LeadingBadge("U") }, progress = routeProgress(vm, Route.Roadmap)) {
|
||||
vm.go(Route.Roadmap)
|
||||
}
|
||||
}
|
||||
|
|
@ -838,7 +916,8 @@ fun DeHomeScreen(vm: AppViewModel) {
|
|||
NavRow(
|
||||
title = vm.tr("de_year$year"),
|
||||
subtitle = vm.tr("de_y${year}_sub"),
|
||||
leading = { Text("$year.", style = MaterialTheme.typography.titleMedium) },
|
||||
leading = { LeadingBadge("$year") },
|
||||
progress = routeProgress(vm, Route.DeMap(year)),
|
||||
) { vm.go(Route.DeMap(year)) }
|
||||
}
|
||||
}
|
||||
|
|
@ -854,7 +933,8 @@ fun EnYearsScreen(vm: AppViewModel) {
|
|||
NavRow(
|
||||
title = vm.tr("en_year$year"),
|
||||
subtitle = vm.tr("en_y${year}_sub"),
|
||||
leading = { Text("$year.", style = MaterialTheme.typography.titleMedium) },
|
||||
leading = { LeadingBadge("$year") },
|
||||
progress = routeProgress(vm, Route.EnMap(year)),
|
||||
) { vm.go(Route.EnMap(year)) }
|
||||
}
|
||||
}
|
||||
|
|
@ -866,10 +946,10 @@ fun OnYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("on_year1"), subtitle = vm.tr("on_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.OnMap) }
|
||||
NavRow(title = vm.tr("on_year2"), subtitle = vm.tr("on2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On2Map) }
|
||||
NavRow(title = vm.tr("on_year3"), subtitle = vm.tr("on3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On3Map) }
|
||||
NavRow(title = vm.tr("on_year4"), subtitle = vm.tr("on4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.On4Map) }
|
||||
NavRow(title = vm.tr("on_year1"), subtitle = vm.tr("on_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.OnMap)) { vm.go(Route.OnMap) }
|
||||
NavRow(title = vm.tr("on_year2"), subtitle = vm.tr("on2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.On2Map)) { vm.go(Route.On2Map) }
|
||||
NavRow(title = vm.tr("on_year3"), subtitle = vm.tr("on3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.On3Map)) { vm.go(Route.On3Map) }
|
||||
NavRow(title = vm.tr("on_year4"), subtitle = vm.tr("on4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.On4Map)) { vm.go(Route.On4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -880,10 +960,10 @@ fun LitYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Literatura"), vm.tr("lit_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("lit_year1"), subtitle = vm.tr("lit_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.LitMap) }
|
||||
NavRow(title = vm.tr("lit_year2"), subtitle = vm.tr("lit2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Lit2Map) }
|
||||
NavRow(title = vm.tr("lit_year3"), subtitle = vm.tr("lit3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Lit3Map) }
|
||||
NavRow(title = vm.tr("lit_year4"), subtitle = vm.tr("lit4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Lit4Map) }
|
||||
NavRow(title = vm.tr("lit_year1"), subtitle = vm.tr("lit_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.LitMap)) { vm.go(Route.LitMap) }
|
||||
NavRow(title = vm.tr("lit_year2"), subtitle = vm.tr("lit2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Lit2Map)) { vm.go(Route.Lit2Map) }
|
||||
NavRow(title = vm.tr("lit_year3"), subtitle = vm.tr("lit3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Lit3Map)) { vm.go(Route.Lit3Map) }
|
||||
NavRow(title = vm.tr("lit_year4"), subtitle = vm.tr("lit4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Lit4Map)) { vm.go(Route.Lit4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -894,11 +974,11 @@ fun MatYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Matematika"), vm.tr("mat_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("mat0_year"), subtitle = vm.tr("mat0_sub"), leading = { Text("ZŠ", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat0Map) }
|
||||
NavRow(title = vm.tr("mat_year1"), subtitle = vm.tr("mat_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.MatMap) }
|
||||
NavRow(title = vm.tr("mat_year2"), subtitle = vm.tr("mat2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat2Map) }
|
||||
NavRow(title = vm.tr("mat_year3"), subtitle = vm.tr("mat3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat3Map) }
|
||||
NavRow(title = vm.tr("mat_year4"), subtitle = vm.tr("mat4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat4Map) }
|
||||
NavRow(title = vm.tr("mat0_year"), subtitle = vm.tr("mat0_sub"), leading = { LeadingBadge("ZŠ") }, progress = routeProgress(vm, Route.Mat0Map)) { vm.go(Route.Mat0Map) }
|
||||
NavRow(title = vm.tr("mat_year1"), subtitle = vm.tr("mat_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.MatMap)) { vm.go(Route.MatMap) }
|
||||
NavRow(title = vm.tr("mat_year2"), subtitle = vm.tr("mat2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Mat2Map)) { vm.go(Route.Mat2Map) }
|
||||
NavRow(title = vm.tr("mat_year3"), subtitle = vm.tr("mat3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Mat3Map)) { vm.go(Route.Mat3Map) }
|
||||
NavRow(title = vm.tr("mat_year4"), subtitle = vm.tr("mat4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Mat4Map)) { vm.go(Route.Mat4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -909,10 +989,10 @@ fun CsYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Programování"), vm.tr("cs_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("cs_year1"), subtitle = vm.tr("cs_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.CsMap) }
|
||||
NavRow(title = vm.tr("cs_year2"), subtitle = vm.tr("cs2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Cs2Map) }
|
||||
NavRow(title = vm.tr("cs_year3"), subtitle = vm.tr("cs3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Cs3Map) }
|
||||
NavRow(title = vm.tr("cs_year4"), subtitle = vm.tr("cs4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Cs4Map) }
|
||||
NavRow(title = vm.tr("cs_year1"), subtitle = vm.tr("cs_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.CsMap)) { vm.go(Route.CsMap) }
|
||||
NavRow(title = vm.tr("cs_year2"), subtitle = vm.tr("cs2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Cs2Map)) { vm.go(Route.Cs2Map) }
|
||||
NavRow(title = vm.tr("cs_year3"), subtitle = vm.tr("cs3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Cs3Map)) { vm.go(Route.Cs3Map) }
|
||||
NavRow(title = vm.tr("cs_year4"), subtitle = vm.tr("cs4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Cs4Map)) { vm.go(Route.Cs4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -923,10 +1003,10 @@ fun TgYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Technická grafika"), vm.tr("tg_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("tg_year1"), subtitle = vm.tr("tg_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.TgMap) }
|
||||
NavRow(title = vm.tr("tg_year2"), subtitle = vm.tr("tg2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Tg2Map) }
|
||||
NavRow(title = vm.tr("tg_year3"), subtitle = vm.tr("tg3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Tg3Map) }
|
||||
NavRow(title = vm.tr("tg_year4"), subtitle = vm.tr("tg4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Tg4Map) }
|
||||
NavRow(title = vm.tr("tg_year1"), subtitle = vm.tr("tg_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.TgMap)) { vm.go(Route.TgMap) }
|
||||
NavRow(title = vm.tr("tg_year2"), subtitle = vm.tr("tg2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Tg2Map)) { vm.go(Route.Tg2Map) }
|
||||
NavRow(title = vm.tr("tg_year3"), subtitle = vm.tr("tg3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Tg3Map)) { vm.go(Route.Tg3Map) }
|
||||
NavRow(title = vm.tr("tg_year4"), subtitle = vm.tr("tg4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Tg4Map)) { vm.go(Route.Tg4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -937,10 +1017,10 @@ fun FyzYearsScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Fyzika"), vm.tr("fyz_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("fyz_year1"), subtitle = vm.tr("fyz_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.FyzMap) }
|
||||
NavRow(title = vm.tr("fyz_year2"), subtitle = vm.tr("fyz2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Fyz2Map) }
|
||||
NavRow(title = vm.tr("fyz_year3"), subtitle = vm.tr("fyz3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Fyz3Map) }
|
||||
NavRow(title = vm.tr("fyz_year4"), subtitle = vm.tr("fyz4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Fyz4Map) }
|
||||
NavRow(title = vm.tr("fyz_year1"), subtitle = vm.tr("fyz_sub"), leading = { LeadingBadge("1") }, progress = routeProgress(vm, Route.FyzMap)) { vm.go(Route.FyzMap) }
|
||||
NavRow(title = vm.tr("fyz_year2"), subtitle = vm.tr("fyz2_sub"), leading = { LeadingBadge("2") }, progress = routeProgress(vm, Route.Fyz2Map)) { vm.go(Route.Fyz2Map) }
|
||||
NavRow(title = vm.tr("fyz_year3"), subtitle = vm.tr("fyz3_sub"), leading = { LeadingBadge("3") }, progress = routeProgress(vm, Route.Fyz3Map)) { vm.go(Route.Fyz3Map) }
|
||||
NavRow(title = vm.tr("fyz_year4"), subtitle = vm.tr("fyz4_sub"), leading = { LeadingBadge("4") }, progress = routeProgress(vm, Route.Fyz4Map)) { vm.go(Route.Fyz4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -954,11 +1034,17 @@ fun SciMapScreen(vm: AppViewModel) {
|
|||
NavRow(
|
||||
title = vm.tr("Chemie"),
|
||||
subtitle = vm.tr("chem_sub"),
|
||||
leading = { Icon(Icons.Filled.Science, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
leading = { LeadingIcon(Icons.Filled.Science) },
|
||||
progress = routeProgress(vm, Route.ChemMap),
|
||||
) { vm.go(Route.ChemMap) }
|
||||
}
|
||||
item {
|
||||
NavRow(title = vm.tr("Biologie"), subtitle = vm.tr("bio_sub")) { vm.go(Route.BioMap) }
|
||||
NavRow(
|
||||
title = vm.tr("Biologie"),
|
||||
subtitle = vm.tr("bio_sub"),
|
||||
leading = { LeadingIcon(Icons.Filled.Eco) },
|
||||
progress = routeProgress(vm, Route.BioMap),
|
||||
) { vm.go(Route.BioMap) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -969,20 +1055,26 @@ fun CzechMapScreen(vm: AppViewModel) {
|
|||
DetailScaffold(vm, vm.tr("Český jazyk a literatura"), vm.tr("czech_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("Literatura"), subtitle = vm.tr("lit_years_sub")) { vm.go(Route.LitYears) }
|
||||
NavRow(
|
||||
title = vm.tr("Literatura"),
|
||||
subtitle = vm.tr("lit_years_sub"),
|
||||
leading = { LeadingIcon(Icons.Filled.AutoStories) },
|
||||
progress = routeProgress(vm, Route.LitYears),
|
||||
) { vm.go(Route.LitYears) }
|
||||
}
|
||||
item {
|
||||
NavRow(
|
||||
title = vm.tr("Mluvnice"),
|
||||
subtitle = vm.tr("mluv_sub"),
|
||||
leading = { Icon(Icons.Filled.FormatListNumbered, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
leading = { LeadingIcon(Icons.Filled.FormatListNumbered) },
|
||||
progress = routeProgress(vm, Route.Mluvnice),
|
||||
) { vm.go(Route.Mluvnice) }
|
||||
}
|
||||
item {
|
||||
NavRow(
|
||||
title = vm.tr("Maturitní četba"),
|
||||
subtitle = vm.tr("reading_sub"),
|
||||
leading = { Icon(Icons.AutoMirrored.Filled.MenuBook, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
leading = { LeadingIcon(Icons.AutoMirrored.Filled.MenuBook) },
|
||||
) { vm.go(Route.ReadingList) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1045,6 +1137,52 @@ fun BookScreen(vm: AppViewModel, id: String) {
|
|||
// ---------------------------------------------------------------------------
|
||||
// Lesson slides
|
||||
|
||||
/** One explanation slide: label, title, tip and numbered points. */
|
||||
@Composable
|
||||
fun SlideContent(slide: J) {
|
||||
Column {
|
||||
Text(slide.str("kicker"), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(slide.str("title"), style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 4.dp, bottom = 12.dp))
|
||||
slide.strOrNull("tip")?.let { tip ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
|
||||
) {
|
||||
Row(Modifier.padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Outlined.Lightbulb, null, tint = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(tip, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
slide.strs("lines").forEachIndexed { index, line ->
|
||||
val numbered = Regex("""^(\d+)\.\s+(.*)$""").find(line)
|
||||
val badge = numbered?.groupValues?.get(1) ?: "${index + 1}"
|
||||
val text = numbered?.groupValues?.get(2) ?: line
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 5.dp)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f), androidx.compose.foundation.shape.RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(28.dp)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(badge, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SlidesScreen(
|
||||
|
|
@ -1095,52 +1233,13 @@ fun SlidesScreen(
|
|||
},
|
||||
) { inner ->
|
||||
HorizontalPager(state = pager, modifier = Modifier.fillMaxSize().padding(inner)) { page ->
|
||||
val slide = slides[page]
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
Text(slide.str("kicker"), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(slide.str("title"), style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 4.dp, bottom = 12.dp))
|
||||
slide.strOrNull("tip")?.let { tip ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
|
||||
) {
|
||||
Row(Modifier.padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Outlined.Lightbulb, null, tint = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(tip, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
slide.strs("lines").forEachIndexed { index, line ->
|
||||
val numbered = Regex("""^(\d+)\.\s+(.*)$""").find(line)
|
||||
val badge = numbered?.groupValues?.get(1) ?: "${index + 1}"
|
||||
val text = numbered?.groupValues?.get(2) ?: line
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 5.dp)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f), androidx.compose.foundation.shape.RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(28.dp)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(badge, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
SlideContent(slides[page])
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue