Stabilizza diretta iOS e aggiunge test simulatore pre-release.
Corregge lifecycle RTMP/preview tra partite, reset tabellone, orientamento con debounce, condivisione su iPad e script test_ios_simulator per CI locale. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
08A245DF805C482C89AEB2E4 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = "<group>"; };
|
||||
0A84A2EA29C441EFA31498AD /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = "<group>"; };
|
||||
0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = "<group>"; };
|
||||
10C6CAC995404909A95482BF /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = "<group>"; };
|
||||
1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = "<group>"; };
|
||||
1ED842B98F33423988B13960 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = "<group>"; };
|
||||
21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = "<group>"; };
|
||||
2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = "<group>"; };
|
||||
227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = "<group>"; };
|
||||
26D7D448D0A34D9384C5E2D3 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = "<group>"; };
|
||||
2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = "<group>"; };
|
||||
2D0386B94B9042918A68CD76 /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = "<group>"; };
|
||||
2E221E68194C4C92925A9CB0 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = "<group>"; };
|
||||
2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = "<group>"; };
|
||||
2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = "<group>"; };
|
||||
316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = "<group>"; };
|
||||
34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = "<group>"; };
|
||||
372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = "<group>"; };
|
||||
388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = "<group>"; };
|
||||
3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = "<group>"; };
|
||||
3F5EB6A380DC4728866BD42A /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = "<group>"; };
|
||||
3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = "<group>"; };
|
||||
4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = "<group>"; };
|
||||
4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = "<group>"; };
|
||||
4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = "<group>"; };
|
||||
4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = "<group>"; };
|
||||
5086F0DA44004334881343ED /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = "<group>"; };
|
||||
551CCAA3C2F64172A59E278B /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = "<group>"; };
|
||||
584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = "<group>"; };
|
||||
5D813398882A483280580618 /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = "<group>"; };
|
||||
6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = "<group>"; };
|
||||
6F05D132A65E45E8AED152F0 /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = "<group>"; };
|
||||
7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = "<group>"; };
|
||||
7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = "<group>"; };
|
||||
7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = "<group>"; };
|
||||
806B1034AB5148B4BC55F093 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = "<group>"; };
|
||||
8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = "<group>"; };
|
||||
8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = "<group>"; };
|
||||
8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = "<group>"; };
|
||||
944972DE154D444781B612ED /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = "<group>"; };
|
||||
94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = "<group>"; };
|
||||
96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = "<group>"; };
|
||||
976DD89356184E0694B20CB8 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = "<group>"; };
|
||||
978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = "<group>"; };
|
||||
9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = "<group>"; };
|
||||
A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = "<group>"; };
|
||||
A2627CF334644FCA8FD9950B /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = "<group>"; };
|
||||
A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = "<group>"; };
|
||||
ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = "<group>"; };
|
||||
AE769961C58E4741B73938B8 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = "<group>"; };
|
||||
B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = "<group>"; };
|
||||
BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = "<group>"; };
|
||||
C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
CCC01BBAC06A412296955DB3 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = "<group>"; };
|
||||
CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = "<group>"; };
|
||||
D6725372B2CE473F87167457 /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = "<group>"; };
|
||||
D6B453249C904B299178A996 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = "<group>"; };
|
||||
D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = "<group>"; };
|
||||
DE5A60F7644B4881985D4F15 /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = "<group>"; };
|
||||
E3DEAC5182ED489798656300 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = "<group>"; };
|
||||
E770890933A84622ABF4EA9F /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = "<group>"; };
|
||||
EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = "<group>"; };
|
||||
EC311445F0144B59AE22DA96 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = "<group>"; };
|
||||
F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = "<group>"; };
|
||||
FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = "<group>"; };
|
||||
073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = "<group>"; };
|
||||
09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = "<group>"; };
|
||||
0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = "<group>"; };
|
||||
136E90D700FD44D89C9D5659 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = "<group>"; };
|
||||
17256285CF354F0D8E4B33A3 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = "<group>"; };
|
||||
177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicyTests.swift; path = MatchLiveTvTests/BroadcastOrientationPolicyTests.swift; sourceTree = "<group>"; };
|
||||
18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = "<group>"; };
|
||||
1E128E2B501C45419220F030 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = "<group>"; };
|
||||
1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = "<group>"; };
|
||||
24FAE643838541119B344969 /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = "<group>"; };
|
||||
2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = "<group>"; };
|
||||
2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ShareSheet.swift; path = MatchLiveTv/UI/System/ShareSheet.swift; sourceTree = "<group>"; };
|
||||
2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = "<group>"; };
|
||||
2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = "<group>"; };
|
||||
2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = "<group>"; };
|
||||
2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = "<group>"; };
|
||||
34DE015C16C54795A496C968 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = "<group>"; };
|
||||
3AA971C263254BDC95AC2423 /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = "<group>"; };
|
||||
3FBA01162B004E85821F206B /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = "<group>"; };
|
||||
46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastVideoOrientation.swift; path = MatchLiveTv/Streaming/BroadcastVideoOrientation.swift; sourceTree = "<group>"; };
|
||||
573991C190AD43BBBDDD186C /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = "<group>"; };
|
||||
5853956279E94634BFC1660C /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = "<group>"; };
|
||||
5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = "<group>"; };
|
||||
677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = "<group>"; };
|
||||
79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = "<group>"; };
|
||||
7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = "<group>"; };
|
||||
7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = "<group>"; };
|
||||
7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = "<group>"; };
|
||||
810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = "<group>"; };
|
||||
818D265485A3445DAB7EAC48 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = "<group>"; };
|
||||
81F07B021AC24DCAA217238C /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = "<group>"; };
|
||||
880F5D2B2B13497891706E0A /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = "<group>"; };
|
||||
8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = "<group>"; };
|
||||
8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = "<group>"; };
|
||||
8E7E256AA201434EBD01B7D4 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = "<group>"; };
|
||||
92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = "<group>"; };
|
||||
951726866CB945DD917376C7 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = "<group>"; };
|
||||
9525D894BB924BFFBC2D244C /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = "<group>"; };
|
||||
9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = "<group>"; };
|
||||
9AB5819FE32342F1961F256E /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = "<group>"; };
|
||||
A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = "<group>"; };
|
||||
A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = "<group>"; };
|
||||
A264DB29100D404EA570CF47 /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = "<group>"; };
|
||||
A2C8DD5093B84D5FBE360C23 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = "<group>"; };
|
||||
A66D5B274FF34093901B981B /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = "<group>"; };
|
||||
A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = "<group>"; };
|
||||
A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = "<group>"; };
|
||||
AB5EAF0FD4BE4B14B5B029B8 /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = "<group>"; };
|
||||
B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
BA461A50E47740818FDC5890 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = "<group>"; };
|
||||
BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = "<group>"; };
|
||||
BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = "<group>"; };
|
||||
BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicy.swift; path = MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift; sourceTree = "<group>"; };
|
||||
BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = "<group>"; };
|
||||
CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = "<group>"; };
|
||||
CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinatorTests.swift; path = MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = "<group>"; };
|
||||
D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = "<group>"; };
|
||||
D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = "<group>"; };
|
||||
DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = "<group>"; };
|
||||
DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = "<group>"; };
|
||||
E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = "<group>"; };
|
||||
E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = "<group>"; };
|
||||
E6BA18F615214E4184E199B2 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = "<group>"; };
|
||||
EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = "<group>"; };
|
||||
F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = "<group>"; };
|
||||
F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = "<group>"; };
|
||||
FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = "<group>"; };
|
||||
FC184551A43F44E09E536D29 /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = "<group>"; };
|
||||
FE0C26984E50402281861375 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
631A87449B654E888E88BF77 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
705BD051C8004C5AA632BB29 /* MatchLiveTv */,
|
||||
8658B6D49A814C9981463B85 /* MatchLiveTvTests */,
|
||||
033861C6E3D248C6A4CEFAF6 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
25CDF42A0993431FA5F457EB = {
|
||||
8658B6D49A814C9981463B85 /* MatchLiveTvTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
146562E9E8F4444781CCEE1C /* MatchLiveTv */,
|
||||
4190C9EAA59D4AA882933799 /* MatchLiveTvTests */,
|
||||
B06BE907209A475AA6898BB6 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
B06BE907209A475AA6898BB6 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3DCC59301E94417BC24492F /* MatchLiveTv.app */,
|
||||
ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="2F5D6393282640E5A53D4CFC" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="553940DB807C4DBFB63E9FFE" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="938E427E770D4DFD8E992BFA" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="79FDBB021E5045C5A353AEE8" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
||||
<Testables>
|
||||
<TestableReference skipped="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="938E427E770D4DFD8E992BFA" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="79FDBB021E5045C5A353AEE8" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
|
||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="2F5D6393282640E5A53D4CFC" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="553940DB807C4DBFB63E9FFE" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
|
||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="2F5D6393282640E5A53D4CFC" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="553940DB807C4DBFB63E9FFE" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction buildConfiguration="Debug"/>
|
||||
|
||||
@@ -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<CodingKeys>, 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 {
|
||||
|
||||
@@ -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?()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> = ["basket", "timed"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Void, Never>?
|
||||
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..<maxAttempts {
|
||||
if previewView != nil { return true }
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
return previewView != nil
|
||||
}
|
||||
|
||||
/// Attende che l'anteprima sia collegata al mixer (dopo `configurePipeline`).
|
||||
func waitForPreviewReady(maxAttempts: Int = 100) async -> Bool {
|
||||
for _ in 0..<maxAttempts {
|
||||
if Task.isCancelled { return previewAttached }
|
||||
await attachPreviewIfNeeded()
|
||||
if previewAttached { return true }
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
return previewAttached
|
||||
}
|
||||
|
||||
func preparePreview(config: BroadcastConfig) async throws {
|
||||
self.config = config
|
||||
publishPending = false
|
||||
@@ -88,12 +117,15 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
try await prepareForBroadcast(config: config)
|
||||
}
|
||||
|
||||
func pauseBroadcast() async {
|
||||
func pauseBroadcast(suppressDisconnectError: Bool = false) async {
|
||||
self.suppressDisconnectError = suppressDisconnectError
|
||||
defer { self.suppressDisconnectError = false }
|
||||
broadcastGeneration += 1
|
||||
publishPending = false
|
||||
publishTask?.cancel()
|
||||
publishTask = nil
|
||||
await teardownRTMP(keepPreview: true)
|
||||
setPhase(.paused)
|
||||
await teardownRTMP(keepPreview: true)
|
||||
}
|
||||
|
||||
func resumeBroadcast(config: BroadcastConfig) async throws {
|
||||
@@ -101,27 +133,35 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
publishPending = true
|
||||
if !pipelineConfigured {
|
||||
try await configurePipeline(config)
|
||||
} else {
|
||||
await attachPreviewIfNeeded()
|
||||
}
|
||||
setPhase(.connecting)
|
||||
await startPendingPublish()
|
||||
}
|
||||
|
||||
func stopBroadcast() async {
|
||||
func stopBroadcast(suppressDisconnectError: Bool = false) async {
|
||||
self.suppressDisconnectError = suppressDisconnectError
|
||||
defer { self.suppressDisconnectError = false }
|
||||
broadcastGeneration += 1
|
||||
publishTask?.cancel()
|
||||
publishTask = nil
|
||||
publishPending = false
|
||||
publishInFlight = false
|
||||
stopOrientationMonitoring()
|
||||
orientationDebounceTask?.cancel()
|
||||
orientationDebounceTask = nil
|
||||
lastAppliedCaptureOrientation = nil
|
||||
orientationTransitionUntil = .distantPast
|
||||
setPhase(.idle)
|
||||
await teardownRTMP(keepPreview: false)
|
||||
if let previewView, previewAttached {
|
||||
await mixer.removeOutput(previewView)
|
||||
}
|
||||
previewAttached = false
|
||||
previewView = nil
|
||||
overlayRenderer.detach()
|
||||
try? await mixer.stopRunning()
|
||||
pipelineConfigured = false
|
||||
setPhase(.idle)
|
||||
}
|
||||
|
||||
func release() async {
|
||||
@@ -133,7 +173,10 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
AVCaptureDevice.authorizationStatus(for: .audio) == .authorized else {
|
||||
throw APIError.http(403, "Permessi camera e microfono richiesti")
|
||||
}
|
||||
if pipelineConfigured { return }
|
||||
if pipelineConfigured {
|
||||
await attachPreviewIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .allowBluetooth])
|
||||
@@ -149,9 +192,11 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
try await mixer.attachAudio(microphone, track: 0)
|
||||
}
|
||||
if let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
|
||||
try await mixer.attachVideo(camera, track: 0)
|
||||
try await mixer.attachVideo(camera, track: 0) { videoUnit in
|
||||
videoUnit.isVideoMirrored = false
|
||||
}
|
||||
}
|
||||
await applyVideoOrientation()
|
||||
await applyVideoOrientationIfNeeded(force: true)
|
||||
startOrientationMonitoring()
|
||||
|
||||
try await mixer.startRunning()
|
||||
@@ -162,11 +207,7 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
isPortrait: isPortrait
|
||||
)
|
||||
pipelineConfigured = true
|
||||
|
||||
if let previewView, !previewAttached {
|
||||
await mixer.addOutput(previewView)
|
||||
previewAttached = true
|
||||
}
|
||||
await attachPreviewIfNeeded()
|
||||
}
|
||||
|
||||
private func configureScreenSize(width: Int, height: Int) async {
|
||||
@@ -186,40 +227,48 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
|
||||
private func waitForPreviewThenPublish() async {
|
||||
guard publishPending, let config else { return }
|
||||
let generation = broadcastGeneration
|
||||
publishInFlight = true
|
||||
defer { publishInFlight = false }
|
||||
|
||||
for _ in 0..<80 {
|
||||
for _ in 0..<120 {
|
||||
if Task.isCancelled { return }
|
||||
if generation != broadcastGeneration { return }
|
||||
await attachPreviewIfNeeded()
|
||||
if previewAttached { break }
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
guard previewAttached else {
|
||||
publishPending = false
|
||||
setPhase(.error, error: "Anteprima camera non pronta")
|
||||
guard previewAttached, generation == broadcastGeneration else {
|
||||
if generation == broadcastGeneration {
|
||||
publishPending = false
|
||||
setPhase(.error, error: "Anteprima camera non pronta")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setPhase(.connecting)
|
||||
try? await Task.sleep(nanoseconds: 350_000_000)
|
||||
guard publishPending else { return }
|
||||
guard publishPending, generation == broadcastGeneration else { return }
|
||||
|
||||
do {
|
||||
try await publish(config: config)
|
||||
publishPending = false
|
||||
try await publish(config: config, generation: generation)
|
||||
if generation == broadcastGeneration {
|
||||
publishPending = false
|
||||
}
|
||||
} catch {
|
||||
guard generation == broadcastGeneration else { return }
|
||||
publishPending = false
|
||||
let message = UserFacingError.message(for: error) ?? "Connessione RTMP fallita"
|
||||
setPhase(.error, error: message)
|
||||
}
|
||||
}
|
||||
|
||||
private func publish(config: BroadcastConfig) async throws {
|
||||
private func publish(config: BroadcastConfig, generation: Int) async throws {
|
||||
guard let url = URL(string: config.rtmpUrl) else {
|
||||
throw APIError.http(400, "URL RTMP non valido")
|
||||
}
|
||||
await Self.ensureRTMPFactoryRegistered()
|
||||
await applyVideoOrientation()
|
||||
await applyVideoOrientationIfNeeded(force: true)
|
||||
await teardownRTMP(keepPreview: true)
|
||||
|
||||
let session = try await SessionBuilderFactory.shared.make(url)
|
||||
@@ -246,14 +295,21 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
})
|
||||
await mixer.addOutput(stream)
|
||||
rtmpSession = session
|
||||
observeReadyState(session)
|
||||
observeReadyState(session, generation: generation)
|
||||
|
||||
try await session.connect { [weak self] in
|
||||
Task { @MainActor in
|
||||
guard let self, self.phase == .live else { return }
|
||||
guard let self,
|
||||
!self.shouldSuppressDisconnectError,
|
||||
self.broadcastGeneration == generation,
|
||||
self.phase == .live else { return }
|
||||
self.setPhase(.error, error: "Connessione RTMP interrotta")
|
||||
}
|
||||
}
|
||||
guard generation == broadcastGeneration else {
|
||||
await teardownRTMP(keepPreview: true)
|
||||
return
|
||||
}
|
||||
setPhase(.live)
|
||||
}
|
||||
|
||||
@@ -277,14 +333,16 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
try await stream.setAudioSettings(audioSettings)
|
||||
}
|
||||
|
||||
private func observeReadyState(_ session: any Session) {
|
||||
private func observeReadyState(_ session: any Session, generation: Int) {
|
||||
readyStateTask?.cancel()
|
||||
readyStateTask = Task {
|
||||
for await state in await session.readyState {
|
||||
guard generation == broadcastGeneration else { return }
|
||||
switch state {
|
||||
case .open:
|
||||
setPhase(.live)
|
||||
case .closed where phase == .live:
|
||||
guard !shouldSuppressDisconnectError else { return }
|
||||
setPhase(.error, error: "Connessione RTMP interrotta")
|
||||
default:
|
||||
break
|
||||
@@ -307,15 +365,49 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func applyVideoOrientation() async {
|
||||
private var shouldSuppressDisconnectError: Bool {
|
||||
suppressDisconnectError || Date() < orientationTransitionUntil
|
||||
}
|
||||
|
||||
private func applyVideoOrientationIfNeeded(force: Bool = false) async {
|
||||
let orientation = BroadcastVideoOrientation.captureOrientation()
|
||||
let landscapeLocked = AppOrientation.mode == .landscape
|
||||
if !force,
|
||||
!BroadcastOrientationPolicy.shouldApplyMixerOrientation(
|
||||
new: orientation,
|
||||
previous: lastAppliedCaptureOrientation,
|
||||
landscapeLocked: landscapeLocked
|
||||
) {
|
||||
return
|
||||
}
|
||||
guard BroadcastOrientationPolicy.isApplicableForBroadcast(orientation, landscapeLocked: landscapeLocked) else {
|
||||
return
|
||||
}
|
||||
|
||||
if phase == .live || phase == .connecting {
|
||||
orientationTransitionUntil = Date().addingTimeInterval(BroadcastOrientationPolicy.suppressDisconnectSeconds)
|
||||
}
|
||||
lastAppliedCaptureOrientation = orientation
|
||||
|
||||
await mixer.setVideoOrientation(orientation)
|
||||
try? await mixer.configuration(video: 0) { videoUnit in
|
||||
videoUnit.isVideoMirrored = false
|
||||
}
|
||||
let portrait = BroadcastVideoOrientation.isPortraitContent
|
||||
guard portrait != isPortrait else { return }
|
||||
isPortrait = portrait
|
||||
overlayRenderer.refreshOrientation(isPortrait: portrait)
|
||||
}
|
||||
|
||||
private func scheduleOrientationUpdate() {
|
||||
orientationDebounceTask?.cancel()
|
||||
orientationDebounceTask = Task {
|
||||
try? await Task.sleep(nanoseconds: BroadcastOrientationPolicy.debounceNanoseconds)
|
||||
guard !Task.isCancelled else { return }
|
||||
await applyVideoOrientationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func startOrientationMonitoring() {
|
||||
stopOrientationMonitoring()
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
@@ -325,12 +417,14 @@ final class LiveBroadcastEngine: ObservableObject {
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
await self?.applyVideoOrientation()
|
||||
self?.scheduleOrientationUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopOrientationMonitoring() {
|
||||
orientationDebounceTask?.cancel()
|
||||
orientationDebounceTask = nil
|
||||
if let orientationObserver {
|
||||
NotificationCenter.default.removeObserver(orientationObserver)
|
||||
self.orientationObserver = nil
|
||||
|
||||
@@ -4,12 +4,12 @@ struct WatermarkElement: OverlayElement {
|
||||
private let logo = UIImage(named: "logo-white-m-256")
|
||||
|
||||
func draw(in context: CGContext, state: OverlayState, layout: OverlayLayout) {
|
||||
guard state.overlayKind != .none, state.watermarkVisible, let logo, let cg = logo.cgImage else { return }
|
||||
guard state.overlayKind != .none, state.watermarkVisible, let logo else { return }
|
||||
let targetWidth = min(72, max(28, Int((CGFloat(layout.canvasWidth) * 0.055).rounded())))
|
||||
let scale = CGFloat(targetWidth) / logo.size.width
|
||||
let targetHeight = Int(logo.size.height * scale)
|
||||
let left = layout.canvasWidth - layout.marginPx - targetWidth
|
||||
let top = layout.canvasHeight - layout.marginPx - targetHeight
|
||||
context.draw(cg, in: CGRect(x: left, y: top, width: targetWidth, height: targetHeight))
|
||||
logo.draw(in: CGRect(x: left, y: top, width: targetWidth, height: targetHeight))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ struct LivePreviewView: UIViewRepresentable {
|
||||
|
||||
func makeUIView(context: Context) -> 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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
28
native/ios/MatchLiveTv/UI/System/ShareSheet.swift
Normal file
28
native/ios/MatchLiveTv/UI/System/ShareSheet.swift
Normal file
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
66
scripts/test_ios_simulator.sh
Executable file
66
scripts/test_ios_simulator.sh
Executable file
@@ -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 =="
|
||||
Reference in New Issue
Block a user