mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
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:
parent
dd8d992bf8
commit
6556c6691b
21 changed files with 490 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue