mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Add progress statistics overview with per-unit breakdown.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
198137b899
commit
a4aa3ce93c
2 changed files with 505 additions and 8 deletions
25
README.md
25
README.md
|
|
@ -27,7 +27,7 @@ GTK 4 interface.
|
||||||
- layout folds into multiple rows when the window is narrow
|
- layout folds into multiple rows when the window is narrow
|
||||||
- **Learning roadmap** – adaptive serpentine learning path with 10 units
|
- **Learning roadmap** – adaptive serpentine learning path with 10 units
|
||||||
- each unit numbered and labeled with its topic (e.g. "Neue Freunde")
|
- each unit numbered and labeled with its topic (e.g. "Neue Freunde")
|
||||||
- units 1 and 2 are open; completed units shown in green with a checkmark
|
- units 1–3 are open; completed units shown in green with a checkmark
|
||||||
- remaining units dimmed with a lock icon
|
- remaining units dimmed with a lock icon
|
||||||
- finish node at the end of the path
|
- finish node at the end of the path
|
||||||
- layout folds into multiple rows when the window is narrow
|
- layout folds into multiple rows when the window is narrow
|
||||||
|
|
@ -42,9 +42,14 @@ GTK 4 interface.
|
||||||
conjugation, prepositions `aus`/`in`, question words, nationalities and
|
conjugation, prepositions `aus`/`in`, question words, nationalities and
|
||||||
countries, spelling/look-and-find tasks, a matching exercise, a number/word
|
countries, spelling/look-and-find tasks, a matching exercise, a number/word
|
||||||
game, free-answer "Steckbrief" tasks and a Hangman game with 5 professions
|
game, free-answer "Steckbrief" tasks and a Hangman game with 5 professions
|
||||||
|
- **Unit 3 exercises** – "Bei uns zu Hause" covers family vocabulary,
|
||||||
|
possessives, accusative and related grammar across 15 exercises
|
||||||
- **Progress tracking** – completed exercises are marked green; progress is
|
- **Progress tracking** – completed exercises are marked green; progress is
|
||||||
saved per unit to `progress/unit1.conf` and `progress/unit2.conf` and
|
saved per unit to `progress/unit1.conf`, `progress/unit2.conf` and
|
||||||
restored on startup
|
`progress/unit3.conf` and restored on startup
|
||||||
|
- **Statistics** – chart icon in the header opens a progress overview with
|
||||||
|
totals (exercises done, percent, finished units) and a per-unit breakdown
|
||||||
|
with progress bars; numbers update as soon as you finish an exercise
|
||||||
- **Settings** – gear icon in the top-right header opens a panel where you can
|
- **Settings** – gear icon in the top-right header opens a panel where you can
|
||||||
choose:
|
choose:
|
||||||
- **mode**: dark or light, as a segmented control
|
- **mode**: dark or light, as a segmented control
|
||||||
|
|
@ -156,13 +161,15 @@ When launched, the application shows the welcome screen. From there:
|
||||||
even if unit 1 is not finished yet.
|
even if unit 1 is not finished yet.
|
||||||
5. Use the **back button** in the top-left corner to return to the previous
|
5. Use the **back button** in the top-left corner to return to the previous
|
||||||
screen.
|
screen.
|
||||||
6. Open **Nastavení / Settings** (gear icon, top-right) to switch dark/light
|
6. Open **Statistiky / Statistics** (chart icon, top-right) to see overall and
|
||||||
|
per-unit progress.
|
||||||
|
7. Open **Nastavení / Settings** (gear icon, top-right) to switch dark/light
|
||||||
mode, pick a color theme, or change the interface language.
|
mode, pick a color theme, or change the interface language.
|
||||||
|
|
||||||
Units 1 and 2 are currently implemented. Locked units do nothing – they are
|
Units 1–3 are currently implemented. Locked units do nothing – they are
|
||||||
placeholders until lessons are added. Completed exercises stay green and are
|
placeholders until lessons are added. Completed exercises stay green and are
|
||||||
saved per unit to `progress/unit1.conf` / `progress/unit2.conf` (created next
|
saved per unit to `progress/unit1.conf` / `progress/unit2.conf` /
|
||||||
to the app on first finish).
|
`progress/unit3.conf` (created next to the app on first finish).
|
||||||
|
|
||||||
### Keyboard shortcuts
|
### Keyboard shortcuts
|
||||||
|
|
||||||
|
|
@ -181,6 +188,7 @@ LICENSE MIT license
|
||||||
progress/ created at runtime
|
progress/ created at runtime
|
||||||
unit1.conf unit 1 exercise completion state
|
unit1.conf unit 1 exercise completion state
|
||||||
unit2.conf unit 2 exercise completion state
|
unit2.conf unit 2 exercise completion state
|
||||||
|
unit3.conf unit 3 exercise completion state
|
||||||
settings.conf theme, dark/light mode and language preference
|
settings.conf theme, dark/light mode and language preference
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -215,13 +223,14 @@ Future enhancements planned for maturita.c:
|
||||||
- [ ] Wire unit nodes to actual German vocabulary/grammar exercises
|
- [ ] Wire unit nodes to actual German vocabulary/grammar exercises
|
||||||
- [x] Unit 1 ("Neue Freunde") – 13 interactive exercises with progress saving
|
- [x] Unit 1 ("Neue Freunde") – 13 interactive exercises with progress saving
|
||||||
- [x] Unit 2 ("Aus aller Welt") – 19 interactive exercises (incl. Hangman)
|
- [x] Unit 2 ("Aus aller Welt") – 19 interactive exercises (incl. Hangman)
|
||||||
|
- [x] Unit 3 ("Bei uns zu Hause") – 15 interactive exercises
|
||||||
- [ ] Lesson progression and unlock system for the remaining units
|
- [ ] Lesson progression and unlock system for the remaining units
|
||||||
- [ ] Add audio pronunciation features
|
- [ ] Add audio pronunciation features
|
||||||
- [x] Add user settings panel (theme, dark/light mode, language)
|
- [x] Add user settings panel (theme, dark/light mode, language)
|
||||||
- [ ] Implement spaced repetition system for vocabulary
|
- [ ] Implement spaced repetition system for vocabulary
|
||||||
- [ ] Add listening and reading comprehension exercises
|
- [ ] Add listening and reading comprehension exercises
|
||||||
- [ ] Add writing practice with feedback
|
- [ ] Add writing practice with feedback
|
||||||
- [ ] Expand progress tracking with statistics
|
- [x] Expand progress tracking with statistics
|
||||||
- [ ] Move CSS styling to an external file
|
- [ ] Move CSS styling to an external file
|
||||||
- [ ] Split the UI code into multiple files
|
- [ ] Split the UI code into multiple files
|
||||||
- [x] Add internationalization support (Czech and English)
|
- [x] Add internationalization support (Czech and English)
|
||||||
|
|
|
||||||
488
maturita.c
488
maturita.c
|
|
@ -500,6 +500,19 @@ static const TrEntry tr_ui[] = {
|
||||||
{"hm_done", "Výborně! Uhodli jste všechna povolání.",
|
{"hm_done", "Výborně! Uhodli jste všechna povolání.",
|
||||||
"Great! You guessed all the jobs."},
|
"Great! You guessed all the jobs."},
|
||||||
{"hm_guess", "Hádejte písmeno", "Guess a letter"},
|
{"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_ex_label", "Cvičení", "Exercises"},
|
||||||
|
{"stats_pct_label", "Hotovo", "Done"},
|
||||||
|
{"stats_units_label", "Jednotky", "Units"},
|
||||||
|
{"stats_section", "Podle jednotek", "By unit"},
|
||||||
|
{"stats_locked", "Zamčeno", "Locked"},
|
||||||
|
{"stats_complete", "Hotovo", "Complete"},
|
||||||
|
{"stats_ex_fmt", "%d / %d", "%d / %d"},
|
||||||
|
{"stats_pct_fmt", "%d %%", "%d %%"},
|
||||||
|
{"stats_units_fmt", "%d / %d", "%d / %d"},
|
||||||
{"subjects_title", "Předměty", "Subjects"},
|
{"subjects_title", "Předměty", "Subjects"},
|
||||||
{"subjects_sub", "Zatím je dostupný předmět Deutsch.",
|
{"subjects_sub", "Zatím je dostupný předmět Deutsch.",
|
||||||
"Only Deutsch is available so far."},
|
"Only Deutsch is available so far."},
|
||||||
|
|
@ -998,6 +1011,8 @@ static void refresh_welcome_heading(void) {
|
||||||
g_free(markup);
|
g_free(markup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void refresh_stats_ui(void);
|
||||||
|
|
||||||
static void apply_language(void) {
|
static void apply_language(void) {
|
||||||
guint i;
|
guint i;
|
||||||
|
|
||||||
|
|
@ -1005,6 +1020,7 @@ static void apply_language(void) {
|
||||||
for (i = 0; i < i18n_binds->len; i++)
|
for (i = 0; i < i18n_binds->len; i++)
|
||||||
i18n_apply_one(&g_array_index(i18n_binds, I18nBind, i));
|
i18n_apply_one(&g_array_index(i18n_binds, I18nBind, i));
|
||||||
refresh_welcome_heading();
|
refresh_welcome_heading();
|
||||||
|
refresh_stats_ui();
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *build_theme_css(const ThemePalette *p) {
|
static char *build_theme_css(const ThemePalette *p) {
|
||||||
|
|
@ -1601,6 +1617,100 @@ static char *build_theme_css(const ThemePalette *p) {
|
||||||
" font-size: 15px;"
|
" font-size: 15px;"
|
||||||
" font-weight: 700;"
|
" font-weight: 700;"
|
||||||
"}"
|
"}"
|
||||||
|
".stats-page {"
|
||||||
|
" padding: 8px 28px 24px 28px;"
|
||||||
|
"}"
|
||||||
|
".stats-summary {"
|
||||||
|
" margin-top: 8px;"
|
||||||
|
" margin-bottom: 18px;"
|
||||||
|
"}"
|
||||||
|
".stats-metric {"
|
||||||
|
" background-color: alpha(@bg_surface0, 0.92);"
|
||||||
|
" border: 1px solid @bg_surface1;"
|
||||||
|
" border-radius: 16px;"
|
||||||
|
" padding: 16px 18px;"
|
||||||
|
" min-width: 120px;"
|
||||||
|
"}"
|
||||||
|
".stats-metric-value {"
|
||||||
|
" color: @fg_text;"
|
||||||
|
" font-size: 26px;"
|
||||||
|
" font-weight: 800;"
|
||||||
|
" letter-spacing: -0.4px;"
|
||||||
|
"}"
|
||||||
|
".stats-metric-label {"
|
||||||
|
" color: @fg_overlay;"
|
||||||
|
" font-size: 11px;"
|
||||||
|
" font-weight: 700;"
|
||||||
|
" letter-spacing: 0.8px;"
|
||||||
|
" margin-top: 4px;"
|
||||||
|
"}"
|
||||||
|
".stats-overall-bar,"
|
||||||
|
".stats-overall-bar trough,"
|
||||||
|
".stats-unit-bar,"
|
||||||
|
".stats-unit-bar trough {"
|
||||||
|
" min-height: 10px;"
|
||||||
|
" border-radius: 999px;"
|
||||||
|
" background-color: alpha(@bg_surface1, 0.85);"
|
||||||
|
" border: none;"
|
||||||
|
" padding: 0;"
|
||||||
|
"}"
|
||||||
|
".stats-overall-bar {"
|
||||||
|
" margin-bottom: 22px;"
|
||||||
|
" min-height: 12px;"
|
||||||
|
"}"
|
||||||
|
".stats-overall-bar progress,"
|
||||||
|
".stats-unit-bar progress {"
|
||||||
|
" min-height: 10px;"
|
||||||
|
" border-radius: 999px;"
|
||||||
|
" background-image: linear-gradient(90deg, @accent 0%%, @accent2 100%%);"
|
||||||
|
" background-color: @accent;"
|
||||||
|
" border: none;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-bar.done progress {"
|
||||||
|
" background-image: linear-gradient(90deg, @success 0%%, @success2 100%%);"
|
||||||
|
" background-color: @success;"
|
||||||
|
"}"
|
||||||
|
".stats-section {"
|
||||||
|
" color: @fg_overlay;"
|
||||||
|
" font-size: 11px;"
|
||||||
|
" font-weight: 700;"
|
||||||
|
" letter-spacing: 0.8px;"
|
||||||
|
" margin-bottom: 10px;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-row {"
|
||||||
|
" background-color: alpha(@bg_surface0, 0.72);"
|
||||||
|
" border: 1px solid @bg_surface1;"
|
||||||
|
" border-radius: 14px;"
|
||||||
|
" padding: 12px 14px;"
|
||||||
|
" margin-bottom: 8px;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-row.locked {"
|
||||||
|
" opacity: 0.55;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-row.done {"
|
||||||
|
" border-color: alpha(@success, 0.55);"
|
||||||
|
"}"
|
||||||
|
".stats-unit-index {"
|
||||||
|
" color: @fg_overlay;"
|
||||||
|
" font-size: 13px;"
|
||||||
|
" font-weight: 700;"
|
||||||
|
" min-width: 22px;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-name {"
|
||||||
|
" color: @fg_text;"
|
||||||
|
" font-size: 15px;"
|
||||||
|
" font-weight: 700;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-count {"
|
||||||
|
" color: @fg_subtext;"
|
||||||
|
" font-size: 13px;"
|
||||||
|
" font-weight: 600;"
|
||||||
|
"}"
|
||||||
|
".stats-unit-badge {"
|
||||||
|
" color: @success;"
|
||||||
|
" font-size: 12px;"
|
||||||
|
" font-weight: 700;"
|
||||||
|
"}"
|
||||||
"tooltip {"
|
"tooltip {"
|
||||||
" background-color: @bg_mantle;"
|
" background-color: @bg_mantle;"
|
||||||
" border: 1px solid @bg_surface1;"
|
" border: 1px solid @bg_surface1;"
|
||||||
|
|
@ -1871,6 +1981,42 @@ static void draw_settings_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
cairo_fill(cr);
|
cairo_fill(cr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
|
int width, int height, gpointer data) {
|
||||||
|
GtkWidget *color_w = data ? GTK_WIDGET(data)
|
||||||
|
: gtk_widget_get_parent(GTK_WIDGET(area));
|
||||||
|
GdkRGBA color;
|
||||||
|
const double bar_w = 3.2;
|
||||||
|
const double gap = 2.6;
|
||||||
|
const double total_w = 3.0 * bar_w + 2.0 * gap;
|
||||||
|
const double heights[3] = {0.42, 0.72, 1.0};
|
||||||
|
double base_x = (width - total_w) / 2.0;
|
||||||
|
double base_y = height * 0.82;
|
||||||
|
double max_h = height * 0.58;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
(void)area;
|
||||||
|
gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
|
||||||
|
cairo_set_source_rgb(cr, color.red, color.green, color.blue);
|
||||||
|
cairo_set_line_width(cr, 1.5);
|
||||||
|
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||||
|
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
double x = base_x + (double)i * (bar_w + gap);
|
||||||
|
double h = max_h * heights[i];
|
||||||
|
double y = base_y - h;
|
||||||
|
double r = 1.2;
|
||||||
|
|
||||||
|
cairo_new_sub_path(cr);
|
||||||
|
cairo_arc(cr, x + r, y + r, r, G_PI, 1.5 * G_PI);
|
||||||
|
cairo_arc(cr, x + bar_w - r, y + r, r, 1.5 * G_PI, 2.0 * G_PI);
|
||||||
|
cairo_line_to(cr, x + bar_w, base_y);
|
||||||
|
cairo_line_to(cr, x, base_y);
|
||||||
|
cairo_close_path(cr);
|
||||||
|
cairo_fill(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
|
static GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
|
||||||
double r, double g, double b, int px) {
|
double r, double g, double b, int px) {
|
||||||
Rgb *col = g_new(Rgb, 1);
|
Rgb *col = g_new(Rgb, 1);
|
||||||
|
|
@ -2067,6 +2213,7 @@ static void refresh_completion_ui(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
all_rails_redraw();
|
all_rails_redraw();
|
||||||
|
refresh_stats_ui();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void mark_done(UnitCtx *u, int n) {
|
static void mark_done(UnitCtx *u, int n) {
|
||||||
|
|
@ -2077,6 +2224,321 @@ static void mark_done(UnitCtx *u, int n) {
|
||||||
unit_save_progress(u);
|
unit_save_progress(u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Statistics */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static GtkWidget *make_back_button(const char *target);
|
||||||
|
static void on_nav_clicked(GtkButton *button, gpointer user_data);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
GtkWidget *ex_value;
|
||||||
|
GtkWidget *pct_value;
|
||||||
|
GtkWidget *units_value;
|
||||||
|
GtkWidget *overall_bar;
|
||||||
|
GtkWidget *unit_count[NUM_UNITS];
|
||||||
|
GtkWidget *unit_badge[NUM_UNITS];
|
||||||
|
GtkWidget *unit_bar[NUM_UNITS];
|
||||||
|
GtkWidget *unit_row[NUM_UNITS];
|
||||||
|
} StatsUi;
|
||||||
|
|
||||||
|
static StatsUi stats_ui;
|
||||||
|
static char *stats_return_page;
|
||||||
|
|
||||||
|
static int unit_done_count(const UnitCtx *u) {
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
if (!u)
|
||||||
|
return 0;
|
||||||
|
for (int i = 1; i <= u->n_ex; i++) {
|
||||||
|
if (u->done[i])
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void refresh_stats_ui(void) {
|
||||||
|
int done_ex = 0;
|
||||||
|
int total_ex = 0;
|
||||||
|
int done_units = 0;
|
||||||
|
int open_units = 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);
|
||||||
|
|
||||||
|
done_ex += d;
|
||||||
|
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"), done_ex, total_ex);
|
||||||
|
gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp);
|
||||||
|
g_free(tmp);
|
||||||
|
|
||||||
|
tmp = g_strdup_printf(tr("stats_pct_fmt"), pct);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_UNITS; i++) {
|
||||||
|
UnitCtx *u = &units[i];
|
||||||
|
GtkWidget *count = stats_ui.unit_count[i];
|
||||||
|
GtkWidget *badge = stats_ui.unit_badge[i];
|
||||||
|
GtkWidget *bar = stats_ui.unit_bar[i];
|
||||||
|
GtkWidget *row = stats_ui.unit_row[i];
|
||||||
|
gboolean unlocked = u->unlocked && u->n_ex > 0;
|
||||||
|
int d = unlocked ? unit_done_count(u) : 0;
|
||||||
|
gboolean complete = unlocked && d == u->n_ex;
|
||||||
|
|
||||||
|
if (!count || !bar || !row)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
gtk_widget_remove_css_class(row, "done");
|
||||||
|
gtk_widget_remove_css_class(row, "locked");
|
||||||
|
gtk_widget_remove_css_class(bar, "done");
|
||||||
|
|
||||||
|
if (!unlocked) {
|
||||||
|
gtk_widget_add_css_class(row, "locked");
|
||||||
|
gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
|
||||||
|
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), 0.0);
|
||||||
|
if (badge)
|
||||||
|
gtk_widget_set_visible(badge, FALSE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = g_strdup_printf(tr("stats_ex_fmt"), d, u->n_ex);
|
||||||
|
gtk_label_set_text(GTK_LABEL(count), tmp);
|
||||||
|
g_free(tmp);
|
||||||
|
gtk_progress_bar_set_fraction(
|
||||||
|
GTK_PROGRESS_BAR(bar),
|
||||||
|
u->n_ex > 0 ? (double)d / (double)u->n_ex : 0.0);
|
||||||
|
|
||||||
|
if (complete) {
|
||||||
|
gtk_widget_add_css_class(row, "done");
|
||||||
|
gtk_widget_add_css_class(bar, "done");
|
||||||
|
if (badge) {
|
||||||
|
gtk_label_set_text(GTK_LABEL(badge), tr("stats_complete"));
|
||||||
|
gtk_widget_set_visible(badge, TRUE);
|
||||||
|
}
|
||||||
|
} else if (badge) {
|
||||||
|
gtk_widget_set_visible(badge, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_stats_back_clicked(GtkButton *button, gpointer user_data) {
|
||||||
|
const char *target = stats_return_page;
|
||||||
|
|
||||||
|
(void)button;
|
||||||
|
(void)user_data;
|
||||||
|
if (!target || !target[0])
|
||||||
|
target = "subjects";
|
||||||
|
gtk_stack_set_visible_child_name(main_stack, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_stats_clicked(GtkButton *button, gpointer user_data) {
|
||||||
|
const char *cur;
|
||||||
|
|
||||||
|
(void)button;
|
||||||
|
(void)user_data;
|
||||||
|
|
||||||
|
cur = gtk_stack_get_visible_child_name(main_stack);
|
||||||
|
if (cur && g_strcmp0(cur, "stats") != 0) {
|
||||||
|
g_free(stats_return_page);
|
||||||
|
stats_return_page = g_strdup(cur);
|
||||||
|
}
|
||||||
|
refresh_stats_ui();
|
||||||
|
gtk_stack_set_visible_child_name(main_stack, "stats");
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out) {
|
||||||
|
GtkWidget *card;
|
||||||
|
GtkWidget *value;
|
||||||
|
GtkWidget *label;
|
||||||
|
|
||||||
|
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_widget_add_css_class(card, "stats-metric");
|
||||||
|
gtk_widget_set_hexpand(card, TRUE);
|
||||||
|
|
||||||
|
value = gtk_label_new("0");
|
||||||
|
gtk_widget_set_halign(value, GTK_ALIGN_START);
|
||||||
|
gtk_widget_add_css_class(value, "stats-metric-value");
|
||||||
|
gtk_box_append(GTK_BOX(card), value);
|
||||||
|
|
||||||
|
label = gtk_label_new(NULL);
|
||||||
|
i18n_bind(label, label_key, 0);
|
||||||
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||||
|
gtk_widget_add_css_class(label, "stats-metric-label");
|
||||||
|
gtk_box_append(GTK_BOX(card), label);
|
||||||
|
|
||||||
|
*value_out = value;
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *build_stats_page(void) {
|
||||||
|
GtkWidget *outer;
|
||||||
|
GtkWidget *scroll;
|
||||||
|
GtkWidget *page;
|
||||||
|
GtkWidget *top;
|
||||||
|
GtkWidget *back;
|
||||||
|
GtkWidget *center;
|
||||||
|
GtkWidget *heading;
|
||||||
|
GtkWidget *sub;
|
||||||
|
GtkWidget *summary;
|
||||||
|
GtkWidget *section;
|
||||||
|
GtkWidget *list;
|
||||||
|
|
||||||
|
outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_widget_add_css_class(outer, "stats-page");
|
||||||
|
|
||||||
|
top = gtk_center_box_new();
|
||||||
|
gtk_widget_set_margin_top(top, 2);
|
||||||
|
gtk_widget_set_margin_bottom(top, 6);
|
||||||
|
gtk_box_append(GTK_BOX(outer), top);
|
||||||
|
|
||||||
|
back = make_back_button("subjects");
|
||||||
|
g_signal_handlers_disconnect_matched(back, G_SIGNAL_MATCH_FUNC,
|
||||||
|
0, 0, NULL, on_nav_clicked, NULL);
|
||||||
|
g_signal_connect(back, "clicked", G_CALLBACK(on_stats_back_clicked), NULL);
|
||||||
|
gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
|
||||||
|
|
||||||
|
center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
|
gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
|
||||||
|
gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
|
||||||
|
|
||||||
|
heading = gtk_label_new(NULL);
|
||||||
|
i18n_bind(heading, "stats_title", 0);
|
||||||
|
gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
|
||||||
|
gtk_widget_add_css_class(heading, "roadmap-title");
|
||||||
|
gtk_box_append(GTK_BOX(center), heading);
|
||||||
|
|
||||||
|
sub = gtk_label_new(NULL);
|
||||||
|
i18n_bind(sub, "stats_sub", 0);
|
||||||
|
gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
|
||||||
|
gtk_widget_add_css_class(sub, "roadmap-sub");
|
||||||
|
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(center), sub);
|
||||||
|
|
||||||
|
scroll = gtk_scrolled_window_new();
|
||||||
|
gtk_widget_set_vexpand(scroll, TRUE);
|
||||||
|
gtk_widget_set_hexpand(scroll, TRUE);
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||||
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_box_append(GTK_BOX(outer), scroll);
|
||||||
|
|
||||||
|
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
gtk_widget_set_halign(page, GTK_ALIGN_FILL);
|
||||||
|
gtk_widget_set_margin_start(page, 4);
|
||||||
|
gtk_widget_set_margin_end(page, 4);
|
||||||
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), page);
|
||||||
|
|
||||||
|
summary = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||||
|
gtk_widget_add_css_class(summary, "stats-summary");
|
||||||
|
gtk_widget_set_halign(summary, GTK_ALIGN_FILL);
|
||||||
|
gtk_box_append(GTK_BOX(page), summary);
|
||||||
|
|
||||||
|
gtk_box_append(GTK_BOX(summary),
|
||||||
|
stats_metric_card("stats_ex_label", &stats_ui.ex_value));
|
||||||
|
gtk_box_append(GTK_BOX(summary),
|
||||||
|
stats_metric_card("stats_pct_label", &stats_ui.pct_value));
|
||||||
|
gtk_box_append(GTK_BOX(summary),
|
||||||
|
stats_metric_card("stats_units_label",
|
||||||
|
&stats_ui.units_value));
|
||||||
|
|
||||||
|
stats_ui.overall_bar = gtk_progress_bar_new();
|
||||||
|
gtk_widget_add_css_class(stats_ui.overall_bar, "stats-overall-bar");
|
||||||
|
gtk_widget_set_hexpand(stats_ui.overall_bar, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(page), stats_ui.overall_bar);
|
||||||
|
|
||||||
|
section = gtk_label_new(NULL);
|
||||||
|
i18n_bind(section, "stats_section", 0);
|
||||||
|
gtk_widget_set_halign(section, GTK_ALIGN_START);
|
||||||
|
gtk_widget_add_css_class(section, "stats-section");
|
||||||
|
gtk_box_append(GTK_BOX(page), section);
|
||||||
|
|
||||||
|
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;
|
||||||
|
GtkWidget *head;
|
||||||
|
GtkWidget *left;
|
||||||
|
GtkWidget *idx;
|
||||||
|
GtkWidget *name;
|
||||||
|
GtkWidget *right;
|
||||||
|
GtkWidget *count;
|
||||||
|
GtkWidget *badge;
|
||||||
|
GtkWidget *bar;
|
||||||
|
char num[8];
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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(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_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[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();
|
||||||
|
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[i] = bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
@ -7740,6 +8202,30 @@ static void on_settings_clicked(GtkButton *button, gpointer user_data) {
|
||||||
gtk_popover_popup(popover);
|
gtk_popover_popup(popover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GtkWidget *build_stats_button(void) {
|
||||||
|
GtkWidget *btn;
|
||||||
|
GtkWidget *icon;
|
||||||
|
|
||||||
|
btn = gtk_button_new();
|
||||||
|
gtk_widget_add_css_class(btn, "settings-btn");
|
||||||
|
gtk_widget_add_css_class(btn, "flat");
|
||||||
|
i18n_bind(btn, "stats", 2);
|
||||||
|
gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
|
||||||
|
gtk_widget_set_focus_on_click(btn, FALSE);
|
||||||
|
|
||||||
|
icon = gtk_drawing_area_new();
|
||||||
|
gtk_widget_set_size_request(icon, 18, 18);
|
||||||
|
gtk_widget_set_can_target(icon, FALSE);
|
||||||
|
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
|
||||||
|
draw_stats_icon, NULL, NULL);
|
||||||
|
gtk_button_set_child(GTK_BUTTON(btn), icon);
|
||||||
|
g_signal_connect_swapped(btn, "state-flags-changed",
|
||||||
|
G_CALLBACK(gtk_widget_queue_draw), icon);
|
||||||
|
g_signal_connect(btn, "clicked", G_CALLBACK(on_stats_clicked), NULL);
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
static GtkWidget *build_settings_button(void) {
|
static GtkWidget *build_settings_button(void) {
|
||||||
GtkWidget *btn;
|
GtkWidget *btn;
|
||||||
GtkWidget *icon;
|
GtkWidget *icon;
|
||||||
|
|
@ -8051,6 +8537,7 @@ static void activate(GtkApplication *app, gpointer user_data) {
|
||||||
gtk_window_set_titlebar(window, headerbar);
|
gtk_window_set_titlebar(window, headerbar);
|
||||||
apply_theme();
|
apply_theme();
|
||||||
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_settings_button());
|
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_settings_button());
|
||||||
|
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_stats_button());
|
||||||
|
|
||||||
main_stack = GTK_STACK(gtk_stack_new());
|
main_stack = GTK_STACK(gtk_stack_new());
|
||||||
gtk_stack_set_transition_type(GTK_STACK(main_stack),
|
gtk_stack_set_transition_type(GTK_STACK(main_stack),
|
||||||
|
|
@ -8062,6 +8549,7 @@ static void activate(GtkApplication *app, gpointer user_data) {
|
||||||
gtk_stack_add_named(main_stack, welcome_page, "welcome");
|
gtk_stack_add_named(main_stack, welcome_page, "welcome");
|
||||||
gtk_stack_add_named(main_stack, build_subjects_page(), "subjects");
|
gtk_stack_add_named(main_stack, build_subjects_page(), "subjects");
|
||||||
gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
|
gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
|
||||||
|
gtk_stack_add_named(main_stack, build_stats_page(), "stats");
|
||||||
|
|
||||||
for (int i = 0; i < NUM_UNLOCKED; i++) {
|
for (int i = 0; i < NUM_UNLOCKED; i++) {
|
||||||
UnitCtx *u = &units[i];
|
UnitCtx *u = &units[i];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue