Graduately/src/search.c
pepaskopec-blip f746447b75 Keep a copy of each search result page name.
Lesson and exercise targets were built in a temporary buffer, so choosing one sometimes opened nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:26:07 +02:00

545 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "maturita.h"
#include <string.h>
#define SEARCH_MAX 24
typedef struct {
char *title;
char *subtitle;
char *hay;
char *target;
gboolean locked;
int rank;
int score;
} SearchItem;
static GtkWidget *search_layer;
static GtkWidget *search_entry;
static GtkWidget *search_list;
static GtkWidget *search_empty;
static GPtrArray *search_items;
static gboolean subject_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ;
}
static const char *subject_page(int i) {
if (i == 0)
return "roadmap";
if (i == NET_SUBJ)
return "netyears";
if (i == HW_SUBJ)
return "hwmap";
if (i == CZ_SUBJ)
return "czechmap";
return "subjects";
}
static void hay_add(GString *h, const char *text) {
if (text && *text) {
g_string_append(h, text);
g_string_append_c(h, ' ');
}
}
static void hay_add_key(GString *h, const char *key) {
int i;
hay_add(h, key);
for (i = 0; tr_ui[i].key; i++) {
if (g_strcmp0(tr_ui[i].key, key) == 0) {
hay_add(h, tr_ui[i].cs);
hay_add(h, tr_ui[i].en);
return;
}
}
}
static SearchItem *item_new(const char *title, const char *subtitle,
const char *target, gboolean locked, int rank,
const char *extra) {
SearchItem *it = g_new0(SearchItem, 1);
GString *h = g_string_new(NULL);
char *norm;
it->title = g_strdup(title ? title : "");
it->subtitle = g_strdup(subtitle ? subtitle : "");
it->target = g_strdup(target ? target : "");
it->locked = locked;
it->rank = rank;
hay_add(h, title);
hay_add(h, subtitle);
hay_add(h, extra);
hay_add(h, target);
norm = normalize_answer(h->str);
it->hay = norm ? norm : g_strdup("");
g_string_free(h, TRUE);
return it;
}
static void item_free(gpointer p) {
SearchItem *it = p;
if (!it)
return;
g_free(it->title);
g_free(it->subtitle);
g_free(it->hay);
g_free(it->target);
g_free(it);
}
static void catalog_add(const char *title, const char *subtitle,
const char *target, gboolean locked, int rank,
const char *extra) {
if (!title || !target)
return;
if (main_stack && !gtk_stack_get_child_by_name(main_stack, target))
return;
g_ptr_array_add(search_items,
item_new(title, subtitle, target, locked, rank, extra));
}
static void catalog_add_key(const char *title_key, const char *sub_key,
const char *target, gboolean locked, int rank,
const char *extra) {
GString *h = g_string_new(NULL);
SearchItem *it;
if (!title_key || !target)
return;
if (main_stack && !gtk_stack_get_child_by_name(main_stack, target))
return;
hay_add_key(h, title_key);
if (sub_key)
hay_add_key(h, sub_key);
hay_add(h, extra);
it = item_new(tr(title_key), sub_key ? tr(sub_key) : "",
target, locked, rank, h->str);
g_string_free(h, TRUE);
g_ptr_array_add(search_items, it);
}
static void catalog_rebuild(void) {
int i, n;
char key[32];
char page[32];
if (!search_items)
search_items = g_ptr_array_new_with_free_func(item_free);
else
g_ptr_array_set_size(search_items, 0);
catalog_add_key("subjects_title", "search_page", "subjects", FALSE, 0,
"predmety subjects mapa");
catalog_add_key("stats", "search_page", "stats", FALSE, 0, "progress");
catalog_add_key("search_home", "search_page", "welcome", FALSE, 0,
"uvod welcome uvodni");
for (i = 0; i < NUM_SUBJECTS; i++) {
gboolean open = subject_open(i);
catalog_add_key(sub_keys[i], "search_subject",
open ? subject_page(i) : "subjects", !open, 0, NULL);
}
for (i = 0; i < NUM_UNITS; i++) {
UnitCtx *u = &units[i];
const char *target = (u->unlocked && u->page) ? u->page : "roadmap";
catalog_add(u->title, tr("search_unit"), target, !u->unlocked, 1,
"deutsch nemcina german");
if (!u->unlocked || !u->ex_tag)
continue;
if (u->has_branch && u->branch_target)
catalog_add(u->branch_name, u->title, u->branch_target, FALSE, 2,
"vokabel slovicka vocabulary");
for (n = 1; n <= u->n_ex; n++) {
g_snprintf(page, sizeof(page), "%se%d", u->ex_tag, n);
catalog_add(u->ex_names[n], u->title, page, FALSE, 3, NULL);
}
}
catalog_add_key("net_years_title", "search_subject", "netyears", FALSE, 0,
"site network ip");
catalog_add_key("net_year1", "search_lesson", "netmap", FALSE, 1, "rocnik");
catalog_add_key("net_year2", "search_lesson", "netyears", TRUE, 1, "rocnik");
catalog_add_key("net_year3", "search_lesson", "netyears", TRUE, 1, "rocnik");
catalog_add_key("net_year4", "search_lesson", "netyears", TRUE, 1, "rocnik");
for (i = 1; i <= NET_LESSONS; i++) {
g_snprintf(key, sizeof(key), "net_unit%d", i);
g_snprintf(page, sizeof(page), "netunit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "site sit");
g_snprintf(page, sizeof(page), "netex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
}
for (i = 1; i <= HW_LESSONS; i++) {
g_snprintf(key, sizeof(key), "hw_unit%d", i);
g_snprintf(page, sizeof(page), "hwunit%d", i);
catalog_add_key(key, "search_lesson", page, FALSE, 2, "hardware hw");
g_snprintf(page, sizeof(page), "hwex%d", i);
catalog_add(tr(key), tr("search_exercise"), page, FALSE, 3, "kviz quiz");
}
catalog_add_key("Český jazyk a literatura", "search_subject",
"czechmap", FALSE, 0, "cestina czech");
catalog_add_key("Literatura", "search_lesson", "czechmap", TRUE, 1, NULL);
catalog_add_key("Mluvnice", "search_lesson", "mluvnice", FALSE, 1,
"gramatika grammar");
catalog_add_key("Maturitní četba", "search_book", "readinglist", FALSE, 1,
"knihy books");
catalog_add("George Orwell – 1984", tr("search_book"), "cetba1984",
FALSE, 2, "orwell dystopie");
catalog_add(tr("lit_quiz_title"), tr("search_exercise"), "cetba1984quiz",
FALSE, 3, "orwell kviz");
catalog_add("Ladislav Fuks – Spalovač mrtvol", tr("search_book"),
"cetbaFuks", FALSE, 2, "fuks cremator");
catalog_add(tr("lit_fuks_quiz_title"), tr("search_exercise"),
"cetbaFuksQuiz", FALSE, 3, "fuks kviz");
{
static const char *mluv_names[] = {
"i/y", "Velká písmena", "Slovní druhy", "Podst. jména", "Slovesa",
"Slovotvorba", "Větné členy", "Vedlejší věty", "Synonyma",
"Antonyma", "s/z", "Najdi chybu", "Tvary", "Test A–D",
"Přímá řeč", "Rozdíly", "Interpunkce", "Obrazná pojmen.",
"Slohové útvary", "Opakování",
};
for (i = 0; i < (int)G_N_ELEMENTS(mluv_names); i++) {
g_snprintf(page, sizeof(page), "mluve%d", i + 1);
catalog_add(mluv_names[i], tr("Mluvnice"), page, FALSE, 3,
"mluvnice gramatika");
}
}
}
static int item_score(const SearchItem *it, const char *needle) {
const char *p;
if (!needle || !*needle)
return 40 - it->rank;
if (!it->hay || !it->hay[0])
return -1;
if (g_strcmp0(it->hay, needle) == 0)
return 200 - it->rank;
if (g_str_has_prefix(it->hay, needle))
return 160 - it->rank;
p = strstr(it->hay, needle);
if (!p)
return -1;
if (p > it->hay && p[-1] == ' ')
return 120 - it->rank;
return 80 - it->rank;
}
static int cmp_item(gconstpointer a, gconstpointer b) {
const SearchItem *ia = *(SearchItem * const *)a;
const SearchItem *ib = *(SearchItem * const *)b;
if (ia->score != ib->score)
return ib->score - ia->score;
return g_strcmp0(ia->title, ib->title);
}
static void search_go(const char *target) {
if (!target || !main_stack)
return;
if (!gtk_stack_get_child_by_name(main_stack, target))
return;
search_close();
gtk_stack_set_visible_child_name(main_stack, target);
}
static void on_row_activated(GtkListBox *box, GtkListBoxRow *row,
gpointer data) {
const char *target;
(void)box;
(void)data;
if (!row)
return;
target = g_object_get_data(G_OBJECT(row), "target");
search_go(target);
}
static void search_fill(void) {
const char *raw;
char *needle;
GPtrArray *hits;
guint i;
if (!search_list)
return;
catalog_rebuild();
raw = gtk_editable_get_text(GTK_EDITABLE(search_entry));
needle = normalize_answer(raw ? raw : "");
hits = g_ptr_array_new();
for (i = 0; i < search_items->len; i++) {
SearchItem *it = g_ptr_array_index(search_items, i);
int score;
if (!needle[0] && it->rank > 1)
continue;
score = item_score(it, needle);
if (score < 0)
continue;
it->score = score;
g_ptr_array_add(hits, it);
}
g_ptr_array_sort(hits, cmp_item);
if (hits->len > SEARCH_MAX)
g_ptr_array_set_size(hits, SEARCH_MAX);
{
GtkWidget *c = gtk_widget_get_first_child(search_list);
while (c) {
GtkWidget *next = gtk_widget_get_next_sibling(c);
gtk_list_box_remove(GTK_LIST_BOX(search_list), c);
c = next;
}
}
for (i = 0; i < hits->len; i++) {
SearchItem *it = g_ptr_array_index(hits, i);
GtkWidget *row;
GtkWidget *box;
GtkWidget *title;
GtkWidget *sub;
GString *sub_text;
row = gtk_list_box_row_new();
gtk_widget_add_css_class(row, "search-hit");
if (it->locked)
gtk_widget_add_css_class(row, "search-hit-locked");
g_object_set_data_full(G_OBJECT(row), "target",
g_strdup(it->target), g_free);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_margin_start(box, 12);
gtk_widget_set_margin_end(box, 12);
gtk_widget_set_margin_top(box, 8);
gtk_widget_set_margin_bottom(box, 8);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
title = gtk_label_new(it->title);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_label_set_xalign(GTK_LABEL(title), 0.0);
gtk_label_set_ellipsize(GTK_LABEL(title), PANGO_ELLIPSIZE_END);
gtk_widget_add_css_class(title, "search-hit-title");
gtk_box_append(GTK_BOX(box), title);
sub_text = g_string_new(it->subtitle);
if (it->locked) {
if (sub_text->len)
g_string_append(sub_text, " · ");
g_string_append(sub_text, tr("stats_locked"));
}
sub = gtk_label_new(sub_text->str);
g_string_free(sub_text, TRUE);
gtk_widget_set_halign(sub, GTK_ALIGN_START);
gtk_label_set_xalign(GTK_LABEL(sub), 0.0);
gtk_widget_add_css_class(sub, "search-hit-sub");
gtk_box_append(GTK_BOX(box), sub);
gtk_list_box_append(GTK_LIST_BOX(search_list), row);
}
gtk_widget_set_visible(search_empty, hits->len == 0);
gtk_widget_set_visible(search_list, hits->len > 0);
if (hits->len > 0)
gtk_list_box_select_row(GTK_LIST_BOX(search_list),
gtk_list_box_get_row_at_index(
GTK_LIST_BOX(search_list), 0));
if (needle[0])
gtk_label_set_text(GTK_LABEL(search_empty), tr("search_empty"));
else
gtk_label_set_text(GTK_LABEL(search_empty), tr("search_hint"));
g_ptr_array_unref(hits);
g_free(needle);
}
static void on_search_changed(GtkEditable *editable, gpointer data) {
(void)editable;
(void)data;
search_fill();
}
static void search_activate_selected(void) {
GtkListBoxRow *row;
if (!search_list)
return;
row = gtk_list_box_get_selected_row(GTK_LIST_BOX(search_list));
if (!row)
row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(search_list), 0);
if (row)
search_go(g_object_get_data(G_OBJECT(row), "target"));
}
static gboolean on_entry_key(GtkEventControllerKey *controller, guint keyval,
guint keycode, GdkModifierType state,
gpointer data) {
(void)controller;
(void)keycode;
(void)state;
(void)data;
if (keyval == GDK_KEY_Escape) {
search_close();
return TRUE;
}
if (keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter) {
search_activate_selected();
return TRUE;
}
if (keyval == GDK_KEY_Down || keyval == GDK_KEY_Tab) {
GtkListBoxRow *row = gtk_list_box_get_row_at_index(
GTK_LIST_BOX(search_list), 0);
if (row)
gtk_widget_grab_focus(GTK_WIDGET(row));
return TRUE;
}
return FALSE;
}
static void on_scrim_clicked(GtkGestureClick *g, gint n_press, gdouble x,
gdouble y, gpointer data) {
(void)g;
(void)n_press;
(void)x;
(void)y;
(void)data;
search_close();
}
gboolean search_is_open(void) {
return search_layer && gtk_widget_get_visible(search_layer);
}
void search_close(void) {
if (search_layer)
gtk_widget_set_visible(search_layer, FALSE);
}
void search_open(void) {
if (!search_layer)
return;
gtk_widget_set_visible(search_layer, TRUE);
if (search_entry) {
gtk_editable_set_text(GTK_EDITABLE(search_entry), "");
gtk_widget_grab_focus(search_entry);
}
search_fill();
}
void search_apply_lang(void) {
if (!search_layer)
return;
if (search_is_open())
search_fill();
}
static void on_search_clicked(GtkButton *button, gpointer data) {
(void)button;
(void)data;
if (search_is_open())
search_close();
else
search_open();
}
GtkWidget *build_search_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, "search", 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_search_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_search_clicked), NULL);
return btn;
}
void search_attach(GtkOverlay *overlay) {
GtkWidget *scrim;
GtkWidget *card;
GtkWidget *scroll;
GtkEventController *keys;
GtkGesture *click;
search_layer = gtk_overlay_new();
gtk_widget_add_css_class(search_layer, "search-layer");
gtk_widget_set_hexpand(search_layer, TRUE);
gtk_widget_set_vexpand(search_layer, TRUE);
gtk_widget_set_visible(search_layer, FALSE);
scrim = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(scrim, "search-scrim");
gtk_widget_set_hexpand(scrim, TRUE);
gtk_widget_set_vexpand(scrim, TRUE);
gtk_overlay_set_child(GTK_OVERLAY(search_layer), scrim);
click = gtk_gesture_click_new();
g_signal_connect(click, "pressed", G_CALLBACK(on_scrim_clicked), NULL);
gtk_widget_add_controller(scrim, GTK_EVENT_CONTROLLER(click));
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_add_css_class(card, "search-card");
gtk_widget_set_halign(card, GTK_ALIGN_CENTER);
gtk_widget_set_valign(card, GTK_ALIGN_START);
gtk_widget_set_margin_top(card, 72);
gtk_widget_set_size_request(card, 560, -1);
gtk_overlay_add_overlay(GTK_OVERLAY(search_layer), card);
search_entry = gtk_entry_new();
gtk_widget_add_css_class(search_entry, "search-entry");
i18n_bind(search_entry, "search_placeholder", 3);
gtk_box_append(GTK_BOX(card), search_entry);
g_signal_connect(search_entry, "changed",
G_CALLBACK(on_search_changed), NULL);
keys = gtk_event_controller_key_new();
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_entry_key), NULL);
gtk_widget_add_controller(search_entry, keys);
search_empty = gtk_label_new(NULL);
i18n_bind(search_empty, "search_hint", 0);
gtk_widget_add_css_class(search_empty, "search-empty");
gtk_box_append(GTK_BOX(card), search_empty);
scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(scroll),
420);
gtk_scrolled_window_set_propagate_natural_height(
GTK_SCROLLED_WINDOW(scroll), TRUE);
gtk_box_append(GTK_BOX(card), scroll);
search_list = gtk_list_box_new();
gtk_widget_add_css_class(search_list, "search-results");
gtk_list_box_set_selection_mode(GTK_LIST_BOX(search_list),
GTK_SELECTION_SINGLE);
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(search_list), TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), search_list);
g_signal_connect(search_list, "row-activated",
G_CALLBACK(on_row_activated), NULL);
gtk_overlay_add_overlay(overlay, search_layer);
}