Files
iceglass/IceGlass/Models/ScoreboardModel.swift
T
rzen 57358797e1 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
2026-04-18 21:51:27 -04:00

131 lines
3.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// ScoreboardModel.swift
// IceGlass
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
struct Scoreboard: Codable {
let focusedDate: String
let focusedDateCount: Int
let gamesByDate: [GameDay]
struct GameDay: Codable {
let date: String // "YYYY-MM-DD"
let games: [Game]
}
struct Game: Codable, Equatable {
static func == (lhs: Game, rhs: Game) -> Bool {
lhs.id == rhs.id
}
let id: Int
let season: Int
let gameType: Int
let gameDate: String
let gameCenterLink: String
let startTimeUTC: String
let gameState: String
let gameScheduleState: String
let awayTeam: Team
let homeTeam: Team
let period: Int?
let periodDescriptor: PeriodDescriptor?
struct LocalizedString: Codable {
let `default`: String
}
struct Team: Codable {
let id: Int
let name: LocalizedString
let commonName: LocalizedString
let abbrev: String
let score: Int?
let record: String?
let logo: String
}
struct PeriodDescriptor: Codable {
let number: Int
let periodType: String
let maxRegulationPeriods: Int
}
// MARK: - Computed Properties
var parsedGameState: GameState {
GameState(rawValue: gameState) ?? .future
}
var date: Date {
ISO8601DateFormatter().date(from: startTimeUTC) ?? .now
}
var gameCenterUrl: String {
"https://www.nhl.com\(gameCenterLink)"
}
var videocastUrl: String {
"https://videocast.nhl.com/game/\(id)/usnded?autoplay=true"
}
/// Time string in ET, right-aligned to 8 chars (e.g. " 9:30 PM", "10:00 PM")
var startTimeET: String {
let raw = date.formatDateET(format: "h:mm a")
return raw.count < 8
? String(repeating: " ", count: 8 - raw.count) + raw
: raw
}
/// Formatted menu title:
/// "NYR @ WAS 0: 2 9:30 PM" (finished/live padded score + time)
/// "DAL @ TOR 7:30 PM" (future no score gap)
var menuTitle: String {
let state = parsedGameState
let matchup = "\(awayTeam.abbrev) @ \(homeTeam.abbrev)"
if state.isFuture {
return "\(matchup) \(startTimeET)"
}
let aScore = String(format: "%2d", awayTeam.score ?? 0)
let hScore = String(format: "%-2d", homeTeam.score ?? 0)
return "\(matchup) \(aScore):\(hScore) \(startTimeET)"
}
/// Sequential game number encoded in the last 4 digits of `id`.
/// Regular season: 1~1312. Playoffs: 111417 (`RSG` round/series/game).
var seasonGameNumber: Int { id % 10_000 }
/// Parsed playoff context; nil for non-playoff games.
var playoffContext: PlayoffContext? {
guard gameType == 3 else { return nil }
let n = id % 1000
return PlayoffContext(
round: n / 100,
seriesInRound: (n / 10) % 10,
gameInSeries: n % 10
)
}
struct PlayoffContext: Equatable {
let round: Int
let seriesInRound: Int
let gameInSeries: Int
/// AO by cumulative position (R1 S1 = A, R1 S8 = H, R2 S1 = I, , R4 S1 = O).
var seriesLetter: String {
let roundStartIndex = [0, 0, 8, 12, 14]
guard round >= 1, round <= 4 else { return "" }
let index = roundStartIndex[round] + (seriesInRound - 1)
guard index >= 0, index < 15 else { return "" }
return String(UnicodeScalar(65 + index)!)
}
}
}
}