Refactor networks quizzes, progress tracking, and page wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-11 18:34:06 +02:00
commit 02c994fb5e
7 changed files with 406 additions and 945 deletions

View file

@ -72,17 +72,17 @@ GTK 4 interface.
8. Přepojování – slides + quiz
9. Vrstevnaté modely a protokoly – slides + quiz
10. Model ISO/OSI a TCP/IP – slides + quiz
Progress for networks is **not** saved yet (unlike Deutsch); the Statistics
page still treats networks as without tracked units
Progress for networks is saved to `progress/net.conf` (one flag per unit);
completed network units show a checkmark on the path and count in Statistics
- **Progress tracking** – completed exercises are marked green; progress is
saved per unit to `progress/unit1.conf`, `progress/unit2.conf` and
`progress/unit3.conf` and restored on startup
- **Statistics** – chart icon in the header opens a per-subject progress
overview: summary totals (exercises done, percent, finished units) at the
top, then every subject with its own progress bar (subjects without tracked
content yet — including networks for now — are shown as locked), and a
per-unit breakdown with progress bars for Deutsch; numbers update as soon
as you finish an exercise
content yet are shown as locked; **networks** use lesson completion from
`progress/net.conf`), and a per-unit breakdown with progress bars for
Deutsch; numbers update as soon as you finish an exercise
- **Settings** – gear icon in the top-right header opens a panel where you can
choose:
- **mode**: dark or light, as a segmented control
@ -295,15 +295,15 @@ When launched, the application shows the welcome screen. From there:
screen.
7. Open **Statistiky / Statistics** (chart icon, top-right) to see progress
grouped per subject (overall totals plus, for Deutsch, a per-unit
breakdown). Network progress is not tracked in Statistics yet.
breakdown). Network lesson completion is included in the subject totals.
8. Open **Nastavení / Settings** (gear icon, top-right) to switch dark/light
mode, pick a color theme, or change the interface language.
German units 1–3 and network units 1–10 are currently implemented. Locked
nodes do nothing – they are placeholders until lessons are added. Completed
Deutsch exercises stay green and are saved per unit to `progress/unit1.conf` /
`progress/unit2.conf` / `progress/unit3.conf` (created next to the app on first
finish).
`progress/unit2.conf` / `progress/unit3.conf`. Completed network units are
saved to `progress/net.conf`.
#### Keyboard shortcuts
@ -347,6 +347,7 @@ progress/ created at runtime
unit1.conf unit 1 exercise completion state
unit2.conf unit 2 exercise completion state
unit3.conf unit 3 exercise completion state
net.conf computer-networks lesson completion
settings.conf theme, dark/light mode and language preference
```
@ -382,7 +383,7 @@ German in both languages.
- [x] Unit 2 ("Aus aller Welt") – 19 interactive exercises (incl. Hangman)
- [x] Unit 3 ("Bei uns zu Hause") – 15 interactive exercises
- [x] Computer networks units 1–10 (IP/VLSM + nine theory units with quizzes)
- [ ] Progress tracking / statistics for the networks path
- [x] Progress tracking / statistics for the networks path
- [ ] Lesson progression and unlock system for the remaining units
- [ ] Add audio pronunciation features
- [x] Add user settings panel (theme, dark/light mode, language)
@ -466,15 +467,15 @@ moderním GTK 4 rozhraním stylovaným přes CSS.
8. Přepojování – snímky + kvíz
9. Vrstevnaté modely a protokoly – snímky + kvíz
10. Model ISO/OSI a TCP/IP – snímky + kvíz
Postup u sítí se **zatím neukládá** (na rozdíl od Deutsch); ve Statistikách
se sítě stále berou jako předmět bez trackovaných jednotek
Postup u sítí se ukládá do `progress/net.conf` (příznak na jednotku);
dokončené jednotky mají fajfku na cestě a počítají se ve Statistikách
- **Ukládání postupu** – hotová cvičení zezelenají; stav se ukládá do
`progress/unit1.conf`, `progress/unit2.conf` a `progress/unit3.conf` a při
startu se načte zpět
- **Statistiky** – ikona grafu v hlavičce ukáže postup po předmětech (součty,
procenta, dokončené jednotky), progress bary u každého předmětu (předměty
bez trackovaného obsahu — včetně sítí — jsou zatím jako zamčené) a rozpad
po jednotkách u Deutsch
bez trackovaného obsahu jsou zamčené; **sítě** berou dokončení lekcí z
`progress/net.conf`) a rozpad po jednotkách u Deutsch
- **Nastavení** – ozubené kolečko vpravo nahoře:
- **režim**: tmavý / světlý
- **téma**: 10 barevných palet
@ -684,15 +685,16 @@ Po spuštění se zobrazí úvodní obrazovka. Odtud:
Jednotky 11–30 zůstávají zamčené.
6. Tlačítkem **zpět** vlevo nahoře se vrátíte na předchozí obrazovku.
7. **Statistiky** (ikona grafu vpravo nahoře) ukazují postup po předmětech
(u Deutsch i po jednotkách). Postup sítí se ve Statistikách zatím
nesleduje.
(u Deutsch i po jednotkách). Dokončené síťové lekce se započítávají do
součů předmětu.
8. **Nastavení** (ozubené kolečko) – tmavý/světlý režim, barevné téma nebo
jazyk rozhraní.
Hotové jsou německé jednotky 1–3 a síťové jednotky 1–10. Zamčené uzly nic
nedělají – jsou to placeholdery. Dokončená německá cvičení zůstanou zelená a
ukládají se do `progress/unit1.conf` / `progress/unit2.conf` /
`progress/unit3.conf` (vzniknou vedle aplikace při prvním dokončení).
`progress/unit3.conf`. Dokončené síťové jednotky se ukládají do
`progress/net.conf`.
#### Klávesové zkratky
@ -736,6 +738,7 @@ progress/ vzniká za běhu
unit1.conf stav cvičení jednotky 1
unit2.conf stav cvičení jednotky 2
unit3.conf stav cvičení jednotky 3
net.conf dokončení síťových lekcí
settings.conf téma, režim a jazyk
```
@ -768,7 +771,7 @@ rozhraní.
- [x] Jednotka 2 („Aus aller Welt“) – 19 cvičení (včetně Hangmana)
- [x] Jednotka 3 („Bei uns zu Hause“) – 15 cvičení
- [x] Počítačové sítě: jednotky 1–10 (IP/VLSM + 9 teoretických s kvízy)
- [ ] Ukládání postupu / statistiky pro síťovou cestu
- [x] Ukládání postupu / statistiky pro síťovou cestu
- [ ] Postupné odemykání zbývajících jednotek
- [ ] Audio / výslovnost
- [x] Panel nastavení (téma, režim, jazyk)

21
i18n.c
View file

@ -253,6 +253,27 @@ const TrEntry tr_ui[] = {
"3 slides about ISO/OSI and TCP/IP, then a quiz."},
{"net_ex10_title", "Kvíz: ISO/OSI a TCP/IP",
"Quiz: ISO/OSI and TCP/IP"},
{"net_quiz_intro",
"Vyberte u každé otázky jednu správnou odpověď a stiskněte Zkontrolovat.",
"Choose one correct answer for each question and press Check."},
{"net_quiz2_head", "Kvíz k základním pojmům sítí",
"Quiz on basic networking concepts"},
{"net_quiz3_head", "Kvíz ke klient–server a cloudu",
"Quiz on client–server and cloud"},
{"net_quiz4_head", "Kvíz k přenosu dat a zabezpečení",
"Quiz on data transmission and security"},
{"net_quiz5_head", "Kvíz k signálům a modulaci",
"Quiz on signals and modulation"},
{"net_quiz6_head", "Kvíz k cestě, kanálu a okruhu",
"Quiz on path, channel and circuit"},
{"net_quiz7_head", "Kvíz k multiplexu a směrům přenosu",
"Quiz on multiplexing and transmission directions"},
{"net_quiz8_head", "Kvíz k přepojování",
"Quiz on switching"},
{"net_quiz9_head", "Kvíz k vrstevnatým modelům a protokolům",
"Quiz on layered models and protocols"},
{"net_quiz10_head", "Kvíz k ISO/OSI a TCP/IP",
"Quiz on ISO/OSI and TCP/IP"},
{"net_slide_next", "Další", "Next"},
{"net_slide_prev", "Zpět", "Back"},
{"net_slide_start", "Přejít na cvičení", "Go to the exercise"},

47
main.c
View file

@ -171,26 +171,33 @@ void activate(GtkApplication *app, gpointer user_data) {
gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
gtk_stack_add_named(main_stack, build_stats_page(), "stats");
gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");
gtk_stack_add_named(main_stack, build_net_unit1_page(), "netunit1");
gtk_stack_add_named(main_stack, build_net_exercise_page(), "netex");
gtk_stack_add_named(main_stack, build_net_unit2_page(), "netunit2");
gtk_stack_add_named(main_stack, build_net_unit2_exercise_page(), "netex2");
gtk_stack_add_named(main_stack, build_net_unit3_page(), "netunit3");
gtk_stack_add_named(main_stack, build_net_unit3_exercise_page(), "netex3");
gtk_stack_add_named(main_stack, build_net_unit4_page(), "netunit4");
gtk_stack_add_named(main_stack, build_net_unit4_exercise_page(), "netex4");
gtk_stack_add_named(main_stack, build_net_unit5_page(), "netunit5");
gtk_stack_add_named(main_stack, build_net_unit5_exercise_page(), "netex5");
gtk_stack_add_named(main_stack, build_net_unit6_page(), "netunit6");
gtk_stack_add_named(main_stack, build_net_unit6_exercise_page(), "netex6");
gtk_stack_add_named(main_stack, build_net_unit7_page(), "netunit7");
gtk_stack_add_named(main_stack, build_net_unit7_exercise_page(), "netex7");
gtk_stack_add_named(main_stack, build_net_unit8_page(), "netunit8");
gtk_stack_add_named(main_stack, build_net_unit8_exercise_page(), "netex8");
gtk_stack_add_named(main_stack, build_net_unit9_page(), "netunit9");
gtk_stack_add_named(main_stack, build_net_unit9_exercise_page(), "netex9");
gtk_stack_add_named(main_stack, build_net_unit10_page(), "netunit10");
gtk_stack_add_named(main_stack, build_net_unit10_exercise_page(), "netex10");
{
typedef GtkWidget *(*NetBuilder)(void);
static const struct {
const char *unit_name;
const char *ex_name;
NetBuilder build_unit;
NetBuilder build_ex;
} net_pages[] = {
{"netunit1", "netex1", build_net_unit1_page, build_net_exercise_page},
{"netunit2", "netex2", build_net_unit2_page, build_net_unit2_exercise_page},
{"netunit3", "netex3", build_net_unit3_page, build_net_unit3_exercise_page},
{"netunit4", "netex4", build_net_unit4_page, build_net_unit4_exercise_page},
{"netunit5", "netex5", build_net_unit5_page, build_net_unit5_exercise_page},
{"netunit6", "netex6", build_net_unit6_page, build_net_unit6_exercise_page},
{"netunit7", "netex7", build_net_unit7_page, build_net_unit7_exercise_page},
{"netunit8", "netex8", build_net_unit8_page, build_net_unit8_exercise_page},
{"netunit9", "netex9", build_net_unit9_page, build_net_unit9_exercise_page},
{"netunit10", "netex10", build_net_unit10_page, build_net_unit10_exercise_page},
};
for (guint i = 0; i < G_N_ELEMENTS(net_pages); i++) {
gtk_stack_add_named(main_stack, net_pages[i].build_unit(),
net_pages[i].unit_name);
gtk_stack_add_named(main_stack, net_pages[i].build_ex(),
net_pages[i].ex_name);
}
}
for (int i = 0; i < NUM_UNLOCKED; i++) {
UnitCtx *u = &units[i];

View file

@ -18,6 +18,7 @@
#define PROGRESS_U2 "progress/unit2.conf"
#define PROGRESS_U3 "progress/unit3.conf"
#define SETTINGS_FILE "progress/settings.conf"
#define PROGRESS_NET "progress/net.conf"
#define NODE_SIZE 88.0
#define PATH_SPAC 240.0
#define ROAD_MX 150.0 /* horizontal canvas margin */
@ -44,7 +45,6 @@
#define HM_ROW3 19 /* start of the ZXCV row */
#define HM_ROW4 26 /* start of the umlaut row */
#define NET_UNITS 30
#define NET_GRID_COLS 6
#define NET_SLIDES 4
#define NET2_SLIDES 5
#define NET3_SLIDES 4
@ -487,6 +487,11 @@ typedef struct {
guint n_slides;
const char *unit_page;
const char *ex_page;
GArray *note_kicks;
GArray *note_titles;
GArray *note_bodies;
GtkWidget *done_icon;
gboolean done;
} NetLesson;
typedef struct {
const char *kicker;
@ -498,6 +503,7 @@ typedef struct {
const ChoiceQ *qs;
int n;
int n_opts;
int lesson_id;
GtkWidget *feedback;
GtkToggleButton **toggles;
GtkWidget **hints;
@ -663,9 +669,6 @@ extern const U3Kw ex14_u3_qs[];
extern const U3Let ex15_u3_rows[];
extern NetLesson net_lessons[NET_LESSONS];
extern GtkWidget *net_note_host;
extern GArray *net_note_kicks;
extern GArray *net_note_titles;
extern GArray *net_note_bodies;
extern int net_note_last_w;
extern GtkWidget *net_scroll;
extern GtkWidget *net_fixed;
@ -700,7 +703,6 @@ void i18n_apply_one(const I18nBind *b);
void i18n_bind(GtkWidget *widget, const char *key, int kind);
void refresh_welcome_heading(void);
void refresh_stats_ui(void);
void net_lessons_apply_lang(void);
void net_rail_theme_reset(void);
void apply_language(void);
char *read_css_file(void);
@ -738,6 +740,11 @@ void load_progress(void);
void all_rails_redraw(void);
void refresh_completion_ui(void);
void mark_done(UnitCtx *u, int n);
void net_load_progress(void);
void net_save_progress(void);
void mark_net_done(int lesson_id);
void refresh_net_completion_ui(void);
void progress_for_net(ProgressSum *out);
GtkWidget *make_back_button(const char *target);
void on_nav_clicked(GtkButton *button, gpointer user_data);
int unit_done_count(const UnitCtx *u);
@ -1035,6 +1042,9 @@ void net_qz_header(GtkWidget *box, const char *text);
GtkWidget *net_qz_make_entry(void);
GtkWidget *build_net_exercise_page(void);
void net_mcq_check(GtkButton *button, gpointer data);
GtkWidget *build_net_mcq_page(int lesson_id, const char *back_page,
const char *title_key, const char *heading_key,
const ChoiceQ *qs, const char **hints, int n);
GtkWidget *build_net_unit2_exercise_page(void);
GtkWidget *build_net_unit3_exercise_page(void);
GtkWidget *build_net_unit4_exercise_page(void);

1126
net.c

File diff suppressed because it is too large Load diff

View file

@ -52,6 +52,7 @@ void unit_load_progress(UnitCtx *u) {
void load_progress(void) {
for (int i = 0; i < NUM_UNLOCKED; i++)
unit_load_progress(&units[i]);
net_load_progress();
}
/* Redraw the exercise rails of every unit that already has a page. */
@ -122,6 +123,7 @@ void refresh_completion_ui(void) {
}
all_rails_redraw();
refresh_net_completion_ui();
refresh_stats_ui();
}
@ -132,3 +134,94 @@ void mark_done(UnitCtx *u, int n) {
refresh_completion_ui();
unit_save_progress(u);
}
void net_load_progress(void) {
GKeyFile *kf = g_key_file_new();
GError *err = NULL;
if (!g_key_file_load_from_file(kf, PROGRESS_NET, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
return;
}
for (int i = 0; i < NET_LESSONS; i++) {
gchar key[8];
g_snprintf(key, sizeof(key), "%d", i + 1);
net_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
g_key_file_free(kf);
}
void net_save_progress(void) {
GKeyFile *kf = g_key_file_new();
for (int i = 0; i < NET_LESSONS; i++) {
gchar key[8];
g_snprintf(key, sizeof(key), "%d", i + 1);
g_key_file_set_boolean(kf, "done", key, net_lessons[i].done);
}
if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
g_key_file_free(kf);
return;
}
gchar *data = g_key_file_to_data(kf, NULL, NULL);
g_key_file_free(kf);
if (data) {
GError *err = NULL;
g_file_set_contents(PROGRESS_NET, data, -1, &err);
if (err)
g_error_free(err);
g_free(data);
}
}
void refresh_net_completion_ui(void) {
for (int i = 0; i < NET_LESSONS; i++) {
GtkWidget *node = net_nodes[i];
GtkWidget *icon = net_lessons[i].done_icon;
if (!node)
continue;
if (net_lessons[i].done) {
gtk_widget_remove_css_class(node, "current");
gtk_widget_add_css_class(node, "done");
if (icon)
gtk_widget_set_visible(icon, TRUE);
} else {
gtk_widget_remove_css_class(node, "done");
gtk_widget_add_css_class(node, "current");
if (icon)
gtk_widget_set_visible(icon, FALSE);
}
}
}
void mark_net_done(int lesson_id) {
if (lesson_id < 0 || lesson_id >= NET_LESSONS)
return;
if (net_lessons[lesson_id].done)
return;
net_lessons[lesson_id].done = TRUE;
net_save_progress();
refresh_net_completion_ui();
refresh_stats_ui();
}
void progress_for_net(ProgressSum *out) {
out->total_ex += NET_LESSONS;
out->open_units += NET_LESSONS;
for (int i = 0; i < NET_LESSONS; i++) {
if (net_lessons[i].done) {
out->done_ex++;
out->done_units++;
}
}
}

21
stats.c
View file

@ -53,8 +53,12 @@ void refresh_stats_ui(void) {
if (!stats_ui.ex_value)
return;
for (int s = 0; s < NUM_SUBJECTS; s++)
progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
for (int s = 0; s < NUM_SUBJECTS; s++) {
if (s == NET_SUBJ)
progress_for_net(&all);
else
progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
}
pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0;
@ -79,11 +83,15 @@ void refresh_stats_ui(void) {
GtkWidget *count = stats_ui.subj[s].count;
GtkWidget *bar = stats_ui.subj[s].bar;
ProgressSum sp = {0};
gboolean has_content = (s == NET_SUBJ) || sub_unit_count[s] > 0;
if (!count || !bar)
continue;
progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
if (sub_unit_count[s] > 0) {
if (s == NET_SUBJ)
progress_for_net(&sp);
else
progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
if (has_content) {
tmp = g_strdup_printf(tr("stats_ex_fmt"), sp.done_ex, sp.total_ex);
gtk_label_set_text(GTK_LABEL(count), tmp);
g_free(tmp);
@ -345,7 +353,8 @@ GtkWidget *build_stats_page(void) {
GtkWidget *name;
GtkWidget *count;
GtkWidget *bar;
gboolean has_units = sub_unit_count[s] > 0;
gboolean has_units = (s == NET_SUBJ) || sub_unit_count[s] > 0;
gboolean has_deutsch_units = sub_unit_count[s] > 0;
subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(subject, "stats-subject");
@ -379,7 +388,7 @@ GtkWidget *build_stats_page(void) {
gtk_box_append(GTK_BOX(subject), bar);
stats_ui.subj[s].bar = bar;
if (has_units) {
if (has_deutsch_units) {
GtkWidget *units_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(units_box, "stats-units");