diff --git a/IceGlass/Managers/MenuManager.swift b/IceGlass/Managers/MenuManager.swift index 50c905b..d8209ff 100644 --- a/IceGlass/Managers/MenuManager.swift +++ b/IceGlass/Managers/MenuManager.swift @@ -220,6 +220,9 @@ class MenuManager: @unchecked Sendable { devSubmenu.addItem( NSMenuItem(title: "Test Goal Scored Notification", action: #selector(triggerTestGoalScored), keyEquivalent: "").withTarget(self) ) + devSubmenu.addItem( + NSMenuItem(title: "Test Game Ended Notification", action: #selector(triggerTestGameEnded), keyEquivalent: "").withTarget(self) + ) devSubmenu.addItem( NSMenuItem(title: "Thumbnail Size Test", action: #selector(triggerThumbnailSizeTest), keyEquivalent: "").withTarget(self) ) @@ -308,6 +311,13 @@ class MenuManager: @unchecked Sendable { } } + @objc private func triggerTestGameEnded() { + let notificationManager = NotificationManager.shared + if let game = mainService.gamesByDate.flatMap(\.games).first { + notificationManager.notifyGameEnded(game, bypassDedup: true) + } + } + @objc private func triggerThumbnailSizeTest() { NotificationManager.shared.sendThumbnailSizeTest() } diff --git a/IceGlass/Managers/NotificationManager.swift b/IceGlass/Managers/NotificationManager.swift index b515a28..c73e1b1 100644 --- a/IceGlass/Managers/NotificationManager.swift +++ b/IceGlass/Managers/NotificationManager.swift @@ -25,6 +25,9 @@ class NotificationManager: @unchecked Sendable { /// Track which score changes we've already notified about (gameId-awayScore-homeScore) private var scoreChangesSent = Set() + /// Track which game endings we've already notified about + private var gameEndsSent = Set() + private init() { logger.info("Initializing") requestNotificationPermissions() @@ -143,6 +146,63 @@ class NotificationManager: @unchecked Sendable { } } + // MARK: - Game Ended + + func notifyGameEnded(_ game: Scoreboard.Game, bypassDedup: Bool = false) { + if !bypassDedup { + guard !gameEndsSent.contains(game.id) else { return } + gameEndsSent.insert(game.id) + } + + let awayScore = game.awayTeam.score ?? 0 + let homeScore = game.homeTeam.score ?? 0 + let winner: Scoreboard.Game.Team + let loser: Scoreboard.Game.Team + if awayScore > homeScore { + winner = game.awayTeam + loser = game.homeTeam + } else if homeScore > awayScore { + winner = game.homeTeam + loser = game.awayTeam + } else { + // Tie — shouldn't happen in NHL, but handle gracefully + winner = game.homeTeam + loser = game.awayTeam + } + let winnerScore = max(awayScore, homeScore) + let loserScore = min(awayScore, homeScore) + + let content = UNMutableNotificationContent() + content.title = "\(winner.abbrev) wins" + content.body = "\(winner.abbrev) \(winnerScore) — \(loser.abbrev) \(loserScore)" + content.sound = .default + content.interruptionLevel = .active + content.userInfo = ["url": game.gameCenterUrl] + + // Attach winning team's logo + if let attachment = teamLogoAttachment(for: winner.abbrev) { + content.attachments = [attachment] + } + + let identifier = bypassDedup + ? "game-end-test-\(UUID().uuidString)" + : "game-end-\(game.id)" + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + ) + + UNUserNotificationCenter.current().add(request) { [weak self] error in + if let error = error { + self?.logger.error("Error sending game end notification: \(error.localizedDescription)") + } else { + self?.logger.info("Game end notification sent: \(winner.abbrev) \(winnerScore) — \(loser.abbrev) \(loserScore)") + } + } + } + // MARK: - Team Logo Attachment /// Creates a UNNotificationAttachment from the bundled team logo PNG. @@ -185,6 +245,7 @@ class NotificationManager: @unchecked Sendable { logger.debug("Resetting notification tracking for new day") gameStartsSent.removeAll() scoreChangesSent.removeAll() + gameEndsSent.removeAll() } #if DEBUG diff --git a/IceGlass/Services/MainService.swift b/IceGlass/Services/MainService.swift index 1e073a6..9676166 100644 --- a/IceGlass/Services/MainService.swift +++ b/IceGlass/Services/MainService.swift @@ -219,6 +219,12 @@ class MainService: @unchecked Sendable { notificationManager.notifyGameStarted(game) } + // Game ended: transition to any over-state (OVER/FINAL/OFF) + if let prevState = previousState, !prevState.isOver, currentState.isOver { + logger.info("Game \(game.id) ended: \(game.awayTeam.abbrev) \(game.awayTeam.score ?? 0) — \(game.homeTeam.abbrev) \(game.homeTeam.score ?? 0)") + notificationManager.notifyGameEnded(game) + } + if game.gameType == 3, let prevState = previousState, !prevState.isOver, currentState.isOver { hadPlayoffFinalTransition = true