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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user