Add iPhone target with shared data layer and persistent cache
Two-target restructure: shared sources (models, services, settings, extensions, team logos) move into Shared/, consumed by both the existing macOS menu bar app and a new iOS app. MainService no longer imports AppKit — platform code attaches via a MainServiceObserver protocol (MacObserverAdapter wires back to MenuManager / StatusItemManager / NotificationManager). iPhone app is a single SwiftUI page mirroring the macOS menu (playoff round + yesterday/today/tomorrow), with a gear-icon settings sheet (display option + IndieAbout for license/changelog). Persistent JSON snapshot in Application Support paints last-known data on cold launch; "Updated …" header escalates secondary → orange (>5min) → red (>30min) so staleness is visually unmistakable. Foreground polling, scenePhase refresh, and pull-to-refresh; no notifications on iOS in v1.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// BracketModel.swift
|
||||
// IceGlass
|
||||
//
|
||||
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PlayoffBracket: Codable {
|
||||
let series: [Series]
|
||||
|
||||
struct Series: Codable {
|
||||
let seriesUrl: String?
|
||||
let seriesLetter: String
|
||||
let playoffRound: Int
|
||||
let seriesTitle: String
|
||||
let topSeedTeam: Team?
|
||||
let bottomSeedTeam: Team?
|
||||
let topSeedWins: Int
|
||||
let bottomSeedWins: Int
|
||||
|
||||
struct Team: Codable {
|
||||
let abbrev: String
|
||||
}
|
||||
|
||||
/// True once both teams are known (i.e. the series is actually matched up).
|
||||
var isMatched: Bool { topSeedTeam != nil && bottomSeedTeam != nil }
|
||||
|
||||
var isOver: Bool { topSeedWins == 4 || bottomSeedWins == 4 }
|
||||
|
||||
/// 1-based game number of the next unplayed game in the series (nil if series is over).
|
||||
var nextGameNumber: Int? {
|
||||
isOver ? nil : topSeedWins + bottomSeedWins + 1
|
||||
}
|
||||
|
||||
/// Abbrev of whichever side has reached 4 wins, nil if series ongoing.
|
||||
var winner: String? {
|
||||
guard isOver else { return nil }
|
||||
return topSeedWins == 4 ? topSeedTeam?.abbrev : bottomSeedTeam?.abbrev
|
||||
}
|
||||
|
||||
/// Absolute URL for the NHL.com series page, or nil if the bracket doesn't provide one.
|
||||
var fullSeriesUrl: String? {
|
||||
seriesUrl.map { "https://www.nhl.com\($0)" }
|
||||
}
|
||||
|
||||
func involves(away: String, home: String) -> Bool {
|
||||
guard let top = topSeedTeam, let bottom = bottomSeedTeam else { return false }
|
||||
let pair = Set([away, home])
|
||||
return pair == Set([top.abbrev, bottom.abbrev])
|
||||
}
|
||||
}
|
||||
|
||||
/// Lowest round number that still has at least one matched, live series.
|
||||
var currentRound: Int? {
|
||||
series
|
||||
.filter { $0.isMatched && !$0.isOver }
|
||||
.map(\.playoffRound)
|
||||
.min()
|
||||
}
|
||||
|
||||
/// All matched series in the current round.
|
||||
var currentRoundSeries: [Series] {
|
||||
guard let round = currentRound else { return [] }
|
||||
return series
|
||||
.filter { $0.playoffRound == round && $0.isMatched }
|
||||
.sorted { $0.seriesLetter < $1.seriesLetter }
|
||||
}
|
||||
|
||||
func series(for game: Scoreboard.Game) -> Series? {
|
||||
series.first { $0.involves(away: game.awayTeam.abbrev, home: game.homeTeam.abbrev) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user