diff --git a/CMakeLists.txt b/CMakeLists.txt index 199e233..ab55985 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,9 @@ add_custom_command(TARGET maturita POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/style.css $/style.css + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/data/changelog.txt + $/changelog.txt COMMAND ${CMAKE_COMMAND} -E make_directory $/icons/hicolor/512x512/apps COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/Makefile b/Makefile index 7f6505f..aee3d04 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,7 @@ bundle: $(TARGET) @mkdir -p dist @cp $(TARGET) dist/ @cp $(DATA_DIR)/style.css dist/ + @cp $(DATA_DIR)/changelog.txt dist/ @mkdir -p dist/icons/hicolor/512x512/apps @cp "$(ICON_DST)" dist/icons/hicolor/512x512/apps/ @cp -R $(DATA_DIR)/share dist/ diff --git a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt index d9a9193..f4904da 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -32,6 +32,8 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { private set var update by mutableStateOf(UpdateState()) private set + var showChangelog by mutableStateOf(shouldShowChangelog()) + private set private val stack = mutableStateListOf(Route.Subjects) val route: Route get() = stack.last() @@ -146,4 +148,21 @@ class AppViewModel(app: Application) : AndroidViewModel(app) { fun installUpdate() { updater.install { update = it } } + + fun dismissChangelog() { + progress.seenCommit = BuildConfig.COMMIT + showChangelog = false + } + + private fun shouldShowChangelog(): Boolean { + if (content.changelog.isEmpty()) return false + return !sameBuild(BuildConfig.COMMIT, progress.seenCommit) + } +} + +internal fun sameBuild(a: String?, b: String?): Boolean { + if (a.isNullOrEmpty() || b.isNullOrEmpty()) return false + if (a.equals(b, ignoreCase = true)) return true + val n = minOf(a.length, b.length) + return n >= 7 && a.regionMatches(0, b, 0, n, ignoreCase = true) } diff --git a/android/app/src/main/java/org/maturita/maturita/data/Content.kt b/android/app/src/main/java/org/maturita/maturita/data/Content.kt index 3905a98..40b38ee 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/Content.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/Content.kt @@ -31,6 +31,7 @@ class Content(root: JSONObject) { val hw = raw.arr("hw") val mluvnice = raw.arr("mluvnice") val books = raw.arr("books") + val changelog = raw.arr("changelog") fun germanUnit(id: Int) = german.firstOrNull { it.int("id") == id } fun netLesson(id: Int) = netLessons.firstOrNull { it.int("id") == id } diff --git a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt index 71b4e1b..ef40e7c 100644 --- a/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt +++ b/android/app/src/main/java/org/maturita/maturita/data/ProgressStore.kt @@ -17,6 +17,10 @@ class ProgressStore(context: Context) { get() = if (prefs.getString("lang", "cs") == "en") UiLang.En else UiLang.Cs set(v) { prefs.edit().putString("lang", if (v == UiLang.En) "en" else "cs").apply() } + var seenCommit: String + get() = prefs.getString("seen_commit", "") ?: "" + set(v) { prefs.edit().putString("seen_commit", v).apply() } + fun germanDone(unit: Int, ex: Int) = prefs.getBoolean("g.$unit.$ex", false) fun markGerman(unit: Int, ex: Int) { prefs.edit().putBoolean("g.$unit.$ex", true).apply() } fun vocabDone(unit: Int) = prefs.getBoolean("g.$unit.vocab", false) diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index 350587f..30fd1c9 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -91,6 +91,9 @@ fun HomeScreen(vm: AppViewModel) { val sum = summarize(vm.content, vm.progress) val pct = if (sum.totalEx == 0) 0f else sum.doneEx / sum.totalEx.toFloat() RootScaffold(vm, "Maturita") { + if (vm.showChangelog) { + item { ChangelogCard(vm) } + } item { Card( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), @@ -141,6 +144,51 @@ fun HomeScreen(vm: AppViewModel) { } } +@Composable +private fun ChangelogCard(vm: AppViewModel) { + val langKey = if (vm.lang == UiLang.En) "en" else "cs" + val blocks = vm.content.changelog.take(3) + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer), + ) { + Column(Modifier.padding(20.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + vm.tr("changelog_title"), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.weight(1f), + ) + val date = blocks.firstOrNull()?.str("date").orEmpty() + if (date.isNotEmpty()) { + Text( + date, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.7f), + ) + } + } + Spacer(Modifier.height(10.dp)) + blocks.forEachIndexed { index, block -> + if (index > 0) Spacer(Modifier.height(8.dp)) + block.strs(langKey).ifEmpty { block.strs("cs") }.forEach { line -> + Text( + "• $line", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.padding(bottom = 4.dp), + ) + } + } + Spacer(Modifier.height(8.dp)) + FilledTonalButton(onClick = { vm.dismissChangelog() }) { + Text(vm.tr("changelog_dismiss")) + } + } + } +} + @Composable private fun UpdateBanner(vm: AppViewModel) { Card( diff --git a/data/changelog.txt b/data/changelog.txt new file mode 100644 index 0000000..5150400 --- /dev/null +++ b/data/changelog.txt @@ -0,0 +1,15 @@ +# User-facing notes, newest first. Copied next to style.css in every +# package and folded into content.json for Android and iOS. +# After an update the home screen shows the latest blocks until dismissed. +# +# --- YYYY-MM-DD +# cs: bullet +# en: bullet + +--- 2026-09-18 +cs: Po aktualizaci se na hlavní stránce ukáže, co je nového +cs: Android updater teď nainstaluje podepsané APK ze stejného klíče +cs: iOS se otevírá z Maturita.xcworkspace a běží nativně na iOS 26 +en: After an update the home screen shows what is new +en: The Android updater now installs a signed APK from a stable key +en: iOS opens from Maturita.xcworkspace and runs natively on iOS 26 diff --git a/data/style.css b/data/style.css index ce09873..9b0ebbc 100644 --- a/data/style.css +++ b/data/style.css @@ -159,6 +159,42 @@ tooltip { font-size: 12px; font-weight: 600; } +.changelog-card { + background-color: alpha(@accent, 0.10); + border: 1px solid alpha(@accent, 0.28); + border-radius: 20px; + padding: 20px 24px 16px 24px; +} +.changelog-title { + color: @fg_text; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.3px; +} +.changelog-date { + color: @fg_subtext; + font-size: 12px; + font-weight: 600; +} +.changelog-item { + color: @fg_text; + font-size: 15px; +} +.changelog-dismiss { + background-image: none; + background-color: @accent; + color: @on_accent; + border: none; + border-radius: 999px; + padding: 8px 16px; + font-size: 13px; + font-weight: 600; + box-shadow: none; + margin-top: 6px; +} +.changelog-dismiss:hover { + background-color: shade(@accent, 1.1); +} .subject-grid { padding: 8px 4px 24px 4px; } diff --git a/ios/Maturita/AppModel.swift b/ios/Maturita/AppModel.swift index c8cdb04..d4bcadf 100644 --- a/ios/Maturita/AppModel.swift +++ b/ios/Maturita/AppModel.swift @@ -14,11 +14,13 @@ final class AppModel: ObservableObject { @Published var searchQuery = "" @Published var tick = 0 @Published var update = UpdateState() + @Published var showChangelog = false init() { themeId = progress.themeId mode = progress.mode lang = progress.lang + showChangelog = !content.changelog.isEmpty && !sameBuild(AppConfig.commit, progress.seenCommit) #if DEBUG // `xcrun simctl launch org.maturita.maturita -route u1e3` opens a page for screenshots. if let page = UserDefaults.standard.string(forKey: "route"), let r = routeFromPage(page) { @@ -141,4 +143,16 @@ final class AppModel: ObservableObject { self?.update = state } } + + func dismissChangelog() { + progress.seenCommit = AppConfig.commit + showChangelog = false + } +} + +func sameBuild(_ a: String?, _ b: String?) -> Bool { + guard let a, let b, !a.isEmpty, !b.isEmpty else { return false } + if a.caseInsensitiveCompare(b) == .orderedSame { return true } + let n = min(a.count, b.count) + return n >= 7 && a.prefix(n).caseInsensitiveCompare(b.prefix(n)) == .orderedSame } diff --git a/ios/Maturita/Data/Content.swift b/ios/Maturita/Data/Content.swift index e556d5b..aefbe32 100644 --- a/ios/Maturita/Data/Content.swift +++ b/ios/Maturita/Data/Content.swift @@ -33,6 +33,7 @@ final class Content { let hw: [J] let mluvnice: [J] let books: [J] + let changelog: [J] init(_ root: [String: Any]) { raw = J(root) @@ -63,6 +64,7 @@ final class Content { hw = raw.arr("hw") mluvnice = raw.arr("mluvnice") books = raw.arr("books") + changelog = raw.arr("changelog") } func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } } diff --git a/ios/Maturita/Data/ProgressStore.swift b/ios/Maturita/Data/ProgressStore.swift index b8c6432..0df5180 100644 --- a/ios/Maturita/Data/ProgressStore.swift +++ b/ios/Maturita/Data/ProgressStore.swift @@ -18,6 +18,11 @@ final class ProgressStore { set { defaults.set(newValue == .en ? "en" : "cs", forKey: "lang") } } + var seenCommit: String { + get { defaults.string(forKey: "seen_commit") ?? "" } + set { defaults.set(newValue, forKey: "seen_commit") } + } + func germanDone(_ unit: Int, _ ex: Int) -> Bool { defaults.bool(forKey: "g.\(unit).\(ex)") } func markGerman(_ unit: Int, _ ex: Int) { defaults.set(true, forKey: "g.\(unit).\(ex)") } func vocabDone(_ unit: Int) -> Bool { defaults.bool(forKey: "g.\(unit).vocab") } diff --git a/ios/Maturita/UI/Screens.swift b/ios/Maturita/UI/Screens.swift index a0424e9..2bbda04 100644 --- a/ios/Maturita/UI/Screens.swift +++ b/ios/Maturita/UI/Screens.swift @@ -7,6 +7,14 @@ struct PracticeHome: View { let sum = summarize(vm.content, vm.progress) let pct = sum.totalEx == 0 ? 0 : Double(sum.doneEx) / Double(sum.totalEx) List { + if vm.showChangelog { + Section { + ChangelogCard(vm: vm) + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + } Section { GlassCard { VStack(alignment: .leading, spacing: 10) { @@ -59,6 +67,55 @@ struct PracticeHome: View { } } +struct ChangelogCard: View { + @ObservedObject var vm: AppModel + + var body: some View { + let langKey = vm.lang == .en ? "en" : "cs" + let blocks = Array(vm.content.changelog.prefix(3)) + GlassCard { + VStack(alignment: .leading, spacing: 10) { + HStack { + Label(vm.tr("changelog_title"), systemImage: "sparkles") + .font(.headline) + Spacer() + if let date = blocks.first?.strOrNull("date") { + Text(date) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + } + } + ForEach(Array(blocks.enumerated()), id: \.offset) { index, block in + ChangelogLines(index: index, lines: changelogLines(block, langKey)) + } + Button(vm.tr("changelog_dismiss")) { vm.dismissChangelog() } + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + } +} + +private func changelogLines(_ block: J, _ langKey: String) -> [String] { + let preferred = block.strs(langKey) + return preferred.isEmpty ? block.strs("cs") : preferred +} + +private struct ChangelogLines: View { + let index: Int + let lines: [String] + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + if index > 0 { Spacer().frame(height: 4) } + ForEach(Array(lines.enumerated()), id: \.offset) { _, line in + Text("• \(line)") + .font(.subheadline) + } + } + } +} + struct SettingsScreen: View { @ObservedObject var vm: AppModel diff --git a/scripts/bundle-linux.sh b/scripts/bundle-linux.sh index 709ef46..ae54bb7 100755 --- a/scripts/bundle-linux.sh +++ b/scripts/bundle-linux.sh @@ -36,6 +36,7 @@ mkdir -p "$APPDIR/usr/bin" \ cp -f "$ROOT/maturita" "$APPDIR/usr/bin/maturita" cp -f "$ROOT/data/style.css" "$APPDIR/style.css" +cp -f "$ROOT/data/changelog.txt" "$APPDIR/changelog.txt" cp -f "$ICON_512" "$APPDIR/icons/hicolor/512x512/apps/maturita.png" cp -f "$ICON_512" "$APPDIR/usr/share/icons/hicolor/512x512/apps/maturita.png" cp -f "$ICON_512" "$APPDIR/maturita.png" diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh index ffd8432..40d04ca 100755 --- a/scripts/bundle-macos.sh +++ b/scripts/bundle-macos.sh @@ -47,6 +47,7 @@ mkdir -p "$MACOS" "$RES/icons/hicolor/512x512/apps" "$RES/share" "$FW" cp -f "$ROOT/maturita" "$MACOS/maturita-bin" cp -f "$ROOT/data/style.css" "$RES/style.css" +cp -f "$ROOT/data/changelog.txt" "$RES/changelog.txt" cp -f "$ICON_512" "$RES/icons/hicolor/512x512/apps/maturita.png" cp -R "$ROOT/data/share/." "$RES/share/" diff --git a/scripts/extract-android-content.py b/scripts/extract-android-content.py index 00bcf2c..12f11e9 100644 --- a/scripts/extract-android-content.py +++ b/scripts/extract-android-content.py @@ -690,6 +690,28 @@ def net_tasks(rows) -> list[dict]: return out +def parse_changelog(path: Path) -> list[dict]: + if not path.is_file(): + return [] + entries: list[dict] = [] + current = None + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("---"): + current = {"date": line[3:].strip(), "cs": [], "en": []} + entries.append(current) + continue + if current is None: + continue + if line.startswith("cs:"): + current["cs"].append(line[3:].strip()) + elif line.startswith("en:"): + current["en"].append(line[3:].strip()) + return [e for e in entries if e["cs"] or e["en"]] + + def parse_i18n(src: str) -> dict: arrays = find_arrays(src) out = {} @@ -1219,6 +1241,7 @@ def main() -> int: "hw": hw_lessons, "mluvnice": mluvnice, "books": books, + "changelog": parse_changelog(ROOT / "data" / "changelog.txt"), } # Fix single-string banks stored as char* "arrays" @@ -1252,6 +1275,7 @@ def main() -> int: print(f" hw lessons: {sum(1 for L in hw_lessons if L['slides'])}") print(f" mluvnice: {len(mluvnice)}") print(f" books quiz items: {[len(b['quiz']) for b in books]}") + print(f" changelog entries: {len(payload.get('changelog', []))}") missing = [] for u in german_units: if not u["unlocked"]: diff --git a/src/changelog.c b/src/changelog.c new file mode 100644 index 0000000..80aef26 --- /dev/null +++ b/src/changelog.c @@ -0,0 +1,234 @@ +#include "maturita.h" + +#include + +/* ------------------------------------------------------------------ */ +/* What's new on the home page after an update */ +/* ------------------------------------------------------------------ */ + +/* The packaged notes live in changelog.txt next to style.css. After a + * published (or first) start whose commit is not the one the user last + * dismissed, the welcome page shows the newest entries. Dismissing stores + * APP_COMMIT in settings so the card stays gone until the next update. */ + +#define CHANGELOG_MAX_BLOCKS 3 + +typedef struct { + char *date; + GPtrArray *cs; + GPtrArray *en; +} ChangelogBlock; + +static struct { + GPtrArray *blocks; + char *seen; + GtkWidget *card; + GtkWidget *date; + GtkWidget *items; + gboolean loaded; +} cl; + +static gboolean same_build(const char *a, const char *b) { + size_t n; + + if (!a || !b || !a[0] || !b[0]) + return FALSE; + if (g_ascii_strcasecmp(a, b) == 0) + return TRUE; + n = MIN(strlen(a), strlen(b)); + return n >= 7 && g_ascii_strncasecmp(a, b, n) == 0; +} + +void changelog_set_seen(const char *sha) { + g_free(cl.seen); + cl.seen = (sha && sha[0]) ? g_strdup(sha) : NULL; +} + +const char *changelog_seen(void) { + return cl.seen ? cl.seen : ""; +} + +static void free_block(gpointer data) { + ChangelogBlock *b = data; + + if (!b) + return; + g_free(b->date); + if (b->cs) + g_ptr_array_free(b->cs, TRUE); + if (b->en) + g_ptr_array_free(b->en, TRUE); + g_free(b); +} + +static ChangelogBlock *new_block(const char *date) { + ChangelogBlock *b = g_new0(ChangelogBlock, 1); + + b->date = g_strdup(date ? date : ""); + b->cs = g_ptr_array_new_with_free_func(g_free); + b->en = g_ptr_array_new_with_free_func(g_free); + return b; +} + +static void load_changelog(void) { + gchar *text = NULL; + gchar **lines; + ChangelogBlock *cur = NULL; + guint i; + + if (cl.loaded) + return; + cl.loaded = TRUE; + cl.blocks = g_ptr_array_new_with_free_func(free_block); + + if (!g_file_get_contents(CHANGELOG_FILE, &text, NULL, NULL) || !text) + return; + + lines = g_strsplit(text, "\n", -1); + g_free(text); + + for (i = 0; lines[i]; i++) { + char *line = g_strstrip(lines[i]); + + if (!line[0] || line[0] == '#') + continue; + if (g_str_has_prefix(line, "---")) { + char *date = g_strstrip(line + 3); + + cur = new_block(date); + g_ptr_array_add(cl.blocks, cur); + continue; + } + if (!cur) + continue; + if (g_str_has_prefix(line, "cs:")) + g_ptr_array_add(cur->cs, g_strdup(g_strstrip(line + 3))); + else if (g_str_has_prefix(line, "en:")) + g_ptr_array_add(cur->en, g_strdup(g_strstrip(line + 3))); + } + g_strfreev(lines); +} + +static gboolean changelog_pending(void) { + load_changelog(); + if (!cl.blocks || cl.blocks->len == 0) + return FALSE; + return !same_build(APP_COMMIT, cl.seen); +} + +static GPtrArray *block_lines(const ChangelogBlock *b) { + GPtrArray *src; + + if (!b) + return NULL; + src = (app_lang == LANG_EN) ? b->en : b->cs; + if (src && src->len > 0) + return src; + return (b->cs && b->cs->len > 0) ? b->cs : b->en; +} + +static void fill_items(void) { + GtkWidget *child; + guint i, shown = 0; + + if (!cl.items) + return; + + while ((child = gtk_widget_get_first_child(cl.items))) + gtk_box_remove(GTK_BOX(cl.items), child); + + if (!cl.blocks) + return; + + for (i = 0; i < cl.blocks->len && shown < CHANGELOG_MAX_BLOCKS; i++) { + const ChangelogBlock *b = g_ptr_array_index(cl.blocks, i); + GPtrArray *lines = block_lines(b); + guint j; + + if (!lines || lines->len == 0) + continue; + if (shown == 0 && cl.date) + gtk_label_set_text(GTK_LABEL(cl.date), b->date ? b->date : ""); + if (shown > 0) { + GtkWidget *gap = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + gtk_widget_set_margin_top(gap, 8); + gtk_box_append(GTK_BOX(cl.items), gap); + } + for (j = 0; j < lines->len; j++) { + char *text = g_strdup_printf("• %s", + (const char *)lines->pdata[j]); + GtkWidget *row = gtk_label_new(text); + + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_label_set_wrap(GTK_LABEL(row), TRUE); + gtk_label_set_xalign(GTK_LABEL(row), 0.0); + gtk_label_set_max_width_chars(GTK_LABEL(row), 52); + gtk_widget_add_css_class(row, "changelog-item"); + gtk_box_append(GTK_BOX(cl.items), row); + g_free(text); + } + shown++; + } +} + +void changelog_apply_lang(void) { + if (cl.card && gtk_widget_get_visible(cl.card)) + fill_items(); +} + +static void on_changelog_dismiss(GtkButton *btn, gpointer data) { + (void)btn; + (void)data; + changelog_set_seen(APP_COMMIT); + save_settings(); + if (cl.card) + gtk_widget_set_visible(cl.card, FALSE); +} + +GtkWidget *build_changelog_card(void) { + GtkWidget *card; + GtkWidget *head; + GtkWidget *title; + GtkWidget *date; + GtkWidget *items; + GtkWidget *dismiss; + + load_changelog(); + + card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_widget_add_css_class(card, "changelog-card"); + gtk_widget_set_hexpand(card, TRUE); + gtk_widget_set_visible(card, changelog_pending()); + + head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + gtk_box_append(GTK_BOX(card), head); + + title = gtk_label_new(NULL); + i18n_bind(title, "changelog_title", 0); + gtk_widget_set_halign(title, GTK_ALIGN_START); + gtk_widget_set_hexpand(title, TRUE); + gtk_widget_add_css_class(title, "changelog-title"); + gtk_box_append(GTK_BOX(head), title); + + date = gtk_label_new(""); + gtk_widget_set_valign(date, GTK_ALIGN_CENTER); + gtk_widget_add_css_class(date, "changelog-date"); + gtk_box_append(GTK_BOX(head), date); + + items = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_box_append(GTK_BOX(card), items); + + dismiss = gtk_button_new(); + i18n_bind(dismiss, "changelog_dismiss", 1); + gtk_widget_set_halign(dismiss, GTK_ALIGN_START); + gtk_widget_add_css_class(dismiss, "changelog-dismiss"); + g_signal_connect(dismiss, "clicked", G_CALLBACK(on_changelog_dismiss), NULL); + gtk_box_append(GTK_BOX(card), dismiss); + + cl.card = card; + cl.date = date; + cl.items = items; + fill_items(); + return card; +} diff --git a/src/i18n.c b/src/i18n.c index 7c408c3..5701117 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -78,6 +78,8 @@ const TrEntry tr_ui[] = { {"update_err_badapk", "Stažený soubor není platný build maturita.c.", "The downloaded file is not a valid maturita.c build."}, + {"changelog_title", "Co je nového", "What's new"}, + {"changelog_dismiss", "Rozumím", "Got it"}, {"welcome_feat1_title", "Efektivní procvičování", "Focused practice"}, {"welcome_feat1_body", "Cvičení, nápovědy a vokabeltraining. Aplikace si pamatuje, kde jste skončili.", @@ -1557,6 +1559,7 @@ void apply_language(void) { refresh_welcome_heading(); refresh_stats_ui(); update_apply_lang(); + changelog_apply_lang(); net_lessons_apply_lang(); hw_lessons_apply_lang(); search_apply_lang(); diff --git a/src/main.c b/src/main.c index 72389f4..ccb2da6 100644 --- a/src/main.c +++ b/src/main.c @@ -414,6 +414,8 @@ static gboolean run_smoke_test(gpointer data) { fprintf(f, "commit=%s\n", APP_COMMIT); fprintf(f, "css=%s\n", g_file_test(CSS_FILE, G_FILE_TEST_IS_REGULAR) ? "ok" : "MISSING"); + fprintf(f, "changelog=%s\n", + g_file_test(CHANGELOG_FILE, G_FILE_TEST_IS_REGULAR) ? "ok" : "MISSING"); fprintf(f, "progress_dir=%s\n", app_progress_dir ? app_progress_dir : "?"); for (i = 0; pages[i]; i++) { diff --git a/src/maturita.h b/src/maturita.h index c0402b1..2ca7795 100644 --- a/src/maturita.h +++ b/src/maturita.h @@ -20,6 +20,7 @@ #define HW_SUBJ 2 /* index of "Technické vybavení" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CSS_FILE "style.css" +#define CHANGELOG_FILE "changelog.txt" #define APP_ID "org.maturita.Maturita" /* Published builds bake in the commit they were made from (see COMMIT in the * Makefile), which is what the updater compares against the build published @@ -907,6 +908,11 @@ void update_apply_lang(void); void update_on_settings_open(void); GtkWidget *build_update_banner(void); GtkWidget *build_update_box(void); +/* changelog.c */ +void changelog_set_seen(const char *sha); +const char *changelog_seen(void); +void changelog_apply_lang(void); +GtkWidget *build_changelog_card(void); void draw_finish_cell(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); void road_point(double t, double *ox, double *oy); diff --git a/src/navigation.c b/src/navigation.c index 527b548..16f58e1 100644 --- a/src/navigation.c +++ b/src/navigation.c @@ -240,6 +240,13 @@ GtkWidget *build_welcome_page(void) { gtk_box_append(GTK_BOX(hero), welcome_stat()); + { + GtkWidget *notes = build_changelog_card(); + + gtk_widget_set_margin_top(notes, 28); + gtk_box_append(GTK_BOX(page), notes); + } + features = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14); gtk_widget_set_margin_top(features, 36); gtk_box_append(GTK_BOX(page), features); diff --git a/src/theme.c b/src/theme.c index 1ed5566..5f91940 100644 --- a/src/theme.c +++ b/src/theme.c @@ -287,6 +287,7 @@ void save_settings(void) { app_color_mode == MODE_LIGHT ? "light" : "dark"); g_key_file_set_string(kf, "ui", "lang", app_lang == LANG_EN ? "en" : "cs"); + g_key_file_set_string(kf, "ui", "seen_commit", changelog_seen()); if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) { g_key_file_free(kf); @@ -347,6 +348,12 @@ void load_settings(void) { g_free(lang); } } + { + gchar *seen = g_key_file_get_string(kf, "ui", "seen_commit", NULL); + + changelog_set_seen(seen); + g_free(seen); + } g_key_file_free(kf); }