From 7b289f0a87a233dab26a2773812c4edf400171a2 Mon Sep 17 00:00:00 2001 From: dvorakmortonxd Date: Wed, 9 Sep 2026 10:24:34 +0200 Subject: [PATCH] added in sections to the statistics --- maturita.c | 291 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 227 insertions(+), 64 deletions(-) diff --git a/maturita.c b/maturita.c index c2bb9b9..9119736 100644 --- a/maturita.c +++ b/maturita.c @@ -7,6 +7,7 @@ #define NUM_UNITS 10 #define NUM_UNLOCKED 3 /* first NUM_UNLOCKED units (index 0..2) are open */ #define MAX_UNIT_EX 20 /* largest exercise count any unit may have */ +#define NUM_SUBJECTS 13 #define PROGRESS_DIR "progress" #define PROGRESS_U1 "progress/unit1.conf" @@ -129,6 +130,12 @@ typedef struct { static UnitCtx units[NUM_UNITS] = {0}; +/* Subject catalogue shared by the subjects page and the statistics page. + * The full definitions live in the "Subjects page" section further down. */ +static const char *sub_keys[NUM_SUBJECTS]; +static const int sub_unit_start[NUM_SUBJECTS]; +static const int sub_unit_count[NUM_SUBJECTS]; + /* Short German bubble titles, indexed 1..n_ex (entry 0 unused). */ static const char *const u1_ex_names[MAX_UNIT_EX + 1] = { NULL, @@ -502,12 +509,11 @@ static const TrEntry tr_ui[] = { {"hm_guess", "Hádejte písmeno", "Guess a letter"}, {"stats", "Statistiky", "Statistics"}, {"stats_title", "Statistiky", "Statistics"}, - {"stats_sub", "Přehled vašeho pokroku v němčině.", - "Overview of your German learning progress."}, + {"stats_sub", "Přehled vašeho pokroku.", "Overview of your progress."}, {"stats_ex_label", "Cvičení", "Exercises"}, {"stats_pct_label", "Hotovo", "Done"}, {"stats_units_label", "Jednotky", "Units"}, - {"stats_section", "Podle jednotek", "By unit"}, + {"stats_section", "Předměty", "Subjects"}, {"stats_locked", "Zamčeno", "Locked"}, {"stats_complete", "Hotovo", "Complete"}, {"stats_ex_fmt", "%d / %d", "%d / %d"}, @@ -1677,6 +1683,45 @@ static char *build_theme_css(const ThemePalette *p) { " letter-spacing: 0.8px;" " margin-bottom: 10px;" "}" + ".stats-subject {" + " margin-bottom: 20px;" + "}" + ".stats-subject-name {" + " color: @fg_text;" + " font-size: 16px;" + " font-weight: 800;" + "}" + ".stats-subject-count {" + " color: @fg_subtext;" + " font-size: 13px;" + " font-weight: 700;" + "}" + ".stats-subject-count.locked {" + " color: @fg_overlay;" + "}" + ".stats-subject-bar," + ".stats-subject-bar trough {" + " min-height: 12px;" + " border-radius: 999px;" + " background-color: alpha(@bg_surface1, 0.85);" + " border: none;" + " padding: 0;" + "}" + ".stats-subject-bar {" + " margin: 4px 0 0 0;" + "}" + ".stats-subject-bar progress {" + " min-height: 10px;" + " border-radius: 999px;" + " background-image: linear-gradient(90deg, @accent 0%%, @accent2 100%%);" + " background-color: @accent;" + " border: none;" + "}" + ".stats-units {" + " margin-top: 8px;" + " border-left: 2px solid alpha(@fg_overlay, 0.22);" + " padding-left: 12px;" + "}" ".stats-unit-row {" " background-color: alpha(@bg_surface0, 0.72);" " border: 1px solid @bg_surface1;" @@ -2236,6 +2281,16 @@ typedef struct { GtkWidget *pct_value; GtkWidget *units_value; GtkWidget *overall_bar; + + /* one progress block per subject (index matches sub_keys[]) */ + struct { + GtkWidget *name; /* subject heading label */ + GtkWidget *count; /* "13 / 47" or the locked text */ + GtkWidget *bar; /* per-subject overall progress bar */ + GtkWidget *units_box; /* holds the unit rows, NULL if no units */ + } subj[NUM_SUBJECTS]; + + /* per-unit rows, indexed by the global units[] index */ GtkWidget *unit_count[NUM_UNITS]; GtkWidget *unit_badge[NUM_UNITS]; GtkWidget *unit_bar[NUM_UNITS]; @@ -2257,31 +2312,48 @@ static int unit_done_count(const UnitCtx *u) { return n; } +typedef struct { + int done_ex; + int total_ex; + int done_units; + int open_units; +} ProgressSum; + +/* Sum progress over units[first .. first+n-1]. Locked placeholder units and + * the finish node (no playable exercises) are ignored. */ +static void progress_for_units(int first, int n, ProgressSum *out) { + for (int j = 0; j < n; j++) { + UnitCtx *u; + int d; + + if (first < 0 || first + j >= NUM_UNITS) + continue; + u = &units[first + j]; + if (!u->unlocked || u->n_ex <= 0) + continue; + d = unit_done_count(u); + out->done_ex += d; + out->total_ex += u->n_ex; + out->open_units++; + if (d == u->n_ex) + out->done_units++; + } +} + static void refresh_stats_ui(void) { - int done_ex = 0; - int total_ex = 0; - int done_units = 0; - int open_units = 0; + ProgressSum all = {0}; int pct; char *tmp; if (!stats_ui.ex_value) return; - for (int i = 0; i < NUM_UNLOCKED; i++) { - UnitCtx *u = &units[i]; - int d = unit_done_count(u); + for (int s = 0; s < NUM_SUBJECTS; s++) + progress_for_units(sub_unit_start[s], sub_unit_count[s], &all); - done_ex += d; - total_ex += u->n_ex; - open_units++; - if (u->n_ex > 0 && d == u->n_ex) - done_units++; - } + pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0; - pct = total_ex > 0 ? (done_ex * 100) / total_ex : 0; - - tmp = g_strdup_printf(tr("stats_ex_fmt"), done_ex, total_ex); + tmp = g_strdup_printf(tr("stats_ex_fmt"), all.done_ex, all.total_ex); gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp); g_free(tmp); @@ -2289,13 +2361,36 @@ static void refresh_stats_ui(void) { gtk_label_set_text(GTK_LABEL(stats_ui.pct_value), tmp); g_free(tmp); - tmp = g_strdup_printf(tr("stats_units_fmt"), done_units, open_units); + tmp = g_strdup_printf(tr("stats_units_fmt"), all.done_units, + all.open_units); gtk_label_set_text(GTK_LABEL(stats_ui.units_value), tmp); g_free(tmp); gtk_progress_bar_set_fraction( GTK_PROGRESS_BAR(stats_ui.overall_bar), - total_ex > 0 ? (double)done_ex / (double)total_ex : 0.0); + all.total_ex > 0 ? (double)all.done_ex / (double)all.total_ex : 0.0); + + for (int s = 0; s < NUM_SUBJECTS; s++) { + GtkWidget *count = stats_ui.subj[s].count; + GtkWidget *bar = stats_ui.subj[s].bar; + ProgressSum sp = {0}; + + if (!count || !bar) + continue; + progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp); + if (sub_unit_count[s] > 0) { + 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); + gtk_progress_bar_set_fraction( + GTK_PROGRESS_BAR(bar), + sp.total_ex > 0 ? (double)sp.done_ex / (double)sp.total_ex + : 0.0); + } else { + gtk_label_set_text(GTK_LABEL(count), tr("stats_locked")); + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), 0.0); + } + } for (int i = 0; i < NUM_UNITS; i++) { UnitCtx *u = &units[i]; @@ -2392,6 +2487,69 @@ static GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out return card; } +/* Build one unit row (index, name, badge, count, bar) for the stats list. + * `num` is the row number shown (1-based within its subject). */ +static GtkWidget *make_unit_stats_row(UnitCtx *u, int num, int global) { + GtkWidget *row; + GtkWidget *head; + GtkWidget *left; + GtkWidget *idx; + GtkWidget *name; + GtkWidget *right; + GtkWidget *count; + GtkWidget *badge; + GtkWidget *bar; + char text[8]; + + row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_add_css_class(row, "stats-unit-row"); + stats_ui.unit_row[global] = row; + + head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); + gtk_box_append(GTK_BOX(row), head); + + left = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_hexpand(left, TRUE); + gtk_box_append(GTK_BOX(head), left); + + g_snprintf(text, sizeof(text), "%d", num); + idx = gtk_label_new(text); + gtk_widget_add_css_class(idx, "stats-unit-index"); + gtk_widget_set_valign(idx, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(left), idx); + + name = gtk_label_new(u->title); + gtk_widget_set_halign(name, GTK_ALIGN_START); + gtk_widget_set_hexpand(name, TRUE); + gtk_label_set_xalign(GTK_LABEL(name), 0.0); + gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END); + gtk_widget_add_css_class(name, "stats-unit-name"); + gtk_box_append(GTK_BOX(left), name); + + right = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_valign(right, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(head), right); + + badge = gtk_label_new(NULL); + gtk_widget_add_css_class(badge, "stats-unit-badge"); + gtk_widget_set_visible(badge, FALSE); + gtk_box_append(GTK_BOX(right), badge); + stats_ui.unit_badge[global] = badge; + + count = gtk_label_new(""); + gtk_widget_add_css_class(count, "stats-unit-count"); + gtk_box_append(GTK_BOX(right), count); + stats_ui.unit_count[global] = count; + + bar = gtk_progress_bar_new(); + gtk_widget_add_css_class(bar, "stats-unit-bar"); + gtk_widget_set_hexpand(bar, TRUE); + gtk_box_append(GTK_BOX(row), bar); + stats_ui.unit_bar[global] = bar; + + return row; +} + static GtkWidget *build_stats_page(void) { GtkWidget *outer; GtkWidget *scroll; @@ -2476,64 +2634,60 @@ static GtkWidget *build_stats_page(void) { list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_box_append(GTK_BOX(page), list); - for (int i = 0; i < NUM_UNITS; i++) { - GtkWidget *row; + for (int s = 0; s < NUM_SUBJECTS; s++) { + GtkWidget *subject; GtkWidget *head; - GtkWidget *left; - GtkWidget *idx; GtkWidget *name; - GtkWidget *right; GtkWidget *count; - GtkWidget *badge; GtkWidget *bar; - char num[8]; + gboolean has_units = sub_unit_count[s] > 0; - row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); - gtk_widget_add_css_class(row, "stats-unit-row"); - gtk_box_append(GTK_BOX(list), row); - stats_ui.unit_row[i] = row; + subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(subject, "stats-subject"); + gtk_box_append(GTK_BOX(list), subject); head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_box_append(GTK_BOX(row), head); + gtk_box_append(GTK_BOX(subject), head); - left = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_hexpand(left, TRUE); - gtk_box_append(GTK_BOX(head), left); - - g_snprintf(num, sizeof(num), "%d", i + 1); - idx = gtk_label_new(num); - gtk_widget_add_css_class(idx, "stats-unit-index"); - gtk_widget_set_valign(idx, GTK_ALIGN_CENTER); - gtk_box_append(GTK_BOX(left), idx); - - name = gtk_label_new(units[i].title); + name = gtk_label_new(NULL); + i18n_bind(name, sub_keys[s], 0); gtk_widget_set_halign(name, GTK_ALIGN_START); gtk_widget_set_hexpand(name, TRUE); gtk_label_set_xalign(GTK_LABEL(name), 0.0); gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END); - gtk_widget_add_css_class(name, "stats-unit-name"); - gtk_box_append(GTK_BOX(left), name); + gtk_widget_add_css_class(name, "stats-subject-name"); + if (!has_units) + gtk_widget_add_css_class(name, "unit-name-locked"); + gtk_box_append(GTK_BOX(head), name); + stats_ui.subj[s].name = name; - right = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_valign(right, GTK_ALIGN_CENTER); - gtk_box_append(GTK_BOX(head), right); - - badge = gtk_label_new(NULL); - gtk_widget_add_css_class(badge, "stats-unit-badge"); - gtk_widget_set_visible(badge, FALSE); - gtk_box_append(GTK_BOX(right), badge); - stats_ui.unit_badge[i] = badge; - - count = gtk_label_new(""); - gtk_widget_add_css_class(count, "stats-unit-count"); - gtk_box_append(GTK_BOX(right), count); - stats_ui.unit_count[i] = count; + count = gtk_label_new(NULL); + gtk_widget_add_css_class(count, "stats-subject-count"); + if (!has_units) + gtk_widget_add_css_class(count, "locked"); + gtk_box_append(GTK_BOX(head), count); + stats_ui.subj[s].count = count; bar = gtk_progress_bar_new(); - gtk_widget_add_css_class(bar, "stats-unit-bar"); + gtk_widget_add_css_class(bar, "stats-subject-bar"); gtk_widget_set_hexpand(bar, TRUE); - gtk_box_append(GTK_BOX(row), bar); - stats_ui.unit_bar[i] = bar; + gtk_box_append(GTK_BOX(subject), bar); + stats_ui.subj[s].bar = bar; + + if (has_units) { + GtkWidget *units_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + gtk_widget_add_css_class(units_box, "stats-units"); + gtk_box_append(GTK_BOX(subject), units_box); + stats_ui.subj[s].units_box = units_box; + + for (int j = 0; j < sub_unit_count[s]; j++) { + int gi = sub_unit_start[s] + j; + + gtk_box_append(GTK_BOX(units_box), + make_unit_stats_row(&units[gi], j + 1, gi)); + } + } } return outer; @@ -3119,7 +3273,6 @@ static GtkWidget *build_roadmap_page(void) { /* Subjects page ("Předměty") - serpentine subject map */ /* ------------------------------------------------------------------ */ -#define NUM_SUBJECTS 13 #define SUB_BUBBLE NODE_SIZE /* round node, matches .unit-node */ #define SUB_MX 110.0 #define SUB_MY 150.0 @@ -3144,6 +3297,16 @@ static const char *sub_keys[NUM_SUBJECTS] = { "Programové vybavení", }; +/* Slice of the global `units[]` table that each subject owns. A start of -1 + * (or a count of 0) means the subject has no content yet; such subjects show + * as locked on the statistics page until their units are added. */ +static const int sub_unit_start[NUM_SUBJECTS] = { + 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; +static const int sub_unit_count[NUM_SUBJECTS] = { + NUM_UNITS, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + static GtkWidget *sub_scroll; static GtkWidget *sub_fixed; static GtkWidget *sub_rail;