Fill in years 2–4 of networks and hardware.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-07 21:07:47 +02:00
commit 10980f24b5
27 changed files with 6345 additions and 64 deletions

View file

@ -946,6 +946,26 @@ GtkWidget *build_dehome_page(void);
void add_de_pages(GtkStack *stack);
void de_load_progress(void);
void progress_for_de(ProgressSum *out);
typedef struct QuizYear QuizYear;
extern QuizYear *qy_net2;
extern QuizYear *qy_net3;
extern QuizYear *qy_net4;
extern QuizYear *qy_hw2;
extern QuizYear *qy_hw3;
extern QuizYear *qy_hw4;
void quizyear_boot(void);
void quizyear_load_all(void);
void quizyear_apply_lang(void);
void progress_for_net_years(ProgressSum *out);
void progress_for_hw_years(ProgressSum *out);
GtkWidget *quizyear_map(QuizYear *Y);
GtkWidget *quizyear_unit(QuizYear *Y, int index, const char *title_key,
const char *sub_key, const NetSlide *slides,
int n_slides);
GtkWidget *quizyear_quiz(QuizYear *Y, int index, const char *title_key,
const char *head_key, const ChoiceQ *qs,
const char **hints, int n);
void add_it_year_pages(GtkStack *stack);
void on2_load_progress(void);
void on2_save_progress(void);
void mark_on2_done(int lesson_id);

View file

@ -383,8 +383,21 @@ static GtkWidget *hw_year_button(int year, gboolean locked) {
gtk_widget_set_valign(icon, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), icon);
} else {
static const char *pages[] = {
"hwmap", "hw2map", "hw3map", "hw4map",
};
static const char *subs[] = {
"hw_sub", "hw2_sub", "hw3_sub", "hw4_sub",
};
GtkWidget *sub = gtk_label_new(NULL);
i18n_bind(sub, subs[year - 1], 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_widget_add_css_class(sub, "quiz-intro");
gtk_box_append(GTK_BOX(texts), sub);
g_object_set_data_full(G_OBJECT(btn), "target",
g_strdup("hwmap"), g_free);
g_strdup(pages[year - 1]), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
}
@ -422,7 +435,7 @@ GtkWidget *build_hwyears_page(void) {
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));
gtk_box_append(GTK_BOX(list), hw_year_button(y, FALSE));
return page;
}

View file

@ -1621,6 +1621,7 @@ const TrEntry tr_ui[] = {
"Drag the story parts into the correct order."},
#include "english_i18n.inc"
#include "german_i18n.inc"
#include "it_years_i18n.inc"
#include "math_i18n.inc"
{NULL, NULL, NULL}
};
@ -2605,6 +2606,7 @@ void on3_lessons_apply_lang(void);
void on4_lessons_apply_lang(void);
void en_lessons_apply_lang(void);
void de_lessons_apply_lang(void);
void quizyear_apply_lang(void);
void lit_lessons_apply_lang(void);
void sci_lessons_apply_lang(void);
void fyz_lessons_apply_lang(void);
@ -2629,6 +2631,7 @@ void apply_language(void) {
on4_lessons_apply_lang();
en_lessons_apply_lang();
de_lessons_apply_lang();
quizyear_apply_lang();
lit_lessons_apply_lang();
sci_lessons_apply_lang();
fyz_lessons_apply_lang();

2606
src/it_year_pages.c Normal file

File diff suppressed because it is too large Load diff

198
src/it_years_i18n.inc Normal file
View file

@ -0,0 +1,198 @@
{"net2_sub", "VLAN, STP, DHCP, DNS, NAT, IPv6, statické směrování a ACL.", "VLANs, STP, DHCP, DNS, NAT, IPv6, static routing and ACLs."},
{"net2_unit1", "VLAN a trunk", "VLANs and trunks"},
{"net2_unit1_sub", "Logické sítě na jednom přepínači a značkovaný spoj.", "Logical networks on one switch and a tagged link."},
{"net2_ex1_title", "Kvíz: VLAN a trunk", "Quiz: VLANs and trunks"},
{"net2_quiz1_head", "Kvíz k tématu VLAN a trunk", "Quiz on VLANs and trunks"},
{"net2_unit2", "STP a smyčky", "STP and loops"},
{"net2_unit2_sub", "Proč smyčka na druhé vrstvě shodí síť a jak ji STP přeruší.", "Why a layer-2 loop takes the network down, and how STP breaks it."},
{"net2_ex2_title", "Kvíz: STP a smyčky", "Quiz: STP and loops"},
{"net2_quiz2_head", "Kvíz k tématu STP a smyčky", "Quiz on STP and loops"},
{"net2_unit3", "DHCP a DNS", "DHCP and DNS"},
{"net2_unit3_sub", "Kdo přiděluje adresu a kdo překládá jméno.", "Who hands out an address, and who translates a name."},
{"net2_ex3_title", "Kvíz: DHCP a DNS", "Quiz: DHCP and DNS"},
{"net2_quiz3_head", "Kvíz k tématu DHCP a DNS", "Quiz on DHCP and DNS"},
{"net2_unit4", "NAT a PAT", "NAT and PAT"},
{"net2_unit4_sub", "Jedna veřejná adresa pro mnoho počítačů.", "One public address for many computers."},
{"net2_ex4_title", "Kvíz: NAT a PAT", "Quiz: NAT and PAT"},
{"net2_quiz4_head", "Kvíz k tématu NAT a PAT", "Quiz on NAT and PAT"},
{"net2_unit5", "IPv6 základy", "IPv6 basics"},
{"net2_unit5_sub", "Delší adresa, jiný zápis a co se nemění.", "A longer address, a different notation, and what stays the same."},
{"net2_ex5_title", "Kvíz: IPv6 základy", "Quiz: IPv6 basics"},
{"net2_quiz5_head", "Kvíz k tématu IPv6 základy", "Quiz on IPv6 basics"},
{"net2_unit6", "Statické směrování", "Static routing"},
{"net2_unit6_sub", "Ruční cesta, metrika a kdy přestane stačit.", "A hand-written route, the metric, and when it is no longer enough."},
{"net2_ex6_title", "Kvíz: Statické směrování", "Quiz: Static routing"},
{"net2_quiz6_head", "Kvíz k tématu Statické směrování", "Quiz on Static routing"},
{"net2_unit7", "ACL a filtrování", "ACLs and filtering"},
{"net2_unit7_sub", "Seznam, který provoz pustí a který zahodí.", "A list that permits traffic or drops it."},
{"net2_ex7_title", "Kvíz: ACL a filtrování", "Quiz: ACLs and filtering"},
{"net2_quiz7_head", "Kvíz k tématu ACL a filtrování", "Quiz on ACLs and filtering"},
{"net2_unit8", "Podniková Wi-Fi", "Enterprise Wi-Fi"},
{"net2_unit8_sub", "SSID, WPA3 a proč jedno heslo na nástěnce nestačí.", "SSIDs, WPA3, and why one password on the wall is not enough."},
{"net2_ex8_title", "Kvíz: Podniková Wi-Fi", "Quiz: Enterprise Wi-Fi"},
{"net2_quiz8_head", "Kvíz k tématu Podniková Wi-Fi", "Quiz on Enterprise Wi-Fi"},
{"net3_sub", "OSPF, směrování mezi VLAN, WAN, VPN, firewall, QoS a dohled.", "OSPF, inter-VLAN routing, WAN, VPN, firewalls, QoS and monitoring."},
{"net3_unit1", "OSPF", "OSPF"},
{"net3_unit1_sub", "Směrovače si samy řeknou, jaké sítě znají.", "Routers tell each other which networks they know."},
{"net3_ex1_title", "Kvíz: OSPF", "Quiz: OSPF"},
{"net3_quiz1_head", "Kvíz k tématu OSPF", "Quiz on OSPF"},
{"net3_unit2", "Směrování mezi VLAN", "Inter-VLAN routing"},
{"net3_unit2_sub", "Jedna brána pro každou VLAN.", "One gateway for each VLAN."},
{"net3_ex2_title", "Kvíz: Směrování mezi VLAN", "Quiz: Inter-VLAN routing"},
{"net3_quiz2_head", "Kvíz k tématu Směrování mezi VLAN", "Quiz on Inter-VLAN routing"},
{"net3_unit3", "WAN", "WAN"},
{"net3_unit3_sub", "Spoj mezi pobočkami a k poskytovateli.", "A link between sites and towards the provider."},
{"net3_ex3_title", "Kvíz: WAN", "Quiz: WAN"},
{"net3_quiz3_head", "Kvíz k tématu WAN", "Quiz on WAN"},
{"net3_unit4", "VPN", "VPN"},
{"net3_unit4_sub", "Šifrovaný tunel přes síť, které nevěříte.", "An encrypted tunnel across a network you do not trust."},
{"net3_ex4_title", "Kvíz: VPN", "Quiz: VPN"},
{"net3_quiz4_head", "Kvíz k tématu VPN", "Quiz on VPN"},
{"net3_unit5", "Firewall a zóny", "Firewalls and zones"},
{"net3_unit5_sub", "Kdo s kým smí mluvit, ne jen jestli kabel svítí.", "Who may talk to whom, not only whether the cable is lit."},
{"net3_ex5_title", "Kvíz: Firewall a zóny", "Quiz: Firewalls and zones"},
{"net3_quiz5_head", "Kvíz k tématu Firewall a zóny", "Quiz on Firewalls and zones"},
{"net3_unit6", "QoS", "QoS"},
{"net3_unit6_sub", "Když je linka plná, někdo musí dostat přednost.", "When the link is full, someone has to go first."},
{"net3_ex6_title", "Kvíz: QoS", "Quiz: QoS"},
{"net3_quiz6_head", "Kvíz k tématu QoS", "Quiz on QoS"},
{"net3_unit7", "Dohled sítě", "Watching the network"},
{"net3_unit7_sub", "SNMP, syslog a co z grafu opravdu plyne.", "SNMP, syslog, and what a graph really tells you."},
{"net3_ex7_title", "Kvíz: Dohled sítě", "Quiz: Watching the network"},
{"net3_quiz7_head", "Kvíz k tématu Dohled sítě", "Quiz on Watching the network"},
{"net3_unit8", "Záloha konfigurace", "Configuration backups"},
{"net3_unit8_sub", "Co uložit, než se přepínač vymění.", "What to save before a switch is replaced."},
{"net3_ex8_title", "Kvíz: Záloha konfigurace", "Quiz: Configuration backups"},
{"net3_quiz8_head", "Kvíz k tématu Záloha konfigurace", "Quiz on Configuration backups"},
{"net4_sub", "Návrh adres, redundance, 802.1X, hledání chyb, cloud a maturita.", "Address plans, redundancy, 802.1X, troubleshooting, cloud and the exam."},
{"net4_unit1", "Adresní plán", "An address plan"},
{"net4_unit1_sub", "Sítě se navrhují dřív, než se zapojí první kabel.", "Networks are designed before the first cable is patched."},
{"net4_ex1_title", "Kvíz: Adresní plán", "Quiz: An address plan"},
{"net4_quiz1_head", "Kvíz k tématu Adresní plán", "Quiz on An address plan"},
{"net4_unit2", "Redundance", "Redundancy"},
{"net4_unit2_sub", "Druhá cesta, která se opravdu použije.", "A second path that is actually used."},
{"net4_ex2_title", "Kvíz: Redundance", "Quiz: Redundancy"},
{"net4_quiz2_head", "Kvíz k tématu Redundance", "Quiz on Redundancy"},
{"net4_unit3", "802.1X", "802.1X"},
{"net4_unit3_sub", "Port pustí data, až když se zařízení představí.", "A port forwards data only after the device identifies itself."},
{"net4_ex3_title", "Kvíz: 802.1X", "Quiz: 802.1X"},
{"net4_quiz3_head", "Kvíz k tématu 802.1X", "Quiz on 802.1X"},
{"net4_unit4", "Hledání chyby", "Troubleshooting"},
{"net4_unit4_sub", "Postup zdola nahoru, ne náhodné restarty.", "Bottom-up steps, not random reboots."},
{"net4_ex4_title", "Kvíz: Hledání chyby", "Quiz: Troubleshooting"},
{"net4_quiz4_head", "Kvíz k tématu Hledání chyby", "Quiz on Troubleshooting"},
{"net4_unit5", "Cloud a hybrid", "Cloud and hybrid networks"},
{"net4_unit5_sub", "Část služeb není ve skříni na chodbě.", "Some services are no longer in the cupboard in the corridor."},
{"net4_ex5_title", "Kvíz: Cloud a hybrid", "Quiz: Cloud and hybrid networks"},
{"net4_quiz5_head", "Kvíz k tématu Cloud a hybrid", "Quiz on Cloud and hybrid networks"},
{"net4_unit6", "Automatizace", "Automation"},
{"net4_unit6_sub", "Opakovanou práci ať dělá skript, ne deset kliknutí.", "Let a script do the repeated work, not ten clicks."},
{"net4_ex6_title", "Kvíz: Automatizace", "Quiz: Automation"},
{"net4_quiz6_head", "Kvíz k tématu Automatizace", "Quiz on Automation"},
{"net4_unit7", "Provoz a maturita", "Operations and the exam"},
{"net4_unit7_sub", "Síť ve škole má pravidla, ne jen blikající porty.", "A school network has rules, not only blinking ports."},
{"net4_ex7_title", "Kvíz: Provoz a maturita", "Quiz: Operations and the exam"},
{"net4_quiz7_head", "Kvíz k tématu Provoz a maturita", "Quiz on Operations and the exam"},
{"net4_unit8", "Dokumentace sítě", "Network documentation"},
{"net4_unit8_sub", "Schéma, které sedí s tím, co je ve zdi.", "A diagram that matches what is actually in the wall."},
{"net4_ex8_title", "Kvíz: Dokumentace sítě", "Quiz: Network documentation"},
{"net4_quiz8_head", "Kvíz k tématu Dokumentace sítě", "Quiz on Network documentation"},
{"hw2_sub", "Monitory, tisk, sběrnice, chlazení, sestavení, UEFI a diagnostika.", "Displays, printing, buses, cooling, assembly, UEFI and diagnostics."},
{"hw2_unit1", "Monitory", "Displays"},
{"hw2_unit1_sub", "Panel, rozlišení a proč 60 Hz není jediné číslo.", "The panel, the resolution, and why 60 Hz is not the only number."},
{"hw2_ex1_title", "Kvíz: Monitory", "Quiz: Displays"},
{"hw2_quiz1_head", "Kvíz k tématu Monitory", "Quiz on Displays"},
{"hw2_unit2", "Tisk a sken", "Print and scan"},
{"hw2_unit2_sub", "Laser, inkoust a síťová fronta.", "Laser, inkjet, and a network queue."},
{"hw2_ex2_title", "Kvíz: Tisk a sken", "Quiz: Print and scan"},
{"hw2_quiz2_head", "Kvíz k tématu Tisk a sken", "Quiz on Print and scan"},
{"hw2_unit3", "Sběrnice a disky", "Buses and drives"},
{"hw2_unit3_sub", "USB, SATA a NVMe nejsou zaměnitelné jen konektorem.", "USB, SATA and NVMe are not interchangeable just because of the plug."},
{"hw2_ex3_title", "Kvíz: Sběrnice a disky", "Quiz: Buses and drives"},
{"hw2_quiz3_head", "Kvíz k tématu Sběrnice a disky", "Quiz on Buses and drives"},
{"hw2_unit4", "Chlazení", "Cooling"},
{"hw2_unit4_sub", "Vzduch musí mít kudy dovnitř a ven.", "Air needs a way in and a way out."},
{"hw2_ex4_title", "Kvíz: Chlazení", "Quiz: Cooling"},
{"hw2_quiz4_head", "Kvíz k tématu Chlazení", "Quiz on Cooling"},
{"hw2_unit5", "Sestavení", "Building a PC"},
{"hw2_unit5_sub", "Pořadí, které nerozbije desku.", "An order that does not break the board."},
{"hw2_ex5_title", "Kvíz: Sestavení", "Quiz: Building a PC"},
{"hw2_quiz5_head", "Kvíz k tématu Sestavení", "Quiz on Building a PC"},
{"hw2_unit6", "UEFI a boot", "UEFI and boot"},
{"hw2_unit6_sub", "Co se děje, než naběhne systém.", "What happens before the operating system starts."},
{"hw2_ex6_title", "Kvíz: UEFI a boot", "Quiz: UEFI and boot"},
{"hw2_quiz6_head", "Kvíz k tématu UEFI a boot", "Quiz on UEFI and boot"},
{"hw2_unit7", "Diagnostika", "Diagnostics"},
{"hw2_unit7_sub", "POST, pípání a minimum, které ještě naběhne.", "POST, beep codes, and the smallest build that still starts."},
{"hw2_ex7_title", "Kvíz: Diagnostika", "Quiz: Diagnostics"},
{"hw2_quiz7_head", "Kvíz k tématu Diagnostika", "Quiz on Diagnostics"},
{"hw2_unit8", "Porty a dokování", "Ports and docks"},
{"hw2_unit8_sub", "Co z notebooku opravdu vyleze.", "What a laptop can actually drive."},
{"hw2_ex8_title", "Kvíz: Porty a dokování", "Quiz: Ports and docks"},
{"hw2_quiz8_head", "Kvíz k tématu Porty a dokování", "Quiz on Ports and docks"},
{"hw3_sub", "RAID, NAS, servery, virtualizace, zálohy, UPS a firmware.", "RAID, NAS, servers, virtualisation, backups, UPS and firmware."},
{"hw3_unit1", "RAID", "RAID"},
{"hw3_unit1_sub", "Více disků jako jeden svazek, ne jako záloha.", "Several drives as one volume, not as a backup."},
{"hw3_ex1_title", "Kvíz: RAID", "Quiz: RAID"},
{"hw3_quiz1_head", "Kvíz k tématu RAID", "Quiz on RAID"},
{"hw3_unit2", "NAS a sítě disků", "NAS and storage networks"},
{"hw3_unit2_sub", "Disky, které nejsou uvnitř jednoho počítače.", "Drives that are not inside one computer."},
{"hw3_ex2_title", "Kvíz: NAS a sítě disků", "Quiz: NAS and storage networks"},
{"hw3_quiz2_head", "Kvíz k tématu NAS a sítě disků", "Quiz on NAS and storage networks"},
{"hw3_unit3", "Servery a rack", "Servers and racks"},
{"hw3_unit3_sub", "Skříň, do které se nedává herní počítač naležato.", "A cabinet that is not a gaming PC on its side."},
{"hw3_ex3_title", "Kvíz: Servery a rack", "Quiz: Servers and racks"},
{"hw3_quiz3_head", "Kvíz k tématu Servery a rack", "Quiz on Servers and racks"},
{"hw3_unit4", "Virtualizace", "Virtualisation"},
{"hw3_unit4_sub", "Více systémů na jednom železe.", "Several systems on one piece of hardware."},
{"hw3_ex4_title", "Kvíz: Virtualizace", "Quiz: Virtualisation"},
{"hw3_quiz4_head", "Kvíz k tématu Virtualizace", "Quiz on Virtualisation"},
{"hw3_unit5", "Zálohování", "Backups"},
{"hw3_unit5_sub", "Kopie, kterou umíte vrátit.", "A copy you can actually restore."},
{"hw3_ex5_title", "Kvíz: Zálohování", "Quiz: Backups"},
{"hw3_quiz5_head", "Kvíz k tématu Zálohování", "Quiz on Backups"},
{"hw3_unit6", "UPS", "UPS"},
{"hw3_unit6_sub", "Minuty na uložení, ne náhradní elektrárna.", "Minutes to shut down cleanly, not a spare power station."},
{"hw3_ex6_title", "Kvíz: UPS", "Quiz: UPS"},
{"hw3_quiz6_head", "Kvíz k tématu UPS", "Quiz on UPS"},
{"hw3_unit7", "Firmware", "Firmware"},
{"hw3_unit7_sub", "Aktualizace, která nejde udělat uprostřed hodiny.", "An update that is not applied in the middle of a lesson."},
{"hw3_ex7_title", "Kvíz: Firmware", "Quiz: Firmware"},
{"hw3_quiz7_head", "Kvíz k tématu Firmware", "Quiz on Firmware"},
{"hw3_unit8", "Měření výkonu", "Measuring performance"},
{"hw3_unit8_sub", "Číslo bez podmínek nic neznamená.", "A number without the conditions means nothing."},
{"hw3_ex8_title", "Kvíz: Měření výkonu", "Quiz: Measuring performance"},
{"hw3_quiz8_head", "Kvíz k tématu Měření výkonu", "Quiz on Measuring performance"},
{"hw4_sub", "Údržba, TPM, nákup, mobily, periferie, ekologie a maturita.", "Maintenance, TPM, buying, mobiles, peripherals, disposal and the exam."},
{"hw4_unit1", "Údržba", "Maintenance"},
{"hw4_unit1_sub", "Čištění a revize dřív, než něco shoří.", "Cleaning and checks before something burns."},
{"hw4_ex1_title", "Kvíz: Údržba", "Quiz: Maintenance"},
{"hw4_quiz1_head", "Kvíz k tématu Údržba", "Quiz on Maintenance"},
{"hw4_unit2", "TPM a Secure Boot", "TPM and Secure Boot"},
{"hw4_unit2_sub", "Železo, které hlídá start systému.", "Hardware that watches the system start."},
{"hw4_ex2_title", "Kvíz: TPM a Secure Boot", "Quiz: TPM and Secure Boot"},
{"hw4_quiz2_head", "Kvíz k tématu TPM a Secure Boot", "Quiz on TPM and Secure Boot"},
{"hw4_unit3", "Nákup sestavy", "Buying a computer"},
{"hw4_unit3_sub", "Nejdřív práce, potom krabice.", "The work comes first, then the box."},
{"hw4_ex3_title", "Kvíz: Nákup sestavy", "Quiz: Buying a computer"},
{"hw4_quiz3_head", "Kvíz k tématu Nákup sestavy", "Quiz on Buying a computer"},
{"hw4_unit4", "Mobilní zařízení", "Mobile devices"},
{"hw4_unit4_sub", "Tablet a telefon ve školní síti.", "Tablets and phones on the school network."},
{"hw4_ex4_title", "Kvíz: Mobilní zařízení", "Quiz: Mobile devices"},
{"hw4_quiz4_head", "Kvíz k tématu Mobilní zařízení", "Quiz on Mobile devices"},
{"hw4_unit5", "Periferie v provozu", "Peripherals in daily use"},
{"hw4_unit5_sub", "Tiskárny, projektory a co se na nich láme.", "Printers, projectors, and what actually breaks."},
{"hw4_ex5_title", "Kvíz: Periferie v provozu", "Quiz: Peripherals in daily use"},
{"hw4_quiz5_head", "Kvíz k tématu Periferie v provozu", "Quiz on Peripherals in daily use"},
{"hw4_unit6", "Ekologie a data", "Disposal and data"},
{"hw4_unit6_sub", "Vyhozený disk pořád obsahuje školu.", "A discarded drive still contains the school."},
{"hw4_ex6_title", "Kvíz: Ekologie a data", "Quiz: Disposal and data"},
{"hw4_quiz6_head", "Kvíz k tématu Ekologie a data", "Quiz on Disposal and data"},
{"hw4_unit7", "Metodika závady", "A fault method"},
{"hw4_unit7_sub", "Jedna věta, která drží celý postup.", "One sentence that holds the whole procedure."},
{"hw4_ex7_title", "Kvíz: Metodika závady", "Quiz: A fault method"},
{"hw4_quiz7_head", "Kvíz k tématu Metodika závady", "Quiz on A fault method"},
{"hw4_unit8", "Opakování k maturitě", "Exam review"},
{"hw4_unit8_sub", "Železo, které umíte pojmenovat a zdůvodnit.", "Hardware you can name and justify."},
{"hw4_ex8_title", "Kvíz: Opakování k maturitě", "Quiz: Exam review"},
{"hw4_quiz8_head", "Kvíz k tématu Opakování k maturitě", "Quiz on Exam review"},

View file

@ -426,6 +426,8 @@ static gboolean run_smoke_test(gpointer data) {
"mluve1", "mluve20",
"netunit1", "netex1", "netunit10", "netex10",
"netunit27", "netex27",
"net2map", "net2unit1", "net2ex1", "net4unit8", "net4ex8",
"hw2map", "hw2unit1", "hw2ex1", "hw4unit8", "hw4ex8",
NULL
};
const char *report = g_getenv("MATURITA_SMOKE");
@ -532,6 +534,7 @@ void activate(GtkApplication *app, gpointer user_data) {
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");
add_it_year_pages(main_stack);
gtk_stack_add_named(main_stack, build_onyears_page(), "onyears");
gtk_stack_add_named(main_stack, build_onmap_page(), "onmap");
add_on_pages(main_stack);

View file

@ -584,8 +584,21 @@ static GtkWidget *net_year_button(int year, gboolean locked) {
gtk_widget_set_valign(icon, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), icon);
} else {
static const char *pages[] = {
"netmap", "net2map", "net3map", "net4map",
};
static const char *subs[] = {
"net_sub", "net2_sub", "net3_sub", "net4_sub",
};
GtkWidget *sub = gtk_label_new(NULL);
i18n_bind(sub, subs[year - 1], 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_widget_add_css_class(sub, "quiz-intro");
gtk_box_append(GTK_BOX(texts), sub);
g_object_set_data_full(G_OBJECT(btn), "target",
g_strdup("netmap"), g_free);
g_strdup(pages[year - 1]), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
}
@ -623,7 +636,7 @@ GtkWidget *build_netyears_page(void) {
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
for (y = 1; y <= 4; y++)
gtk_box_append(GTK_BOX(list), net_year_button(y, y > 1));
gtk_box_append(GTK_BOX(list), net_year_button(y, FALSE));
return page;
}

View file

@ -60,6 +60,7 @@ void load_progress(void) {
on4_load_progress();
en_load_progress();
de_load_progress();
quizyear_load_all();
lit_load_progress();
sci_load_progress();
fyz_load_progress();
@ -259,6 +260,7 @@ void progress_for_net(ProgressSum *out) {
out->done_units++;
}
}
progress_for_net_years(out);
}
void hw_load_progress(void) {
@ -350,6 +352,7 @@ void progress_for_hw(ProgressSum *out) {
out->done_units++;
}
}
progress_for_hw_years(out);
}
void on_load_progress(void) {

451
src/quizyear.c Normal file
View file

@ -0,0 +1,451 @@
#include "graduately.h"
/* Later years of networks and hardware. Year 1 keeps its own path.
* These years are a list of lessons, slides and a four-question quiz. */
#define QY_N 8
#define QY_COUNT 6
struct QuizYear {
const char *prefix;
const char *map_page;
const char *back_page;
const char *title_key;
const char *sub_key;
const char *file;
NetLesson lessons[QY_N];
char unit_page[QY_N][16];
char ex_page[QY_N][16];
char unit_key[QY_N][20];
char sub_keybuf[QY_N][24];
GtkWidget *nodes[QY_N];
};
static QuizYear years[QY_COUNT];
static gboolean qy_ready;
QuizYear *qy_net2;
QuizYear *qy_net3;
QuizYear *qy_net4;
QuizYear *qy_hw2;
QuizYear *qy_hw3;
QuizYear *qy_hw4;
static char map_store[QY_COUNT][16];
static char file_store[QY_COUNT][16];
void quizyear_boot(void) {
static const char *prefix[] = {
"net2", "net3", "net4", "hw2", "hw3", "hw4",
};
static const char *back[] = {
"netyears", "netyears", "netyears",
"hwyears", "hwyears", "hwyears",
};
static const char *title[] = {
"net_year2", "net_year3", "net_year4",
"hw_year2", "hw_year3", "hw_year4",
};
static const char *sub[] = {
"net2_sub", "net3_sub", "net4_sub",
"hw2_sub", "hw3_sub", "hw4_sub",
};
QuizYear **slot[] = {
&qy_net2, &qy_net3, &qy_net4, &qy_hw2, &qy_hw3, &qy_hw4,
};
if (qy_ready)
return;
for (int y = 0; y < QY_COUNT; y++) {
QuizYear *Y = &years[y];
Y->prefix = prefix[y];
Y->back_page = back[y];
Y->title_key = title[y];
Y->sub_key = sub[y];
g_snprintf(map_store[y], sizeof map_store[y], "%smap", prefix[y]);
g_snprintf(file_store[y], sizeof file_store[y], "%s.conf", prefix[y]);
Y->map_page = map_store[y];
Y->file = file_store[y];
for (int i = 0; i < QY_N; i++) {
g_snprintf(Y->unit_page[i], sizeof Y->unit_page[i],
"%sunit%d", prefix[y], i + 1);
g_snprintf(Y->ex_page[i], sizeof Y->ex_page[i],
"%sex%d", prefix[y], i + 1);
g_snprintf(Y->unit_key[i], sizeof Y->unit_key[i],
"%s_unit%d", prefix[y], i + 1);
g_snprintf(Y->sub_keybuf[i], sizeof Y->sub_keybuf[i],
"%s_unit%d_sub", prefix[y], i + 1);
Y->lessons[i].unit_page = Y->unit_page[i];
Y->lessons[i].ex_page = Y->ex_page[i];
Y->lessons[i].n_slides = 3;
}
*slot[y] = Y;
}
qy_ready = TRUE;
}
static char *qy_path(QuizYear *Y) {
return g_build_filename(app_progress_dir, Y->file, NULL);
}
static void qy_save(QuizYear *Y) {
GKeyFile *kf = g_key_file_new();
char *path = qy_path(Y);
gchar *data;
GError *err = NULL;
for (int i = 0; i < QY_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d", i + 1);
g_key_file_set_boolean(kf, "done", key, Y->lessons[i].done);
}
data = g_key_file_to_data(kf, NULL, NULL);
if (!g_file_set_contents(path, data, -1, &err)) {
g_warning("quiz year: %s", err->message);
g_error_free(err);
}
g_free(data);
g_free(path);
g_key_file_free(kf);
}
static void qy_load_one(QuizYear *Y) {
GKeyFile *kf = g_key_file_new();
char *path = qy_path(Y);
GError *err = NULL;
if (!g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
g_free(path);
return;
}
for (int i = 0; i < QY_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d", i + 1);
Y->lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
g_key_file_free(kf);
g_free(path);
}
void quizyear_load_all(void) {
quizyear_boot();
for (int y = 0; y < QY_COUNT; y++)
qy_load_one(&years[y]);
}
static void qy_refresh_node(QuizYear *Y, int index) {
GtkWidget *node = Y->nodes[index];
if (!node)
return;
if (Y->lessons[index].done) {
gtk_widget_remove_css_class(node, "current");
gtk_widget_add_css_class(node, "done");
} else {
gtk_widget_remove_css_class(node, "done");
gtk_widget_add_css_class(node, "current");
}
}
static void qy_mark(QuizYear *Y, int index) {
if (index < 0 || index >= QY_N || Y->lessons[index].done)
return;
Y->lessons[index].done = TRUE;
qy_save(Y);
qy_refresh_node(Y, index);
refresh_stats_ui();
}
static void qy_add(ProgressSum *out, int first, int n) {
for (int y = first; y < first + n; y++) {
out->total_ex += QY_N;
out->open_units += QY_N;
for (int i = 0; i < QY_N; i++) {
if (years[y].lessons[i].done) {
out->done_ex++;
out->done_units++;
}
}
}
}
void progress_for_net_years(ProgressSum *out) {
quizyear_boot();
qy_add(out, 0, 3);
}
void progress_for_hw_years(ProgressSum *out) {
quizyear_boot();
qy_add(out, 3, 3);
}
static void qy_slide_prev(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (L && L->idx > 0) {
L->idx--;
skill_slide_apply(L);
}
}
static void qy_slide_next(GtkButton *button, gpointer data) {
NetLesson *L = data;
(void)button;
if (!L)
return;
if (L->idx + 1 < L->n_slides) {
L->idx++;
skill_slide_apply(L);
} else if (L->ex_page) {
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
}
}
void quizyear_apply_lang(void) {
if (!qy_ready)
return;
for (int y = 0; y < QY_COUNT; y++)
for (int i = 0; i < QY_N; i++)
skill_slide_apply(&years[y].lessons[i]);
}
GtkWidget *quizyear_unit(QuizYear *Y, int index, const char *title_key,
const char *sub_key, const NetSlide *slides,
int n_slides) {
NetLesson *L = &Y->lessons[index];
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *nav;
net_lesson_notes_ensure(L);
net_notes_target = L;
L->n_slides = (guint)n_slides;
gtk_widget_set_margin_start(page, 28);
gtk_widget_set_margin_end(page, 28);
gtk_widget_set_margin_top(page, 20);
gtk_widget_set_margin_bottom(page, 20);
gtk_box_append(GTK_BOX(page), top_bar(Y->map_page, title_key, sub_key));
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, 10);
gtk_box_append(GTK_BOX(page), scroll);
L->stack = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(L->stack),
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack);
for (int s = 0; s < n_slides; s++) {
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
char name[16];
g_snprintf(name, sizeof name, "slide%d", s);
gtk_widget_set_margin_top(card, 8);
gtk_widget_set_margin_bottom(card, 8);
net_note_kicker(card, slides[s].kicker);
net_note_title(card, slides[s].title);
for (int i = 0; i < 8 && slides[s].line[i]; i++)
net_note_line(card, slides[s].line[i], FALSE);
if (slides[s].tip)
net_note_line(card, slides[s].tip, TRUE);
gtk_stack_add_named(GTK_STACK(L->stack), card, name);
}
nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_box_set_homogeneous(GTK_BOX(nav), TRUE);
gtk_widget_set_margin_top(nav, 10);
gtk_box_append(GTK_BOX(page), nav);
L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev"));
gtk_widget_add_css_class(L->prev_btn, "btn-primary");
gtk_box_append(GTK_BOX(nav), L->prev_btn);
g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(qy_slide_prev), L);
L->next_btn = gtk_button_new_with_label(tr("net_slide_next"));
gtk_widget_add_css_class(L->next_btn, "btn-primary");
gtk_box_append(GTK_BOX(nav), L->next_btn);
g_signal_connect(L->next_btn, "clicked", G_CALLBACK(qy_slide_next), L);
net_rescale_lesson_notes(L, 18, 32, 15);
net_notes_target = NULL;
L->idx = 0;
skill_slide_apply(L);
return page;
}
typedef struct {
QuizYear *year;
int index;
const ChoiceQ *qs;
int n;
int n_opts;
GtkWidget *feedback;
GtkToggleButton **toggles;
GtkWidget **hints;
} QyQuiz;
static void qy_check(GtkButton *button, gpointer data) {
QyQuiz *ctx = data;
int ok = 0;
(void)button;
for (int i = 0; i < ctx->n; i++) {
int active = -1;
for (int o = 0; o < ctx->n_opts; o++) {
GtkToggleButton *tb = ctx->toggles[i * ctx->n_opts + o];
gtk_widget_remove_css_class(GTK_WIDGET(tb), "ok");
gtk_widget_remove_css_class(GTK_WIDGET(tb), "wrong");
if (gtk_toggle_button_get_active(tb))
active = o;
}
if (active == ctx->qs[i].correct) {
ok++;
if (active >= 0)
gtk_widget_add_css_class(
GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "ok");
} else if (active >= 0) {
gtk_widget_add_css_class(
GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "wrong");
}
if (ctx->hints && ctx->hints[i])
gtk_widget_set_visible(ctx->hints[i], TRUE);
}
if (ok == ctx->n) {
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
qy_mark(ctx->year, ctx->index);
} else {
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
}
}
GtkWidget *quizyear_quiz(QuizYear *Y, int index, const char *title_key,
const char *head_key, const ChoiceQ *qs,
const char **hints, int n) {
QyQuiz *ctx = g_new0(QyQuiz, 1);
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
GtkWidget *head = gtk_label_new(NULL);
GtkWidget *intro = gtk_label_new(NULL);
GtkWidget *check;
int n_opts = qs[0].n_options;
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(Y->lessons[index].unit_page, title_key, NULL));
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 12);
gtk_box_append(GTK_BOX(page), scroll);
gtk_widget_set_hexpand(body, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
i18n_bind(head, head_key, 0);
gtk_widget_set_halign(head, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(head), TRUE);
gtk_widget_add_css_class(head, "quiz-heading");
gtk_box_append(GTK_BOX(body), head);
i18n_bind(intro, "net_quiz_intro", 0);
gtk_widget_set_halign(intro, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(intro), TRUE);
gtk_widget_add_css_class(intro, "quiz-intro");
gtk_box_append(GTK_BOX(body), intro);
ctx->year = Y;
ctx->index = index;
ctx->qs = qs;
ctx->n = n;
ctx->n_opts = n_opts;
ctx->toggles = g_new0(GtkToggleButton *, n * n_opts);
ctx->hints = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) {
GtkWidget *card = mcq_append_question(body, i + 1, &qs[i],
&ctx->toggles[i * n_opts]);
ctx->hints[i] = meaning_add(card, hints ? hints[i] : NULL);
}
check = gtk_button_new();
i18n_bind(check, "check", 1);
gtk_widget_add_css_class(check, "btn-primary");
gtk_widget_set_halign(check, GTK_ALIGN_START);
gtk_widget_set_margin_top(check, 8);
gtk_box_append(GTK_BOX(body), check);
g_signal_connect(check, "clicked", G_CALLBACK(qy_check), ctx);
ctx->feedback = gtk_label_new("");
gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(ctx->feedback), TRUE);
gtk_box_append(GTK_BOX(body), ctx->feedback);
return page;
}
GtkWidget *quizyear_map(QuizYear *Y) {
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
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(Y->back_page, Y->title_key, Y->sub_key));
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);
gtk_widget_set_halign(list, GTK_ALIGN_CENTER);
gtk_widget_set_size_request(list, 520, -1);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
for (int i = 0; i < QY_N; i++) {
GtkWidget *btn = gtk_button_new();
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
GtkWidget *texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
GtkWidget *title = gtk_label_new(NULL);
GtkWidget *sub = gtk_label_new(NULL);
char badge[8];
GtkWidget *num;
g_snprintf(badge, sizeof badge, "%d", i + 1);
num = gtk_label_new(badge);
gtk_widget_add_css_class(btn, "unit-node");
gtk_widget_add_css_class(btn, "current");
gtk_widget_set_hexpand(btn, TRUE);
gtk_widget_set_margin_start(row, 18);
gtk_widget_set_margin_end(row, 18);
gtk_widget_set_margin_top(row, 10);
gtk_widget_set_margin_bottom(row, 10);
gtk_button_set_child(GTK_BUTTON(btn), row);
gtk_widget_add_css_class(num, "unit-number");
gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), num);
gtk_widget_set_hexpand(texts, TRUE);
gtk_box_append(GTK_BOX(row), texts);
i18n_bind(title, Y->unit_key[i], 0);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_add_css_class(title, "unit-name");
gtk_box_append(GTK_BOX(texts), title);
i18n_bind(sub, Y->sub_keybuf[i], 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_label_set_xalign(GTK_LABEL(sub), 0);
gtk_widget_add_css_class(sub, "quiz-intro");
gtk_box_append(GTK_BOX(texts), sub);
g_object_set_data_full(G_OBJECT(btn), "target",
g_strdup(Y->unit_page[i]), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
Y->nodes[i] = btn;
qy_refresh_node(Y, i);
gtk_box_append(GTK_BOX(list), btn);
}
return page;
}

View file

@ -188,9 +188,9 @@ static void catalog_rebuild(void) {
catalog_add_key("net_years_title", "search_subject", "netyears", FALSE, 0,
"site network ip");
catalog_add_key("net_year1", "search_lesson", "netmap", FALSE, 1, "rocnik");
catalog_add_key("net_year2", "search_lesson", "netyears", TRUE, 1, "rocnik");
catalog_add_key("net_year3", "search_lesson", "netyears", TRUE, 1, "rocnik");
catalog_add_key("net_year4", "search_lesson", "netyears", TRUE, 1, "rocnik");
catalog_add_key("net_year2", "search_lesson", "net2map", FALSE, 1, "rocnik");
catalog_add_key("net_year3", "search_lesson", "net3map", FALSE, 1, "rocnik");
catalog_add_key("net_year4", "search_lesson", "net4map", FALSE, 1, "rocnik");
for (i = 1; i <= NET_LESSONS; i++) {
g_snprintf(key, sizeof(key), "net_unit%d", i);
g_snprintf(page, sizeof(page), "netunit%d", i);
@ -198,11 +198,18 @@ static void catalog_rebuild(void) {
g_snprintf(page, sizeof(page), "netex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
}
for (i = 2; i <= 4; i++) {
for (n = 1; n <= 8; n++) {
g_snprintf(key, sizeof key, "net%d_unit%d", i, n);
g_snprintf(page, sizeof page, "net%dunit%d", i, n);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "site sit");
}
}
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");
catalog_add_key("hw_year2", "search_lesson", "hw2map", FALSE, 1, "rocnik");
catalog_add_key("hw_year3", "search_lesson", "hw3map", FALSE, 1, "rocnik");
catalog_add_key("hw_year4", "search_lesson", "hw4map", FALSE, 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);
@ -210,6 +217,13 @@ static void catalog_rebuild(void) {
g_snprintf(page, sizeof(page), "hwex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
}
for (i = 2; i <= 4; i++) {
for (n = 1; n <= 8; n++) {
g_snprintf(key, sizeof key, "hw%d_unit%d", i, n);
g_snprintf(page, sizeof page, "hw%dunit%d", i, n);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "hardware hw");
}
}
catalog_add_key("on_year1", "search_lesson", "onmap", FALSE, 1, "rocnik");
catalog_add_key("on_year2", "search_lesson", "on2map", FALSE, 1, "rocnik");