EntryView.swift
3932 bytes
1import SwiftUI
2
3struct EntryView: View {
4 let entry: LogEntry
5
6 @State private var showRomaji = false
7 @State private var showEnglish = false
8
9 private var hasRomaji: Bool { !(entry.romaji ?? "").isEmpty }
10 private var hasEnglish: Bool { !(entry.english ?? "").isEmpty }
11
12 var body: some View {
13 VStack(alignment: .leading, spacing: 10) {
14 if let action = entry.action {
15 Text("> \(action)")
16 .font(.callout.monospaced())
17 .foregroundStyle(.secondary)
18 }
19 if let desc = entry.desc {
20 Text(desc)
21 }
22 if entry.hasSpeech && (hasRomaji || hasEnglish) {
23 speechCard
24 }
25 if let judge = entry.judge {
26 if let said = judge.romaji, !said.isEmpty {
27 Text("said: \(said)")
28 .font(.callout)
29 .foregroundStyle(.secondary)
30 }
31 section("Judge") {
32 row("Score", "\(judge.score)/10")
33 if let feedback = judge.feedback, !feedback.isEmpty {
34 row("Feedback", feedback)
35 }
36 }
37 }
38 if let ask = entry.ask {
39 section("Ask") {
40 if let romaji = ask.romaji, !romaji.isEmpty {
41 row("Romaji", romaji)
42 }
43 if let translation = ask.translation, !translation.isEmpty {
44 row("Translation", translation)
45 }
46 if let breakdown = ask.breakdown, !breakdown.isEmpty {
47 row("Breakdown", breakdown)
48 }
49 }
50 }
51 }
52 }
53
54 private var speechCard: some View {
55 GlassEffectContainer(spacing: 6) {
56 VStack(alignment: .leading, spacing: 8) {
57 if hasRomaji {
58 revealChip(title: "Romaji", revealed: $showRomaji, text: entry.romaji)
59 }
60 if hasEnglish {
61 revealChip(title: "Translation", revealed: $showEnglish, text: entry.english)
62 }
63 }
64 }
65 }
66
67 private func revealChip(title: String, revealed: Binding<Bool>, text: String?) -> some View {
68 Button {
69 withAnimation(.snappy) {
70 revealed.wrappedValue.toggle()
71 }
72 } label: {
73 HStack(alignment: .firstTextBaseline, spacing: 8) {
74 if revealed.wrappedValue {
75 Text(text ?? "")
76 } else {
77 Image(systemName: "eye.slash")
78 .font(.caption)
79 Text(title)
80 .font(.callout)
81 }
82 Spacer(minLength: 0)
83 }
84 .foregroundStyle(revealed.wrappedValue ? AnyShapeStyle(.primary) : AnyShapeStyle(.secondary))
85 .frame(maxWidth: .infinity, alignment: .leading)
86 .padding(.horizontal, 14)
87 .padding(.vertical, 10)
88 .glassEffect(in: .rect(cornerRadius: 12))
89 .contentShape(.interaction, .rect(cornerRadius: 12))
90 }
91 .buttonStyle(.plain)
92 }
93
94 private func section(_ title: String, @ViewBuilder rows: () -> some View) -> some View {
95 VStack(alignment: .leading, spacing: 4) {
96 Text(title)
97 .font(.footnote.weight(.semibold))
98 .foregroundStyle(.secondary)
99 rows()
100 }
101 }
102
103 private func row(_ key: String, _ value: String) -> some View {
104 HStack(alignment: .firstTextBaseline, spacing: 12) {
105 Text(key)
106 .foregroundStyle(.secondary)
107 .frame(width: 84, alignment: .leading)
108 Text(value)
109 }
110 .font(.footnote)
111 }
112}