diff --git a/backend/app/views/public/live/show.html.erb b/backend/app/views/public/live/show.html.erb index 9a1122c..6390bb9 100644 --- a/backend/app/views/public/live/show.html.erb +++ b/backend/app/views/public/live/show.html.erb @@ -277,7 +277,7 @@ offlineMsg.hidden = true; if (!publisherOnline && showingCover && !paused) { awaitingMsg.hidden = false; - awaitingMsg.textContent = "Copertina in onda — in attesa del segnale dal telefono."; + awaitingMsg.textContent = "In riconnessione - in attesa del video dall'evento"; } else { awaitingMsg.hidden = !awaitingSignal; } diff --git a/native/ios/MatchLiveTv.xcodeproj/project.pbxproj b/native/ios/MatchLiveTv.xcodeproj/project.pbxproj index 047b732..532857a 100644 --- a/native/ios/MatchLiveTv.xcodeproj/project.pbxproj +++ b/native/ios/MatchLiveTv.xcodeproj/project.pbxproj @@ -7,293 +7,305 @@ objects = { /* Begin PBXBuildFile section */ - 020986FB10004F36BB7ED0BD /* TokenStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806B1034AB5148B4BC55F093 /* TokenStore.swift */; }; - 0AF514A970C5405EA7A54950 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A245DF805C482C89AEB2E4 /* SplashScreen.swift */; }; - 100C9225A2EB448C8C3132D4 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5EB6A380DC4728866BD42A /* LoginScreen.swift */; }; - 216F56B707484618AE30847E /* ScoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */; }; - 279D1B02380D46DF835831B8 /* TeamColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5086F0DA44004334881343ED /* TeamColorPicker.swift */; }; - 3040471A57FA4032BF347025 /* OverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED842B98F33423988B13960 /* OverlayRenderer.swift */; }; - 3478D3DBB96D41D880BCA499 /* LivePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0386B94B9042918A68CD76 /* LivePreviewView.swift */; }; - 404F2B3994194914994D5998 /* ScoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F05D132A65E45E8AED152F0 /* ScoreController.swift */; }; - 40D98FC5AA9B4AB28AC96902 /* MatchScoringRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */; }; - 4131A683E5784D51B19916D5 /* ScreenOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */; }; - 45CF302AAFD947ACAD8BDF81 /* OverlayMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */; }; - 45DF0F887F314A498AC82C6D /* WatermarkElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */; }; - 467525056A0648EDA1E2A0CD /* BroadcastControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */; }; - 475FE75875AC47A58609BF5B /* WizardComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2627CF334644FCA8FD9950B /* WizardComponents.swift */; }; - 4BDAEE6159624FACA21E178D /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */; }; - 4C5F8E4B2FE308D700962D65 /* HaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = F652BB68B82C49218CCB7CD0 /* HaishinKit */; }; - 4C5F8E4C2FE308D700962D65 /* RTMPHaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */; }; - 4C8522208CD14180831F780B /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */; }; - 4F6973E4E707495FAED4E582 /* MatchScreenScaffold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */; }; - 518508A5943C425492A428C5 /* ScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */; }; - 538CA13BDD594A96B444350E /* MatchLiveTvApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */; }; - 572553E2029047128A322BE0 /* MatchSecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */; }; - 58119B329D05438F84507448 /* MatchLiveAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */; }; - 587FE4FFD78E472B84911501 /* BroadcastVideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0461F47FF06E487491BA53F3 /* BroadcastVideoOrientation.swift */; }; - 59FAC2E6EDD54B90B41BFD34 /* ApiDtos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */; }; - 65DDC9416BE940A889C8647A /* MediaUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */; }; - 67BBA82C7CD04A9B85B08F23 /* MatchScoringRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */; }; - 6868D193A5874D62BD640F8E /* ScoreActionDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */; }; - 6AF878B33937472981276ED6 /* OverlayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84A2EA29C441EFA31498AD /* OverlayState.swift */; }; - 6C6F8189C8554FA298D0209D /* LiveBroadcastEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03F27B7CFAC4095A48C1C83 /* LiveBroadcastEngine.swift */; }; - 798F944B9FFA401E8EDD6B28 /* WizardSessionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */; }; - 7ACD03602B744B4982DB542C /* ScoreControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */; }; - 82D2A2AC562647C9BD18055D /* ColorHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */; }; - 82D2D8D7BBF642EE8E042CDF /* ApiInstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */; }; - 8414C3672E11414CB0F980C3 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D7D448D0A34D9384C5E2D3 /* Models.swift */; }; - 85162099CD364680AA080E20 /* MatchPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */; }; - 87528ECDBB644E73A6DD4C4B /* OverlayCanvasRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */; }; - 8CF81F29F432430CBD56C405 /* BroadcastModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */; }; - 906FC2F226DA4F958260FEA0 /* ActionCableClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */; }; - 9139292098C24797BE1AFB5E /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10C6CAC995404909A95482BF /* SessionRepository.swift */; }; - 9C8922FBA5DD4DCA9885E649 /* StepTransmissionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D813398882A483280580618 /* StepTransmissionScreen.swift */; }; - 9DA3E777DC3C46CD9F6C2933 /* DeviceTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */; }; - 9DA716746DA24ECCA2182B7A /* LiveBroadcastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */; }; - 9E6D271CDEDF4484BAD8161E /* ScoreState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5A60F7644B4881985D4F15 /* ScoreState.swift */; }; - A0B9F06D1A12410E9C1C361D /* OverlayLogoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */; }; - A608CDA9340247E688A315F6 /* UserFacingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC311445F0144B59AE22DA96 /* UserFacingError.swift */; }; - A7C1C8C352404A72A688CEF4 /* StepNetworkTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */; }; - A80C909B160C46CDADF1ABF8 /* WizardShellScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */; }; - ACBB5846610943D9AE3FD2B8 /* MatchLiveTv/Resources/Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */; }; - B2A84ED6937E4D6780A8372B /* MatchColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE769961C58E4741B73938B8 /* MatchColors.swift */; }; - B469E3D9850E46698740395B /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E221E68194C4C92925A9CB0 /* AppConfig.swift */; }; - B54EEA69C2984D0989DDDC3E /* CompactScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */; }; - B6499B988D134CE78DCFF375 /* ModalRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */; }; - BACB4C45854B4531AC2742F3 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC01BBAC06A412296955DB3 /* Routes.swift */; }; - C580112F135E4134A55E4DC7 /* SessionCableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976DD89356184E0694B20CB8 /* SessionCableService.swift */; }; - CD2D69C03B22451E8E75BEE5 /* ApiInstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6725372B2CE473F87167457 /* ApiInstant.swift */; }; - D40F96BF9A98482D861B8714 /* MatchRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */; }; - D5399E5D54444A43B1E3919B /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3DEAC5182ED489798656300 /* AppContainer.swift */; }; - D654C3E362EA4786AE9526E5 /* MatchSessionLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */; }; - D687E626CEC44A11B2BE78FB /* TeamBrandingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */; }; - D792D3DE5C984BB880BE19DD /* AppNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */; }; - D7AAD6BB475B4734BA4EC4EA /* StepMatchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */; }; - D81D1E21B0E44F38A8F5C256 /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */; }; - DAFBA526BBFC4302A851BB93 /* KeepScreenOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */; }; - DC75AFA217B5445AA4FA8BCF /* BroadcastScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */; }; - DEE3503ADE4E48F48A49001D /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B453249C904B299178A996 /* AuthRepository.swift */; }; - DFEE71197E4E41CBB835D0BA /* MatchHubFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944972DE154D444781B612ED /* MatchHubFilter.swift */; }; - E382BF9CB4274DDA96A10A98 /* LiveScoreActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E770890933A84622ABF4EA9F /* LiveScoreActions.swift */; }; - F588C2FE35B140418C13963F /* MatchLiveWordmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */; }; - FB14C028958F46EC8A8FAB18 /* SponsorElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43F8C92A1094AC39A7FE978 /* SponsorElement.swift */; }; + 01BBD9C8228A4654BE791A09 /* WizardSessionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */; }; + 0712F271074B47B992C54B02 /* CompactScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */; }; + 0747277C94AD48939CBAE80B /* ScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */; }; + 07C244E32A4D48A185963C6C /* ScoreControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */; }; + 1C71C5745AF541A793FBD415 /* TeamBrandingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */; }; + 1DE6EABFB67F4046957C9B88 /* StepMatchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */; }; + 266CD29FA3C048CC9413460A /* OverlayCanvasRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */; }; + 2757AF56199F46A49036C307 /* MatchScreenScaffold.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */; }; + 306216CA292D4974B612AC06 /* SponsorElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A0F1558C174BD584AE0377 /* SponsorElement.swift */; }; + 3477E99DCB6843BB8037EB21 /* WizardComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC184551A43F44E09E536D29 /* WizardComponents.swift */; }; + 3628957732604F1597784B04 /* OverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */; }; + 38938AED35E94A99A4921FF3 /* OverlayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA971C263254BDC95AC2423 /* OverlayState.swift */; }; + 3C4DD1CE0BFA4CD9928234CD /* BroadcastVideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */; }; + 4236C0FB1B10435EB03C39DF /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */; }; + 44519A77D19C46218318593E /* OverlayMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */; }; + 49586D0F50AA4BE692FDCF3F /* AppNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */; }; + 4958D62D7A8944EAB3E54F18 /* ScoreState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880F5D2B2B13497891706E0A /* ScoreState.swift */; }; + 4B1080F3C1354A89A859ACC6 /* MatchRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */; }; + 4C26392C2FE69A090015AAF3 /* HaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3242173ACEDC4B288ACD56E4 /* HaishinKit */; }; + 4C26392D2FE69A090015AAF3 /* RTMPHaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = F3539A3AB0894C54992DB7D5 /* RTMPHaishinKit */; }; + 57B21B3B008A4345AEDFD7D2 /* ColorHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818D265485A3445DAB7EAC48 /* ColorHex.swift */; }; + 5C80FF3E9F574F4EA2FDFEFF /* DeviceTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */; }; + 639783D4083F4049B5424C52 /* MediaUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA461A50E47740818FDC5890 /* MediaUrl.swift */; }; + 674D6A9F61D742DF95F93C5B /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A66D5B274FF34093901B981B /* AppConfig.swift */; }; + 7DC17CB11FC246378F6624C0 /* TeamColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573991C190AD43BBBDDD186C /* TeamColorPicker.swift */; }; + 7F7C79F0C4EA4B06A5A2E4BA /* ScreenOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */; }; + 815C82E2C5C54746ABF73BEE /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17256285CF354F0D8E4B33A3 /* SplashScreen.swift */; }; + 82D6D70F2E554414961F27C3 /* MatchScoringRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */; }; + 830325BE85674DD6A31CE672 /* WatermarkElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */; }; + 832E24DBB2844E9895717F6F /* LiveScoreActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */; }; + 83E38DFBBFFF4412BF2ABEE9 /* MatchSecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */; }; + 89F47A469F6044B9A92D647A /* LivePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F07B021AC24DCAA217238C /* LivePreviewView.swift */; }; + 8CDFA36E735A4A94A5A60DBF /* BroadcastControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */; }; + 8DB866F0F33A4F3BAA074660 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */; }; + 8DE2FE7CF4A34222B2D3066A /* ApiDtos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB5819FE32342F1961F256E /* ApiDtos.swift */; }; + 9102501263E1477BAA6DEDD1 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C8DD5093B84D5FBE360C23 /* Models.swift */; }; + 95848029AD4D4683B80DEA8C /* ScoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */; }; + 9770A3D9677F42AC942E98A4 /* BroadcastScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */; }; + 9A208AE54E434798BD5DF06F /* WizardShellScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */; }; + 9EAD5508F44243409C697F41 /* BroadcastOrientationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */; }; + 9EDC5C176D2E40F29455CCEF /* ScoreActionDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */; }; + 9EF4BA3F81C341B4ACAB643F /* StepNetworkTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */; }; + A3D8F65EBA8B479485321837 /* SessionCableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */; }; + AB3DF660C79648CFAECB03CF /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951726866CB945DD917376C7 /* MatchStatusBadge.swift */; }; + ABE74DCBA6804EDEB5398F8F /* MatchPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0C26984E50402281861375 /* MatchPrimaryButton.swift */; }; + B578A98681A8413DBFEF58CA /* MatchColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */; }; + B5937876D40F4E21A63DB92A /* ApiInstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5853956279E94634BFC1660C /* ApiInstant.swift */; }; + B86EA543F6114B198D334FAF /* LiveBroadcastEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */; }; + BAE5062DD4B749E6BC569CCA /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */; }; + BB73AB6E535F473EA6C1D232 /* MatchSessionLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */; }; + BD2628C89CAD450ABF613183 /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */; }; + C0CA6EBDA3E84641AFEC3A12 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136E90D700FD44D89C9D5659 /* LoginScreen.swift */; }; + C1D94B81E44C4BFD8351B165 /* LiveBroadcastCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */; }; + C9B2434416364C4BB263BB3F /* LiveBroadcastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E22842CB3AF4514965D3791 /* LiveBroadcastCoordinator.swift */; }; + CBEE59E2E2DE4C058FF42022 /* MatchHubFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A264DB29100D404EA570CF47 /* MatchHubFilter.swift */; }; + CCB2F270DD464E608046A60C /* MatchLiveTvApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FAE643838541119B344969 /* MatchLiveTvApp.swift */; }; + CEACB53FE13049B9959896F1 /* ScoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D894BB924BFFBC2D244C /* ScoreController.swift */; }; + D1334C2CFEC145328396B7EC /* ActionCableClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */; }; + D3C0878AD7354D41B540AD5B /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DE015C16C54795A496C968 /* AppContainer.swift */; }; + D67C481FC7DE46C6B7AD09C4 /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */; }; + D6F532927A6345339AD2149D /* TokenStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E7E256AA201434EBD01B7D4 /* TokenStore.swift */; }; + DFC67AC6380B41BC8A8CE866 /* ApiInstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */; }; + E054E5E1703A4876B4E13F64 /* MatchScoringRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */; }; + E5B931C144DA46819918A5EC /* MatchLiveAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */; }; + E75B414604CB429A9CCE797C /* BroadcastModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E128E2B501C45419220F030 /* BroadcastModels.swift */; }; + E93FDA6122664F67A5448E2D /* UserFacingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6BA18F615214E4184E199B2 /* UserFacingError.swift */; }; + EA206B930BD448D3AC1E2578 /* OverlayLogoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */; }; + EB3C108379594E7EBCBCE016 /* ModalRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */; }; + EED290CEF4A141608F7BE515 /* KeepScreenOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FBA01162B004E85821F206B /* KeepScreenOn.swift */; }; + EF984C15882E4E328BFCDA74 /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */; }; + F05733319E284083A5AA2784 /* MatchLiveTv/Resources/Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */; }; + F212F8F1DA0C42619D13D30E /* StepTransmissionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */; }; + F36DADA0DA9245A48E3DDB29 /* BroadcastOrientationPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */; }; + F5B6F5CD2D9245BDB8E81055 /* MatchLiveWordmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - C081F7F08D6748AB8C4B8DFB /* PBXContainerItemProxy */ = { + E0F627D71C6349F1857AFED5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = A8764A9D2A234F91B52DDC89 /* Project object */; + containerPortal = 72A102A7C8964137A464FF32 /* Project object */; proxyType = 1; - remoteGlobalIDString = 2F5D6393282640E5A53D4CFC; + remoteGlobalIDString = 553940DB807C4DBFB63E9FFE; remoteInfo = MatchLiveTv; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0461F47FF06E487491BA53F3 /* BroadcastVideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastVideoOrientation.swift; path = MatchLiveTv/Streaming/BroadcastVideoOrientation.swift; sourceTree = ""; }; - 08A245DF805C482C89AEB2E4 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = ""; }; - 0A84A2EA29C441EFA31498AD /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = ""; }; - 0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = ""; }; - 10C6CAC995404909A95482BF /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = ""; }; - 1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = ""; }; - 1ED842B98F33423988B13960 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = ""; }; - 21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = ""; }; - 2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = ""; }; - 227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = ""; }; - 26D7D448D0A34D9384C5E2D3 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = ""; }; - 2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = ""; }; - 2D0386B94B9042918A68CD76 /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = ""; }; - 2E221E68194C4C92925A9CB0 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = ""; }; - 2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = ""; }; - 2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = ""; }; - 316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = ""; }; - 34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = ""; }; - 372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = ""; }; - 388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = ""; }; - 3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = ""; }; - 3F5EB6A380DC4728866BD42A /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = ""; }; - 3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = ""; }; - 4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = ""; }; - 4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = ""; }; - 4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = ""; }; - 4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = ""; }; - 5086F0DA44004334881343ED /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = ""; }; - 551CCAA3C2F64172A59E278B /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = ""; }; - 584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = ""; }; - 5D813398882A483280580618 /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = ""; }; - 6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = ""; }; - 6F05D132A65E45E8AED152F0 /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = ""; }; - 7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = ""; }; - 7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = ""; }; - 7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = ""; }; - 806B1034AB5148B4BC55F093 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = ""; }; - 8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = ""; }; - 8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = ""; }; - 8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = ""; }; - 944972DE154D444781B612ED /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = ""; }; - 94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = ""; }; - 96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = ""; }; - 976DD89356184E0694B20CB8 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = ""; }; - 978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = ""; }; - 9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = ""; }; - A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = ""; }; - A2627CF334644FCA8FD9950B /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = ""; }; - A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = ""; }; - ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = ""; }; - AE769961C58E4741B73938B8 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = ""; }; - B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = ""; }; - BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = ""; }; - C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = ""; }; - C3DCC59301E94417BC24492F /* MatchLiveTv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchLiveTv.app; sourceTree = BUILT_PRODUCTS_DIR; }; - C43F8C92A1094AC39A7FE978 /* SponsorElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SponsorElement.swift; path = MatchLiveTv/Streaming/Overlay/SponsorElement.swift; sourceTree = ""; }; - CCC01BBAC06A412296955DB3 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = ""; }; - CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = ""; }; - D6725372B2CE473F87167457 /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = ""; }; - D6B453249C904B299178A996 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = ""; }; - D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = ""; }; - DE5A60F7644B4881985D4F15 /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = ""; }; - E3DEAC5182ED489798656300 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = ""; }; - E770890933A84622ABF4EA9F /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = ""; }; - EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = ""; }; - EC311445F0144B59AE22DA96 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = ""; }; - ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatchLiveTvTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F03F27B7CFAC4095A48C1C83 /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = ""; }; - F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = ""; }; - F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = ""; }; - FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = ""; }; + 073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = ""; }; + 09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = ""; }; + 0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = ""; }; + 136E90D700FD44D89C9D5659 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = ""; }; + 17256285CF354F0D8E4B33A3 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = ""; }; + 177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicyTests.swift; path = MatchLiveTvTests/BroadcastOrientationPolicyTests.swift; sourceTree = ""; }; + 18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = ""; }; + 1E128E2B501C45419220F030 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = ""; }; + 1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = ""; }; + 24FAE643838541119B344969 /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = ""; }; + 2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = ""; }; + 2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ShareSheet.swift; path = MatchLiveTv/UI/System/ShareSheet.swift; sourceTree = ""; }; + 2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = ""; }; + 2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = ""; }; + 2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = ""; }; + 2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = ""; }; + 34DE015C16C54795A496C968 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = ""; }; + 3AA971C263254BDC95AC2423 /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = ""; }; + 3FBA01162B004E85821F206B /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = ""; }; + 46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastVideoOrientation.swift; path = MatchLiveTv/Streaming/BroadcastVideoOrientation.swift; sourceTree = ""; }; + 573991C190AD43BBBDDD186C /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = ""; }; + 5853956279E94634BFC1660C /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = ""; }; + 5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = ""; }; + 5C49570E1FB3460BBEB502FC /* MatchLiveTvTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatchLiveTvTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E22842CB3AF4514965D3791 /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = ""; }; + 5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = ""; }; + 677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = ""; }; + 79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = ""; }; + 7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = ""; }; + 7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = ""; }; + 7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = ""; }; + 810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = ""; }; + 818D265485A3445DAB7EAC48 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = ""; }; + 81F07B021AC24DCAA217238C /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = ""; }; + 880F5D2B2B13497891706E0A /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = ""; }; + 8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = ""; }; + 8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = ""; }; + 8E7E256AA201434EBD01B7D4 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = ""; }; + 92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = ""; }; + 951726866CB945DD917376C7 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = ""; }; + 9525D894BB924BFFBC2D244C /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = ""; }; + 9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = ""; }; + 9AB5819FE32342F1961F256E /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = ""; }; + A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = ""; }; + A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = ""; }; + A264DB29100D404EA570CF47 /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = ""; }; + A2C8DD5093B84D5FBE360C23 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = ""; }; + A66D5B274FF34093901B981B /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = ""; }; + A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = ""; }; + A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = ""; }; + AB5EAF0FD4BE4B14B5B029B8 /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = ""; }; + B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = ""; }; + B165DBC9A0DA4C62824D41C3 /* MatchLiveTv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchLiveTv.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B5A0F1558C174BD584AE0377 /* SponsorElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SponsorElement.swift; path = MatchLiveTv/Streaming/Overlay/SponsorElement.swift; sourceTree = ""; }; + BA461A50E47740818FDC5890 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = ""; }; + BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = ""; }; + BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = ""; }; + BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicy.swift; path = MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift; sourceTree = ""; }; + BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = ""; }; + CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = ""; }; + CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinatorTests.swift; path = MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift; sourceTree = ""; }; + D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = ""; }; + D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = ""; }; + D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = ""; }; + DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = ""; }; + DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = ""; }; + E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = ""; }; + E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = ""; }; + E6BA18F615214E4184E199B2 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = ""; }; + EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = ""; }; + F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = ""; }; + F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = ""; }; + FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = ""; }; + FC184551A43F44E09E536D29 /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = ""; }; + FE0C26984E50402281861375 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 02B7024F8C4E4D41AE3B203A /* Frameworks */ = { + 085E7F6CFD2B4475AC00D329 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4C26392D2FE69A090015AAF3 /* RTMPHaishinKit in Frameworks */, + 4C26392C2FE69A090015AAF3 /* HaishinKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 4CD553EA1678427BB1BB0478 /* Frameworks */ = { + 521337FFC4E94E5880B1F174 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4C5F8E4C2FE308D700962D65 /* RTMPHaishinKit in Frameworks */, - 4C5F8E4B2FE308D700962D65 /* HaishinKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 146562E9E8F4444781CCEE1C /* MatchLiveTv */ = { + 033861C6E3D248C6A4CEFAF6 /* Products */ = { isa = PBXGroup; children = ( - EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */, - D6725372B2CE473F87167457 /* ApiInstant.swift */, - 2E221E68194C4C92925A9CB0 /* AppConfig.swift */, - 4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */, - F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */, - 8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */, - 806B1034AB5148B4BC55F093 /* TokenStore.swift */, - EC311445F0144B59AE22DA96 /* UserFacingError.swift */, - 8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */, - 0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */, - E3DEAC5182ED489798656300 /* AppContainer.swift */, - 9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */, - 976DD89356184E0694B20CB8 /* SessionCableService.swift */, - D6B453249C904B299178A996 /* AuthRepository.swift */, - 7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */, - ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */, - A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */, - 10C6CAC995404909A95482BF /* SessionRepository.swift */, - 6F05D132A65E45E8AED152F0 /* ScoreController.swift */, - 4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */, - 944972DE154D444781B612ED /* MatchHubFilter.swift */, - 7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */, - 26D7D448D0A34D9384C5E2D3 /* Models.swift */, - DE5A60F7644B4881985D4F15 /* ScoreState.swift */, - 4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */, - 0461F47FF06E487491BA53F3 /* BroadcastVideoOrientation.swift */, - 6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */, - F03F27B7CFAC4095A48C1C83 /* LiveBroadcastEngine.swift */, - B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */, - BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */, - CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */, - 4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */, - 1ED842B98F33423988B13960 /* OverlayRenderer.swift */, - 0A84A2EA29C441EFA31498AD /* OverlayState.swift */, - D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */, - C43F8C92A1094AC39A7FE978 /* SponsorElement.swift */, - 96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */, - 2D0386B94B9042918A68CD76 /* LivePreviewView.swift */, - 584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */, - 3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */, - E770890933A84622ABF4EA9F /* LiveScoreActions.swift */, - 388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */, - A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */, - 316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */, - 7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */, - 372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */, - 3F5EB6A380DC4728866BD42A /* LoginScreen.swift */, - 34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */, - 3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */, - 978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */, - CCC01BBAC06A412296955DB3 /* Routes.swift */, - 1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */, - 08A245DF805C482C89AEB2E4 /* SplashScreen.swift */, - 2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */, - C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */, - AE769961C58E4741B73938B8 /* MatchColors.swift */, - 21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */, - FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */, - 5D813398882A483280580618 /* StepTransmissionScreen.swift */, - 2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */, - 5086F0DA44004334881343ED /* TeamColorPicker.swift */, - A2627CF334644FCA8FD9950B /* WizardComponents.swift */, - 2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */, - 8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */, - 551CCAA3C2F64172A59E278B /* MatchLiveTv/Resources/Info.plist */, + B165DBC9A0DA4C62824D41C3 /* MatchLiveTv.app */, + 5C49570E1FB3460BBEB502FC /* MatchLiveTvTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 631A87449B654E888E88BF77 = { + isa = PBXGroup; + children = ( + 705BD051C8004C5AA632BB29 /* MatchLiveTv */, + 8658B6D49A814C9981463B85 /* MatchLiveTvTests */, + 033861C6E3D248C6A4CEFAF6 /* Products */, + ); + sourceTree = ""; + }; + 705BD051C8004C5AA632BB29 /* MatchLiveTv */ = { + isa = PBXGroup; + children = ( + 24FAE643838541119B344969 /* MatchLiveTvApp.swift */, + 5853956279E94634BFC1660C /* ApiInstant.swift */, + A66D5B274FF34093901B981B /* AppConfig.swift */, + 818D265485A3445DAB7EAC48 /* ColorHex.swift */, + 92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */, + BA461A50E47740818FDC5890 /* MediaUrl.swift */, + 8E7E256AA201434EBD01B7D4 /* TokenStore.swift */, + E6BA18F615214E4184E199B2 /* UserFacingError.swift */, + 9AB5819FE32342F1961F256E /* ApiDtos.swift */, + BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */, + 34DE015C16C54795A496C968 /* AppContainer.swift */, + BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */, + 0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */, + 7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */, + BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */, + D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */, + D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */, + DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */, + 9525D894BB924BFFBC2D244C /* ScoreController.swift */, + E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */, + A264DB29100D404EA570CF47 /* MatchHubFilter.swift */, + B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */, + A2C8DD5093B84D5FBE360C23 /* Models.swift */, + 880F5D2B2B13497891706E0A /* ScoreState.swift */, + 1E128E2B501C45419220F030 /* BroadcastModels.swift */, + BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */, + 46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */, + 5E22842CB3AF4514965D3791 /* LiveBroadcastCoordinator.swift */, + 2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */, + A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */, + FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */, + 18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */, + 2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */, + 677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */, + 3AA971C263254BDC95AC2423 /* OverlayState.swift */, + 9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */, + B5A0F1558C174BD584AE0377 /* SponsorElement.swift */, + 073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */, + 81F07B021AC24DCAA217238C /* LivePreviewView.swift */, + 810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */, + 5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */, + 2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */, + 5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */, + FE0C26984E50402281861375 /* MatchPrimaryButton.swift */, + CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */, + 8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */, + 951726866CB945DD917376C7 /* MatchStatusBadge.swift */, + 136E90D700FD44D89C9D5659 /* LoginScreen.swift */, + 7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */, + 1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */, + 2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */, + D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */, + A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */, + 17256285CF354F0D8E4B33A3 /* SplashScreen.swift */, + 3FBA01162B004E85821F206B /* KeepScreenOn.swift */, + 8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */, + 2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */, + DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */, + 09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */, + 7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */, + E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */, + 2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */, + 573991C190AD43BBBDDD186C /* TeamColorPicker.swift */, + FC184551A43F44E09E536D29 /* WizardComponents.swift */, + F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */, + A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */, + AB5EAF0FD4BE4B14B5B029B8 /* MatchLiveTv/Resources/Info.plist */, ); name = MatchLiveTv; sourceTree = ""; }; - 25CDF42A0993431FA5F457EB = { + 8658B6D49A814C9981463B85 /* MatchLiveTvTests */ = { isa = PBXGroup; children = ( - 146562E9E8F4444781CCEE1C /* MatchLiveTv */, - 4190C9EAA59D4AA882933799 /* MatchLiveTvTests */, - B06BE907209A475AA6898BB6 /* Products */, - ); - sourceTree = ""; - }; - 4190C9EAA59D4AA882933799 /* MatchLiveTvTests */ = { - isa = PBXGroup; - children = ( - 2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */, - 227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */, - F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */, - 94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */, + F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */, + 177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */, + CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */, + EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */, + A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */, + 79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */, ); name = MatchLiveTvTests; sourceTree = ""; }; - B06BE907209A475AA6898BB6 /* Products */ = { - isa = PBXGroup; - children = ( - C3DCC59301E94417BC24492F /* MatchLiveTv.app */, - ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 2F5D6393282640E5A53D4CFC /* MatchLiveTv */ = { + 553940DB807C4DBFB63E9FFE /* MatchLiveTv */ = { isa = PBXNativeTarget; - buildConfigurationList = 56CDE22EB06747D988C0F28D /* Build configuration list for PBXNativeTarget "MatchLiveTv" */; + buildConfigurationList = 4332218393F64E3CA1F31139 /* Build configuration list for PBXNativeTarget "MatchLiveTv" */; buildPhases = ( - A547C0346D1345579D28B86D /* Sources */, - 4CD553EA1678427BB1BB0478 /* Frameworks */, - CA0D00DDC25242438A7EBFEC /* Resources */, + 12E25B1D521C492F8F41CECF /* Sources */, + 085E7F6CFD2B4475AC00D329 /* Frameworks */, + 3944EAB1EB4B4139BBA13CBB /* Resources */, ); buildRules = ( ); @@ -301,40 +313,40 @@ ); name = MatchLiveTv; packageProductDependencies = ( - F652BB68B82C49218CCB7CD0 /* HaishinKit */, - 8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */, + 3242173ACEDC4B288ACD56E4 /* HaishinKit */, + F3539A3AB0894C54992DB7D5 /* RTMPHaishinKit */, ); productName = MatchLiveTv; - productReference = C3DCC59301E94417BC24492F /* MatchLiveTv.app */; + productReference = B165DBC9A0DA4C62824D41C3 /* MatchLiveTv.app */; productType = "com.apple.product-type.application"; }; - 938E427E770D4DFD8E992BFA /* MatchLiveTvTests */ = { + 79FDBB021E5045C5A353AEE8 /* MatchLiveTvTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 7CCBB8C54461461C9BF93E4A /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */; + buildConfigurationList = 6F0E68806C0E4C1FBAC0863B /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */; buildPhases = ( - F741ACE11A14421D99BBA54D /* Sources */, - 02B7024F8C4E4D41AE3B203A /* Frameworks */, + 83261799604545698E663330 /* Sources */, + 521337FFC4E94E5880B1F174 /* Frameworks */, ); buildRules = ( ); dependencies = ( - 0831318C61FF46EDA63542ED /* PBXTargetDependency */, + 89AF4750097043F984332B2B /* PBXTargetDependency */, ); name = MatchLiveTvTests; productName = MatchLiveTvTests; - productReference = ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */; + productReference = 5C49570E1FB3460BBEB502FC /* MatchLiveTvTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - A8764A9D2A234F91B52DDC89 /* Project object */ = { + 72A102A7C8964137A464FF32 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 0; LastSwiftUpdateCheck = 1600; }; - buildConfigurationList = 1B3B522758634988987B6EE5 /* Build configuration list for PBXProject "MatchLiveTv" */; + buildConfigurationList = E2AD215689B3443E85A02B4C /* Build configuration list for PBXProject "MatchLiveTv" */; compatibilityVersion = "Xcode 15.0"; developmentRegion = it; hasScannedForEncodings = 0; @@ -343,158 +355,129 @@ en, Base, ); - mainGroup = 25CDF42A0993431FA5F457EB; + mainGroup = 631A87449B654E888E88BF77; packageReferences = ( - 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */, + 66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */, ); - productRefGroup = B06BE907209A475AA6898BB6 /* Products */; + productRefGroup = 033861C6E3D248C6A4CEFAF6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 2F5D6393282640E5A53D4CFC /* MatchLiveTv */, - 938E427E770D4DFD8E992BFA /* MatchLiveTvTests */, + 553940DB807C4DBFB63E9FFE /* MatchLiveTv */, + 79FDBB021E5045C5A353AEE8 /* MatchLiveTvTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - CA0D00DDC25242438A7EBFEC /* Resources */ = { + 3944EAB1EB4B4139BBA13CBB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - ACBB5846610943D9AE3FD2B8 /* MatchLiveTv/Resources/Assets.xcassets in Resources */, + F05733319E284083A5AA2784 /* MatchLiveTv/Resources/Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - A547C0346D1345579D28B86D /* Sources */ = { + 12E25B1D521C492F8F41CECF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 538CA13BDD594A96B444350E /* MatchLiveTvApp.swift in Sources */, - CD2D69C03B22451E8E75BEE5 /* ApiInstant.swift in Sources */, - B469E3D9850E46698740395B /* AppConfig.swift in Sources */, - 82D2A2AC562647C9BD18055D /* ColorHex.swift in Sources */, - 9DA3E777DC3C46CD9F6C2933 /* DeviceTelemetry.swift in Sources */, - 65DDC9416BE940A889C8647A /* MediaUrl.swift in Sources */, - 020986FB10004F36BB7ED0BD /* TokenStore.swift in Sources */, - A608CDA9340247E688A315F6 /* UserFacingError.swift in Sources */, - 59FAC2E6EDD54B90B41BFD34 /* ApiDtos.swift in Sources */, - 58119B329D05438F84507448 /* MatchLiveAPI.swift in Sources */, - D5399E5D54444A43B1E3919B /* AppContainer.swift in Sources */, - 906FC2F226DA4F958260FEA0 /* ActionCableClient.swift in Sources */, - C580112F135E4134A55E4DC7 /* SessionCableService.swift in Sources */, - DEE3503ADE4E48F48A49001D /* AuthRepository.swift in Sources */, - D40F96BF9A98482D861B8714 /* MatchRepository.swift in Sources */, - D654C3E362EA4786AE9526E5 /* MatchSessionLauncher.swift in Sources */, - 216F56B707484618AE30847E /* ScoreRepository.swift in Sources */, - 9139292098C24797BE1AFB5E /* SessionRepository.swift in Sources */, - 404F2B3994194914994D5998 /* ScoreController.swift in Sources */, - 798F944B9FFA401E8EDD6B28 /* WizardSessionHolder.swift in Sources */, - DFEE71197E4E41CBB835D0BA /* MatchHubFilter.swift in Sources */, - 40D98FC5AA9B4AB28AC96902 /* MatchScoringRules.swift in Sources */, - 8414C3672E11414CB0F980C3 /* Models.swift in Sources */, - 9E6D271CDEDF4484BAD8161E /* ScoreState.swift in Sources */, - 8CF81F29F432430CBD56C405 /* BroadcastModels.swift in Sources */, - 587FE4FFD78E472B84911501 /* BroadcastVideoOrientation.swift in Sources */, - 9DA716746DA24ECCA2182B7A /* LiveBroadcastCoordinator.swift in Sources */, - 6C6F8189C8554FA298D0209D /* LiveBroadcastEngine.swift in Sources */, - B54EEA69C2984D0989DDDC3E /* CompactScoreboardElement.swift in Sources */, - 87528ECDBB644E73A6DD4C4B /* OverlayCanvasRenderer.swift in Sources */, - A0B9F06D1A12410E9C1C361D /* OverlayLogoCache.swift in Sources */, - 45CF302AAFD947ACAD8BDF81 /* OverlayMappings.swift in Sources */, - 3040471A57FA4032BF347025 /* OverlayRenderer.swift in Sources */, - 6AF878B33937472981276ED6 /* OverlayState.swift in Sources */, - 518508A5943C425492A428C5 /* ScoreboardElement.swift in Sources */, - FB14C028958F46EC8A8FAB18 /* SponsorElement.swift in Sources */, - 45DF0F887F314A498AC82C6D /* WatermarkElement.swift in Sources */, - 3478D3DBB96D41D880BCA499 /* LivePreviewView.swift in Sources */, - 467525056A0648EDA1E2A0CD /* BroadcastControlsOverlay.swift in Sources */, - DC75AFA217B5445AA4FA8BCF /* BroadcastScreen.swift in Sources */, - E382BF9CB4274DDA96A10A98 /* LiveScoreActions.swift in Sources */, - F588C2FE35B140418C13963F /* MatchLiveWordmark.swift in Sources */, - 85162099CD364680AA080E20 /* MatchPrimaryButton.swift in Sources */, - 4F6973E4E707495FAED4E582 /* MatchScreenScaffold.swift in Sources */, - 572553E2029047128A322BE0 /* MatchSecondaryButton.swift in Sources */, - 4C8522208CD14180831F780B /* MatchStatusBadge.swift in Sources */, - 100C9225A2EB448C8C3132D4 /* LoginScreen.swift in Sources */, - D81D1E21B0E44F38A8F5C256 /* MatchesScreen.swift in Sources */, - D792D3DE5C984BB880BE19DD /* AppNavHost.swift in Sources */, - B6499B988D134CE78DCFF375 /* ModalRoutes.swift in Sources */, - BACB4C45854B4531AC2742F3 /* Routes.swift in Sources */, - 4BDAEE6159624FACA21E178D /* BroadcastPermissions.swift in Sources */, - 0AF514A970C5405EA7A54950 /* SplashScreen.swift in Sources */, - DAFBA526BBFC4302A851BB93 /* KeepScreenOn.swift in Sources */, - 4131A683E5784D51B19916D5 /* ScreenOrientation.swift in Sources */, - B2A84ED6937E4D6780A8372B /* MatchColors.swift in Sources */, - D7AAD6BB475B4734BA4EC4EA /* StepMatchScreen.swift in Sources */, - A7C1C8C352404A72A688CEF4 /* StepNetworkTestScreen.swift in Sources */, - 9C8922FBA5DD4DCA9885E649 /* StepTransmissionScreen.swift in Sources */, - D687E626CEC44A11B2BE78FB /* TeamBrandingEditor.swift in Sources */, - 279D1B02380D46DF835831B8 /* TeamColorPicker.swift in Sources */, - 475FE75875AC47A58609BF5B /* WizardComponents.swift in Sources */, - A80C909B160C46CDADF1ABF8 /* WizardShellScreen.swift in Sources */, + CCB2F270DD464E608046A60C /* MatchLiveTvApp.swift in Sources */, + B5937876D40F4E21A63DB92A /* ApiInstant.swift in Sources */, + 674D6A9F61D742DF95F93C5B /* AppConfig.swift in Sources */, + 57B21B3B008A4345AEDFD7D2 /* ColorHex.swift in Sources */, + 5C80FF3E9F574F4EA2FDFEFF /* DeviceTelemetry.swift in Sources */, + 639783D4083F4049B5424C52 /* MediaUrl.swift in Sources */, + D6F532927A6345339AD2149D /* TokenStore.swift in Sources */, + E93FDA6122664F67A5448E2D /* UserFacingError.swift in Sources */, + 8DE2FE7CF4A34222B2D3066A /* ApiDtos.swift in Sources */, + E5B931C144DA46819918A5EC /* MatchLiveAPI.swift in Sources */, + D3C0878AD7354D41B540AD5B /* AppContainer.swift in Sources */, + D1334C2CFEC145328396B7EC /* ActionCableClient.swift in Sources */, + A3D8F65EBA8B479485321837 /* SessionCableService.swift in Sources */, + D67C481FC7DE46C6B7AD09C4 /* AuthRepository.swift in Sources */, + 4B1080F3C1354A89A859ACC6 /* MatchRepository.swift in Sources */, + BB73AB6E535F473EA6C1D232 /* MatchSessionLauncher.swift in Sources */, + 95848029AD4D4683B80DEA8C /* ScoreRepository.swift in Sources */, + EF984C15882E4E328BFCDA74 /* SessionRepository.swift in Sources */, + CEACB53FE13049B9959896F1 /* ScoreController.swift in Sources */, + 01BBD9C8228A4654BE791A09 /* WizardSessionHolder.swift in Sources */, + CBEE59E2E2DE4C058FF42022 /* MatchHubFilter.swift in Sources */, + E054E5E1703A4876B4E13F64 /* MatchScoringRules.swift in Sources */, + 9102501263E1477BAA6DEDD1 /* Models.swift in Sources */, + 4958D62D7A8944EAB3E54F18 /* ScoreState.swift in Sources */, + E75B414604CB429A9CCE797C /* BroadcastModels.swift in Sources */, + 9EAD5508F44243409C697F41 /* BroadcastOrientationPolicy.swift in Sources */, + 3C4DD1CE0BFA4CD9928234CD /* BroadcastVideoOrientation.swift in Sources */, + C9B2434416364C4BB263BB3F /* LiveBroadcastCoordinator.swift in Sources */, + B86EA543F6114B198D334FAF /* LiveBroadcastEngine.swift in Sources */, + 0712F271074B47B992C54B02 /* CompactScoreboardElement.swift in Sources */, + 266CD29FA3C048CC9413460A /* OverlayCanvasRenderer.swift in Sources */, + EA206B930BD448D3AC1E2578 /* OverlayLogoCache.swift in Sources */, + 44519A77D19C46218318593E /* OverlayMappings.swift in Sources */, + 3628957732604F1597784B04 /* OverlayRenderer.swift in Sources */, + 38938AED35E94A99A4921FF3 /* OverlayState.swift in Sources */, + 0747277C94AD48939CBAE80B /* ScoreboardElement.swift in Sources */, + 306216CA292D4974B612AC06 /* SponsorElement.swift in Sources */, + 830325BE85674DD6A31CE672 /* WatermarkElement.swift in Sources */, + 89F47A469F6044B9A92D647A /* LivePreviewView.swift in Sources */, + 8CDFA36E735A4A94A5A60DBF /* BroadcastControlsOverlay.swift in Sources */, + 9770A3D9677F42AC942E98A4 /* BroadcastScreen.swift in Sources */, + 832E24DBB2844E9895717F6F /* LiveScoreActions.swift in Sources */, + F5B6F5CD2D9245BDB8E81055 /* MatchLiveWordmark.swift in Sources */, + ABE74DCBA6804EDEB5398F8F /* MatchPrimaryButton.swift in Sources */, + 2757AF56199F46A49036C307 /* MatchScreenScaffold.swift in Sources */, + 83E38DFBBFFF4412BF2ABEE9 /* MatchSecondaryButton.swift in Sources */, + AB3DF660C79648CFAECB03CF /* MatchStatusBadge.swift in Sources */, + C0CA6EBDA3E84641AFEC3A12 /* LoginScreen.swift in Sources */, + 4236C0FB1B10435EB03C39DF /* MatchesScreen.swift in Sources */, + 49586D0F50AA4BE692FDCF3F /* AppNavHost.swift in Sources */, + EB3C108379594E7EBCBCE016 /* ModalRoutes.swift in Sources */, + 8DB866F0F33A4F3BAA074660 /* Routes.swift in Sources */, + BD2628C89CAD450ABF613183 /* BroadcastPermissions.swift in Sources */, + 815C82E2C5C54746ABF73BEE /* SplashScreen.swift in Sources */, + EED290CEF4A141608F7BE515 /* KeepScreenOn.swift in Sources */, + 7F7C79F0C4EA4B06A5A2E4BA /* ScreenOrientation.swift in Sources */, + BAE5062DD4B749E6BC569CCA /* ShareSheet.swift in Sources */, + B578A98681A8413DBFEF58CA /* MatchColors.swift in Sources */, + 1DE6EABFB67F4046957C9B88 /* StepMatchScreen.swift in Sources */, + 9EF4BA3F81C341B4ACAB643F /* StepNetworkTestScreen.swift in Sources */, + F212F8F1DA0C42619D13D30E /* StepTransmissionScreen.swift in Sources */, + 1C71C5745AF541A793FBD415 /* TeamBrandingEditor.swift in Sources */, + 7DC17CB11FC246378F6624C0 /* TeamColorPicker.swift in Sources */, + 3477E99DCB6843BB8037EB21 /* WizardComponents.swift in Sources */, + 9A208AE54E434798BD5DF06F /* WizardShellScreen.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F741ACE11A14421D99BBA54D /* Sources */ = { + 83261799604545698E663330 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 82D2D8D7BBF642EE8E042CDF /* ApiInstantTests.swift in Sources */, - 67BBA82C7CD04A9B85B08F23 /* MatchScoringRulesTests.swift in Sources */, - 6868D193A5874D62BD640F8E /* ScoreActionDecodeTests.swift in Sources */, - 7ACD03602B744B4982DB542C /* ScoreControllerTests.swift in Sources */, + DFC67AC6380B41BC8A8CE866 /* ApiInstantTests.swift in Sources */, + F36DADA0DA9245A48E3DDB29 /* BroadcastOrientationPolicyTests.swift in Sources */, + C1D94B81E44C4BFD8351B165 /* LiveBroadcastCoordinatorTests.swift in Sources */, + 82D6D70F2E554414961F27C3 /* MatchScoringRulesTests.swift in Sources */, + 9EDC5C176D2E40F29455CCEF /* ScoreActionDecodeTests.swift in Sources */, + 07C244E32A4D48A185963C6C /* ScoreControllerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 0831318C61FF46EDA63542ED /* PBXTargetDependency */ = { + 89AF4750097043F984332B2B /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 2F5D6393282640E5A53D4CFC /* MatchLiveTv */; - targetProxy = C081F7F08D6748AB8C4B8DFB /* PBXContainerItemProxy */; + target = 553940DB807C4DBFB63E9FFE /* MatchLiveTv */; + targetProxy = E0F627D71C6349F1857AFED5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 5C13053150F24474B8F31912 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 5EAD837968924A88BDE4D77F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - API_BASE_URL = "https://www.matchlivetv.it"; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 2.0.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_ENABLE_EXPLICIT_MODULES = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 978E94D1AF904CFE86165956 /* Debug */ = { + 07CBA422677E4E909A377797 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; @@ -510,9 +493,25 @@ SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv"; }; + name = Release; + }; + 38453F3C33434DFD8F94862C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 5C7F305207F64DC79FEA28D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + SWIFT_VERSION = 5.0; + }; name = Debug; }; - 9A8D41AAA16F4A448DFA4ED9 /* Debug */ = { + A1EF7CCA2C8D46938E2764C4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { API_BASE_URL = "https://www.matchlivetv.it"; @@ -539,15 +538,33 @@ }; name = Debug; }; - B9E60323FF35415C9388228C /* Debug */ = { + BE33A670C527464E85B1F231 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + API_BASE_URL = "https://www.matchlivetv.it"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 21; + DEVELOPMENT_TEAM = S8Q9TWBRG5; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Debug; + name = Release; }; - CB913E17F096470F990A496F /* Release */ = { + F6DDD32ED386440687F72916 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; @@ -563,34 +580,34 @@ SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv"; }; - name = Release; + name = Debug; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 1B3B522758634988987B6EE5 /* Build configuration list for PBXProject "MatchLiveTv" */ = { + 4332218393F64E3CA1F31139 /* Build configuration list for PBXNativeTarget "MatchLiveTv" */ = { isa = XCConfigurationList; buildConfigurations = ( - B9E60323FF35415C9388228C /* Debug */, - 5C13053150F24474B8F31912 /* Release */, + A1EF7CCA2C8D46938E2764C4 /* Debug */, + BE33A670C527464E85B1F231 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 56CDE22EB06747D988C0F28D /* Build configuration list for PBXNativeTarget "MatchLiveTv" */ = { + 6F0E68806C0E4C1FBAC0863B /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 9A8D41AAA16F4A448DFA4ED9 /* Debug */, - 5EAD837968924A88BDE4D77F /* Release */, + F6DDD32ED386440687F72916 /* Debug */, + 07CBA422677E4E909A377797 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7CCBB8C54461461C9BF93E4A /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */ = { + E2AD215689B3443E85A02B4C /* Build configuration list for PBXProject "MatchLiveTv" */ = { isa = XCConfigurationList; buildConfigurations = ( - 978E94D1AF904CFE86165956 /* Debug */, - CB913E17F096470F990A496F /* Release */, + 5C7F305207F64DC79FEA28D2 /* Debug */, + 38453F3C33434DFD8F94862C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -598,7 +615,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */ = { + 66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/shogo4405/HaishinKit.swift"; requirement = { @@ -609,17 +626,17 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */ = { + 3242173ACEDC4B288ACD56E4 /* HaishinKit */ = { isa = XCSwiftPackageProductDependency; - package = 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */; - productName = RTMPHaishinKit; - }; - F652BB68B82C49218CCB7CD0 /* HaishinKit */ = { - isa = XCSwiftPackageProductDependency; - package = 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */; + package = 66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */; productName = HaishinKit; }; + F3539A3AB0894C54992DB7D5 /* RTMPHaishinKit */ = { + isa = XCSwiftPackageProductDependency; + package = 66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */; + productName = RTMPHaishinKit; + }; /* End XCSwiftPackageProductDependency section */ }; - rootObject = A8764A9D2A234F91B52DDC89 /* Project object */; + rootObject = 72A102A7C8964137A464FF32 /* Project object */; } diff --git a/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme b/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme index 2974b5a..114461b 100644 --- a/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme +++ b/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme @@ -3,28 +3,28 @@ - + - + - + - + - + diff --git a/native/ios/MatchLiveTv/Data/API/ApiDtos.swift b/native/ios/MatchLiveTv/Data/API/ApiDtos.swift index 61fbbcd..ce5199d 100644 --- a/native/ios/MatchLiveTv/Data/API/ApiDtos.swift +++ b/native/ios/MatchLiveTv/Data/API/ApiDtos.swift @@ -201,6 +201,29 @@ struct SetPartialDto: Codable { let set: Int? let home: Int? let away: Int? + + init(set: Int?, home: Int?, away: Int?) { + self.set = set + self.home = home + self.away = away + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + set = Self.decodeInt(container, key: .set) + home = Self.decodeInt(container, key: .home) + away = Self.decodeInt(container, key: .away) + } + + enum CodingKeys: String, CodingKey { + case set, home, away + } + + private static func decodeInt(_ container: KeyedDecodingContainer, key: CodingKeys) -> Int? { + if let value = try? container.decode(Int.self, forKey: key) { return value } + if let text = try? container.decode(String.self, forKey: key), let value = Int(text) { return value } + return nil + } } struct ScoreStateDto: Decodable { diff --git a/native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift b/native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift index 8014028..a79de5b 100644 --- a/native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift +++ b/native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift @@ -18,6 +18,7 @@ final class SessionCableService: ObservableObject { private var deviceRole = "camera" func connect(sessionId: String, accessToken: String, deviceRole: String = "camera") { + disconnect() self.sessionId = sessionId self.accessToken = accessToken self.deviceRole = deviceRole @@ -29,9 +30,12 @@ final class SessionCableService: ObservableObject { func disconnect() { intentionalDisconnect = true reconnectTask?.cancel() + reconnectTask = nil client?.disconnect() client = nil connected = false + sessionId = nil + accessToken = nil } func sendScoreUpdate(_ score: ScoreState) { @@ -84,8 +88,8 @@ extension SessionCableService: ActionCableClient.Listener { case "score_update": onScoreUpdate?(ScoreState.fromCablePayload(payload)) case "command": - let command = payload["command"] as? String ?? "" - switch command { + let action = payload["action"] as? String ?? "" + switch action { case "pause_stream": onPauseStream?() case "resume_stream": onResumeStream?() case "stop_stream": onStopStream?() diff --git a/native/ios/MatchLiveTv/Data/Scoring/ScoreController.swift b/native/ios/MatchLiveTv/Data/Scoring/ScoreController.swift index 6a8eb53..3560e33 100644 --- a/native/ios/MatchLiveTv/Data/Scoring/ScoreController.swift +++ b/native/ios/MatchLiveTv/Data/Scoring/ScoreController.swift @@ -18,13 +18,24 @@ final class ScoreController: ObservableObject { } func bind(sessionId: String, initial: ScoreState?) { + let sessionChanged = self.sessionId != sessionId self.sessionId = sessionId - if let initial { score = initial } + if sessionChanged { + syncTask?.cancel() + syncGeneration += 1 + lastActionError = nil + suppressRemoteUntil = .distantPast + score = initial ?? ScoreState() + } else if let initial { + score = initial + } } func applyRemote(_ remote: ScoreState) { if remote == score { return } - if Date() < suppressRemoteUntil && remote.progressKey() <= score.progressKey() { return } + // Non accettare mai uno stato "più vecchio" (es. polling che sovrascrive close_set). + if remote.progressKey() < score.progressKey() { return } + if Date() < suppressRemoteUntil, remote.progressKey() == score.progressKey() { return } score = remote } @@ -85,23 +96,55 @@ final class ScoreController: ObservableObject { /// Chiude il set corrente tramite `close_set` sul motore backend. func closeSetAsync() async -> Bool { + lastActionError = nil syncTask?.cancel() syncGeneration += 1 guard let sessionId else { return false } + + let optimistic = closedSetState(from: score) + score = optimistic + sessionCable.sendScoreUpdate(optimistic) + do { if let updated = try await scoreRepository.applyScoreAction(sessionId: sessionId, action: "close_set") { - suppressRemoteUntil = Date().addingTimeInterval(1.2) + suppressRemoteUntil = Date().addingTimeInterval(3) score = updated sessionCable.sendScoreUpdate(updated) return true } - return false + suppressRemoteUntil = Date().addingTimeInterval(3) + return true } catch { lastActionError = UserFacingError.message(for: error) return false } } + private func closedSetState(from current: ScoreState) -> ScoreState { + let homeWon = current.homePoints > current.awayPoints + let awayWon = current.awayPoints > current.homePoints + var partials = current.setPartials + if current.homePoints > 0 || current.awayPoints > 0 { + partials.append(SetPartial(set: current.currentSet, home: current.homePoints, away: current.awayPoints)) + } + return ScoreState( + homeSets: homeWon ? current.homeSets + 1 : current.homeSets, + awaySets: awayWon ? current.awaySets + 1 : current.awaySets, + homePoints: 0, + awayPoints: 0, + currentSet: current.currentSet + 1, + setPartials: partials, + timeoutHome: false, + timeoutAway: false, + boardType: current.boardType, + period: current.period, + periodLabel: current.periodLabel, + clockSecs: current.clockSecs, + clockRunning: current.clockRunning, + overtime: current.overtime + ) + } + private func updateScore(_ transform: (inout ScoreState) -> Void) { var next = score transform(&next) @@ -143,28 +186,7 @@ final class ScoreController: ObservableObject { } func closeSet() { - let current = score - let homeWon = current.homePoints > current.awayPoints - var partials = current.setPartials - if current.homePoints > 0 || current.awayPoints > 0 { - partials.append(SetPartial(set: current.currentSet, home: current.homePoints, away: current.awayPoints)) - } - score = ScoreState( - homeSets: homeWon ? current.homeSets + 1 : current.homeSets, - awaySets: homeWon ? current.awaySets : current.awaySets + 1, - homePoints: 0, - awayPoints: 0, - currentSet: current.currentSet + 1, - setPartials: partials, - timeoutHome: false, - timeoutAway: false, - boardType: current.boardType, - period: current.period, - periodLabel: current.periodLabel, - clockSecs: current.clockSecs, - clockRunning: current.clockRunning, - overtime: current.overtime - ) + score = closedSetState(from: score) schedulePush() } diff --git a/native/ios/MatchLiveTv/Domain/ScoreState.swift b/native/ios/MatchLiveTv/Domain/ScoreState.swift index b3afba1..2a1eb5a 100644 --- a/native/ios/MatchLiveTv/Domain/ScoreState.swift +++ b/native/ios/MatchLiveTv/Domain/ScoreState.swift @@ -51,12 +51,19 @@ struct ScoreState: Equatable, Sendable { } func progressKey() -> Int64 { - Int64(currentSet) * 10_000_000_000 - + Int64(homeSets) * 1_000_000 - + Int64(awaySets) * 100_000 - + Int64(homePoints) * 1_000 - + Int64(awayPoints) - + Int64(clockSecs) + var key = Int64(currentSet) * 10_000_000 + key &+= Int64(homeSets) * 100_000 + key &+= Int64(awaySets) * 1_000 + key &+= Int64(homePoints) * 10 + key &+= Int64(awayPoints) + key &+= Int64(clockSecs) + key &+= Int64(setPartials.count) * 1_000_000 + for partial in setPartials { + key &+= Int64(partial.set) * 10_000 + key &+= Int64(partial.home) * 100 + key &+= Int64(partial.away) + } + return key } static let periodBoards: Set = ["basket", "timed"] diff --git a/native/ios/MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift b/native/ios/MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift new file mode 100644 index 0000000..3634bb9 --- /dev/null +++ b/native/ios/MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift @@ -0,0 +1,28 @@ +import AVFoundation + +/// Regole orientamento in diretta (allineate al debounce Android). +enum BroadcastOrientationPolicy { + /// Attesa prima di applicare rotazione al mixer (evita burst da giro iPad). + static let debounceNanoseconds: UInt64 = 350_000_000 + /// Finestra in cui ignorare `closed` RTMP dopo rotazione encoder. + static let suppressDisconnectSeconds: TimeInterval = 1.0 + + static func shouldApplyMixerOrientation( + new: AVCaptureVideoOrientation, + previous: AVCaptureVideoOrientation?, + landscapeLocked: Bool + ) -> Bool { + guard isApplicableForBroadcast(new, landscapeLocked: landscapeLocked) else { return false } + guard let previous else { return true } + return new != previous + } + + /// In diretta landscape ignoriamo portrait/unknown: evita glitch quando il sensore passa da verticale. + static func isApplicableForBroadcast( + _ orientation: AVCaptureVideoOrientation, + landscapeLocked: Bool + ) -> Bool { + guard landscapeLocked else { return true } + return orientation == .landscapeLeft || orientation == .landscapeRight + } +} diff --git a/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift b/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift index 256eb09..13a275f 100644 --- a/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift +++ b/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift @@ -1,15 +1,16 @@ import AVFoundation +import HaishinKit import UIKit -/// Allinea l'orientamento del sensore camera all'interfaccia (broadcast sempre landscape). +/// Orientamento camera allineato a HaishinKit DeviceUtil (broadcast landscape su iPad). enum BroadcastVideoOrientation { @MainActor static func captureOrientation() -> AVCaptureVideoOrientation { - if let scene = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .first(where: { $0.activationState == .foregroundActive }) { - return captureOrientation(for: scene.interfaceOrientation) + if let scene = activeWindowScene(), + let orientation = DeviceUtil.videoOrientation(by: scene.interfaceOrientation) { + return orientation } + // BroadcastScreen forza landscapeRight. return .landscapeRight } @@ -19,13 +20,10 @@ enum BroadcastVideoOrientation { return orientation == .portrait || orientation == .portraitUpsideDown } - private static func captureOrientation(for orientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation { - switch orientation { - case .portrait: return .portrait - case .portraitUpsideDown: return .portraitUpsideDown - case .landscapeLeft: return .landscapeLeft - case .landscapeRight: return .landscapeRight - default: return .landscapeRight - } + @MainActor + private static func activeWindowScene() -> UIWindowScene? { + UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first(where: { $0.activationState == .foregroundActive }) } } diff --git a/native/ios/MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift b/native/ios/MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift index d74b735..5fcc2fc 100644 --- a/native/ios/MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift +++ b/native/ios/MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift @@ -6,6 +6,8 @@ final class LiveBroadcastCoordinator: ObservableObject { let engine = LiveBroadcastEngine() @Published private(set) var metrics = BroadcastMetrics() + private(set) var activeBroadcastSessionId: String? + init() { engine.setMetricsListener { [weak self] value in Task { @MainActor in self?.metrics = value } @@ -22,24 +24,56 @@ final class LiveBroadcastCoordinator: ObservableObject { try? session.setActive(true) } - func startBroadcast(config: BroadcastConfig) async throws { + func waitForPreviewSurface() async -> Bool { + await engine.waitForPreviewSurface() + } + + func prepareBroadcast(sessionId: String, config: BroadcastConfig, paused: Bool) async throws { configureBackgroundAudio() - try await engine.startBroadcast(config: config) + if Self.shouldStopPreviousSession(active: activeBroadcastSessionId, incoming: sessionId) { + await engine.stopBroadcast(suppressDisconnectError: true) + } + activeBroadcastSessionId = sessionId + if paused { + try await engine.preparePreview(config: config) + _ = await engine.waitForPreviewReady() + } else { + try await engine.prepareForBroadcast(config: config) + } + } + + func startBroadcast(sessionId: String, config: BroadcastConfig) async throws { + try await prepareBroadcast(sessionId: sessionId, config: config, paused: false) } func pauseBroadcast() async { - await engine.pauseBroadcast() + await engine.pauseBroadcast(suppressDisconnectError: true) } func resumeBroadcast(config: BroadcastConfig) async throws { try await engine.resumeBroadcast(config: config) } - func stopBroadcast() async { - await engine.stopBroadcast() + /// Ferma RTMP solo se la sessione indicata è ancora quella attiva (evita race tra dirette). + func stopBroadcast(sessionId: String) async { + guard activeBroadcastSessionId == sessionId else { return } + activeBroadcastSessionId = nil + await engine.stopBroadcast(suppressDisconnectError: true) + } + + /// Chiusura cover / uscita forzata: ferma sempre lo stream corrente. + func forceStopBroadcast() async { + activeBroadcastSessionId = nil + await engine.stopBroadcast(suppressDisconnectError: true) } func release() async { - await engine.release() + await forceStopBroadcast() + } + + /// `true` solo quando si passa da una diretta già attiva a un'altra sessione (non al primo avvio). + nonisolated static func shouldStopPreviousSession(active: String?, incoming: String) -> Bool { + guard let active else { return false } + return active != incoming } } diff --git a/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift b/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift index 34f2df3..91dd04b 100644 --- a/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift +++ b/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift @@ -39,6 +39,11 @@ final class LiveBroadcastEngine: ObservableObject { private var previewAttached = false private var publishPending = false private var publishInFlight = false + private var broadcastGeneration = 0 + private var suppressDisconnectError = false + private var orientationTransitionUntil: Date = .distantPast + private var lastAppliedCaptureOrientation: AVCaptureVideoOrientation? + private var orientationDebounceTask: Task? private var rtmpSession: (any Session)? private static var rtmpFactoryRegistered = false @@ -60,14 +65,38 @@ final class LiveBroadcastEngine: ObservableObject { /// Anteprima locale: output del mixer (come HaishinKit PublishViewModel), non dello stream RTMP. func bindPreview(to view: MTHKView) async { previewView = view - guard pipelineConfigured, !previewAttached else { return } - await mixer.addOutput(view) + await attachPreviewIfNeeded() + } + + private func attachPreviewIfNeeded() async { + guard let previewView, pipelineConfigured, !previewAttached else { return } + await mixer.addOutput(previewView) previewAttached = true if publishPending { await startPendingPublish() } } + /// Attende che SwiftUI monti la surface di anteprima (MTHKView). + func waitForPreviewSurface(maxAttempts: Int = 100) async -> Bool { + for _ in 0.. Bool { + for _ in 0.. MTHKView { let view = MTHKView(frame: .zero) - view.videoGravity = .resizeAspectFill + // resizeAspect: mostra tutto il frame 16:9 trasmesso (resizeAspectFill taglia i lati su iPad). + view.videoGravity = .resizeAspect view.isUserInteractionEnabled = false Task { await engine.bindPreview(to: view) } return view } - func updateUIView(_ uiView: MTHKView, context: Context) {} + func updateUIView(_ uiView: MTHKView, context: Context) { + Task { await engine.bindPreview(to: uiView) } + } } diff --git a/native/ios/MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift b/native/ios/MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift index 3c41464..c2d0810 100644 --- a/native/ios/MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift +++ b/native/ios/MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift @@ -130,6 +130,8 @@ struct BroadcastControlsOverlay: View { ) } } + .frame(width: sideToolbarWidth) + .allowsHitTesting(true) } private var rightToolbar: some View { @@ -449,6 +451,7 @@ private struct SideIconButton: View { .font(.system(size: 16, weight: .semibold)) .foregroundStyle(enabled ? .white : MatchColors.textSecondary) .frame(width: iconButtonSize, height: iconButtonSize) + .contentShape(Rectangle()) .background(backgroundColor, in: RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) diff --git a/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift b/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift index 2ea5f72..1b51047 100644 --- a/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift +++ b/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift @@ -25,12 +25,21 @@ struct BroadcastScreen: View { @State private var controlsVisible = true @State private var logoReady = 0 @State private var pauseInFlight = false + @State private var resumeInFlight = false @State private var deviceHealth = DeviceTelemetry.snapshot() @State private var snackbarMessage: String? + @State private var shareItem: ShareItem? + @State private var bootstrapGeneration = 0 var body: some View { ZStack { Color.black.ignoresSafeArea() + if permissions.allGranted { + LivePreviewView(engine: broadcastCoordinator.engine) + .ignoresSafeArea() + .allowsHitTesting(false) + .opacity(loading || session == nil || match == nil ? 0 : 1) + } if loading { ProgressView().tint(MatchColors.primaryRed) } else if !permissions.allGranted { @@ -46,9 +55,6 @@ struct BroadcastScreen: View { } .padding(24) } else if let session, let match { - LivePreviewView(engine: broadcastCoordinator.engine) - .ignoresSafeArea() - .allowsHitTesting(false) broadcastOverlay(session: session, match: match) .id(scoreController.score.progressKey()) .zIndex(1) @@ -59,22 +65,47 @@ struct BroadcastScreen: View { .onAppear { UIDevice.current.isBatteryMonitoringEnabled = true } - .onDisappear { - Task { await teardown() } - } .task { await permissions.refresh() if !permissions.allGranted { await permissions.requestAll() } } - .task(id: permissions.allGranted) { + .task(id: "\(sessionId)-\(permissions.allGranted)") { guard permissions.allGranted else { loading = false return } await bootstrap() } + .task(id: sessionId) { + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 4_000_000_000) + if let remote = try? await container.sessionRepository.fetchSession(id: sessionId).score { + container.scoreController.applyRemote(remote) + } + } + } + .task(id: sessionId) { + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 2_000_000_000) + deviceHealth = DeviceTelemetry.snapshot() + } + } + .task(id: sessionId) { + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 10_000_000_000) + let health = DeviceTelemetry.snapshot() + let metrics = container.broadcastCoordinator.metrics + try? await container.sessionRepository.postTelemetry( + sessionId: sessionId, + health: health, + currentBitrate: metrics.bitrateKbps * 1000, + targetBitrate: session?.targetBitrate, + fps: metrics.fps + ) + } + } .onChange(of: scoreController.score) { _ in updateOverlay() } .onChange(of: scoreController.lastActionError) { message in if let message { snackbarMessage = message } @@ -90,6 +121,11 @@ struct BroadcastScreen: View { } message: { Text(error ?? "") } + .sheet(item: $shareItem) { item in + ShareSheet(items: item.items, subject: item.subject) { + shareItem = nil + } + } .background { if let match { ScoreDialogRouter( @@ -215,7 +251,7 @@ struct BroadcastScreen: View { onTerminate: { Task { await stopStream() } }, onShareLive: { shareLiveLink(session: session, subject: shareSubject) }, onShareRegia: { shareRegiaLink(subject: shareSubject) }, - shareLiveEnabled: session.watchShareUrl() != nil, + shareLiveEnabled: true, fps: metrics.fps, targetFps: session.targetFps, bitrateKbps: metrics.bitrateKbps, @@ -229,7 +265,8 @@ struct BroadcastScreen: View { rules: MatchScoringContext(match: match), scoreController: scoreController, dialogHost: scoreDialogHost, - onStopStream: { await stopStream() } + onStopStream: { await stopStream() }, + onScoreboardChanged: { updateOverlay() } ) } @@ -254,58 +291,62 @@ struct BroadcastScreen: View { } private func shareLiveLink(session: StreamSession, subject: String) { - guard let urlString = session.watchShareUrl(), let url = URL(string: urlString) else { + let urlString = session.watchShareUrl() ?? "\(AppConfig.apiBaseUrl)/live/\(session.id)" + guard let url = URL(string: urlString) else { snackbarMessage = "Link diretta non ancora disponibile" return } - presentShare(items: [url], subject: subject) + shareItem = ShareItem(items: [url], subject: subject) } private func shareRegiaLink(subject: String) { - Task { + Task { @MainActor in do { let urlString = try await container.sessionRepository.createRegiaLink(sessionId: sessionId) - guard let url = URL(string: urlString) else { return } - presentShare(items: [url], subject: "Link regia — \(subject)") + guard let url = URL(string: urlString) else { + snackbarMessage = "Link regia non valido" + return + } + shareItem = ShareItem(items: [url], subject: "Link regia — \(subject)") } catch { snackbarMessage = UserFacingError.message(for: error) ?? "Errore link regia" } } } - private func presentShare(items: [Any], subject: String) { - guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let root = scene.windows.first?.rootViewController else { return } - let controller = UIActivityViewController(activityItems: items, applicationActivities: nil) - controller.setValue(subject, forKey: "subject") - root.present(controller, animated: true) - } - private func bootstrap() async { + bootstrapGeneration += 1 + let generation = bootstrapGeneration loading = true error = nil - await container.broadcastCoordinator.stopBroadcast() + container.sessionCable.disconnect() do { let loaded = try await container.sessionRepository.fetchSession(id: sessionId) + guard generation == bootstrapGeneration else { return } let loadedMatch = try await container.matchRepository.fetchMatch(matchId: loaded.matchId) + guard generation == bootstrapGeneration else { return } session = loaded match = loadedMatch container.scoreController.bind(sessionId: sessionId, initial: loaded.score) wireCable() await preloadLogos(for: loadedMatch) - loading = false updateOverlay() - try? await Task.sleep(nanoseconds: 100_000_000) + loading = false + guard generation == bootstrapGeneration else { return } + guard await container.broadcastCoordinator.waitForPreviewSurface() else { + throw APIError.http(500, "Anteprima camera non pronta") + } + guard generation == bootstrapGeneration else { return } if let url = loaded.rtmpIngestUrl, !url.isEmpty { let config = broadcastConfig(for: loaded, rtmpUrl: url) - if loaded.isPaused { - try await container.broadcastCoordinator.engine.preparePreview(config: config) - } else { - try await container.broadcastCoordinator.engine.prepareForBroadcast(config: config) - } + try await container.broadcastCoordinator.prepareBroadcast( + sessionId: sessionId, + config: config, + paused: loaded.isPaused + ) } - startPolling() } catch { + guard generation == bootstrapGeneration else { return } if let message = UserFacingError.message(for: error) { self.error = message } @@ -319,11 +360,15 @@ struct BroadcastScreen: View { container?.scoreController.applyRemote(remote) } container.sessionCable.onPauseStream = { [weak container] in - Task { @MainActor in await container?.broadcastCoordinator.pauseBroadcast() } + Task { @MainActor in + guard let container, !pauseInFlight else { return } + await applyRemotePause(container: container) + } } container.sessionCable.onResumeStream = { [weak container] in Task { @MainActor in - await resumeStream() + guard let container, !resumeInFlight else { return } + await applyRemoteResume(container: container) } } container.sessionCable.onStopStream = { [weak container] in @@ -381,48 +426,26 @@ struct BroadcastScreen: View { container.broadcastCoordinator.updateOverlay(state) } - private func startPolling() { - Task { - while !Task.isCancelled { - try? await Task.sleep(nanoseconds: 4_000_000_000) - if let remote = try? await container.sessionRepository.fetchSession(id: sessionId).score { - container.scoreController.applyRemote(remote) - } - } - } - Task { - while !Task.isCancelled { - try? await Task.sleep(nanoseconds: 2_000_000_000) - deviceHealth = DeviceTelemetry.snapshot() - } - } - Task { - while !Task.isCancelled { - try? await Task.sleep(nanoseconds: 10_000_000_000) - let health = DeviceTelemetry.snapshot() - let metrics = container.broadcastCoordinator.metrics - try? await container.sessionRepository.postTelemetry( - sessionId: sessionId, - health: health, - currentBitrate: metrics.bitrateKbps * 1000, - targetBitrate: session?.targetBitrate, - fps: metrics.fps - ) - } - } - } - private func pauseStream() { guard !pauseInFlight else { return } pauseInFlight = true Task { - _ = try? await container.sessionRepository.pauseSession(id: sessionId) - await container.broadcastCoordinator.pauseBroadcast() - pauseInFlight = false + defer { pauseInFlight = false } + do { + let updated = try await container.sessionRepository.pauseSession(id: sessionId) + session = updated + await container.broadcastCoordinator.pauseBroadcast() + snackbarMessage = "Diretta in pausa" + } catch { + snackbarMessage = UserFacingError.message(for: error) ?? "Pausa non riuscita" + } } } private func resumeStream() async { + guard !resumeInFlight else { return } + resumeInFlight = true + defer { resumeInFlight = false } do { var updated = try await container.sessionRepository.resumeSession(id: sessionId) session = updated @@ -431,10 +454,38 @@ struct BroadcastScreen: View { try await container.broadcastCoordinator.resumeBroadcast(config: config) updated = try await container.sessionRepository.fetchSession(id: sessionId) session = updated + snackbarMessage = "Diretta ripresa" } catch { - if let message = UserFacingError.message(for: error) { - self.error = message - } + snackbarMessage = UserFacingError.message(for: error) ?? "Ripresa non riuscita" + } + } + + /// Pausa dalla regia / echo cable: solo RTMP locale, senza PATCH pause. + private func applyRemotePause(container: AppContainer) async { + guard !pauseInFlight else { return } + pauseInFlight = true + defer { pauseInFlight = false } + await container.broadcastCoordinator.pauseBroadcast() + if let fetched = try? await container.sessionRepository.fetchSession(id: sessionId) { + session = fetched + } + snackbarMessage = "Pausa dalla regia" + } + + /// Ripresa dalla regia / echo cable: solo RTMP locale, senza PATCH resume. + private func applyRemoteResume(container: AppContainer) async { + guard !resumeInFlight else { return } + resumeInFlight = true + defer { resumeInFlight = false } + do { + let current = try await container.sessionRepository.fetchSession(id: sessionId) + session = current + guard let url = current.rtmpIngestUrl, !url.isEmpty else { return } + let config = broadcastConfig(for: current, rtmpUrl: url) + try await container.broadcastCoordinator.resumeBroadcast(config: config) + snackbarMessage = "Diretta ripresa" + } catch { + snackbarMessage = UserFacingError.message(for: error) ?? "Ripresa RTMP non riuscita" } } @@ -456,7 +507,11 @@ struct BroadcastScreen: View { } private func teardown() async { + container.sessionCable.onScoreUpdate = nil + container.sessionCable.onPauseStream = nil + container.sessionCable.onResumeStream = nil + container.sessionCable.onStopStream = nil container.sessionCable.disconnect() - await container.broadcastCoordinator.stopBroadcast() + await container.broadcastCoordinator.stopBroadcast(sessionId: sessionId) } } diff --git a/native/ios/MatchLiveTv/UI/Broadcast/LiveScoreActions.swift b/native/ios/MatchLiveTv/UI/Broadcast/LiveScoreActions.swift index 8c10547..6ee266f 100644 --- a/native/ios/MatchLiveTv/UI/Broadcast/LiveScoreActions.swift +++ b/native/ios/MatchLiveTv/UI/Broadcast/LiveScoreActions.swift @@ -43,17 +43,20 @@ final class LiveScoreActions { private let scoreController: ScoreController private let dialogHost: LiveScoreDialogHost private let onStopStream: () async -> Void + private let onScoreboardChanged: () -> Void init( rules: MatchScoringContext, scoreController: ScoreController, dialogHost: LiveScoreDialogHost, - onStopStream: @escaping () async -> Void + onStopStream: @escaping () async -> Void, + onScoreboardChanged: @escaping () -> Void = {} ) { self.rules = rules self.scoreController = scoreController self.dialogHost = dialogHost self.onStopStream = onStopStream + self.onScoreboardChanged = onScoreboardChanged } func afterPointChange(score: ScoreState) async { @@ -74,6 +77,7 @@ final class LiveScoreActions { ) if close { guard await scoreController.closeSetAsync() else { return } + onScoreboardChanged() await afterCloseSet() } } @@ -92,6 +96,7 @@ final class LiveScoreActions { if !ok { return } } guard await scoreController.closeSetAsync() else { return } + onScoreboardChanged() await afterCloseSet() } diff --git a/native/ios/MatchLiveTv/UI/Navigation/AppNavHost.swift b/native/ios/MatchLiveTv/UI/Navigation/AppNavHost.swift index d6a230f..d943d55 100644 --- a/native/ios/MatchLiveTv/UI/Navigation/AppNavHost.swift +++ b/native/ios/MatchLiveTv/UI/Navigation/AppNavHost.swift @@ -58,10 +58,17 @@ struct AppNavHost: View { .lockPortraitOrientation() } .fullScreenCover(item: $broadcastRoute, onDismiss: { + Task { @MainActor in + container.sessionCable.disconnect() + await container.broadcastCoordinator.forceStopBroadcast() + } AppOrientation.lockPortrait() matchesRefreshToken += 1 }) { route in BroadcastScreen(container: container, sessionId: route.sessionId) { + Task { @MainActor in + await container.broadcastCoordinator.stopBroadcast(sessionId: route.sessionId) + } AppOrientation.lockPortrait() container.wizardSession.reset() broadcastRoute = nil diff --git a/native/ios/MatchLiveTv/UI/System/ShareSheet.swift b/native/ios/MatchLiveTv/UI/System/ShareSheet.swift new file mode 100644 index 0000000..5dc9afd --- /dev/null +++ b/native/ios/MatchLiveTv/UI/System/ShareSheet.swift @@ -0,0 +1,28 @@ +import SwiftUI +import UIKit + +struct ShareItem: Identifiable { + let id = UUID() + let items: [Any] + let subject: String? +} + +/// Foglio condivisione compatibile iPhone/iPad (evita present() diretto da root VC). +struct ShareSheet: UIViewControllerRepresentable { + let items: [Any] + var subject: String? + var onComplete: (() -> Void)? + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: items, applicationActivities: nil) + if let subject { + controller.setValue(subject, forKey: "subject") + } + controller.completionWithItemsHandler = { _, _, _, _ in + onComplete?() + } + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} diff --git a/native/ios/MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift b/native/ios/MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift index 3920643..f32501a 100644 --- a/native/ios/MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift +++ b/native/ios/MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift @@ -19,6 +19,7 @@ struct StepNetworkTestScreen: View { @State private var selectedQualityLabel: String? @State private var starting = false @State private var currentSession: StreamSession + @State private var shareItem: ShareItem? init( container: AppContainer, @@ -154,6 +155,11 @@ struct StepNetworkTestScreen: View { .task(id: session.id) { await pollYoutubeIfNeeded() } + .sheet(item: $shareItem) { item in + ShareSheet(items: item.items, subject: item.subject) { + shareItem = nil + } + } } private func runTest() { @@ -223,8 +229,14 @@ struct StepNetworkTestScreen: View { Task { @MainActor in do { let url = try await container.sessionRepository.createRegiaLink(sessionId: currentSession.id) - guard let link = URL(string: url) else { return } - presentShare(items: [link]) + guard let link = URL(string: url) else { + onError("Link regia non valido") + return + } + shareItem = ShareItem( + items: [link], + subject: "Link regia — \(match.teamName) vs \(match.opponentName)" + ) } catch { if let message = UserFacingError.message(for: error) { onError(message) @@ -233,13 +245,6 @@ struct StepNetworkTestScreen: View { } } - private func presentShare(items: [Any]) { - guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let root = scene.windows.first?.rootViewController else { return } - let controller = UIActivityViewController(activityItems: items, applicationActivities: nil) - root.present(controller, animated: true) - } - private func formatQualityLabel(_ result: NetworkTestResponse) -> String { let bitrateMbps = Double(result.targetBitrate ?? 2_500_000) / 1_000_000.0 let fps = result.targetFps ?? 30 diff --git a/native/ios/MatchLiveTvTests/BroadcastOrientationPolicyTests.swift b/native/ios/MatchLiveTvTests/BroadcastOrientationPolicyTests.swift new file mode 100644 index 0000000..d603321 --- /dev/null +++ b/native/ios/MatchLiveTvTests/BroadcastOrientationPolicyTests.swift @@ -0,0 +1,59 @@ +import AVFoundation +import XCTest +@testable import MatchLiveTv + +final class BroadcastOrientationPolicyTests: XCTestCase { + func testAppliesFirstLandscapeOrientation() { + XCTAssertTrue( + BroadcastOrientationPolicy.shouldApplyMixerOrientation( + new: .landscapeRight, + previous: nil, + landscapeLocked: true + ) + ) + } + + func testSkipsDuplicateLandscapeOrientation() { + XCTAssertFalse( + BroadcastOrientationPolicy.shouldApplyMixerOrientation( + new: .landscapeRight, + previous: .landscapeRight, + landscapeLocked: true + ) + ) + } + + func testAppliesLandscapeFlip() { + XCTAssertTrue( + BroadcastOrientationPolicy.shouldApplyMixerOrientation( + new: .landscapeLeft, + previous: .landscapeRight, + landscapeLocked: true + ) + ) + } + + func testIgnoresPortraitWhenLandscapeLocked() { + XCTAssertFalse( + BroadcastOrientationPolicy.isApplicableForBroadcast(.portrait, landscapeLocked: true) + ) + XCTAssertFalse( + BroadcastOrientationPolicy.shouldApplyMixerOrientation( + new: .portrait, + previous: .landscapeRight, + landscapeLocked: true + ) + ) + } + + func testAllowsPortraitWhenNotLandscapeLocked() { + XCTAssertTrue( + BroadcastOrientationPolicy.isApplicableForBroadcast(.portrait, landscapeLocked: false) + ) + } + + func testDebounceAndSuppressWindowsArePositive() { + XCTAssertGreaterThan(BroadcastOrientationPolicy.debounceNanoseconds, 0) + XCTAssertGreaterThan(BroadcastOrientationPolicy.suppressDisconnectSeconds, 0) + } +} diff --git a/native/ios/MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift b/native/ios/MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift new file mode 100644 index 0000000..768387f --- /dev/null +++ b/native/ios/MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift @@ -0,0 +1,16 @@ +import XCTest +@testable import MatchLiveTv + +final class LiveBroadcastCoordinatorTests: XCTestCase { + func testShouldStopPreviousSessionFalseOnFirstLaunch() { + XCTAssertFalse(LiveBroadcastCoordinator.shouldStopPreviousSession(active: nil, incoming: "session-a")) + } + + func testShouldStopPreviousSessionFalseForSameSession() { + XCTAssertFalse(LiveBroadcastCoordinator.shouldStopPreviousSession(active: "session-a", incoming: "session-a")) + } + + func testShouldStopPreviousSessionTrueWhenSwitching() { + XCTAssertTrue(LiveBroadcastCoordinator.shouldStopPreviousSession(active: "session-a", incoming: "session-b")) + } +} diff --git a/native/ios/MatchLiveTvTests/ScoreActionDecodeTests.swift b/native/ios/MatchLiveTvTests/ScoreActionDecodeTests.swift index 24fdb29..6ddf00d 100644 --- a/native/ios/MatchLiveTvTests/ScoreActionDecodeTests.swift +++ b/native/ios/MatchLiveTvTests/ScoreActionDecodeTests.swift @@ -33,4 +33,36 @@ final class ScoreActionDecodeTests: XCTestCase { XCTAssertEqual(score.homePoints, 1) XCTAssertEqual(score.boardType, "volley") } + + func testDecodesCloseSetResponseWithPartials() throws { + let json = """ + { + "id": "821ddb81-9dc2-466a-9ad5-2555e78f4653", + "match_id": "e887ce1d-10ee-4680-8323-79bb8f11e0aa", + "status": "live", + "platform": "matchlivetv", + "score": { + "type": "score_update", + "board_type": "volley", + "home_sets": 1, + "away_sets": 0, + "home_points": 0, + "away_points": 0, + "current_set": 2, + "set_partials": [{ "set": 1, "home": 25, "away": 18 }], + "timeout_home": false, + "timeout_away": false + }, + "set_won": true, + "match_won": false, + "winner": "home" + } + """.data(using: .utf8)! + let dto = try ApiInstant.decoder.decode(StreamSessionDto.self, from: json) + let score = try XCTUnwrap(dto.score?.toDomain()) + XCTAssertEqual(score.currentSet, 2) + XCTAssertEqual(score.setPartials.count, 1) + XCTAssertEqual(score.setPartials[0].home, 25) + XCTAssertEqual(score.setPartials[0].away, 18) + } } diff --git a/native/ios/MatchLiveTvTests/ScoreControllerTests.swift b/native/ios/MatchLiveTvTests/ScoreControllerTests.swift index 557edc5..b34473d 100644 --- a/native/ios/MatchLiveTvTests/ScoreControllerTests.swift +++ b/native/ios/MatchLiveTvTests/ScoreControllerTests.swift @@ -3,6 +3,36 @@ import XCTest @MainActor final class ScoreControllerTests: XCTestCase { + func testBindNewSessionResetsScoreToZero() { + let controller = ScoreController( + scoreRepository: ScoreRepository(api: MatchLiveAPI()), + sessionCable: SessionCableService() + ) + var previous = ScoreState() + previous.homePoints = 3 + previous.awayPoints = 3 + controller.bind(sessionId: "session-1", initial: previous) + XCTAssertEqual(controller.score.homePoints, 3) + + controller.bind(sessionId: "session-2", initial: nil) + XCTAssertEqual(controller.score.homePoints, 0) + XCTAssertEqual(controller.score.awayPoints, 0) + } + + func testBindNewSessionUsesServerInitialState() { + let controller = ScoreController( + scoreRepository: ScoreRepository(api: MatchLiveAPI()), + sessionCable: SessionCableService() + ) + controller.bind(sessionId: "session-1", initial: ScoreState(homePoints: 2, awayPoints: 1)) + var fresh = ScoreState() + fresh.homePoints = 1 + fresh.awayPoints = 0 + controller.bind(sessionId: "session-2", initial: fresh) + XCTAssertEqual(controller.score.homePoints, 1) + XCTAssertEqual(controller.score.awayPoints, 0) + } + func testIncrementHomeUpdatesPublishedScore() { let controller = ScoreController( scoreRepository: ScoreRepository(api: MatchLiveAPI()), diff --git a/scripts/build_ios_release.sh b/scripts/build_ios_release.sh index 0ff882c..4629594 100755 --- a/scripts/build_ios_release.sh +++ b/scripts/build_ios_release.sh @@ -8,6 +8,9 @@ SCHEME="MatchLiveTv" CONFIG="${CONFIG:-Release}" API_BASE_URL="${API_BASE_URL:-https://www.matchlivetv.it}" +echo "== Pre-release: test simulatore ==" +"$ROOT/scripts/test_ios_simulator.sh" + cd "$IOS_DIR" if [[ ! -d "$PROJECT" ]]; then diff --git a/scripts/test_ios_simulator.sh b/scripts/test_ios_simulator.sh new file mode 100755 index 0000000..5653fca --- /dev/null +++ b/scripts/test_ios_simulator.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Test iOS su simulatore (arm64) — eseguire prima di ogni test su dispositivo / rilascio. +# Uso: ./scripts/test_ios_simulator.sh +# API=http://localhost:3000 ./scripts/test_ios_simulator.sh --e2e +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +IOS_DIR="$ROOT/native/ios" +SIM_ID="${SIM_ID:-48A837E2-845E-4043-B168-B8D4FDCAAE21}" +DERIVED="$IOS_DIR/build/DerivedData" +RUN_E2E=false + +for arg in "$@"; do + case "$arg" in + --e2e) RUN_E2E=true ;; + -h|--help) + echo "Uso: $0 [--e2e]" + echo " --e2e include anche scripts/e2e_ios_simulator.sh (richiede API Rails su localhost:3000)" + exit 0 + ;; + esac +done + +cd "$IOS_DIR" +python3 generate_xcodeproj.py + +DEST="platform=iOS Simulator,id=$SIM_ID" +ARCH_FLAGS=(ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64) + +echo "== Unit test iOS (simulatore arm64) ==" +xcodebuild \ + -project MatchLiveTv.xcodeproj \ + -scheme MatchLiveTv \ + -destination "$DEST" \ + -derivedDataPath "$DERIVED" \ + -configuration Debug \ + "${ARCH_FLAGS[@]}" \ + test 2>&1 | tee /tmp/matchlivetv-ios-test.log | tail -30 + +if grep -q "TEST SUCCEEDED" /tmp/matchlivetv-ios-test.log; then + echo "== Unit test: OK ==" +else + echo "== Unit test: FALLITI (vedi /tmp/matchlivetv-ios-test.log) ==" >&2 + exit 1 +fi + +echo "== Build simulatore (smoke) ==" +xcodebuild \ + -project MatchLiveTv.xcodeproj \ + -scheme MatchLiveTv \ + -destination "$DEST" \ + -derivedDataPath "$DERIVED" \ + -configuration Debug \ + "${ARCH_FLAGS[@]}" \ + build 2>&1 | tail -5 + +APP="$DERIVED/Build/Products/Debug-iphonesimulator/MatchLiveTv.app" +test -d "$APP" +echo "== Build simulatore: OK ($APP) ==" + +if $RUN_E2E; then + echo "== E2E API + simulatore ==" + API="${API:-http://localhost:3000}" "$ROOT/scripts/e2e_ios_simulator.sh" +fi + +echo "== test_ios_simulator.sh completato =="