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 "-:-"
This commit is contained in:
2026-05-30 08:21:28 -04:00
parent 8df88e10d6
commit 541aa3d52c
13 changed files with 201 additions and 61 deletions
+2 -1
View File
@@ -106,7 +106,8 @@ struct GameRow: View {
}
private func open() {
guard let url = URL(string: game.gameCenterUrl) else { return }
guard let urlString = game.gameCenterUrl,
let url = URL(string: urlString) else { return }
UIApplication.shared.open(url)
}
}
+12 -8
View File
@@ -21,17 +21,21 @@ struct MainView: View {
)
.padding(.horizontal)
if !viewModel.currentRoundSeriesItems.isEmpty,
let round = viewModel.currentRoundNumber {
PlayoffRoundSection(
round: round,
items: viewModel.currentRoundSeriesItems
)
.padding(.horizontal)
let roundGroups = viewModel.roundSeriesGroups
let latestRound = roundGroups.last?.round
ForEach(roundGroups, id: \.round) { group in
if !group.items.isEmpty {
PlayoffRoundSection(
round: group.round,
items: group.items,
showStatus: group.round == latestRound
)
.padding(.horizontal)
}
}
let gameDays = viewModel.gamesByDate
if gameDays.isEmpty && viewModel.currentRoundSeriesItems.isEmpty {
if gameDays.isEmpty && roundGroups.isEmpty {
emptyState
.padding(.horizontal)
.padding(.top, 40)
+2 -1
View File
@@ -10,6 +10,7 @@ import SwiftUI
struct PlayoffRoundSection: View {
let round: Int
let items: [MainService.RoundSeriesItem]
var showStatus: Bool = true
var body: some View {
VStack(alignment: .leading, spacing: 8) {
@@ -20,7 +21,7 @@ struct PlayoffRoundSection: View {
VStack(spacing: 0) {
ForEach(items, id: \.series.seriesLetter) { item in
SeriesRow(item: item)
SeriesRow(item: item, showStatus: showStatus)
if item.series.seriesLetter != items.last?.series.seriesLetter {
Divider()
}
+8 -1
View File
@@ -9,6 +9,9 @@ import SwiftUI
struct SeriesRow: View {
let item: MainService.RoundSeriesItem
/// Completed earlier rounds hide the status/next-game column; the struck-out
/// loser already conveys the result.
var showStatus: Bool = true
private static let logoSize: CGFloat = 40
@@ -17,7 +20,9 @@ struct SeriesRow: View {
HStack(alignment: .center, spacing: 10) {
matchupBlock
Spacer(minLength: 8)
rightContent
if showStatus {
rightContent
}
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -34,6 +39,7 @@ struct SeriesRow: View {
Text(bottom)
.font(.title3.monospaced())
.fontWeight(.semibold)
.strikethrough(item.series.loser == bottom)
.foregroundStyle(.primary)
.lineLimit(1)
.fixedSize()
@@ -47,6 +53,7 @@ struct SeriesRow: View {
Text(top)
.font(.title3.monospaced())
.fontWeight(.semibold)
.strikethrough(item.series.loser == top)
.foregroundStyle(.primary)
.lineLimit(1)
.fixedSize()