mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Clarify book study flow with notes-first cards and practice actions.
Make reading-list pages show how to learn: readable note cards, then quiz/plot, with clearer book list metadata. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cc4415a8a6
commit
f6043c33c9
3 changed files with 274 additions and 37 deletions
|
|
@ -799,27 +799,50 @@ struct LitQuizScreen: View {
|
||||||
let qs = book.arr("quiz")
|
let qs = book.arr("quiz")
|
||||||
let p = vm.palette
|
let p = vm.palette
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
if finished {
|
if finished {
|
||||||
GlassCard {
|
GlassCard {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Label(vm.tr("lit_finished"), systemImage: "rosette")
|
Label(vm.tr("lit_finished"), systemImage: "rosette")
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.largeTitle.weight(.bold))
|
||||||
|
#else
|
||||||
.font(.title2.weight(.bold))
|
.font(.title2.weight(.bold))
|
||||||
Text(vm.tr("lit_finished_text")).foregroundStyle(.secondary)
|
#endif
|
||||||
Text(vm.fmt("lit_score_fmt", score, qs.count)).font(.headline)
|
Text(vm.tr("lit_finished_text"))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title3)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(vm.fmt("lit_score_fmt", score, qs.count))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title2.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.headline)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if idx < qs.count {
|
} else if idx < qs.count {
|
||||||
let q = qs[idx]
|
let q = qs[idx]
|
||||||
ProgressView(value: Double(idx), total: Double(qs.count))
|
ProgressView(value: Double(idx + (answered ? 1 : 0)), total: Double(qs.count))
|
||||||
HStack {
|
HStack {
|
||||||
Text(vm.fmt("lit_question_fmt", idx + 1, qs.count))
|
Text(vm.fmt("lit_question_fmt", idx + 1, qs.count))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(vm.fmt("lit_score_fmt", score, qs.count))
|
Text(vm.fmt("lit_score_fmt", score, qs.count))
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.body.weight(.semibold))
|
||||||
|
#else
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
|
#endif
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text(q.str("prompt")).font(.title3.weight(.semibold)).padding(.vertical, 12)
|
Text(q.str("prompt"))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
#endif
|
||||||
|
.padding(.vertical, 12)
|
||||||
ForEach(Array(q.strs("options").enumerated()), id: \.offset) { i, opt in
|
ForEach(Array(q.strs("options").enumerated()), id: \.offset) { i, opt in
|
||||||
let correct = i == q.int("correct")
|
let correct = i == q.int("correct")
|
||||||
let chosen = answered && picked == i
|
let chosen = answered && picked == i
|
||||||
|
|
|
||||||
|
|
@ -971,21 +971,59 @@ struct BookListScreen: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(Array(vm.content.books.enumerated()), id: \.offset) { _, b in
|
ForEach(Array(vm.content.books.enumerated()), id: \.offset) { _, b in
|
||||||
|
let parts = bookTitleParts(b.str("title"))
|
||||||
|
let quizDone = !b.arr("quiz").isEmpty && vm.progress.bookQuiz(b.str("id"))
|
||||||
|
let plotDone = !b.arr("plot").isEmpty && vm.progress.bookPlot(b.str("id"))
|
||||||
NavigationLink(value: Route.book(b.str("id"))) {
|
NavigationLink(value: Route.book(b.str("id"))) {
|
||||||
Label {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
ListRowLabel(
|
|
||||||
title: b.str("title"),
|
|
||||||
subtitle: {
|
|
||||||
let genre = b.str("genre")
|
|
||||||
return genre.isEmpty ? vm.tr(b.str("subKey")) : genre
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
} icon: {
|
|
||||||
Image(systemName: "book.fill")
|
Image(systemName: "book.fill")
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
.font(.title2)
|
||||||
|
#else
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
#endif
|
#endif
|
||||||
|
.foregroundStyle(.tint)
|
||||||
|
.frame(width: 28)
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(parts.title)
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.body.weight(.semibold))
|
||||||
|
#endif
|
||||||
|
if let author = parts.author {
|
||||||
|
Text(author)
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.body)
|
||||||
|
#else
|
||||||
|
.font(.subheadline)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
let genre = b.str("genre")
|
||||||
|
Text(genre.isEmpty ? vm.tr(b.str("subKey")) : genre)
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.subheadline)
|
||||||
|
#else
|
||||||
|
.font(.caption)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
if quizDone || plotDone {
|
||||||
|
Label(vm.tr("book_done"), systemImage: "checkmark.circle.fill")
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1003,50 +1041,219 @@ struct BookScreen: View {
|
||||||
if let b = vm.content.book(id) {
|
if let b = vm.content.book(id) {
|
||||||
let hasQuiz = !b.arr("quiz").isEmpty
|
let hasQuiz = !b.arr("quiz").isEmpty
|
||||||
let hasPlot = !b.arr("plot").isEmpty
|
let hasPlot = !b.arr("plot").isEmpty
|
||||||
List {
|
let notes = b.arr("notes")
|
||||||
if hasQuiz {
|
let quizDone = vm.progress.bookQuiz(id)
|
||||||
NavigationLink(value: Route.bookQuiz(id)) {
|
let plotDone = vm.progress.bookPlot(id)
|
||||||
Label {
|
let parts = bookTitleParts(b.str("title"))
|
||||||
ListRowLabel(title: vm.tr(b.str("quizTitle")), subtitle: vm.tr(b.str("quizSub")))
|
ScrollView {
|
||||||
} icon: {
|
VStack(alignment: .leading, spacing: 28) {
|
||||||
Image(systemName: "questionmark.circle")
|
GlassCard {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
if let author = parts.author {
|
||||||
|
Text(author)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
|
#else
|
||||||
|
.font(.subheadline)
|
||||||
#endif
|
#endif
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
if !b.str("genre").isEmpty {
|
||||||
|
Text(b.str("genre"))
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundStyle(.tint)
|
||||||
|
}
|
||||||
|
Text(vm.tr("book_study_hint"))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title3)
|
||||||
|
#else
|
||||||
|
.font(.body)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !notes.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
Text(vm.tr("book_notes"))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.title2.weight(.semibold))
|
||||||
|
#endif
|
||||||
|
ForEach(Array(notes.enumerated()), id: \.offset) { _, note in
|
||||||
|
BookNoteCard(note: note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasQuiz || hasPlot {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
Text(vm.tr("book_practice"))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.title2.weight(.semibold))
|
||||||
|
#endif
|
||||||
|
if hasQuiz {
|
||||||
|
NavigationLink(value: Route.bookQuiz(id)) {
|
||||||
|
BookActionCard(
|
||||||
|
title: vm.tr(b.str("quizTitle")),
|
||||||
|
subtitle: vm.tr(b.str("quizSub")),
|
||||||
|
systemImage: "questionmark.circle.fill",
|
||||||
|
done: quizDone,
|
||||||
|
doneLabel: vm.tr("book_done")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
if hasPlot {
|
if hasPlot {
|
||||||
NavigationLink(value: Route.bookPlot(id)) {
|
NavigationLink(value: Route.bookPlot(id)) {
|
||||||
Label {
|
BookActionCard(
|
||||||
ListRowLabel(title: vm.tr(b.str("plotTitle")), subtitle: vm.tr(b.str("plotSub")))
|
title: vm.tr(b.str("plotTitle")),
|
||||||
} icon: {
|
subtitle: vm.tr(b.str("plotSub")),
|
||||||
Image(systemName: "list.number")
|
systemImage: "list.number",
|
||||||
|
done: plotDone,
|
||||||
|
doneLabel: vm.tr("book_done")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.font(.title3)
|
.padding(28)
|
||||||
|
.frame(maxWidth: MacChrome.exerciseMaxWidth, alignment: .leading)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
#else
|
||||||
|
.padding()
|
||||||
|
.padding(.bottom, 24)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.navigationTitle(parts.title)
|
||||||
|
.navigationSubtitle(parts.author ?? vm.tr(b.str("subKey")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ForEach(Array(b.arr("notes").enumerated()), id: \.offset) { _, note in
|
}
|
||||||
Section(note.str("title")) {
|
|
||||||
|
private struct BookActionCard: View {
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let systemImage: String
|
||||||
|
let done: Bool
|
||||||
|
let doneLabel: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 16) {
|
||||||
|
Image(systemName: systemImage)
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.largeTitle)
|
||||||
|
#else
|
||||||
|
.font(.title)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.tint)
|
||||||
|
.frame(width: 44)
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.headline)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Text(subtitle)
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.body)
|
||||||
|
#else
|
||||||
|
.font(.subheadline)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
if done {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title2)
|
||||||
|
#else
|
||||||
|
.font(.title3)
|
||||||
|
#endif
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
.accessibilityLabel(doneLabel)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.body.weight(.semibold))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(20)
|
||||||
|
#else
|
||||||
|
.padding(16)
|
||||||
|
#endif
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.softSurface(cornerRadius: 18)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct BookNoteCard: View {
|
||||||
|
let note: J
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GlassCard {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Label(note.str("title"), systemImage: noteSymbol(note.str("icon")))
|
||||||
|
#if os(macOS)
|
||||||
|
.font(.title2.weight(.semibold))
|
||||||
|
#else
|
||||||
|
.font(.headline)
|
||||||
|
#endif
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
ForEach(Array(note.strs("lines").enumerated()), id: \.offset) { _, line in
|
ForEach(Array(note.strs("lines").enumerated()), id: \.offset) { _, line in
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
Text("•")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
Text(line)
|
Text(line)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.padding(.vertical, 4)
|
#else
|
||||||
|
.font(.body)
|
||||||
#endif
|
#endif
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(b.str("title"))
|
|
||||||
.navigationSubtitle(vm.tr(b.str("subKey")))
|
|
||||||
.appListStyle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func bookTitleParts(_ title: String) -> (author: String?, title: String) {
|
||||||
|
let separators = [" – ", " — ", " - "]
|
||||||
|
for sep in separators {
|
||||||
|
if let range = title.range(of: sep) {
|
||||||
|
let author = String(title[..<range.lowerBound]).trimmingCharacters(in: .whitespaces)
|
||||||
|
let name = String(title[range.upperBound...]).trimmingCharacters(in: .whitespaces)
|
||||||
|
if !author.isEmpty && !name.isEmpty {
|
||||||
|
return (author, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (nil, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func noteSymbol(_ icon: String) -> String {
|
||||||
|
switch icon {
|
||||||
|
case "book": return "book.fill"
|
||||||
|
case "globe": return "globe.europe.africa.fill"
|
||||||
|
case "people": return "person.2.fill"
|
||||||
|
case "bulb": return "lightbulb.fill"
|
||||||
|
default: return "text.alignleft"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct RouteDestination: View {
|
struct RouteDestination: View {
|
||||||
@ObservedObject var vm: AppModel
|
@ObservedObject var vm: AppModel
|
||||||
let route: Route
|
let route: Route
|
||||||
|
|
|
||||||
|
|
@ -549,6 +549,13 @@ const TrEntry tr_ui[] = {
|
||||||
{"Maturitní četba", "Maturitní četba", "Maturita reading list"},
|
{"Maturitní četba", "Maturitní četba", "Maturita reading list"},
|
||||||
{"reading_sub", "Vyberte knihu, kterou chcete studovat.",
|
{"reading_sub", "Vyberte knihu, kterou chcete studovat.",
|
||||||
"Pick a book to study."},
|
"Pick a book to study."},
|
||||||
|
{"book_study_hint",
|
||||||
|
"Nejdřív si projděte zápisky. Pak si látku ověřte v kvízu a seřaďte děj.",
|
||||||
|
"Read the study notes first. Then check yourself with the quiz and order the plot."},
|
||||||
|
{"book_notes", "Zápisky k maturitě", "Study notes"},
|
||||||
|
{"book_practice", "Procvičení", "Practice"},
|
||||||
|
{"book_done", "Hotovo", "Done"},
|
||||||
|
{"book_open", "Otevřít", "Open"},
|
||||||
{"book_1984_sub", "Antiutopický román • 1949", "Dystopian novel • 1949"},
|
{"book_1984_sub", "Antiutopický román • 1949", "Dystopian novel • 1949"},
|
||||||
{"cetba1984_sub", "Přehled knihy, klíčové pojmy a cvičení.",
|
{"cetba1984_sub", "Přehled knihy, klíčové pojmy a cvičení.",
|
||||||
"Book overview, key terms and exercises."},
|
"Book overview, key terms and exercises."},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue