Add a C# course with a side-by-side code runner.

Each lesson explains a topic and runs the edited program next to the text through the local .NET SDK.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-11 14:37:14 +02:00
commit 333085ee53
86 changed files with 2482 additions and 5 deletions

View file

@ -0,0 +1,128 @@
package org.maturita.maturita.desktop
import org.maturita.maturita.platform.CSharpRun
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
/** One cached console project. The student's program replaces Program.cs and `dotnet run` executes it. */
object CSharpLab {
private val gate = Object()
fun run(source: String, stdin: String): CSharpRun {
if (source.length > 100_000) return CSharpRun("fail", "Program je příliš dlouhý.")
val dotnet = findDotnet() ?: return CSharpRun("unavailable", "")
synchronized(gate) {
return try {
execute(dotnet, source, stdin)
} catch (e: Exception) {
CSharpRun("fail", e.message ?: "Spuštění selhalo.")
}
}
}
private fun execute(dotnet: String, source: String, stdin: String): CSharpRun {
val dir = File(System.getProperty("user.home"), ".graduately/csharp-lab")
dir.mkdirs()
val csproj = File(dir, "Lab.csproj")
if (!csproj.isFile) {
csproj.writeText(projectFile(targetFramework(dotnet)), StandardCharsets.UTF_8)
val restored = command(dotnet, listOf("restore", dir.absolutePath), dir, "", 90)
if (restored.status != "ok") return restored
}
File(dir, "Program.cs").writeText(source, StandardCharsets.UTF_8)
val args = mutableListOf("run", "--project", dir.absolutePath, "--verbosity", "quiet")
if (File(dir, "obj").isDirectory) args += "--no-restore"
return command(dotnet, args, dir, stdin, 20)
}
private fun command(dotnet: String, args: List<String>, dir: File, stdin: String, seconds: Long): CSharpRun {
val process = ProcessBuilder(listOf(dotnet) + args)
.directory(dir)
.redirectErrorStream(true)
.apply {
environment().clear()
environment().putAll(environmentFor(dotnet))
}
.start()
process.outputStream.use { it.write(stdin.toByteArray(StandardCharsets.UTF_8)) }
val output = ByteArrayOutputStream()
val reader = Thread {
process.inputStream.use { it.copyTo(output) }
}
reader.isDaemon = true
reader.start()
val finished = process.waitFor(seconds, TimeUnit.SECONDS)
if (!finished) {
process.descendants().forEach { it.destroyForcibly() }
process.destroyForcibly()
process.waitFor(2, TimeUnit.SECONDS)
reader.join(1000)
return CSharpRun("timeout", clip(output.toString(StandardCharsets.UTF_8)))
}
reader.join(2000)
val text = clip(output.toString(StandardCharsets.UTF_8)).trim()
return if (process.exitValue() == 0) CSharpRun("ok", text) else CSharpRun("fail", text)
}
private fun clip(text: String): String =
if (text.length <= 16_000) text else text.take(16_000) + "\n…"
private fun environmentFor(dotnet: String): Map<String, String> {
val home = System.getProperty("user.home")
val root = File(dotnet).parent
val path = listOf(root, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin")
.joinToString(File.pathSeparator)
val env = mutableMapOf(
"PATH" to path,
"HOME" to home,
"USERPROFILE" to home,
"DOTNET_ROOT" to root,
"DOTNET_CLI_TELEMETRY_OPTOUT" to "1",
"DOTNET_NOLOGO" to "1",
"DOTNET_SKIP_FIRST_TIME_EXPERIENCE" to "1",
"TEMP" to System.getProperty("java.io.tmpdir"),
"TMP" to System.getProperty("java.io.tmpdir"),
)
System.getenv("SystemRoot")?.let { env["SystemRoot"] = it }
System.getenv("ComSpec")?.let { env["ComSpec"] = it }
return env
}
private fun targetFramework(dotnet: String): String {
val version = command(dotnet, listOf("--version"), File(System.getProperty("java.io.tmpdir")), "", 20)
val major = version.output.trim().substringBefore('.').filter { it.isDigit() }
return if (major.isEmpty()) "net8.0" else "net$major.0"
}
private fun projectFile(framework: String) = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$framework</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
""".trimIndent()
private fun findDotnet(): String? {
val home = System.getProperty("user.home")
val programFiles = System.getenv("ProgramFiles") ?: "C:\\Program Files"
val names = listOf(
"/usr/local/share/dotnet/dotnet",
"/usr/share/dotnet/dotnet",
"/opt/homebrew/share/dotnet/dotnet",
"$home/.dotnet/dotnet",
"$programFiles\\dotnet\\dotnet.exe",
)
names.firstOrNull { File(it).canExecute() }?.let { return it }
val path = System.getenv("PATH") ?: return null
return path.split(File.pathSeparator)
.map { File(it, if (System.getProperty("os.name").startsWith("Windows")) "dotnet.exe" else "dotnet") }
.firstOrNull { it.canExecute() }
?.absolutePath
}
}

View file

@ -1,8 +1,10 @@
package org.maturita.maturita.desktop
import org.maturita.maturita.platform.CSharpRun
import org.maturita.maturita.platform.Platform
import org.maturita.maturita.platform.Prefs
import org.maturita.maturita.platform.Speaker
import javax.swing.SwingUtilities
class DesktopPlatform(
override val commit: String,
@ -14,4 +16,11 @@ class DesktopPlatform(
override val updater = DesktopUpdater(commit, install)
override fun speaker(): Speaker? = DesktopSpeaker.create()
override fun runCSharp(source: String, stdin: String, cb: (CSharpRun) -> Unit) {
Thread {
val result = CSharpLab.run(source, stdin)
SwingUtilities.invokeLater { cb(result) }
}.apply { isDaemon = true }.start()
}
}

View file

@ -23,6 +23,8 @@ import org.maturita.maturita.data.ColorMode
import org.maturita.maturita.data.Palette
import org.maturita.maturita.ui.BookListScreen
import org.maturita.maturita.ui.BookScreen
import org.maturita.maturita.ui.CodeLessonScreen
import org.maturita.maturita.ui.CsYearsScreen
import org.maturita.maturita.ui.CzechMapScreen
import org.maturita.maturita.ui.GermanExercise
import org.maturita.maturita.ui.HomeScreen
@ -584,6 +586,51 @@ private fun MaturitaAppBody(vm: AppViewModel) {
}
}
is Route.Tg4Ex -> TgQuizScreen(vm, 4, r.id)
Route.CsYears -> CsYearsScreen(vm)
Route.CsMap -> LessonListScreen(
vm, vm.tr("cs_year1"), vm.tr("cs_sub"),
vm.content.cs.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.csDone(id), Route.CsLesson(id))
},
)
is Route.CsLesson -> {
val l = vm.content.csLesson(r.id)
if (l != null) CodeLessonScreen(vm, l) { vm.markCs(r.id) }
}
Route.Cs2Map -> LessonListScreen(
vm, vm.tr("cs_year2"), vm.tr("cs2_sub"),
vm.content.cs2.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.cs2Done(id), Route.Cs2Lesson(id))
},
)
is Route.Cs2Lesson -> {
val l = vm.content.cs2Lesson(r.id)
if (l != null) CodeLessonScreen(vm, l) { vm.markCs2(r.id) }
}
Route.Cs3Map -> LessonListScreen(
vm, vm.tr("cs_year3"), vm.tr("cs3_sub"),
vm.content.cs3.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.cs3Done(id), Route.Cs3Lesson(id))
},
)
is Route.Cs3Lesson -> {
val l = vm.content.cs3Lesson(r.id)
if (l != null) CodeLessonScreen(vm, l) { vm.markCs3(r.id) }
}
Route.Cs4Map -> LessonListScreen(
vm, vm.tr("cs_year4"), vm.tr("cs4_sub"),
vm.content.cs4.map { l ->
val id = l.int("id")
LessonRow(vm.tr(l.str("titleKey")), vm.progress.cs4Done(id), Route.Cs4Lesson(id))
},
)
is Route.Cs4Lesson -> {
val l = vm.content.cs4Lesson(r.id)
if (l != null) CodeLessonScreen(vm, l) { vm.markCs4(r.id) }
}
Route.SciMap -> SciMapScreen(vm)
Route.ChemMap -> LessonListScreen(
vm, vm.tr("Chemie"), vm.tr("chem_sub"),

View file

@ -262,6 +262,26 @@ class AppViewModel(val content: Content, val platform: Platform) {
refresh()
}
fun markCs(id: Int) {
progress.markCs(id)
refresh()
}
fun markCs2(id: Int) {
progress.markCs2(id)
refresh()
}
fun markCs3(id: Int) {
progress.markCs3(id)
refresh()
}
fun markCs4(id: Int) {
progress.markCs4(id)
refresh()
}
fun markMluv(n: Int) {
progress.markMluv(n)
refresh()

View file

@ -118,6 +118,15 @@ sealed class Route(val page: String) {
data object Tg4Map : Route("tg4map")
data class Tg4Lesson(val id: Int) : Route("tg4unit$id")
data class Tg4Ex(val id: Int) : Route("tg4ex$id")
data object CsYears : Route("csyears")
data object CsMap : Route("csmap")
data class CsLesson(val id: Int) : Route("csunit$id")
data object Cs2Map : Route("cs2map")
data class Cs2Lesson(val id: Int) : Route("cs2unit$id")
data object Cs3Map : Route("cs3map")
data class Cs3Lesson(val id: Int) : Route("cs3unit$id")
data object Cs4Map : Route("cs4map")
data class Cs4Lesson(val id: Int) : Route("cs4unit$id")
data object CzechMap : Route("czechmap")
data object Mluvnice : Route("mluvnice")
data class MluvEx(val n: Int) : Route("mluve$n")
@ -263,6 +272,15 @@ sealed class Route(val page: String) {
Tg4Map -> listOf(Subjects, TgYears, Tg4Map)
is Tg4Lesson -> listOf(Subjects, TgYears, Tg4Map, this)
is Tg4Ex -> listOf(Subjects, TgYears, Tg4Map, Tg4Lesson(id), this)
CsYears -> listOf(Subjects, CsYears)
CsMap -> listOf(Subjects, CsYears, CsMap)
is CsLesson -> listOf(Subjects, CsYears, CsMap, this)
Cs2Map -> listOf(Subjects, CsYears, Cs2Map)
is Cs2Lesson -> listOf(Subjects, CsYears, Cs2Map, this)
Cs3Map -> listOf(Subjects, CsYears, Cs3Map)
is Cs3Lesson -> listOf(Subjects, CsYears, Cs3Map, this)
Cs4Map -> listOf(Subjects, CsYears, Cs4Map)
is Cs4Lesson -> listOf(Subjects, CsYears, Cs4Map, this)
CzechMap -> listOf(Subjects, CzechMap)
Mluvnice -> listOf(Subjects, CzechMap, Mluvnice)
is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this)
@ -320,6 +338,11 @@ fun routeFromPage(page: String): Route? = when (page) {
"tg2map" -> Route.Tg2Map
"tg3map" -> Route.Tg3Map
"tg4map" -> Route.Tg4Map
"csyears" -> Route.CsYears
"csmap" -> Route.CsMap
"cs2map" -> Route.Cs2Map
"cs3map" -> Route.Cs3Map
"cs4map" -> Route.Cs4Map
"czechmap" -> Route.CzechMap
"mluvnice" -> Route.Mluvnice
"readinglist" -> Route.ReadingList
@ -441,6 +464,18 @@ fun routeFromPage(page: String): Route? = when (page) {
Regex("""tgex(\d+)""").matchEntire(page)?.let {
return Route.TgEx(it.groupValues[1].toInt())
}
Regex("""cs4unit(\d+)""").matchEntire(page)?.let {
return Route.Cs4Lesson(it.groupValues[1].toInt())
}
Regex("""cs3unit(\d+)""").matchEntire(page)?.let {
return Route.Cs3Lesson(it.groupValues[1].toInt())
}
Regex("""cs2unit(\d+)""").matchEntire(page)?.let {
return Route.Cs2Lesson(it.groupValues[1].toInt())
}
Regex("""csunit(\d+)""").matchEntire(page)?.let {
return Route.CsLesson(it.groupValues[1].toInt())
}
Regex("""lit4unit(\d+)""").matchEntire(page)?.let {
return Route.Lit4Lesson(it.groupValues[1].toInt())
}

View file

@ -60,6 +60,10 @@ class Content(root: JSONObject) {
val tg2 = raw.arr("tg2")
val tg3 = raw.arr("tg3")
val tg4 = raw.arr("tg4")
val cs = raw.arr("cs")
val cs2 = raw.arr("cs2")
val cs3 = raw.arr("cs3")
val cs4 = raw.arr("cs4")
val mat0 = raw.arr("mat0")
val mat = raw.arr("mat")
val mat2 = raw.arr("mat2")
@ -116,6 +120,10 @@ class Content(root: JSONObject) {
fun tg2Lesson(id: Int) = tg2.firstOrNull { it.int("id") == id }
fun tg3Lesson(id: Int) = tg3.firstOrNull { it.int("id") == id }
fun tg4Lesson(id: Int) = tg4.firstOrNull { it.int("id") == id }
fun csLesson(id: Int) = cs.firstOrNull { it.int("id") == id }
fun cs2Lesson(id: Int) = cs2.firstOrNull { it.int("id") == id }
fun cs3Lesson(id: Int) = cs3.firstOrNull { it.int("id") == id }
fun cs4Lesson(id: Int) = cs4.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 }

View file

@ -99,6 +99,14 @@ class ProgressStore(private val prefs: Prefs, content: Content) {
fun markTg3(id: Int) { mark("tg3.$id") }
fun tg4Done(id: Int) = done("tg4.$id")
fun markTg4(id: Int) { mark("tg4.$id") }
fun csDone(id: Int) = done("cs.$id")
fun markCs(id: Int) { mark("cs.$id") }
fun cs2Done(id: Int) = done("cs2.$id")
fun markCs2(id: Int) { mark("cs2.$id") }
fun cs3Done(id: Int) = done("cs3.$id")
fun markCs3(id: Int) { mark("cs3.$id") }
fun cs4Done(id: Int) = done("cs4.$id")
fun markCs4(id: Int) { mark("cs4.$id") }
fun mat0Done(id: Int) = done("mat0.$id")
fun markMat0(id: Int) { mark("mat0.$id") }
fun matDone(id: Int) = done("mat.$id")
@ -320,6 +328,38 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1
}
}
content.cs.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.csDone(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.cs2.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.cs2Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.cs3.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.cs3Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.cs4.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1
if (p.cs4Done(l.int("id"))) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
content.mat0.forEach { l ->
sum.totalEx += 1
sum.openUnits += 1

View file

@ -18,8 +18,18 @@ interface Platform {
/** A speech engine for listening exercises, or null when there is none. */
fun speaker(): Speaker?
/**
* Compiles and runs a C# program. [cb] runs on the UI thread.
* Status is ok, fail, timeout or unavailable.
*/
fun runCSharp(source: String, stdin: String, cb: (CSharpRun) -> Unit) {
cb(CSharpRun("unavailable", ""))
}
}
data class CSharpRun(val status: String, val output: String)
/** The subset of Android's SharedPreferences the app uses. */
interface Prefs {
fun getBoolean(key: String, fallback: Boolean): Boolean

View file

@ -0,0 +1,158 @@
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.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.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
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.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.maturita.maturita.AppViewModel
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 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) }
fun run() {
if (running) return
running = true
output = vm.tr("cs_running")
vm.platform.runCSharp(code, stdin) { result ->
running = false
output = when (result.status) {
"unavailable" -> vm.tr("cs_unavailable")
"timeout" -> (result.output + "\n\n" + vm.tr("cs_timeout")).trim()
else -> result.output.ifEmpty { vm.tr("cs_empty") }
}
if (result.status == "ok") onSuccess()
}
}
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())
}
} 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())
}
}
}
}
}
@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)
}
}
}
}
@Composable
private fun EditorColumn(
vm: AppViewModel,
code: String,
onCode: (String) -> Unit,
stdin: String,
onStdin: (String) -> Unit,
expand: 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()),
)
Text(vm.tr("cs_stdin"), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
BasicTextField(
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),
)
}
}
@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")) }
}
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)

View file

@ -36,6 +36,7 @@ import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Quiz
import androidx.compose.material.icons.filled.Science
import androidx.compose.material.icons.filled.Terminal
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.Lightbulb
import androidx.compose.material3.Card
@ -227,6 +228,7 @@ private fun SubjectIcon(icon: String, open: Boolean) {
"bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp))
"function" -> Icon(Icons.Filled.Functions, null, tint = tint, modifier = Modifier.size(26.dp))
"ruler" -> Icon(Icons.Filled.Architecture, null, tint = tint, modifier = Modifier.size(26.dp))
"code" -> Icon(Icons.Filled.Terminal, null, tint = tint, modifier = Modifier.size(26.dp))
else -> Icon(Icons.Filled.Lock, null, tint = tint)
}
}
@ -350,6 +352,13 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
vm.content.fyz3.count { vm.progress.fyz3Done(it.int("id")) } +
vm.content.fyz4.count { vm.progress.fyz4Done(it.int("id")) }
}
"csyears", "csmap", "cs2map", "cs3map", "cs4map" -> {
sum.totalEx = vm.content.cs.size + vm.content.cs2.size + vm.content.cs3.size + vm.content.cs4.size
sum.doneEx = vm.content.cs.count { vm.progress.csDone(it.int("id")) } +
vm.content.cs2.count { vm.progress.cs2Done(it.int("id")) } +
vm.content.cs3.count { vm.progress.cs3Done(it.int("id")) } +
vm.content.cs4.count { vm.progress.cs4Done(it.int("id")) }
}
"tgyears", "tgmap", "tg2map", "tg3map", "tg4map" -> {
sum.totalEx = vm.content.tg.size + vm.content.tg2.size + vm.content.tg3.size + vm.content.tg4.size
sum.doneEx = vm.content.tg.count { vm.progress.tgDone(it.int("id")) } +
@ -679,6 +688,23 @@ private fun buildSearch(vm: AppViewModel): List<Hit> {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg4unit${l.int("id")}", extra = "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg4ex${l.int("id")}")
}
add(vm.tr("Programování"), vm.tr("search_lesson"), "csyears", extra = "csharp c#")
add(vm.tr("cs_year1"), vm.tr("search_lesson"), "csmap", extra = "rocnik")
add(vm.tr("cs_year2"), vm.tr("search_lesson"), "cs2map", extra = "rocnik")
add(vm.tr("cs_year3"), vm.tr("search_lesson"), "cs3map", extra = "rocnik")
add(vm.tr("cs_year4"), vm.tr("search_lesson"), "cs4map", extra = "rocnik")
vm.content.cs.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "csunit${l.int("id")}", extra = "csharp")
}
vm.content.cs2.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs2unit${l.int("id")}", extra = "csharp")
}
vm.content.cs3.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs3unit${l.int("id")}", extra = "csharp")
}
vm.content.cs4.forEach { l ->
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs4unit${l.int("id")}", extra = "csharp")
}
add(vm.tr("Chemie"), vm.tr("search_lesson"), "chemmap", extra = "chemie")
add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra = "biologie")
vm.content.chem.forEach { l ->
@ -878,6 +904,20 @@ fun MatYearsScreen(vm: AppViewModel) {
}
}
@Composable
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) }
}
}
}
}
@Composable
fun TgYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Technická grafika"), vm.tr("tg_years_sub")) { inner ->