ad58ce83b61ae0d7ac2e32f10e2ca2987959a375
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/Package.swift b/Package.swift
2index 1facf09daa3b7e5d20f1163c0954356d710a4d27..7a6154c24bce5ede164e1ba0a6d7af5a9b4c58c4 100644
3--- a/Package.swift
4+++ b/Package.swift
5@@ -5,8 +5,8 @@ import PackageDescription
6 let package = Package(
7 name: "japanese",
8 platforms: [
9- .iOS(.v17),
10- .macOS(.v14),
11+ .iOS("26.0"),
12+ .macOS("26.0"),
13 ],
14 products: [
15 // An xtool project should contain exactly one library product,
16diff --git a/Sources/japanese/EntryView.swift b/Sources/japanese/EntryView.swift
17index 0aa7496e7941c478f5e6a43c7840ae963806cf4b..0f1d7993c7d17e5b1e1ffa98a83c498a582670c8 100644
18--- a/Sources/japanese/EntryView.swift
19+++ b/Sources/japanese/EntryView.swift
20@@ -3,7 +3,11 @@ import SwiftUI
21 struct EntryView: View {
22 let entry: LogEntry
23
24- @State private var isRevealed = false
25+ @State private var showRomaji = false
26+ @State private var showEnglish = false
27+
28+ private var hasRomaji: Bool { !(entry.romaji ?? "").isEmpty }
29+ private var hasEnglish: Bool { !(entry.english ?? "").isEmpty }
30
31 var body: some View {
32 VStack(alignment: .leading, spacing: 10) {
33@@ -15,7 +19,7 @@ struct EntryView: View {
34 if let desc = entry.desc {
35 Text(desc)
36 }
37- if entry.hasSpeech {
38+ if entry.hasSpeech && (hasRomaji || hasEnglish) {
39 speechCard
40 }
41 if let judge = entry.judge {
42@@ -48,26 +52,38 @@ struct EntryView: View {
43 }
44
45 private var speechCard: some View {
46+ VStack(alignment: .leading, spacing: 0) {
47+ if hasRomaji {
48+ revealRow(title: "Romaji", revealed: $showRomaji, text: entry.romaji)
49+ }
50+ if hasRomaji && hasEnglish {
51+ Divider()
52+ }
53+ if hasEnglish {
54+ revealRow(title: "Translation", revealed: $showEnglish, text: entry.english)
55+ }
56+ }
57+ .glassEffect(in: .rect(cornerRadius: 16))
58+ }
59+
60+ private func revealRow(title: String, revealed: Binding<Bool>, text: String?) -> some View {
61 Button {
62- isRevealed.toggle()
63+ revealed.wrappedValue.toggle()
64 } label: {
65- VStack(alignment: .leading, spacing: 4) {
66- if isRevealed {
67- Text("» \(entry.romaji ?? "")")
68- if let english = entry.english, !english.isEmpty {
69- Text(english)
70- .font(.footnote)
71- .foregroundStyle(.secondary)
72- }
73+ HStack(alignment: .firstTextBaseline, spacing: 8) {
74+ if revealed.wrappedValue {
75+ Text(text ?? "")
76 } else {
77- Label("Tap to reveal", systemImage: "eye.slash")
78- .font(.footnote)
79- .foregroundStyle(.secondary)
80+ Image(systemName: "eye.slash")
81+ .font(.caption)
82+ Text(title)
83+ .font(.callout)
84 }
85+ Spacer(minLength: 0)
86 }
87+ .foregroundStyle(revealed.wrappedValue ? AnyShapeStyle(.primary) : AnyShapeStyle(.secondary))
88 .frame(maxWidth: .infinity, alignment: .leading)
89 .padding(12)
90- .background(.quaternary.opacity(0.5), in: .rect(cornerRadius: 12))
91 }
92 .buttonStyle(.plain)
93 }
94diff --git a/Sources/japanese/GameStore.swift b/Sources/japanese/GameStore.swift
95index 31a0bdb9fa7e2b18aff9afab440858aef788bfeb..b9cf8738426c91083eab2cd34093eba13a9757e6 100644
96--- a/Sources/japanese/GameStore.swift
97+++ b/Sources/japanese/GameStore.swift
98@@ -59,6 +59,9 @@ final class GameStore {
99 defer { setPhase(.idle) }
100 do {
101 let id = try await api.createSession(using: configuration)
102+ entries = []
103+ lastAudio = nil
104+ status = nil
105 setSession(id)
106 apply(try await api.performAction(sessionID: id, action: Self.openingAction, using: configuration))
107 } catch {
108diff --git a/Sources/japanese/GameView.swift b/Sources/japanese/GameView.swift
109index e89407721e0dd540e6b82e4f6c4d53b26301102e..1abc62658f9d819f8730bef0fdda279f9d8da842 100644
110--- a/Sources/japanese/GameView.swift
111+++ b/Sources/japanese/GameView.swift
112@@ -5,6 +5,7 @@ struct GameView: View {
113
114 @State private var store: GameStore
115 @State private var input = ""
116+ @State private var confirmingNewGame = false
117
118 init(settings: SettingsStore) {
119 self.settings = settings
120@@ -37,17 +38,27 @@ struct GameView: View {
121 .accessibilityLabel("Replay last line")
122 .disabled(!store.hasAudio || store.phase != .idle)
123
124- Button {
125- Task { await store.newGame() }
126- } label: {
127+ Button(action: startNewGame) {
128 if store.phase == .processing && store.entries.isEmpty {
129 ProgressView()
130 } else {
131 Image(systemName: "book")
132 }
133 }
134- .accessibilityLabel("New game")
135+ .accessibilityLabel("New adventure")
136 .disabled(store.phase != .idle)
137+ .confirmationDialog(
138+ "Start a new adventure?",
139+ isPresented: $confirmingNewGame,
140+ titleVisibility: .visible
141+ ) {
142+ Button("Start") {
143+ Task { await store.newGame() }
144+ }
145+ Button("Cancel", role: .cancel) {}
146+ } message: {
147+ Text("The current adventure will be discarded.")
148+ }
149
150 NavigationLink {
151 SettingsView(settings: settings)
152@@ -73,7 +84,7 @@ struct GameView: View {
153 Text("Start")
154 }
155 }
156- .buttonStyle(.borderedProminent)
157+ .buttonStyle(.glassProminent)
158
159 NavigationLink {
160 SettingsView(settings: settings)
161@@ -104,39 +115,53 @@ struct GameView: View {
162 .font(.footnote)
163 .foregroundStyle(.secondary)
164 .frame(maxWidth: .infinity, alignment: .leading)
165- .padding(.horizontal)
166 }
167- HStack(spacing: 8) {
168- Button(action: store.toggleTalk) {
169- if store.phase == .recording {
170- Image(systemName: "stop.circle.fill")
171- .foregroundStyle(.red)
172- } else {
173- Image(systemName: "mic")
174- }
175- }
176- .disabled(store.phase != .idle && store.phase != .recording)
177- .accessibilityLabel(store.phase == .recording ? "Stop recording" : "Start recording")
178+ GlassEffectContainer(spacing: 8) {
179+ HStack(spacing: 12) {
180+ TextField("What do you do?", text: $input)
181+ .textFieldStyle(.plain)
182+ .frame(maxWidth: .infinity)
183+ .frame(minHeight: 44)
184+ .padding(.horizontal, 16)
185+ .glassEffect()
186+ .disabled(store.phase != .idle)
187+ .onSubmit(submit)
188
189- TextField("What do you do?", text: $input)
190- .textFieldStyle(.roundedBorder)
191- .disabled(store.phase != .idle)
192- .onSubmit(submit)
193+ Button(action: store.toggleTalk) {
194+ Image(systemName: store.phase == .recording ? "stop.fill" : "mic")
195+ .foregroundStyle(store.phase == .recording ? Color.red : Color.primary)
196+ .frame(width: 44, height: 44)
197+ .glassEffect(.regular.interactive(), in: .circle)
198+ }
199+ .buttonStyle(.plain)
200+ .disabled(store.phase != .idle && store.phase != .recording)
201+ .accessibilityLabel(store.phase == .recording ? "Stop recording" : "Start recording")
202
203- if store.phase == .processing {
204- ProgressView()
205- .frame(width: 24)
206- } else {
207- Button(action: submit) {
208- Image(systemName: "arrow.up.circle")
209+ if store.phase == .processing {
210+ ProgressView()
211+ .frame(width: 44, height: 44)