ad58ce83b61ae0d7ac2e32f10e2ca2987959a375

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Adopt Liquid Glass UI and start new adventures in-app

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)