Files
iceglass/IceGlass-iOS/Views/GameRow.swift
T
rzen 541aa3d52c Show full playoff bracket, mark series results, harden API decoding
Playoffs:
- List every round played so far (Round 1 → current) instead of only the
  current round, on both macOS menu and iPhone
- Strike through the eliminated team's tricode in a finished series and drop
  the now-redundant "(Final … wins)" tag on completed earlier rounds
- Refetch the bracket when a finished game implies more completed games than
  the cached bracket records, so the series score and round no longer get
  stuck on stale data after cold launch or the NHL bracket endpoint's lag

API robustness:
- Tolerate optional gameCenterLink/startTimeUTC on TBD playoff matchups so the
  scoreboard decode no longer aborts
- Reject API state regressions via a monotonic FUT→…→OFF progression rank so a
  brief glitch can't downgrade a finished game back to "-:-"
2026-05-30 08:21:28 -04:00

114 lines
3.4 KiB
Swift

//
// GameRow.swift
// IceGlass-iOS
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct GameRow: View {
let game: Scoreboard.Game
private static let logoSize: CGFloat = 40
var body: some View {
Button(action: open) {
HStack(spacing: 10) {
if game.gameType == 2 {
Text("#\(game.seasonGameNumber)")
.font(.caption2.monospacedDigit())
.foregroundStyle(.tertiary)
.frame(width: 44, alignment: .leading)
}
matchupBlock
Spacer(minLength: 8)
rightContent
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
private var matchupBlock: some View {
let state = game.parsedGameState
let showScore = !state.isFuture
return HStack(spacing: 6) {
TeamLogo(abbrev: game.awayTeam.abbrev, size: Self.logoSize)
Text(game.awayTeam.abbrev)
.font(.title3.monospaced())
.fontWeight(.semibold)
.foregroundStyle(.primary)
.lineLimit(1)
.fixedSize()
Text(showScore ? scoreText : "-:-")
.font(.title3.monospaced())
.fontWeight(.bold)
.foregroundStyle(.secondary)
.lineLimit(1)
.fixedSize()
.padding(.horizontal, 2)
Text(game.homeTeam.abbrev)
.font(.title3.monospaced())
.fontWeight(.semibold)
.foregroundStyle(.primary)
.lineLimit(1)
.fixedSize()
TeamLogo(abbrev: game.homeTeam.abbrev, size: Self.logoSize)
}
}
@ViewBuilder
private var rightContent: some View {
let state = game.parsedGameState
VStack(alignment: .trailing, spacing: 2) {
Text(primaryRightLine)
.font(.subheadline.monospacedDigit())
.foregroundStyle(state.isLive ? .red : .secondary)
if let secondary = secondaryRightLine {
Text(secondary)
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
}
private var primaryRightLine: String {
let state = game.parsedGameState
if state.isFuture {
return game.startTimeET.trimmingCharacters(in: .whitespaces)
}
let tag = state.shortTag
return tag.isEmpty
? game.startTimeET.trimmingCharacters(in: .whitespaces)
: tag
}
private var secondaryRightLine: String? {
let state = game.parsedGameState
guard !state.isFuture, !state.shortTag.isEmpty else { return nil }
// For finished games, show kickoff time below FINAL/OFF; for live games, just show tag.
if state.isLive { return nil }
return game.startTimeET.trimmingCharacters(in: .whitespaces)
}
private var scoreText: String {
let a = game.awayTeam.score ?? 0
let h = game.homeTeam.score ?? 0
return "\(a):\(h)"
}
private func open() {
guard let urlString = game.gameCenterUrl,
let url = URL(string: urlString) else { return }
UIApplication.shared.open(url)
}
}