added in sections to the statistics

This commit is contained in:
dvorakmortonxd 2026-09-09 10:24:34 +02:00
commit 7b289f0a87

View file

@ -7,6 +7,7 @@
#define NUM_UNITS 10 #define NUM_UNITS 10
#define NUM_UNLOCKED 3 /* first NUM_UNLOCKED units (index 0..2) are open */ #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 MAX_UNIT_EX 20 /* largest exercise count any unit may have */
#define NUM_SUBJECTS 13
#define PROGRESS_DIR "progress" #define PROGRESS_DIR "progress"
#define PROGRESS_U1 "progress/unit1.conf" #define PROGRESS_U1 "progress/unit1.conf"
@ -129,6 +130,12 @@ typedef struct {
static UnitCtx units[NUM_UNITS] = {0}; 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). */ /* Short German bubble titles, indexed 1..n_ex (entry 0 unused). */
static const char *const u1_ex_names[MAX_UNIT_EX + 1] = { static const char *const u1_ex_names[MAX_UNIT_EX + 1] = {
NULL, NULL,
@ -502,12 +509,11 @@ static const TrEntry tr_ui[] = {
{"hm_guess", "Hádejte písmeno", "Guess a letter"}, {"hm_guess", "Hádejte písmeno", "Guess a letter"},
{"stats", "Statistiky", "Statistics"}, {"stats", "Statistiky", "Statistics"},
{"stats_title", "Statistiky", "Statistics"}, {"stats_title", "Statistiky", "Statistics"},
{"stats_sub", "Přehled vašeho pokroku v němčině.", {"stats_sub", "Přehled vašeho pokroku.", "Overview of your progress."},
"Overview of your German learning progress."},
{"stats_ex_label", "Cvičení", "Exercises"}, {"stats_ex_label", "Cvičení", "Exercises"},
{"stats_pct_label", "Hotovo", "Done"}, {"stats_pct_label", "Hotovo", "Done"},
{"stats_units_label", "Jednotky", "Units"}, {"stats_units_label", "Jednotky", "Units"},
{"stats_section", "Podle jednotek", "By unit"}, {"stats_section", "Předměty", "Subjects"},
{"stats_locked", "Zamčeno", "Locked"}, {"stats_locked", "Zamčeno", "Locked"},
{"stats_complete", "Hotovo", "Complete"}, {"stats_complete", "Hotovo", "Complete"},
{"stats_ex_fmt", "%d / %d", "%d / %d"}, {"stats_ex_fmt", "%d / %d", "%d / %d"},
@ -1677,6 +1683,45 @@ static char *build_theme_css(const ThemePalette *p) {
" letter-spacing: 0.8px;" " letter-spacing: 0.8px;"
" margin-bottom: 10px;" " 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 {" ".stats-unit-row {"
" background-color: alpha(@bg_surface0, 0.72);" " background-color: alpha(@bg_surface0, 0.72);"
" border: 1px solid @bg_surface1;" " border: 1px solid @bg_surface1;"
@ -2236,6 +2281,16 @@ typedef struct {
GtkWidget *pct_value; GtkWidget *pct_value;
GtkWidget *units_value; GtkWidget *units_value;
GtkWidget *overall_bar; 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_count[NUM_UNITS];
GtkWidget *unit_badge[NUM_UNITS]; GtkWidget *unit_badge[NUM_UNITS];
GtkWidget *unit_bar[NUM_UNITS]; GtkWidget *unit_bar[NUM_UNITS];
@ -2257,31 +2312,48 @@ static int unit_done_count(const UnitCtx *u) {
return n; 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) { static void refresh_stats_ui(void) {
int done_ex = 0; ProgressSum all = {0};
int total_ex = 0;
int done_units = 0;
int open_units = 0;
int pct; int pct;
char *tmp; char *tmp;
if (!stats_ui.ex_value) if (!stats_ui.ex_value)
return; return;
for (int i = 0; i < NUM_UNLOCKED; i++) { for (int s = 0; s < NUM_SUBJECTS; s++)
UnitCtx *u = &units[i]; progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
int d = unit_done_count(u);
done_ex += d; pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0;
total_ex += u->n_ex;
open_units++;
if (u->n_ex > 0 && d == u->n_ex)
done_units++;
}
pct = total_ex > 0 ? (done_ex * 100) / total_ex : 0; tmp = g_strdup_printf(tr("stats_ex_fmt"), all.done_ex, all.total_ex);
tmp = g_strdup_printf(tr("stats_ex_fmt"), done_ex, total_ex);
gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp); gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp);
g_free(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); gtk_label_set_text(GTK_LABEL(stats_ui.pct_value), tmp);
g_free(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); gtk_label_set_text(GTK_LABEL(stats_ui.units_value), tmp);
g_free(tmp); g_free(tmp);
gtk_progress_bar_set_fraction( gtk_progress_bar_set_fraction(
GTK_PROGRESS_BAR(stats_ui.overall_bar), 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++) { for (int i = 0; i < NUM_UNITS; i++) {
UnitCtx *u = &units[i]; UnitCtx *u = &units[i];
@ -2392,6 +2487,69 @@ static GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out
return card; 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) { static GtkWidget *build_stats_page(void) {
GtkWidget *outer; GtkWidget *outer;
GtkWidget *scroll; GtkWidget *scroll;
@ -2476,64 +2634,60 @@ static GtkWidget *build_stats_page(void) {
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_append(GTK_BOX(page), list); gtk_box_append(GTK_BOX(page), list);
for (int i = 0; i < NUM_UNITS; i++) { for (int s = 0; s < NUM_SUBJECTS; s++) {
GtkWidget *row; GtkWidget *subject;
GtkWidget *head; GtkWidget *head;
GtkWidget *left;
GtkWidget *idx;
GtkWidget *name; GtkWidget *name;
GtkWidget *right;
GtkWidget *count; GtkWidget *count;
GtkWidget *badge;
GtkWidget *bar; GtkWidget *bar;
char num[8]; gboolean has_units = sub_unit_count[s] > 0;
row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(row, "stats-unit-row"); gtk_widget_add_css_class(subject, "stats-subject");
gtk_box_append(GTK_BOX(list), row); gtk_box_append(GTK_BOX(list), subject);
stats_ui.unit_row[i] = row;
head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); 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); name = gtk_label_new(NULL);
gtk_widget_set_hexpand(left, TRUE); i18n_bind(name, sub_keys[s], 0);
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);
gtk_widget_set_halign(name, GTK_ALIGN_START); gtk_widget_set_halign(name, GTK_ALIGN_START);
gtk_widget_set_hexpand(name, TRUE); gtk_widget_set_hexpand(name, TRUE);
gtk_label_set_xalign(GTK_LABEL(name), 0.0); gtk_label_set_xalign(GTK_LABEL(name), 0.0);
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END); gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
gtk_widget_add_css_class(name, "stats-unit-name"); gtk_widget_add_css_class(name, "stats-subject-name");
gtk_box_append(GTK_BOX(left), 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); count = gtk_label_new(NULL);
gtk_widget_set_valign(right, GTK_ALIGN_CENTER); gtk_widget_add_css_class(count, "stats-subject-count");
gtk_box_append(GTK_BOX(head), right); if (!has_units)
gtk_widget_add_css_class(count, "locked");
badge = gtk_label_new(NULL); gtk_box_append(GTK_BOX(head), count);
gtk_widget_add_css_class(badge, "stats-unit-badge"); stats_ui.subj[s].count = count;
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;
bar = gtk_progress_bar_new(); 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_widget_set_hexpand(bar, TRUE);
gtk_box_append(GTK_BOX(row), bar); gtk_box_append(GTK_BOX(subject), bar);
stats_ui.unit_bar[i] = 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; return outer;
@ -3119,7 +3273,6 @@ static GtkWidget *build_roadmap_page(void) {
/* Subjects page ("Předměty") - serpentine subject map */ /* Subjects page ("Předměty") - serpentine subject map */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
#define NUM_SUBJECTS 13
#define SUB_BUBBLE NODE_SIZE /* round node, matches .unit-node */ #define SUB_BUBBLE NODE_SIZE /* round node, matches .unit-node */
#define SUB_MX 110.0 #define SUB_MX 110.0
#define SUB_MY 150.0 #define SUB_MY 150.0
@ -3144,6 +3297,16 @@ static const char *sub_keys[NUM_SUBJECTS] = {
"Programové vybavení", "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_scroll;
static GtkWidget *sub_fixed; static GtkWidget *sub_fixed;
static GtkWidget *sub_rail; static GtkWidget *sub_rail;