Add four years of German listening, reading and writing beside the textbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-07 19:30:00 +02:00
commit b822c57875
31 changed files with 3589 additions and 102 deletions

234
src/german_course.c Normal file
View file

@ -0,0 +1,234 @@
#include "graduately.h"
/* Skill course next to the three textbook units. Four years, same
* listening / reading / writing player as English. */
#define DE_YEARS 4
#define DE_N 6
#include "german_lessons.inc"
static NetLesson de_rt[DE_YEARS][DE_N];
static char de_map_page[DE_YEARS][16];
static char de_unit_page[DE_YEARS][DE_N][16];
static char de_ex_page[DE_YEARS][DE_N][16];
static char de_title_key[DE_YEARS][DE_N][16];
static char de_sub_key[DE_YEARS][DE_N][20];
static gboolean de_ready;
static void de_prepare(void) {
if (de_ready)
return;
de_ready = TRUE;
for (int y = 0; y < DE_YEARS; y++) {
g_snprintf(de_map_page[y], sizeof de_map_page[y], "de%dmap", y + 1);
for (int i = 0; i < DE_N; i++) {
g_snprintf(de_unit_page[y][i], sizeof de_unit_page[y][i],
"de%dunit%d", y + 1, i + 1);
g_snprintf(de_ex_page[y][i], sizeof de_ex_page[y][i],
"de%dex%d", y + 1, i + 1);
g_snprintf(de_title_key[y][i], sizeof de_title_key[y][i],
"de_y%d_u%d", y + 1, i + 1);
g_snprintf(de_sub_key[y][i], sizeof de_sub_key[y][i],
"de_y%d_u%d_sub", y + 1, i + 1);
de_rt[y][i].unit_page = de_unit_page[y][i];
de_rt[y][i].ex_page = de_ex_page[y][i];
de_rt[y][i].n_slides = (guint)de_lessons[y][i].n_slides;
}
}
}
static void de_save_progress(void) {
GKeyFile *kf = g_key_file_new();
gchar *data;
GError *err = NULL;
for (int y = 0; y < DE_YEARS; y++) {
for (int i = 0; i < DE_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1);
g_key_file_set_boolean(kf, "done", key, de_rt[y][i].done);
}
}
data = g_key_file_to_data(kf, NULL, NULL);
if (!g_file_set_contents(PROGRESS_DE, data, -1, &err)) {
g_warning("german course: %s", err->message);
g_error_free(err);
}
g_free(data);
g_key_file_free(kf);
}
void de_load_progress(void) {
GKeyFile *kf = g_key_file_new();
GError *err = NULL;
de_prepare();
if (!g_key_file_load_from_file(kf, PROGRESS_DE, G_KEY_FILE_NONE, &err)) {
if (err)
g_error_free(err);
g_key_file_free(kf);
return;
}
for (int y = 0; y < DE_YEARS; y++) {
for (int i = 0; i < DE_N; i++) {
char key[8];
g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1);
de_rt[y][i].done = g_key_file_get_boolean(kf, "done", key, NULL);
}
}
g_key_file_free(kf);
}
static void mark_de_done(int year, int index) {
if (year < 0 || year >= DE_YEARS || index < 0 || index >= DE_N)
return;
if (de_rt[year][index].done)
return;
de_rt[year][index].done = TRUE;
de_save_progress();
refresh_stats_ui();
}
void progress_for_de(ProgressSum *out) {
out->total_ex += DE_YEARS * DE_N;
out->open_units += DE_YEARS * DE_N;
for (int y = 0; y < DE_YEARS; y++) {
for (int i = 0; i < DE_N; i++) {
if (de_rt[y][i].done) {
out->done_ex++;
out->done_units++;
}
}
}
}
void de_lessons_apply_lang(void) {
for (int y = 0; y < DE_YEARS; y++)
for (int i = 0; i < DE_N; i++)
skill_slide_apply(&de_rt[y][i]);
}
static GtkWidget *de_nav_button(const char *title_key, const char *sub_key,
const char *target, const char *badge) {
GtkWidget *btn = gtk_button_new();
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
GtkWidget *num = gtk_label_new(badge);
GtkWidget *texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
GtkWidget *title = gtk_label_new(NULL);
GtkWidget *sub = gtk_label_new(NULL);
gtk_widget_add_css_class(btn, "unit-node");
gtk_widget_add_css_class(btn, "current");
gtk_widget_set_hexpand(btn, TRUE);
gtk_widget_set_margin_start(row, 18);
gtk_widget_set_margin_end(row, 18);
gtk_widget_set_margin_top(row, 10);
gtk_widget_set_margin_bottom(row, 10);
gtk_button_set_child(GTK_BUTTON(btn), row);
gtk_widget_add_css_class(num, "unit-number");
gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), num);
gtk_widget_set_hexpand(texts, TRUE);
gtk_box_append(GTK_BOX(row), texts);
i18n_bind(title, title_key, 0);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_add_css_class(title, "unit-name");
gtk_box_append(GTK_BOX(texts), title);
i18n_bind(sub, sub_key, 0);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_label_set_xalign(GTK_LABEL(sub), 0);
gtk_widget_add_css_class(sub, "quiz-intro");
gtk_box_append(GTK_BOX(texts), sub);
g_object_set_data_full(G_OBJECT(btn), "target", g_strdup(target), g_free);
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
return btn;
}
static GtkWidget *de_list_page(const char *back, const char *title,
const char *sub, GtkWidget *list) {
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *scroll = gtk_scrolled_window_new();
gtk_widget_set_hexpand(page, TRUE);
gtk_widget_set_vexpand(page, TRUE);
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, title, sub));
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 18);
gtk_box_append(GTK_BOX(page), scroll);
gtk_widget_set_halign(list, GTK_ALIGN_CENTER);
gtk_widget_set_size_request(list, 520, -1);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
return page;
}
GtkWidget *build_dehome_page(void) {
GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
static const char *years[] = {
"de_year1", "de_year2", "de_year3", "de_year4",
};
static const char *subs[] = {
"de_y1_sub", "de_y2_sub", "de_y3_sub", "de_y4_sub",
};
de_prepare();
gtk_box_append(GTK_BOX(list),
de_nav_button("de_book_title", "de_book_sub", "roadmap", "U"));
for (int y = 0; y < DE_YEARS; y++) {
char badge[8];
g_snprintf(badge, sizeof badge, "%d", y + 1);
gtk_box_append(GTK_BOX(list),
de_nav_button(years[y], subs[y], de_map_page[y], badge));
}
return de_list_page("subjects", "Deutsch", "de_home_sub", list);
}
static GtkWidget *build_de_map(int year) {
GtkWidget *list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
static const char *year_keys[] = {
"de_year1", "de_year2", "de_year3", "de_year4",
};
static const char *sub_keys[] = {
"de_y1_sub", "de_y2_sub", "de_y3_sub", "de_y4_sub",
};
de_prepare();
for (int i = 0; i < DE_N; i++) {
char badge[8];
g_snprintf(badge, sizeof badge, "%d", i + 1);
gtk_box_append(GTK_BOX(list),
de_nav_button(de_title_key[year][i],
de_sub_key[year][i],
de_unit_page[year][i], badge));
}
return de_list_page("dehome", year_keys[year], sub_keys[year], list);
}
void add_de_pages(GtkStack *stack) {
de_prepare();
for (int y = 0; y < DE_YEARS; y++) {
gtk_stack_add_named(stack, build_de_map(y), de_map_page[y]);
for (int i = 0; i < DE_N; i++) {
gtk_stack_add_named(stack,
skill_unit_page(&de_rt[y][i], &de_lessons[y][i],
de_map_page[y], de_title_key[y][i],
de_sub_key[y][i]),
de_unit_page[y][i]);
gtk_stack_add_named(stack,
skill_ex_page(&de_lessons[y][i], y, i, mark_de_done,
de_unit_page[y][i], de_title_key[y][i], "de"),
de_ex_page[y][i]);
}
}
}