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:
11
mlbIOS/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
11
mlbIOS/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
8
mlbIOS/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
8
mlbIOS/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
6
mlbIOS/Assets.xcassets/Contents.json
Normal file
6
mlbIOS/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
42
mlbIOS/Info.plist
Normal file
42
mlbIOS/Info.plist
Normal 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>
|
||||||
106
mlbIOS/Views/mlbIOSRootView.swift
Normal file
106
mlbIOS/Views/mlbIOSRootView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
mlbIOS/mlbIOS.entitlements
Normal file
6
mlbIOS/mlbIOS.entitlements
Normal 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
26
mlbIOS/mlbIOSApp.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
39
mlbTVOS/Views/Components/PlatformUI.swift
Normal file
39
mlbTVOS/Views/Components/PlatformUI.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ struct GameCardView: View {
|
|||||||
y: 8
|
y: 8
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.card)
|
.platformCardStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
20
project.yml
20
project.yml
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user