Files
iceglass/IceGlass/Models/ScoreboardModel.swift
T
rzen 89060d7177 Rebake team logos at 72×72 for crisp notification thumbnails
The notification thumbnail's native display size is ~48 physical pixels
(measured empirically with a size-ladder test attached via the dev menu).
Shipping logos at 512×342 forced macOS to downsample ~10×, which is what
was producing the blurry/aliased team logos in banner thumbnails.

- Pre-render logos at 72×72 (1.5× over native; stays sharp, gives a little
  extra detail for retina displays without triggering aliasing)
- Trim transparent margins before fitting: NHL brand SVGs pad their
  viewBox generously, so the actual logo was only ~60% of the bundled
  image. square_logo.swift now scans the alpha channel, crops to the
  tight bounding box, then fits aspect-preserved into the 72×72 canvas.
- Drop the 32 unused TeamLogo_*.imageset asset-catalog entries (dead code
  since the team-filter feature was removed); notifications load PNGs
  from the filesystem bundle subdir
- Move TeamLogos/ → Resources/TeamLogos/ and update project.yml source
  paths; excludes: on the recursive scan prevents duplicate flat copies
  that were bloating the bundle
- Simplify NotificationManager: drop SVG fallback (macOS doesn't accept
  SVG attachments) and content-hash identifier experiments; back to the
  minimal working config
- Dev menu: add "Thumbnail Size Test" which fires a ladder of 10 test
  notifications (16…128px) for future sizing verification
- Fire a test game-start notification on startup in DEBUG builds so the
  dev loop doesn't require clicking through the menu after each launch
- Scripts/square_logo.swift: alpha-bbox trim + aspect-preserved fit
2026-04-19 20:59:33 -04:00

133 lines
4.0 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 FINAL" (finished/live padded score + time + state tag)
/// "DAL @ TOR 7:30 PM" (future no score gap, no tag)
var menuTitle: String {
let state = parsedGameState
let matchup = "\(awayTeam.abbrev) @ \(homeTeam.abbrev)"
let tag = state.shortTag
let tagSuffix = tag.isEmpty ? "" : " \(tag)"
if state.isFuture {
return "\(matchup) \(startTimeET)\(tagSuffix)"
}
let aScore = String(format: "%2d", awayTeam.score ?? 0)
let hScore = String(format: "%-2d", homeTeam.score ?? 0)
return "\(matchup) \(aScore):\(hScore) \(startTimeET)\(tagSuffix)"
}
/// 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)!)
}
}
}
}