mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Bake the release tag into the binary so the app knows what it is running; plain builds keep the "dev" default and never offer an update. The CI workflow already exported the tag on Linux and macOS, so only the Windows job needed it. Read the newest version from the redirect GitHub serves for /releases/latest, which avoids the REST API and its unauthenticated rate limit and needs no JSON parser. Networking goes through the curl command rather than a linked library: curl handles TLS, redirects and proxies, it ships on all three platforms, and it keeps the bundling scripts free of another shared library. An application cannot reliably replace its own files while running, so every platform follows one shape: unpack beside the current install, write a script that waits for this process to exit before swapping, then launch it detached and quit. Staging beside the install keeps the move on one filesystem and fails early when the location is read-only. The Windows script copies without mirroring so the progress files kept inside the install directory survive. Check quietly a few seconds after startup and offer the result in a banner above the pages, with a manual check in the settings popover. Co-authored-by: Cursor <cursoragent@cursor.com>
352 lines
13 KiB
C
352 lines
13 KiB
C
#include "maturita.h"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Settings */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void draw_theme_swatch(GtkDrawingArea *area, cairo_t *cr,
|
|
int width, int height, gpointer data) {
|
|
ThemeId id = (ThemeId)GPOINTER_TO_INT(data);
|
|
ThemePalette p = theme_palette(id, app_color_mode);
|
|
Rgb base = color_from_hex(p.base);
|
|
Rgb a1 = color_from_hex(p.accent);
|
|
Rgb a2 = color_from_hex(p.accent3);
|
|
Rgb ok = color_from_hex(p.success);
|
|
Rgb accent = color_from_hex(app_theme.accent);
|
|
gboolean selected = (app_theme_id == id);
|
|
double size = MIN(width, height);
|
|
double cx = width / 2.0;
|
|
double cy = height / 2.0;
|
|
double r = size / 2.0 - (selected ? 3.0 : 1.0);
|
|
double dot = MAX(3.5, r * 0.26);
|
|
double gap = dot * 2.05;
|
|
|
|
(void)area;
|
|
|
|
if (selected) {
|
|
cairo_new_path(cr);
|
|
cairo_arc(cr, cx, cy, size / 2.0 - 0.5, 0.0, 2.0 * G_PI);
|
|
cairo_set_source_rgb(cr, accent.r, accent.g, accent.b);
|
|
cairo_set_line_width(cr, 2.2);
|
|
cairo_stroke(cr);
|
|
}
|
|
|
|
cairo_new_path(cr);
|
|
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
|
cairo_set_source_rgb(cr, base.r, base.g, base.b);
|
|
cairo_fill(cr);
|
|
|
|
cairo_new_path(cr);
|
|
cairo_arc(cr, cx - gap, cy, dot, 0.0, 2.0 * G_PI);
|
|
cairo_set_source_rgb(cr, a1.r, a1.g, a1.b);
|
|
cairo_fill(cr);
|
|
|
|
cairo_new_path(cr);
|
|
cairo_arc(cr, cx, cy, dot, 0.0, 2.0 * G_PI);
|
|
cairo_set_source_rgb(cr, a2.r, a2.g, a2.b);
|
|
cairo_fill(cr);
|
|
|
|
cairo_new_path(cr);
|
|
cairo_arc(cr, cx + gap, cy, dot, 0.0, 2.0 * G_PI);
|
|
cairo_set_source_rgb(cr, ok.r, ok.g, ok.b);
|
|
cairo_fill(cr);
|
|
}
|
|
|
|
void on_mode_toggled(GtkToggleButton *btn, gpointer user_data) {
|
|
ColorMode mode = (ColorMode)GPOINTER_TO_INT(user_data);
|
|
|
|
if (!gtk_toggle_button_get_active(btn))
|
|
return;
|
|
if (app_color_mode == mode)
|
|
return;
|
|
app_color_mode = mode;
|
|
apply_theme();
|
|
save_settings();
|
|
}
|
|
|
|
void on_theme_toggled(GtkToggleButton *btn, gpointer user_data) {
|
|
ThemeId id = (ThemeId)GPOINTER_TO_INT(user_data);
|
|
|
|
if (!gtk_toggle_button_get_active(btn))
|
|
return;
|
|
if (app_theme_id == id)
|
|
return;
|
|
app_theme_id = id;
|
|
apply_theme();
|
|
save_settings();
|
|
}
|
|
|
|
void on_lang_toggled(GtkToggleButton *btn, gpointer user_data) {
|
|
UiLang lang = (UiLang)GPOINTER_TO_INT(user_data);
|
|
|
|
if (!gtk_toggle_button_get_active(btn))
|
|
return;
|
|
if (app_lang == lang)
|
|
return;
|
|
app_lang = lang;
|
|
apply_language();
|
|
save_settings();
|
|
}
|
|
|
|
GtkWidget *make_lang_chip(const char *label, UiLang lang,
|
|
GtkToggleButton *group) {
|
|
GtkWidget *btn = gtk_toggle_button_new_with_label(label);
|
|
|
|
gtk_widget_add_css_class(btn, "mode-chip");
|
|
gtk_widget_set_hexpand(btn, TRUE);
|
|
if (group)
|
|
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
|
|
g_signal_connect(btn, "toggled", G_CALLBACK(on_lang_toggled),
|
|
GINT_TO_POINTER(lang));
|
|
return btn;
|
|
}
|
|
|
|
GtkWidget *make_mode_chip(const char *label, ColorMode mode,
|
|
GtkToggleButton *group) {
|
|
GtkWidget *btn = gtk_toggle_button_new();
|
|
|
|
i18n_bind(btn, label, 1);
|
|
gtk_widget_add_css_class(btn, "mode-chip");
|
|
gtk_widget_set_hexpand(btn, TRUE);
|
|
if (group)
|
|
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
|
|
g_signal_connect(btn, "toggled", G_CALLBACK(on_mode_toggled),
|
|
GINT_TO_POINTER(mode));
|
|
return btn;
|
|
}
|
|
|
|
GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group) {
|
|
GtkWidget *btn;
|
|
GtkWidget *vbox;
|
|
GtkWidget *swatch;
|
|
GtkWidget *name;
|
|
|
|
btn = gtk_toggle_button_new();
|
|
gtk_widget_add_css_class(btn, "theme-card");
|
|
gtk_widget_set_hexpand(btn, TRUE);
|
|
if (group)
|
|
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
|
|
|
|
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
|
gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
|
|
gtk_button_set_child(GTK_BUTTON(btn), vbox);
|
|
|
|
swatch = gtk_drawing_area_new();
|
|
gtk_widget_set_size_request(swatch, 44, 44);
|
|
gtk_widget_set_halign(swatch, GTK_ALIGN_CENTER);
|
|
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(swatch),
|
|
draw_theme_swatch,
|
|
GINT_TO_POINTER(id), NULL);
|
|
theme_swatch_areas[id] = swatch;
|
|
gtk_box_append(GTK_BOX(vbox), swatch);
|
|
|
|
name = gtk_label_new(theme_names[id]);
|
|
gtk_widget_add_css_class(name, "theme-card-name");
|
|
gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
|
|
gtk_box_append(GTK_BOX(vbox), name);
|
|
|
|
g_signal_connect(btn, "toggled", G_CALLBACK(on_theme_toggled),
|
|
GINT_TO_POINTER(id));
|
|
return btn;
|
|
}
|
|
|
|
void on_settings_clicked(GtkButton *button, gpointer user_data) {
|
|
GtkPopover *popover = GTK_POPOVER(user_data);
|
|
|
|
(void)button;
|
|
gtk_popover_popup(popover);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
GtkWidget *build_settings_button(void) {
|
|
GtkWidget *btn;
|
|
GtkWidget *icon;
|
|
GtkWidget *popover;
|
|
GtkWidget *box;
|
|
GtkWidget *title;
|
|
GtkWidget *label;
|
|
GtkWidget *mode_row;
|
|
GtkWidget *lang_row;
|
|
GtkWidget *theme_grid;
|
|
GtkWidget *dark_btn;
|
|
GtkWidget *light_btn;
|
|
GtkWidget *cs_btn;
|
|
GtkWidget *en_btn;
|
|
GtkWidget *first_theme = NULL;
|
|
int i;
|
|
|
|
btn = gtk_button_new();
|
|
gtk_widget_add_css_class(btn, "settings-btn");
|
|
gtk_widget_add_css_class(btn, "flat");
|
|
i18n_bind(btn, "settings", 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_settings_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);
|
|
|
|
popover = gtk_popover_new();
|
|
gtk_widget_add_css_class(popover, "settings-popover");
|
|
gtk_popover_set_has_arrow(GTK_POPOVER(popover), TRUE);
|
|
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
|
|
gtk_widget_set_parent(popover, btn);
|
|
g_signal_connect_swapped(btn, "destroy",
|
|
G_CALLBACK(gtk_widget_unparent), popover);
|
|
g_signal_connect(btn, "clicked", G_CALLBACK(on_settings_clicked), popover);
|
|
|
|
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
|
|
gtk_widget_add_css_class(box, "settings-box");
|
|
|
|
title = gtk_label_new(NULL);
|
|
i18n_bind(title, "settings_title", 0);
|
|
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(title, "settings-title");
|
|
gtk_box_append(GTK_BOX(box), title);
|
|
|
|
label = gtk_label_new(NULL);
|
|
i18n_bind(label, "mode", 0);
|
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(label, "settings-label");
|
|
gtk_widget_set_margin_top(label, 4);
|
|
gtk_box_append(GTK_BOX(box), label);
|
|
|
|
mode_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
|
gtk_widget_add_css_class(mode_row, "settings-seg");
|
|
gtk_box_append(GTK_BOX(box), mode_row);
|
|
|
|
dark_btn = make_mode_chip("mode_dark", MODE_DARK, NULL);
|
|
light_btn = make_mode_chip("mode_light", MODE_LIGHT,
|
|
GTK_TOGGLE_BUTTON(dark_btn));
|
|
g_signal_handlers_block_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_mode_toggled, NULL);
|
|
g_signal_handlers_block_matched(light_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_mode_toggled, NULL);
|
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dark_btn),
|
|
app_color_mode == MODE_DARK);
|
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(light_btn),
|
|
app_color_mode == MODE_LIGHT);
|
|
g_signal_handlers_unblock_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_mode_toggled, NULL);
|
|
g_signal_handlers_unblock_matched(light_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_mode_toggled, NULL);
|
|
gtk_box_append(GTK_BOX(mode_row), dark_btn);
|
|
gtk_box_append(GTK_BOX(mode_row), light_btn);
|
|
|
|
label = gtk_label_new(NULL);
|
|
i18n_bind(label, "theme", 0);
|
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(label, "settings-label");
|
|
gtk_widget_set_margin_top(label, 2);
|
|
gtk_box_append(GTK_BOX(box), label);
|
|
|
|
theme_grid = gtk_flow_box_new();
|
|
gtk_widget_add_css_class(theme_grid, "theme-grid");
|
|
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(theme_grid),
|
|
GTK_SELECTION_NONE);
|
|
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
|
|
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
|
|
gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(theme_grid), 8);
|
|
gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(theme_grid), 8);
|
|
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(theme_grid), TRUE);
|
|
gtk_widget_set_hexpand(theme_grid, TRUE);
|
|
gtk_box_append(GTK_BOX(box), theme_grid);
|
|
|
|
for (i = 0; i < THEME_COUNT; i++) {
|
|
GtkWidget *card = make_theme_card(
|
|
(ThemeId)i,
|
|
first_theme ? GTK_TOGGLE_BUTTON(first_theme) : NULL);
|
|
if (!first_theme)
|
|
first_theme = card;
|
|
if (app_theme_id == (ThemeId)i) {
|
|
g_signal_handlers_block_matched(card, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_theme_toggled, NULL);
|
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(card), TRUE);
|
|
g_signal_handlers_unblock_matched(card, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_theme_toggled, NULL);
|
|
}
|
|
gtk_flow_box_insert(GTK_FLOW_BOX(theme_grid), card, -1);
|
|
}
|
|
|
|
label = gtk_label_new(NULL);
|
|
i18n_bind(label, "language", 0);
|
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(label, "settings-label");
|
|
gtk_widget_set_margin_top(label, 4);
|
|
gtk_box_append(GTK_BOX(box), label);
|
|
|
|
lang_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
|
gtk_widget_add_css_class(lang_row, "settings-seg");
|
|
gtk_box_append(GTK_BOX(box), lang_row);
|
|
|
|
cs_btn = make_lang_chip("Čeština", LANG_CS, NULL);
|
|
en_btn = make_lang_chip("English", LANG_EN, GTK_TOGGLE_BUTTON(cs_btn));
|
|
g_signal_handlers_block_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_lang_toggled, NULL);
|
|
g_signal_handlers_block_matched(en_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_lang_toggled, NULL);
|
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs_btn),
|
|
app_lang == LANG_CS);
|
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(en_btn),
|
|
app_lang == LANG_EN);
|
|
g_signal_handlers_unblock_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_lang_toggled, NULL);
|
|
g_signal_handlers_unblock_matched(en_btn, G_SIGNAL_MATCH_FUNC,
|
|
0, 0, NULL, on_lang_toggled, NULL);
|
|
gtk_box_append(GTK_BOX(lang_row), cs_btn);
|
|
gtk_box_append(GTK_BOX(lang_row), en_btn);
|
|
|
|
label = gtk_label_new(NULL);
|
|
i18n_bind(label, "updates", 0);
|
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(label, "settings-label");
|
|
gtk_widget_set_margin_top(label, 4);
|
|
gtk_box_append(GTK_BOX(box), label);
|
|
gtk_box_append(GTK_BOX(box), build_update_box());
|
|
|
|
{
|
|
GtkWidget *sw = gtk_scrolled_window_new();
|
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
|
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
|
/* Tall enough for every section, including updates at the bottom; GTK
|
|
* still shrinks the popover to fit the window and scrolls the rest. */
|
|
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), 820);
|
|
gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(sw), FALSE);
|
|
gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(sw), TRUE);
|
|
gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(sw), TRUE);
|
|
gtk_widget_set_hexpand(sw, FALSE);
|
|
gtk_widget_set_vexpand(sw, FALSE);
|
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), box);
|
|
gtk_popover_set_child(GTK_POPOVER(popover), sw);
|
|
}
|
|
|
|
return btn;
|
|
}
|