commit dc67961e056fcd810bc6754a5ab354388e151419
parent 5f3b3d5ce893c77a5e3b683edb38436e73e458cf
Author: Michael Camilleri <[email protected]>
Date: Tue, 14 Apr 2026 14:06:12 +0900
Add player menu
This commit adds a player menu to the toolbar in the PuzzleView view.
The player can use this menu to change their name and/or change the tint
colour that is used to indicate selection.
Diffstat:
3 files changed, 87 insertions(+), 4 deletions(-)
diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift
@@ -73,6 +73,7 @@ struct RootView: View {
@Environment(NYTAuthService.self) private var nytAuth
@Environment(UbiquityMonitor.self) private var ubiquityMonitor
@Environment(\.scenePhase) private var scenePhase
+ @AppStorage("playerColorID") private var playerColorID: String = PlayerColor.blue.id
@State private var syncBootstrapped = false
@State private var lastVisitedGameID: UUID?
@State private var navigationPath = NavigationPath()
@@ -99,6 +100,7 @@ struct RootView: View {
}
}
}
+ .environment(\.playerColor, PlayerColor.color(for: playerColorID))
.task {
guard !syncBootstrapped else { return }
syncBootstrapped = true
diff --git a/Crossmate/Models/PlayerColor.swift b/Crossmate/Models/PlayerColor.swift
@@ -23,15 +23,25 @@ struct PlayerColor: Sendable, Identifiable, Hashable {
}
extension PlayerColor {
- static let blue = PlayerColor(id: "blue", name: "Blue", tint: .blue)
static let red = PlayerColor(id: "red", name: "Red", tint: .red)
- static let green = PlayerColor(id: "green", name: "Green", tint: .green)
static let orange = PlayerColor(id: "orange", name: "Orange", tint: .orange)
+ static let yellow = PlayerColor(id: "yellow", name: "Yellow", tint: .yellow)
+ static let green = PlayerColor(id: "green", name: "Green", tint: .green)
+ static let blue = PlayerColor(id: "blue", name: "Blue", tint: .blue)
static let purple = PlayerColor(id: "purple", name: "Purple", tint: .purple)
- static let pink = PlayerColor(id: "pink", name: "Pink", tint: .pink)
+ static let brown = PlayerColor(
+ id: "brown",
+ name: "Brown",
+ tint: Color(red: 0.76, green: 0.60, blue: 0.42)
+ )
/// Order in which colours appear in any picker UI.
- static let palette: [PlayerColor] = [.blue, .red, .green, .orange, .purple, .pink]
+ static let palette: [PlayerColor] = [.red, .orange, .yellow, .green, .blue, .purple, .brown]
+
+ /// Look up a colour by its stored identifier, falling back to blue.
+ static func color(for id: String) -> PlayerColor {
+ palette.first { $0.id == id } ?? .blue
+ }
}
extension EnvironmentValues {
diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift
@@ -3,6 +3,18 @@ import SwiftUI
struct PuzzleView: View {
@Bindable var session: PlayerSession
@Environment(\.playerColor) private var playerColor
+ @AppStorage("playerColorID") private var playerColorID: String = PlayerColor.blue.id
+ @AppStorage("playerName") private var playerName: String = "Player"
+ @State private var isRenaming = false
+ @State private var renameDraft = ""
+
+ private var isShared: Bool { false }
+
+ private func swatchImage(for color: PlayerColor) -> Image {
+ let tint = UIColor(color.tint)
+ let base = UIImage(systemName: "circle.fill") ?? UIImage()
+ return Image(uiImage: base.withTintColor(tint, renderingMode: .alwaysOriginal))
+ }
private struct TitleParts {
let title: String
@@ -97,7 +109,66 @@ struct PuzzleView: View {
} label: {
Label("Clear", systemImage: "eraser")
}
+
+ Menu {
+ Section {
+ Button {} label: {
+ Label {
+ Text(playerName)
+ } icon: {
+ swatchImage(for: PlayerColor.color(for: playerColorID))
+ }
+ }
+ .disabled(true)
+ }
+
+ Section {
+ Menu("Change Colour") {
+ ForEach(PlayerColor.palette) { color in
+ Button {
+ playerColorID = color.id
+ } label: {
+ Label {
+ Text(color.id == playerColorID ? "\(color.name) ✓" : color.name)
+ } icon: {
+ swatchImage(for: color)
+ }
+ }
+ }
+ }
+
+ Button("Change Name") {
+ renameDraft = playerName
+ isRenaming = true
+ }
+ }
+
+ Section {
+ Button("Leave Game", role: .destructive) {
+ // TODO: leave shared game
+ }
+ .disabled(!isShared)
+
+ Button("Share Game") {
+ // TODO: present share sheet
+ }
+ }
+ } label: {
+ Label("Players", systemImage: "person.2")
+ }
+ }
+ }
+ .alert("Change Name", isPresented: $isRenaming) {
+ TextField("Name", text: $renameDraft)
+ Button("Cancel", role: .cancel) {}
+ Button("Save") {
+ let trimmed = renameDraft.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !trimmed.isEmpty {
+ playerName = trimmed
+ }
}
+ } message: {
+ Text("Enter the name other players will see.")
}
}
}