Show a what's-new card on the home screen after each update.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-18 19:51:52 +02:00
commit 6556c6691b
21 changed files with 490 additions and 0 deletions

View file

@ -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 <udid> 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
}

View file

@ -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 } }

View file

@ -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") }

View file

@ -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