Initial commit: IceGlass NHL game tracker

macOS menu bar app providing NHL game situational awareness with
league-wide scoreboard, dynamic polling, notifications with team
logos, and configurable display options.
This commit is contained in:
2026-04-13 21:44:08 -04:00
commit 8f8f8b2755
158 changed files with 2752 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
//
// GameState.swift
// IceGlass
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
enum GameState: String, Codable {
case future = "FUT" // More than 30 minutes prior to game start
case pre = "PRE" // Pre-game, <30 minutes until puck drops
case live = "LIVE" // Game has started
case crit = "CRIT" // Last 5 minutes of regulation, OT or SO
case over = "OVER" // Soft final
case final_ = "FINAL" // Hard final
case official = "OFF" // Official
var isLive: Bool {
self == .live || self == .crit
}
var isOver: Bool {
self == .over || self == .final_ || self == .official
}
var isFuture: Bool {
self == .future || self == .pre
}
var pollingInterval: PollingInterval {
switch self {
case .future:
return .gameDay
case .pre:
return .preGame
case .live, .crit:
return .liveGame
case .over, .final_:
return .everyMinute
case .official:
return .idle
}
}
}
+82
View File
@@ -0,0 +1,82 @@
//
// NHLTeam.swift
// IceGlass
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
enum NHLTeam: String, CaseIterable {
case ana = "ANA"
case bos = "BOS"
case buf = "BUF"
case car = "CAR"
case cbj = "CBJ"
case cgy = "CGY"
case chi = "CHI"
case col = "COL"
case dal = "DAL"
case det = "DET"
case edm = "EDM"
case fla = "FLA"
case lak = "LAK"
case min = "MIN"
case mtl = "MTL"
case njd = "NJD"
case nsh = "NSH"
case nyi = "NYI"
case nyr = "NYR"
case ott = "OTT"
case phi = "PHI"
case pit = "PIT"
case sea = "SEA"
case sjs = "SJS"
case stl = "STL"
case tbl = "TBL"
case tor = "TOR"
case uta = "UTA"
case van = "VAN"
case vgk = "VGK"
case wpg = "WPG"
case wsh = "WSH"
var abbreviation: String { rawValue }
var fullName: String {
switch self {
case .ana: return "Anaheim Ducks"
case .bos: return "Boston Bruins"
case .buf: return "Buffalo Sabres"
case .car: return "Carolina Hurricanes"
case .cbj: return "Columbus Blue Jackets"
case .cgy: return "Calgary Flames"
case .chi: return "Chicago Blackhawks"
case .col: return "Colorado Avalanche"
case .dal: return "Dallas Stars"
case .det: return "Detroit Red Wings"
case .edm: return "Edmonton Oilers"
case .fla: return "Florida Panthers"
case .lak: return "Los Angeles Kings"
case .min: return "Minnesota Wild"
case .mtl: return "Montreal Canadiens"
case .njd: return "New Jersey Devils"
case .nsh: return "Nashville Predators"
case .nyi: return "New York Islanders"
case .nyr: return "New York Rangers"
case .ott: return "Ottawa Senators"
case .phi: return "Philadelphia Flyers"
case .pit: return "Pittsburgh Penguins"
case .sea: return "Seattle Kraken"
case .sjs: return "San Jose Sharks"
case .stl: return "St. Louis Blues"
case .tbl: return "Tampa Bay Lightning"
case .tor: return "Toronto Maple Leafs"
case .uta: return "Utah Hockey Club"
case .van: return "Vancouver Canucks"
case .vgk: return "Vegas Golden Knights"
case .wpg: return "Winnipeg Jets"
case .wsh: return "Washington Capitals"
}
}
}
+102
View File
@@ -0,0 +1,102 @@
//
// 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 for display (e.g., "7:00 PM")
var startTimeET: String {
date.formatDateET(format: "h:mm a")
}
/// Formatted menu title: "NYR @ WAS 0:2 (FINAL)" or "DAL @ TOR Today @ 7:30 PM"
var menuTitle: String {
let state = parsedGameState
let matchup = "\(awayTeam.abbrev) @ \(homeTeam.abbrev)"
if state.isFuture {
let isToday = gameDate == Date.todayET
let prefix = isToday ? "Today @ " : ""
return "\(matchup) \(prefix)\(startTimeET)"
}
// Has scores
let score = "\(awayTeam.score ?? 0):\(homeTeam.score ?? 0)"
return "\(matchup) \(score) (\(gameState))"
}
/// Whether this game involves a specific team
func involves(team abbrev: String) -> Bool {
awayTeam.abbrev == abbrev || homeTeam.abbrev == abbrev
}
}
}