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:
@@ -91,9 +91,10 @@ class MenuManager: @unchecked Sendable {
|
||||
let monoFont = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
|
||||
let boldFont = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize, weight: .bold)
|
||||
|
||||
let roundItems = mainService.currentRoundSeriesItems
|
||||
if !roundItems.isEmpty, let round = mainService.bracket?.currentRound {
|
||||
let headerText = "ROUND \(round)"
|
||||
let roundGroups = mainService.roundSeriesGroups
|
||||
let latestRound = roundGroups.last?.round
|
||||
for group in roundGroups where !group.items.isEmpty {
|
||||
let headerText = "ROUND \(group.round)"
|
||||
let headerItem = NSMenuItem(title: headerText, action: nil, keyEquivalent: "")
|
||||
headerItem.attributedTitle = NSAttributedString(
|
||||
string: headerText,
|
||||
@@ -101,8 +102,8 @@ class MenuManager: @unchecked Sendable {
|
||||
)
|
||||
menu.addItem(headerItem)
|
||||
|
||||
for item in roundItems {
|
||||
menu.addItem(createRoundSeriesItem(for: item, font: monoFont))
|
||||
for item in group.items {
|
||||
menu.addItem(createRoundSeriesItem(for: item, showStatus: group.round == latestRound, font: monoFont))
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
@@ -254,7 +255,8 @@ class MenuManager: @unchecked Sendable {
|
||||
|
||||
@objc private func openGame(_ sender: NSMenuItem) {
|
||||
guard let game = sender.representedObject as? Scoreboard.Game,
|
||||
let url = URL(string: game.gameCenterUrl) else { return }
|
||||
let urlString = game.gameCenterUrl,
|
||||
let url = URL(string: urlString) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@@ -352,32 +354,50 @@ class MenuManager: @unchecked Sendable {
|
||||
return [item, altItem]
|
||||
}
|
||||
|
||||
private func createRoundSeriesItem(for roundItem: MainService.RoundSeriesItem, font: NSFont) -> NSMenuItem {
|
||||
private func createRoundSeriesItem(for roundItem: MainService.RoundSeriesItem, showStatus: Bool, font: NSFont) -> NSMenuItem {
|
||||
let series = roundItem.series
|
||||
let top = series.topSeedTeam?.abbrev ?? "TBD"
|
||||
let bottom = series.bottomSeedTeam?.abbrev ?? "TBD"
|
||||
let matchup = "\(bottom) @ \(top)"
|
||||
let score = "\(series.bottomSeedWins)-\(series.topSeedWins)"
|
||||
|
||||
let statusTag: String
|
||||
let trailing: String
|
||||
if let winner = series.winner {
|
||||
statusTag = "(Final \(winner) wins)"
|
||||
trailing = ""
|
||||
} else if let n = series.nextGameNumber {
|
||||
statusTag = "(Game \(n))"
|
||||
trailing = roundItem.nextGame?.nextGameLabel ?? ""
|
||||
// Completed earlier rounds show just the matchup and score — the struck-out
|
||||
// loser already conveys the result, so the "(Final … wins)" tag is dropped.
|
||||
let title: String
|
||||
if showStatus {
|
||||
let statusTag: String
|
||||
let trailing: String
|
||||
if let winner = series.winner {
|
||||
statusTag = "(Final \(winner) wins)"
|
||||
trailing = ""
|
||||
} else if let n = series.nextGameNumber {
|
||||
statusTag = "(Game \(n))"
|
||||
trailing = roundItem.nextGame?.nextGameLabel ?? ""
|
||||
} else {
|
||||
statusTag = ""
|
||||
trailing = ""
|
||||
}
|
||||
let tagColumn = statusTag.padding(toLength: 20, withPad: " ", startingAt: 0)
|
||||
title = " \(matchup) \(score) \(tagColumn)\(trailing)"
|
||||
} else {
|
||||
statusTag = ""
|
||||
trailing = ""
|
||||
title = " \(matchup) \(score)"
|
||||
}
|
||||
let tagColumn = statusTag.padding(toLength: 20, withPad: " ", startingAt: 0)
|
||||
let title = " \(matchup) \(score) \(tagColumn)\(trailing)"
|
||||
|
||||
let item = NSMenuItem(title: title, action: #selector(openSeriesPage(_:)), keyEquivalent: "")
|
||||
item.target = self
|
||||
item.representedObject = series.fullSeriesUrl
|
||||
item.attributedTitle = NSAttributedString(string: title, attributes: [.font: font])
|
||||
|
||||
let attributed = NSMutableAttributedString(string: title, attributes: [.font: font])
|
||||
// Strike through the eliminated team's tricode, searching only within the
|
||||
// matchup region so the same abbrev inside the "(Final … wins)" tag stays plain.
|
||||
if let loser = series.loser {
|
||||
let matchupRange = NSRange(location: 2, length: (matchup as NSString).length)
|
||||
let loserRange = (title as NSString).range(of: loser, options: [], range: matchupRange)
|
||||
if loserRange.location != NSNotFound {
|
||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: loserRange)
|
||||
}
|
||||
}
|
||||
item.attributedTitle = attributed
|
||||
return item
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ class NotificationManager: @unchecked Sendable {
|
||||
content.body = "\(game.awayTeam.abbrev) @ \(game.homeTeam.abbrev)"
|
||||
content.sound = .default
|
||||
content.interruptionLevel = .active
|
||||
content.userInfo = ["url": game.gameCenterUrl]
|
||||
if let url = game.gameCenterUrl {
|
||||
content.userInfo = ["url": url]
|
||||
}
|
||||
|
||||
// Attach away team logo (visiting team at the other team's arena)
|
||||
if let attachment = teamLogoAttachment(for: game.awayTeam.abbrev) {
|
||||
@@ -120,7 +122,9 @@ class NotificationManager: @unchecked Sendable {
|
||||
content.body = "\(game.awayTeam.abbrev) \(awayScore) : \(game.homeTeam.abbrev) \(homeScore)"
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName("Glass"))
|
||||
content.interruptionLevel = .active
|
||||
content.userInfo = ["url": game.gameCenterUrl]
|
||||
if let url = game.gameCenterUrl {
|
||||
content.userInfo = ["url": url]
|
||||
}
|
||||
|
||||
// Attach scoring team's logo
|
||||
if let attachment = teamLogoAttachment(for: scoringTeam.abbrev) {
|
||||
@@ -177,7 +181,9 @@ class NotificationManager: @unchecked Sendable {
|
||||
content.body = "\(winner.abbrev) \(winnerScore) — \(loser.abbrev) \(loserScore)"
|
||||
content.sound = .default
|
||||
content.interruptionLevel = .active
|
||||
content.userInfo = ["url": game.gameCenterUrl]
|
||||
if let url = game.gameCenterUrl {
|
||||
content.userInfo = ["url": url]
|
||||
}
|
||||
|
||||
// Attach winning team's logo
|
||||
if let attachment = teamLogoAttachment(for: winner.abbrev) {
|
||||
|
||||
Reference in New Issue
Block a user