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:
2026-04-25 06:34:36 -04:00
parent 18c4ef64d6
commit aaffa3771c
70 changed files with 1011 additions and 65 deletions
+75
View File
@@ -0,0 +1,75 @@
//
// SeriesRow.swift
// IceGlass-iOS
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct SeriesRow: View {
let item: MainService.RoundSeriesItem
var body: some View {
Button(action: open) {
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text(matchupText)
.font(.body)
.fontWeight(.medium)
.foregroundStyle(.primary)
Text(scoreText)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(statusText)
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
if let trailing = trailingText {
Text(trailing)
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
private var matchupText: String {
let top = item.series.topSeedTeam?.abbrev ?? "TBD"
let bottom = item.series.bottomSeedTeam?.abbrev ?? "TBD"
return "\(bottom) @ \(top)"
}
private var scoreText: String {
"\(item.series.bottomSeedWins) \(item.series.topSeedWins)"
}
private var statusText: String {
if let winner = item.series.winner {
return "Final · \(winner) wins"
}
if let n = item.series.nextGameNumber {
return "Game \(n)"
}
return ""
}
private var trailingText: String? {
guard item.series.winner == nil else { return nil }
return item.nextGame?.nextGameLabel
}
private func open() {
guard let urlString = item.series.fullSeriesUrl,
let url = URL(string: urlString) else { return }
UIApplication.shared.open(url)
}
}