diff --git a/ios/Maturita/UI/ExercisesMore.swift b/ios/Maturita/UI/ExercisesMore.swift index 5934373..cfc9513 100644 --- a/ios/Maturita/UI/ExercisesMore.swift +++ b/ios/Maturita/UI/ExercisesMore.swift @@ -799,27 +799,50 @@ struct LitQuizScreen: View { let qs = book.arr("quiz") let p = vm.palette ScrollView { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 14) { if finished { GlassCard { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 10) { Label(vm.tr("lit_finished"), systemImage: "rosette") + #if os(macOS) + .font(.largeTitle.weight(.bold)) + #else .font(.title2.weight(.bold)) - Text(vm.tr("lit_finished_text")).foregroundStyle(.secondary) - Text(vm.fmt("lit_score_fmt", score, qs.count)).font(.headline) + #endif + 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 { let q = qs[idx] - ProgressView(value: Double(idx), total: Double(qs.count)) + ProgressView(value: Double(idx + (answered ? 1 : 0)), total: Double(qs.count)) HStack { Text(vm.fmt("lit_question_fmt", idx + 1, qs.count)) Spacer() Text(vm.fmt("lit_score_fmt", score, qs.count)) } + #if os(macOS) + .font(.body.weight(.semibold)) + #else .font(.footnote) + #endif .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 let correct = i == q.int("correct") let chosen = answered && picked == i diff --git a/ios/Maturita/UI/Screens.swift b/ios/Maturita/UI/Screens.swift index 61fcf24..a0f6d14 100644 --- a/ios/Maturita/UI/Screens.swift +++ b/ios/Maturita/UI/Screens.swift @@ -971,21 +971,59 @@ struct BookListScreen: View { var body: some View { List { 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"))) { - Label { - ListRowLabel( - title: b.str("title"), - subtitle: { - let genre = b.str("genre") - return genre.isEmpty ? vm.tr(b.str("subKey")) : genre - }() - ) - } icon: { + HStack(alignment: .top, spacing: 12) { Image(systemName: "book.fill") #if os(macOS) + .font(.title2) + #else .font(.title3) #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) { let hasQuiz = !b.arr("quiz").isEmpty let hasPlot = !b.arr("plot").isEmpty - List { - if hasQuiz { - NavigationLink(value: Route.bookQuiz(id)) { - Label { - ListRowLabel(title: vm.tr(b.str("quizTitle")), subtitle: vm.tr(b.str("quizSub"))) - } icon: { - Image(systemName: "questionmark.circle") + let notes = b.arr("notes") + let quizDone = vm.progress.bookQuiz(id) + let plotDone = vm.progress.bookPlot(id) + let parts = bookTitleParts(b.str("title")) + ScrollView { + VStack(alignment: .leading, spacing: 28) { + GlassCard { + VStack(alignment: .leading, spacing: 10) { + if let author = parts.author { + Text(author) + #if os(macOS) + .font(.title3) + #else + .font(.subheadline) + #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 { + NavigationLink(value: Route.bookPlot(id)) { + BookActionCard( + title: vm.tr(b.str("plotTitle")), + subtitle: vm.tr(b.str("plotSub")), + systemImage: "list.number", + done: plotDone, + doneLabel: vm.tr("book_done") + ) + } + .buttonStyle(.plain) + } } } } - if hasPlot { - NavigationLink(value: Route.bookPlot(id)) { - Label { - ListRowLabel(title: vm.tr(b.str("plotTitle")), subtitle: vm.tr(b.str("plotSub"))) - } icon: { - Image(systemName: "list.number") - #if os(macOS) - .font(.title3) - #endif - } - } - } - ForEach(Array(b.arr("notes").enumerated()), id: \.offset) { _, note in - Section(note.str("title")) { - ForEach(Array(note.strs("lines").enumerated()), id: \.offset) { _, line in + #if os(macOS) + .padding(28) + .frame(maxWidth: MacChrome.exerciseMaxWidth, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) + #else + .padding() + .padding(.bottom, 24) + #endif + } + .navigationTitle(parts.title) + .navigationSubtitle(parts.author ?? vm.tr(b.str("subKey"))) + } + } +} + +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 + HStack(alignment: .top, spacing: 10) { + Text("•") + .foregroundStyle(.secondary) Text(line) #if os(macOS) .font(.title3) - .padding(.vertical, 4) + #else + .font(.body) #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[.. 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 { @ObservedObject var vm: AppModel let route: Route diff --git a/src/i18n.c b/src/i18n.c index 3360050..f395ed2 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -549,6 +549,13 @@ const TrEntry tr_ui[] = { {"Maturitní četba", "Maturitní četba", "Maturita reading list"}, {"reading_sub", "Vyberte knihu, kterou chcete studovat.", "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"}, {"cetba1984_sub", "Přehled knihy, klíčové pojmy a cvičení.", "Book overview, key terms and exercises."},