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

@ -73,6 +73,8 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
ročníky 2–4 po 8 lekcích (monitory, sestavení, RAID, servery, údržba) ročníky 2–4 po 8 lekcích (monitory, sestavení, RAID, servery, údržba)
- **Technická grafika** — 4 ročníky AutoCADu, v každém 8 lekcí: - **Technická grafika** — 4 ročníky AutoCADu, v každém 8 lekcí:
výkres a souřadnice, úpravy a kóty, pohledy a tisk, 3D a maturitní zadání výkres a souřadnice, úpravy a kóty, pohledy a tisk, 3D a maturitní zadání
- **Programování** — 4 ročníky C# po vzoru W3Schools. U každé lekce se kód
píše a spouští vedle výkladu, od výpisu po třídy, LINQ a async
- **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov - **Český jazyk a literatura** — literatura (4 ročníky, 32 lekcí podle osnov
středních škol), mluvnice (20 cvičení) a maturitní četba středních škol), mluvnice (20 cvičení) a maturitní četba
(82 knih: zápisky, kvíz, sestavení děje) (82 knih: zápisky, kvíz, sestavení děje)
@ -92,7 +94,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
### Použití ### Použití
1. **Úvod** ukáže postup a mřížku předmětů. Otevřený předmět spustí cestu. Zamčené dlaždice zatím nic neotevřou: prezentační grafika, programování a programové vybavení. 1. **Úvod** ukáže postup a mřížku předmětů. Otevřený předmět spustí cestu. Zamčené dlaždice zatím nic neotevřou: prezentační grafika a programové vybavení.
2. U němčiny je učebnice (Neue Freunde, Aus aller Welt, Bei uns zu Hause) a čtyři ročníky. Ostatní otevřené předměty mají ročníky, lekce nebo knihy. 2. U němčiny je učebnice (Neue Freunde, Aus aller Welt, Bei uns zu Hause) a čtyři ročníky. Ostatní otevřené předměty mají ročníky, lekce nebo knihy.
3. Z cesty se otevře výklad, poslech, čtení, psaní nebo kvíz. 3. Z cesty se otevře výklad, poslech, čtení, psaní nebo kvíz.
4. **Statistiky**, **Nastavení** a **Hledat** jsou v postranním panelu na Macu. Na telefonu a v desktopové aplikaci jsou na záložkách, hledání navíc ikonou lupy. Zkratka je `Cmd/Ctrl+K`. 4. **Statistiky**, **Nastavení** a **Hledat** jsou v postranním panelu na Macu. Na telefonu a v desktopové aplikaci jsou na záložkách, hledání navíc ikonou lupy. Zkratka je `Cmd/Ctrl+K`.
@ -263,6 +265,8 @@ Intel Mac, another architecture, or hacking on the code →
years 2–4 have 8 lessons each (displays, assembly, RAID, servers, maintenance) years 2–4 have 8 lessons each (displays, assembly, RAID, servers, maintenance)
- **Technical graphics** — 4 years of AutoCAD, 8 lessons each: - **Technical graphics** — 4 years of AutoCAD, 8 lessons each:
the sheet and coordinates, editing and dimensions, views and plotting, 3D and the exam drawing the sheet and coordinates, editing and dimensions, views and plotting, 3D and the exam drawing
- **Programming** — 4 years of C# in the style of W3Schools. Each lesson has
code you edit and run beside the explanation, from output to classes, LINQ and async
- **Czech language** — literature (4 years, 32 lessons from the secondary-school - **Czech language** — literature (4 years, 32 lessons from the secondary-school
syllabus), grammar (20 exercises) and required reading syllabus), grammar (20 exercises) and required reading
(82 books: notes, quiz, plot ordering) (82 books: notes, quiz, plot ordering)
@ -283,7 +287,7 @@ Intel Mac, another architecture, or hacking on the code →
### Usage ### Usage
1. **Welcome** shows progress and the subject grid. An open subject starts its path. Locked tiles do nothing yet: presentation graphics, programming and software. 1. **Welcome** shows progress and the subject grid. An open subject starts its path. Locked tiles do nothing yet: presentation graphics and software.
2. German has the textbook (Neue Freunde, Aus aller Welt, Bei uns zu Hause) and four years. The other open subjects have years, lessons or books. 2. German has the textbook (Neue Freunde, Aus aller Welt, Bei uns zu Hause) and four years. The other open subjects have years, lessons or books.
3. A path opens an overview, listening, reading, writing or a quiz. 3. A path opens an overview, listening, reading, writing or a quiz.
4. **Statistics**, **Settings** and **Search** sit in the sidebar on Mac. On the phone and the desktop app they are tabs, with search also on the magnifier. The shortcut is `Cmd/Ctrl+K`. 4. **Statistics**, **Settings** and **Search** sit in the sidebar on Mac. On the phone and the desktop app they are tabs, with search also on the magnifier. The shortcut is `Cmd/Ctrl+K`.

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 package org.maturita.maturita.desktop
import org.maturita.maturita.platform.CSharpRun
import org.maturita.maturita.platform.Platform import org.maturita.maturita.platform.Platform
import org.maturita.maturita.platform.Prefs import org.maturita.maturita.platform.Prefs
import org.maturita.maturita.platform.Speaker import org.maturita.maturita.platform.Speaker
import javax.swing.SwingUtilities
class DesktopPlatform( class DesktopPlatform(
override val commit: String, override val commit: String,
@ -14,4 +16,11 @@ class DesktopPlatform(
override val updater = DesktopUpdater(commit, install) override val updater = DesktopUpdater(commit, install)
override fun speaker(): Speaker? = DesktopSpeaker.create() 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.data.Palette
import org.maturita.maturita.ui.BookListScreen import org.maturita.maturita.ui.BookListScreen
import org.maturita.maturita.ui.BookScreen 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.CzechMapScreen
import org.maturita.maturita.ui.GermanExercise import org.maturita.maturita.ui.GermanExercise
import org.maturita.maturita.ui.HomeScreen import org.maturita.maturita.ui.HomeScreen
@ -584,6 +586,51 @@ private fun MaturitaAppBody(vm: AppViewModel) {
} }
} }
is Route.Tg4Ex -> TgQuizScreen(vm, 4, r.id) 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.SciMap -> SciMapScreen(vm)
Route.ChemMap -> LessonListScreen( Route.ChemMap -> LessonListScreen(
vm, vm.tr("Chemie"), vm.tr("chem_sub"), vm, vm.tr("Chemie"), vm.tr("chem_sub"),

View file

@ -262,6 +262,26 @@ class AppViewModel(val content: Content, val platform: Platform) {
refresh() 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) { fun markMluv(n: Int) {
progress.markMluv(n) progress.markMluv(n)
refresh() refresh()

View file

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

View file

@ -60,6 +60,10 @@ class Content(root: JSONObject) {
val tg2 = raw.arr("tg2") val tg2 = raw.arr("tg2")
val tg3 = raw.arr("tg3") val tg3 = raw.arr("tg3")
val tg4 = raw.arr("tg4") 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 mat0 = raw.arr("mat0")
val mat = raw.arr("mat") val mat = raw.arr("mat")
val mat2 = raw.arr("mat2") val mat2 = raw.arr("mat2")
@ -116,6 +120,10 @@ class Content(root: JSONObject) {
fun tg2Lesson(id: Int) = tg2.firstOrNull { it.int("id") == id } fun tg2Lesson(id: Int) = tg2.firstOrNull { it.int("id") == id }
fun tg3Lesson(id: Int) = tg3.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 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 mat0Lesson(id: Int) = mat0.firstOrNull { it.int("id") == id }
fun matLesson(id: Int) = mat.firstOrNull { it.int("id") == id } fun matLesson(id: Int) = mat.firstOrNull { it.int("id") == id }
fun mat2Lesson(id: Int) = mat2.firstOrNull { it.int("id") == id } fun 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 markTg3(id: Int) { mark("tg3.$id") }
fun tg4Done(id: Int) = done("tg4.$id") fun tg4Done(id: Int) = done("tg4.$id")
fun markTg4(id: Int) { mark("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 mat0Done(id: Int) = done("mat0.$id")
fun markMat0(id: Int) { mark("mat0.$id") } fun markMat0(id: Int) { mark("mat0.$id") }
fun matDone(id: Int) = done("mat.$id") fun matDone(id: Int) = done("mat.$id")
@ -320,6 +328,38 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
sum.doneUnits += 1 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 -> content.mat0.forEach { l ->
sum.totalEx += 1 sum.totalEx += 1
sum.openUnits += 1 sum.openUnits += 1

View file

@ -18,8 +18,18 @@ interface Platform {
/** A speech engine for listening exercises, or null when there is none. */ /** A speech engine for listening exercises, or null when there is none. */
fun speaker(): Speaker? 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. */ /** The subset of Android's SharedPreferences the app uses. */
interface Prefs { interface Prefs {
fun getBoolean(key: String, fallback: Boolean): Boolean 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.Lock
import androidx.compose.material.icons.filled.Quiz import androidx.compose.material.icons.filled.Quiz
import androidx.compose.material.icons.filled.Science 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.filled.Search
import androidx.compose.material.icons.outlined.Lightbulb import androidx.compose.material.icons.outlined.Lightbulb
import androidx.compose.material3.Card 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)) "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)) "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)) "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) 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.fyz3.count { vm.progress.fyz3Done(it.int("id")) } +
vm.content.fyz4.count { vm.progress.fyz4Done(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" -> { "tgyears", "tgmap", "tg2map", "tg3map", "tg4map" -> {
sum.totalEx = vm.content.tg.size + vm.content.tg2.size + vm.content.tg3.size + vm.content.tg4.size 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")) } + 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_lesson"), "tg4unit${l.int("id")}", extra = "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg4ex${l.int("id")}") 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("Chemie"), vm.tr("search_lesson"), "chemmap", extra = "chemie")
add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra = "biologie") add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra = "biologie")
vm.content.chem.forEach { l -> 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 @Composable
fun TgYearsScreen(vm: AppViewModel) { fun TgYearsScreen(vm: AppViewModel) {
DetailScaffold(vm, vm.tr("Technická grafika"), vm.tr("tg_years_sub")) { inner -> DetailScaffold(vm, vm.tr("Technická grafika"), vm.tr("tg_years_sub")) { inner ->

View file

@ -18,7 +18,7 @@ Skript vypíše počet lekcí, nebo chyby s cestou a číslem řádku
| Složka | Obsah | | Složka | Obsah |
|---|---| |---|---|
| `pocitacove-site/`, `technicke-vybaveni/`, `technicka-grafika/`, `obcanska-nauka/`, `fyzika/` | `N-rocnik/`, v nich lekce | | `pocitacove-site/`, `technicke-vybaveni/`, `technicka-grafika/`, `programovani/`, `obcanska-nauka/`, `fyzika/` | `N-rocnik/`, v nich lekce |
| `prirodni-vedy/chemie`, `prirodni-vedy/biologie` | lekce | | `prirodni-vedy/chemie`, `prirodni-vedy/biologie` | lekce |
| `cestina/literatura/N-rocnik` | lekce literatury | | `cestina/literatura/N-rocnik` | lekce literatury |
| `cestina/mluvnice` | cvičení z mluvnice | | `cestina/mluvnice` | cvičení z mluvnice |

View file

@ -5,6 +5,12 @@
# cs: bullet # cs: bullet
# en: bullet # en: bullet
--- 2026-10-11
cs: Programování: čtyři ročníky C#, od výpisu po LINQ a async
cs: Kód se píše v lekci a hned vedle se spustí
en: Programming: four years of C#, from output to LINQ and async
en: You write the code in the lesson and run it right beside the text
--- 2026-10-11 --- 2026-10-11
cs: Technická grafika: čtyři ročníky AutoCADu, od výkresu po 3D cs: Technická grafika: čtyři ročníky AutoCADu, od výkresu po 3D
cs: V každé lekci je postup, zadání s rozměry a kvíz cs: V každé lekci je postup, zadání s rozměry a kvíz

View file

@ -13,5 +13,5 @@
| Základy přírodních věd | Základy přírodních věd | Fundamentals of Natural Sciences | flask | scimap | ano | | Základy přírodních věd | Základy přírodních věd | Fundamentals of Natural Sciences | flask | scimap | ano |
| Technická grafika | Technická grafika | Technical Graphics | ruler | tgyears | ano | | Technická grafika | Technická grafika | Technical Graphics | ruler | tgyears | ano |
| Prezentační grafika | Prezentační grafika | Presentation Graphics | lock | | ne | | Prezentační grafika | Prezentační grafika | Presentation Graphics | lock | | ne |
| Programování | Programování | Programming | lock | | ne | | Programování | Programování | Programming | code | csyears | ano |
| Programové vybavení | Programové vybavení | Software | lock | | ne | | Programové vybavení | Programové vybavení | Software | lock | | ne |

View file

@ -243,6 +243,29 @@
| tg3_sub | Pohledy, řezy, bloky a tisk. | Views, sections, blocks and plotting. | | tg3_sub | Pohledy, řezy, bloky a tisk. | Views, sections, blocks and plotting. |
| tg4_sub | 3D model a výkres k maturitě. | A 3D model and the exam drawing. | | tg4_sub | 3D model a výkres k maturitě. | A 3D model and the exam drawing. |
# Programování
| Klíč | Česky | Anglicky |
|---|---|---|
| cs_years_sub | C# od prvního výpisu po pokročilejší nástroje. Kód se píše a spouští vedle výkladu. | C# from the first line of output to the later tools. You write the code and run it beside the lesson. |
| cs_year1 | 1. ročník | Year 1 |
| cs_year2 | 2. ročník | Year 2 |
| cs_year3 | 3. ročník | Year 3 |
| cs_year4 | 4. ročník | Year 4 |
| cs_sub | Výpis, proměnné, typy, operátory a řetězce. | Output, variables, types, operators and strings. |
| cs2_sub | Podmínky, cykly, pole a metody. | Conditions, loops, arrays and methods. |
| cs3_sub | Třídy, dědičnost, rozhraní a výčty. | Classes, inheritance, interfaces and enums. |
| cs4_sub | Výjimky, kolekce, LINQ, soubory a async. | Exceptions, collections, LINQ, files and async. |
| cs_code | Kód | Code |
| cs_output | Výstup | Output |
| cs_stdin | Vstup pro Console.ReadLine | Input for Console.ReadLine |
| cs_run | Spustit | Run |
| cs_reset | Obnovit ukázku | Reset example |
| cs_running | Překládám… | Compiling… |
| cs_empty | Zatím nic. Upravte kód a spusťte ho. | Nothing yet. Edit the code and run it. |
| cs_timeout | Program běžel příliš dlouho a byl zastaven. | The program ran too long and was stopped. |
| cs_unavailable | Tady C# nespustíte. Na počítači s nainstalovaným .NET se výstup objeví vedle kódu. | C# cannot run here. On a computer with .NET installed the output appears beside the code. |
# Angličtina a němčina # Angličtina a němčina
| Klíč | Česky | Anglicky | | Klíč | Česky | Anglicky |

View file

@ -0,0 +1,4 @@
---
nazev: Hello, world
popis: The first program: writing to the console.
---

View file

@ -0,0 +1,42 @@
---
id: cs-1-ahoj-svete
nazev: Ahoj světe
popis: První program: výpis na konzoli.
---
# Výklad
## Výpis
*WriteLine*
- Console.WriteLine vypíše text a odřádkuje.
- Console.Write vypíše text a zůstane na stejném řádku.
- Text se píše do uvozovek. Středník ukončuje příkaz.
> Tip: Bez středníku se program nepřeloží.
## Krátký a dlouhý zápis
*Main*
- Starší učebnice mají třídu Program a metodu Main.
- Nový projekt stejný program zapíše několika řádky bez třídy.
- Obojí spustí stejný výpis. V lekcích používáme krátký zápis.
> Tip: Krátký zápis je pořád C#. Není to jiný jazyk.
## Vyzkoušejte
*Editor*
- Vpravo je kód. Upravte ho a stiskněte Spustit.
- Výstup se objeví hned vedle.
- Obnovit ukázku vrátí původní program.
> Tip: Změňte text v uvozovkách a spusťte znovu.
# Kód
```
Console.WriteLine("Ahoj světe");
Console.Write("Toto je ");
Console.WriteLine("C#");
```

View file

@ -0,0 +1,4 @@
---
nazev: Comments
popis: Notes the program does not run.
---

View file

@ -0,0 +1,35 @@
---
id: cs-1-komentare
nazev: Komentáře
popis: Poznámky, které se nespouštějí.
---
# Výklad
## Jednořádkový
*//*
- Vše za // na stejném řádku počítač přeskočí.
- Komentář vysvětluje, proč tam řádek je, ne co je vidět.
- Kód bez komentáře pořád běží stejně.
> Tip: Komentář není výpis. Na konzoli se neobjeví.
## Více řádků
*/* */*
- Blok začíná /* a končí */.
- Hodí se, když chcete dočasně vypnout několik řádků.
- Zapomenuté */ rozbije zbytek souboru.
> Tip: Nevnořujte blokové komentáře do sebe.
# Kód
```
// Tohle se nespustí.
Console.WriteLine("Tento řádek ano");
/*
Console.WriteLine("A tento ne");
*/
```

View file

@ -0,0 +1,4 @@
---
nazev: Variables
popis: A named value you can change.
---

View file

@ -0,0 +1,36 @@
---
id: cs-1-promenne
nazev: Proměnné
popis: Pojmenovaná hodnota, kterou lze změnit.
---
# Výklad
## Deklarace
*typ jméno*
- Nejdřív typ, pak jméno, pak hodnota: int vek = 17;
- Jméno začíná písmenem. Mezery v něm být nesmí.
- Do stejné proměnné lze později uložit novou hodnotu stejného typu.
> Tip: int drží celé číslo. string drží text.
## Čtení
*jméno*
- Jméno bez uvozovek je proměnná. Uvozovky by z něj udělaly text.
- WriteLine umí vypsat proměnnou i text spojený plusem.
- Hodnota se dosadí až při běhu, ne při psaní.
> Tip: Změňte vek na 18 a spusťte znovu.
# Kód
```
string jmeno = "Anna";
int vek = 17;
jmeno = "Eva";
Console.WriteLine(jmeno);
Console.WriteLine(vek);
Console.WriteLine(jmeno + " má " + vek + " let");
```

View file

@ -0,0 +1,4 @@
---
nazev: Data types
popis: int, double, bool, char and string.
---

View file

@ -0,0 +1,40 @@
---
id: cs-1-typy
nazev: Datové typy
popis: int, double, bool, char a string.
---
# Výklad
## Čísla
*int a double*
- int je celé číslo: 42.
- double je číslo s desetinnou tečkou: 3.14.
- V kódu je desetinná tečka, ne čárka.
> Tip: 3,14 by C# přečetl jako dva údaje oddělené čárkou.
## Text a ano/ne
*string, char, bool*
- string je text v uvozovkách.
- char je jeden znak v apostrofech: 'A'.
- bool je jen true nebo false.
> Tip: Apostrof a uvozovky nejsou totéž. 'A' je znak, "A" je text.
# Kód
```
int cele = 42;
double desetinne = 3.14;
bool prsi = false;
char pismeno = 'A';
string text = "ahoj";
Console.WriteLine(cele);
Console.WriteLine(desetinne);
Console.WriteLine(prsi);
Console.WriteLine(pismeno);
Console.WriteLine(text);
```

View file

@ -0,0 +1,4 @@
---
nazev: Type conversion
popis: From double to int and from text to a number.
---

View file

@ -0,0 +1,36 @@
---
id: cs-1-prevody
nazev: Převody typů
popis: Z double na int a z textu na číslo.
---
# Výklad
## Ořez desetin
*(int)*
- (int)3.9 je 3. Desetinná část se odřízne, nezaokrouhlí.
- Opačně int do double ztrátu nemá: 3 se stane 3.0.
- Převod napište před hodnotu do závorky.
> Tip: Když potřebujete zaokrouhlit, (int) na to není.
## Text na číslo
*Parse*
- int.Parse("20") udělá z textu číslo 20.
- Když text číslo není, program spadne. To později chytíme výjimkou.
- Číslo na text udělá ToString nebo zápis do řetězce.
> Tip: Vstup z konzole je vždy text, i když uživatel napíše číslici.
# Kód
```
double cena = 19.9;
int cele = (int)cena;
Console.WriteLine(cele);
string vstup = "20";
int cislo = int.Parse(vstup);
Console.WriteLine(cislo + 1);
```

View file

@ -0,0 +1,4 @@
---
nazev: Operators
popis: Arithmetic, remainder, comparison and logic.
---

View file

@ -0,0 +1,37 @@
---
id: cs-1-operatory
nazev: Operátory
popis: Počítání, zbytek, porovnání a a zároveň.
---
# Výklad
## Počet
*+ - * / %*
- Sčítání, odčítání a násobení fungují, jak čekáte.
- Dělení dvou int je celočíselné: 17 / 5 je 3.
- % je zbytek po dělení: 17 % 5 je 2.
> Tip: Když chcete desetinný podíl, jedno číslo udělejte double: 17 / 5.0.
## Porovnání
*true a false*
- == je rovnost. = je uložení. Nejsou totéž.
- && znamená a zároveň. || znamená nebo.
- ! otočí bool: !true je false.
> Tip: Jedno = v podmínce je častá chyba. Podmínka chce ==.
# Kód
```
int a = 17;
int b = 5;
Console.WriteLine(a + b);
Console.WriteLine(a / b);
Console.WriteLine(a % b);
Console.WriteLine(a > b);
Console.WriteLine(a > 10 && b < 10);
```

View file

@ -0,0 +1,4 @@
---
nazev: Strings
popis: Joining text, length and a slice.
---

View file

@ -0,0 +1,36 @@
---
id: cs-1-retezce
nazev: Řetězce
popis: Skládání textu, délka a výřez.
---
# Výklad
## Interpolace
*$*
- $"Ahoj, {jmeno}" dosadí hodnotu do složených závorek.
- Je přehlednější než spojování plusem.
- Do závorek může i výpočet: {vek + 1}.
> Tip: Bez dolaru před uvozovkami se {jmeno} vypíše doslova.
## Metody textu
*Length*
- Length je počet znaků.
- ToUpper vrátí velká písmena a původní text nezmění.
- Substring(0, 2) vezme dva znaky od začátku.
> Tip: První znak má index 0, ne 1.
# Kód
```
string jmeno = "Anna";
int vek = 17;
Console.WriteLine($"Ahoj, {jmeno}. Za rok ti bude {vek + 1}.");
Console.WriteLine(jmeno.Length);
Console.WriteLine(jmeno.ToUpper());
Console.WriteLine(jmeno.Substring(0, 2));
```

View file

@ -0,0 +1,4 @@
---
nazev: Console input
popis: Console.ReadLine reads a line.
---

View file

@ -0,0 +1,34 @@
---
id: cs-1-vstup
nazev: Vstup z konzole
popis: Console.ReadLine přečte řádek.
---
# Výklad
## ReadLine
*text*
- ReadLine počká na řádek a vrátí ho jako string.
- V této lekci řádek napište do pole Vstup a pak Spustit.
- Každé další ReadLine vezme další řádek vstupu.
> Tip: Když vstup necháte prázdný, ReadLine dostane konec a program může spadnout.
## Číslo ze vstupu
*Parse*
- ReadLine nikdy nevrací int. Vrací text.
- int.Parse z něj číslo udělá.
- Napište do vstupu třeba 17.
> Tip: Zkuste ve vstupu Anna a uvidíte chybu. Parse čeká číslo.
# Kód
```
Console.WriteLine("Kolik ti je?");
string radek = Console.ReadLine();
int vek = int.Parse(radek);
Console.WriteLine($"Za rok ti bude {vek + 1}.");
```

View file

@ -0,0 +1,4 @@
---
nazev: If and else
popis: A branch that runs only sometimes.
---

View file

@ -0,0 +1,43 @@
---
id: cs-2-if
nazev: If a else
popis: Větev, která se spustí jen někdy.
---
# Výklad
## Podmínka
*if*
- if (vek >= 18) spustí blok ve složených závorkách, jen když je podmínka true.
- else je větev pro opačný případ.
- else if přidá další podmínku mezi ně.
> Tip: Za if není středník. Středník by podmínku oddělil od bloku.
## Porovnání
*==*
- Čísla porovnávejte >, <, >=, <=, == a !=.
- Text se porovnává ==. "Anna" == "anna" je false.
- Do podmínky patří bool, ne přiřazení.
> Tip: Změňte vek a sledujte, která věta se vypíše.
# Kód
```
int vek = 17;
if (vek >= 18)
{
Console.WriteLine("plnoletý");
}
else if (vek >= 15)
{
Console.WriteLine("téměř");
}
else
{
Console.WriteLine("ještě ne");
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Switch
popis: Several branches for one value.
---

View file

@ -0,0 +1,43 @@
---
id: cs-2-switch
nazev: Switch
popis: Víc větví podle jedné hodnoty.
---
# Výklad
## Větve
*case*
- switch vybere case, který se rovná hodnotě.
- break ukončí větev. Bez něj se překlad v klasickém zápisu zastaví.
- default je větev, když nesedí žádný case.
> Tip: Na známku 1 až 5 se switch hodí víc než řada else if.
## Nový zápis
*šipka*
- case 1 => "výborně" je krátká větev bez break.
- Výraz switch umí hodnotu rovnou vrátit.
- V lekci je klasický zápis, ať je vidět break.
> Tip: Přidejte case 5 a spusťte se znamkou 5.
# Kód
```
int znamka = 2;
switch (znamka)
{
case 1:
Console.WriteLine("výborně");
break;
case 2:
Console.WriteLine("chvalitebně");
break;
default:
Console.WriteLine("jiná známka");
break;
}
```

View file

@ -0,0 +1,4 @@
---
nazev: While
popis: Repeat while a condition holds.
---

View file

@ -0,0 +1,38 @@
---
id: cs-2-while
nazev: While
popis: Opakování, dokud podmínka platí.
---
# Výklad
## Smyčka
*while*
- Tělo se opakuje, dokud je podmínka true.
- Když podmínku nikdy nezměníte, smyčka neskončí.
- Aplikace dlouhý běh po chvíli zastaví.
> Tip: i = i + 1 nebo i++ posune čítač. Bez toho se i nemění.
## Součet
*1 až 4*
- Začněte součtem 0 a přičítejte i.
- 1+2+3+4 je 10.
- Podmínka i <= 4 pustí i číslo 4 dovnitř.
> Tip: Změňte 4 na 10. Součet 1 až 10 je 55.
# Kód
```
int i = 1;
int soucet = 0;
while (i <= 4)
{
soucet = soucet + i;
i++;
}
Console.WriteLine(soucet);
```

View file

@ -0,0 +1,4 @@
---
nazev: For
popis: A loop with its counter in one place.
---

View file

@ -0,0 +1,34 @@
---
id: cs-2-for
nazev: For
popis: Smyčka s čítačem na jednom místě.
---
# Výklad
## Tři části
*start; podmínka; krok*
- for (int i = 0; i < 3; i++) má začátek, podmínku a krok.
- i < 3 pustí 0, 1 a 2. Trojku už ne.
- Krok i++ se provede až po těle.
> Tip: Když potřebujete přesný počet opakování, for je přehlednější než while.
## Násobilka
*výpis*
- V těle můžete počítat i * 2.
- WriteLine v těle vypíše jeden řádek na každé opakování.
- Čítač nemusí začínat nulou.
> Tip: Přepište podmínku na i <= 5 a sledujte další řádek.
# Kód
```
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"{i} krát 2 je {i * 2}");
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Arrays
popis: Several values of one type under one name.
---

View file

@ -0,0 +1,35 @@
---
id: cs-2-pole
nazev: Pole
popis: Víc hodnot stejného typu pod jedním jménem.
---
# Výklad
## Index
*od nuly*
- new int[] { 2, 4, 6 } je pole tří čísel.
- První prvek je znamky[0], poslední znamky[2].
- Length je počet prvků, ne index posledního.
> Tip: znamky[3] u pole délky 3 spadne. Poslední index je 2.
## Zápis
*prvku*
- Do prvku se dá uložit nová hodnota: znamky[1] = 5.
- Ostatní prvky zůstanou.
- Pole má pevnou délku. Přidat čtvrtý prvek nejde, na to je později List.
> Tip: Vyměňte první známku a spusťte znovu.
# Kód
```
int[] znamky = { 1, 3, 2 };
Console.WriteLine(znamky[0]);
Console.WriteLine(znamky.Length);
znamky[1] = 5;
Console.WriteLine(znamky[1]);
```

View file

@ -0,0 +1,4 @@
---
nazev: Foreach
popis: Visit every item without an index.
---

View file

@ -0,0 +1,35 @@
---
id: cs-2-foreach
nazev: Foreach
popis: Projít každý prvek bez indexu.
---
# Výklad
## Projítí
*foreach*
- foreach (int z in znamky) vezme prvky od prvního po poslední.
- z je kopie hodnoty. U int se zápisem do z pole nezmění.
- Když index nepotřebujete, foreach je kratší než for.
> Tip: Součet ve smyčce přičítá aktuální z.
## Texty
*string[]*
- Pole může být i ze stringů.
- Stejný foreach funguje pro oba typy, jen se liší typ prvku.
- Pořadí je pořadí v závorce při vytvoření.
> Tip: Přidejte do pole třetí jméno.
# Kód
```
string[] jmena = { "Anna", "Eva", "Jan" };
foreach (string jmeno in jmena)
{
Console.WriteLine(jmeno);
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Methods
popis: A named piece of the program you can call.
---

View file

@ -0,0 +1,37 @@
---
id: cs-2-metody
nazev: Metody
popis: Pojmenovaný kus programu, který lze zavolat.
---
# Výklad
## Volání
*jméno()*
- static void Pozdrav() je metoda bez výsledku.
- Zavolá se Pozdrav(); včetně závorek.
- Bez závorek by to bylo jen jméno, ne volání.
> Tip: Metodu lze zavolat víckrát a pokaždé udělá totéž.
## Proč
*jednou*
- Kód, který se opakuje, patří do metody.
- Jméno metody říká, co dělá: Pozdrav, ne Metoda1.
- void znamená, že nevrací hodnotu.
> Tip: Přidejte druhé volání Pozdrav().
# Kód
```
Pozdrav();
Pozdrav();
static void Pozdrav()
{
Console.WriteLine("Ahoj");
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Parameters and return
popis: A method receives values and can return one.
---

View file

@ -0,0 +1,38 @@
---
id: cs-2-navrat
nazev: Parametry a návrat
popis: Metoda dostane hodnoty a může jednu vrátit.
---
# Výklad
## Parametr
*vstup*
- int a v závorce je parametr. Volající dodá argument.
- Součet(2, 5) uloží 2 do a a 5 do b.
- Jména parametrů platí jen uvnitř metody.
> Tip: Změňte argumenty a výsledek se změní, metoda ne.
## return
*výstup*
- int před jménem je typ výsledku.
- return hodnotu vrátí a metodu ukončí.
- Volající si výsledek může uložit: int s = Soucet(2, 5);
> Tip: return 0 omylem na začátku by zbytek metody nespustil.
# Kód
```
int s = Soucet(2, 5);
Console.WriteLine(s);
Console.WriteLine(Soucet(10, 1));
static int Soucet(int a, int b)
{
return a + b;
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Class and object
popis: Your own type and one concrete piece of it.
---

View file

@ -0,0 +1,40 @@
---
id: cs-3-trida
nazev: Třída a objekt
popis: Vlastní typ a jeho konkrétní kus.
---
# Výklad
## Třída
*návod*
- class Zak popisuje, co žák má: jméno a věk.
- new Zak() vytvoří objekt, jeden konkrétní kus.
- Dva new jsou dva objekty. Změna jednoho druhý nemění.
> Tip: Třída sama o sobě není žák. Je to popis.
## Pole objektu
*tečka*
- z.Jmeno se čte a zapisuje přes tečku.
- Na začátek dáme veřejná pole, ať je vidět, komu patří.
- Později je schováme za vlastnost.
> Tip: Vytvořte druhého žáka a vypište obě jména.
# Kód
```
Zak z = new Zak();
z.Jmeno = "Anna";
z.Vek = 17;
Console.WriteLine($"{z.Jmeno}, {z.Vek}");
class Zak
{
public string Jmeno;
public int Vek;
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Constructor
popis: A method that fills the object immediately.
---

View file

@ -0,0 +1,44 @@
---
id: cs-3-konstruktor
nazev: Konstruktor
popis: Metoda, která objekt hned naplní.
---
# Výklad
## Jméno třídy
*new Zak*
- Konstruktor se jmenuje jako třída a nemá návratový typ.
- new Zak("Anna", 17) ho zavolá.
- Prázdný new Zak() přestane fungovat, jakmile napíšete vlastní konstruktor.
> Tip: Hodnoty patří do new, ne až na další řádky.
## this
*tento objekt*
- this.Jmeno je pole objektu. Jmeno bez this by v konstruktoru byl parametr.
- Když se jmenují stejně, this je nutný.
- Mimo konstruktor this často není potřeba.
> Tip: Přidejte do výpisu věk z konstruktoru.
# Kód
```
Zak z = new Zak("Anna", 17);
Console.WriteLine($"{z.Jmeno}, {z.Vek}");
class Zak
{
public string Jmeno;
public int Vek;
public Zak(string jmeno, int vek)
{
Jmeno = jmeno;
Vek = vek;
}
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Properties
popis: get and set instead of a bare field.
---

View file

@ -0,0 +1,50 @@
---
id: cs-3-vlastnosti
nazev: Vlastnosti
popis: get a set místo holého pole.
---
# Výklad
## get a set
*tečka zůstává*
- public string Jmeno { get; set; } se zvenku používá jako pole.
- Uvnitř může set odmítnout špatnou hodnotu.
- get hodnotu vrátí, set ji uloží.
> Tip: Zápis z.Vek = -1 může set zarazit, holé pole ne.
## Kontrola
*set*
- V příkladu set u věku záporné číslo neuloží.
- get pořád vrátí poslední platnou hodnotu.
- Tak se objekt nepokazí zvenku.
> Tip: Zkuste z.Vek = -3 a vypište věk. Zůstane 17.
# Kód
```
Zak z = new Zak("Anna", 17);
z.Vek = -3;
Console.WriteLine(z.Vek);
class Zak
{
public string Jmeno { get; set; }
private int vek;
public int Vek
{
get { return vek; }
set { if (value >= 0) vek = value; }
}
public Zak(string jmeno, int vek)
{
Jmeno = jmeno;
Vek = vek;
}
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Encapsulation
popis: Private data and public methods.
---

View file

@ -0,0 +1,49 @@
---
id: cs-3-zapouzdreni
nazev: Zapouzdření
popis: Soukromá data a veřejné metody.
---
# Výklad
## private
*schované*
- private pole vidí jen kód té třídy.
- public metoda je dveře, kterými se s objektem mluví.
- Zvenku z.penize nenapíšete. Zavoláte Pridej.
> Tip: Když je vše public, kdokoli rozbije pravidla objektu.
## Metoda
*Pridej*
- Pridej přičte a vrátí nový stav.
- Záporný vklad lze v metodě odmítnout.
- Výpis stavu jde přes Kolik, ne přes pole.
> Tip: Zkuste Pridej(-10). Stav se nemá zmenšit.
# Kód
```
Pokladna p = new Pokladna();
p.Pridej(50);
p.Pridej(-10);
Console.WriteLine(p.Kolik());
class Pokladna
{
private int penize;
public void Pridej(int castka)
{
if (castka > 0) penize = penize + castka;
}
public int Kolik()
{
return penize;
}
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Inheritance
popis: A new class takes over a finished one.
---

View file

@ -0,0 +1,52 @@
---
id: cs-3-dedicnost
nazev: Dědičnost
popis: Nová třída převezme hotovou.
---
# Výklad
## base
*: Zak*
- class Student : Zak znamená, že student je žák a má něco navíc.
- Dostane Jmeno a Vek, aniž by se psaly znovu.
- base(...) zavolá konstruktor předka.
> Tip: Student umí to, co Zak, a ještě obor.
## Je
*typ*
- Student lze uložit do proměnné Zak. Pořád je to student.
- Opačně žák bez oboru studentem není.
- Na specialitu sáhnete, až když máte původní typ.
> Tip: Vypište i obor. Patří studentovi, ne obecnému žákovi.
# Kód
```
Student s = new Student("Eva", 18, "informatika");
Console.WriteLine($"{s.Jmeno}, {s.Obor}");
class Zak
{
public string Jmeno { get; set; }
public int Vek { get; set; }
public Zak(string jmeno, int vek)
{
Jmeno = jmeno;
Vek = vek;
}
}
class Student : Zak
{
public string Obor { get; set; }
public Student(string jmeno, int vek, string obor) : base(jmeno, vek)
{
Obor = obor;
}
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Polymorphism
popis: The same call, a different answer from the child.
---

View file

@ -0,0 +1,50 @@
---
id: cs-3-polymorfismus
nazev: Polymorfismus
popis: Stejné volání, jiná odpověď potomka.
---
# Výklad
## virtual a override
*přepsání*
- virtual u předka dovolí potomkovi metodu vyměnit.
- override u potomka řekne, že jde o tu samou metodu.
- Bez virtual by potomek jen schoval jméno a přes typ předka by se volala stará verze.
> Tip: Proměnná je Zvire, objekt je Pes. Zavolá se štěk, ne obecné zvíře.
## Seznam
*víc tvarů*
- Do pole Zvire lze dát psa i kočku.
- foreach volá Zvuk a každý objekt odpoví po svém.
- Nové zvíře přidáte třídou, ne úpravou smyčky.
> Tip: Přidejte do pole kočku a třetí řádek výstupu bude mňau.
# Kód
```
Zvire[] zvirata = { new Pes(), new Kocka() };
foreach (Zvire z in zvirata)
{
Console.WriteLine(z.Zvuk());
}
class Zvire
{
public virtual string Zvuk() { return "..."; }
}
class Pes : Zvire
{
public override string Zvuk() { return "haf"; }
}
class Kocka : Zvire
{
public override string Zvuk() { return "mňau"; }
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Interface
popis: A contract: which methods a type must have.
---

View file

@ -0,0 +1,50 @@
---
id: cs-3-rozhrani
nazev: Rozhraní
popis: Smlouva: které metody typ musí mít.
---
# Výklad
## interface
*I*
- interface nemá hotový kód, jen hlavičky metod.
- class Pes : IMluvici slíbí, že Mluv napíše.
- Jedna třída může splnit víc rozhraní. Dědit může jen jednu třídu.
> Tip: Jméno rozhraní se často píše s I na začátku: IMluvici.
## Společný typ
*seznam*
- Proměnná typu rozhraní udrží kohokoli, kdo smlouvu splnil.
- Pes a Radio nemají společného předka, ale oba umí Mluv.
- Smyčka jim rozumí přes smlouvu, ne přes původ.
> Tip: Rozhraní neříká, jak metoda funguje. To napíše až třída.
# Kód
```
IMluvici[] veci = { new Pes(), new Radio() };
foreach (IMluvici v in veci)
{
Console.WriteLine(v.Mluv());
}
interface IMluvici
{
string Mluv();
}
class Pes : IMluvici
{
public string Mluv() { return "haf"; }
}
class Radio : IMluvici
{
public string Mluv() { return "hudba"; }
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Enum
popis: A fixed list of named values.
---

View file

@ -0,0 +1,50 @@
---
id: cs-3-enum
nazev: Výčet
popis: Pevný seznam pojmenovaných hodnot.
---
# Výklad
## enum
*Den*
- enum Den { Po, Ut, St } jsou tři pojmenované hodnoty.
- Místo magického čísla 1 píšete Den.Po.
- Překladač nepustí hodnotu, která ve výčtu není.
> Tip: Čísla 0, 1, 2 by za měsíc nikdo nepřečetl. Jména ano.
## switch
*přes výčet*
- switch na enum má case pro každou hodnotu.
- Když přidáte čtvrtek a zapomenete case, překlad na to v novém zápisu upozorní.
- Výčet je typ. Není to string.
> Tip: Změňte dnes na Den.St.
# Kód
```
Den dnes = Den.Po;
switch (dnes)
{
case Den.Po:
Console.WriteLine("pondělí");
break;
case Den.Ut:
Console.WriteLine("úterý");
break;
default:
Console.WriteLine("jiný den");
break;
}
enum Den
{
Po,
Ut,
St
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Exceptions
popis: An error you can catch and explain.
---

View file

@ -0,0 +1,43 @@
---
id: cs-4-vyjimky
nazev: Výjimky
popis: Chyba, kterou lze chytit a vysvětlit.
---
# Výklad
## try
*pokus*
- try je kus, který může spadnout.
- catch (Exception e) ho chytí, místo aby spadl celý program.
- e.Message je text chyby.
> Tip: Bez try by Parse na textu abc program ukončil.
## finally
*vždy*
- finally se spustí, ať try prošel, nebo ne.
- Hodí se na úklid. V příkladu jen vypíše, že končíme.
- catch může být víc, pro různé typy chyb.
> Tip: Změňte abc na 5. catch se přeskočí, finally ne.
# Kód
```
try
{
int n = int.Parse("abc");
Console.WriteLine(n);
}
catch (Exception e)
{
Console.WriteLine("Chyba: " + e.Message);
}
finally
{
Console.WriteLine("konec");
}
```

View file

@ -0,0 +1,4 @@
---
nazev: List
popis: A list that can grow.
---

View file

@ -0,0 +1,39 @@
---
id: cs-4-list
nazev: List
popis: Seznam, který umí růst.
---
# Výklad
## Přidávání
*Add*
- List<string> začne prázdný. Add přidá na konec.
- Count je počet. Indexy jsou zase od nuly.
- Na rozdíl od pole délku dopředu neříkáte.
> Tip: List<string> potřebuje špičaté závorky s typem prvku.
## Odebrání
*Remove*
- Remove("Eva") odebere první Evu.
- RemoveAt(0) odebere podle indexu.
- foreach pak projde, co zůstalo.
> Tip: Přidejte třetí jméno před odebráním.
# Kód
```
List<string> jmena = new List<string>();
jmena.Add("Anna");
jmena.Add("Eva");
jmena.Remove("Eva");
foreach (string j in jmena)
{
Console.WriteLine(j);
}
Console.WriteLine(jmena.Count);
```

View file

@ -0,0 +1,4 @@
---
nazev: Dictionary
popis: A value under a key.
---

View file

@ -0,0 +1,39 @@
---
id: cs-4-slovnik
nazev: Slovník
popis: Hodnota pod klíčem.
---
# Výklad
## Klíč
*jednou*
- Dictionary<string, int> má klíč text a hodnotu číslo.
- znamky["Anna"] = 1 uloží nebo přepíše.
- Stejný klíč je v slovníku jen jednou.
> Tip: Pole je podle čísla. Slovník je podle jména, které si zvolíte.
## Čtení
*ContainsKey*
- Čtení chybějícího klíče spadne.
- ContainsKey napřed zjistí, jestli klíč je.
- foreach projde dvojice Key a Value.
> Tip: Zeptejte se na jméno, které v slovníku není, až po ContainsKey.
# Kód
```
Dictionary<string, int> znamky = new Dictionary<string, int>();
znamky["Anna"] = 1;
znamky["Eva"] = 2;
znamky["Anna"] = 3;
if (znamky.ContainsKey("Eva"))
{
Console.WriteLine(znamky["Eva"]);
}
Console.WriteLine(znamky["Anna"]);
```

View file

@ -0,0 +1,4 @@
---
nazev: LINQ
popis: Queries over a list.
---

View file

@ -0,0 +1,36 @@
---
id: cs-4-linq
nazev: LINQ
popis: Dotazy nad seznamem.
---
# Výklad
## Where
*filtr*
- Where nechá jen prvky, které splní podmínku.
- n => n >= 10 je krátká funkce: vezmi n a vrať, jestli je aspoň 10.
- Samotné Where ještě nic neprojde. ToList nebo foreach výsledek vezme.
> Tip: Šipka => není porovnání. Je to zápis malé funkce.
## Select
*převod*
- Select z každého prvku udělá jiný.
- OrderBy seřadí.
- Dotaz lze řetězit: Where a pak Select.
> Tip: Změňte 10 na 12 a dvojka z výpisu zmizí.
# Kód
```
int[] cisla = { 4, 11, 7, 20 };
var velka = cisla.Where(n => n >= 10).OrderBy(n => n);
foreach (int n in velka)
{
Console.WriteLine(n);
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Files
popis: Writing and reading text.
---

View file

@ -0,0 +1,33 @@
---
id: cs-4-soubory
nazev: Soubory
popis: Zápis a čtení textu.
---
# Výklad
## WriteAllText
*jednou*
- File.WriteAllText zapíše celý soubor. Když existuje, přepíše ho.
- Cesta pozdrav.txt je vedle programu, v jeho složce.
- ReadAllText celý text vrátí jako string.
> Tip: Na telefonu se soubor nespustí. Na počítači s .NET ano.
## Řádky
*Append*
- AppendAllText přidá na konec a soubor nesmaže.
- ReadAllLines vrátí pole řádků.
- Po skončení programu soubor ve složce laboratoře zůstane.
> Tip: Spusťte dvakrát. Druhý zápis WriteAllText starý text nahradí.
# Kód
```
File.WriteAllText("pozdrav.txt", "Ahoj ze souboru");
string zpet = File.ReadAllText("pozdrav.txt");
Console.WriteLine(zpet);
```

View file

@ -0,0 +1,4 @@
---
nazev: Generics
popis: One method for several types.
---

View file

@ -0,0 +1,37 @@
---
id: cs-4-generika
nazev: Generika
popis: Jedna metoda pro víc typů.
---
# Výklad
## T
*typový parametr*
- static void Vypis<T>(T hodnota) přijme int i string.
- T se doplní při volání podle argumentu.
- Jedna metoda tak nenutí kopírovat kód pro každý typ.
> Tip: Když metoda s T má umět jen porovnávat, T se omezí. Tady stačí výpis.
## List
*už ho znáte*
- List<string> je generická třída. string je dosazený T.
- Stejný List<int> drží čísla a metody jsou stejné.
- Generika hlídá typ. Do List<int> string nedáte.
> Tip: Zavolejte Vypis s číslem i s textem.
# Kód
```
Vypis(7);
Vypis("ahoj");
static void Vypis<T>(T hodnota)
{
Console.WriteLine(hodnota);
}
```

View file

@ -0,0 +1,4 @@
---
nazev: Delegates
popis: A variable that holds a method.
---

View file

@ -0,0 +1,34 @@
---
id: cs-4-delegaty
nazev: Delegáty
popis: Proměnná, do které uložíte metodu.
---
# Výklad
## Func
*funkce*
- Func<int, int> je metoda, která dostane int a vrátí int.
- x => x * x je taková metoda bez jména.
- Do proměnné ji uložíte a zavoláte později.
> Tip: Závorky pocet(4) metodu spustí. Bez nich byste ji jen předali dál.
## Action
*bez výsledku*
- Action nic nevrací. Hodí se na kroky.
- Metodu můžete předat jiné metodě jako argument.
- Tak se píše i Where z minulé lekce.
> Tip: Vyměňte x * x za x + 1.
# Kód
```
Func<int, int> pocet = x => x * x;
Console.WriteLine(pocet(4));
Action pozdrav = () => Console.WriteLine("ahoj");
pozdrav();
```

View file

@ -0,0 +1,4 @@
---
nazev: Async and await
popis: Work that waits without freezing the way you write it.
---

View file

@ -0,0 +1,33 @@
---
id: cs-4-async
nazev: Async a await
popis: Práce, která počká, aniž by zamkla zbytek zápisu.
---
# Výklad
## await
*počkej*
- await Task.Delay čeká, ale zápis zůstane řádek za řádkem.
- Metoda s await má v hlavičce async a vrací Task.
- V krátkém programu smí await stát i na nejvyšší úrovni.
> Tip: Delay(300) čeká 300 milisekund. Výpis hotovo přijde až po něm.
## Task
*slib*
- Task je slib, že práce doběhne.
- await slib rozbalí. Bez await byste měli slib, ne výsledek.
- Na síť a soubory se await hodí. Na 2 + 2 ne.
> Tip: Dejte za první výpis ještě jeden await a druhý řádek.
# Kód
```
Console.WriteLine("začátek");
await Task.Delay(300);
Console.WriteLine("hotovo");
```

View file

@ -254,6 +254,26 @@ final class AppModel: ObservableObject {
refresh() refresh()
} }
func markCs(_ id: Int) {
progress.markCs(id)
refresh()
}
func markCs2(_ id: Int) {
progress.markCs2(id)
refresh()
}
func markCs3(_ id: Int) {
progress.markCs3(id)
refresh()
}
func markCs4(_ id: Int) {
progress.markCs4(id)
refresh()
}
func markMat0(_ id: Int) { func markMat0(_ id: Int) {
progress.markMat0(id) progress.markMat0(id)
refresh() refresh()

View file

@ -63,6 +63,10 @@ final class Content {
let tg2: [J] let tg2: [J]
let tg3: [J] let tg3: [J]
let tg4: [J] let tg4: [J]
let cs: [J]
let cs2: [J]
let cs3: [J]
let cs4: [J]
let mat0: [J] let mat0: [J]
let mat: [J] let mat: [J]
let mat2: [J] let mat2: [J]
@ -133,6 +137,10 @@ final class Content {
tg2 = raw.arr("tg2") tg2 = raw.arr("tg2")
tg3 = raw.arr("tg3") tg3 = raw.arr("tg3")
tg4 = raw.arr("tg4") tg4 = raw.arr("tg4")
cs = raw.arr("cs")
cs2 = raw.arr("cs2")
cs3 = raw.arr("cs3")
cs4 = raw.arr("cs4")
mat0 = raw.arr("mat0") mat0 = raw.arr("mat0")
mat = raw.arr("mat") mat = raw.arr("mat")
mat2 = raw.arr("mat2") mat2 = raw.arr("mat2")
@ -199,6 +207,10 @@ final class Content {
func tg2Lesson(_ id: Int) -> J? { tg2.first { $0.int("id") == id } } func tg2Lesson(_ id: Int) -> J? { tg2.first { $0.int("id") == id } }
func tg3Lesson(_ id: Int) -> J? { tg3.first { $0.int("id") == id } } func tg3Lesson(_ id: Int) -> J? { tg3.first { $0.int("id") == id } }
func tg4Lesson(_ id: Int) -> J? { tg4.first { $0.int("id") == id } } func tg4Lesson(_ id: Int) -> J? { tg4.first { $0.int("id") == id } }
func csLesson(_ id: Int) -> J? { cs.first { $0.int("id") == id } }
func cs2Lesson(_ id: Int) -> J? { cs2.first { $0.int("id") == id } }
func cs3Lesson(_ id: Int) -> J? { cs3.first { $0.int("id") == id } }
func cs4Lesson(_ id: Int) -> J? { cs4.first { $0.int("id") == id } }
func mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } } func mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } }
func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } } func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } }
func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } } func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } }

View file

@ -102,6 +102,14 @@ final class ProgressStore {
func markTg3(_ id: Int) { mark("tg3.\(id)") } func markTg3(_ id: Int) { mark("tg3.\(id)") }
func tg4Done(_ id: Int) -> Bool { done("tg4.\(id)") } func tg4Done(_ id: Int) -> Bool { done("tg4.\(id)") }
func markTg4(_ id: Int) { mark("tg4.\(id)") } func markTg4(_ id: Int) { mark("tg4.\(id)") }
func csDone(_ id: Int) -> Bool { done("cs.\(id)") }
func markCs(_ id: Int) { mark("cs.\(id)") }
func cs2Done(_ id: Int) -> Bool { done("cs2.\(id)") }
func markCs2(_ id: Int) { mark("cs2.\(id)") }
func cs3Done(_ id: Int) -> Bool { done("cs3.\(id)") }
func markCs3(_ id: Int) { mark("cs3.\(id)") }
func cs4Done(_ id: Int) -> Bool { done("cs4.\(id)") }
func markCs4(_ id: Int) { mark("cs4.\(id)") }
func mat0Done(_ id: Int) -> Bool { done("mat0.\(id)") } func mat0Done(_ id: Int) -> Bool { done("mat0.\(id)") }
func markMat0(_ id: Int) { mark("mat0.\(id)") } func markMat0(_ id: Int) { mark("mat0.\(id)") }
func matDone(_ id: Int) -> Bool { done("mat.\(id)") } func matDone(_ id: Int) -> Bool { done("mat.\(id)") }
@ -328,6 +336,38 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1 sum.doneUnits += 1
} }
} }
for l in content.cs {
sum.totalEx += 1
sum.openUnits += 1
if p.csDone(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.cs2 {
sum.totalEx += 1
sum.openUnits += 1
if p.cs2Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.cs3 {
sum.totalEx += 1
sum.openUnits += 1
if p.cs3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.cs4 {
sum.totalEx += 1
sum.openUnits += 1
if p.cs4Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.mat0 { for l in content.mat0 {
sum.totalEx += 1 sum.totalEx += 1
sum.openUnits += 1 sum.openUnits += 1

View file

@ -124,6 +124,15 @@ enum Route: Hashable {
case tg4Map case tg4Map
case tg4Lesson(Int) case tg4Lesson(Int)
case tg4Ex(Int) case tg4Ex(Int)
case csYears
case csMap
case csLesson(Int)
case cs2Map
case cs2Lesson(Int)
case cs3Map
case cs3Lesson(Int)
case cs4Map
case cs4Lesson(Int)
case czechMap case czechMap
case mluvnice case mluvnice
case mluvEx(Int) case mluvEx(Int)
@ -250,6 +259,15 @@ enum Route: Hashable {
case .tg4Map: return "tg4map" case .tg4Map: return "tg4map"
case .tg4Lesson(let id): return "tg4unit\(id)" case .tg4Lesson(let id): return "tg4unit\(id)"
case .tg4Ex(let id): return "tg4ex\(id)" case .tg4Ex(let id): return "tg4ex\(id)"
case .csYears: return "csyears"
case .csMap: return "csmap"
case .csLesson(let id): return "csunit\(id)"
case .cs2Map: return "cs2map"
case .cs2Lesson(let id): return "cs2unit\(id)"
case .cs3Map: return "cs3map"
case .cs3Lesson(let id): return "cs3unit\(id)"
case .cs4Map: return "cs4map"
case .cs4Lesson(let id): return "cs4unit\(id)"
case .czechMap: return "czechmap" case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice" case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)" case .mluvEx(let n): return "mluve\(n)"
@ -500,6 +518,24 @@ enum Route: Hashable {
return [.tgYears, .tg4Map, .tg4Lesson(id)] return [.tgYears, .tg4Map, .tg4Lesson(id)]
case .tg4Ex(let id): case .tg4Ex(let id):
return [.tgYears, .tg4Map, .tg4Lesson(id), .tg4Ex(id)] return [.tgYears, .tg4Map, .tg4Lesson(id), .tg4Ex(id)]
case .csYears:
return [.csYears]
case .csMap:
return [.csYears, .csMap]
case .csLesson(let id):
return [.csYears, .csMap, .csLesson(id)]
case .cs2Map:
return [.csYears, .cs2Map]
case .cs2Lesson(let id):
return [.csYears, .cs2Map, .cs2Lesson(id)]
case .cs3Map:
return [.csYears, .cs3Map]
case .cs3Lesson(let id):
return [.csYears, .cs3Map, .cs3Lesson(id)]
case .cs4Map:
return [.csYears, .cs4Map]
case .cs4Lesson(let id):
return [.csYears, .cs4Map, .cs4Lesson(id)]
case .czechMap: case .czechMap:
return [.czechMap] return [.czechMap]
case .mluvnice: case .mluvnice:
@ -565,6 +601,11 @@ func routeFromPage(_ page: String) -> Route? {
case "tg2map": return .tg2Map case "tg2map": return .tg2Map
case "tg3map": return .tg3Map case "tg3map": return .tg3Map
case "tg4map": return .tg4Map case "tg4map": return .tg4Map
case "csyears": return .csYears
case "csmap": return .csMap
case "cs2map": return .cs2Map
case "cs3map": return .cs3Map
case "cs4map": return .cs4Map
case "czechmap": return .czechMap case "czechmap": return .czechMap
case "mluvnice": return .mluvnice case "mluvnice": return .mluvnice
case "readinglist": return .readingList case "readinglist": return .readingList
@ -698,6 +739,18 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /tgex(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /tgex(\d+)/), let n = Int(m.1) {
return .tgEx(n) return .tgEx(n)
} }
if let m = page.wholeMatch(of: /cs4unit(\d+)/), let n = Int(m.1) {
return .cs4Lesson(n)
}
if let m = page.wholeMatch(of: /cs3unit(\d+)/), let n = Int(m.1) {
return .cs3Lesson(n)
}
if let m = page.wholeMatch(of: /cs2unit(\d+)/), let n = Int(m.1) {
return .cs2Lesson(n)
}
if let m = page.wholeMatch(of: /csunit(\d+)/), let n = Int(m.1) {
return .csLesson(n)
}
if let m = page.wholeMatch(of: /lit4unit(\d+)/), let n = Int(m.1) { if let m = page.wholeMatch(of: /lit4unit(\d+)/), let n = Int(m.1) {
return .lit4Lesson(n) return .lit4Lesson(n)
} }

View file

@ -680,6 +680,7 @@ func subjectSymbol(_ icon: String) -> String {
case "bolt": return "bolt.fill" case "bolt": return "bolt.fill"
case "function": return "function" case "function": return "function"
case "ruler": return "ruler.fill" case "ruler": return "ruler.fill"
case "code": return "chevron.left.forwardslash.chevron.right"
default: return "lock.fill" default: return "lock.fill"
} }
} }

View file

@ -713,6 +713,12 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
+ vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count + vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count
+ vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count + vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count
+ vm.content.mat4.filter { vm.progress.mat4Done($0.int("id")) }.count + vm.content.mat4.filter { vm.progress.mat4Done($0.int("id")) }.count
case "csyears", "csmap", "cs2map", "cs3map", "cs4map":
sum.totalEx = vm.content.cs.count + vm.content.cs2.count + vm.content.cs3.count + vm.content.cs4.count
sum.doneEx = vm.content.cs.filter { vm.progress.csDone($0.int("id")) }.count
+ vm.content.cs2.filter { vm.progress.cs2Done($0.int("id")) }.count
+ vm.content.cs3.filter { vm.progress.cs3Done($0.int("id")) }.count
+ vm.content.cs4.filter { vm.progress.cs4Done($0.int("id")) }.count
case "tgyears", "tgmap", "tg2map", "tg3map", "tg4map": case "tgyears", "tgmap", "tg2map", "tg3map", "tg4map":
sum.totalEx = vm.content.tg.count + vm.content.tg2.count + vm.content.tg3.count + vm.content.tg4.count sum.totalEx = vm.content.tg.count + vm.content.tg2.count + vm.content.tg3.count + vm.content.tg4.count
sum.doneEx = vm.content.tg.filter { vm.progress.tgDone($0.int("id")) }.count sum.doneEx = vm.content.tg.filter { vm.progress.tgDone($0.int("id")) }.count
@ -1042,6 +1048,23 @@ private func buildSearch(_ vm: AppModel) -> [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_lesson"), "tg4unit\(l.int("id"))", extra: "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg4ex\(l.int("id"))") 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")
for l in vm.content.cs {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "csunit\(l.int("id"))", extra: "csharp")
}
for l in vm.content.cs2 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs2unit\(l.int("id"))", extra: "csharp")
}
for l in vm.content.cs3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs3unit\(l.int("id"))", extra: "csharp")
}
for l in vm.content.cs4 {
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("Chemie"), vm.tr("search_lesson"), "chemmap", extra: "chemie")
add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra: "biologie") add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra: "biologie")
for l in vm.content.chem { for l in vm.content.chem {
@ -1776,6 +1799,47 @@ struct MatYearsScreen: View {
} }
} }
struct CsYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let years: [(String, String, Route)] = [
("cs_year1", "cs_sub", .csMap),
("cs_year2", "cs2_sub", .cs2Map),
("cs_year3", "cs3_sub", .cs3Map),
("cs_year4", "cs4_sub", .cs4Map),
]
MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
NavigationLink(value: years[i].2) {
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
}
.buttonStyle(PressScaleStyle())
}
.navigationTitle(vm.tr("Programování"))
.navigationSubtitle(vm.tr("cs_years_sub"))
#else
List {
NavigationLink(value: Route.csMap) {
ListRowLabel(title: vm.tr("cs_year1"), subtitle: vm.tr("cs_sub"))
}
NavigationLink(value: Route.cs2Map) {
ListRowLabel(title: vm.tr("cs_year2"), subtitle: vm.tr("cs2_sub"))
}
NavigationLink(value: Route.cs3Map) {
ListRowLabel(title: vm.tr("cs_year3"), subtitle: vm.tr("cs3_sub"))
}
NavigationLink(value: Route.cs4Map) {
ListRowLabel(title: vm.tr("cs_year4"), subtitle: vm.tr("cs4_sub"))
}
}
.navigationTitle(vm.tr("Programování"))
.navigationSubtitle(vm.tr("cs_years_sub"))
.appListStyle()
#endif
}
}
struct TgYearsScreen: View { struct TgYearsScreen: View {
@ObservedObject var vm: AppModel @ObservedObject var vm: AppModel
@ -2876,6 +2940,64 @@ struct RouteDestination: View {
} }
case .fyz4Ex(let id): case .fyz4Ex(let id):
FyzQuizScreen(vm: vm, year: 4, id: id) FyzQuizScreen(vm: vm, year: 4, id: id)
case .csYears:
CsYearsScreen(vm: vm)
case .csMap:
LessonListScreen(
vm: vm,
title: vm.tr("cs_year1"),
subtitle: vm.tr("cs_sub"),
rows: vm.content.cs.map { l in
let id = l.int("id")
return LessonRow(id: "c\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.csDone(id), destination: .csLesson(id))
}
)
case .csLesson(let id):
if let l = vm.content.csLesson(id) {
CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs(id) })
}
case .cs2Map:
LessonListScreen(
vm: vm,
title: vm.tr("cs_year2"),
subtitle: vm.tr("cs2_sub"),
rows: vm.content.cs2.map { l in
let id = l.int("id")
return LessonRow(id: "c2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.cs2Done(id), destination: .cs2Lesson(id))
}
)
case .cs2Lesson(let id):
if let l = vm.content.cs2Lesson(id) {
CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs2(id) })
}
case .cs3Map:
LessonListScreen(
vm: vm,
title: vm.tr("cs_year3"),
subtitle: vm.tr("cs3_sub"),
rows: vm.content.cs3.map { l in
let id = l.int("id")
return LessonRow(id: "c3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.cs3Done(id), destination: .cs3Lesson(id))
}
)
case .cs3Lesson(let id):
if let l = vm.content.cs3Lesson(id) {
CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs3(id) })
}
case .cs4Map:
LessonListScreen(
vm: vm,
title: vm.tr("cs_year4"),
subtitle: vm.tr("cs4_sub"),
rows: vm.content.cs4.map { l in
let id = l.int("id")
return LessonRow(id: "c4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.cs4Done(id), destination: .cs4Lesson(id))
}
)
case .cs4Lesson(let id):
if let l = vm.content.cs4Lesson(id) {
CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs4(id) })
}
case .tgYears: case .tgYears:
TgYearsScreen(vm: vm) TgYearsScreen(vm: vm)
case .tgMap: case .tgMap:
@ -2970,3 +3092,251 @@ struct RouteDestination: View {
} }
} }
} }
struct CodeLessonScreen: View {
@ObservedObject var vm: AppModel
let lesson: J
let onSuccess: () -> Void
@State private var code = ""
@State private var stdin = ""
@State private var output = ""
@State private var running = false
var body: some View {
GeometryReader { geo in
if geo.size.width >= 840 {
HStack(alignment: .top, spacing: 16) {
explanation.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
editor.frame(maxWidth: .infinity, maxHeight: .infinity)
outputPane.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
} else {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
explanation
editor.frame(minHeight: 240)
outputPane.frame(minHeight: 160)
}
}
}
}
.padding(16)
.detailScreen(vm.tr(lesson.str("titleKey")), subtitle: vm.tr(lesson.str("subKey")))
.onAppear {
if code.isEmpty { code = lesson.str("code") }
}
}
private var explanation: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
ForEach(Array(lesson.arr("slides").enumerated()), id: \.offset) { _, slide in
VStack(alignment: .leading, spacing: 8) {
Text(slide.str("kicker"))
.font(.subheadline.weight(.semibold))
.foregroundStyle(Color.accentColor)
Text(slide.str("title"))
.font(.title3.bold())
ForEach(slide.strs("lines"), id: \.self) { line in
Text("• \(line)")
.frame(maxWidth: .infinity, alignment: .leading)
}
if let tip = slide.strOrNull("tip") {
Text(tip)
.foregroundStyle(.secondary)
}
}
}
}
}
}
private var editor: some View {
VStack(alignment: .leading, spacing: 8) {
Text(vm.tr("cs_code")).font(.headline)
TextEditor(text: $code)
.font(.system(.body, design: .monospaced))
.scrollContentBackground(.hidden)
.padding(8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(codeFill, in: RoundedRectangle(cornerRadius: 12))
Text(vm.tr("cs_stdin"))
.font(.caption)
.foregroundStyle(.secondary)
TextField("", text: $stdin, axis: .vertical)
.font(.system(.body, design: .monospaced))
.lineLimit(2...4)
.textFieldStyle(.roundedBorder)
}
}
private var outputPane: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Button(vm.tr("cs_run")) { run() }
.buttonStyle(.borderedProminent)
.disabled(running)
Button(vm.tr("cs_reset")) { code = lesson.str("code") }
.disabled(running)
}
Text(vm.tr("cs_output")).font(.headline)
ScrollView {
Text(output.isEmpty ? vm.tr("cs_empty") : output)
.font(.system(.body, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .topLeading)
.textSelection(.enabled)
}
.padding(12)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(codeFill, in: RoundedRectangle(cornerRadius: 12))
}
}
private var codeFill: Color {
#if os(macOS)
Color(.textBackgroundColor).opacity(0.55)
#else
Color(.secondarySystemBackground)
#endif
}
private func run() {
running = true
output = vm.tr("cs_running")
CSharpRunner.run(code, stdin: stdin) { status, text in
running = false
switch status {
case "unavailable":
output = vm.tr("cs_unavailable")
case "timeout":
output = (text + "\n\n" + vm.tr("cs_timeout")).trimmingCharacters(in: .whitespacesAndNewlines)
default:
output = text.isEmpty ? vm.tr("cs_empty") : text
}
if status == "ok" { onSuccess() }
}
}
}
enum CSharpRunner {
static func run(_ source: String, stdin: String, done: @escaping (String, String) -> Void) {
#if os(macOS)
DispatchQueue.global(qos: .userInitiated).async {
let result = runMac(source, stdin: stdin)
DispatchQueue.main.async { done(result.0, result.1) }
}
#else
done("unavailable", "")
#endif
}
#if os(macOS)
private static func runMac(_ source: String, stdin: String) -> (String, String) {
guard source.count <= 100_000 else { return ("fail", "Program je příliš dlouhý.") }
guard let dotnet = findDotnet() else { return ("unavailable", "") }
let dir = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".graduately/csharp-lab")
let csproj = dir.appendingPathComponent("Lab.csproj")
if !FileManager.default.fileExists(atPath: csproj.path) {
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let framework = targetFramework(dotnet)
let xml = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>\(framework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
"""
try? xml.write(to: csproj, atomically: true, encoding: .utf8)
let restored = command(dotnet, ["restore", dir.path], dir, "", 90)
if restored.0 != "ok" { return restored }
}
do {
try source.write(to: dir.appendingPathComponent("Program.cs"), atomically: true, encoding: .utf8)
} catch {
return ("fail", error.localizedDescription)
}
var args = ["run", "--project", dir.path, "--verbosity", "quiet"]
if FileManager.default.fileExists(atPath: dir.appendingPathComponent("obj").path) {
args.append("--no-restore")
}
return command(dotnet, args, dir, stdin, 20)
}
private static func command(_ dotnet: String, _ args: [String], _ dir: URL, _ stdin: String, _ seconds: Double) -> (String, String) {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: dotnet)
proc.arguments = args
proc.currentDirectoryURL = dir
var env = ProcessInfo.processInfo.environment
let root = (dotnet as NSString).deletingLastPathComponent
env["PATH"] = [root, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].joined(separator: ":")
env["DOTNET_ROOT"] = root
env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
env["DOTNET_NOLOGO"] = "1"
env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"
proc.environment = env
let out = Pipe()
let input = Pipe()
proc.standardOutput = out
proc.standardError = out
proc.standardInput = input
var collected = Data()
let lock = NSLock()
out.fileHandleForReading.readabilityHandler = { handle in
let chunk = handle.availableData
guard !chunk.isEmpty else { return }
lock.lock()
collected.append(chunk)
lock.unlock()
}
do { try proc.run() } catch { return ("fail", error.localizedDescription) }
if let data = stdin.data(using: .utf8) { input.fileHandleForWriting.write(data) }
try? input.fileHandleForWriting.close()
let group = DispatchGroup()
group.enter()
proc.terminationHandler = { _ in group.leave() }
if group.wait(timeout: .now() + seconds) == .timedOut {
proc.terminate()
kill(proc.processIdentifier, SIGKILL)
out.fileHandleForReading.readabilityHandler = nil
return ("timeout", clip(collected, lock))
}
out.fileHandleForReading.readabilityHandler = nil
let rest = out.fileHandleForReading.readDataToEndOfFile()
lock.lock()
collected.append(rest)
lock.unlock()
let text = clip(collected, lock)
return proc.terminationStatus == 0 ? ("ok", text) : ("fail", text)
}
private static func clip(_ data: Data, _ lock: NSLock) -> String {
lock.lock()
let copy = data
lock.unlock()
let text = String(data: copy, encoding: .utf8) ?? ""
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.count <= 16_000 ? trimmed : String(trimmed.prefix(16_000)) + "\n…"
}
private static func targetFramework(_ dotnet: String) -> String {
let version = command(dotnet, ["--version"], URL(fileURLWithPath: NSTemporaryDirectory()), "", 20)
let major = version.1.split(separator: ".").first.map(String.init) ?? ""
return major.allSatisfy(\.isNumber) && !major.isEmpty ? "net\(major).0" : "net8.0"
}
private static func findDotnet() -> String? {
let names = [
"/usr/local/share/dotnet/dotnet",
"/opt/homebrew/share/dotnet/dotnet",
"/usr/local/bin/dotnet",
NSHomeDirectory() + "/.dotnet/dotnet",
]
return names.first { FileManager.default.isExecutableFile(atPath: $0) }
}
#endif
}

View file

@ -116,9 +116,20 @@ def parse_doc(path: Path) -> Doc:
para: Node | None = None para: Node | None = None
table: Node | None = None table: Node | None = None
in_comment = False in_comment = False
for n in range(i, len(lines)): indexes = iter(range(i, len(lines)))
for n in indexes:
ln = lines[n].rstrip() ln = lines[n].rstrip()
no = n + 1 no = n + 1
if ln.startswith("```"):
para = table = None
body: list[str] = []
for n2 in indexes:
raw_line = lines[n2]
if raw_line.rstrip().startswith("```"):
break
body.append(raw_line)
nodes.append(Node("code", "\n".join(body).strip("\n"), no))
continue
if in_comment: if in_comment:
if "-->" in ln: if "-->" in ln:
in_comment = False in_comment = False
@ -539,6 +550,36 @@ def lesson_files(directory: Path) -> list[Path]:
return files return files
def code_of(p: Part | None) -> str:
if p is None:
return ""
for nd in p.nodes:
if nd.kind == "code":
return nd.text
return ""
def code_lessons(directory: Path, prefix: str) -> list[dict]:
"""Lessons with an explanation and a C# program the student can edit and run."""
out = []
for i, path in enumerate(lesson_files(directory), start=1):
doc, en = parse_doc(path), en_doc(path)
load_translations(en)
lid = lesson_id(doc, f"{prefix}.{i}")
code = code_of(part(doc, "Kód", "Code"))
if not code.strip():
fail(path, None, "lekce nemá ukázkový kód v sekci # Kód")
out.append({
"id": i,
"key": lid,
"titleKey": keyed(f"{prefix}_unit{i}", doc, en, "nazev"),
"subKey": keyed(f"{prefix}_unit{i}_sub", doc, en, "popis"),
"slides": read_slides(doc, part(doc, "Výklad")),
"code": code,
})
return out
def std_lessons(directory: Path, prefix: str) -> list[dict]: def std_lessons(directory: Path, prefix: str) -> list[dict]:
"""Lessons with slides and a choice quiz (IT, civics, literature, sciences, physics).""" """Lessons with slides and a choice quiz (IT, civics, literature, sciences, physics)."""
out = [] out = []
@ -1068,6 +1109,10 @@ STD = [
("technicka-grafika/1-rocnik", "tg"), ("technicka-grafika/2-rocnik", "tg2"), ("technicka-grafika/1-rocnik", "tg"), ("technicka-grafika/2-rocnik", "tg2"),
("technicka-grafika/3-rocnik", "tg3"), ("technicka-grafika/4-rocnik", "tg4"), ("technicka-grafika/3-rocnik", "tg3"), ("technicka-grafika/4-rocnik", "tg4"),
] ]
CODE = [
("programovani/1-rocnik", "cs"), ("programovani/2-rocnik", "cs2"),
("programovani/3-rocnik", "cs3"), ("programovani/4-rocnik", "cs4"),
]
MATH = [ MATH = [
("matematika/0-opakovani", "mat0"), ("matematika/1-rocnik", "mat"), ("matematika/0-opakovani", "mat0"), ("matematika/1-rocnik", "mat"),
("matematika/2-rocnik", "mat2"), ("matematika/3-rocnik", "mat3"), ("matematika/2-rocnik", "mat2"), ("matematika/3-rocnik", "mat3"),
@ -1091,6 +1136,8 @@ def build(content: Path = CONTENT) -> dict:
payload[key] = std_lessons(content / d, key) payload[key] = std_lessons(content / d, key)
for d, key in MATH: for d, key in MATH:
payload[key] = math_lessons(content / d, key) payload[key] = math_lessons(content / d, key)
for d, key in CODE:
payload[key] = code_lessons(content / d, key)
for d, tag in SKILLS: for d, tag in SKILLS:
for year in range(1, 5): for year in range(1, 5):
payload[tag if year == 1 else f"{tag}{year}"] = skill_lessons( payload[tag if year == 1 else f"{tag}{year}"] = skill_lessons(