Add playoff round view, game numbers, goal scorer notifications, standings

- Fetch NHL standings and surface league/season game counts in the menu bar
- Prefix regular-season rows with the league-wide game number (from gameId)
- New ROUND section shows each active playoff series (matchup, series score,
  next game number + time) derived from /v1/playoff-bracket; rows always open
  the NHL series page so completed series remain clickable
- Goal notifications include scorer sweater, abbreviated name, and strength
  (PPG/SHG/EN), resolved via /v1/gamecenter/{id}/play-by-play
- Drop the per-team filter submenu and NHLTeam enum
- Regenerate AppIcon with the full 10-size macOS set (alpha preserved) so
  notifications render the app icon correctly; rename the iOS marketing PNG
  to icon-ios-1024.png
- gitignore .claude/ local tooling settings
This commit is contained in:
2026-04-18 21:51:27 -04:00
parent 8f8f8b2755
commit 57358797e1
44 changed files with 596 additions and 286 deletions
+74
View File
@@ -0,0 +1,74 @@
//
// BracketModel.swift
// IceGlass
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
struct PlayoffBracket: Codable {
let series: [Series]
struct Series: Codable {
let seriesUrl: String?
let seriesLetter: String
let playoffRound: Int
let seriesTitle: String
let topSeedTeam: Team?
let bottomSeedTeam: Team?
let topSeedWins: Int
let bottomSeedWins: Int
struct Team: Codable {
let abbrev: String
}
/// True once both teams are known (i.e. the series is actually matched up).
var isMatched: Bool { topSeedTeam != nil && bottomSeedTeam != nil }
var isOver: Bool { topSeedWins == 4 || bottomSeedWins == 4 }
/// 1-based game number of the next unplayed game in the series (nil if series is over).
var nextGameNumber: Int? {
isOver ? nil : topSeedWins + bottomSeedWins + 1
}
/// Abbrev of whichever side has reached 4 wins, nil if series ongoing.
var winner: String? {
guard isOver else { return nil }
return topSeedWins == 4 ? topSeedTeam?.abbrev : bottomSeedTeam?.abbrev
}
/// Absolute URL for the NHL.com series page, or nil if the bracket doesn't provide one.
var fullSeriesUrl: String? {
seriesUrl.map { "https://www.nhl.com\($0)" }
}
func involves(away: String, home: String) -> Bool {
guard let top = topSeedTeam, let bottom = bottomSeedTeam else { return false }
let pair = Set([away, home])
return pair == Set([top.abbrev, bottom.abbrev])
}
}
/// Lowest round number that still has at least one matched, live series.
var currentRound: Int? {
series
.filter { $0.isMatched && !$0.isOver }
.map(\.playoffRound)
.min()
}
/// All matched series in the current round.
var currentRoundSeries: [Series] {
guard let round = currentRound else { return [] }
return series
.filter { $0.playoffRound == round && $0.isMatched }
.sorted { $0.seriesLetter < $1.seriesLetter }
}
func series(for game: Scoreboard.Game) -> Series? {
series.first { $0.involves(away: game.awayTeam.abbrev, home: game.homeTeam.abbrev) }
}
}