Add iOS/iPad target with platform-adaptive UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-30 21:30:28 -05:00
parent 127125ae1b
commit fda809fd2f
21 changed files with 851 additions and 129 deletions

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,8 @@
{
"images" : [
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

42
mlbIOS/Info.plist Normal file
View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,106 @@
import SwiftUI
private enum mlbIOSSection: String, CaseIterable, Identifiable {
case games
case league
case multiView
case settings
var id: String { rawValue }
var title: String {
switch self {
case .games: "Games"
case .league: "League"
case .multiView: "Multi-View"
case .settings: "Settings"
}
}
var systemImage: String {
switch self {
case .games: "sportscourt.fill"
case .league: "list.bullet.rectangle.portrait.fill"
case .multiView: "rectangle.split.2x2.fill"
case .settings: "gearshape.fill"
}
}
}
struct mlbIOSRootView: View {
@Environment(GamesViewModel.self) private var viewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var selectedSection: mlbIOSSection? = .games
private var usesCompactTabs: Bool {
horizontalSizeClass == .compact
}
private var multiViewTitle: String {
let count = viewModel.activeStreams.count
return count > 0 ? "Multi-View (\(count))" : "Multi-View"
}
var body: some View {
Group {
if usesCompactTabs {
compactTabs
} else {
splitView
}
}
.task {
if viewModel.games.isEmpty, !viewModel.isLoading {
await viewModel.loadGames()
}
}
}
private var compactTabs: some View {
TabView {
Tab("Games", systemImage: "sportscourt.fill") {
DashboardView()
}
Tab("League", systemImage: "list.bullet.rectangle.portrait.fill") {
LeagueCenterView()
}
Tab(multiViewTitle, systemImage: "rectangle.split.2x2.fill") {
MultiStreamView()
}
Tab("Settings", systemImage: "gearshape.fill") {
SettingsView()
}
}
}
private var splitView: some View {
NavigationSplitView {
List(mlbIOSSection.allCases, selection: $selectedSection) { section in
Label(section.title, systemImage: section.systemImage)
.tag(section)
}
.navigationTitle("MLB")
} detail: {
NavigationStack {
detailView(for: selectedSection ?? .games)
.navigationTitle(selectedSection?.title ?? mlbIOSSection.games.title)
.navigationBarTitleDisplayMode(.inline)
}
}
.navigationSplitViewStyle(.balanced)
}
@ViewBuilder
private func detailView(for section: mlbIOSSection) -> some View {
switch section {
case .games:
DashboardView()
case .league:
LeagueCenterView()
case .multiView:
MultiStreamView()
case .settings:
SettingsView()
}
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

26
mlbIOS/mlbIOSApp.swift Normal file
View File

@@ -0,0 +1,26 @@
import AVFoundation
import SwiftUI
@main
struct mlbIOSApp: App {
@State private var viewModel = GamesViewModel()
init() {
configureAudioSession()
}
var body: some Scene {
WindowGroup {
mlbIOSRootView()
.environment(viewModel)
}
}
private func configureAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(.ambient)
} catch {
print("Failed to set audio session: \(error)")
}
}
}

View File

@@ -7,32 +7,62 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
02BA6240A20E957B5337A545 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABC999C711B074CF0B536DD /* Game.swift */; };
051C3D14A06061D44E325FCC /* LinescoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 327A33F2676712C2B5B4AF2F /* LinescoreView.swift */; }; 051C3D14A06061D44E325FCC /* LinescoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 327A33F2676712C2B5B4AF2F /* LinescoreView.swift */; };
0B1C43E83CA856543F58E16A /* MultiStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D10F2D91602CE1FB6D7342E /* MultiStreamView.swift */; }; 0B1C43E83CA856543F58E16A /* MultiStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D10F2D91602CE1FB6D7342E /* MultiStreamView.swift */; };
11640185C392B0406BBE619D /* MLBServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70E610D24BA83FB2F7490B00 /* MLBServerAPI.swift */; };
1555D223058B858E4C6F419D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB99FBEDA096B95C3D2E5B4 /* ContentView.swift */; };
1AA11AA11AA11AA11AA11AA1 /* GameCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB1 /* GameCenterView.swift */; }; 1AA11AA11AA11AA11AA11AA1 /* GameCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB1 /* GameCenterView.swift */; };
1AA11AA11AA11AA11AA11AA2 /* LeagueCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB2 /* LeagueCenterView.swift */; }; 1AA11AA11AA11AA11AA11AA2 /* LeagueCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB2 /* LeagueCenterView.swift */; };
1AA11AA11AA11AA11AA11AA3 /* GameCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB3 /* GameCenterViewModel.swift */; }; 1AA11AA11AA11AA11AA11AA3 /* GameCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB3 /* GameCenterViewModel.swift */; };
1AA11AA11AA11AA11AA11AA4 /* LeagueCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB4 /* LeagueCenterViewModel.swift */; }; 1AA11AA11AA11AA11AA11AA4 /* LeagueCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB4 /* LeagueCenterViewModel.swift */; };
1E31C6F500CEEA5284081D22 /* MLBServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70E610D24BA83FB2F7490B00 /* MLBServerAPI.swift */; }; 1E31C6F500CEEA5284081D22 /* MLBServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70E610D24BA83FB2F7490B00 /* MLBServerAPI.swift */; };
1E5405E1C27A16971961091C /* ScoreOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */; };
22B1A4EB0E8E279B054FEC3E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E04B78C78BA9DA73B62AA3EE /* Foundation.framework */; };
24C6D5F4A3B291807F6E5D4C /* ScoresTickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C6D5F4A3B291807F6E5D4B /* ScoresTickerView.swift */; }; 24C6D5F4A3B291807F6E5D4C /* ScoresTickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C6D5F4A3B291807F6E5D4B /* ScoresTickerView.swift */; };
29627CA5EE85BA103B38D1F5 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABC999C711B074CF0B536DD /* Game.swift */; }; 29627CA5EE85BA103B38D1F5 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABC999C711B074CF0B536DD /* Game.swift */; };
31353216A6288D986544F61E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE9E708FA75ABB9F49D2392 /* SettingsView.swift */; };
342EED600E1A5B4A3D083E50 /* TeamAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E77DD8183D54EFC97CFB5 /* TeamAssets.swift */; }; 342EED600E1A5B4A3D083E50 /* TeamAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E77DD8183D54EFC97CFB5 /* TeamAssets.swift */; };
37BFF8231552B49E4775AA59 /* GamesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD2DEA6BBA55E86B2555099 /* GamesViewModel.swift */; };
3FB9BCB07B6DBFE6D1D2C523 /* GameCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB1 /* GameCenterView.swift */; };
4318098424F9B3FF3B256EED /* LeagueCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB2 /* LeagueCenterView.swift */; };
43AC37B4F957CDC3986F044A /* mlbIOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0209C78036DA08FA794D8D9 /* mlbIOSApp.swift */; };
43B16E04582A874E87514C30 /* mlbIOSRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F874FDD20F82AFEB475CD73 /* mlbIOSRootView.swift */; };
4F67D44E9A36BED9CD8724CF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB99FBEDA096B95C3D2E5B4 /* ContentView.swift */; }; 4F67D44E9A36BED9CD8724CF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB99FBEDA096B95C3D2E5B4 /* ContentView.swift */; };
52498A59FF923A1494A0139B /* GameListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5D51D6E4990515411D3BCD /* GameListView.swift */; }; 52498A59FF923A1494A0139B /* GameListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5D51D6E4990515411D3BCD /* GameListView.swift */; };
5A76A7BB97FFA089156A0ABA /* PlatformUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBA78A938A07E07B064CEA54 /* PlatformUI.swift */; };
5DFD3805A76DDE49B0DC1E2D /* SingleStreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4AF68CDF4FC8D07194B4D0 /* SingleStreamPlayerView.swift */; }; 5DFD3805A76DDE49B0DC1E2D /* SingleStreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4AF68CDF4FC8D07194B4D0 /* SingleStreamPlayerView.swift */; };
63AF55498896C9BBCD1D4337 /* GameCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21E3115B8A9B4C798ECE828 /* GameCardView.swift */; }; 63AF55498896C9BBCD1D4337 /* GameCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21E3115B8A9B4C798ECE828 /* GameCardView.swift */; };
7D2D21DAE73958578FE2E730 /* LeagueCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB4 /* LeagueCenterViewModel.swift */; };
80A690C52C9C8140E7C519E8 /* TeamLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E99F69727800D0CB795503 /* TeamLogoView.swift */; }; 80A690C52C9C8140E7C519E8 /* TeamLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E99F69727800D0CB795503 /* TeamLogoView.swift */; };
820FB03D0E97C521BA239071 /* MLBNetworkSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152ABCC9AACFD91BAC8C95B /* MLBNetworkSheet.swift */; }; 820FB03D0E97C521BA239071 /* MLBNetworkSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152ABCC9AACFD91BAC8C95B /* MLBNetworkSheet.swift */; };
82DEB54FF3BFD51A18ED98A1 /* MLBStatsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6067FE4F340FC0DCCFA62C67 /* MLBStatsAPI.swift */; };
8D5FE07166FAD17836A0AFF4 /* PitcherHeadshotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F6A7B8C9D0E1F2A3B4C5 /* PitcherHeadshotView.swift */; };
9D29D3508CD05CB3F4975910 /* GamesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD2DEA6BBA55E86B2555099 /* GamesViewModel.swift */; }; 9D29D3508CD05CB3F4975910 /* GamesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD2DEA6BBA55E86B2555099 /* GamesViewModel.swift */; };
A1B2C3D4E5F60718091A2B3C /* PitcherHeadshotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F6A7B8C9D0E1F2A3B4C5 /* PitcherHeadshotView.swift */; }; A1B2C3D4E5F60718091A2B3C /* PitcherHeadshotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F6A7B8C9D0E1F2A3B4C5 /* PitcherHeadshotView.swift */; };
A506FDC86C3A78F4A2C04C10 /* GameListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5D51D6E4990515411D3BCD /* GameListView.swift */; };
AFA76F6EBE7977914A849447 /* FeaturedGameCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102E5E8674E188A04637D3EB /* FeaturedGameCard.swift */; };
B215941F5A269C59C0939958 /* TeamLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E99F69727800D0CB795503 /* TeamLogoView.swift */; };
B2C3D4E5F6A70819A1B2C3D4 /* ScoreOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */; }; B2C3D4E5F6A70819A1B2C3D4 /* ScoreOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */; };
B921B07F43A13CBBDE9C499D /* GameCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21E3115B8A9B4C798ECE828 /* GameCardView.swift */; };
BA1B3240360F79FE2DF42543 /* LinescoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 327A33F2676712C2B5B4AF2F /* LinescoreView.swift */; };
BB48CC4C732672536CD5EF96 /* TeamAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E77DD8183D54EFC97CFB5 /* TeamAssets.swift */; };
C1DA213471DC246B8DF3F840 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC5BCB2E1DA0EC63E422B113 /* Assets.xcassets */; }; C1DA213471DC246B8DF3F840 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC5BCB2E1DA0EC63E422B113 /* Assets.xcassets */; };
C67442678865108796537BBC /* GameCenterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB22BB22BB22BB22BB22BB3 /* GameCenterViewModel.swift */; };
CA20CF293D75E6058E0CB78C /* SingleStreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4AF68CDF4FC8D07194B4D0 /* SingleStreamPlayerView.swift */; };
D199D622D64E3489E049B04A /* LiveIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81584770090BAFBEF3ABE6FD /* LiveIndicator.swift */; };
D48041F6CFBCEB2F3492747F /* MultiStreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D10F2D91602CE1FB6D7342E /* MultiStreamView.swift */; };
DA156E98ACD90B582F3BBAB2 /* PlatformUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBA78A938A07E07B064CEA54 /* PlatformUI.swift */; };
DB6E6A4890D0D22526DA5D96 /* ScoresTickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C6D5F4A3B291807F6E5D4B /* ScoresTickerView.swift */; };
DC37555DE8C367E929974484 /* MLBStatsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6067FE4F340FC0DCCFA62C67 /* MLBStatsAPI.swift */; }; DC37555DE8C367E929974484 /* MLBStatsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6067FE4F340FC0DCCFA62C67 /* MLBStatsAPI.swift */; };
E7FB366456557069990F550C /* LiveIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81584770090BAFBEF3ABE6FD /* LiveIndicator.swift */; }; E7FB366456557069990F550C /* LiveIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81584770090BAFBEF3ABE6FD /* LiveIndicator.swift */; };
F36A71E407BC707B8E03457D /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA4C33F029665B80F1A6B6D /* DashboardView.swift */; }; F36A71E407BC707B8E03457D /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA4C33F029665B80F1A6B6D /* DashboardView.swift */; };
F60F24246B08223DC2BA5825 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA4C33F029665B80F1A6B6D /* DashboardView.swift */; };
F761076F826FEB86E972D516 /* MLBNetworkSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152ABCC9AACFD91BAC8C95B /* MLBNetworkSheet.swift */; };
FAFA9B29273C4D8A54CD5916 /* FeaturedGameCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102E5E8674E188A04637D3EB /* FeaturedGameCard.swift */; }; FAFA9B29273C4D8A54CD5916 /* FeaturedGameCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102E5E8674E188A04637D3EB /* FeaturedGameCard.swift */; };
FD31074216AC0115B3F1C057 /* mlbTVOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45142562E644AF04F7719083 /* mlbTVOSApp.swift */; }; FD31074216AC0115B3F1C057 /* mlbTVOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45142562E644AF04F7719083 /* mlbTVOSApp.swift */; };
FD9322BED3A2B827CB021163 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE9E708FA75ABB9F49D2392 /* SettingsView.swift */; }; FD9322BED3A2B827CB021163 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE9E708FA75ABB9F49D2392 /* SettingsView.swift */; };
FE80BDA15CC8CE2F18DB4883 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56358D0D5DC5CC531034287F /* Assets.xcassets */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -46,10 +76,13 @@
2BB22BB22BB22BB22BB22BB3 /* GameCenterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCenterViewModel.swift; sourceTree = "<group>"; }; 2BB22BB22BB22BB22BB22BB3 /* GameCenterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCenterViewModel.swift; sourceTree = "<group>"; };
2BB22BB22BB22BB22BB22BB4 /* LeagueCenterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeagueCenterViewModel.swift; sourceTree = "<group>"; }; 2BB22BB22BB22BB22BB22BB4 /* LeagueCenterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeagueCenterViewModel.swift; sourceTree = "<group>"; };
327A33F2676712C2B5B4AF2F /* LinescoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinescoreView.swift; sourceTree = "<group>"; }; 327A33F2676712C2B5B4AF2F /* LinescoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinescoreView.swift; sourceTree = "<group>"; };
3F874FDD20F82AFEB475CD73 /* mlbIOSRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = mlbIOSRootView.swift; sourceTree = "<group>"; };
3FB99FBEDA096B95C3D2E5B4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 3FB99FBEDA096B95C3D2E5B4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
45142562E644AF04F7719083 /* mlbTVOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mlbTVOSApp.swift; sourceTree = "<group>"; }; 45142562E644AF04F7719083 /* mlbTVOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mlbTVOSApp.swift; sourceTree = "<group>"; };
4DE9E708FA75ABB9F49D2392 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 4DE9E708FA75ABB9F49D2392 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
5152ABCC9AACFD91BAC8C95B /* MLBNetworkSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLBNetworkSheet.swift; sourceTree = "<group>"; }; 5152ABCC9AACFD91BAC8C95B /* MLBNetworkSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLBNetworkSheet.swift; sourceTree = "<group>"; };
55A3DC0B5087BAD6BC59B35A /* mlbIOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mlbIOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
56358D0D5DC5CC531034287F /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
5EA4C33F029665B80F1A6B6D /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; }; 5EA4C33F029665B80F1A6B6D /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
6067FE4F340FC0DCCFA62C67 /* MLBStatsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLBStatsAPI.swift; sourceTree = "<group>"; }; 6067FE4F340FC0DCCFA62C67 /* MLBStatsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLBStatsAPI.swift; sourceTree = "<group>"; };
645E77DD8183D54EFC97CFB5 /* TeamAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamAssets.swift; sourceTree = "<group>"; }; 645E77DD8183D54EFC97CFB5 /* TeamAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamAssets.swift; sourceTree = "<group>"; };
@@ -57,17 +90,54 @@
766441BC900073529EE93D69 /* mlbTVOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mlbTVOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 766441BC900073529EE93D69 /* mlbTVOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mlbTVOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
81584770090BAFBEF3ABE6FD /* LiveIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveIndicator.swift; sourceTree = "<group>"; }; 81584770090BAFBEF3ABE6FD /* LiveIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveIndicator.swift; sourceTree = "<group>"; };
8D10F2D91602CE1FB6D7342E /* MultiStreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiStreamView.swift; sourceTree = "<group>"; }; 8D10F2D91602CE1FB6D7342E /* MultiStreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiStreamView.swift; sourceTree = "<group>"; };
95C4DEDEAA64C74AF3DB24F2 /* mlbIOS.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = mlbIOS.entitlements; sourceTree = "<group>"; };
9BD2DEA6BBA55E86B2555099 /* GamesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamesViewModel.swift; sourceTree = "<group>"; }; 9BD2DEA6BBA55E86B2555099 /* GamesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamesViewModel.swift; sourceTree = "<group>"; };
A6FE9AFE34BB8CE4F33B62D0 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AC5BCB2E1DA0EC63E422B113 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; AC5BCB2E1DA0EC63E422B113 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
BBA78A938A07E07B064CEA54 /* PlatformUI.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PlatformUI.swift; sourceTree = "<group>"; };
C1F593C51BC11444A3D514D3 /* mlbTVOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = mlbTVOS.entitlements; sourceTree = "<group>"; }; C1F593C51BC11444A3D514D3 /* mlbTVOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = mlbTVOS.entitlements; sourceTree = "<group>"; };
C2E99F69727800D0CB795503 /* TeamLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamLogoView.swift; sourceTree = "<group>"; }; C2E99F69727800D0CB795503 /* TeamLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamLogoView.swift; sourceTree = "<group>"; };
D4E5F6A7B8C9D0E1F2A3B4C5 /* PitcherHeadshotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PitcherHeadshotView.swift; sourceTree = "<group>"; }; D4E5F6A7B8C9D0E1F2A3B4C5 /* PitcherHeadshotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PitcherHeadshotView.swift; sourceTree = "<group>"; };
E04B78C78BA9DA73B62AA3EE /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
E21E3115B8A9B4C798ECE828 /* GameCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCardView.swift; sourceTree = "<group>"; }; E21E3115B8A9B4C798ECE828 /* GameCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCardView.swift; sourceTree = "<group>"; };
E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreOverlayView.swift; sourceTree = "<group>"; }; E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreOverlayView.swift; sourceTree = "<group>"; };
F0209C78036DA08FA794D8D9 /* mlbIOSApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = mlbIOSApp.swift; sourceTree = "<group>"; };
FA4AF68CDF4FC8D07194B4D0 /* SingleStreamPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleStreamPlayerView.swift; sourceTree = "<group>"; }; FA4AF68CDF4FC8D07194B4D0 /* SingleStreamPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleStreamPlayerView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
EF6C5375C48E5C8359230651 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
22B1A4EB0E8E279B054FEC3E /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
0B56828FEC5E5902FE078143 /* iOS */ = {
isa = PBXGroup;
children = (
E04B78C78BA9DA73B62AA3EE /* Foundation.framework */,
);
name = iOS;
sourceTree = "<group>";
};
1646E84AFB7C540A2D228B64 /* mlbIOS */ = {
isa = PBXGroup;
children = (
96E8DA91A0CDD10D8953C8BA /* Views */,
A6FE9AFE34BB8CE4F33B62D0 /* Info.plist */,
95C4DEDEAA64C74AF3DB24F2 /* mlbIOS.entitlements */,
56358D0D5DC5CC531034287F /* Assets.xcassets */,
F0209C78036DA08FA794D8D9 /* mlbIOSApp.swift */,
);
name = mlbIOS;
path = mlbIOS;
sourceTree = "<group>";
};
2C171EA64FFA962254F20583 /* ViewModels */ = { 2C171EA64FFA962254F20583 /* ViewModels */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -93,6 +163,14 @@
path = mlbTVOS; path = mlbTVOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5AE491A6B2C1767EFEF5CE88 /* Frameworks */ = {
isa = PBXGroup;
children = (
0B56828FEC5E5902FE078143 /* iOS */,
);
name = Frameworks;
sourceTree = "<group>";
};
62F95C3E8B0F6AFE3BEDE9C3 /* Components */ = { 62F95C3E8B0F6AFE3BEDE9C3 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -102,6 +180,7 @@
24C6D5F4A3B291807F6E5D4B /* ScoresTickerView.swift */, 24C6D5F4A3B291807F6E5D4B /* ScoresTickerView.swift */,
E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */, E5F6A7B8C9D0E1F2A3B4C5D6 /* ScoreOverlayView.swift */,
C2E99F69727800D0CB795503 /* TeamLogoView.swift */, C2E99F69727800D0CB795503 /* TeamLogoView.swift */,
BBA78A938A07E07B064CEA54 /* PlatformUI.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -111,6 +190,8 @@
children = ( children = (
4FAE5C2D72CD78BF7C15C4FC /* mlbTVOS */, 4FAE5C2D72CD78BF7C15C4FC /* mlbTVOS */,
88274023705E1F09B25F86FF /* Products */, 88274023705E1F09B25F86FF /* Products */,
1646E84AFB7C540A2D228B64 /* mlbIOS */,
5AE491A6B2C1767EFEF5CE88 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -118,10 +199,20 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
766441BC900073529EE93D69 /* mlbTVOS.app */, 766441BC900073529EE93D69 /* mlbTVOS.app */,
55A3DC0B5087BAD6BC59B35A /* mlbIOS.app */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
96E8DA91A0CDD10D8953C8BA /* Views */ = {
isa = PBXGroup;
children = (
3F874FDD20F82AFEB475CD73 /* mlbIOSRootView.swift */,
);
name = Views;
path = Views;
sourceTree = "<group>";
};
C6DC4CC5AAB9AF84D3CB9F3B /* Services */ = { C6DC4CC5AAB9AF84D3CB9F3B /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -174,12 +265,27 @@
dependencies = ( dependencies = (
); );
name = mlbTVOS; name = mlbTVOS;
packageProductDependencies = (
);
productName = mlbTVOS; productName = mlbTVOS;
productReference = 766441BC900073529EE93D69 /* mlbTVOS.app */; productReference = 766441BC900073529EE93D69 /* mlbTVOS.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
9C5FE08A60985D23E089CEE5 /* mlbIOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 67C1F966BDD84456877DC373 /* Build configuration list for PBXNativeTarget "mlbIOS" */;
buildPhases = (
9386CC819A80738E79744F6A /* Sources */,
EF6C5375C48E5C8359230651 /* Frameworks */,
3CEE2E3AB4AAF6D8CC0F12D7 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = mlbIOS;
productName = mlbIOS;
productReference = 55A3DC0B5087BAD6BC59B35A /* mlbIOS.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -199,15 +305,25 @@
); );
mainGroup = 86C1117ADD32A1A57AA32A08; mainGroup = 86C1117ADD32A1A57AA32A08;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
productRefGroup = 88274023705E1F09B25F86FF /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
6ACA532AEDED1D5945D6153A /* mlbTVOS */, 6ACA532AEDED1D5945D6153A /* mlbTVOS */,
9C5FE08A60985D23E089CEE5 /* mlbIOS */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
3CEE2E3AB4AAF6D8CC0F12D7 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FE80BDA15CC8CE2F18DB4883 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
5C7F99E1451B54AD3CC382CB /* Resources */ = { 5C7F99E1451B54AD3CC382CB /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -219,6 +335,40 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
9386CC819A80738E79744F6A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
02BA6240A20E957B5337A545 /* Game.swift in Sources */,
BB48CC4C732672536CD5EF96 /* TeamAssets.swift in Sources */,
11640185C392B0406BBE619D /* MLBServerAPI.swift in Sources */,
82DEB54FF3BFD51A18ED98A1 /* MLBStatsAPI.swift in Sources */,
C67442678865108796537BBC /* GameCenterViewModel.swift in Sources */,
37BFF8231552B49E4775AA59 /* GamesViewModel.swift in Sources */,
7D2D21DAE73958578FE2E730 /* LeagueCenterViewModel.swift in Sources */,
BA1B3240360F79FE2DF42543 /* LinescoreView.swift in Sources */,
D199D622D64E3489E049B04A /* LiveIndicator.swift in Sources */,
8D5FE07166FAD17836A0AFF4 /* PitcherHeadshotView.swift in Sources */,
DA156E98ACD90B582F3BBAB2 /* PlatformUI.swift in Sources */,
1E5405E1C27A16971961091C /* ScoreOverlayView.swift in Sources */,
DB6E6A4890D0D22526DA5D96 /* ScoresTickerView.swift in Sources */,
B215941F5A269C59C0939958 /* TeamLogoView.swift in Sources */,
1555D223058B858E4C6F419D /* ContentView.swift in Sources */,
F60F24246B08223DC2BA5825 /* DashboardView.swift in Sources */,
AFA76F6EBE7977914A849447 /* FeaturedGameCard.swift in Sources */,
B921B07F43A13CBBDE9C499D /* GameCardView.swift in Sources */,
3FB9BCB07B6DBFE6D1D2C523 /* GameCenterView.swift in Sources */,
A506FDC86C3A78F4A2C04C10 /* GameListView.swift in Sources */,
4318098424F9B3FF3B256EED /* LeagueCenterView.swift in Sources */,
F761076F826FEB86E972D516 /* MLBNetworkSheet.swift in Sources */,
D48041F6CFBCEB2F3492747F /* MultiStreamView.swift in Sources */,
31353216A6288D986544F61E /* SettingsView.swift in Sources */,
CA20CF293D75E6058E0CB78C /* SingleStreamPlayerView.swift in Sources */,
43AC37B4F957CDC3986F044A /* mlbIOSApp.swift in Sources */,
43B16E04582A874E87514C30 /* mlbIOSRootView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C959C957D7367748B74B83ED /* Sources */ = { C959C957D7367748B74B83ED /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -248,6 +398,7 @@
342EED600E1A5B4A3D083E50 /* TeamAssets.swift in Sources */, 342EED600E1A5B4A3D083E50 /* TeamAssets.swift in Sources */,
80A690C52C9C8140E7C519E8 /* TeamLogoView.swift in Sources */, 80A690C52C9C8140E7C519E8 /* TeamLogoView.swift in Sources */,
FD31074216AC0115B3F1C057 /* mlbTVOSApp.swift in Sources */, FD31074216AC0115B3F1C057 /* mlbTVOSApp.swift in Sources */,
5A76A7BB97FFA089156A0ABA /* PlatformUI.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -318,6 +469,53 @@
}; };
name = Debug; name = Debug;
}; };
0D8A815004F8CC601240D2DB /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = mlbIOS/mlbIOS.entitlements;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = mlbIOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.treyt.mlbIOS;
PRODUCT_NAME = mlbIOS;
SDKROOT = iphoneos;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
1FC82661BC31026C62934BD7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = mlbIOS/mlbIOS.entitlements;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = mlbIOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.treyt.mlbIOS;
PRODUCT_NAME = mlbIOS;
SDKROOT = iphoneos;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
1FE0846F10814B35388166A8 /* Debug */ = { 1FE0846F10814B35388166A8 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -425,6 +623,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug; defaultConfigurationName = Debug;
}; };
67C1F966BDD84456877DC373 /* Build configuration list for PBXNativeTarget "mlbIOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1FC82661BC31026C62934BD7 /* Release */,
0D8A815004F8CC601240D2DB /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B8EE3C08200DF28CFF279E15 /* Build configuration list for PBXProject "mlbTVOS" */ = { B8EE3C08200DF28CFF279E15 /* Build configuration list for PBXProject "mlbTVOS" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

View File

@@ -1,9 +1,11 @@
import Foundation import Foundation
actor MLBServerAPI { actor MLBServerAPI {
static let defaultBaseURL = "https://ballgame.treytartt.com"
let baseURL: String let baseURL: String
init(baseURL: String = "http://10.3.3.11:5714") { init(baseURL: String = defaultBaseURL) {
self.baseURL = baseURL self.baseURL = baseURL
} }

View File

@@ -25,7 +25,8 @@ private func gamesViewModelDebugURLDescription(_ url: URL) -> String {
} }
private struct RemoteVideoFeedEntry: Decodable { private struct RemoteVideoFeedEntry: Decodable {
let videoFile: String let videoFile: String?
let hlsUrl: String?
let genderValue: String? let genderValue: String?
} }
@@ -46,7 +47,7 @@ final class GamesViewModel {
var multiViewLayoutMode: MultiViewLayoutMode = .balanced var multiViewLayoutMode: MultiViewLayoutMode = .balanced
var audioFocusStreamID: String? var audioFocusStreamID: String?
var serverBaseURL: String = "http://10.3.3.11:5714" var serverBaseURL: String = MLBServerAPI.defaultBaseURL
var defaultResolution: String = "best" var defaultResolution: String = "best"
@ObservationIgnored @ObservationIgnored
@@ -172,7 +173,8 @@ final class GamesViewModel {
overrideHeaders: activeStreams[streamIdx].overrideHeaders, overrideHeaders: activeStreams[streamIdx].overrideHeaders,
player: activeStreams[streamIdx].player, player: activeStreams[streamIdx].player,
isPlaying: activeStreams[streamIdx].isPlaying, isPlaying: activeStreams[streamIdx].isPlaying,
isMuted: activeStreams[streamIdx].isMuted isMuted: activeStreams[streamIdx].isMuted,
forceMuteAudio: activeStreams[streamIdx].forceMuteAudio
) )
} }
} }
@@ -358,8 +360,6 @@ final class GamesViewModel {
func addStream(broadcast: Broadcast, game: Game) { func addStream(broadcast: Broadcast, game: Game) {
guard activeStreams.count < 4 else { return } guard activeStreams.count < 4 else { return }
guard !activeStreams.contains(where: { $0.id == broadcast.id }) else { return } guard !activeStreams.contains(where: { $0.id == broadcast.id }) else { return }
let shouldCaptureAudio = activeStreams.isEmpty && audioFocusStreamID == nil
let stream = ActiveStream( let stream = ActiveStream(
id: broadcast.id, id: broadcast.id,
game: game, game: game,
@@ -368,7 +368,7 @@ final class GamesViewModel {
streamURLString: broadcast.streamURL streamURLString: broadcast.streamURL
) )
activeStreams.append(stream) activeStreams.append(stream)
if shouldCaptureAudio { if shouldCaptureAudio(for: stream) {
audioFocusStreamID = stream.id audioFocusStreamID = stream.id
} }
syncAudioFocus() syncAudioFocus()
@@ -376,8 +376,6 @@ final class GamesViewModel {
func addStreamByTeam(teamCode: String, game: Game) { func addStreamByTeam(teamCode: String, game: Game) {
guard activeStreams.count < 4 else { return } guard activeStreams.count < 4 else { return }
let shouldCaptureAudio = activeStreams.isEmpty && audioFocusStreamID == nil
let config = StreamConfig(team: teamCode, resolution: defaultResolution, date: selectedDate) let config = StreamConfig(team: teamCode, resolution: defaultResolution, date: selectedDate)
let stream = ActiveStream( let stream = ActiveStream(
id: "\(teamCode)-\(game.id)", id: "\(teamCode)-\(game.id)",
@@ -386,7 +384,7 @@ final class GamesViewModel {
config: config config: config
) )
activeStreams.append(stream) activeStreams.append(stream)
if shouldCaptureAudio { if shouldCaptureAudio(for: stream) {
audioFocusStreamID = stream.id audioFocusStreamID = stream.id
} }
syncAudioFocus() syncAudioFocus()
@@ -397,21 +395,22 @@ final class GamesViewModel {
label: String, label: String,
game: Game, game: Game,
url: URL, url: URL,
headers: [String: String] = [:] headers: [String: String] = [:],
forceMuteAudio: Bool = false
) { ) {
guard activeStreams.count < 4 else { return } guard activeStreams.count < 4 else { return }
guard !activeStreams.contains(where: { $0.id == id }) else { return } guard !activeStreams.contains(where: { $0.id == id }) else { return }
let shouldCaptureAudio = activeStreams.isEmpty && audioFocusStreamID == nil
let stream = ActiveStream( let stream = ActiveStream(
id: id, id: id,
game: game, game: game,
label: label, label: label,
overrideURL: url, overrideURL: url,
overrideHeaders: headers.isEmpty ? nil : headers overrideHeaders: headers.isEmpty ? nil : headers,
forceMuteAudio: forceMuteAudio
) )
activeStreams.append(stream) activeStreams.append(stream)
if shouldCaptureAudio { if shouldCaptureAudio(for: stream) {
audioFocusStreamID = stream.id audioFocusStreamID = stream.id
} }
syncAudioFocus() syncAudioFocus()
@@ -422,7 +421,8 @@ final class GamesViewModel {
label: String, label: String,
game: Game, game: Game,
feedURL: URL, feedURL: URL,
headers: [String: String] = [:] headers: [String: String] = [:],
forceMuteAudio: Bool = false
) async -> Bool { ) async -> Bool {
guard let resolvedURL = await resolveAuthenticatedVideoFeedURL(feedURL: feedURL, headers: headers) else { guard let resolvedURL = await resolveAuthenticatedVideoFeedURL(feedURL: feedURL, headers: headers) else {
return false return false
@@ -433,7 +433,8 @@ final class GamesViewModel {
label: label, label: label,
game: game, game: game,
url: resolvedURL, url: resolvedURL,
headers: headers headers: headers,
forceMuteAudio: forceMuteAudio
) )
return true return true
} }
@@ -464,7 +465,7 @@ final class GamesViewModel {
audioFocusStreamID = nil audioFocusStreamID = nil
} else if removedWasAudioFocus { } else if removedWasAudioFocus {
let replacementIndex = min(index, activeStreams.count - 1) let replacementIndex = min(index, activeStreams.count - 1)
audioFocusStreamID = activeStreams[replacementIndex].id audioFocusStreamID = preferredAudioFocusStreamID(preferredIndex: replacementIndex)
} }
syncAudioFocus() syncAudioFocus()
} }
@@ -496,8 +497,13 @@ final class GamesViewModel {
} }
func setAudioFocus(streamID: String?) { func setAudioFocus(streamID: String?) {
if let streamID, activeStreams.contains(where: { $0.id == streamID }) { if let streamID,
let stream = activeStreams.first(where: { $0.id == streamID }),
!stream.forceMuteAudio {
audioFocusStreamID = streamID audioFocusStreamID = streamID
} else if streamID != nil {
syncAudioFocus()
return
} else { } else {
audioFocusStreamID = nil audioFocusStreamID = nil
} }
@@ -512,7 +518,7 @@ final class GamesViewModel {
guard let index = activeStreams.firstIndex(where: { $0.id == streamID }) else { return } guard let index = activeStreams.firstIndex(where: { $0.id == streamID }) else { return }
activeStreams[index].player = player activeStreams[index].player = player
activeStreams[index].isPlaying = true activeStreams[index].isPlaying = true
let shouldMute = audioFocusStreamID != streamID let shouldMute = shouldMuteAudio(for: activeStreams[index])
activeStreams[index].isMuted = shouldMute activeStreams[index].isMuted = shouldMute
player.isMuted = shouldMute player.isMuted = shouldMute
} }
@@ -528,13 +534,42 @@ final class GamesViewModel {
} }
private func syncAudioFocus() { private func syncAudioFocus() {
if let audioFocusStreamID,
!activeStreams.contains(where: { $0.id == audioFocusStreamID && !$0.forceMuteAudio }) {
self.audioFocusStreamID = preferredAudioFocusStreamID()
}
for index in activeStreams.indices { for index in activeStreams.indices {
let shouldMute = activeStreams[index].id != audioFocusStreamID let shouldMute = shouldMuteAudio(for: activeStreams[index])
activeStreams[index].isMuted = shouldMute activeStreams[index].isMuted = shouldMute
activeStreams[index].player?.isMuted = shouldMute activeStreams[index].player?.isMuted = shouldMute
} }
} }
private func shouldCaptureAudio(for stream: ActiveStream) -> Bool {
!stream.forceMuteAudio && audioFocusStreamID == nil
}
private func shouldMuteAudio(for stream: ActiveStream) -> Bool {
stream.forceMuteAudio || audioFocusStreamID != stream.id
}
private func preferredAudioFocusStreamID(preferredIndex: Int? = nil) -> String? {
let eligibleIndices = activeStreams.indices.filter { !activeStreams[$0].forceMuteAudio }
guard !eligibleIndices.isEmpty else { return nil }
if let preferredIndex {
if let forwardIndex = eligibleIndices.first(where: { $0 >= preferredIndex }) {
return activeStreams[forwardIndex].id
}
if let fallbackIndex = eligibleIndices.last {
return activeStreams[fallbackIndex].id
}
}
return activeStreams[eligibleIndices[0]].id
}
func buildStreamURL(for config: StreamConfig) async -> URL { func buildStreamURL(for config: StreamConfig) async -> URL {
let startedAt = Date() let startedAt = Date()
logGamesViewModel("buildStreamURL start mediaId=\(config.mediaId) resolution=\(config.resolution)") logGamesViewModel("buildStreamURL start mediaId=\(config.mediaId) resolution=\(config.resolution)")
@@ -623,8 +658,9 @@ final class GamesViewModel {
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
let entries = try decoder.decode([RemoteVideoFeedEntry].self, from: data) let entries = try decoder.decode([RemoteVideoFeedEntry].self, from: data)
let urls = entries.compactMap { entry in let urls: [URL] = entries.compactMap { entry -> URL? in
URL(string: entry.videoFile, relativeTo: feedURL)?.absoluteURL guard let path = entry.hlsUrl ?? entry.videoFile else { return nil }
return URL(string: path, relativeTo: feedURL)?.absoluteURL
} }
guard !urls.isEmpty else { guard !urls.isEmpty else {
@@ -763,7 +799,6 @@ final class GamesViewModel {
func addMLBNetwork() async { func addMLBNetwork() async {
guard activeStreams.count < 4 else { return } guard activeStreams.count < 4 else { return }
guard !activeStreams.contains(where: { $0.id == "MLBN" }) else { return } guard !activeStreams.contains(where: { $0.id == "MLBN" }) else { return }
let shouldCaptureAudio = activeStreams.isEmpty && audioFocusStreamID == nil
let dummyGame = Game( let dummyGame = Game(
id: "MLBN", id: "MLBN",
@@ -778,7 +813,7 @@ final class GamesViewModel {
id: "MLBN", game: dummyGame, label: "MLB Network", overrideURL: url id: "MLBN", game: dummyGame, label: "MLB Network", overrideURL: url
) )
activeStreams.append(stream) activeStreams.append(stream)
if shouldCaptureAudio { if shouldCaptureAudio(for: stream) {
audioFocusStreamID = stream.id audioFocusStreamID = stream.id
} }
syncAudioFocus() syncAudioFocus()
@@ -818,4 +853,5 @@ struct ActiveStream: Identifiable, @unchecked Sendable {
var player: AVPlayer? var player: AVPlayer?
var isPlaying = false var isPlaying = false
var isMuted = false var isMuted = false
var forceMuteAudio = false
} }

View File

@@ -0,0 +1,39 @@
import SwiftUI
struct PlatformPressButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.opacity(configuration.isPressed ? 0.92 : 1.0)
.animation(.easeOut(duration: 0.16), value: configuration.isPressed)
}
}
extension View {
@ViewBuilder
func platformCardStyle() -> some View {
#if os(tvOS)
self.buttonStyle(.card)
#else
self.buttonStyle(PlatformPressButtonStyle())
#endif
}
@ViewBuilder
func platformFocusSection() -> some View {
#if os(tvOS)
self.focusSection()
#else
self
#endif
}
@ViewBuilder
func platformFocusable(_ enabled: Bool = true) -> some View {
#if os(tvOS)
self.focusable(enabled)
#else
self
#endif
}
}

View File

@@ -11,14 +11,14 @@ private func logDashboard(_ message: String) {
enum SpecialPlaybackChannelConfig { enum SpecialPlaybackChannelConfig {
static let werkoutNSFWStreamID = "WKNSFW" static let werkoutNSFWStreamID = "WKNSFW"
static let werkoutNSFWTitle = "Werkout NSFW" static let werkoutNSFWTitle = "Werkout NSFW"
static let werkoutNSFWSubtitle = "Authenticated private HLS feed" static let werkoutNSFWSubtitle = "Authenticated OF media feed"
static let werkoutNSFWFeedURLString = "https://dev.werkout.fitness/videos/nsfw_videos/" static let werkoutNSFWFeedURLString = "https://ofapp.treytartt.com/api/media?users=tabatachang,werkout&type=video"
static let werkoutNSFWAuthToken = "15d7565cde9e8c904ae934f8235f68f6a24b4a03" static let werkoutNSFWCookie = "ofapp_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc3NDY0MjEyNiwiZXhwIjoxNzc1MjQ2OTI2fQ.rDvZzLQ70MM9drHwA8hRxmqcTTgBGBHXxv1Cc55HSqc"
static let werkoutNSFWTeamCode = "WK" static let werkoutNSFWTeamCode = "WK"
static var werkoutNSFWHeaders: [String: String] { static var werkoutNSFWHeaders: [String: String] {
[ [
"authorization": "Token \(werkoutNSFWAuthToken)", "Cookie": werkoutNSFWCookie,
] ]
} }
@@ -50,15 +50,48 @@ enum SpecialPlaybackChannelConfig {
struct DashboardView: View { struct DashboardView: View {
@Environment(GamesViewModel.self) private var viewModel @Environment(GamesViewModel.self) private var viewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var selectedGame: Game? @State private var selectedGame: Game?
@State private var fullScreenBroadcast: BroadcastSelection? @State private var fullScreenBroadcast: BroadcastSelection?
@State private var pendingFullScreenBroadcast: BroadcastSelection? @State private var pendingFullScreenBroadcast: BroadcastSelection?
@State private var showMLBNetworkSheet = false @State private var showMLBNetworkSheet = false
@State private var showWerkoutNSFWSheet = false @State private var showWerkoutNSFWSheet = false
private var horizontalPadding: CGFloat {
#if os(iOS)
horizontalSizeClass == .compact ? 20 : 32
#else
60
#endif
}
private var verticalPadding: CGFloat {
#if os(iOS)
horizontalSizeClass == .compact ? 24 : 32
#else
40
#endif
}
private var contentSpacing: CGFloat {
#if os(iOS)
horizontalSizeClass == .compact ? 32 : 42
#else
50
#endif
}
private var shelfCardWidth: CGFloat {
#if os(iOS)
horizontalSizeClass == .compact ? 300 : 360
#else
400
#endif
}
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 50) { VStack(alignment: .leading, spacing: contentSpacing) {
headerSection headerSection
if viewModel.isLoading { if viewModel.isLoading {
@@ -111,8 +144,8 @@ struct DashboardView: View {
multiViewStatus multiViewStatus
} }
} }
.padding(.horizontal, 60) .padding(.horizontal, horizontalPadding)
.padding(.vertical, 40) .padding(.vertical, verticalPadding)
} }
.onAppear { .onAppear {
logDashboard("DashboardView appeared") logDashboard("DashboardView appeared")
@@ -210,7 +243,8 @@ struct DashboardView: View {
} }
return SingleStreamPlaybackSource( return SingleStreamPlaybackSource(
url: url, url: url,
httpHeaders: SpecialPlaybackChannelConfig.werkoutNSFWHeaders httpHeaders: SpecialPlaybackChannelConfig.werkoutNSFWHeaders,
forceMuteAudio: true
) )
} }
let stream = ActiveStream( let stream = ActiveStream(
@@ -240,7 +274,8 @@ struct DashboardView: View {
} }
return SingleStreamPlaybackSource( return SingleStreamPlaybackSource(
url: nextURL, url: nextURL,
httpHeaders: SpecialPlaybackChannelConfig.werkoutNSFWHeaders httpHeaders: SpecialPlaybackChannelConfig.werkoutNSFWHeaders,
forceMuteAudio: true
) )
} }
@@ -303,10 +338,10 @@ struct DashboardView: View {
GameCardView(game: game) { GameCardView(game: game) {
selectedGame = game selectedGame = game
} }
.frame(width: 400) .frame(width: shelfCardWidth)
} }
} }
.padding(.vertical, 20) .padding(.vertical, 12)
} }
.scrollClipDisabled() .scrollClipDisabled()
} }
@@ -392,12 +427,19 @@ struct DashboardView: View {
@ViewBuilder @ViewBuilder
private var featuredChannelsSection: some View { private var featuredChannelsSection: some View {
HStack(alignment: .top, spacing: 24) { ViewThatFits {
mlbNetworkCard HStack(alignment: .top, spacing: 24) {
.frame(maxWidth: .infinity) mlbNetworkCard
.frame(maxWidth: .infinity)
nsfwVideosCard nsfwVideosCard
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
}
VStack(alignment: .leading, spacing: 16) {
mlbNetworkCard
nsfwVideosCard
}
} }
} }
@@ -436,7 +478,7 @@ struct DashboardView: View {
.background(.regularMaterial) .background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20)) .clipShape(RoundedRectangle(cornerRadius: 20))
} }
.buttonStyle(.card) .platformCardStyle()
} }
@ViewBuilder @ViewBuilder
@@ -478,7 +520,7 @@ struct DashboardView: View {
.background(.regularMaterial) .background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20)) .clipShape(RoundedRectangle(cornerRadius: 20))
} }
.buttonStyle(.card) .platformCardStyle()
} }
// MARK: - Multi-View Status // MARK: - Multi-View Status
@@ -528,6 +570,7 @@ struct BroadcastSelection: Identifiable {
struct WerkoutNSFWSheet: View { struct WerkoutNSFWSheet: View {
var onWatchFullScreen: () -> Void var onWatchFullScreen: () -> Void
@Environment(GamesViewModel.self) private var viewModel @Environment(GamesViewModel.self) private var viewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var isResolvingMultiViewSource = false @State private var isResolvingMultiViewSource = false
@State private var multiViewErrorMessage: String? @State private var multiViewErrorMessage: String?
@@ -540,17 +583,41 @@ struct WerkoutNSFWSheet: View {
added || viewModel.activeStreams.count < 4 added || viewModel.activeStreams.count < 4
} }
private var usesStackedLayout: Bool {
#if os(iOS)
horizontalSizeClass == .compact
#else
false
#endif
}
private var outerHorizontalPadding: CGFloat {
usesStackedLayout ? 20 : 94
}
private var outerVerticalPadding: CGFloat {
usesStackedLayout ? 24 : 70
}
var body: some View { var body: some View {
ZStack { ZStack {
sheetBackground sheetBackground
.ignoresSafeArea() .ignoresSafeArea()
HStack(alignment: .top, spacing: 32) { ViewThatFits {
overviewColumn HStack(alignment: .top, spacing: 32) {
.frame(maxWidth: .infinity, alignment: .leading) overviewColumn
.frame(maxWidth: .infinity, alignment: .leading)
actionColumn actionColumn
.frame(width: 360, alignment: .leading) .frame(width: 360, alignment: .leading)
}
VStack(alignment: .leading, spacing: 24) {
overviewColumn
actionColumn
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
.padding(38) .padding(38)
.background( .background(
@@ -561,8 +628,8 @@ struct WerkoutNSFWSheet: View {
.strokeBorder(.white.opacity(0.08), lineWidth: 1) .strokeBorder(.white.opacity(0.08), lineWidth: 1)
} }
) )
.padding(.horizontal, 94) .padding(.horizontal, outerHorizontalPadding)
.padding(.vertical, 70) .padding(.vertical, outerVerticalPadding)
} }
} }
@@ -711,7 +778,8 @@ struct WerkoutNSFWSheet: View {
label: SpecialPlaybackChannelConfig.werkoutNSFWTitle, label: SpecialPlaybackChannelConfig.werkoutNSFWTitle,
game: SpecialPlaybackChannelConfig.werkoutNSFWGame, game: SpecialPlaybackChannelConfig.werkoutNSFWGame,
feedURL: SpecialPlaybackChannelConfig.werkoutNSFWFeedURL, feedURL: SpecialPlaybackChannelConfig.werkoutNSFWFeedURL,
headers: SpecialPlaybackChannelConfig.werkoutNSFWHeaders headers: SpecialPlaybackChannelConfig.werkoutNSFWHeaders,
forceMuteAudio: true
) )
isResolvingMultiViewSource = false isResolvingMultiViewSource = false
if didAddStream { if didAddStream {
@@ -770,7 +838,7 @@ struct WerkoutNSFWSheet: View {
.fill(fill) .fill(fill)
) )
} }
.buttonStyle(.card) .platformCardStyle()
.disabled(disabled) .disabled(disabled)
} }

View File

@@ -26,12 +26,20 @@ struct FeaturedGameCard: View {
var body: some View { var body: some View {
Button(action: onSelect) { Button(action: onSelect) {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(alignment: .top, spacing: 28) { ViewThatFits {
matchupColumn HStack(alignment: .top, spacing: 28) {
.frame(maxWidth: .infinity, alignment: .leading) matchupColumn
.frame(maxWidth: .infinity, alignment: .leading)
sidePanel sidePanel
.frame(width: 760, alignment: .leading) .frame(width: 760, alignment: .leading)
}
VStack(alignment: .leading, spacing: 24) {
matchupColumn
sidePanel
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
.padding(.horizontal, 34) .padding(.horizontal, 34)
.padding(.top, 30) .padding(.top, 30)
@@ -60,7 +68,7 @@ struct FeaturedGameCard: View {
) )
.shadow(color: shadowColor, radius: 24, y: 10) .shadow(color: shadowColor, radius: 24, y: 10)
} }
.buttonStyle(.card) .platformCardStyle()
} }
@ViewBuilder @ViewBuilder

View File

@@ -62,7 +62,7 @@ struct GameCardView: View {
y: 8 y: 8
) )
} }
.buttonStyle(.card) .platformCardStyle()
} }
@ViewBuilder @ViewBuilder

View File

@@ -76,7 +76,7 @@ struct GameCenterView: View {
.background(.white.opacity(0.08)) .background(.white.opacity(0.08))
.clipShape(Capsule()) .clipShape(Capsule())
} }
.buttonStyle(.card) .platformCardStyle()
.disabled(game.gamePk == nil) .disabled(game.gamePk == nil)
} }
.padding(22) .padding(22)

View File

@@ -4,6 +4,7 @@ struct StreamOptionsSheet: View {
let game: Game let game: Game
var onWatch: ((BroadcastSelection) -> Void)? var onWatch: ((BroadcastSelection) -> Void)?
@Environment(GamesViewModel.self) private var viewModel @Environment(GamesViewModel.self) private var viewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
private var awayColor: Color { TeamAssets.color(for: game.awayTeam.code) } private var awayColor: Color { TeamAssets.color(for: game.awayTeam.code) }
@@ -17,6 +18,22 @@ struct StreamOptionsSheet: View {
viewModel.activeStreams.count < 4 viewModel.activeStreams.count < 4
} }
private var usesStackedLayout: Bool {
#if os(iOS)
horizontalSizeClass == .compact
#else
false
#endif
}
private var horizontalPadding: CGFloat {
usesStackedLayout ? 20 : 56
}
private var verticalPadding: CGFloat {
usesStackedLayout ? 24 : 42
}
private var awayPitcherName: String? { private var awayPitcherName: String? {
guard let pitchers = game.pitchers else { return nil } guard let pitchers = game.pitchers else { return nil }
let parts = pitchers.components(separatedBy: " vs ") let parts = pitchers.components(separatedBy: " vs ")
@@ -32,12 +49,20 @@ struct StreamOptionsSheet: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 28) { VStack(spacing: 28) {
HStack(alignment: .top, spacing: 28) { ViewThatFits {
matchupColumn HStack(alignment: .top, spacing: 28) {
.frame(maxWidth: .infinity, alignment: .leading) matchupColumn
.frame(maxWidth: .infinity, alignment: .leading)
actionRail actionRail
.frame(width: 520, alignment: .leading) .frame(width: 520, alignment: .leading)
}
VStack(alignment: .leading, spacing: 24) {
matchupColumn
actionRail
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
if !canAddMoreStreams { if !canAddMoreStreams {
@@ -53,8 +78,8 @@ struct StreamOptionsSheet: View {
.background(panelBackground) .background(panelBackground)
} }
} }
.padding(.horizontal, 56) .padding(.horizontal, horizontalPadding)
.padding(.vertical, 42) .padding(.vertical, verticalPadding)
} }
.background(sheetBackground.ignoresSafeArea()) .background(sheetBackground.ignoresSafeArea())
} }
@@ -470,7 +495,7 @@ struct StreamOptionsSheet: View {
.fill(fill) .fill(fill)
) )
} }
.buttonStyle(.card) .platformCardStyle()
.disabled(disabled) .disabled(disabled)
} }

View File

@@ -2,14 +2,28 @@ import SwiftUI
struct LeagueCenterView: View { struct LeagueCenterView: View {
@Environment(GamesViewModel.self) private var gamesViewModel @Environment(GamesViewModel.self) private var gamesViewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var viewModel = LeagueCenterViewModel() @State private var viewModel = LeagueCenterViewModel()
@State private var selectedGame: Game? @State private var selectedGame: Game?
private let rosterColumns = [ private var rosterColumns: [GridItem] {
GridItem(.flexible(), spacing: 14), let columnCount: Int
GridItem(.flexible(), spacing: 14), #if os(iOS)
GridItem(.flexible(), spacing: 14), columnCount = horizontalSizeClass == .compact ? 1 : 2
] #else
columnCount = 3
#endif
return Array(repeating: GridItem(.flexible(), spacing: 14), count: columnCount)
}
private var horizontalPadding: CGFloat {
#if os(iOS)
horizontalSizeClass == .compact ? 20 : 32
#else
56
#endif
}
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -40,7 +54,7 @@ struct LeagueCenterView: View {
messagePanel(playerErrorMessage, tint: .orange) messagePanel(playerErrorMessage, tint: .orange)
} }
} }
.padding(.horizontal, 56) .padding(.horizontal, horizontalPadding)
.padding(.vertical, 40) .padding(.vertical, 40)
} }
.background(screenBackground.ignoresSafeArea()) .background(screenBackground.ignoresSafeArea())
@@ -158,7 +172,7 @@ struct LeagueCenterView: View {
.padding(22) .padding(22)
.background(sectionPanel) .background(sectionPanel)
} }
.buttonStyle(.card) .platformCardStyle()
.disabled(linkedGame == nil) .disabled(linkedGame == nil)
} }
@@ -218,7 +232,7 @@ struct LeagueCenterView: View {
} }
.padding(.vertical, 4) .padding(.vertical, 4)
} }
.focusSection() .platformFocusSection()
.scrollClipDisabled() .scrollClipDisabled()
} }
} }
@@ -306,12 +320,12 @@ struct LeagueCenterView: View {
.stroke(.white.opacity(0.08), lineWidth: 1) .stroke(.white.opacity(0.08), lineWidth: 1)
) )
} }
.buttonStyle(.card) .platformCardStyle()
} }
} }
.padding(.vertical, 4) .padding(.vertical, 4)
} }
.focusSection() .platformFocusSection()
.scrollClipDisabled() .scrollClipDisabled()
} }
} }
@@ -356,7 +370,7 @@ struct LeagueCenterView: View {
.padding(24) .padding(24)
.background(sectionPanel) .background(sectionPanel)
.contentShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) .contentShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.focusable(true) .platformFocusable()
} }
} }
} }
@@ -391,7 +405,7 @@ struct LeagueCenterView: View {
.padding(16) .padding(16)
.background(sectionPanel) .background(sectionPanel)
} }
.buttonStyle(.card) .platformCardStyle()
} }
} }
} }
@@ -482,7 +496,7 @@ struct LeagueCenterView: View {
.padding(24) .padding(24)
.background(sectionPanel) .background(sectionPanel)
.contentShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) .contentShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.focusable(true) .platformFocusable()
} }
} }
} }
@@ -580,7 +594,7 @@ struct LeagueCenterView: View {
.background(.white.opacity(0.08)) .background(.white.opacity(0.08))
.clipShape(Capsule()) .clipShape(Capsule())
} }
.buttonStyle(.card) .platformCardStyle()
} }
private func loadingPanel(title: String) -> some View { private func loadingPanel(title: String) -> some View {

View File

@@ -3,6 +3,7 @@ import SwiftUI
struct MLBNetworkSheet: View { struct MLBNetworkSheet: View {
var onWatchFullScreen: () -> Void var onWatchFullScreen: () -> Void
@Environment(GamesViewModel.self) private var viewModel @Environment(GamesViewModel.self) private var viewModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
private var added: Bool { private var added: Bool {
@@ -13,17 +14,41 @@ struct MLBNetworkSheet: View {
added || viewModel.activeStreams.count < 4 added || viewModel.activeStreams.count < 4
} }
private var usesStackedLayout: Bool {
#if os(iOS)
horizontalSizeClass == .compact
#else
false
#endif
}
private var outerHorizontalPadding: CGFloat {
usesStackedLayout ? 20 : 94
}
private var outerVerticalPadding: CGFloat {
usesStackedLayout ? 24 : 70
}
var body: some View { var body: some View {
ZStack { ZStack {
sheetBackground sheetBackground
.ignoresSafeArea() .ignoresSafeArea()
HStack(alignment: .top, spacing: 32) { ViewThatFits {
networkOverview HStack(alignment: .top, spacing: 32) {
.frame(maxWidth: .infinity, alignment: .leading) networkOverview
.frame(maxWidth: .infinity, alignment: .leading)
actionColumn actionColumn
.frame(width: 360, alignment: .leading) .frame(width: 360, alignment: .leading)
}
VStack(alignment: .leading, spacing: 24) {
networkOverview
actionColumn
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
.padding(38) .padding(38)
.background( .background(
@@ -34,8 +59,8 @@ struct MLBNetworkSheet: View {
.strokeBorder(.white.opacity(0.08), lineWidth: 1) .strokeBorder(.white.opacity(0.08), lineWidth: 1)
} }
) )
.padding(.horizontal, 94) .padding(.horizontal, outerHorizontalPadding)
.padding(.vertical, 70) .padding(.vertical, outerVerticalPadding)
} }
} }
@@ -209,7 +234,7 @@ struct MLBNetworkSheet: View {
.fill(fill) .fill(fill)
) )
} }
.buttonStyle(.card) .platformCardStyle()
.disabled(disabled) .disabled(disabled)
} }

View File

@@ -180,7 +180,7 @@ struct MultiStreamView: View {
destructive: false destructive: false
) )
} }
.buttonStyle(.card) .platformCardStyle()
Button { Button {
viewModel.clearAllStreams() viewModel.clearAllStreams()
@@ -192,7 +192,7 @@ struct MultiStreamView: View {
destructive: true destructive: true
) )
} }
.buttonStyle(.card) .platformCardStyle()
} }
} }
} }
@@ -233,7 +233,9 @@ struct MultiStreamView: View {
private struct MultiViewCanvas: View { private struct MultiViewCanvas: View {
@Environment(GamesViewModel.self) private var viewModel @Environment(GamesViewModel.self) private var viewModel
#if os(tvOS)
@FocusState private var focusedStreamID: String? @FocusState private var focusedStreamID: String?
#endif
let contentInsets: CGFloat let contentInsets: CGFloat
let gap: CGFloat let gap: CGFloat
@@ -250,35 +252,23 @@ private struct MultiViewCanvas: View {
inset: contentInsets, inset: contentInsets,
gap: gap gap: gap
) )
#if os(tvOS)
let focusEntries = Array(viewModel.activeStreams.enumerated()).compactMap { index, stream -> MultiViewFocusEntry? in let focusEntries = Array(viewModel.activeStreams.enumerated()).compactMap { index, stream -> MultiViewFocusEntry? in
guard index < frames.count else { return nil } guard index < frames.count else { return nil }
return MultiViewFocusEntry(streamID: stream.id, frame: frames[index]) return MultiViewFocusEntry(streamID: stream.id, frame: frames[index])
} }
#endif
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
ForEach(Array(viewModel.activeStreams.enumerated()), id: \.element.id) { index, stream in ForEach(Array(viewModel.activeStreams.enumerated()), id: \.element.id) { index, stream in
if index < frames.count { if index < frames.count {
let frame = frames[index] let frame = frames[index]
MultiStreamTile( tileView(for: stream, frame: frame, position: index + 1)
stream: stream,
position: index + 1,
isPrimary: viewModel.isPrimaryStream(stream.id),
isAudioFocused: viewModel.audioFocusStreamID == stream.id,
isFocused: focusedStreamID == stream.id,
showsPrimaryBadge: viewModel.multiViewLayoutMode == .spotlight
&& viewModel.isPrimaryStream(stream.id)
&& viewModel.activeStreams.count > 1,
videoGravity: videoGravity,
cornerRadius: cornerRadius,
onSelect: { onSelect(stream) }
)
.frame(width: frame.width, height: frame.height)
.position(x: frame.midX, y: frame.midY)
.focused($focusedStreamID, equals: stream.id)
} }
} }
} }
.focusSection() .platformFocusSection()
#if os(tvOS)
.onMoveCommand { direction in .onMoveCommand { direction in
let currentID = focusedStreamID ?? viewModel.audioFocusStreamID ?? viewModel.activeStreams.first?.id let currentID = focusedStreamID ?? viewModel.audioFocusStreamID ?? viewModel.activeStreams.first?.id
if let nextID = nextMultiViewFocusID( if let nextID = nextMultiViewFocusID(
@@ -289,7 +279,9 @@ private struct MultiViewCanvas: View {
focusedStreamID = nextID focusedStreamID = nextID
} }
} }
#endif
} }
#if os(tvOS)
.onAppear { .onAppear {
if focusedStreamID == nil { if focusedStreamID == nil {
focusedStreamID = viewModel.audioFocusStreamID ?? viewModel.activeStreams.first?.id focusedStreamID = viewModel.audioFocusStreamID ?? viewModel.activeStreams.first?.id
@@ -307,6 +299,41 @@ private struct MultiViewCanvas: View {
viewModel.setAudioFocus(streamID: streamID) viewModel.setAudioFocus(streamID: streamID)
} }
} }
#endif
}
@ViewBuilder
private func tileView(for stream: ActiveStream, frame: CGRect, position: Int) -> some View {
let tile = MultiStreamTile(
stream: stream,
position: position,
isPrimary: viewModel.isPrimaryStream(stream.id),
isAudioFocused: viewModel.audioFocusStreamID == stream.id,
isFocused: {
#if os(tvOS)
focusedStreamID == stream.id
#else
false
#endif
}(),
showsPrimaryBadge: viewModel.multiViewLayoutMode == .spotlight
&& viewModel.isPrimaryStream(stream.id)
&& viewModel.activeStreams.count > 1,
videoGravity: videoGravity,
cornerRadius: cornerRadius,
onSelect: { onSelect(stream) }
)
#if os(tvOS)
tile
.frame(width: frame.width, height: frame.height)
.position(x: frame.midX, y: frame.midY)
.focused($focusedStreamID, equals: stream.id)
#else
tile
.frame(width: frame.width, height: frame.height)
.position(x: frame.midX, y: frame.midY)
#endif
} }
} }
@@ -404,9 +431,8 @@ private struct MultiStreamTile: View {
radius: isFocused ? 26 : 20, radius: isFocused ? 26 : 20,
y: 10 y: 10
) )
.focusEffectDisabled()
.focusable(true)
.animation(.easeOut(duration: 0.18), value: isFocused) .animation(.easeOut(duration: 0.18), value: isFocused)
.platformFocusable()
.onAppear { .onAppear {
logMultiView("tile appeared id=\(stream.id) label=\(stream.label)") logMultiView("tile appeared id=\(stream.id) label=\(stream.label)")
} }
@@ -418,6 +444,8 @@ private struct MultiStreamTile: View {
qualityUpgradeTask = nil qualityUpgradeTask = nil
playbackDiagnostics.clear(streamID: stream.id, reason: "tile disappeared") playbackDiagnostics.clear(streamID: stream.id, reason: "tile disappeared")
} }
#if os(tvOS)
.focusEffectDisabled()
.onPlayPauseCommand { .onPlayPauseCommand {
if player?.rate == 0 { if player?.rate == 0 {
player?.play() player?.play()
@@ -425,6 +453,7 @@ private struct MultiStreamTile: View {
player?.pause() player?.pause()
} }
} }
#endif
.onTapGesture { .onTapGesture {
onSelect() onSelect()
} }
@@ -507,7 +536,7 @@ private struct MultiStreamTile: View {
) )
if let player { if let player {
player.isMuted = viewModel.audioFocusStreamID != stream.id player.isMuted = stream.forceMuteAudio || viewModel.audioFocusStreamID != stream.id
playbackDiagnostics.attach( playbackDiagnostics.attach(
to: player, to: player,
streamID: stream.id, streamID: stream.id,
@@ -529,7 +558,7 @@ private struct MultiStreamTile: View {
} }
if let existingPlayer = stream.player { if let existingPlayer = stream.player {
existingPlayer.isMuted = viewModel.audioFocusStreamID != stream.id existingPlayer.isMuted = stream.forceMuteAudio || viewModel.audioFocusStreamID != stream.id
self.player = existingPlayer self.player = existingPlayer
hasError = false hasError = false
playbackDiagnostics.attach( playbackDiagnostics.attach(
@@ -725,12 +754,10 @@ private struct MultiStreamTile: View {
.value .value
} }
private func playbackEndedHandler(for player: AVPlayer) -> (() -> Void)? { private func playbackEndedHandler(for player: AVPlayer) -> (@MainActor @Sendable () async -> Void)? {
guard stream.id == SpecialPlaybackChannelConfig.werkoutNSFWStreamID else { return nil } guard stream.id == SpecialPlaybackChannelConfig.werkoutNSFWStreamID else { return nil }
return { return {
Task { @MainActor in await playNextWerkoutClip(on: player)
await playNextWerkoutClip(on: player)
}
} }
} }
@@ -832,7 +859,7 @@ private final class MultiStreamPlaybackDiagnostics: ObservableObject {
to player: AVPlayer, to player: AVPlayer,
streamID: String, streamID: String,
label: String, label: String,
onPlaybackEnded: (() -> Void)? = nil onPlaybackEnded: (@MainActor @Sendable () async -> Void)? = nil
) { ) {
let playerIdentifier = ObjectIdentifier(player) let playerIdentifier = ObjectIdentifier(player)
let itemIdentifier = player.currentItem.map { ObjectIdentifier($0) } let itemIdentifier = player.currentItem.map { ObjectIdentifier($0) }
@@ -922,7 +949,10 @@ private final class MultiStreamPlaybackDiagnostics: ObservableObject {
queue: .main queue: .main
) { _ in ) { _ in
logMultiView("playerItem didPlayToEnd id=\(streamID)") logMultiView("playerItem didPlayToEnd id=\(streamID)")
onPlaybackEnded?() guard let onPlaybackEnded else { return }
Task { @MainActor in
await onPlaybackEnded()
}
} }
) )
@@ -1002,7 +1032,7 @@ private struct MultiViewLayoutPicker: View {
.stroke(viewModel.multiViewLayoutMode == mode ? .blue.opacity(0.9) : .white.opacity(0.12), lineWidth: 1) .stroke(viewModel.multiViewLayoutMode == mode ? .blue.opacity(0.9) : .white.opacity(0.12), lineWidth: 1)
) )
} }
.buttonStyle(.card) .platformCardStyle()
} }
} }
} }
@@ -1027,6 +1057,10 @@ struct StreamControlSheet: View {
viewModel.audioFocusStreamID == streamID viewModel.audioFocusStreamID == streamID
} }
private var forceMuteAudio: Bool {
stream?.forceMuteAudio == true
}
var body: some View { var body: some View {
ZStack { ZStack {
LinearGradient( LinearGradient(
@@ -1052,18 +1086,21 @@ struct StreamControlSheet: View {
HStack(spacing: 10) { HStack(spacing: 10) {
controlBadge(title: isPrimary ? "Primary Tile" : "Secondary Tile", tint: .blue) controlBadge(title: isPrimary ? "Primary Tile" : "Secondary Tile", tint: .blue)
controlBadge(title: isAudioFocused ? "Live Audio" : "Muted", tint: isAudioFocused ? .green : .white) controlBadge(
title: forceMuteAudio ? "Video Only" : (isAudioFocused ? "Live Audio" : "Muted"),
tint: forceMuteAudio ? .orange : (isAudioFocused ? .green : .white)
)
} }
} }
VStack(spacing: 14) { VStack(spacing: 14) {
HStack(spacing: 14) { HStack(spacing: 14) {
actionCard( actionCard(
title: isAudioFocused ? "Mute All" : "Listen Here", title: forceMuteAudio ? "Audio Disabled" : (isAudioFocused ? "Mute All" : "Listen Here"),
subtitle: isAudioFocused ? "Silence the multiview mix." : "Route game audio to this tile.", subtitle: forceMuteAudio ? "This channel is always muted." : (isAudioFocused ? "Silence the multiview mix." : "Route game audio to this tile."),
icon: isAudioFocused ? "speaker.slash.fill" : "speaker.wave.2.fill", icon: forceMuteAudio ? "speaker.slash.circle.fill" : (isAudioFocused ? "speaker.slash.fill" : "speaker.wave.2.fill"),
tint: isAudioFocused ? .white : .green, tint: forceMuteAudio ? .orange : (isAudioFocused ? .white : .green),
disabled: false disabled: forceMuteAudio
) { ) {
viewModel.toggleAudioFocus(streamID: streamID) viewModel.toggleAudioFocus(streamID: streamID)
} }
@@ -1181,7 +1218,7 @@ struct StreamControlSheet: View {
) )
} }
.disabled(disabled) .disabled(disabled)
.buttonStyle(.card) .platformCardStyle()
} }
} }
@@ -1210,9 +1247,23 @@ struct MultiStreamFullScreenView: View {
.padding(.horizontal, 18) .padding(.horizontal, 18)
.padding(.bottom, 12) .padding(.bottom, 12)
} }
.overlay(alignment: .topTrailing) {
#if os(iOS)
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(.white.opacity(0.9))
.padding(20)
}
#endif
}
#if os(tvOS)
.onExitCommand { .onExitCommand {
dismiss() dismiss()
} }
#endif
.onChange(of: viewModel.activeStreams.count) { _, count in .onChange(of: viewModel.activeStreams.count) { _, count in
if count == 0 { if count == 0 {
dismiss() dismiss()
@@ -1308,6 +1359,7 @@ private struct MultiViewFocusEntry {
let frame: CGRect let frame: CGRect
} }
#if os(tvOS)
private func nextMultiViewFocusID( private func nextMultiViewFocusID(
from currentID: String?, from currentID: String?,
direction: MoveCommandDirection, direction: MoveCommandDirection,
@@ -1357,3 +1409,4 @@ private func nextMultiViewFocusID(
.entry .entry
.streamID .streamID
} }
#endif

View File

@@ -76,6 +76,7 @@ private func makeSingleStreamPlayerItem(from source: SingleStreamPlaybackSource)
} }
struct SingleStreamPlaybackScreen: View { struct SingleStreamPlaybackScreen: View {
@Environment(\.dismiss) private var dismiss
let resolveSource: @Sendable () async -> SingleStreamPlaybackSource? let resolveSource: @Sendable () async -> SingleStreamPlaybackSource?
var resolveNextSource: (@Sendable (URL?) async -> SingleStreamPlaybackSource?)? = nil var resolveNextSource: (@Sendable (URL?) async -> SingleStreamPlaybackSource?)? = nil
let tickerGames: [Game] let tickerGames: [Game]
@@ -90,6 +91,18 @@ struct SingleStreamPlaybackScreen: View {
.padding(.horizontal, 18) .padding(.horizontal, 18)
.padding(.bottom, 14) .padding(.bottom, 14)
} }
.overlay(alignment: .topTrailing) {
#if os(iOS)
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(.white.opacity(0.9))
.padding(20)
}
#endif
}
.ignoresSafeArea() .ignoresSafeArea()
.onAppear { .onAppear {
logSingleStream("SingleStreamPlaybackScreen appeared tickerGames=\(tickerGames.count) tickerMode=marqueeOverlay") logSingleStream("SingleStreamPlaybackScreen appeared tickerGames=\(tickerGames.count) tickerMode=marqueeOverlay")
@@ -103,10 +116,12 @@ struct SingleStreamPlaybackScreen: View {
struct SingleStreamPlaybackSource: Sendable { struct SingleStreamPlaybackSource: Sendable {
let url: URL let url: URL
let httpHeaders: [String: String] let httpHeaders: [String: String]
let forceMuteAudio: Bool
init(url: URL, httpHeaders: [String: String] = [:]) { init(url: URL, httpHeaders: [String: String] = [:], forceMuteAudio: Bool = false) {
self.url = url self.url = url
self.httpHeaders = httpHeaders self.httpHeaders = httpHeaders
self.forceMuteAudio = forceMuteAudio
} }
} }
@@ -285,6 +300,9 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
let controller = AVPlayerViewController() let controller = AVPlayerViewController()
controller.allowsPictureInPicturePlayback = true controller.allowsPictureInPicturePlayback = true
controller.showsPlaybackControls = true controller.showsPlaybackControls = true
#if os(iOS)
controller.canStartPictureInPictureAutomaticallyFromInline = true
#endif
logSingleStream("AVPlayerViewController configured without contentOverlayView ticker") logSingleStream("AVPlayerViewController configured without contentOverlayView ticker")
Task { @MainActor in Task { @MainActor in
@@ -311,6 +329,7 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
let playerItem = makeSingleStreamPlayerItem(from: source) let playerItem = makeSingleStreamPlayerItem(from: source)
let player = AVPlayer(playerItem: playerItem) let player = AVPlayer(playerItem: playerItem)
player.automaticallyWaitsToMinimizeStalling = false player.automaticallyWaitsToMinimizeStalling = false
player.isMuted = source.forceMuteAudio
logSingleStream("Configured player for fast start preferredForwardBufferDuration=2 automaticallyWaitsToMinimizeStalling=false") logSingleStream("Configured player for fast start preferredForwardBufferDuration=2 automaticallyWaitsToMinimizeStalling=false")
context.coordinator.attachDebugObservers(to: player, url: url, resolveNextSource: resolveNextSource) context.coordinator.attachDebugObservers(to: player, url: url, resolveNextSource: resolveNextSource)
controller.player = player controller.player = player
@@ -433,6 +452,7 @@ struct SingleStreamPlayerView: UIViewControllerRepresentable {
let nextItem = makeSingleStreamPlayerItem(from: nextSource) let nextItem = makeSingleStreamPlayerItem(from: nextSource)
player.replaceCurrentItem(with: nextItem) player.replaceCurrentItem(with: nextItem)
player.automaticallyWaitsToMinimizeStalling = false player.automaticallyWaitsToMinimizeStalling = false
player.isMuted = nextSource.forceMuteAudio
self.attachDebugObservers(to: player, url: nextSource.url, resolveNextSource: resolveNextSource) self.attachDebugObservers(to: player, url: nextSource.url, resolveNextSource: resolveNextSource)
logSingleStream("Autoplay replacing item and replaying url=\(singleStreamDebugURLDescription(nextSource.url))") logSingleStream("Autoplay replacing item and replaying url=\(singleStreamDebugURLDescription(nextSource.url))")
player.playImmediately(atRate: 1.0) player.playImmediately(atRate: 1.0)

View File

@@ -2,6 +2,7 @@ name: mlbTVOS
options: options:
bundleIdPrefix: com.treyt bundleIdPrefix: com.treyt
deploymentTarget: deploymentTarget:
iOS: "18.0"
tvOS: "18.0" tvOS: "18.0"
xcodeVersion: "26.3" xcodeVersion: "26.3"
generateEmptyDirectories: true generateEmptyDirectories: true
@@ -22,3 +23,22 @@ targets:
CODE_SIGN_ENTITLEMENTS: mlbTVOS/mlbTVOS.entitlements CODE_SIGN_ENTITLEMENTS: mlbTVOS/mlbTVOS.entitlements
PRODUCT_BUNDLE_IDENTIFIER: com.treyt.mlbTVOS PRODUCT_BUNDLE_IDENTIFIER: com.treyt.mlbTVOS
SWIFT_STRICT_CONCURRENCY: complete SWIFT_STRICT_CONCURRENCY: complete
mlbIOS:
type: application
platform: iOS
sources:
- path: mlbTVOS
excludes:
- mlbTVOSApp.swift
- Info.plist
- mlbTVOS.entitlements
- Assets.xcassets
- path: mlbIOS
settings:
base:
INFOPLIST_FILE: mlbIOS/Info.plist
CODE_SIGN_ENTITLEMENTS: mlbIOS/mlbIOS.entitlements
PRODUCT_BUNDLE_IDENTIFIER: com.treyt.mlbIOS
TARGETED_DEVICE_FAMILY: "1,2"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
SWIFT_STRICT_CONCURRENCY: complete