crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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:
MCrossmate/CrossmateApp.swift | 2++
MCrossmate/Models/PlayerColor.swift | 18++++++++++++++----
MCrossmate/Views/PuzzleView.swift | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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.") } } }