aaffa3771c
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.
69 lines
2.3 KiB
Swift
69 lines
2.3 KiB
Swift
//
|
|
// NHLDataCache.swift
|
|
// IceGlass
|
|
//
|
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Snapshot of API state persisted between app launches so cold-launch shows
|
|
/// last-known data with an "as-of" timestamp instead of an empty page.
|
|
struct CachedSnapshot: Codable, Sendable {
|
|
let lastUpdated: Date
|
|
let scoreboard: Scoreboard?
|
|
let standings: Standings?
|
|
let bracket: PlayoffBracket?
|
|
}
|
|
|
|
/// Single-file Codable cache in Application Support. Atomic writes; fail-soft
|
|
/// reads — corrupt or version-mismatched payloads return nil and the next
|
|
/// fetch overwrites with fresh data.
|
|
actor NHLDataCache {
|
|
private let logger = IceGlassLogger(
|
|
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass",
|
|
category: "NHLDataCache"
|
|
)
|
|
|
|
private let fileURL: URL
|
|
|
|
init(filename: String = "nhl-snapshot.json") {
|
|
let fm = FileManager.default
|
|
let supportDir = (try? fm.url(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask,
|
|
appropriateFor: nil,
|
|
create: true
|
|
)) ?? fm.temporaryDirectory
|
|
|
|
let bundleId = Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass"
|
|
let appDir = supportDir.appendingPathComponent(bundleId, isDirectory: true)
|
|
try? fm.createDirectory(at: appDir, withIntermediateDirectories: true)
|
|
self.fileURL = appDir.appendingPathComponent(filename)
|
|
}
|
|
|
|
func load() -> CachedSnapshot? {
|
|
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
|
|
do {
|
|
let data = try Data(contentsOf: fileURL)
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
return try decoder.decode(CachedSnapshot.self, from: data)
|
|
} catch {
|
|
logger.warning("Cache load failed (will refetch): \(error.localizedDescription)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func save(_ snapshot: CachedSnapshot) {
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
let data = try encoder.encode(snapshot)
|
|
try data.write(to: fileURL, options: .atomic)
|
|
} catch {
|
|
logger.error("Cache save failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|