mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 07:04:35 +00:00
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:
parent
6662eb185e
commit
333085ee53
86 changed files with 2482 additions and 5 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
158
android/shared/src/org/maturita/maturita/ui/CodeLesson.kt
Normal file
158
android/shared/src/org/maturita/maturita/ui/CodeLesson.kt
Normal 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)
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue