diff --git a/README.md b/README.md index 4c22890..03fbf37 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/i18n.c b/i18n.c index 15d2c84..197031e 100644 --- a/i18n.c +++ b/i18n.c @@ -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"}, diff --git a/main.c b/main.c index 3dbab3d..776c319 100644 --- a/main.c +++ b/main.c @@ -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]; diff --git a/maturita.h b/maturita.h index 51695c3..9e31b6d 100644 --- a/maturita.h +++ b/maturita.h @@ -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); diff --git a/net.c b/net.c index 5769cda..5ec8be5 100644 --- a/net.c +++ b/net.c @@ -6,7 +6,7 @@ NetLesson net_lessons[NET_LESSONS] = { /* UI unit N maps to lesson content: unit 1 = IP/VLSM (extra); * units 2–10 = source topics 01–09 (Základní pojmy … ISO/OSI). */ - { .n_slides = NET_SLIDES, .unit_page = "netunit1", .ex_page = "netex" }, + { .n_slides = NET_SLIDES, .unit_page = "netunit1", .ex_page = "netex1" }, { .n_slides = NET2_SLIDES, .unit_page = "netunit2", .ex_page = "netex2" }, { .n_slides = NET3_SLIDES, .unit_page = "netunit3", .ex_page = "netex3" }, { .n_slides = NET4_SLIDES, .unit_page = "netunit4", .ex_page = "netex4" }, @@ -39,17 +39,15 @@ void net_heading(GtkWidget *box, const char *text) { /* Note-card slides with text that scales with the window. */ GtkWidget *net_note_host; -GArray *net_note_kicks; -GArray *net_note_titles; -GArray *net_note_bodies; int net_note_last_w = -1; +static NetLesson *net_notes_target; -void net_notes_ensure(void) { - if (net_note_kicks) +void net_lesson_notes_ensure(NetLesson *L) { + if (L->note_kicks) return; - net_note_kicks = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); - net_note_titles = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); - net_note_bodies = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); + L->note_kicks = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); + L->note_titles = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); + L->note_bodies = g_array_new(FALSE, FALSE, sizeof(GtkWidget *)); } void net_note_font(GtkWidget *l, int px) { @@ -60,11 +58,20 @@ void net_note_font(GtkWidget *l, int px) { pango_attr_list_unref(attrs); } +void net_rescale_lesson_notes(NetLesson *L, int body, int head, int kick) { + if (!L || !L->note_kicks) + return; + for (guint i = 0; i < L->note_kicks->len; i++) + net_note_font(g_array_index(L->note_kicks, GtkWidget *, i), kick); + for (guint i = 0; i < L->note_titles->len; i++) + net_note_font(g_array_index(L->note_titles, GtkWidget *, i), head); + for (guint i = 0; i < L->note_bodies->len; i++) + net_note_font(g_array_index(L->note_bodies, GtkWidget *, i), body); +} + void net_rescale_notes_at(int w) { int body, head, kick; - if (!net_note_kicks) - return; if (w < 160) w = 160; body = w / 46; @@ -75,12 +82,8 @@ void net_rescale_notes_at(int w) { kick = body - 3; if (kick < 11) kick = 11; - for (guint i = 0; i < net_note_kicks->len; i++) - net_note_font(g_array_index(net_note_kicks, GtkWidget *, i), kick); - for (guint i = 0; i < net_note_titles->len; i++) - net_note_font(g_array_index(net_note_titles, GtkWidget *, i), head); - for (guint i = 0; i < net_note_bodies->len; i++) - net_note_font(g_array_index(net_note_bodies, GtkWidget *, i), body); + for (int i = 0; i < NET_LESSONS; i++) + net_rescale_lesson_notes(&net_lessons[i], body, head, kick); } void net_rescale_notes(void) { @@ -118,7 +121,8 @@ void net_note_kicker(GtkWidget *box, const char *text) { gtk_label_set_text(GTK_LABEL(l), text); gtk_box_append(GTK_BOX(box), l); - g_array_append_val(net_note_kicks, l); + if (net_notes_target) + g_array_append_val(net_notes_target->note_kicks, l); } void net_note_title(GtkWidget *box, const char *text) { @@ -126,7 +130,8 @@ void net_note_title(GtkWidget *box, const char *text) { gtk_label_set_text(GTK_LABEL(l), text); gtk_box_append(GTK_BOX(box), l); - g_array_append_val(net_note_titles, l); + if (net_notes_target) + g_array_append_val(net_notes_target->note_titles, l); } void net_note_line(GtkWidget *box, const char *text, gboolean tip) { @@ -135,7 +140,8 @@ void net_note_line(GtkWidget *box, const char *text, gboolean tip) { gtk_label_set_text(GTK_LABEL(l), text); gtk_widget_set_margin_top(l, 2); gtk_box_append(GTK_BOX(box), l); - g_array_append_val(net_note_bodies, l); + if (net_notes_target) + g_array_append_val(net_notes_target->note_bodies, l); } void net_slide_apply(NetLesson *L) { @@ -481,6 +487,7 @@ void net_add_node(GtkFixed *fixed, int index) { icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18); gtk_widget_set_visible(icon, FALSE); gtk_box_append(GTK_BOX(vbox), icon); + net_lessons[index].done_icon = icon; g_signal_connect(card, "clicked", G_CALLBACK(net_open_unit), &net_lessons[index]); } @@ -563,6 +570,7 @@ GtkWidget *build_netmap_page(void) { G_CALLBACK(net_adjust_notify), NULL); net_relayout_later(); + refresh_net_completion_ui(); return page; } @@ -572,9 +580,12 @@ GtkWidget *build_net_unit_page(NetLesson *L, const char *title_key, GtkWidget *page; GtkWidget *scroll; GtkWidget *nav; + char sub_key[64]; - net_notes_ensure(); + net_lesson_notes_ensure(L); + net_notes_target = L; L->n_slides = n_slides; + g_snprintf(sub_key, sizeof(sub_key), "%s_sub", title_key); page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_margin_start(page, 28); @@ -582,7 +593,7 @@ GtkWidget *build_net_unit_page(NetLesson *L, const char *title_key, gtk_widget_set_margin_top(page, 20); gtk_widget_set_margin_bottom(page, 20); - gtk_box_append(GTK_BOX(page), top_bar("netmap", title_key, NULL)); + gtk_box_append(GTK_BOX(page), top_bar("netmap", title_key, sub_key)); scroll = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), @@ -624,6 +635,7 @@ GtkWidget *build_net_unit_page(NetLesson *L, const char *title_key, gtk_widget_add_tick_callback(L->stack, net_note_tick, NULL, NULL); net_note_last_w = -1; net_rescale_notes(); + net_notes_target = NULL; nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); gtk_box_set_homogeneous(GTK_BOX(nav), TRUE); @@ -1294,6 +1306,8 @@ gboolean net_qz_entry_ok(GtkWidget *entry, const char *want) { return got && net_txt_eq(got, want); } +static gboolean net_u1_task_ok[4]; + void net_qz_check(GtkButton *button, gpointer data) { NetQz *q = data; int ok = 0; @@ -1325,10 +1339,15 @@ void net_qz_check(GtkButton *button, gpointer data) { ok++; } - if (ok == 4) + if (ok == 4) { set_feedback(q->feed, TRUE, tr("feedback_ok")); - else + net_u1_task_ok[q->id] = TRUE; + if (net_u1_task_ok[0] && net_u1_task_ok[1] && net_u1_task_ok[2] && + net_u1_task_ok[3]) + mark_net_done(0); + } else { set_feedback(q->feed, FALSE, tr("feedback_retry")); + } } void net_qz_fill(GtkButton *button, gpointer data) { @@ -1348,6 +1367,12 @@ void net_qz_fill(GtkButton *button, gpointer data) { } if (q->note) gtk_widget_set_visible(q->note, TRUE); + if (q->infeasible) { + net_u1_task_ok[q->id] = TRUE; + if (net_u1_task_ok[0] && net_u1_task_ok[1] && net_u1_task_ok[2] && + net_u1_task_ok[3]) + mark_net_done(0); + } } void net_qz_header(GtkWidget *box, const char *text) { @@ -1493,7 +1518,151 @@ GtkWidget *build_net_exercise_page(void) { return page; } -/* ---- Unit 2: multiple-choice quiz on basic networking concepts ---- */ +/* ---- Units 2–10: shared multiple-choice quizzes -------------------- */ + +void net_mcq_check(GtkButton *button, gpointer data) { + NetMcqCtx *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++; + 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")); + mark_net_done(ctx->lesson_id); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); + } +} + +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 *page; + GtkWidget *scroll; + GtkWidget *body; + GtkWidget *check; + GtkWidget *btns; + GtkWidget *head; + GtkWidget *intro; + NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); + int n_opts = qs[0].n_options; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + 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(back_page, title_key, NULL)); + + scroll = gtk_scrolled_window_new(); + 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); + + body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); + + head = gtk_label_new(NULL); + i18n_bind(head, heading_key, 0); + gtk_widget_set_halign(head, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(head), TRUE); + gtk_widget_set_margin_top(head, 6); + gtk_widget_add_css_class(head, "ex-sub"); + gtk_box_append(GTK_BOX(body), head); + + intro = gtk_label_new(NULL); + 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, "ex-prompt"); + gtk_box_append(GTK_BOX(body), intro); + + ctx->qs = qs; + ctx->n = n; + ctx->n_opts = n_opts; + ctx->lesson_id = lesson_id; + ctx->toggles = g_new0(GtkToggleButton *, n * n_opts); + ctx->hints = g_new0(GtkWidget *, n); + + for (int i = 0; i < n; i++) { + GtkWidget *prompt; + GtkWidget *row; + GtkToggleButton *first = NULL; + char *qtext; + + qtext = g_strdup_printf("%d.) %s", i + 1, qs[i].prompt); + prompt = gtk_label_new(qtext); + g_free(qtext); + gtk_widget_set_halign(prompt, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); + gtk_widget_add_css_class(prompt, "ex-prompt"); + gtk_widget_set_margin_top(prompt, 8); + gtk_box_append(GTK_BOX(body), prompt); + + row = gtk_flow_box_new(); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); + gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), row); + + for (int o = 0; o < n_opts; o++) { + GtkWidget *tb = gtk_toggle_button_new_with_label(qs[i].options[o]); + + gtk_widget_add_css_class(tb, "pill"); + gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); + if (!first) + first = GTK_TOGGLE_BUTTON(tb); + gtk_flow_box_append(GTK_FLOW_BOX(row), tb); + ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); + } + + ctx->hints[i] = meaning_add(body, hints[i]); + } + + btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_top(btns, 12); + gtk_box_append(GTK_BOX(body), btns); + + check = gtk_button_new(); + i18n_bind(check, "check", 1); + gtk_widget_add_css_class(check, "pill"); + gtk_widget_set_halign(check, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(btns), check); + g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); + + ctx->feedback = gtk_label_new(""); + gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), ctx->feedback); + + return page; +} const ChoiceQ net2_qs[] = { {"Kolik bitů má jeden byte?", @@ -1532,138 +1701,12 @@ const char *net2_hints[] = { "Server řídí a poskytuje služby, má vyšší výkon", }; -void net_mcq_check(GtkButton *button, gpointer data) { - NetMcqCtx *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++; - 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")); - else - set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short")); -} - GtkWidget *build_net_unit2_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net2_qs); - int n_opts = net2_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit2", "net_ex2_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k základním pojmům sítí"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net2_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net2_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net2_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net2_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(1, "netunit2", "net_ex2_title", "net_quiz2_head", + net2_qs, net2_hints, + (int)G_N_ELEMENTS(net2_qs)); } -/* ---- Unit 3: client–server and cloud quiz --------------------------- */ - const ChoiceQ net3_qs[] = { {"Co typicky dělají servery v síti?", {"Jen tisknou dokumenty", "Ověřují uživatele a povolují přístup", @@ -1704,102 +1747,11 @@ const char *net3_hints[] = { }; GtkWidget *build_net_unit3_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net3_qs); - int n_opts = net3_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit3", "net_ex3_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz ke klient–server a cloudu"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net3_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net3_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net3_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net3_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(2, "netunit3", "net_ex3_title", "net_quiz3_head", + net3_qs, net3_hints, + (int)G_N_ELEMENTS(net3_qs)); } -/* ---- Unit 4: data transmission and security quiz -------------------- */ - const ChoiceQ net4_qs[] = { {"Paralelní přenos znamená:", {"Bit po bitu po jednom vodiči", @@ -1838,102 +1790,11 @@ const char *net4_hints[] = { }; GtkWidget *build_net_unit4_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net4_qs); - int n_opts = net4_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit4", "net_ex4_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k přenosu dat a zabezpečení"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net4_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net4_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net4_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net4_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(3, "netunit4", "net_ex4_title", "net_quiz4_head", + net4_qs, net4_hints, + (int)G_N_ELEMENTS(net4_qs)); } -/* ---- Unit 5: signals and modulation quiz ---------------------------- */ - const ChoiceQ net5_qs[] = { {"Digitální signál nabývá:", {"Libovolných spojitých hodnot", "Diskrétních hodnot 0 a 1", @@ -1970,102 +1831,11 @@ const char *net5_hints[] = { }; GtkWidget *build_net_unit5_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net5_qs); - int n_opts = net5_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit5", "net_ex5_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k signálům a modulaci"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net5_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net5_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net5_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net5_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(4, "netunit5", "net_ex5_title", "net_quiz5_head", + net5_qs, net5_hints, + (int)G_N_ELEMENTS(net5_qs)); } -/* ---- Unit 6: path, channel and circuit quiz ------------------------- */ - const ChoiceQ net6_qs[] = { {"Datový spoj slouží k:", {"Jen měření amplitudy", "Výměně informací", "Jen FM modulaci", @@ -2101,102 +1871,11 @@ const char *net6_hints[] = { }; GtkWidget *build_net_unit6_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net6_qs); - int n_opts = net6_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit6", "net_ex6_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k cestě, kanálu a okruhu"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net6_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net6_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net6_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net6_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(5, "netunit6", "net_ex6_title", "net_quiz6_head", + net6_qs, net6_hints, + (int)G_N_ELEMENTS(net6_qs)); } -/* ---- Unit 7: multiplexing and transmission directions quiz ---------- */ - const ChoiceQ net7_qs[] = { {"Multiplex znamená:", {"Jen jeden uživatel na kanál", @@ -2238,102 +1917,11 @@ const char *net7_hints[] = { }; GtkWidget *build_net_unit7_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net7_qs); - int n_opts = net7_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit7", "net_ex7_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k multiplexu a směrům přenosu"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net7_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net7_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net7_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net7_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(6, "netunit7", "net_ex7_title", "net_quiz7_head", + net7_qs, net7_hints, + (int)G_N_ELEMENTS(net7_qs)); } -/* ---- Unit 8: switching quiz ----------------------------------------- */ - const ChoiceQ net8_qs[] = { {"Přepojování okruhů znamená:", {"Pakety jdou vždy jinou cestou", @@ -2376,102 +1964,11 @@ const char *net8_hints[] = { }; GtkWidget *build_net_unit8_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net8_qs); - int n_opts = net8_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit8", "net_ex8_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k přepojování"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net8_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net8_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net8_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net8_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(7, "netunit8", "net_ex8_title", "net_quiz8_head", + net8_qs, net8_hints, + (int)G_N_ELEMENTS(net8_qs)); } -/* ---- Unit 9: layered models and protocols quiz ---------------------- */ - const ChoiceQ net9_qs[] = { {"Standardizace v sítích především:", {"Zvyšuje nekompatibilitu", "Zajišťuje kompatibilitu mezi výrobci", @@ -2509,102 +2006,11 @@ const char *net9_hints[] = { }; GtkWidget *build_net_unit9_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net9_qs); - int n_opts = net9_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit9", "net_ex9_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k vrstevnatým modelům a protokolům"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net9_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net9_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net9_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net9_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(8, "netunit9", "net_ex9_title", "net_quiz9_head", + net9_qs, net9_hints, + (int)G_N_ELEMENTS(net9_qs)); } -/* ---- Unit 10: ISO/OSI and TCP/IP quiz -------------------------------- */ - const ChoiceQ net10_qs[] = { {"Model ISO/OSI má:", {"4 vrstvy", "5 vrstev", "7 vrstev", "10 vrstev"}, 4, 2}, @@ -2635,96 +2041,8 @@ const char *net10_hints[] = { }; GtkWidget *build_net_unit10_exercise_page(void) { - GtkWidget *page; - GtkWidget *scroll; - GtkWidget *body; - GtkWidget *check; - GtkWidget *btns; - NetMcqCtx *ctx = g_new0(NetMcqCtx, 1); - int n = (int)G_N_ELEMENTS(net10_qs); - int n_opts = net10_qs[0].n_options; - - page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - 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("netunit10", "net_ex10_title", NULL)); - - scroll = gtk_scrolled_window_new(); - 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); - - body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); - - net_heading(body, "Kvíz k ISO/OSI a TCP/IP"); - net_paragraph(body, - "Vyberte u každé otázky jednu správnou odpověď " - "a stiskněte Zkontrolovat."); - - ctx->qs = net10_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 *prompt; - GtkWidget *row; - GtkToggleButton *first = NULL; - char *qtext; - - qtext = g_strdup_printf("%d.) %s", i + 1, net10_qs[i].prompt); - prompt = gtk_label_new(qtext); - g_free(qtext); - gtk_widget_set_halign(prompt, GTK_ALIGN_START); - gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); - gtk_widget_add_css_class(prompt, "ex-prompt"); - gtk_widget_set_margin_top(prompt, 8); - gtk_box_append(GTK_BOX(body), prompt); - - row = gtk_flow_box_new(); - gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); - gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); - gtk_widget_set_halign(row, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), row); - - for (int o = 0; o < n_opts; o++) { - GtkWidget *tb = - gtk_toggle_button_new_with_label(net10_qs[i].options[o]); - - gtk_widget_add_css_class(tb, "pill"); - gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); - if (!first) - first = GTK_TOGGLE_BUTTON(tb); - gtk_flow_box_append(GTK_FLOW_BOX(row), tb); - ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); - } - - ctx->hints[i] = meaning_add(body, net10_hints[i]); - } - - btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_top(btns, 12); - gtk_box_append(GTK_BOX(body), btns); - - check = gtk_button_new(); - i18n_bind(check, "check", 1); - gtk_widget_add_css_class(check, "pill"); - gtk_widget_set_halign(check, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(btns), check); - g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); - - ctx->feedback = gtk_label_new(""); - gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(body), ctx->feedback); - - return page; + return build_net_mcq_page(9, "netunit10", "net_ex10_title", "net_quiz10_head", + net10_qs, net10_hints, + (int)G_N_ELEMENTS(net10_qs)); } + diff --git a/progress.c b/progress.c index 5b5e5f5..6ac3437 100644 --- a/progress.c +++ b/progress.c @@ -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++; + } + } +} diff --git a/stats.c b/stats.c index 12ae25c..e31ae56 100644 --- a/stats.c +++ b/stats.c @@ -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");