mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Add in-app search for subjects, lessons and exercises.
The header magnifier and Cmd/Ctrl+K open a palette that jumps straight to a matching page. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
45ed500fa2
commit
cafa18b189
8 changed files with 658 additions and 5 deletions
|
|
@ -63,7 +63,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
|
||||||
- **Technické vybavení** — 3 lekce (architektura, historie, bit/byte)
|
- **Technické vybavení** — 3 lekce (architektura, historie, bit/byte)
|
||||||
- **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba
|
- **Český jazyk a literatura** — mluvnice (20 cvičení) a maturitní četba
|
||||||
(1984, Spalovač mrtvol); literatura je zatím zamčená
|
(1984, Spalovač mrtvol); literatura je zatím zamčená
|
||||||
- statistiky, 10 témat, tmavý/světlý režim, čeština/angličtina
|
- statistiky, hledání (lupa nebo `Cmd/Ctrl+K`), 10 témat, tmavý/světlý režim, čeština/angličtina
|
||||||
- ukončení: `Cmd/Super+Q` nebo `Alt+F4`
|
- ukončení: `Cmd/Super+Q` nebo `Alt+F4`
|
||||||
|
|
||||||
### Použití
|
### Použití
|
||||||
|
|
@ -71,7 +71,7 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
|
||||||
1. **Pokračuj** otevře mapu předmětů.
|
1. **Pokračuj** otevře mapu předmětů.
|
||||||
2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka) vede na učební cestu.
|
2. Otevřený předmět (vlajka, Wi‑Fi, čip, česká vlajka) vede na učební cestu.
|
||||||
3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají.
|
3. Uzly cvičení / lekcí otevřou obsah; zamčené nic nedělají.
|
||||||
4. **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře.
|
4. **Hledat** (lupa), **Statistiky** (graf) a **Nastavení** (ozubené kolo) jsou vpravo nahoře.
|
||||||
|
|
||||||
### Sestavení ze zdroje
|
### Sestavení ze zdroje
|
||||||
|
|
||||||
|
|
@ -200,7 +200,7 @@ Intel Mac, another architecture, or hacking on the code →
|
||||||
- **Computer hardware** — 3 lessons (architecture, history, bit/byte)
|
- **Computer hardware** — 3 lessons (architecture, history, bit/byte)
|
||||||
- **Czech language** — grammar (20 exercises) and required reading
|
- **Czech language** — grammar (20 exercises) and required reading
|
||||||
(1984, Spalovač mrtvol); literature theory is still locked
|
(1984, Spalovač mrtvol); literature theory is still locked
|
||||||
- statistics, 10 palettes, dark/light mode, Czech/English
|
- statistics, search (magnifier or `Cmd/Ctrl+K`), 10 palettes, dark/light mode, Czech/English
|
||||||
- quit with `Cmd/Super+Q` or `Alt+F4`
|
- quit with `Cmd/Super+Q` or `Alt+F4`
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
@ -208,7 +208,7 @@ Intel Mac, another architecture, or hacking on the code →
|
||||||
1. **Continue** opens the subject map.
|
1. **Continue** opens the subject map.
|
||||||
2. An open subject (flag, Wi‑Fi, chip, Czech flag) opens its path.
|
2. An open subject (flag, Wi‑Fi, chip, Czech flag) opens its path.
|
||||||
3. Exercise / lesson nodes open content; locked nodes do nothing.
|
3. Exercise / lesson nodes open content; locked nodes do nothing.
|
||||||
4. **Statistics** (chart) and **Settings** (gear) sit in the top-right.
|
4. **Search** (magnifier), **Statistics** (chart) and **Settings** (gear) sit in the top-right.
|
||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ tooltip {
|
||||||
.btn-primary:focus-visible,
|
.btn-primary:focus-visible,
|
||||||
.back-btn:focus-visible,
|
.back-btn:focus-visible,
|
||||||
.settings-btn:focus-visible,
|
.settings-btn:focus-visible,
|
||||||
|
.search-hit:focus-visible,
|
||||||
.pill:focus-visible,
|
.pill:focus-visible,
|
||||||
.chip:focus-visible,
|
.chip:focus-visible,
|
||||||
.group-btn:focus-visible,
|
.group-btn:focus-visible,
|
||||||
|
|
@ -1040,6 +1041,53 @@ flowboxchild {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-scrim {
|
||||||
|
background-color: alpha(@bg_crust, 0.46);
|
||||||
|
}
|
||||||
|
.search-card {
|
||||||
|
background-color: @bg_surface0;
|
||||||
|
border: 1px solid alpha(@fg_text, 0.10);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
min-width: 520px;
|
||||||
|
}
|
||||||
|
.search-entry {
|
||||||
|
min-height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.search-empty {
|
||||||
|
color: @fg_subtext;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 18px 8px 8px;
|
||||||
|
}
|
||||||
|
.search-results {
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.search-results row {
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.search-results row:hover,
|
||||||
|
.search-results row:selected {
|
||||||
|
background-color: alpha(@fg_text, 0.07);
|
||||||
|
}
|
||||||
|
.search-hit-title {
|
||||||
|
color: @fg_text;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.search-hit-sub {
|
||||||
|
color: @fg_subtext;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.search-hit-locked .search-hit-title,
|
||||||
|
.search-hit-locked .search-hit-sub {
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
/* no-scale */
|
/* no-scale */
|
||||||
/* Rules below keep a fixed size (symbols/icons) even when the window grows. */
|
/* Rules below keep a fixed size (symbols/icons) even when the window grows. */
|
||||||
.bubble-number.branch-star {
|
.bubble-number.branch-star {
|
||||||
|
|
|
||||||
14
src/i18n.c
14
src/i18n.c
|
|
@ -221,6 +221,19 @@ const TrEntry tr_ui[] = {
|
||||||
{"stats_ex_fmt", "%d / %d", "%d / %d"},
|
{"stats_ex_fmt", "%d / %d", "%d / %d"},
|
||||||
{"stats_pct_fmt", "%d %%", "%d %%"},
|
{"stats_pct_fmt", "%d %%", "%d %%"},
|
||||||
{"stats_units_fmt", "%d / %d", "%d / %d"},
|
{"stats_units_fmt", "%d / %d", "%d / %d"},
|
||||||
|
{"search", "Hledat", "Search"},
|
||||||
|
{"search_placeholder", "Hledat předmět, lekci nebo cvičení",
|
||||||
|
"Search a subject, lesson or exercise"},
|
||||||
|
{"search_empty", "Nic se nenašlo", "No matches"},
|
||||||
|
{"search_hint", "Zkuste předmět, lekci nebo název cvičení",
|
||||||
|
"Try a subject, lesson or exercise name"},
|
||||||
|
{"search_subject", "Předmět", "Subject"},
|
||||||
|
{"search_unit", "Jednotka", "Unit"},
|
||||||
|
{"search_lesson", "Lekce", "Lesson"},
|
||||||
|
{"search_exercise", "Cvičení", "Exercise"},
|
||||||
|
{"search_book", "Četba", "Reading"},
|
||||||
|
{"search_page", "Stránka", "Page"},
|
||||||
|
{"search_home", "Úvod", "Welcome"},
|
||||||
{"subjects_title", "Předměty", "Subjects"},
|
{"subjects_title", "Předměty", "Subjects"},
|
||||||
{"subjects_sub", "Otevřený předmět spustí učební cestu. Zamčené zatím jen čekají.",
|
{"subjects_sub", "Otevřený předmět spustí učební cestu. Zamčené zatím jen čekají.",
|
||||||
"An open subject opens its path. Locked ones are placeholders for now."},
|
"An open subject opens its path. Locked ones are placeholders for now."},
|
||||||
|
|
@ -1519,4 +1532,5 @@ void apply_language(void) {
|
||||||
update_apply_lang();
|
update_apply_lang();
|
||||||
net_lessons_apply_lang();
|
net_lessons_apply_lang();
|
||||||
hw_lessons_apply_lang();
|
hw_lessons_apply_lang();
|
||||||
|
search_apply_lang();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/icons.c
24
src/icons.c
|
|
@ -450,6 +450,30 @@ void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void draw_search_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;
|
||||||
|
double cx = width / 2.0 - 1.4;
|
||||||
|
double cy = height / 2.0 - 1.4;
|
||||||
|
double r = MIN(width, height) * 0.28;
|
||||||
|
|
||||||
|
(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.7);
|
||||||
|
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||||
|
|
||||||
|
cairo_new_sub_path(cr);
|
||||||
|
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||||
|
cairo_stroke(cr);
|
||||||
|
|
||||||
|
cairo_move_to(cr, cx + r * 0.72, cy + r * 0.72);
|
||||||
|
cairo_line_to(cr, cx + r * 1.55, cy + r * 1.55);
|
||||||
|
cairo_stroke(cr);
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
|
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);
|
||||||
|
|
|
||||||
|
|
@ -468,6 +468,7 @@ void activate(GtkApplication *app, gpointer user_data) {
|
||||||
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());
|
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_stats_button());
|
||||||
|
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_search_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),
|
||||||
|
|
@ -477,11 +478,14 @@ void activate(GtkApplication *app, gpointer user_data) {
|
||||||
* something to report, so it never shifts the layout unprompted. */
|
* something to report, so it never shifts the layout unprompted. */
|
||||||
{
|
{
|
||||||
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
GtkWidget *overlay = gtk_overlay_new();
|
||||||
|
|
||||||
gtk_box_append(GTK_BOX(root), build_update_banner());
|
gtk_box_append(GTK_BOX(root), build_update_banner());
|
||||||
gtk_widget_set_vexpand(GTK_WIDGET(main_stack), TRUE);
|
gtk_widget_set_vexpand(GTK_WIDGET(main_stack), TRUE);
|
||||||
gtk_box_append(GTK_BOX(root), GTK_WIDGET(main_stack));
|
gtk_box_append(GTK_BOX(root), GTK_WIDGET(main_stack));
|
||||||
gtk_window_set_child(window, root);
|
gtk_overlay_set_child(GTK_OVERLAY(overlay), root);
|
||||||
|
gtk_window_set_child(window, overlay);
|
||||||
|
search_attach(GTK_OVERLAY(overlay));
|
||||||
}
|
}
|
||||||
|
|
||||||
welcome_page = build_welcome_page();
|
welcome_page = build_welcome_page();
|
||||||
|
|
|
||||||
|
|
@ -803,6 +803,8 @@ void draw_settings_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
int width, int height, gpointer data);
|
int width, int height, gpointer data);
|
||||||
void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
|
void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
int width, int height, gpointer data);
|
int width, int height, gpointer data);
|
||||||
|
void draw_search_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
|
int width, int height, gpointer data);
|
||||||
GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
|
GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
|
||||||
double r, double g, double b, int px);
|
double r, double g, double b, int px);
|
||||||
void setup_portable_paths(void);
|
void setup_portable_paths(void);
|
||||||
|
|
@ -1120,7 +1122,13 @@ GtkWidget *make_mode_chip(const char *label, ColorMode mode,
|
||||||
GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group);
|
GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group);
|
||||||
void on_settings_clicked(GtkButton *button, gpointer user_data);
|
void on_settings_clicked(GtkButton *button, gpointer user_data);
|
||||||
GtkWidget *build_stats_button(void);
|
GtkWidget *build_stats_button(void);
|
||||||
|
GtkWidget *build_search_button(void);
|
||||||
GtkWidget *build_settings_button(void);
|
GtkWidget *build_settings_button(void);
|
||||||
|
void search_attach(GtkOverlay *overlay);
|
||||||
|
void search_open(void);
|
||||||
|
void search_close(void);
|
||||||
|
gboolean search_is_open(void);
|
||||||
|
void search_apply_lang(void);
|
||||||
void settings_open(void);
|
void settings_open(void);
|
||||||
void unit_meta_init(void);
|
void unit_meta_init(void);
|
||||||
void unit_configure(int idx, const char *const *names, int n);
|
void unit_configure(int idx, const char *const *names, int n);
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,18 @@ gboolean on_window_key_pressed(GtkEventControllerKey *controller,
|
||||||
return GDK_EVENT_STOP;
|
return GDK_EVENT_STOP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (search_is_open() && keyval == GDK_KEY_Escape) {
|
||||||
|
search_close();
|
||||||
|
return GDK_EVENT_STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((keyval == GDK_KEY_k || keyval == GDK_KEY_K ||
|
||||||
|
keyval == GDK_KEY_f || keyval == GDK_KEY_F) &&
|
||||||
|
(state & (GDK_CONTROL_MASK | GDK_SUPER_MASK | GDK_META_MASK)) != 0) {
|
||||||
|
search_open();
|
||||||
|
return GDK_EVENT_STOP;
|
||||||
|
}
|
||||||
|
|
||||||
return GDK_EVENT_PROPAGATE;
|
return GDK_EVENT_PROPAGATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
543
src/search.c
Normal file
543
src/search.c
Normal file
|
|
@ -0,0 +1,543 @@
|
||||||
|
#include "maturita.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define SEARCH_MAX 24
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *title;
|
||||||
|
char *subtitle;
|
||||||
|
char *hay;
|
||||||
|
const 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 = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(G_OBJECT(row), "target", (gpointer)it->target);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue