moved the css to an external file

This commit is contained in:
Martin Dvořák 2026-09-11 09:59:12 +02:00
commit 4cc68e3e46
5 changed files with 804 additions and 769 deletions

View file

@ -10,6 +10,7 @@
#define NUM_SUBJECTS 13
#define NET_SUBJ 1 /* index of "Správa počítačových sítí" */
#define CSS_FILE "style.css"
#define PROGRESS_DIR "progress"
#define PROGRESS_U1 "progress/unit1.conf"
#define PROGRESS_U2 "progress/unit2.conf"
@ -1058,771 +1059,54 @@ static void apply_language(void) {
net_slide_apply();
}
static char *read_css_file(void) {
gchar *contents = NULL;
GError *err = NULL;
if (!g_file_get_contents(CSS_FILE, &contents, NULL, &err)) {
g_warning("Could not load stylesheet '%s': %s", CSS_FILE,
err ? err->message : "unknown error");
if (err)
g_error_free(err);
return g_strdup("");
}
return contents;
}
/* The palette is data-driven, so its @define-color bindings are generated
* here and prepended to the rules loaded from the external stylesheet. */
static char *build_theme_css(const ThemePalette *p) {
return g_strdup_printf(
"@define-color bg_crust #%06x;"
"@define-color bg_base #%06x;"
"@define-color bg_mantle #%06x;"
"@define-color bg_surface0 #%06x;"
"@define-color bg_surface1 #%06x;"
"@define-color bg_surface2 #%06x;"
"@define-color fg_overlay #%06x;"
"@define-color fg_text #%06x;"
"@define-color fg_subtext #%06x;"
"@define-color accent #%06x;"
"@define-color accent2 #%06x;"
"@define-color accent3 #%06x;"
"@define-color success #%06x;"
"@define-color success2 #%06x;"
"@define-color warning #%06x;"
"@define-color error #%06x;"
"@define-color on_accent #%06x;"
"@define-color node_bg #%06x;"
"@define-color locked_bg #%06x;"
"@define-color locked_border #%06x;"
"window {"
" background-color: @bg_base;"
" background-image: linear-gradient(160deg, @bg_crust 0%%, @bg_base 40%%, @bg_surface0 100%%);"
"}"
"button {"
" background-image: none;"
"}"
".app-title {"
" color: @fg_text;"
" font-weight: 700;"
" font-size: 14px;"
"}"
".settings-btn,"
".settings-btn:hover,"
".settings-btn:active,"
".settings-btn:checked,"
".settings-btn:focus,"
".settings-btn:focus-visible {"
" min-width: 34px;"
" min-height: 34px;"
" padding: 0;"
" border-radius: 999px;"
" background: transparent;"
" background-color: transparent;"
" background-image: none;"
" border: none;"
" outline: none;"
" box-shadow: none;"
" color: @fg_text;"
"}"
".settings-btn:hover {"
" color: @accent;"
" background-color: alpha(@bg_surface0, 0.55);"
"}"
".settings-btn:active {"
" color: @accent;"
" background-color: alpha(@bg_surface1, 0.4);"
"}"
"popover,"
"popover.background,"
".settings-popover {"
" background: @bg_mantle;"
" background-color: @bg_mantle;"
" color: @fg_text;"
" border: 1px solid @bg_surface1;"
" border-radius: 18px;"
" box-shadow: 0 18px 40px alpha(@bg_crust, 0.45);"
"}"
"popover > contents,"
".settings-popover > contents {"
" background: transparent;"
" background-color: transparent;"
" border: none;"
" border-radius: 18px;"
" box-shadow: none;"
" padding: 0;"
"}"
".settings-box {"
" background-color: transparent;"
" color: @fg_text;"
" min-width: 292px;"
" padding: 16px 16px 14px 16px;"
"}"
".settings-title {"
" color: @fg_text;"
" font-size: 17px;"
" font-weight: 800;"
" letter-spacing: -0.2px;"
"}"
".settings-label {"
" color: @fg_overlay;"
" font-size: 11px;"
" font-weight: 700;"
" letter-spacing: 0.8px;"
"}"
".settings-seg {"
" background-color: alpha(@bg_surface0, 0.55);"
" border: none;"
" border-radius: 12px;"
" padding: 3px;"
"}"
".mode-chip {"
" background-image: none;"
" background-color: transparent;"
" color: @fg_subtext;"
" border: none;"
" border-radius: 10px;"
" padding: 9px 12px;"
" font-size: 13px;"
" font-weight: 600;"
" min-width: 0;"
" box-shadow: none;"
" transition: background-color 140ms ease, color 140ms ease,"
" box-shadow 140ms ease;"
"}"
".mode-chip:hover {"
" background-color: alpha(@bg_surface1, 0.28);"
" color: @fg_text;"
"}"
".mode-chip:checked {"
" background-image: linear-gradient(135deg, @accent 0%%, @accent2 100%%);"
" color: @on_accent;"
" font-weight: 800;"
" box-shadow: 0 4px 12px alpha(@accent, 0.25);"
"}"
".mode-chip:checked:hover {"
" background-image: linear-gradient(135deg, @accent 0%%, @accent2 100%%);"
" color: @on_accent;"
"}"
".theme-grid {"
" background-color: transparent;"
" border: none;"
" padding: 0;"
"}"
"flowboxchild {"
" padding: 0;"
" margin: 0;"
" background: transparent;"
" border: none;"
" box-shadow: none;"
"}"
".theme-card {"
" background-image: none;"
" background-color: transparent;"
" color: @fg_text;"
" border: none;"
" border-radius: 12px;"
" padding: 6px 4px;"
" min-width: 0;"
" box-shadow: none;"
" transition: background-color 140ms ease;"
"}"
".theme-card:hover {"
" background-color: alpha(@bg_surface0, 0.45);"
"}"
".theme-card:checked {"
" background-color: transparent;"
" box-shadow: none;"
"}"
".theme-card:checked:hover {"
" background-color: alpha(@bg_surface0, 0.35);"
"}"
".theme-card-name {"
" color: @fg_subtext;"
" font-size: 11px;"
" font-weight: 600;"
"}"
".theme-card:checked .theme-card-name {"
" color: @fg_text;"
" font-weight: 700;"
"}"
"headerbar,"
"headerbar.titlebar,"
".titlebar {"
" background-color: @bg_base;"
" background-image: none;"
" color: @fg_text;"
" box-shadow: none;"
" border: none;"
"}"
"windowcontrols button:not(:hover) {"
" color: @fg_text;"
"}"
"windowcontrols button:not(:hover) image {"
" color: @fg_text;"
"}"
".heading {"
" color: @fg_text;"
" font-size: 32px;"
" font-weight: 800;"
"}"
".card {"
" background-color: alpha(@bg_surface0, 0.92);"
" border: 1px solid @bg_surface1;"
" border-radius: 18px;"
" padding: 30px 34px;"
" color: @fg_text;"
" font-size: 16px;"
"}"
".btn-primary {"
" background-image: linear-gradient(135deg, @accent 0%%, @accent2 100%%);"
" color: @on_accent;"
" font-size: 16px;"
" font-weight: 700;"
" padding: 12px 40px;"
" border: none;"
" border-radius: 999px;"
" box-shadow: 0 6px 18px alpha(@accent, 0.25);"
" transition: box-shadow 150ms ease, background-image 150ms ease;"
"}"
".btn-primary:hover {"
" box-shadow: 0 8px 24px alpha(@accent, 0.45);"
" background-image: linear-gradient(135deg, @accent2 0%%, @accent3 100%%);"
"}"
".btn-primary:active {"
" box-shadow: 0 3px 10px alpha(@accent, 0.35);"
"}"
".roadmap-title {"
" color: @fg_text;"
" font-size: 28px;"
" font-weight: 800;"
"}"
".roadmap-sub {"
" color: @fg_subtext;"
" font-size: 14px;"
"}"
".back-btn {"
" min-width: 42px;"
" min-height: 42px;"
" padding: 0;"
" border-radius: 999px;"
" background-color: alpha(@bg_surface0, 0.9);"
" border: 1px solid @bg_surface1;"
" color: @fg_text;"
" box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);"
" transition: background-color 150ms ease, color 150ms ease,"
" border-color 150ms ease, box-shadow 150ms ease;"
"}"
".back-btn:hover {"
" background-color: @bg_surface1;"
" border-color: @bg_surface2;"
" color: @accent;"
" box-shadow: 0 6px 20px alpha(@accent, 0.35);"
"}"
".back-btn:active {"
" background-color: @bg_surface0;"
" box-shadow: 0 2px 8px alpha(@bg_crust, 0.35);"
"}"
".unit-node {"
" background-image: none;"
" background-color: @node_bg;"
" color: @fg_text;"
" border: 2px solid @locked_border;"
" border-radius: 999px;"
" padding: 0;"
" box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);"
" transition: transform 150ms ease, box-shadow 150ms ease,"
" border-color 150ms ease;"
"}"
".unit-node.done {"
" background-image: linear-gradient(135deg, @success 0%%, @success2 100%%);"
" border-color: @success;"
" color: @on_accent;"
" box-shadow: 0 4px 16px alpha(@success, 0.35);"
"}"
".unit-node.current {"
" background-image: linear-gradient(135deg, @accent 0%%, @accent2 55%%, @accent3 100%%);"
" border-color: @accent;"
" color: @on_accent;"
" box-shadow: 0 4px 18px alpha(@accent, 0.5),"
" 0 0 0 4px alpha(@bg_base, 0.9),"
" 0 0 0 7px alpha(@accent, 0.45);"
"}"
".unit-node.done:hover,"
".unit-node.current:hover {"
" transition: box-shadow 150ms ease, border-color 150ms ease;"
"}"
".unit-node.done:hover {"
" box-shadow: 0 8px 22px alpha(@success, 0.5);"
"}"
".unit-node.current:hover {"
" box-shadow: 0 8px 26px alpha(@accent, 0.65),"
" 0 0 0 4px alpha(@bg_base, 0.9),"
" 0 0 0 7px alpha(@accent, 0.55);"
"}"
".unit-node.locked {"
" background-image: none;"
" background-color: @locked_bg;"
" border: 2px solid @locked_border;"
" color: @fg_overlay;"
" box-shadow: none;"
"}"
".unit-node.locked:hover {"
" transform: none;"
" box-shadow: none;"
"}"
".unit-node.finish {"
" background-color: transparent;"
" background-image: none;"
" border: none;"
" color: @fg_overlay;"
" box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);"
"}"
".unit-node.finish:hover {"
" transform: none;"
" box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);"
"}"
".unit-node:focus,"
".unit-node:focus-visible,"
".unit-node:hover {"
" outline: none;"
"}"
".unit-number {"
" font-size: 24px;"
" font-weight: 800;"
"}"
".unit-node.done .unit-number,"
".unit-node.current .unit-number {"
" color: @on_accent;"
"}"
".unit-node.locked .unit-number {"
" color: @fg_overlay;"
"}"
".state-icon {"
" color: @on_accent;"
"}"
".unit-node.locked .lock-icon {"
" color: @bg_surface2;"
"}"
".unit-name {"
" color: @fg_text;"
" font-size: 13px;"
" font-weight: 600;"
"}"
".unit-name-locked {"
" color: @fg_overlay;"
"}"
".ex-bubble {"
" min-width: 64px;"
" min-height: 64px;"
" padding: 0;"
" border-radius: 999px;"
" background-color: @node_bg;"
" color: @fg_text;"
" border: 2px solid @locked_border;"
" box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);"
" transition: border-color 150ms ease, box-shadow 150ms ease;"
"}"
".ex-bubble:hover {"
" background-image: none;"
" border-color: @accent;"
" box-shadow: 0 6px 20px alpha(@accent, 0.35);"
"}"
".ex-bubble.done {"
" background-image: linear-gradient(135deg, @success 0%%, @success2 100%%);"
" border-color: @success;"
" color: @on_accent;"
" box-shadow: 0 4px 16px alpha(@success, 0.35);"
"}"
".ex-bubble.done:hover {"
" box-shadow: 0 8px 22px alpha(@success, 0.5);"
"}"
".ex-bubble:focus,"
".ex-bubble:focus-visible {"
" outline: none;"
"}"
".bubble-number {"
" font-size: 22px;"
" font-weight: 800;"
"}"
".ex-bubble.done .bubble-number {"
" color: @on_accent;"
"}"
".bubble-icon {"
" color: @on_accent;"
"}"
".ex-label {"
" color: @fg_text;"
" font-size: 12px;"
" font-weight: 600;"
"}"
".ex-prompt {"
" color: @fg_text;"
" font-size: 16px;"
" font-weight: 500;"
"}"
".ex-sub {"
" color: @fg_subtext;"
" font-size: 13px;"
" font-weight: 700;"
"}"
".hint {"
" color: @fg_subtext;"
" font-size: 13px;"
" font-style: italic;"
"}"
".meaning {"
" color: @fg_subtext;"
" font-size: 13px;"
" font-style: italic;"
"}"
".u3-answer {"
" color: @success;"
" font-size: 13px;"
" font-weight: 700;"
"}"
".notes-card {"
" background-color: alpha(@bg_surface0, 0.9);"
" border: 1px solid @bg_surface1;"
" border-radius: 24px;"
" padding: 24px 34px;"
" box-shadow: 0 10px 30px alpha(@bg_crust, 0.20);"
"}"
".notes-kicker {"
" color: @accent2;"
" font-weight: 800;"
" letter-spacing: 2px;"
"}"
".notes-title {"
" color: @fg_text;"
" font-weight: 800;"
"}"
".notes-body {"
" color: @fg_text;"
"}"
".notes-tip {"
" color: @success2;"
" font-weight: 700;"
"}"
".dlg-speaker {"
" color: @accent;"
" font-weight: 700;"
" font-size: 15px;"
"}"
".number-big {"
" color: @warning;"
" font-size: 26px;"
" font-weight: 800;"
"}"
".hm-word {"
" color: @fg_text;"
" font-family: monospace;"
" font-size: 30px;"
" font-weight: 800;"
"}"
".chain {"
" color: @warning;"
" font-size: 20px;"
" font-weight: 700;"
" letter-spacing: 1px;"
"}"
"entry {"
" background-color: @bg_mantle;"
" color: @fg_text;"
" border: 1px solid @bg_surface1;"
" border-radius: 10px;"
" padding: 8px 12px;"
" font-size: 15px;"
" caret-color: @accent;"
"}"
"entry:focus {"
" border-color: @accent;"
"}"
"combobox {"
" background-color: @bg_mantle;"
" color: @fg_text;"
"}"
"combobox button {"
" background-image: none;"
" background-color: @bg_mantle;"
" color: @fg_text;"
" border: 1px solid @bg_surface1;"
" border-radius: 10px;"
" padding: 4px 10px;"
"}"
"combobox button:hover {"
" background-image: none;"
" border-color: @accent;"
"}"
"combobox.answer-ok button {"
" border-color: @success;"
" background-color: alpha(@success, 0.12);"
"}"
"combobox.answer-wrong button {"
" border-color: @error;"
" background-color: alpha(@error, 0.12);"
"}"
"entry.answer-ok {"
" border-color: @success;"
" background-color: alpha(@success, 0.08);"
"}"
"entry.answer-wrong {"
" border-color: @error;"
" background-color: alpha(@error, 0.08);"
"}"
".pill {"
" background-image: none;"
" background-color: @node_bg;"
" color: @fg_text;"
" border: 2px solid @locked_border;"
" border-radius: 999px;"
" padding: 6px 18px;"
" font-size: 15px;"
" transition: border-color 150ms ease, box-shadow 150ms ease;"
"}"
".pill:hover {"
" background-image: none;"
" background-color: @node_bg;"
" border-color: @accent;"
" box-shadow: none;"
"}"
".pill:active {"
" background-image: none;"
" background-color: @bg_surface1;"
" box-shadow: none;"
"}"
".pill:checked {"
" background-image: linear-gradient(135deg, @accent 0%%, @accent2 100%%);"
" color: @on_accent;"
" border-color: @accent;"
" font-weight: 700;"
"}"
".pill.ok {"
" border-color: @success;"
" box-shadow: 0 0 0 2px alpha(@success, 0.4);"
"}"
".pill.wrong {"
" border-color: @error;"
" box-shadow: 0 0 0 2px alpha(@error, 0.4);"
"}"
".pill.hm-key {"
" padding: 4px 5px;"
" min-width: 32px;"
" font-size: 13px;"
"}"
".chip {"
" background-image: none;"
" background-color: @bg_surface1;"
" color: @fg_text;"
" border: 1px solid @bg_surface2;"
" border-radius: 999px;"
" padding: 6px 14px;"
" font-size: 14px;"
" font-weight: 600;"
" transition: background-color 150ms ease, border-color 150ms ease;"
"}"
".chip:hover {"
" background-image: none;"
" background-color: @bg_surface2;"
" border-color: @accent;"
" box-shadow: none;"
"}"
".chip:active {"
" background-image: none;"
" background-color: @bg_surface2;"
" box-shadow: none;"
"}"
".chip.ghost-host {"
" opacity: 0;"
"}"
".group-btn {"
" background-image: none;"
" background-color: @node_bg;"
" color: @fg_text;"
" border: 2px solid @locked_border;"
" border-radius: 12px;"
" padding: 8px 18px;"
" font-size: 14px;"
" font-weight: 700;"
" transition: border-color 150ms ease, box-shadow 150ms ease;"
"}"
".group-btn:hover {"
" background-image: none;"
" background-color: @node_bg;"
" border-color: @accent;"
" box-shadow: none;"
"}"
".group-btn:active {"
" background-image: none;"
" background-color: @bg_surface1;"
" box-shadow: none;"
"}"
".group-btn:checked {"
" background-image: linear-gradient(135deg, @accent3 0%%, @success2 100%%);"
" color: @on_accent;"
" border-color: @accent3;"
"}"
".group-panel {"
" background-color: alpha(@bg_surface0, 0.5);"
" border: 2px dashed @bg_surface1;"
" border-radius: 14px;"
" padding: 10px;"
"}"
".group-panel-label {"
" color: @fg_subtext;"
" font-size: 13px;"
" font-weight: 700;"
"}"
".sent-group {"
" background-color: alpha(@bg_surface0, 0.5);"
" border: 1px solid @bg_surface1;"
" border-radius: 14px;"
" padding: 12px;"
"}"
".sent-group.ok {"
" border-color: @success;"
"}"
".sent-group.wrong {"
" border-color: @error;"
"}"
".feedback-ok {"
" color: @success;"
" font-size: 15px;"
" font-weight: 700;"
"}"
".feedback-err {"
" color: @error;"
" font-size: 15px;"
" 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-subject {"
" margin-bottom: 20px;"
"}"
".stats-subject-name {"
" color: @fg_text;"
" font-size: 16px;"
" font-weight: 800;"
"}"
".stats-subject-count {"
" color: @fg_subtext;"
" font-size: 13px;"
" font-weight: 700;"
"}"
".stats-subject-count.locked {"
" color: @fg_overlay;"
"}"
".stats-subject-bar,"
".stats-subject-bar trough {"
" min-height: 12px;"
" border-radius: 999px;"
" background-color: alpha(@bg_surface1, 0.85);"
" border: none;"
" padding: 0;"
"}"
".stats-subject-bar {"
" margin: 4px 0 0 0;"
"}"
".stats-subject-bar progress {"
" min-height: 10px;"
" border-radius: 999px;"
" background-image: linear-gradient(90deg, @accent 0%%, @accent2 100%%);"
" background-color: @accent;"
" border: none;"
"}"
".stats-units {"
" margin-top: 8px;"
" border-left: 2px solid alpha(@fg_overlay, 0.22);"
" padding-left: 12px;"
"}"
".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 {"
" background-color: @bg_mantle;"
" border: 1px solid @bg_surface1;"
" border-radius: 10px;"
" color: @fg_text;"
"}",
char *palette = g_strdup_printf(
"@define-color bg_crust #%06x;\n"
"@define-color bg_base #%06x;\n"
"@define-color bg_mantle #%06x;\n"
"@define-color bg_surface0 #%06x;\n"
"@define-color bg_surface1 #%06x;\n"
"@define-color bg_surface2 #%06x;\n"
"@define-color fg_overlay #%06x;\n"
"@define-color fg_text #%06x;\n"
"@define-color fg_subtext #%06x;\n"
"@define-color accent #%06x;\n"
"@define-color accent2 #%06x;\n"
"@define-color accent3 #%06x;\n"
"@define-color success #%06x;\n"
"@define-color success2 #%06x;\n"
"@define-color warning #%06x;\n"
"@define-color error #%06x;\n"
"@define-color on_accent #%06x;\n"
"@define-color node_bg #%06x;\n"
"@define-color locked_bg #%06x;\n"
"@define-color locked_border #%06x;\n",
p->crust, p->base, p->mantle, p->surface0, p->surface1, p->surface2,
p->overlay, p->text, p->subtext, p->accent, p->accent2, p->accent3,
p->success, p->success2, p->warning, p->error, p->on_accent,
p->node, p->locked_bg, p->locked_border);
char *rules = read_css_file();
char *css = g_strconcat(palette, rules, NULL);
g_free(palette);
g_free(rules);
return css;
}
static void save_settings(void) {