From 9e072cb8aedd18dc8079d07db24a60b97a17762c Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Fri, 25 Sep 2026 21:32:40 +0200 Subject: [PATCH] Group the existing hardware lessons under year 1 and lock later years. Co-authored-by: Cursor --- README.md | 4 +- .../org/maturita/maturita/MainActivity.kt | 3 +- .../main/java/org/maturita/maturita/Route.kt | 9 +- .../java/org/maturita/maturita/ui/Screens.kt | 20 +++- data/changelog.txt | 2 + ios/Maturita/Route.swift | 11 +- ios/Maturita/UI/Screens.swift | 33 +++++- scripts/extract-android-content.py | 2 +- src/hw.c | 105 +++++++++++++++++- src/i18n.c | 7 ++ src/main.c | 3 +- src/maturita.h | 1 + src/search.c | 6 +- src/subjects.c | 2 +- 14 files changed, 191 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a67a995..131a1b0 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses Hause): cvičení, nápovědy, vokabeltraining - **Správa počítačových sítí** — 1. ročník, 27 lekcí (VLSM + teorie a kvízy); ročníky 2–4 jsou zamčené -- **Technické vybavení** — 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení) +- **Technické vybavení** — 1. ročník, 29 lekcí (architektura, data, skříň a deska, karty, disky, vstupní zařízení); ročníky 2–4 jsou zamčené - **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba (82 knih: zápisky, kvíz, sestavení děje); literární teorie je zatím zamčená - statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina @@ -259,7 +259,7 @@ Intel Mac, another architecture, or hacking on the code → exercises, hints, vocabulary training - **Computer networks** — year 1, 27 lessons (VLSM + theory/quizzes); years 2–4 are locked -- **Computer hardware** — 29 lessons (architecture, data, case and board, cards, drives, input devices) +- **Computer hardware** — year 1, 29 lessons (architecture, data, case and board, cards, drives, input devices); years 2–4 are locked - **Czech language** — grammar (20 exercises) and required reading (82 books: notes, quiz, plot ordering); literature theory is still locked - statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English diff --git a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt index e6ed9d2..3791392 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -210,8 +210,9 @@ private fun MaturitaAppBody(vm: AppViewModel) { } } is Route.NetEx -> NetQuizScreen(vm, r.id) + Route.HwYears -> HwYearsScreen(vm) Route.HwMap -> LessonListScreen( - vm, vm.tr("Technické vybavení"), vm.tr("hw_sub"), + vm, vm.tr("hw_year1"), vm.tr("hw_sub"), vm.content.hw.map { l -> val id = l.int("id") LessonRow(vm.tr(l.str("titleKey")), vm.progress.hwDone(id), Route.HwLesson(id)) diff --git a/android/app/src/main/java/org/maturita/maturita/Route.kt b/android/app/src/main/java/org/maturita/maturita/Route.kt index daf5ead..7b9d500 100644 --- a/android/app/src/main/java/org/maturita/maturita/Route.kt +++ b/android/app/src/main/java/org/maturita/maturita/Route.kt @@ -13,6 +13,7 @@ sealed class Route(val page: String) { data object NetMap : Route("netmap") data class NetLesson(val id: Int) : Route("netunit$id") data class NetEx(val id: Int) : Route("netex$id") + data object HwYears : Route("hwyears") data object HwMap : Route("hwmap") data class HwLesson(val id: Int) : Route("hwunit$id") data class HwEx(val id: Int) : Route("hwex$id") @@ -45,9 +46,10 @@ sealed class Route(val page: String) { NetMap -> listOf(Subjects, NetYears, NetMap) is NetLesson -> listOf(Subjects, NetYears, NetMap, this) is NetEx -> listOf(Subjects, NetYears, NetMap, NetLesson(id), this) - HwMap -> listOf(Subjects, HwMap) - is HwLesson -> listOf(Subjects, HwMap, this) - is HwEx -> listOf(Subjects, HwMap, HwLesson(id), this) + HwYears -> listOf(Subjects, HwYears) + HwMap -> listOf(Subjects, HwYears, HwMap) + is HwLesson -> listOf(Subjects, HwYears, HwMap, this) + is HwEx -> listOf(Subjects, HwYears, HwMap, HwLesson(id), this) CzechMap -> listOf(Subjects, CzechMap) Mluvnice -> listOf(Subjects, CzechMap, Mluvnice) is MluvEx -> listOf(Subjects, CzechMap, Mluvnice, this) @@ -66,6 +68,7 @@ fun routeFromPage(page: String): Route? = when (page) { "roadmap" -> Route.Roadmap "netyears" -> Route.NetYears "netmap" -> Route.NetMap + "hwyears" -> Route.HwYears "hwmap" -> Route.HwMap "czechmap" -> Route.CzechMap "mluvnice" -> Route.Mluvnice diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index b822e0c..f7a3868 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -294,7 +294,7 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum { sum.totalEx = vm.content.netLessons.size sum.doneEx = vm.content.netLessons.count { vm.progress.netDone(it.int("id")) } } - "hwmap" -> { + "hwyears", "hwmap" -> { sum.totalEx = vm.content.hw.size sum.doneEx = vm.content.hw.count { vm.progress.hwDone(it.int("id")) } } @@ -477,6 +477,10 @@ private fun buildSearch(vm: AppViewModel): List { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit${l.int("id")}", extra = "site") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex${l.int("id")}") } + add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra = "rocnik") + add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") + add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") + add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked = true, extra = "rocnik") vm.content.hw.forEach { l -> add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit${l.int("id")}", extra = "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex${l.int("id")}") @@ -565,6 +569,20 @@ fun NetYearsScreen(vm: AppViewModel) { } } +@Composable +fun HwYearsScreen(vm: AppViewModel) { + DetailScaffold(vm, vm.tr("Technické vybavení"), vm.tr("hw_years_sub")) { inner -> + LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) { + item { + NavRow(title = vm.tr("hw_year1"), subtitle = vm.tr("hw_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.HwMap) } + } + items(listOf("hw_year2", "hw_year3", "hw_year4")) { key -> + NavRow(title = vm.tr(key), subtitle = vm.tr("hw_year_locked_sub"), locked = true, onClick = null) + } + } + } +} + @Composable fun CzechMapScreen(vm: AppViewModel) { DetailScaffold(vm, vm.tr("Český jazyk a literatura"), vm.tr("czech_sub")) { inner -> diff --git a/data/changelog.txt b/data/changelog.txt index ef65b07..3f7e6ee 100644 --- a/data/changelog.txt +++ b/data/changelog.txt @@ -9,8 +9,10 @@ --- 2026-09-25 cs: Technické vybavení: pět lekcí o digitalizaci zvuku (vzorkování, kvantování, PCM) cs: Technické vybavení: skříň, zdroj, deska, CPU, karty, disky a vstupní zařízení +cs: Technické vybavení: dosavadní lekce jsou 1. ročník, další ročníky jsou zatím zamčené en: Hardware: five lessons on digitizing sound (sampling, quantization, PCM) en: Hardware: case, PSU, board, CPU, cards, drives and input devices +en: Hardware: current lessons are year 1; later years are still locked --- 2026-09-23 cs: Repozitář se jmenuje Graduately; instalátory a updater míří tam diff --git a/ios/Maturita/Route.swift b/ios/Maturita/Route.swift index 2084de6..f25440d 100644 --- a/ios/Maturita/Route.swift +++ b/ios/Maturita/Route.swift @@ -19,6 +19,7 @@ enum Route: Hashable { case netMap case netLesson(Int) case netEx(Int) + case hwYears case hwMap case hwLesson(Int) case hwEx(Int) @@ -43,6 +44,7 @@ enum Route: Hashable { case .netMap: return "netmap" case .netLesson(let id): return "netunit\(id)" case .netEx(let id): return "netex\(id)" + case .hwYears: return "hwyears" case .hwMap: return "hwmap" case .hwLesson(let id): return "hwunit\(id)" case .hwEx(let id): return "hwex\(id)" @@ -86,12 +88,14 @@ enum Route: Hashable { return [.netYears, .netMap, .netLesson(id)] case .netEx(let id): return [.netYears, .netMap, .netLesson(id), .netEx(id)] + case .hwYears: + return [.hwYears] case .hwMap: - return [.hwMap] + return [.hwYears, .hwMap] case .hwLesson(let id): - return [.hwMap, .hwLesson(id)] + return [.hwYears, .hwMap, .hwLesson(id)] case .hwEx(let id): - return [.hwMap, .hwLesson(id), .hwEx(id)] + return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)] case .czechMap: return [.czechMap] case .mluvnice: @@ -118,6 +122,7 @@ func routeFromPage(_ page: String) -> Route? { case "roadmap": return .roadmap case "netyears": return .netYears case "netmap": return .netMap + case "hwyears": return .hwYears case "hwmap": return .hwMap case "czechmap": return .czechMap case "mluvnice": return .mluvnice diff --git a/ios/Maturita/UI/Screens.swift b/ios/Maturita/UI/Screens.swift index c71491d..6eeacd0 100644 --- a/ios/Maturita/UI/Screens.swift +++ b/ios/Maturita/UI/Screens.swift @@ -420,7 +420,7 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum { case "netyears": sum.totalEx = vm.content.netLessons.count sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count - case "hwmap": + case "hwyears", "hwmap": sum.totalEx = vm.content.hw.count sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count case "czechmap": @@ -553,6 +553,10 @@ private func buildSearch(_ vm: AppModel) -> [Hit] { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit\(l.int("id"))", extra: "site") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex\(l.int("id"))") } + add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra: "rocnik") + add(vm.tr("hw_year2"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik") + add(vm.tr("hw_year3"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik") + add(vm.tr("hw_year4"), vm.tr("search_lesson"), "hwyears", locked: true, extra: "rocnik") for l in vm.content.hw { add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware") add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))") @@ -750,6 +754,29 @@ struct NetYearsScreen: View { } } +struct HwYearsScreen: View { + @ObservedObject var vm: AppModel + + var body: some View { + List { + NavigationLink(value: Route.hwMap) { + ListRowLabel(title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub")) + } + ForEach(["hw_year2", "hw_year3", "hw_year4"], id: \.self) { key in + Label { + ListRowLabel(title: vm.tr(key), subtitle: vm.tr("hw_year_locked_sub")) + } icon: { + Image(systemName: "lock.fill") + } + .foregroundStyle(.secondary) + } + } + .navigationTitle(vm.tr("Technické vybavení")) + .navigationSubtitle(vm.tr("hw_years_sub")) + .appListStyle() + } +} + struct LessonRow: Identifiable { let id: String let title: String @@ -1295,10 +1322,12 @@ struct RouteDestination: View { } case .netEx(let id): NetQuizScreen(vm: vm, id: id) + case .hwYears: + HwYearsScreen(vm: vm) case .hwMap: LessonListScreen( vm: vm, - title: vm.tr("Technické vybavení"), + title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub"), rows: vm.content.hw.map { l in let id = l.int("id") diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index f297980..124eff2 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -1479,7 +1479,7 @@ def main() -> int: subjects = [ {"key": "Deutsch", "open": True, "target": "roadmap", "icon": "de"}, {"key": "Správa počítačových sítí", "open": True, "target": "netyears", "icon": "wifi"}, - {"key": "Technické vybavení", "open": True, "target": "hwmap", "icon": "chip"}, + {"key": "Technické vybavení", "open": True, "target": "hwyears", "icon": "chip"}, {"key": "Český jazyk a literatura", "open": True, "target": "czechmap", "icon": "cz"}, {"key": "Občanská nauka", "open": False, "target": None, "icon": "lock"}, {"key": "English", "open": False, "target": None, "icon": "lock"}, diff --git a/src/hw.c b/src/hw.c index 0f0a0a2..d857ad5 100644 --- a/src/hw.c +++ b/src/hw.c @@ -324,6 +324,109 @@ void hw_add_node(GtkFixed *fixed, int index) { gtk_fixed_put(fixed, name, 0, 0); } +static GtkWidget *hw_year_button(int year, gboolean locked) { + static const char *year_keys[] = { + "hw_year1", "hw_year2", "hw_year3", "hw_year4", + }; + GtkWidget *btn; + GtkWidget *row; + GtkWidget *num; + GtkWidget *texts; + GtkWidget *title; + char *num_text; + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "unit-node"); + gtk_widget_set_can_focus(btn, FALSE); + gtk_widget_set_hexpand(btn, TRUE); + gtk_widget_set_size_request(btn, -1, 72); + gtk_widget_set_sensitive(btn, !locked); + if (locked) + gtk_widget_add_css_class(btn, "locked"); + else + gtk_widget_add_css_class(btn, "current"); + + row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_widget_set_valign(row, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(row, 18); + gtk_widget_set_margin_end(row, 18); + gtk_button_set_child(GTK_BUTTON(btn), row); + + num_text = g_strdup_printf("%d", year); + num = gtk_label_new(num_text); + g_free(num_text); + gtk_widget_add_css_class(num, "unit-number"); + gtk_widget_set_valign(num, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), num); + + texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(texts, GTK_ALIGN_START); + gtk_widget_set_hexpand(texts, TRUE); + gtk_box_append(GTK_BOX(row), texts); + + title = gtk_label_new(NULL); + i18n_bind(title, year_keys[year - 1], 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_add_css_class(title, "unit-name"); + gtk_box_append(GTK_BOX(texts), title); + + if (locked) { + GtkWidget *sub = gtk_label_new(NULL); + GtkWidget *icon = icon_area_new(draw_lock_icon, 0.3451, 0.3569, + 0.4392, 18); + + i18n_bind(sub, "hw_year_locked_sub", 0); + gtk_widget_set_halign(sub, GTK_ALIGN_START); + gtk_widget_add_css_class(sub, "unit-name-locked"); + gtk_box_append(GTK_BOX(texts), sub); + gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(row), icon); + } else { + g_object_set_data_full(G_OBJECT(btn), "target", + g_strdup("hwmap"), g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); + } + + return btn; +} + +GtkWidget *build_hwyears_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *list; + int y; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_hexpand(page, TRUE); + gtk_widget_set_vexpand(page, TRUE); + gtk_widget_set_margin_start(page, 32); + gtk_widget_set_margin_end(page, 32); + gtk_widget_set_margin_top(page, 24); + gtk_widget_set_margin_bottom(page, 24); + + gtk_box_append(GTK_BOX(page), + top_bar("subjects", "Technické vybavení", "hw_years_sub")); + + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(scroll, TRUE); + gtk_widget_set_margin_top(scroll, 18); + gtk_box_append(GTK_BOX(page), scroll); + + list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_halign(list, GTK_ALIGN_CENTER); + gtk_widget_set_valign(list, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(list, 360, -1); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list); + + for (y = 1; y <= 4; y++) + gtk_box_append(GTK_BOX(list), hw_year_button(y, y > 1)); + + return page; +} + GtkWidget *build_hwmap_page(void) { GtkWidget *page; GtkWidget *scroll; @@ -344,7 +447,7 @@ GtkWidget *build_hwmap_page(void) { gtk_widget_set_margin_bottom(page, 24); gtk_box_append(GTK_BOX(page), - top_bar("subjects", "Technické vybavení", "hw_sub")); + top_bar("hwyears", "hw_year1", "hw_sub")); scroll = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), diff --git a/src/i18n.c b/src/i18n.c index 347a72d..c20ccfa 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -521,6 +521,13 @@ const TrEntry tr_ui[] = { "This assignment does not fit into a /24 network — tap “Show solution” to see why."}, {"hw_sub", "Vyberte lekci na cestě a začněte procvičovat.", "Pick a unit on the path and start practicing."}, + {"hw_years_sub", "Vyberte ročník a začněte procvičovat.", + "Pick a school year and start practicing."}, + {"hw_year1", "1. ročník", "Year 1"}, + {"hw_year2", "2. ročník", "Year 2"}, + {"hw_year3", "3. ročník", "Year 3"}, + {"hw_year4", "4. ročník", "Year 4"}, + {"hw_year_locked_sub", "Zatím bez obsahu", "No content yet"}, {"hw_unit1", "Architektura počítače", "Computer architecture"}, {"hw_unit1_sub", "3 snímky o Von Neumannovi a schématu a pak kvíz.", "3 slides about von Neumann and the model, then a quiz."}, diff --git a/src/main.c b/src/main.c index 62d6abf..897e494 100644 --- a/src/main.c +++ b/src/main.c @@ -385,7 +385,7 @@ static gboolean shot_later(gpointer data) { static gboolean run_smoke_test(gpointer data) { static const char *pages[] = { "welcome", "subjects", "roadmap", "stats", - "netyears", "netmap", "hwmap", "czechmap", + "netyears", "netmap", "hwyears", "hwmap", "czechmap", "mluvnice", "readinglist", "cetba1984", "cetba1984quiz", "cetba1984dej", "cetbaFuks", "cetbaFuksQuiz", "cetbaFuksDej", @@ -504,6 +504,7 @@ void activate(GtkApplication *app, gpointer user_data) { gtk_stack_add_named(main_stack, build_stats_page(), "stats"); gtk_stack_add_named(main_stack, build_netyears_page(), "netyears"); gtk_stack_add_named(main_stack, build_netmap_page(), "netmap"); + gtk_stack_add_named(main_stack, build_hwyears_page(), "hwyears"); gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap"); gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap"); gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice"); diff --git a/src/maturita.h b/src/maturita.h index 1cbbb47..c8a339b 100644 --- a/src/maturita.h +++ b/src/maturita.h @@ -843,6 +843,7 @@ void progress_for_hw(ProgressSum *out); void hw_rail_theme_reset(void); void czech_rail_theme_reset(void); void hw_lessons_apply_lang(void); +GtkWidget *build_hwyears_page(void); GtkWidget *build_hwmap_page(void); GtkWidget *build_czechmap_page(void); void mluvnice_init(void); diff --git a/src/search.c b/src/search.c index 9131b5a..46345b1 100644 --- a/src/search.c +++ b/src/search.c @@ -31,7 +31,7 @@ static const char *subject_page(int i) { if (i == NET_SUBJ) return "netyears"; if (i == HW_SUBJ) - return "hwmap"; + return "hwyears"; if (i == CZ_SUBJ) return "czechmap"; return "subjects"; @@ -187,6 +187,10 @@ static void catalog_rebuild(void) { catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz"); } + catalog_add_key("hw_year1", "search_lesson", "hwmap", FALSE, 1, "rocnik"); + catalog_add_key("hw_year2", "search_lesson", "hwyears", TRUE, 1, "rocnik"); + catalog_add_key("hw_year3", "search_lesson", "hwyears", TRUE, 1, "rocnik"); + catalog_add_key("hw_year4", "search_lesson", "hwyears", TRUE, 1, "rocnik"); for (i = 1; i <= HW_LESSONS; i++) { g_snprintf(key, sizeof(key), "hw_unit%d", i); g_snprintf(page, sizeof(page), "hwunit%d", i); diff --git a/src/subjects.c b/src/subjects.c index 023cfe6..de191a5 100644 --- a/src/subjects.c +++ b/src/subjects.c @@ -184,7 +184,7 @@ static const char *subject_target(int i) { if (i == NET_SUBJ) return "netyears"; if (i == HW_SUBJ) - return "hwmap"; + return "hwyears"; if (i == CZ_SUBJ) return "czechmap"; return NULL;