diff --git a/.xcode-version b/.xcode-version index 3d3be3c32..9dc738e69 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -15.0 \ No newline at end of file +15.0.1 \ No newline at end of file diff --git a/Application/Application-Info.plist b/Application/Application-Info.plist index 1b5560b0e..c4aeaef4b 100755 --- a/Application/Application-Info.plist +++ b/Application/Application-Info.plist @@ -14,6 +14,8 @@ $(CONFIG__APPCENTER_SECRET) AppCenterURL $(APPCENTER_URL) + AppStoreAppleId + $(CONFIG__APPSTORE_APPLE_ID) ApplicationGroupIdentifier $(COMMON__APP_GROUP_IDENTIFIER) CFBundleDisplayName diff --git a/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json b/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json index 694206b61..fa2f91a9c 100755 --- a/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json +++ b/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json @@ -5,7 +5,7 @@ "tvSiteName": "rsi-player-tvos-apple", "voiceOverLanguageCode": "it", "appStoreProductIdentifier": 920753497, - "playURL": "https://www.rsi.ch/play/", + "playURLs": "{\"rsi\":\"https://www.rsi.ch/play/\",\"rtr\":\"https://www.rtr.ch/play/\",\"rts\":\"https://www.rts.ch/play/\",\"srf\":\"https://www.srf.ch/play/\",\"swi\":\"https://play.swissinfo.ch/play/\"}", "playServiceURL": "https://www.rsi.ch/play/", "middlewareURL": "https://playfff.herokuapp.com", "sourceCodeURL": "https://github.com/SRGSSR/playsrg-apple", @@ -29,5 +29,6 @@ "searchSettingSubtitledHidden": true, "subtitleAvailabilityHidden": true, "audioDescriptionAvailabilityHidden": true, + "tvGuideOtherBouquets": "srf,rts", "userConsentDefaultLanguage": "it" } diff --git a/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings b/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings index 04e30839b..cf457c661 100755 --- a/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings @@ -100,9 +100,6 @@ /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Utente connesso: %@"; -/* Accessibility text for the login / signup header */ -"Login or sign up" = "Login o registrazione"; - /* Accessibility hint for the profile header when user is logged in */ "Manages account information" = "Gestire le informazioni sull'account"; @@ -114,9 +111,6 @@ Button to access more episodes */ "More episodes" = "Più episodi"; -/* Text displayed when a user is logged in but no information has been retrieved yet */ -"My account" = "Il mio account"; - /* Next day button label in program guide */ "Next day" = "Giorno successivo"; @@ -186,7 +180,9 @@ /* Settings button label on home view */ "Settings" = "Impostazioni"; -/* Share button label on player view */ +/* Share button label on content page view + Share button label on player view + Share button label on section detail view */ "Share" = "Condividi"; /* Accessibility label of the song list handle when closed */ diff --git a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings index 6bf155ac3..090f63f54 100755 --- a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings @@ -72,7 +72,7 @@ /* Context menu action to add a show to favorites Label displayed in the show view when a show can be favorited */ -"Add to favorites" = "Aggiungere ai preferiti"; +"Add to favorites" = "Aggiungi preferiti"; /* Advanced features section header */ "Advanced features" = "Funzioni avanzate"; @@ -681,6 +681,9 @@ /* Information message displayed when support information has been copied to the pasteboard */ "Support information has been copied to the pasteboard" = "Le informazioni di supporto sono state copiate negli appunti"; +/* Label of the button to open Apple TestFlight application and see other testable builds */ +"Switch version" = "Cambiare versione"; + /* Login benefits description footer */ "Synchronize favorites, playback history and content saved for later on all devices connected to your account." = "Sincronizza i preferiti, la cronologia e i contenuti da guardare dopo su tutti i dispositivi collegati al tuo account."; @@ -760,6 +763,7 @@ "This might interest you" = "Potrebbero interessarti"; /* Advanced features section footer + Bottom additional information section footer Reset section footer */ "This section is only available in nightly and beta versions, and won't appear in the production version." = "Questa sezione è solo disponibile solo nelle versioni nightly e beta, e non verrà visualizzata nella versione di produzione."; diff --git a/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json b/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json index 7ff72d13a..108b1046c 100755 --- a/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json +++ b/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json @@ -4,7 +4,7 @@ "siteName": "rtr-player-ios-v", "tvSiteName": "rtr-player-tvos-apple", "appStoreProductIdentifier": 920754925, - "playURL": "https://www.rtr.ch/play/", + "playURLs": "{\"rsi\":\"https://www.rsi.ch/play/\",\"rtr\":\"https://www.rtr.ch/play/\",\"rts\":\"https://www.rts.ch/play/\",\"srf\":\"https://www.srf.ch/play/\",\"swi\":\"https://play.swissinfo.ch/play/\"}", "playServiceURL": "https://www.rtr.ch/play/", "middlewareURL": "https://playfff.herokuapp.com", "sourceCodeURL": "https://github.com/SRGSSR/playsrg-apple", @@ -26,6 +26,6 @@ "hiddenOnboardings": "account,favorites_account,resume_playback_account,watch_later_account", "discoverySubtitleOptionLanguage": "de", "audioDescriptionAvailabilityHidden": true, - "tvThirdPartyChannelsAvailable": true, + "tvGuideOtherBouquets": "thirdparty,rts,rsi", "userConsentDefaultLanguage": "de" } diff --git a/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings b/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings index 2eae47a2a..6c51ab9ff 100755 --- a/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings @@ -100,9 +100,6 @@ /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Utilisader annunzià: %@"; -/* Accessibility text for the login / signup header */ -"Login or sign up" = "Login u registrar\n"; - /* Accessibility hint for the profile header when user is logged in */ "Manages account information" = "Organisescha las infurmaziuns da l'account"; @@ -114,9 +111,6 @@ Button to access more episodes */ "More episodes" = "Dapli episodas"; -/* Text displayed when a user is logged in but no information has been retrieved yet */ -"My account" = "Mes conto"; - /* Next day button label in program guide */ "Next day" = "Di suandant"; @@ -186,7 +180,9 @@ /* Settings button label on home view */ "Settings" = "Opziuns"; -/* Share button label on player view */ +/* Share button label on content page view + Share button label on player view + Share button label on section detail view */ "Share" = "Divida"; /* Accessibility label of the song list handle when closed */ diff --git a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings index 5e09ed472..95a7da9ea 100755 --- a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings @@ -681,6 +681,9 @@ /* Information message displayed when support information has been copied to the pasteboard */ "Support information has been copied to the pasteboard" = "Infurmaziuns da support èn copiadas en las notizias"; +/* Label of the button to open Apple TestFlight application and see other testable builds */ +"Switch version" = "Midar la versiun"; + /* Login benefits description footer */ "Synchronize favorites, playback history and content saved for later on all devices connected to your account." = "Sincronisar cronologia, favurits e cuntegns arcunads per pli tard sin tut ils apparats colliads."; @@ -760,6 +763,7 @@ "This might interest you" = "Quai pudess interessar Vus"; /* Advanced features section footer + Bottom additional information section footer Reset section footer */ "This section is only available in nightly and beta versions, and won't appear in the production version." = "Questa secziun è mo disponibla en la versiun nightly ni beta e na vegn betg en producziun."; diff --git a/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json b/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json index f1c14838d..34e9f968d 100755 --- a/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json +++ b/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json @@ -5,7 +5,7 @@ "tvSiteName": "rts-player-tvos-apple", "voiceOverLanguageCode": "fr", "appStoreProductIdentifier": 920754415, - "playURL": "https://www.rts.ch/play/", + "playURLs": "{\"rsi\":\"https://www.rsi.ch/play/\",\"rtr\":\"https://www.rtr.ch/play/\",\"rts\":\"https://www.rts.ch/play/\",\"srf\":\"https://www.srf.ch/play/\",\"swi\":\"https://play.swissinfo.ch/play/\"}", "playServiceURL": "https://www.rts.ch/play/", "middlewareURL": "https://playfff.herokuapp.com", "identityWebserviceURL": "https://hummingbird.rts.ch/api/profile", @@ -22,7 +22,7 @@ "dataProtectionURL": "https://www.rts.ch/article/8994006", "whatsNewURL": "https://srgssr.github.io/playsrg-apple/releases/release_notes-ios-rts.html", "radioChannels": "[{\"uid\":\"a9e7621504c6959e35c3ecbe7f6bed0446cdf8da\",\"name\":\"La 1ère\",\"resourceUid\":\"la1ere\",\"songsViewStyle\":\"collapsed\",\"color\":\"#E20026\",\"secondColor\":\"#5A285B\"},{\"uid\":\"a83f29dee7a5d0d3f9fccdb9c92161b1afb512db\",\"name\":\"Espace 2\",\"resourceUid\":\"espace2\",\"songsViewStyle\":\"collapsed\",\"color\":\"#0071CE\",\"secondColor\":\"#23B7C1\"},{\"uid\":\"8ceb28d9b3f1dd876d1df1780f908578cbefc3d7\",\"name\":\"Couleur 3\",\"resourceUid\":\"couleur3\",\"songsViewStyle\":\"collapsed\",\"color\":\"#E60096\",\"secondColor\":\"#FB5952\"},{\"uid\":\"f8517e5319a515e013551eea15aa114fa5cfbc3a\",\"name\":\"Option Musique\",\"resourceUid\":\"option_musique\",\"songsViewStyle\":\"expanded\",\"color\":\"#00CC99\",\"secondColor\":\"#CBC57A\"},{\"uid\":\"123456789101112131415161718192021222324x\",\"name\":\"Podcasts Originaux\",\"resourceUid\":\"podcasts_originaux\",\"color\":\"#A550F9\",\"homeSections\":\"radioLatestEpisodes,radioShowsAccess,radioFavoriteShows,radioLatestEpisodesFromFavorites,radioResumePlayback,radioMostPopular,radioWatchLater,radioAllShows\"}]", - "tvChannels": "[{\"uid\":\"143932a79bb5a123a646b68b1d1188d7ae493e5b\",\"name\":\"RTS 1\",\"resourceUid\":\"rts_un\",\"color\":\"#00D6F3\",\"secondColor\":\"#1D4C57\",\"titleColor\":\"#161616\"},{\"uid\":\"d7dfff28deee44e1d3c49a3d37d36d492b29671b\",\"name\":\"RTS 2\",\"resourceUid\":\"rts_deux\",\"color\":\"#BB66FF\",\"secondColor\":\"#3C215B\"},{\"uid\":\"5d332a26e06d08eec8ad385d566187df72955623\",\"name\":\"RTS Info\",\"resourceUid\":\"rts_info\",\"color\":\"#AF001E\",\"secondColor\":\"#860017\"}]", + "tvChannels": "[{\"uid\":\"143932a79bb5a123a646b68b1d1188d7ae493e5b\",\"name\":\"RTS 1\",\"resourceUid\":\"rts_un\",\"color\":\"#00D6F3\",\"secondColor\":\"#1D4C57\",\"titleColor\":\"#161616\"},{\"uid\":\"d7dfff28deee44e1d3c49a3d37d36d492b29671b\",\"name\":\"RTS 2\",\"resourceUid\":\"rts_deux\",\"color\":\"#BB66FF\",\"secondColor\":\"#3C215B\"},{\"uid\":\"5d332a26e06d08eec8ad385d566187df72955623\",\"name\":\"RTS Info\",\"resourceUid\":\"rts_info\",\"color\":\"#3787FF\",\"secondColor\":\"#153567\"}]", "satelliteRadioChannels": "[{\"uid\":\"rsp\",\"name\":\"Radio Swiss Pop\",\"resourceUid\":\"rsp\",\"songsViewStyle\":\"expanded\",\"color\":\"#F01F73\",\"secondColor\":\"#D31A3C\",\"homepageHidden\":true},{\"uid\":\"rsc-fr\",\"name\":\"Radio Swiss Classic\",\"resourceUid\":\"rsc\",\"songsViewStyle\":\"expanded\",\"color\":\"#09A1DE\",\"secondColor\":\"#036E99\",\"homepageHidden\":true},{\"uid\":\"rsj\",\"name\":\"Radio Swiss Jazz\",\"resourceUid\":\"rsj\",\"songsViewStyle\":\"expanded\",\"color\":\"#F7B222\",\"secondColor\":\"#CC7A00\",\"homepageHidden\":true}]", "continuousPlaybackPlayerViewTransitionDuration": 10, "continuousPlaybackForegroundTransitionDuration": 0, @@ -31,5 +31,6 @@ "hiddenOnboardings": "favorites,resume_playback,watch_later", "searchSettingSubtitledHidden": true, "showLeadPreferred": true, + "tvGuideOtherBouquets": "srf,rsi", "userConsentDefaultLanguage": "fr" } diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Contents.json b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Contents.json index 5f9c1842d..b33612504 100644 --- a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Contents.json +++ b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Contents.json @@ -1,15 +1,15 @@ { "images" : [ { - "filename" : "RTS_Info.pdf", + "filename" : "rtsinfo_32.pdf", "idiom" : "iphone" }, { - "filename" : "RTS_Info-1.pdf", + "filename" : "rtsinfo_32 1.pdf", "idiom" : "ipad" }, { - "filename" : "Logos_rtsInfo_60.pdf", + "filename" : "rtsinfo_60.pdf", "idiom" : "tv" } ], diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Logos_rtsInfo_60.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Logos_rtsInfo_60.pdf deleted file mode 100644 index 2b309020f..000000000 Binary files a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/Logos_rtsInfo_60.pdf and /dev/null differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/RTS_Info-1.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/RTS_Info-1.pdf deleted file mode 100644 index b4e819231..000000000 Binary files a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/RTS_Info-1.pdf and /dev/null differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/RTS_Info.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/RTS_Info.pdf deleted file mode 100644 index b4e819231..000000000 Binary files a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/RTS_Info.pdf and /dev/null differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_32 1.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_32 1.pdf new file mode 100644 index 000000000..6e2e97da4 Binary files /dev/null and b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_32 1.pdf differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_32.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_32.pdf new file mode 100644 index 000000000..6e2e97da4 Binary files /dev/null and b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_32.pdf differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_60.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_60.pdf new file mode 100644 index 000000000..230ede98b Binary files /dev/null and b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info-large.imageset/rtsinfo_60.pdf differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/Contents.json b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/Contents.json index f9c018ccd..40390933d 100755 --- a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/Contents.json +++ b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/Contents.json @@ -1,15 +1,15 @@ { "images" : [ { - "filename" : "logo_rtsInfo.pdf", + "filename" : "rtsinfo_22.pdf", "idiom" : "iphone" }, { - "filename" : "logo_rtsInfo-1.pdf", + "filename" : "rtsinfo_22 1.pdf", "idiom" : "ipad" }, { - "filename" : "logo_rtsInfo-2.pdf", + "filename" : "rtsinfo_22 2.pdf", "idiom" : "tv" } ], diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo-1.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo-1.pdf deleted file mode 100644 index 53f1e9450..000000000 Binary files a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo-1.pdf and /dev/null differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo-2.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo-2.pdf deleted file mode 100644 index 53f1e9450..000000000 Binary files a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo-2.pdf and /dev/null differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo.pdf deleted file mode 100644 index 53f1e9450..000000000 Binary files a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/logo_rtsInfo.pdf and /dev/null differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22 1.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22 1.pdf new file mode 100644 index 000000000..47ed57dad Binary files /dev/null and b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22 1.pdf differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22 2.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22 2.pdf new file mode 100644 index 000000000..47ed57dad Binary files /dev/null and b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22 2.pdf differ diff --git a/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22.pdf b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22.pdf new file mode 100644 index 000000000..47ed57dad Binary files /dev/null and b/Application/Resources/Apps/Play RTS/RTSResources.xcassets/TV/RTS Info/logo_rts_info.imageset/rtsinfo_22.pdf differ diff --git a/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings b/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings index 5436632bd..d3817e8d1 100644 --- a/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings @@ -100,9 +100,6 @@ /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Utilisateur connecté : %@"; -/* Accessibility text for the login / signup header */ -"Login or sign up" = "Connexion ou inscription"; - /* Accessibility hint for the profile header when user is logged in */ "Manages account information" = "Gère les informations du compte"; @@ -114,9 +111,6 @@ Button to access more episodes */ "More episodes" = "Autres épisodes"; -/* Text displayed when a user is logged in but no information has been retrieved yet */ -"My account" = "Mon compte"; - /* Next day button label in program guide */ "Next day" = "Jour suivant"; @@ -186,7 +180,9 @@ /* Settings button label on home view */ "Settings" = "Paramètres"; -/* Share button label on player view */ +/* Share button label on content page view + Share button label on player view + Share button label on section detail view */ "Share" = "Partager"; /* Accessibility label of the song list handle when closed */ diff --git a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings index 23233ca66..114fdf9b6 100644 --- a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings @@ -681,6 +681,9 @@ /* Information message displayed when support information has been copied to the pasteboard */ "Support information has been copied to the pasteboard" = "Les informations de support ont été copiées dans le presse-papier"; +/* Label of the button to open Apple TestFlight application and see other testable builds */ +"Switch version" = "Changer de version"; + /* Login benefits description footer */ "Synchronize favorites, playback history and content saved for later on all devices connected to your account." = "Synchronisez les favoris, l'historique de lecture et le contenu à consulter plus tard sur tous les appareils liés à votre compte."; @@ -760,6 +763,7 @@ "This might interest you" = "Cela pourrait vous intéresser"; /* Advanced features section footer + Bottom additional information section footer Reset section footer */ "This section is only available in nightly and beta versions, and won't appear in the production version." = "Cette section n’est disponible que dans les versions nightly et beta, et n'apparaîtra pas en production."; @@ -864,4 +868,4 @@ "You have been automatically logged out. Login again to keep your data synchronized across devices." = "Vous avez été automatiquement déconnecté. Connectez-vous à nouveau pour conserver vos données synchronisées entre vos appareils."; /* Add to Calendar alert title */ -"“%@” would like to access to your calendar" = "« %@ » souhaite accéder à vote calendrier."; +"“%@” would like to access to your calendar" = "« %@ » souhaite accéder à votre calendrier."; diff --git a/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json b/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json index 634c95379..c37b36fb7 100755 --- a/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json +++ b/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json @@ -5,7 +5,7 @@ "tvSiteName": "srf-player-tvos-apple", "voiceOverLanguageCode": "de", "appStoreProductIdentifier": 638194352, - "playURL": "https://www.srf.ch/play/", + "playURLs": "{\"rsi\":\"https://www.rsi.ch/play/\",\"rtr\":\"https://www.rtr.ch/play/\",\"rts\":\"https://www.rts.ch/play/\",\"srf\":\"https://www.srf.ch/play/\",\"swi\":\"https://play.swissinfo.ch/play/\"}", "playServiceURL": "https://www.srf.ch/play/", "middlewareURL": "https://playfff.herokuapp.com", "sourceCodeURL": "https://github.com/SRGSSR/playsrg-apple", @@ -27,6 +27,6 @@ "hiddenOnboardings": "account,favorites_account,resume_playback_account,watch_later_account", "audioDescriptionAvailabilityHidden": true, "posterImagesEnabled": true, - "tvThirdPartyChannelsAvailable": true, + "tvGuideOtherBouquets": "thirdparty,rts,rsi", "userConsentDefaultLanguage": "de" } diff --git a/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings b/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings index fef3ed4ae..40c74eaee 100755 --- a/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings @@ -100,9 +100,6 @@ /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Angemeldeter Nutzer: %@"; -/* Accessibility text for the login / signup header */ -"Login or sign up" = "Anmelden oder registrieren"; - /* Accessibility hint for the profile header when user is logged in */ "Manages account information" = "Anmeldeinformationen verwalten"; @@ -114,9 +111,6 @@ Button to access more episodes */ "More episodes" = "Mehr zur Sendung"; -/* Text displayed when a user is logged in but no information has been retrieved yet */ -"My account" = "Mein Konto"; - /* Next day button label in program guide */ "Next day" = "Nächster Tag"; @@ -186,7 +180,9 @@ /* Settings button label on home view */ "Settings" = "Einstellungen"; -/* Share button label on player view */ +/* Share button label on content page view + Share button label on player view + Share button label on section detail view */ "Share" = "Inhalt teilen"; /* Accessibility label of the song list handle when closed */ diff --git a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings index df46a28ee..300a8e333 100755 --- a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings @@ -72,7 +72,7 @@ /* Context menu action to add a show to favorites Label displayed in the show view when a show can be favorited */ -"Add to favorites" = "Zu Favoriten hinzufügen"; +"Add to favorites" = "Favoriten hinzufügen"; /* Advanced features section header */ "Advanced features" = "Erweiterte Funktionen"; @@ -681,6 +681,9 @@ /* Information message displayed when support information has been copied to the pasteboard */ "Support information has been copied to the pasteboard" = "Die Informationen wurden in die Zwischenablage kopiert"; +/* Label of the button to open Apple TestFlight application and see other testable builds */ +"Switch version" = "Version ändern"; + /* Login benefits description footer */ "Synchronize favorites, playback history and content saved for later on all devices connected to your account." = "Synchronisieren Sie den Wiedergabeverlauf, die Favoriten und die für später gemerkten Inhalte auf allen mit Ihrem Konto verbundenen Geräten."; @@ -760,6 +763,7 @@ "This might interest you" = "Das könnte Sie auch interessieren"; /* Advanced features section footer + Bottom additional information section footer Reset section footer */ "This section is only available in nightly and beta versions, and won't appear in the production version." = "Die erweiterten Funktionen sind nur in Nightlies oder Betas sichtbar."; diff --git a/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json b/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json index a0966fabc..e5a499552 100755 --- a/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json +++ b/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json @@ -5,7 +5,7 @@ "tvSiteName": "swi-player-tvos-apple", "voiceOverLanguageCode": "en", "appStoreProductIdentifier": 920785201, - "playURL": "https://play.swissinfo.ch/play/", + "playURLs": "{\"rsi\":\"https://www.rsi.ch/play/\",\"rtr\":\"https://www.rtr.ch/play/\",\"rts\":\"https://www.rts.ch/play/\",\"srf\":\"https://www.srf.ch/play/\",\"swi\":\"https://play.swissinfo.ch/play/\"}", "playServiceURL": "https://play.swissinfo.ch/play/", "middlewareURL": "https://playfff.herokuapp.com", "sourceCodeURL": "https://github.com/SRGSSR/playsrg-apple", @@ -16,6 +16,7 @@ "whatsNewURL": "https://srgssr.github.io/playsrg-apple/releases/release_notes-ios-swi.html", "downloadsHintsHidden": true, "showsUnavailable": true, + "predefinedShowPagePreferred": true, "continuousPlaybackPlayerViewTransitionDuration": 10, "continuousPlaybackForegroundTransitionDuration": 0, "continuousPlaybackBackgroundTransitionDuration": 0, diff --git a/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings b/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings index c7b9bf60b..db2a08fe6 100755 --- a/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings @@ -100,9 +100,6 @@ /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Logged in user: %@"; -/* Accessibility text for the login / signup header */ -"Login or sign up" = "Login or sign up"; - /* Accessibility hint for the profile header when user is logged in */ "Manages account information" = "Manages account information"; @@ -114,9 +111,6 @@ Button to access more episodes */ "More episodes" = "More episodes"; -/* Text displayed when a user is logged in but no information has been retrieved yet */ -"My account" = "My account"; - /* Next day button label in program guide */ "Next day" = "Next day"; @@ -186,7 +180,9 @@ /* Settings button label on home view */ "Settings" = "Settings"; -/* Share button label on player view */ +/* Share button label on content page view + Share button label on player view + Share button label on section detail view */ "Share" = "Share"; /* Accessibility label of the song list handle when closed */ diff --git a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings index f70c70b42..cff052b80 100755 --- a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings @@ -681,6 +681,9 @@ /* Information message displayed when support information has been copied to the pasteboard */ "Support information has been copied to the pasteboard" = "Support information has been copied to the pasteboard"; +/* Label of the button to open Apple TestFlight application and see other testable builds */ +"Switch version" = "Switch version"; + /* Login benefits description footer */ "Synchronize favorites, playback history and content saved for later on all devices connected to your account." = "Synchronize favorites, playback history and content saved for later on all devices connected to your account."; @@ -760,6 +763,7 @@ "This might interest you" = "This might interest you"; /* Advanced features section footer + Bottom additional information section footer Reset section footer */ "This section is only available in nightly and beta versions, and won't appear in the production version." = "This section is only available in nightly and beta versions, and won't appear in the production version."; diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt index 68bc4de35..37a786972 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt @@ -159,13 +159,13 @@ name: PLCrashReporter, nameSpecified: PLCrashReporter, owner: microsoft, version name: promises, nameSpecified: Promises, owner: google, version: 2.3.1, source: https://github.com/google/promises -name: srganalytics-apple, nameSpecified: SRGAnalytics, owner: SRGSSR, version: 9.0.1, source: https://github.com/SRGSSR/srganalytics-apple +name: srganalytics-apple, nameSpecified: SRGAnalytics, owner: SRGSSR, version: 9.0.2, source: https://github.com/SRGSSR/srganalytics-apple name: srgappearance-apple, nameSpecified: SRGAppearance, owner: SRGSSR, version: 5.2.1, source: https://github.com/SRGSSR/srgappearance-apple name: srgcontentprotection-apple, nameSpecified: SRGContentProtection, owner: SRGSSR, version: 3.1.0, source: https://github.com/SRGSSR/srgcontentprotection-apple -name: srgdataprovider-apple, nameSpecified: SRGDataProvider, owner: SRGSSR, version: 18.0.0, source: https://github.com/SRGSSR/srgdataprovider-apple +name: srgdataprovider-apple, nameSpecified: SRGDataProvider, owner: SRGSSR, version: 18.1.0, source: https://github.com/SRGSSR/srgdataprovider-apple name: srgdiagnostics-apple, nameSpecified: SRGDiagnostics, owner: SRGSSR, version: 3.1.0, source: https://github.com/SRGSSR/srgdiagnostics-apple diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist index e6b7ff4b0..6d4a35d44 100755 --- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist +++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist @@ -270,7 +270,7 @@ File com.mono0926.LicensePlist/srganalytics-apple Title - SRGAnalytics (9.0.1) + SRGAnalytics (9.0.2) Type PSChildPaneSpecifier @@ -294,7 +294,7 @@ File com.mono0926.LicensePlist/srgdataprovider-apple Title - SRGDataProvider (18.0.0) + SRGDataProvider (18.1.0) Type PSChildPaneSpecifier diff --git a/Application/Sources/Application/AppDelegate.m b/Application/Sources/Application/AppDelegate.m index 386666a5d..58db861f9 100755 --- a/Application/Sources/Application/AppDelegate.m +++ b/Application/Sources/Application/AppDelegate.m @@ -225,36 +225,7 @@ - (void)setupDataProvider #if defined(DEBUG) || defined(NIGHTLY) || defined(BETA) dataProvider.globalParameters = ApplicationSettingGlobalParameters(); - NSString *environment = nil; - - NSString *host = serviceURL.host; - if ([host containsString:@"test"]) { - environment = @"test"; - } - else if ([host containsString:@"stage"]) { - environment = @"stage"; - } - - if (environment) { - static dispatch_once_t s_onceToken2; - static NSDictionary *s_suffixes; - dispatch_once(&s_onceToken2, ^{ - s_suffixes = @{ @(SRGVendorRSI) : @"rsi", - @(SRGVendorRTR) : @"rtr", - @(SRGVendorRTS) : @"rts", - @(SRGVendorSRF) : @"srf", - @(SRGVendorSWI) : @"swi" }; - }); - SRGVendor vendor = ApplicationConfiguration.sharedApplicationConfiguration.vendor; - NSString *suffix = s_suffixes[@(vendor)]; - if (suffix) { - NSString *URLString = [NSString stringWithFormat:@"https://srgplayer-%@.%@.srf.ch/play/", suffix, environment]; - [ApplicationConfiguration.sharedApplicationConfiguration setOverridePlayURL:[NSURL URLWithString:URLString]]; - } - } - else { - [ApplicationConfiguration.sharedApplicationConfiguration setOverridePlayURL:nil]; - } + [ApplicationConfiguration.sharedApplicationConfiguration setOverridePlayURLForVendorBasedOnServiceURL:serviceURL]; #endif SRGDataProvider.currentDataProvider = dataProvider; } diff --git a/Application/Sources/Application/Navigation.swift b/Application/Sources/Application/Navigation.swift index fa368a7e9..45a13b4b5 100644 --- a/Application/Sources/Application/Navigation.swift +++ b/Application/Sources/Application/Navigation.swift @@ -67,8 +67,8 @@ extension UIViewController { } func navigateToShow(_ show: SRGShow, animated: Bool = true, completion: (() -> Void)? = nil) { - let showViewController = SectionViewController(section: .configured(.show(show))) - present(showViewController, animated: animated, completion: completion) + let pageViewController = PageViewController(id: .show(show)) + present(pageViewController, animated: animated, completion: completion) } func navigateToTopic(_ topic: SRGTopic, animated: Bool = true, completion: (() -> Void)? = nil) { @@ -224,8 +224,8 @@ extension UIViewController { } } receiveValue: { [weak self] show in guard let navigationController = self?.navigationController else { return } - let showViewController = SectionViewController.showViewController(for: show) - navigationController.pushViewController(showViewController, animated: animated) + let pageViewController = PageViewController(id: .show(show)) + navigationController.pushViewController(pageViewController, animated: animated) AnalyticsEvent.notification(action: .displayShow, from: .application, @@ -266,8 +266,8 @@ extension UIViewController { play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated, completion: nil) case let .show(show): guard let navigationController else { return } - let showViewController = SectionViewController.showViewController(for: show) - navigationController.pushViewController(showViewController, animated: animated) + let pageViewController = PageViewController(id: .show(show)) + navigationController.pushViewController(pageViewController, animated: animated) case let .topic(topic): guard let navigationController else { return } let pageViewController = PageViewController(id: .topic(topic)) diff --git a/Application/Sources/Application/SceneDelegate.m b/Application/Sources/Application/SceneDelegate.m index 4fd0ac1b3..6e633c845 100644 --- a/Application/Sources/Application/SceneDelegate.m +++ b/Application/Sources/Application/SceneDelegate.m @@ -458,15 +458,15 @@ - (void)openShowURN:(NSString *)showURN show:(SRGShow *)show fromPushNotificatio { [UserConsentHelper waitCollectingConsentRetain]; if (show) { - SectionViewController *showViewController = [SectionViewController showViewControllerFor:show fromPushNotification:fromPushNotification]; - [self.rootTabBarController pushViewController:showViewController animated:YES]; + PageViewController *pageViewController = [PageViewController showViewControllerFor:show fromPushNotification:fromPushNotification]; + [self.rootTabBarController pushViewController:pageViewController animated:YES]; [UserConsentHelper waitCollectingConsentRelease]; } else { [[SRGDataProvider.currentDataProvider showWithURN:showURN completionBlock:^(SRGShow * _Nullable show, NSHTTPURLResponse * _Nullable HTTPResponse, NSError * _Nullable error) { if (show) { - SectionViewController *showViewController = [SectionViewController showViewControllerFor:show fromPushNotification:fromPushNotification]; - [self.rootTabBarController pushViewController:showViewController animated:YES]; + PageViewController *pageViewController = [PageViewController showViewControllerFor:show fromPushNotification:fromPushNotification]; + [self.rootTabBarController pushViewController:pageViewController animated:YES]; } else { NSError *error = [NSError errorWithDomain:PlayErrorDomain @@ -486,8 +486,8 @@ - (void)openTopicURN:(NSString *)topicURN NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @keypath(SRGTopic.new, URN), topicURN]; SRGTopic *topic = [topics filteredArrayUsingPredicate:predicate].firstObject; if (topic) { - UIViewController *topicViewController = [PageViewController topicViewControllerFor:topic]; - [self.rootTabBarController pushViewController:topicViewController animated:YES]; + PageViewController *pageViewController = [PageViewController topicViewControllerFor:topic]; + [self.rootTabBarController pushViewController:pageViewController animated:YES]; } else { NSError *error = [NSError errorWithDomain:PlayErrorDomain diff --git a/Application/Sources/Configuration/ApplicationConfiguration.h b/Application/Sources/Configuration/ApplicationConfiguration.h index 19df10dd8..f3c3c3fde 100755 --- a/Application/Sources/Configuration/ApplicationConfiguration.h +++ b/Application/Sources/Configuration/ApplicationConfiguration.h @@ -38,7 +38,6 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification; @property (nonatomic, readonly, copy) NSNumber *appStoreProductIdentifier; -@property (nonatomic, readonly) NSURL *playURL; @property (nonatomic, readonly) NSURL *playServiceURL; @property (nonatomic, readonly) NSURL *middlewareURL; @property (nonatomic, readonly, nullable) NSURL *identityWebserviceURL; @@ -59,7 +58,6 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification; @property (nonatomic, readonly, getter=areDownloadsHintsHidden) BOOL downloadsHintsHidden; @property (nonatomic, readonly, getter=areShowsUnavailable) BOOL showsUnavailable; @property (nonatomic, readonly, getter=isTvGuideUnavailable) BOOL tvGuideUnavailable; -@property (nonatomic, readonly, getter=areTvThirdPartyChannelsAvailable) BOOL tvThirdPartyChannelsAvailable; @property (nonatomic, readonly, getter=isSubtitleAvailabilityHidden) BOOL subtitleAvailabilityHidden; @property (nonatomic, readonly, getter=isAudioDescriptionAvailabilityHidden) BOOL audioDescriptionAvailabilityHidden; @@ -77,6 +75,8 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification; @property (nonatomic, readonly) NSArray *tvChannels; @property (nonatomic, readonly) NSArray *satelliteRadioChannels; +@property (nonatomic, readonly) NSArray *tvGuideOtherBouquetsObjc; + @property (nonatomic, readonly) NSUInteger pageSize; // page size to be used in general throughout the app @property (nonatomic, readonly) NSUInteger detailPageSize; // page size to be used in general throughout the app @@ -95,6 +95,7 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification; @property (nonatomic, readonly, getter=isSearchSettingSubtitledHidden) BOOL searchSettingSubtitledHidden; @property (nonatomic, readonly, getter=isShowsSearchHidden) BOOL showsSearchHidden; +@property (nonatomic, readonly, getter=isPredefinedShowPagePreferred) BOOL predefinedShowPagePreferred; @property (nonatomic, readonly, getter=isShowLeadPreferred) BOOL showLeadPreferred; @property (nonatomic, readonly, copy, nullable) NSString *userConsentDefaultLanguage; @@ -104,6 +105,9 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification; - (nullable TVChannel *)tvChannelForUid:(nullable NSString *)uid; - (nullable __kindof Channel *)channelForUid:(nullable NSString *)uid; + +- (nullable NSURL *)playURLForVendor:(SRGVendor)vendor; + /** * URLs to be used for sharing */ @@ -114,9 +118,9 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification; #if defined(DEBUG) || defined(NIGHTLY) || defined(BETA) /** - * An optionnal override play URL for test and stage environnements. Use `playURL` property to get the current URL. + * Optionnal override play URLs for test and stage environnements. Use `playURLForVendor:` property to get the current URL. */ -- (void)setOverridePlayURL:(nullable NSURL *)overridePlayURL; +- (void)setOverridePlayURLForVendorBasedOnServiceURL:(nullable NSURL *)serviceURL; #endif diff --git a/Application/Sources/Configuration/ApplicationConfiguration.m b/Application/Sources/Configuration/ApplicationConfiguration.m index 438309599..7f2c654c8 100755 --- a/Application/Sources/Configuration/ApplicationConfiguration.m +++ b/Application/Sources/Configuration/ApplicationConfiguration.m @@ -122,7 +122,7 @@ @interface ApplicationConfiguration () @property (nonatomic, copy) NSNumber *appStoreProductIdentifier; -@property (nonatomic) NSURL *playURL; +@property (nonatomic) NSDictionary *playURLs; @property (nonatomic) NSURL *playServiceURL; @property (nonatomic) NSURL *middlewareURL; @property (nonatomic) NSURL *identityWebserviceURL; @@ -143,7 +143,6 @@ @interface ApplicationConfiguration () @property (nonatomic, getter=areDownloadsHintsHidden) BOOL downloadsHintsHidden; @property (nonatomic, getter=areShowsUnavailable) BOOL showsUnavailable; @property (nonatomic, getter=isTvGuideUnavailable) BOOL tvGuideUnavailable; -@property (nonatomic, getter=areTvThirdPartyChannelsAvailable) BOOL tvThirdPartyChannelsAvailable; @property (nonatomic, getter=isSubtitleAvailabilityHidden) BOOL subtitleAvailabilityHidden; @property (nonatomic, getter=isAudioDescriptionAvailabilityHidden) BOOL audioDescriptionAvailabilityHidden; @@ -163,6 +162,8 @@ @interface ApplicationConfiguration () @property (nonatomic) NSArray *satelliteRadioChannels; +@property (nonatomic) NSArray *tvGuideOtherBouquetsObjc; + @property (nonatomic) NSUInteger pageSize; @property (nonatomic) NSUInteger detailPageSize; @@ -179,12 +180,13 @@ @interface ApplicationConfiguration () @property (nonatomic, getter=isSearchSettingSubtitledHidden) BOOL searchSettingSubtitledHidden; @property (nonatomic, getter=isShowsSearchHidden) BOOL showsSearchHidden; +@property (nonatomic, getter=isPredefinedShowPagePreferred) BOOL predefinedShowPagePreferred; @property (nonatomic, getter=isShowLeadPreferred) BOOL showLeadPreferred; @property (nonatomic, copy) NSString *userConsentDefaultLanguage; #if defined(DEBUG) || defined(NIGHTLY) || defined(BETA) -@property (nonatomic) NSURL *overridePlayURL; +@property (nonatomic) NSDictionary *overridePlayURLs; #endif @end @@ -261,6 +263,43 @@ - (BOOL)arePosterImagesEnabled #endif } +#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA) +- (void)setOverridePlayURLForVendorBasedOnServiceURL:(NSURL *)serviceURL +{ + NSString *environment = nil; + + NSString *host = serviceURL.host; + if ([host containsString:@"test"]) { + environment = @"test"; + } + else if ([host containsString:@"stage"]) { + environment = @"stage"; + } + + if (environment) { + static dispatch_once_t s_onceToken; + static NSDictionary *s_vendorPaths; + dispatch_once(&s_onceToken, ^{ + s_vendorPaths = @{ @(SRGVendorRSI) : @"rsi", + @(SRGVendorRTR) : @"rtr", + @(SRGVendorRTS) : @"rts", + @(SRGVendorSRF) : @"srf", + @(SRGVendorSWI) : @"swi" }; + }); + + NSMutableDictionary *overridePlayURLs = [NSMutableDictionary new]; + for (NSNumber *vendorNumber in s_vendorPaths.allKeys) { + NSString *URLString = [NSString stringWithFormat:@"https://play-web.herokuapp.com/%@/%@/play/", s_vendorPaths[vendorNumber], environment]; + [overridePlayURLs setObject:[NSURL URLWithString:URLString] forKey:vendorNumber]; + } + self.overridePlayURLs = overridePlayURLs.copy; + } + else { + self.overridePlayURLs = nil; + } +} +#endif + #pragma mark Remote configuration // Return YES iff the activated remote configuration is valid, and stores the corresponding values. If the configuration @@ -306,8 +345,8 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba return NO; } - NSString *playURLString = [firebaseConfiguration stringForKey:@"playURL"]; - NSURL *playURL = playURLString ? [NSURL URLWithString:playURLString] : nil; + NSDictionary *playURLs = [firebaseConfiguration playURLsForKey:@"playURLs"]; + NSURL *playURL = playURLs[@(vendor)]; if (! playURL) { return NO; } @@ -346,7 +385,7 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba self.siteName = tvSiteName; #endif - self.playURL = playURL; + self.playURLs = playURLs; self.playServiceURL = playServiceURL; self.middlewareURL = middlewareURL; self.whatsNewURL = whatsNewURL; @@ -397,7 +436,6 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba self.downloadsHintsHidden = [firebaseConfiguration boolForKey:@"downloadsHintsHidden"]; self.showsUnavailable = [firebaseConfiguration boolForKey:@"showsUnavailable"]; self.tvGuideUnavailable = [firebaseConfiguration boolForKey:@"tvGuideUnavailable"]; - self.tvThirdPartyChannelsAvailable = [firebaseConfiguration boolForKey:@"tvThirdPartyChannelsAvailable"]; self.subtitleAvailabilityHidden = [firebaseConfiguration boolForKey:@"subtitleAvailabilityHidden"]; self.audioDescriptionAvailabilityHidden = [firebaseConfiguration boolForKey:@"audioDescriptionAvailabilityHidden"]; @@ -418,6 +456,8 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba self.tvChannels = [firebaseConfiguration tvChannelsForKey:@"tvChannels"]; self.satelliteRadioChannels = [firebaseConfiguration radioChannelsForKey:@"satelliteRadioChannels" defaultHomeSections:nil]; + self.tvGuideOtherBouquetsObjc = [firebaseConfiguration tvGuideOtherBouquetsForKey:@"tvGuideOtherBouquets" vendor:vendor]; + NSNumber *pageSize = [firebaseConfiguration numberForKey:@"pageSize"]; self.pageSize = pageSize ? MAX(pageSize.unsignedIntegerValue, 1) : 20; @@ -445,6 +485,7 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba self.searchSettingSubtitledHidden = [firebaseConfiguration boolForKey:@"searchSettingSubtitledHidden"]; self.showsSearchHidden = [firebaseConfiguration boolForKey:@"showsSearchHidden"]; + self.predefinedShowPagePreferred = [firebaseConfiguration boolForKey:@"predefinedShowPagePreferred"]; self.showLeadPreferred = [firebaseConfiguration boolForKey:@"showLeadPreferred"]; self.userConsentDefaultLanguage = [firebaseConfiguration stringForKey:@"userConsentDefaultLanguage"]; @@ -457,17 +498,6 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba #pragma mark Getters and setters -- (NSURL *)playURL -{ - NSURL *playURL = _playURL; -#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA) - if (self.overridePlayURL) { - playURL = self.overridePlayURL; - } -#endif - return playURL; -} - - (NSArray *)radioHomepageChannels { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == NO", @keypath(RadioChannel.new, homepageHidden)]; @@ -509,14 +539,25 @@ - (Channel *)channelForUid:(NSString *)uid return [self radioChannelForUid:uid] ?: [self tvChannelForUid:uid]; } +- (NSURL *)playURLForVendor:(SRGVendor)vendor +{ + NSDictionary *playURLs = _playURLs; +#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA) + if (self.overridePlayURLs) { + playURLs = self.overridePlayURLs; + } +#endif + return playURLs[@(vendor)]; +} + - (NSURL *)sharingURLForMedia:(SRGMedia *)media atTime:(CMTime)time { - if (! self.playURL || ! media) { + if (! media || ! [self playURLForVendor:media.vendor]) { return nil; } if ([SRGMedia PlayIsSwissTXTURN:media.URN]) { - NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:self.playURL resolvingAgainstBaseURL:NO]; + NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:[self playURLForVendor:media.vendor] resolvingAgainstBaseURL:NO]; URLComponents.path = [[[[URLComponents.path stringByAppendingPathComponent:@"tv"] stringByAppendingPathComponent:@"-"] stringByAppendingPathComponent:@"video"] @@ -525,7 +566,7 @@ - (NSURL *)sharingURLForMedia:(SRGMedia *)media atTime:(CMTime)time return URLComponents.URL; } else if (media.channel.vendor == SRGVendorSSATR) { - NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:self.playURL resolvingAgainstBaseURL:NO]; + NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:[self playURLForVendor:media.vendor] resolvingAgainstBaseURL:NO]; URLComponents.path = [URLComponents.path stringByAppendingPathComponent:@"embed"]; URLComponents.queryItems = @[ [NSURLQueryItem queryItemWithName:@"urn" value:media.URN] ]; return URLComponents.URL; @@ -543,7 +584,7 @@ - (NSURL *)sharingURLForMedia:(SRGMedia *)media atTime:(CMTime)time return nil; } - NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:self.playURL resolvingAgainstBaseURL:NO]; + NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:[self playURLForVendor:media.vendor] resolvingAgainstBaseURL:NO]; URLComponents.path = [[[[URLComponents.path stringByAppendingPathComponent:mediaTypeName] stringByAppendingPathComponent:@"redirect"] stringByAppendingPathComponent:@"detail"] @@ -562,7 +603,7 @@ - (NSURL *)sharingURLForMedia:(SRGMedia *)media atTime:(CMTime)time - (NSURL *)sharingURLForShow:(SRGShow *)show { - if (! self.playURL || ! show) { + if (! show || ! [self playURLForVendor:show.vendor]) { return nil; } @@ -579,7 +620,7 @@ - (NSURL *)sharingURLForShow:(SRGShow *)show return nil; } - NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:self.playURL resolvingAgainstBaseURL:NO]; + NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:[self playURLForVendor:show.vendor] resolvingAgainstBaseURL:NO]; URLComponents.path = [[[URLComponents.path stringByAppendingPathComponent:showTypeName] stringByAppendingPathComponent:@"quicklink"] stringByAppendingPathComponent:show.uid]; @@ -588,7 +629,7 @@ - (NSURL *)sharingURLForShow:(SRGShow *)show - (NSURL *)sharingURLForContentSection:(SRGContentSection *)contentSection { - if (! self.playURL || ! contentSection) { + if (! contentSection || ! [self playURLForVendor:contentSection.vendor]) { return nil; } @@ -596,7 +637,7 @@ - (NSURL *)sharingURLForContentSection:(SRGContentSection *)contentSection return nil; } - NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:self.playURL resolvingAgainstBaseURL:NO]; + NSURLComponents *URLComponents = [NSURLComponents componentsWithURL:[self playURLForVendor:contentSection.vendor] resolvingAgainstBaseURL:NO]; URLComponents.path = [[[URLComponents.path stringByAppendingPathComponent:@"tv"] stringByAppendingPathComponent:@"detail"] stringByAppendingPathComponent:@"section"]; diff --git a/Application/Sources/Configuration/ApplicationConfiguration.swift b/Application/Sources/Configuration/ApplicationConfiguration.swift index cec001d77..4a97f50ff 100644 --- a/Application/Sources/Configuration/ApplicationConfiguration.swift +++ b/Application/Sources/Configuration/ApplicationConfiguration.swift @@ -81,10 +81,16 @@ extension ApplicationConfiguration { return Self.typeformUrlWithParameters(feedbackUrl) } + + var tvGuideOtherBouquets: [TVGuideBouquet] { + return self.tvGuideOtherBouquetsObjc.map { number in + return TVGuideBouquet(rawValue: number.intValue)! + } + } } enum ConfiguredSection: Hashable { - case show(SRGShow) + case availableEpisodes(SRGShow) case favoriteShows case history diff --git a/Application/Sources/Configuration/PlayFirebaseConfiguration.h b/Application/Sources/Configuration/PlayFirebaseConfiguration.h index ce2f793b3..8fe0f7159 100644 --- a/Application/Sources/Configuration/PlayFirebaseConfiguration.h +++ b/Application/Sources/Configuration/PlayFirebaseConfiguration.h @@ -9,6 +9,7 @@ #import "TVChannel.h" @import Foundation; +@import SRGDataProviderModel; NS_ASSUME_NONNULL_BEGIN @@ -56,6 +57,16 @@ OBJC_EXPORT NSArray * _Nullable FirebaseConfigurat - (NSArray *)radioChannelsForKey:(NSString *)key defaultHomeSections:(nullable NSArray *)defaultHomeSections; - (NSArray *)tvChannelsForKey:(NSString *)key; +/** + * TV guide other bouquets accessor, main bouquet excluded. Return an empty array if no valid data is found under the specified key. + */ +- (NSArray *)tvGuideOtherBouquetsForKey:(NSString *)key vendor:(SRGVendor)vendor; + +/** + * Play URLs accessor. Return an empty dictionnary if no valid data is found under the specified key. + */ +- (NSDictionary *)playURLsForKey:(NSString *)key; + @end NS_ASSUME_NONNULL_END diff --git a/Application/Sources/Configuration/PlayFirebaseConfiguration.m b/Application/Sources/Configuration/PlayFirebaseConfiguration.m index 9a374e55f..caa31f8d4 100644 --- a/Application/Sources/Configuration/PlayFirebaseConfiguration.m +++ b/Application/Sources/Configuration/PlayFirebaseConfiguration.m @@ -59,6 +59,57 @@ static HomeSection HomeSectionWithString(NSString *string) return homeSections.copy; } +static NSNumber * TVGuideBouquetWithString(NSString *string) +{ + static dispatch_once_t s_onceToken; + static NSDictionary *s_bouqets; + dispatch_once(&s_onceToken, ^{ + s_bouqets = @{ @"thirdparty" : @(TVGuideBouquetThirdParty), + @"rsi" : @(TVGuideBouquetRSI), + @"rts" : @(TVGuideBouquetRTS), + @"srf" : @(TVGuideBouquetSRF) + }; + }); + return s_bouqets[string]; +} + +static BOOL TVGuideBouquetIsMainBouquet(TVGuideBouquet tvGuideBouquet, SRGVendor vendor) +{ + switch (tvGuideBouquet) { + case TVGuideBouquetThirdParty: + return NO; + case TVGuideBouquetRSI: + return vendor == SRGVendorRSI; + case TVGuideBouquetRTS: + return vendor == SRGVendorRTS; + case TVGuideBouquetSRF: + return vendor == SRGVendorSRF; + } +} + +NSArray *FirebaseConfigurationTVGuideOtherBouquets(NSString *string, SRGVendor vendor) +{ + NSMutableArray *tvGuideBouquets = [NSMutableArray array]; + + NSArray *tvGuideBouquetIdentifiers = [string componentsSeparatedByString:@","]; + for (NSString *identifier in tvGuideBouquetIdentifiers) { + NSNumber * tvGuideBouquet = TVGuideBouquetWithString(identifier); + if (tvGuideBouquet != nil) { + if (!([tvGuideBouquets containsObject:tvGuideBouquet] || TVGuideBouquetIsMainBouquet(tvGuideBouquet.intValue, vendor))) { + [tvGuideBouquets addObject:tvGuideBouquet]; + } + else { + PlayLogWarning(@"configuration", @"TV guide other bouquet identifier %@ duplicated or main one. Skipped.", identifier); + } + } + else { + PlayLogWarning(@"configuration", @"Unknown TV guide other bouquet identifier %@. Skipped.", identifier); + } + } + + return tvGuideBouquets.copy; +} + @interface PlayFirebaseConfiguration () @property (nonatomic) FIRRemoteConfig *remoteConfig; @@ -246,6 +297,46 @@ - (NSDictionary *)JSONDictionaryForKey:(NSString *)key return tvChannels.copy; } +- (NSArray *)tvGuideOtherBouquetsForKey:(NSString *)key vendor:(SRGVendor)vendor +{ + NSString *tvGuideBouquetsString = [self stringForKey:key]; + return tvGuideBouquetsString ? FirebaseConfigurationTVGuideOtherBouquets(tvGuideBouquetsString, vendor) : @[]; +} + +- (NSDictionary *)playURLsForKey:(NSString *)key +{ + static NSDictionary *s_vendors; + static dispatch_once_t s_onceToken; + dispatch_once(&s_onceToken, ^{ + s_vendors = @{ @"rsi" : @(SRGVendorRSI), + @"rtr" : @(SRGVendorRTR), + @"rts" : @(SRGVendorRTS), + @"srf" : @(SRGVendorSRF), + @"swi" : @(SRGVendorSWI) }; + }); + + NSMutableDictionary *playURLs = [NSMutableDictionary dictionary]; + + NSDictionary *playURLsDictionary = [self JSONDictionaryForKey:key]; + for (NSString *key in playURLsDictionary) { + NSNumber *vendor = s_vendors[key]; + if (vendor) { + NSURL *URL = [NSURL URLWithString:playURLsDictionary[key]]; + if (URL) { + playURLs[vendor] = URL; + } + else { + PlayLogWarning(@"configuration", @"Play URL configuration is not valid. The URL of %@ is not valid.", key); + } + } + else { + PlayLogWarning(@"configuration", @"Play URL configuration business unit identifier is not valid. %@ is not valid.", key); + } + } + + return playURLs.copy; +} + #pragma mark Update - (void)update diff --git a/Application/Sources/Configuration/TVChannel.h b/Application/Sources/Configuration/TVChannel.h index 961d952d5..3954a51db 100755 --- a/Application/Sources/Configuration/TVChannel.h +++ b/Application/Sources/Configuration/TVChannel.h @@ -8,6 +8,22 @@ NS_ASSUME_NONNULL_BEGIN +/** + * TV guide bouquet. + */ +typedef NS_CLOSED_ENUM(NSInteger, TVGuideBouquet) { + /** + * Third party bouquet. + */ + TVGuideBouquetThirdParty = 0, + /** + * SRG SSR bouquets. + */ + TVGuideBouquetRSI, + TVGuideBouquetRTS, + TVGuideBouquetSRF +}; + /** * Represent a TV channel in the application configuration. */ diff --git a/Application/Sources/Content/Content.swift b/Application/Sources/Content/Content.swift index 3cc2a311a..91aeae26b 100644 --- a/Application/Sources/Content/Content.swift +++ b/Application/Sources/Content/Content.swift @@ -13,13 +13,13 @@ private let kDefaultNumberOfLivestreamPlaceholders = 4 enum Content { enum Section: Hashable { - case content(SRGContentSection) + case content(SRGContentSection, show: SRGShow? = nil) case configured(ConfiguredSection) var properties: SectionProperties { switch self { - case let .content(section): - return ContentSectionProperties(contentSection: section) + case let .content(section, show): + return ContentSectionProperties(contentSection: section, show: show) case let .configured(section): return ConfiguredSectionProperties(configuredSection: section) } @@ -141,9 +141,9 @@ protocol SectionProperties { var emptyType: EmptyContentView.`Type` { get } var hasHighlightedItem: Bool { get } + var displayedShow: SRGShow? { get } #if os(iOS) var sharingItem: SharingItem? { get } - var displayedShow: SRGShow? { get } var canResetApplicationBadge: Bool { get } #endif @@ -175,6 +175,7 @@ protocol SectionProperties { private extension Content { struct ContentSectionProperties: SectionProperties { let contentSection: SRGContentSection + let show: SRGShow? private var presentation: SRGContentPresentation { return contentSection.presentation @@ -282,15 +283,14 @@ private extension Content { return presentation.type == .showPromotion } + var displayedShow: SRGShow? { + return show + } #if os(iOS) var sharingItem: SharingItem? { return SharingItem(for: contentSection) } - var displayedShow: SRGShow? { - return nil - } - var canResetApplicationBadge: Bool { return false } @@ -366,7 +366,7 @@ private extension Content { return [.showPlaceholder(index: 0)] case .topicSelector: return (0..() @@ -34,16 +35,9 @@ final class PageViewController: UIViewController { } private var refreshTriggered = false + private var showHeaderVisible = false #endif - private var globalHeaderTitle: String? { -#if os(tvOS) - return tabBarController == nil ? model.title : nil -#else - return nil -#endif - } - private var analyticsPageViewTracked = false private static func snapshot(from state: PageViewModel.State) -> NSDiffableDataSourceSnapshot { @@ -71,16 +65,21 @@ final class PageViewController: UIViewController { } #endif - init(id: PageViewModel.Id) { + init(id: PageViewModel.Id, fromPushNotification: Bool = false) { model = PageViewModel(id: id) + self.fromPushNotification = fromPushNotification super.init(nibName: nil, bundle: nil) - title = model.title + title = id.title } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + var id: PageViewModel.Id { + return model.id + } + @objc var radioChannel: RadioChannel? { if case let .audio(channel: channel) = model.id { return channel @@ -94,7 +93,7 @@ final class PageViewController: UIViewController { let view = UIView(frame: UIScreen.main.bounds) view.backgroundColor = .srgGray16 - let collectionView = CollectionView(frame: .zero, collectionViewLayout: layout()) + let collectionView = CollectionView(frame: .zero, collectionViewLayout: layout(for: model)) collectionView.delegate = self collectionView.backgroundColor = .clear view.addSubview(collectionView) @@ -135,10 +134,8 @@ final class PageViewController: UIViewController { super.viewDidLoad() #if os(iOS) - // Avoid iOS automatic scroll insets / offset bugs occurring if large titles are desired by a view controller - // but the navigation bar is hidden. The scroll insets are incorrect and sometimes the scroll offset might - // be incorrect at the top. - navigationItem.largeTitleDisplayMode = model.id.isNavigationBarHidden ? .never : .always + navigationItem.largeTitleDisplayMode = model.id.isLargeTitleDisplayMode ? .always : .never + showHeaderVisible = model.id.hasShowHeaderView #endif let cellRegistration = UICollectionView.CellRegistration, PageViewModel.Item> { [model] cell, _, item in @@ -151,7 +148,12 @@ final class PageViewController: UIViewController { let globalHeaderViewRegistration = UICollectionView.SupplementaryRegistration>(elementKind: Header.global.rawValue) { [weak self] view, _, _ in guard let self else { return } - view.content = TitleView(text: globalHeaderTitle) + view.content = TitleView(text: model.id.displayedTitle) + } + + let showHeaderViewRegistration = UICollectionView.SupplementaryRegistration>(elementKind: Header.showHeader.rawValue) { [weak self] view, _, _ in + guard let self else { return } + view.content = ShowHeaderView(show: model.id.displayedShow, horizontalPadding: Self.layoutHorizontalMargin) } let sectionHeaderViewRegistration = UICollectionView.SupplementaryRegistration>(elementKind: UICollectionView.elementKindSectionHeader) { [weak self] view, _, indexPath in @@ -165,6 +167,9 @@ final class PageViewController: UIViewController { if kind == Header.global.rawValue { return collectionView.dequeueConfiguredReusableSupplementary(using: globalHeaderViewRegistration, for: indexPath) } + if kind == Header.showHeader.rawValue { + return collectionView.dequeueConfiguredReusableSupplementary(using: showHeaderViewRegistration, for: indexPath) + } else { return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderViewRegistration, for: indexPath) } @@ -197,21 +202,48 @@ final class PageViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + updateLayoutConfiguration() model.reload() deselectItems(in: collectionView, animated: animated) #if os(iOS) updateNavigationBar(animated: animated) #endif + userActivity = model.userActivity + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + userActivity = nil + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + updateLayoutConfiguration() + } + + private func updateLayoutConfiguration() { + // Update configuration supplementary views layouts (ie: show header layout). + // Update configuration forces a collection view layout refresh. Updating only configuration.boundarySupplementaryItems does not. + if let collectionViewLayout = self.collectionView.collectionViewLayout as? UICollectionViewCompositionalLayout { + collectionViewLayout.configuration = Self.layoutConfiguration(model: model, layoutWidth: view.safeAreaLayoutGuide.layoutFrame.width, horizontalSizeClass: view.traitCollection.horizontalSizeClass, offsetX: view.safeAreaLayoutGuide.layoutFrame.minX) + } + } + + private func emptyViewEdgeInsets() -> EdgeInsets { + let configuration = Self.layoutConfiguration(model: model, layoutWidth: view.safeAreaLayoutGuide.layoutFrame.width, horizontalSizeClass: view.traitCollection.horizontalSizeClass, offsetX: view.safeAreaLayoutGuide.layoutFrame.minX) + let supplementaryItemsHeight = configuration.boundarySupplementaryItems.map { $0.layoutSize.heightDimension.dimension }.reduce(0, +) + return EdgeInsets(top: supplementaryItemsHeight, leading: 0, bottom: 0, trailing: 0) } private func reloadData(for state: PageViewModel.State) { switch state { case .loading: - emptyContentView.content = EmptyContentView(state: .loading) + emptyContentView.content = EmptyContentView(state: .loading, insets: emptyViewEdgeInsets()) case let .failed(error: error, _): - emptyContentView.content = EmptyContentView(state: .failed(error: error)) + emptyContentView.content = EmptyContentView(state: .failed(error: error), insets: emptyViewEdgeInsets()) case let .loaded(rows: rows, _): - emptyContentView.content = rows.isEmpty ? EmptyContentView(state: .empty(type: .generic)) : nil + emptyContentView.content = rows.isEmpty ? EmptyContentView(state: .empty(type: .generic), insets: emptyViewEdgeInsets()) : nil } DispatchQueue.global(qos: .userInteractive).async { @@ -253,7 +285,20 @@ final class PageViewController: UIViewController { self.googleCastButton?.removeFromSuperview() } + navigationItem.title = !showHeaderVisible ? title : nil navigationController?.setNavigationBarHidden(isNavigationBarHidden, animated: animated) + + if model.id.sharingItem != nil { + let shareButtonItem = UIBarButtonItem(image: UIImage(named: "share"), + style: .plain, + target: self, + action: #selector(self.shareContent(_:))) + shareButtonItem.accessibilityLabel = PlaySRGAccessibilityLocalizedString("Share", comment: "Share button label on content page view") + navigationItem.rightBarButtonItem = shareButtonItem + } + else { + navigationItem.rightBarButtonItem = nil + } } @objc private func pullToRefresh(_ refreshControl: RefreshControl) { @@ -262,6 +307,18 @@ final class PageViewController: UIViewController { } refreshTriggered = true } + + @objc private func shareContent(_ barButtonItem: UIBarButtonItem) { + guard let sharingItem = model.id.sharingItem else { return } + + let activityViewController = UIActivityViewController(sharingItem: sharingItem, from: .button) + activityViewController.modalPresentationStyle = .popover + + let popoverPresentationController = activityViewController.popoverPresentationController + popoverPresentationController?.barButtonItem = barButtonItem + + self.present(activityViewController, animated: true, completion: nil) + } #endif private func trackPageView(state: PageViewModel.State) { @@ -272,11 +329,11 @@ final class PageViewController: UIViewController { guard !self.analyticsPageViewTracked else { return } self.analyticsPageViewTracked = true - SRGAnalyticsTracker.shared.trackPageView(withTitle: model.analyticsPageViewTitle, - type: model.analyticsPageViewType, - levels: model.analyticsPageViewLevels, - labels: model.analyticsPageViewLabels(pageUid: pageUid), - fromPushNotification: false) + SRGAnalyticsTracker.shared.trackPageView(withTitle: model.id.analyticsPageViewTitle, + type: model.id.analyticsPageViewType, + levels: model.id.analyticsPageViewLevels, + labels: model.id.analyticsPageViewLabels(pageUid: pageUid), + fromPushNotification: fromPushNotification) } } } @@ -286,6 +343,7 @@ final class PageViewController: UIViewController { private extension PageViewController { enum Header: String { case global + case showHeader } #if os(iOS) @@ -298,21 +356,25 @@ private extension PageViewController { // MARK: Objective-C API extension PageViewController { - @objc static func videosViewController() -> UIViewController { + @objc static func videosViewController() -> PageViewController { return PageViewController(id: .video) } - @objc static func audiosViewController(forRadioChannel channel: RadioChannel) -> UIViewController { + @objc static func audiosViewController(forRadioChannel channel: RadioChannel) -> PageViewController { return PageViewController(id: .audio(channel: channel)) } - @objc static func liveViewController() -> UIViewController { + @objc static func liveViewController() -> PageViewController { return PageViewController(id: .live) } - @objc static func topicViewController(for topic: SRGTopic) -> UIViewController { + @objc static func topicViewController(for topic: SRGTopic) -> PageViewController { return PageViewController(id: .topic(topic)) } + + @objc static func showViewController(for show: SRGShow, fromPushNotification: Bool = false) -> PageViewController { + return PageViewController(id: .show(show), fromPushNotification: fromPushNotification) + } } // MARK: Protocols @@ -324,7 +386,7 @@ extension PageViewController: ContentInsets { var play_paddingContentInsets: UIEdgeInsets { #if os(iOS) - let top = isNavigationBarHidden ? 0 : Self.layoutVerticalMargin + let top = (isNavigationBarHidden || model.id.hasShowHeaderView) ? 0 : Self.layoutVerticalMargin #else let top = Self.layoutVerticalMargin #endif @@ -357,8 +419,8 @@ extension PageViewController: UICollectionViewDelegate { play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) case let .show(show): if let navigationController { - let showViewController = SectionViewController.showViewController(for: show) - navigationController.pushViewController(showViewController, animated: true) + let pageViewController = PageViewController(id: .show(show)) + navigationController.pushViewController(pageViewController, animated: true) } case let .topic(topic): if let navigationController { @@ -368,8 +430,8 @@ extension PageViewController: UICollectionViewDelegate { case let .highlight(_, highlightedItem): if let navigationController { if case let .show(show) = highlightedItem { - let showViewController = SectionViewController.showViewController(for: show) - navigationController.pushViewController(showViewController, animated: true) + let pageViewController = PageViewController(id: .show(show)) + navigationController.pushViewController(pageViewController, animated: true) } else { let sectionViewController = SectionViewController(section: section.wrappedValue, filter: model.id) @@ -412,6 +474,26 @@ extension PageViewController: UICollectionViewDelegate { return preview(for: configuration, in: collectionView) } + func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) { + switch elementKind { + case Header.showHeader.rawValue: + showHeaderVisible = true + updateNavigationBar(animated: true) + default: + break + } + } + + func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) { + switch elementKind { + case Header.showHeader.rawValue: + showHeaderVisible = false + updateNavigationBar(animated: true) + default: + break + } + } + private func preview(for configuration: UIContextMenuConfiguration, in collectionView: UICollectionView) -> UITargetedPreview? { guard let interactionView = ContextMenu.interactionView(in: collectionView, with: configuration) else { return nil } let parameters = UIPreviewParameters() @@ -519,113 +601,159 @@ extension PageViewController: TabBarActionable { #endif +extension PageViewController: ShowHeaderViewAction { + func showMore(sender: Any?, event: ShowMoreEvent?) { + guard let event else { return } + +#if os(iOS) + let sheetTextViewController = UIHostingController(rootView: SheetTextView(content: event.content)) + if #available(iOS 15.0, *) { + if let sheet = sheetTextViewController.sheetPresentationController { + sheet.detents = [.medium()] + } + } + present(sheetTextViewController, animated: true, completion: nil) +#else + navigateToText(event.content) +#endif + } +} + // MARK: Layout private extension PageViewController { private static let itemSpacing: CGFloat = constant(iOS: 8, tvOS: 40) private static let layoutHorizontalMargin: CGFloat = constant(iOS: 16, tvOS: 0) private static let layoutVerticalMargin: CGFloat = constant(iOS: 8, tvOS: 0) + private static let layoutHorizontalConfigurationViewMargin: CGFloat = constant(iOS: 0, tvOS: 8) - private func layoutConfiguration() -> UICollectionViewCompositionalLayoutConfiguration { + private static func layoutConfiguration(model: PageViewModel, layoutWidth: CGFloat, horizontalSizeClass: UIUserInterfaceSizeClass, offsetX: CGFloat) -> UICollectionViewCompositionalLayoutConfiguration { let configuration = UICollectionViewCompositionalLayoutConfiguration() configuration.interSectionSpacing = constant(iOS: 35, tvOS: 70) configuration.contentInsetsReference = constant(iOS: .automatic, tvOS: .layoutMargins) - let headerSize = TitleViewSize.recommended(forText: globalHeaderTitle) - let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: Header.global.rawValue, alignment: .topLeading) - configuration.boundarySupplementaryItems = [header] + if let show = model.id.displayedShow { + let showHeaderSize = ShowHeaderViewSize.recommended(for: show, horizontalPadding: layoutHorizontalMargin, layoutWidth: layoutWidth - layoutHorizontalConfigurationViewMargin * 2, horizontalSizeClass: horizontalSizeClass) + configuration.boundarySupplementaryItems = [ NSCollectionLayoutBoundarySupplementaryItem(layoutSize: showHeaderSize, elementKind: Header.showHeader.rawValue, alignment: .topLeading, absoluteOffset: CGPoint(x: offsetX + layoutHorizontalConfigurationViewMargin, y: 0)) ] + } + else if let title = model.id.displayedTitle { + let globalHeaderSize = TitleViewSize.recommended(forText: title) + configuration.boundarySupplementaryItems = [ NSCollectionLayoutBoundarySupplementaryItem(layoutSize: globalHeaderSize, elementKind: Header.global.rawValue, alignment: .topLeading, absoluteOffset: CGPoint(x: offsetX, y: 0)) ] + } return configuration } - private func layout() -> UICollectionViewLayout { + private func layout(for model: PageViewModel) -> UICollectionViewLayout { return UICollectionViewCompositionalLayout(sectionProvider: { [weak self] sectionIndex, layoutEnvironment in let layoutWidth = layoutEnvironment.container.effectiveContentSize.width + let horizontalSizeClass = layoutEnvironment.traitCollection.horizontalSizeClass - func sectionSupplementaryItems(for section: PageViewModel.Section) -> [NSCollectionLayoutBoundarySupplementaryItem] { + func sectionSupplementaryItems(for section: PageViewModel.Section, horizontalMargin: CGFloat) -> [NSCollectionLayoutBoundarySupplementaryItem] { let headerSize = SectionHeaderView.size(section: section, layoutWidth: layoutWidth) let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading) return [header] } + func horizontalMargin(for section: PageViewModel.Section) -> CGFloat { + switch section.viewModelProperties.layout { + case .mediaList: +#if os(iOS) + return horizontalSizeClass == .compact ? Self.layoutHorizontalMargin : Self.layoutHorizontalMargin * 2 +#else + return Self.layoutHorizontalMargin +#endif + default: + return Self.layoutHorizontalMargin + } + } + func layoutSection(for section: PageViewModel.Section) -> NSCollectionLayoutSection { - let horizontalSizeClass = layoutEnvironment.traitCollection.horizontalSizeClass - switch section.viewModelProperties.layout { case .heroStage: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in return HeroMediaCellSize.recommended(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass) } layoutSection.orthogonalScrollingBehavior = .groupPaging return layoutSection case .highlight: - return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, _ in + return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in return HighlightCellSize.fullWidth(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass) } case .headline: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in return FeaturedContentCellSize.headline(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass) } layoutSection.orthogonalScrollingBehavior = .groupPaging return layoutSection case .element: - return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, _ in + return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in return FeaturedContentCellSize.element(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass) } case .elementSwimlane: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in return FeaturedContentCellSize.element(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass) } layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary return layoutSection case .mediaSwimlane: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { _, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in return MediaCellSize.swimlane() } layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary return layoutSection case .liveMediaSwimlane: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { _, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in return LiveMediaCellSize.swimlane() } layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary return layoutSection case .showSwimlane: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { _, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in return ShowCellSize.swimlane(for: section.properties.imageVariant) } layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary return layoutSection case .topicSelector: - let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { _, _ in + let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in return TopicCellSize.swimlane() } layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary return layoutSection case .mediaGrid: if horizontalSizeClass == .compact { - return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { _, _ in + return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in return MediaCellSize.fullWidth() } } else { - return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, spacing in + return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in return MediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing) } } case .liveMediaGrid: - return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, spacing in + return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in return LiveMediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing) } case .showGrid: - return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { layoutWidth, spacing in + return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in return ShowCellSize.grid(for: section.properties.imageVariant, layoutWidth: layoutWidth, spacing: spacing) } #if os(iOS) case .showAccess: - return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: Self.layoutHorizontalMargin, spacing: Self.itemSpacing) { _, _ in + return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in return ShowAccessCellSize.fullWidth() } +#endif + case .mediaList: +#if os(iOS) + return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in + return MediaCellSize.fullWidth(horizontalSizeClass: horizontalSizeClass) + } +#else + return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in + return MediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing) + } #endif } } @@ -636,9 +764,9 @@ private extension PageViewController { let section = snapshot.sectionIdentifiers[sectionIndex] let layoutSection = layoutSection(for: section) - layoutSection.boundarySupplementaryItems = sectionSupplementaryItems(for: section) + layoutSection.boundarySupplementaryItems = sectionSupplementaryItems(for: section, horizontalMargin: horizontalMargin(for: section)) return layoutSection - }, configuration: layoutConfiguration()) + }, configuration: Self.layoutConfiguration(model: model, layoutWidth: 0, horizontalSizeClass: .unspecified, offsetX: 0)) } } @@ -660,7 +788,9 @@ private extension PageViewController { case .liveMediaSwimlane, .liveMediaGrid: LiveMediaCell(media: media) case .mediaGrid: - PlaySRG.MediaCell(media: media, style: .show) + PlaySRG.MediaCell(media: media, style: section.properties.displayedShow != nil ? .date : .show) + case .mediaList: + PlaySRG.MediaCell(media: media, style: .dateAndSummary, layout: .horizontal) default: PlaySRG.MediaCell(media: media, style: .show, layout: .vertical) } diff --git a/Application/Sources/Content/PageViewModel.swift b/Application/Sources/Content/PageViewModel.swift index 7161acdfc..73443ff06 100644 --- a/Application/Sources/Content/PageViewModel.swift +++ b/Application/Sources/Content/PageViewModel.swift @@ -11,63 +11,9 @@ import SRGDataProviderCombine final class PageViewModel: Identifiable, ObservableObject { let id: Id - var title: String? { - switch id { - case .video: - return NSLocalizedString("Videos", comment: "Title displayed at the top of the video view") - case .audio: - return NSLocalizedString("Audios", comment: "Title displayed at the top of the audio view") - case .live: - return NSLocalizedString("Livestreams", comment: "Title displayed at the top of the livestreams view") - case let .topic(topic): - return topic.title - } - } - @Published private(set) var state: State = .loading @Published private(set) var serviceMessage: ServiceMessage? - var analyticsPageViewTitle: String { - switch id { - case .video, .audio, .live: - return AnalyticsPageTitle.home.rawValue - case let .topic(topic): - return topic.title - } - } - - var analyticsPageViewType: String { - switch id { - case .video, .audio: - return AnalyticsPageType.landingPage.rawValue - case .live: - return AnalyticsPageType.live.rawValue - case .topic: - return AnalyticsPageType.overview.rawValue - } - } - - var analyticsPageViewLevels: [String]? { - switch id { - case .video: - return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue] - case let .audio(channel: channel): - return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.audio.rawValue, channel.name] - case .live: - return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.live.rawValue] - case .topic: - return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue, AnalyticsPageLevel.topic.rawValue] - } - } - - func analyticsPageViewLabels(pageUid: String?) -> SRGAnalyticsPageViewLabels? { - guard let pageUid else { return nil } - - let pageViewLabels = SRGAnalyticsPageViewLabels() - pageViewLabels.customInfo = ["pac_page_id": pageUid] - return pageViewLabels - } - private let trigger = Trigger() init(id: Id) { @@ -159,7 +105,7 @@ final class PageViewModel: Identifiable, ObservableObject { } private static func hasLoadMore(for section: Section, in sections: [Section]) -> Bool { - if section == sections.last && section.viewModelProperties.hasGridLayout { + if section == sections.last && section.viewModelProperties.hasLoadMore { return true } else { @@ -194,8 +140,21 @@ extension PageViewModel { case audio(channel: RadioChannel) case live case topic(_ topic: SRGTopic) + case show(_ show: SRGShow) #if os(iOS) + var isLargeTitleDisplayMode: Bool { + if case .show = self { + return false + } + else { + // Avoid iOS automatic scroll insets / offset bugs occurring if large titles are desired by a view controller + // but the navigation bar is hidden. The scroll insets are incorrect and sometimes the scroll offset might + // be incorrect at the top. + return !isNavigationBarHidden + } + } + var isNavigationBarHidden: Bool { switch self { case .video: @@ -204,6 +163,15 @@ extension PageViewModel { return false } } + + var sharingItem: SharingItem? { + switch self { + case let .show(show): + return SharingItem(for: show) + default: + return nil + } + } #endif var supportsCastButton: Bool { @@ -224,6 +192,93 @@ extension PageViewModel { } } + var title: String? { + switch self { + case .video: + return NSLocalizedString("Videos", comment: "Title displayed at the top of the video view") + case .audio: + return NSLocalizedString("Audios", comment: "Title displayed at the top of the audio view") + case .live: + return NSLocalizedString("Livestreams", comment: "Title displayed at the top of the livestreams view") + case let .topic(topic): + return topic.title + case let .show(show): + return show.title + } + } + + var displayedShow: SRGShow? { + if case let .show(show) = self { + return show + } + else { + return nil + } + } + + var hasShowHeaderView: Bool { + return displayedShow != nil + } + + var displayedTitle: String? { +#if os(tvOS) + if case .topic = self { + return title + } + else { + return nil + } +#else + return nil +#endif + } + + var analyticsPageViewTitle: String { + switch self { + case .video, .audio, .live: + return AnalyticsPageTitle.home.rawValue + case let .topic(topic): + return topic.title + case let .show(show): + return show.title + } + } + + var analyticsPageViewType: String { + switch self { + case .video, .audio: + return AnalyticsPageType.landingPage.rawValue + case .live: + return AnalyticsPageType.live.rawValue + case .topic, .show: + return AnalyticsPageType.overview.rawValue + } + } + + var analyticsPageViewLevels: [String]? { + switch self { + case .video: + return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue] + case let .audio(channel: channel): + return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.audio.rawValue, channel.name] + case .live: + return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.live.rawValue] + case .topic: + return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue, AnalyticsPageLevel.topic.rawValue] + case let .show(show): + let level1 = show.transmission == .radio ? AnalyticsPageLevel.audio.rawValue : AnalyticsPageLevel.video.rawValue + return [AnalyticsPageLevel.play.rawValue, level1, AnalyticsPageLevel.show.rawValue] + } + } + + func analyticsPageViewLabels(pageUid: String?) -> SRGAnalyticsPageViewLabels? { + guard let pageUid else { return nil } + + let pageViewLabels = SRGAnalyticsPageViewLabels() + pageViewLabels.customInfo = ["pac_page_id": pageUid] + return pageViewLabels + } + func canContain(show: SRGShow) -> Bool { switch self { case .video: @@ -294,6 +349,7 @@ extension PageViewModel { case liveMediaGrid case liveMediaSwimlane case mediaGrid + case mediaList case mediaSwimlane case showGrid case showSwimlane @@ -323,7 +379,7 @@ extension PageViewModel { var viewModelProperties: PageViewModelProperties { switch wrappedValue { - case let .content(section): + case let .content(section, _): return ContentSectionProperties(contentSection: section) case let .configured(section): return ConfiguredSectionProperties(configuredSection: section, index: index) @@ -355,6 +411,41 @@ extension PageViewModel { } } +// MARK: User activity + +extension PageViewModel { + var userActivity: NSUserActivity? { + { + guard let bundleIdentifier = Bundle.main.bundleIdentifier, + let applicationVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") else { + return nil + } + + if case let .show(show) = id { + guard let data = try? NSKeyedArchiver.archivedData(withRootObject: show, requiringSecureCoding: false) else { return nil } + let userActivity = NSUserActivity(activityType: bundleIdentifier.appending(".displaying")) + userActivity.title = String(format: NSLocalizedString("Display %@ episodes", comment: "User activity title when displaying a show page"), show.title) + userActivity.webpageURL = ApplicationConfiguration.shared.sharingURL(for: show) + userActivity.addUserInfoEntries(from: [ + "URNString": show.urn, + "SRGShowData": data, + "applicationVersion": applicationVersion + ]) +#if os(iOS) + userActivity.isEligibleForPrediction = true + userActivity.persistentIdentifier = show.urn + let suggestedInvocationPhraseFormat = show.transmission == .radio ? NSLocalizedString("Listen to %@", comment: "Suggested invocation phrase to listen to a show") : NSLocalizedString("Watch %@", comment: "Suggested invocation phrase to watch a show") + userActivity.suggestedInvocationPhrase = String(format: suggestedInvocationPhraseFormat, show.title) +#endif + return userActivity + } + else { + return nil + } + }() + } +} + // MARK: Publishers private extension PageViewModel { @@ -368,6 +459,17 @@ private extension PageViewModel { return SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, topicWithUrn: topic.urn) .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0), index: $1) }) } .eraseToAnyPublisher() + case let .show(show): + if show.transmission == .TV && !ApplicationConfiguration.shared.isPredefinedShowPagePreferred { + return SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, product: show.transmission == .radio ? .playAudio : .playVideo, showWithUrn: show.urn) + .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, show: show), index: $1) }) } + .eraseToAnyPublisher() + } + else { + return Just(Page(uid: nil, sections: [ Section(.configured(.availableEpisodes(show)), index: 0) ] )) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } case let .audio(channel: channel): return Just(Page(uid: nil, sections: channel.configuredSections().enumeratedMap { Section(.configured($0), index: $1) })) .setFailureType(to: Error.self) @@ -440,9 +542,9 @@ extension PageViewModelProperties { } #endif - var hasGridLayout: Bool { + var hasLoadMore: Bool { switch layout { - case .mediaGrid, .showGrid, .liveMediaGrid: + case .mediaGrid, .mediaList, .showGrid, .liveMediaGrid: return true default: return false @@ -482,6 +584,12 @@ private extension PageViewModel { return (contentSection.type == .shows) ? .showSwimlane : .mediaSwimlane case .grid: return (contentSection.type == .shows) ? .showGrid : .mediaGrid + case .availableEpisodes: +#if os(iOS) + return .mediaList +#else + return .mediaGrid +#endif case .livestreams: return .liveMediaSwimlane default: @@ -513,13 +621,19 @@ private extension PageViewModel { #else return .liveMediaSwimlane #endif - case .favoriteShows, .radioFavoriteShows, .show: + case .favoriteShows, .radioFavoriteShows: return .showSwimlane case .radioAllShows, .tvAllShows: return .showGrid #if os(iOS) case .radioShowAccess: return .showAccess +#endif + case .availableEpisodes: +#if os(iOS) + return .mediaList +#else + return .mediaGrid #endif default: return .mediaSwimlane diff --git a/Application/Sources/Content/Publishers.swift b/Application/Sources/Content/Publishers.swift index ff63c97f4..2da5c157e 100644 --- a/Application/Sources/Content/Publishers.swift +++ b/Application/Sources/Content/Publishers.swift @@ -203,6 +203,67 @@ extension SRGDataProvider { .map { filter?.compatibleShows($0) ?? $0 } .eraseToAnyPublisher() } + + func tvProgramsPublisher(day: SRGDay? = nil, mainProvider: Bool, minimal: Bool = false) -> AnyPublisher<[PlayProgramComposition], Error> { + let applicationConfiguration = ApplicationConfiguration.shared + if mainProvider { + return SRGDataProvider.current!.tvPrograms(for: applicationConfiguration.vendor, day: day, minimal: minimal) + .map { Array($0.map({ PlayProgramComposition(channel: $0.channel, programs: $0.programs, external: false) })) } + .eraseToAnyPublisher() + } + else { + let tvOtherPartyProgramsPublishers = applicationConfiguration.tvGuideOtherBouquets + .map { tvOtherPartyProgramsPublisher(day: day, bouquet: $0, minimal: minimal) } + return Publishers.concatenateMany(tvOtherPartyProgramsPublishers) + .tryReduce([]) { $0 + $1 } + .eraseToAnyPublisher() + } + } + + private func tvOtherPartyProgramsPublisher(day: SRGDay? = nil, bouquet: TVGuideBouquet, minimal: Bool = false) -> AnyPublisher<[PlayProgramComposition], Error> { + switch bouquet { + case .RSI: + return SRGDataProvider.current!.tvPrograms(for: .RSI, day: day, minimal: minimal) + .map { Array($0.map({ PlayProgramComposition(channel: $0.channel, programs: $0.programs, external: false) })) } + .eraseToAnyPublisher() + case .RTS: + return SRGDataProvider.current!.tvPrograms(for: .RTS, day: day, minimal: minimal) + .map { Array($0.map({ PlayProgramComposition(channel: $0.channel, programs: $0.programs, external: false) })) } + .eraseToAnyPublisher() + case .SRF: + return SRGDataProvider.current!.tvPrograms(for: .SRF, day: day, minimal: minimal) + .map { Array($0.map({ PlayProgramComposition(channel: $0.channel, programs: $0.programs, external: false) })) } + .eraseToAnyPublisher() + case .thirdParty: + return SRGDataProvider.current!.tvPrograms(for: ApplicationConfiguration.shared.vendor, provider: .thirdParty, day: day, minimal: minimal) + .map { Array($0.map({ PlayProgramComposition(channel: $0.channel, programs: $0.programs, external: true) })) } + .eraseToAnyPublisher() + } + } +} + +/// Input data for tv programs publisher +struct PlayProgramComposition: Hashable { + let channel: PlayChannel + let programs: [SRGProgram]? + + init(channel: SRGChannel, programs: [SRGProgram]?, external: Bool) { + self.channel = PlayChannel(wrappedValue: channel, external: external) + self.programs = programs + } +} + +struct PlayChannel: Hashable { + let wrappedValue: SRGChannel + let external: Bool +} + +extension Publishers { + static func concatenateMany(_ publishers: [AnyPublisher]) -> AnyPublisher { + return publishers.reduce(Empty().eraseToAnyPublisher()) { acc, elem in + Publishers.Concatenate(prefix: acc, suffix: elem).eraseToAnyPublisher() + } + } } enum UserDataPublishers { diff --git a/Application/Sources/Content/SectionViewController.swift b/Application/Sources/Content/SectionViewController.swift index 59e1c39dc..730abe079 100644 --- a/Application/Sources/Content/SectionViewController.swift +++ b/Application/Sources/Content/SectionViewController.swift @@ -32,7 +32,6 @@ final class SectionViewController: UIViewController { private weak var refreshControl: UIRefreshControl! private var refreshTriggered = false - private var firstHeaderVisible = true #endif private var contentInsets: UIEdgeInsets @@ -192,12 +191,6 @@ final class SectionViewController: UIViewController { model.reload() deselectItems(in: collectionView, animated: animated) navigationController?.setNavigationBarHidden(false, animated: animated) - userActivity = model.configuration.viewModelProperties.userActivity - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - userActivity = nil } #if os(iOS) @@ -237,20 +230,20 @@ final class SectionViewController: UIViewController { navigationItem.leftBarButtonItem = deleteBarButtonItem } else { - navigationItem.title = (model.displaysTitle || !firstHeaderVisible) ? model.title : nil + navigationItem.title = model.displaysTitle ? model.title : nil editButtonItem.title = NSLocalizedString("Select", comment: "Select button title") navigationItem.leftBarButtonItem = leftBarButtonItem } } else { - navigationItem.title = (model.displaysTitle || !firstHeaderVisible) ? model.title : nil + navigationItem.title = model.displaysTitle ? model.title : nil if model.configuration.properties.sharingItem != nil { let shareButtonItem = UIBarButtonItem(image: UIImage(resource: .share), style: .plain, target: self, action: #selector(self.shareContent(_:))) - shareButtonItem.accessibilityLabel = PlaySRGAccessibilityLocalizedString("Share", comment: "Share button label on player view") + shareButtonItem.accessibilityLabel = PlaySRGAccessibilityLocalizedString("Share", comment: "Share button label on section detail view") navigationItem.rightBarButtonItem = shareButtonItem } else { @@ -449,14 +442,6 @@ extension SectionViewController { @objc static func showsViewController(forChannelUid channelUid: String?) -> SectionViewController { return showsViewController(forChannelUid: channelUid, initialSectionId: nil) } - - @objc static func showViewController(for show: SRGShow, fromPushNotification: Bool) -> SectionViewController { - return SectionViewController(section: .configured(.show(show)), fromPushNotification: fromPushNotification) - } - - @objc static func showViewController(for show: SRGShow) -> SectionViewController { - return showViewController(for: show, fromPushNotification: false) - } } // MARK: Protocols @@ -538,30 +523,6 @@ extension SectionViewController: UICollectionViewDelegate { return preview(for: configuration, in: collectionView) } - func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) { - switch elementKind { - case UICollectionView.elementKindSectionHeader: - if indexPath.section == 0 { - firstHeaderVisible = true - updateNavigationBar() - } - default: - break - } - } - - func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) { - switch elementKind { - case UICollectionView.elementKindSectionHeader: - if indexPath.section == 0 { - firstHeaderVisible = false - updateNavigationBar() - } - default: - break - } - } - private func preview(for configuration: UIContextMenuConfiguration, in collectionView: UICollectionView) -> UITargetedPreview? { guard let interactionView = ContextMenu.interactionView(in: collectionView, with: configuration) else { return nil } let parameters = UIPreviewParameters() @@ -630,8 +591,8 @@ extension SectionViewController: SectionShowHeaderViewAction { navigateToShow(event.show) #else if let navigationController { - let showViewController = SectionViewController.showViewController(for: event.show) - navigationController.pushViewController(showViewController, animated: true) + let pageViewController = PageViewController(id: .show(event.show)) + navigationController.pushViewController(pageViewController, animated: true) } #endif } @@ -762,11 +723,26 @@ private extension SectionViewController { switch item { case let .media(media): switch configuration.wrappedValue { - case .content: - MediaCell(media: media, style: .show) + case let .content(contentSection, _): + switch contentSection.type { + case .predefined: + switch contentSection.presentation.type { + case .availableEpisodes: + if configuration.viewModelProperties.layout == .mediaList { + MediaCell(media: media, style: .dateAndSummary, layout: .horizontal) + } + else { + MediaCell(media: media, style: .date) + } + default: + MediaCell(media: media, style: .show) + } + default: + MediaCell(media: media, style: .show) + } case let .configured(configuredSection): switch configuredSection { - case .show: + case .availableEpisodes: if configuration.viewModelProperties.layout == .mediaList { MediaCell(media: media, style: .dateAndSummary, layout: .horizontal) } @@ -782,7 +758,7 @@ private extension SectionViewController { case let .show(show): let imageVariant = configuration.properties.imageVariant switch configuration.wrappedValue { - case let .content(contentSection): + case let .content(contentSection, _): switch contentSection.type { case .predefined: switch contentSection.presentation.type { @@ -837,8 +813,6 @@ private extension SectionViewController { default: Color.clear } - case let .show(show): - ShowHeaderView(show: show, horizontalPadding: SectionViewController.layoutHorizontalMargin) case .none: Color.clear } @@ -855,8 +829,6 @@ private extension SectionViewController { default: return NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(LayoutHeaderHeightZero)) } - case let .show(show): - return ShowHeaderViewSize.recommended(for: show, horizontalPadding: SectionViewController.layoutHorizontalMargin, layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass) case .none: return NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(LayoutHeaderHeightZero)) } diff --git a/Application/Sources/Content/SectionViewModel.swift b/Application/Sources/Content/SectionViewModel.swift index a36f17e15..9bf3e841e 100644 --- a/Application/Sources/Content/SectionViewModel.swift +++ b/Application/Sources/Content/SectionViewModel.swift @@ -142,7 +142,7 @@ extension SectionViewModel { var viewModelProperties: SectionViewModelProperties { switch wrappedValue { - case let .content(section): + case let .content(section, _): return ContentSectionProperties(contentSection: section) case let .configured(section): return ConfiguredSectionProperties(configuredSection: section) @@ -160,7 +160,6 @@ extension SectionViewModel { case none case title(String) case item(Content.Item) - case show(SRGShow) var sectionTopInset: CGFloat { switch self { @@ -175,7 +174,7 @@ extension SectionViewModel { switch self { case .title: return .small - case .item, .show: + case .item: return .large case .none: return .zero @@ -303,7 +302,6 @@ extension SectionViewModel { protocol SectionViewModelProperties { var layout: SectionViewModel.SectionLayout { get } var pinHeadersToVisibleBounds: Bool { get } - var userActivity: NSUserActivity? { get } var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { get } func rows(from items: [SectionViewModel.Item]) -> [SectionViewModel.Row] @@ -327,6 +325,12 @@ private extension SectionViewModel { return .liveMediaGrid case .swimlane, .grid: return (contentSection.type == .shows) ? .showGrid : .mediaGrid + case .availableEpisodes: +#if os(iOS) + return .mediaList +#else + return .mediaGrid +#endif default: return .mediaGrid } @@ -354,10 +358,6 @@ private extension SectionViewModel { #endif } - var userActivity: NSUserActivity? { - return nil - } - var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { switch contentSection.type { case .showAndMedias: @@ -405,7 +405,7 @@ private extension SectionViewModel { case .notifications: return .notificationList #endif - case .show: + case .availableEpisodes: #if os(iOS) return .mediaList #else @@ -429,44 +429,8 @@ private extension SectionViewModel { #endif } - var userActivity: NSUserActivity? { - guard let bundleIdentifier = Bundle.main.bundleIdentifier, - let applicationVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") else { - return nil - } - - switch configuredSection { - case let .show(show): - guard let data = try? NSKeyedArchiver.archivedData(withRootObject: show, requiringSecureCoding: false) else { return nil } - let userActivity = NSUserActivity(activityType: bundleIdentifier.appending(".displaying")) - userActivity.title = String(format: NSLocalizedString("Display %@ episodes", comment: "User activity title when displaying a show page"), show.title) - userActivity.webpageURL = ApplicationConfiguration.shared.sharingURL(for: show) - userActivity.addUserInfoEntries(from: [ - "URNString": show.urn, - "SRGShowData": data, - "applicationVersion": applicationVersion - ]) - -#if os(iOS) - userActivity.isEligibleForPrediction = true - userActivity.persistentIdentifier = show.urn - let suggestedInvocationPhraseFormat = (show.transmission == .radio) ? NSLocalizedString("Listen to %@", comment: "Suggested invocation phrase to listen to a show") : NSLocalizedString("Watch %@", comment: "Suggested invocation phrase to watch a show") - userActivity.suggestedInvocationPhrase = String(format: suggestedInvocationPhraseFormat, show.title) -#endif - - return userActivity - default: - return nil - } - } - var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { - switch configuredSection { - case .show: - return .never - default: - return .always - } + return .always } func rows(from items: [SectionViewModel.Item]) -> [SectionViewModel.Row] { @@ -475,8 +439,6 @@ private extension SectionViewModel { return SectionViewModel.alphabeticalRows(from: items, smart: true) case .radioAllShows, .tvAllShows: return SectionViewModel.alphabeticalRows(from: items, smart: false) - case let .show(show): - return SectionViewModel.consolidatedRows(with: items, header: .show(show)) #if os(iOS) case .downloads: return SectionViewModel.consolidatedRows(with: items, footer: .diskInfo) diff --git a/Application/Sources/Content/ShowHeaderView.swift b/Application/Sources/Content/ShowHeaderView.swift index 235a687a2..90f39c343 100644 --- a/Application/Sources/Content/ShowHeaderView.swift +++ b/Application/Sources/Content/ShowHeaderView.swift @@ -30,14 +30,14 @@ class ShowMoreEvent: UIEvent { /// Behavior: h-hug, v-hug struct ShowHeaderView: View { - @Binding private(set) var show: SRGShow + @Binding private(set) var show: SRGShow? let horizontalPadding: CGFloat @StateObject private var model = ShowHeaderViewModel() fileprivate static let verticalSpacing: CGFloat = 24 - init(show: SRGShow, horizontalPadding: CGFloat) { + init(show: SRGShow?, horizontalPadding: CGFloat) { _show = .constant(show) self.horizontalPadding = horizontalPadding } @@ -242,12 +242,17 @@ struct ShowHeaderView: View { // MARK: Size enum ShowHeaderViewSize { - static func recommended(for show: SRGShow, horizontalPadding: CGFloat, layoutWidth: CGFloat, horizontalSizeClass: UIUserInterfaceSizeClass) -> NSCollectionLayoutSize { - let fittingSize = CGSize(width: layoutWidth, height: UIView.layoutFittingExpandedSize.height) - let model = ShowHeaderViewModel() - model.show = show - let size = ShowHeaderView.MainView(model: model, horizontalPadding: horizontalPadding).adaptiveSizeThatFits(in: fittingSize, for: horizontalSizeClass) - return NSCollectionLayoutSize(widthDimension: .absolute(layoutWidth), heightDimension: .absolute(size.height)) + static func recommended(for show: SRGShow?, horizontalPadding: CGFloat, layoutWidth: CGFloat, horizontalSizeClass: UIUserInterfaceSizeClass) -> NSCollectionLayoutSize { + if let show { + let fittingSize = CGSize(width: layoutWidth, height: UIView.layoutFittingExpandedSize.height) + let model = ShowHeaderViewModel() + model.show = show + let size = ShowHeaderView.MainView(model: model, horizontalPadding: horizontalPadding).adaptiveSizeThatFits(in: fittingSize, for: horizontalSizeClass) + return NSCollectionLayoutSize(widthDimension: .absolute(size.width), heightDimension: .absolute(size.height)) + } + else { + return NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(LayoutHeaderHeightZero)) + } } } diff --git a/Application/Sources/Content/ShowHeaderViewModel.swift b/Application/Sources/Content/ShowHeaderViewModel.swift index fd0a9392a..ddbeb6fff 100644 --- a/Application/Sources/Content/ShowHeaderViewModel.swift +++ b/Application/Sources/Content/ShowHeaderViewModel.swift @@ -99,7 +99,7 @@ final class ShowHeaderViewModel: ObservableObject { #if os(iOS) var isSubscriptionPossible: Bool { - return PushService.shared != nil && isFavorite + return PushService.shared != nil } var subscriptionIcon: ImageResource { diff --git a/Application/Sources/Favorites/Favorites.m b/Application/Sources/Favorites/Favorites.m index 53ac33bb5..99ce34aa4 100755 --- a/Application/Sources/Favorites/Favorites.m +++ b/Application/Sources/Favorites/Favorites.m @@ -104,7 +104,7 @@ BOOL FavoritesIsSubscribedToShow(SRGShow *show) BOOL FavoritesToggleSubscriptionForShow(SRGShow *show) { if (! FavoritesContainsShow(show)) { - return NO; + FavoritesToggleShow(show); } if (! [PushService.sharedService toggleSubscriptionForShow:show]) { diff --git a/Application/Sources/Helpers/ApplicationSectionInfo.m b/Application/Sources/Helpers/ApplicationSectionInfo.m index 9ccbc75b0..8071fa5d7 100755 --- a/Application/Sources/Helpers/ApplicationSectionInfo.m +++ b/Application/Sources/Helpers/ApplicationSectionInfo.m @@ -58,7 +58,7 @@ + (ApplicationSectionInfo *)applicationSectionInfoWithNotification:(UserNotifica { NSMutableArray *sectionInfos = [NSMutableArray array]; #if TARGET_OS_IOS - if (PushService.sharedService.enabled) { + if (PushService.sharedService != nil) { [sectionInfos addObject:[self applicationSectionInfoWithApplicationSection:ApplicationSectionNotifications radioChannel:nil]]; } #endif diff --git a/Application/Sources/Helpers/ProgramAndChannel.swift b/Application/Sources/Helpers/ProgramAndChannel.swift index 83b9c34dd..825f1cbb5 100644 --- a/Application/Sources/Helpers/ProgramAndChannel.swift +++ b/Application/Sources/Helpers/ProgramAndChannel.swift @@ -8,14 +8,14 @@ import SRGDataProviderModel struct ProgramAndChannel: Hashable { let program: SRGProgram - let channel: SRGChannel + let channel: PlayChannel func programGuideImageUrl(size: SRGImageSize) -> URL? { if let image = program.image { return PlaySRG.url(for: image, size: size) } // Couldn't use channel image in Play SRG image service. Use raw image. - else if let channelRawImage = channel.rawImage { + else if let channelRawImage = channel.wrappedValue.rawImage { return PlaySRG.url(for: channelRawImage, size: size) } else { diff --git a/Application/Sources/Model/Mock.swift b/Application/Sources/Model/Mock.swift index 113aee5f3..c4a3f23df 100644 --- a/Application/Sources/Model/Mock.swift +++ b/Application/Sources/Model/Mock.swift @@ -28,6 +28,10 @@ enum Mock { return mockObject(kind.rawValue, type: SRGChannel.self) } + static func playChannel(_ kind: Channel = .standard) -> PlayChannel { + return PlayChannel(wrappedValue: mockObject(kind.rawValue, type: SRGChannel.self), external: false) + } + enum ContentSection: String { case standard case overflow diff --git a/Application/Sources/Player/MediaPlayerViewController.m b/Application/Sources/Player/MediaPlayerViewController.m index 0b899f529..178744bfb 100755 --- a/Application/Sources/Player/MediaPlayerViewController.m +++ b/Application/Sources/Player/MediaPlayerViewController.m @@ -2100,8 +2100,8 @@ - (IBAction)openShow:(id)sender SceneDelegate *sceneDelegate = UIApplication.sharedApplication.mainSceneDelegate; [sceneDelegate.rootTabBarController openApplicationSectionInfo:applicationSectionInfo]; - SectionViewController *showViewController = [SectionViewController showViewControllerFor:show]; - [sceneDelegate.rootTabBarController pushViewController:showViewController animated:NO]; + PageViewController *pageViewController = [PageViewController showViewControllerFor:show fromPushNotification:NO]; + [sceneDelegate.rootTabBarController pushViewController:pageViewController animated:NO]; [sceneDelegate.window play_dismissAllViewControllersWithAnimated:YES completion:nil]; } diff --git a/Application/Sources/ProgramGuide/ProgramCell.swift b/Application/Sources/ProgramGuide/ProgramCell.swift index bafa5758d..49cf241c4 100644 --- a/Application/Sources/ProgramGuide/ProgramCell.swift +++ b/Application/Sources/ProgramGuide/ProgramCell.swift @@ -17,7 +17,7 @@ struct ProgramCell: View { @Environment(\.isSelected) private var isSelected - init(program: SRGProgram, channel: SRGChannel, direction: StackDirection) { + init(program: SRGProgram, channel: PlayChannel, direction: StackDirection) { _data = .constant(.init(program: program, channel: channel)) self.direction = direction } @@ -170,39 +170,39 @@ struct ProgramCell_Previews: PreviewProvider { private static let height: CGFloat = constant(iOS: 80, tvOS: 120) static var previews: some View { - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .horizontal) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .horizontal) .previewLayout(.fixed(width: size.width, height: size.height)) .background(Color.white) .previewDisplayName("horizontal") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 500, height: height)) .background(Color.white) .previewDisplayName("vertical, w 500") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 80, height: height)) .background(Color.white) .previewDisplayName("vertical, w 80") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 40, height: height)) .background(Color.white) .previewDisplayName("vertical, w 40") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 30, height: height)) .background(Color.white) .previewDisplayName("vertical, w 30") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 24, height: height)) .background(Color.white) .previewDisplayName("vertical, w 24") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 20, height: height)) .background(Color.white) .previewDisplayName("vertical, w 20") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 10, height: height)) .background(Color.white) .previewDisplayName("vertical, w 10") - ProgramCell(program: Mock.program(), channel: Mock.channel(), direction: .vertical) + ProgramCell(program: Mock.program(), channel: Mock.playChannel(), direction: .vertical) .previewLayout(.fixed(width: 1, height: height)) .background(Color.white) .previewDisplayName("vertical, w 1") diff --git a/Application/Sources/ProgramGuide/ProgramCellViewModel.swift b/Application/Sources/ProgramGuide/ProgramCellViewModel.swift index 7f95f114d..03c0cc55b 100644 --- a/Application/Sources/ProgramGuide/ProgramCellViewModel.swift +++ b/Application/Sources/ProgramGuide/ProgramCellViewModel.swift @@ -24,7 +24,7 @@ final class ProgramCellViewModel: ObservableObject { } var accessibilityLabel: String? { - return data?.program.play_accessibilityLabel(with: data?.channel) + return data?.program.play_accessibilityLabel(with: data?.channel.wrappedValue) } var timeRange: String? { @@ -36,8 +36,7 @@ final class ProgramCellViewModel: ObservableObject { } var canPlay: Bool { - // The TV channel must be a BU channel to be playable (as declared by the application configuration) - guard let channel = data?.channel, ApplicationConfiguration.shared.tvChannel(forUid: channel.uid) != nil else { + guard let channel = data?.channel, !channel.external else { return false } return progress != nil || data?.program.mediaURN != nil diff --git a/Application/Sources/ProgramGuide/ProgramGuideDailyViewController.swift b/Application/Sources/ProgramGuide/ProgramGuideDailyViewController.swift index 913bd803d..533fecb8b 100644 --- a/Application/Sources/ProgramGuide/ProgramGuideDailyViewController.swift +++ b/Application/Sources/ProgramGuide/ProgramGuideDailyViewController.swift @@ -28,7 +28,7 @@ final class ProgramGuideDailyViewController: UIViewController { return model.day } - private static func snapshot(from state: ProgramGuideDailyViewModel.State, for channel: SRGChannel?) -> NSDiffableDataSourceSnapshot { + private static func snapshot(from state: ProgramGuideDailyViewModel.State, for channel: PlayChannel?) -> NSDiffableDataSourceSnapshot { var snapshot = NSDiffableDataSourceSnapshot() if let channel { snapshot.appendSections([channel]) @@ -42,7 +42,7 @@ final class ProgramGuideDailyViewController: UIViewController { model = programGuideDailyModel } else { - model = ProgramGuideDailyViewModel(day: day, firstPartyChannels: programGuideModel.firstPartyChannels, thirdPartyChannels: programGuideModel.thirdPartyChannels) + model = ProgramGuideDailyViewModel(day: day, mainPartyChannels: programGuideModel.mainPartyChannels, otherPartyChannels: programGuideModel.otherPartyChannels) } self.programGuideModel = programGuideModel scrollTargetTime = programGuideModel.time @@ -122,11 +122,11 @@ final class ProgramGuideDailyViewController: UIViewController { scrollToTime(programGuideModel.time, animated: false) } - private func reloadData(for channel: SRGChannel? = nil) { + private func reloadData(for channel: PlayChannel? = nil) { reloadData(for: model.state, channel: channel) } - private func reloadData(for state: ProgramGuideDailyViewModel.State, channel: SRGChannel? = nil) { + private func reloadData(for state: ProgramGuideDailyViewModel.State, channel: PlayChannel? = nil) { let currentChannel = channel ?? self.programGuideModel.selectedChannel switch state { diff --git a/Application/Sources/ProgramGuide/ProgramGuideDailyViewModel.swift b/Application/Sources/ProgramGuide/ProgramGuideDailyViewModel.swift index e49caec5a..3a773e4c8 100644 --- a/Application/Sources/ProgramGuide/ProgramGuideDailyViewModel.swift +++ b/Application/Sources/ProgramGuide/ProgramGuideDailyViewModel.swift @@ -14,9 +14,9 @@ final class ProgramGuideDailyViewModel: ObservableObject { @Published private(set) var state: State /// Channels can be provided if available for more efficient content loading - init(day: SRGDay, firstPartyChannels: [SRGChannel], thirdPartyChannels: [SRGChannel]) { + init(day: SRGDay, mainPartyChannels: [PlayChannel], otherPartyChannels: [PlayChannel]) { self.day = day - self.state = .loading(firstPartyChannels: firstPartyChannels, thirdPartyChannels: thirdPartyChannels, day: day) + self.state = .loading(mainPartyChannels: mainPartyChannels, otherPartyChannels: otherPartyChannels, day: day) Publishers.PublishAndRepeat(onOutputFrom: ApplicationSignal.wokenUp()) { [weak self, $day] in $day @@ -36,7 +36,7 @@ final class ProgramGuideDailyViewModel: ObservableObject { // MARK: Types extension ProgramGuideDailyViewModel { - typealias Section = SRGChannel + typealias Section = PlayChannel struct Item: Hashable { enum WrappedValue: Hashable { @@ -77,8 +77,8 @@ extension ProgramGuideDailyViewModel { } enum Bouquet { - case loading(channels: [SRGChannel]) - case content(programCompositions: [SRGProgramComposition]) + case loading(channels: [PlayChannel]) + case content(programCompositions: [PlayProgramComposition]) fileprivate static var empty: Self { return .content(programCompositions: []) @@ -114,7 +114,7 @@ extension ProgramGuideDailyViewModel { } } - fileprivate var channels: [SRGChannel] { + fileprivate var channels: [PlayChannel] { switch self { case let .loading(channels: channels): return channels @@ -123,15 +123,15 @@ extension ProgramGuideDailyViewModel { } } - fileprivate func contains(channel: SRGChannel) -> Bool { + fileprivate func contains(channel: PlayChannel) -> Bool { return channels.contains(channel) } - private static func programs(for channel: SRGChannel, in programCompositions: [SRGProgramComposition]) -> [SRGProgram] { + private static func programs(for channel: PlayChannel, in programCompositions: [PlayProgramComposition]) -> [SRGProgram] { return programCompositions.first(where: { $0.channel == channel })?.programs ?? [] } - fileprivate func isEmpty(for channel: SRGChannel) -> Bool { + fileprivate func isEmpty(for channel: PlayChannel) -> Bool { switch self { case .loading: return false @@ -140,7 +140,7 @@ extension ProgramGuideDailyViewModel { } } - fileprivate func items(for channel: SRGChannel, day: SRGDay) -> [Item] { + fileprivate func items(for channel: PlayChannel, day: SRGDay) -> [Item] { switch self { case .loading: return [Item(wrappedValue: .loading, section: channel, day: day)] @@ -157,16 +157,16 @@ extension ProgramGuideDailyViewModel { } enum State { - case content(firstPartyBouquet: Bouquet, thirdPartyBouquet: Bouquet, day: SRGDay) + case content(mainPartyBouquet: Bouquet, otherPartyBouquet: Bouquet, day: SRGDay) case failed(error: Error) - fileprivate static func loading(firstPartyChannels: [SRGChannel], thirdPartyChannels: [SRGChannel], day: SRGDay) -> Self { - return .content(firstPartyBouquet: .loading(channels: firstPartyChannels), thirdPartyBouquet: .loading(channels: thirdPartyChannels), day: day) + fileprivate static func loading(mainPartyChannels: [PlayChannel], otherPartyChannels: [PlayChannel], day: SRGDay) -> Self { + return .content(mainPartyBouquet: .loading(channels: mainPartyChannels), otherPartyBouquet: .loading(channels: otherPartyChannels), day: day) } private var day: SRGDay? { switch self { - case let .content(firstPartyBouquet: _, thirdPartyBouquet: _, day: day): + case let .content(mainPartyBouquet: _, otherPartyBouquet: _, day: day): return day case .failed: return nil @@ -175,8 +175,8 @@ extension ProgramGuideDailyViewModel { private var bouquets: [Bouquet] { switch self { - case let .content(firstPartyBouquet: firstPartyBouquet, thirdPartyBouquet: thirdPartyBouquet, day: _): - return [firstPartyBouquet, thirdPartyBouquet] + case let .content(mainPartyBouquet: mainPartyBouquet, otherPartyBouquet: otherPartyBouquet, day: _): + return [mainPartyBouquet, otherPartyBouquet] case .failed: return [] } @@ -188,12 +188,12 @@ extension ProgramGuideDailyViewModel { private func bouquet(for section: Section) -> Bouquet? { switch self { - case let .content(firstPartyBouquet: firstPartyBouquet, thirdPartyBouquet: thirdPartyBouquet, day: _): - if firstPartyBouquet.contains(channel: section) { - return firstPartyBouquet + case let .content(mainPartyBouquet: mainPartyBouquet, otherPartyBouquet: otherPartyBouquet, day: _): + if mainPartyBouquet.contains(channel: section) { + return mainPartyBouquet } - else if thirdPartyBouquet.contains(channel: section) { - return thirdPartyBouquet + else if otherPartyBouquet.contains(channel: section) { + return otherPartyBouquet } else { return nil @@ -245,38 +245,38 @@ private extension ProgramGuideDailyViewModel { static func state(from state: State?, for day: SRGDay) -> AnyPublisher { let applicationConfiguration = ApplicationConfiguration.shared let vendor = applicationConfiguration.vendor - if applicationConfiguration.areTvThirdPartyChannelsAvailable { + if !applicationConfiguration.tvGuideOtherBouquets.isEmpty { return Publishers.CombineLatest( - Self.bouquet(for: vendor, provider: .SRG, day: day, from: state), - Self.bouquet(for: vendor, provider: .thirdParty, day: day, from: state) + Self.bouquet(for: vendor, mainProvider: true, day: day, from: state), + Self.bouquet(for: vendor, mainProvider: false, day: day, from: state) ) - .map { .content(firstPartyBouquet: $0, thirdPartyBouquet: $1, day: day) } + .map { .content(mainPartyBouquet: $0, otherPartyBouquet: $1, day: day) } .eraseToAnyPublisher() } else { - return Self.bouquet(for: vendor, provider: .SRG, day: day, from: state) - .map { .content(firstPartyBouquet: $0, thirdPartyBouquet: .empty, day: day) } + return Self.bouquet(for: vendor, mainProvider: true, day: day, from: state) + .map { .content(mainPartyBouquet: $0, otherPartyBouquet: .empty, day: day) } .eraseToAnyPublisher() } } - static func bouquet(from state: State?, for provider: SRGProgramProvider, day otherDay: SRGDay) -> Bouquet { + static func bouquet(from state: State?, for mainProvider: Bool, day otherDay: SRGDay) -> Bouquet { guard let state else { return .empty } switch state { - case let .content(firstPartyBouquet: firstPartyBouquet, thirdPartyBouquet: thirdPartyBouquet, day: day): + case let .content(mainPartyBouquet: mainPartyBouquet, otherPartyBouquet: otherPartyBouquet, day: day): guard otherDay.compare(day) == .orderedSame else { - return provider == .thirdParty ? .loading(channels: thirdPartyBouquet.channels) : .loading(channels: firstPartyBouquet.channels) + return mainProvider ? .loading(channels: mainPartyBouquet.channels) : .loading(channels: otherPartyBouquet.channels) } - return provider == .thirdParty ? thirdPartyBouquet : firstPartyBouquet + return mainProvider ? mainPartyBouquet : otherPartyBouquet case .failed: return .empty } } - static func bouquet(for vendor: SRGVendor, provider: SRGProgramProvider, day: SRGDay, from state: State?) -> AnyPublisher { - let bouquet = bouquet(from: state, for: provider, day: day) - return SRGDataProvider.current!.tvPrograms(for: vendor, provider: provider, day: day, minimal: true) - .append(SRGDataProvider.current!.tvPrograms(for: vendor, provider: provider, day: day)) + static func bouquet(for vendor: SRGVendor, mainProvider: Bool, day: SRGDay, from state: State?) -> AnyPublisher { + let bouquet = bouquet(from: state, for: mainProvider, day: day) + return SRGDataProvider.current!.tvProgramsPublisher(day: day, mainProvider: mainProvider, minimal: true) + .append(SRGDataProvider.current!.tvProgramsPublisher(day: day, mainProvider: mainProvider)) .map { .content(programCompositions: $0) } .tryCatch { error in guard bouquet.hasPrograms else { throw error } diff --git a/Application/Sources/ProgramGuide/ProgramGuideGridViewController.swift b/Application/Sources/ProgramGuide/ProgramGuideGridViewController.swift index 206b9c1cc..0156f3f6c 100644 --- a/Application/Sources/ProgramGuide/ProgramGuideGridViewController.swift +++ b/Application/Sources/ProgramGuide/ProgramGuideGridViewController.swift @@ -38,7 +38,7 @@ final class ProgramGuideGridViewController: UIViewController { self.dailyModel = dailyModel } else { - self.dailyModel = ProgramGuideDailyViewModel(day: model.day, firstPartyChannels: model.firstPartyChannels, thirdPartyChannels: model.thirdPartyChannels) + self.dailyModel = ProgramGuideDailyViewModel(day: model.day, mainPartyChannels: model.mainPartyChannels, otherPartyChannels: model.otherPartyChannels) } super.init(nibName: nil, bundle: nil) } @@ -84,7 +84,7 @@ final class ProgramGuideGridViewController: UIViewController { cell.content = ItemCell(item: item) #if os(tvOS) if let program = item.program { - cell.accessibilityLabel = program.play_accessibilityLabel(with: item.section) + cell.accessibilityLabel = program.play_accessibilityLabel(with: item.section.wrappedValue) cell.accessibilityHint = PlaySRGAccessibilityLocalizedString("Opens details.", comment: "Program cell hint") } else { @@ -102,7 +102,7 @@ final class ProgramGuideGridViewController: UIViewController { guard let self else { return } let snapshot = dataSource.snapshot() let channel = snapshot.sectionIdentifiers[indexPath.section] - view.content = ChannelHeaderView(channel: channel) + view.content = ChannelHeaderView(channel: channel.wrappedValue) } dataSource.supplementaryViewProvider = { collectionView, _, indexPath in @@ -190,7 +190,7 @@ private extension ProgramGuideGridViewController { return ProgramGuideGridLayout.xOffset(centeringDate: model.date(for: time), in: collectionView, day: model.day) } - func yOffset(for channel: SRGChannel?) -> CGFloat? { + func yOffset(for channel: PlayChannel?) -> CGFloat? { guard let channel, let sectionIndex = dataSource.snapshot().sectionIdentifiers.firstIndex(of: channel) else { return nil } return ProgramGuideGridLayout.yOffset(forSectionIndex: sectionIndex, in: collectionView) } @@ -222,16 +222,16 @@ private extension ProgramGuideGridViewController { private extension ProgramGuideGridViewController { struct ScrollTarget { - let channel: SRGChannel? + let channel: PlayChannel? let time: TimeInterval? - init?(channel: SRGChannel?, time: TimeInterval?) { + init?(channel: PlayChannel?, time: TimeInterval?) { guard channel != nil || time != nil else { return nil } self.channel = channel self.time = time } - init(channel: SRGChannel) { + init(channel: PlayChannel) { self.channel = channel self.time = nil } @@ -283,7 +283,7 @@ extension ProgramGuideGridViewController: UICollectionViewDelegate { } #if os(tvOS) - navigateToProgram(program, in: channel) + navigateToProgram(program, in: channel.wrappedValue) #else AnalyticsClickEvent.tvGuideOpenInfoBox(program: program, programGuideLayout: .grid).send() // Deselection is managed here rather than in view appearance methods, as those are not called with the diff --git a/Application/Sources/ProgramGuide/ProgramGuideHeaderView.swift b/Application/Sources/ProgramGuide/ProgramGuideHeaderView.swift index 51bbdfb67..d1e9c38e5 100644 --- a/Application/Sources/ProgramGuide/ProgramGuideHeaderView.swift +++ b/Application/Sources/ProgramGuide/ProgramGuideHeaderView.swift @@ -168,15 +168,15 @@ struct ProgramGuideHeaderView: View { ScrollViewReader { proxy in HStack(spacing: 10) { if !model.channels.isEmpty { - ForEach(model.channels, id: \.uid) { channel in - ChannelButton(channel: channel) { + ForEach(model.channels, id: \.wrappedValue.uid) { channel in + ChannelButton(channel: channel.wrappedValue) { model.selectedChannel = channel } .environment(\.isSelected, channel == model.selectedChannel) } .onAppear { if let selectedChannel = model.selectedChannel { - proxy.scrollTo(selectedChannel.uid) + proxy.scrollTo(selectedChannel.wrappedValue.uid) } } } @@ -207,7 +207,7 @@ enum ProgramGuideHeaderViewSize { return (horizontalSizeClass == .compact) ? 216 : 136 } #else - return ApplicationConfiguration.shared.areTvThirdPartyChannelsAvailable ? 650 : 760 + return ApplicationConfiguration.shared.tvGuideOtherBouquets.isEmpty ? 760 : 650 #endif } } diff --git a/Application/Sources/ProgramGuide/ProgramGuideViewModel.swift b/Application/Sources/ProgramGuide/ProgramGuideViewModel.swift index 8849404bc..6334db295 100644 --- a/Application/Sources/ProgramGuide/ProgramGuideViewModel.swift +++ b/Application/Sources/ProgramGuide/ProgramGuideViewModel.swift @@ -29,25 +29,25 @@ final class ProgramGuideViewModel: ObservableObject { return date.timeIntervalSince(day.date) } - var channels: [SRGChannel] { + var channels: [PlayChannel] { return data.channels } - var firstPartyChannels: [SRGChannel] { - return data.firstPartyChannels + var mainPartyChannels: [PlayChannel] { + return data.mainPartyChannels } - var thirdPartyChannels: [SRGChannel] { - return data.thirdPartyChannels + var otherPartyChannels: [PlayChannel] { + return data.otherPartyChannels } - var selectedChannel: SRGChannel? { + var selectedChannel: PlayChannel? { get { return data.selectedChannel } set { if let newValue, channels.contains(newValue), newValue != data.selectedChannel { - data = Data(firstPartyChannels: firstPartyChannels, thirdPartyChannels: thirdPartyChannels, selectedChannel: newValue) + data = Data(mainPartyChannels: mainPartyChannels, otherPartyChannels: otherPartyChannels, selectedChannel: newValue) change = .channel(newValue) } } @@ -126,16 +126,16 @@ final class ProgramGuideViewModel: ObservableObject { extension ProgramGuideViewModel { struct Data { - let firstPartyChannels: [SRGChannel] - let thirdPartyChannels: [SRGChannel] - let selectedChannel: SRGChannel? + let mainPartyChannels: [PlayChannel] + let otherPartyChannels: [PlayChannel] + let selectedChannel: PlayChannel? static var empty: Self { - return Self(firstPartyChannels: [], thirdPartyChannels: [], selectedChannel: nil) + return Self(mainPartyChannels: [], otherPartyChannels: [], selectedChannel: nil) } - var channels: [SRGChannel] { - return firstPartyChannels + thirdPartyChannels + var channels: [PlayChannel] { + return mainPartyChannels + otherPartyChannels } } @@ -144,14 +144,14 @@ extension ProgramGuideViewModel { case day(SRGDay) case time(TimeInterval) case dayAndTime(day: SRGDay, time: TimeInterval) - case channel(SRGChannel) + case channel(PlayChannel) } } // MARK: Publishers private extension ProgramGuideViewModel { - static func matchingChannel(_ channel: SRGChannel?, in channels: [SRGChannel]) -> SRGChannel? { + static func matchingChannel(_ channel: PlayChannel?, in channels: [PlayChannel]) -> PlayChannel? { if let channel, channels.contains(channel) { return channel } @@ -162,8 +162,8 @@ private extension ProgramGuideViewModel { // TODO: Once an IL request is available to get the channel list without any day, use this request and // remove the day parameter. - static func channels(for vendor: SRGVendor, provider: SRGProgramProvider, day: SRGDay) -> AnyPublisher<[SRGChannel], Error> { - return SRGDataProvider.current!.tvPrograms(for: vendor, provider: provider, day: day, minimal: true) + static func channels(for vendor: SRGVendor, mainProvider: Bool, day: SRGDay) -> AnyPublisher<[PlayChannel], Error> { + return SRGDataProvider.current!.tvProgramsPublisher(day: day, mainProvider: mainProvider, minimal: true) .map { $0.map(\.channel) } .eraseToAnyPublisher() } @@ -172,18 +172,18 @@ private extension ProgramGuideViewModel { let applicationConfiguration = ApplicationConfiguration.shared let vendor = applicationConfiguration.vendor - if applicationConfiguration.areTvThirdPartyChannelsAvailable { + if !applicationConfiguration.tvGuideOtherBouquets.isEmpty { return Publishers.CombineLatest( - channels(for: vendor, provider: .SRG, day: day), - channels(for: vendor, provider: .thirdParty, day: day) + channels(for: vendor, mainProvider: true, day: day), + channels(for: vendor, mainProvider: false, day: day) ) - .map { Data(firstPartyChannels: $0, thirdPartyChannels: $1, selectedChannel: matchingChannel(data.selectedChannel, in: $0 + $1)) } + .map { Data(mainPartyChannels: $0, otherPartyChannels: $1, selectedChannel: matchingChannel(data.selectedChannel, in: $0 + $1)) } .replaceError(with: data) .eraseToAnyPublisher() } else { - return channels(for: vendor, provider: .SRG, day: day) - .map { Data(firstPartyChannels: $0, thirdPartyChannels: [], selectedChannel: matchingChannel(data.selectedChannel, in: $0)) } + return channels(for: vendor, mainProvider: true, day: day) + .map { Data(mainPartyChannels: $0, otherPartyChannels: [], selectedChannel: matchingChannel(data.selectedChannel, in: $0)) } .replaceError(with: data) .eraseToAnyPublisher() } diff --git a/Application/Sources/ProgramGuide/ProgramPreview.swift b/Application/Sources/ProgramGuide/ProgramPreview.swift index fb6ea49fa..fd43ba310 100644 --- a/Application/Sources/ProgramGuide/ProgramPreview.swift +++ b/Application/Sources/ProgramGuide/ProgramPreview.swift @@ -88,9 +88,9 @@ struct ProgramPreview: View { struct ProgramPreview_Previews: PreviewProvider { static var previews: some View { Group { - ProgramPreview(data: ProgramAndChannel(program: Mock.program(), channel: Mock.channel())) - ProgramPreview(data: ProgramAndChannel(program: Mock.program(.overflow), channel: Mock.channel())) - ProgramPreview(data: ProgramAndChannel(program: Mock.program(.fallbackImageUrl), channel: Mock.channel())) + ProgramPreview(data: ProgramAndChannel(program: Mock.program(), channel: Mock.playChannel())) + ProgramPreview(data: ProgramAndChannel(program: Mock.program(.overflow), channel: Mock.playChannel())) + ProgramPreview(data: ProgramAndChannel(program: Mock.program(.fallbackImageUrl), channel: Mock.playChannel())) ProgramPreview(data: nil) } .previewLayout(.fixed(width: 1920, height: 700)) diff --git a/Application/Sources/ProgramGuide/ProgramView.swift b/Application/Sources/ProgramGuide/ProgramView.swift index b6f69f7da..db3105def 100644 --- a/Application/Sources/ProgramGuide/ProgramView.swift +++ b/Application/Sources/ProgramGuide/ProgramView.swift @@ -14,11 +14,11 @@ struct ProgramView: View { @Binding var data: ProgramAndChannel @StateObject private var model = ProgramViewModel() - static func viewController(for program: SRGProgram, channel: SRGChannel) -> UIViewController { + static func viewController(for program: SRGProgram, channel: PlayChannel) -> UIViewController { return ProgramViewController(program: program, channel: channel) } - init(program: SRGProgram, channel: SRGChannel) { + init(program: SRGProgram, channel: PlayChannel) { _data = .constant(.init(program: program, channel: channel)) } @@ -285,7 +285,7 @@ struct ProgramView: View { // MARK: View controller private final class ProgramViewController: UIHostingController { - init(program: SRGProgram, channel: SRGChannel) { + init(program: SRGProgram, channel: PlayChannel) { super.init(rootView: ProgramView(program: program, channel: channel)) } @@ -313,9 +313,9 @@ struct ProgramView_Previews: PreviewProvider { static var previews: some View { Group { - ProgramView(program: Mock.program(), channel: Mock.channel()) - ProgramView(program: Mock.program(.overflow), channel: Mock.channel()) - ProgramView(program: Mock.program(.fallbackImageUrl), channel: Mock.channel()) + ProgramView(program: Mock.program(), channel: Mock.playChannel()) + ProgramView(program: Mock.program(.overflow), channel: Mock.playChannel()) + ProgramView(program: Mock.program(.fallbackImageUrl), channel: Mock.playChannel()) } .previewLayout(.fixed(width: size.width, height: size.height)) } diff --git a/Application/Sources/ProgramGuide/ProgramViewModel.swift b/Application/Sources/ProgramGuide/ProgramViewModel.swift index 5e7d98530..c86336ace 100644 --- a/Application/Sources/ProgramGuide/ProgramViewModel.swift +++ b/Application/Sources/ProgramGuide/ProgramViewModel.swift @@ -19,10 +19,10 @@ final class ProgramViewModel: ObservableObject { Self.mediaDataPublisher(for: data?.program) .receive(on: DispatchQueue.main) .assign(to: &$mediaData) - Self.livestreamMediaPublisher(for: data?.channel) + Self.livestreamMediaPublisher(for: data?.channel.wrappedValue) .receive(on: DispatchQueue.main) .assign(to: &$livestreamMedia) - eventEditViewDelegateObject.channel = data?.channel + eventEditViewDelegateObject.channel = data?.channel.wrappedValue } } @@ -52,7 +52,7 @@ final class ProgramViewModel: ObservableObject { } private var channel: SRGChannel? { - return data?.channel + return data?.channel.wrappedValue } private var isLive: Bool { @@ -191,14 +191,14 @@ final class ProgramViewModel: ObservableObject { return { [self] in if let data { if media.contentType == .livestream { - AnalyticsClickEvent.tvGuidePlayLivestream(program: data.program, channel: data.channel).send() + AnalyticsClickEvent.tvGuidePlayLivestream(program: data.program, channel: data.channel.wrappedValue).send() } else { - AnalyticsClickEvent.tvGuidePlayMedia(media: media, programIsLive: isLive, channel: data.channel).send() + AnalyticsClickEvent.tvGuidePlayMedia(media: media, programIsLive: isLive, channel: data.channel.wrappedValue).send() } } - tabBarController.play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) + tabBarController.dismissAndPresentMediaPlayer(with: media, position: nil) } } else { @@ -214,7 +214,7 @@ final class ProgramViewModel: ObservableObject { guard isLive, let media, media.blockingReason(at: Date()) == .none else { return nil } let data = self.data - let analyticsClickEvent = data != nil ? AnalyticsClickEvent.tvGuidePlayMedia(media: media, programIsLive: true, channel: data!.channel) : nil + let analyticsClickEvent = data != nil ? AnalyticsClickEvent.tvGuidePlayMedia(media: media, programIsLive: true, channel: data!.channel.wrappedValue) : nil return ButtonProperties( icon: .startOver, label: NSLocalizedString("Watch from start", comment: "Button to watch some program from the start"), @@ -227,12 +227,12 @@ final class ProgramViewModel: ObservableObject { alertController.addAction(UIAlertAction(title: NSLocalizedString("Resume", comment: "Alert choice to resume playback"), style: .default, handler: { _ in analyticsClickEvent?.send() - tabBarController.play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) + tabBarController.dismissAndPresentMediaPlayer(with: media, position: nil) })) alertController.addAction(UIAlertAction(title: NSLocalizedString("Watch from start", comment: "Alert choice to watch content from start"), style: .default, handler: { _ in analyticsClickEvent?.send() - tabBarController.play_presentMediaPlayer(with: media, position: .default, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) + tabBarController.dismissAndPresentMediaPlayer(with: media, position: .default) })) alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Title of a cancel button"), style: .cancel, handler: nil)) tabBarController.play_top.present(alertController, animated: true, completion: nil) @@ -240,7 +240,7 @@ final class ProgramViewModel: ObservableObject { else { analyticsClickEvent?.send() - tabBarController.play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) + tabBarController.dismissAndPresentMediaPlayer(with: media, position: nil) } } ) @@ -310,7 +310,7 @@ final class ProgramViewModel: ObservableObject { guard let self else { return } if granted { let event = EKEvent(eventStore: eventStore) - event.title = "\(program.title) - \(channel.title)" + event.title = "\(program.title) - \(channel.wrappedValue.title)" event.startDate = program.startDate event.endDate = program.endDate event.url = self.calendarUrl @@ -349,8 +349,8 @@ final class ProgramViewModel: ObservableObject { let window = UIApplication.shared.mainWindow else { return } - let showViewController = SectionViewController.showViewController(for: show) - tabBarController.pushViewController(showViewController, animated: false) + let pageViewController = PageViewController(id: .show(show)) + tabBarController.pushViewController(pageViewController, animated: false) window.play_dismissAllViewControllers(animated: true, completion: nil) } ) @@ -367,7 +367,7 @@ final class ProgramViewModel: ObservableObject { return ApplicationConfiguration.shared.sharingURL(for: show) } else { - return ApplicationConfiguration.shared.playURL + return ApplicationConfiguration.shared.playURL(for: self.channel?.vendor ?? ApplicationConfiguration.shared.vendor) } } @@ -489,6 +489,21 @@ extension ProgramViewModel { } } +fileprivate extension TabBarController { + func dismissAndPresentMediaPlayer(with media: SRGMedia, position: SRGPosition?) { + var presentMediaPlayer: Void { self.play_presentMediaPlayer(with: media, position: position, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) } + + if let presentedViewController = self.presentedViewController { + presentedViewController.dismiss(animated: true) { + presentMediaPlayer + } + } + else { + presentMediaPlayer + } + } +} + // MARK: UIKit delegate object private final class EventEditViewDelegateObject: NSObject, EKEventEditViewDelegate { diff --git a/Application/Sources/RadioChannels/RadioChannelsViewController.m b/Application/Sources/RadioChannels/RadioChannelsViewController.m index 9548d4466..628f9b98a 100755 --- a/Application/Sources/RadioChannels/RadioChannelsViewController.m +++ b/Application/Sources/RadioChannels/RadioChannelsViewController.m @@ -25,9 +25,9 @@ - (instancetype)initWithRadioChannels:(NSArray *)radioChannels NSMutableArray *viewControllers = [NSMutableArray array]; for (RadioChannel *radioChannel in radioChannels) { - UIViewController *viewController = [PageViewController audiosViewControllerForRadioChannel:radioChannel]; - viewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:radioChannel.name image:RadioChannelLogoImage(radioChannel) tag:0]; - [viewControllers addObject:viewController]; + PageViewController *pageViewController = [PageViewController audiosViewControllerForRadioChannel:radioChannel]; + pageViewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:radioChannel.name image:RadioChannelLogoImage(radioChannel) tag:0]; + [viewControllers addObject:pageViewController]; } NSUInteger initialPage = [radioChannels indexOfObject:ApplicationSettingLastOpenedRadioChannel()]; diff --git a/Application/Sources/Search/SearchViewController.swift b/Application/Sources/Search/SearchViewController.swift index 4e70e075a..69806573a 100644 --- a/Application/Sources/Search/SearchViewController.swift +++ b/Application/Sources/Search/SearchViewController.swift @@ -432,16 +432,16 @@ extension SearchViewController: UICollectionViewDelegate { play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil) case let .show(show): guard let navigationController else { return } - let showViewController = SectionViewController.showViewController(for: show) - navigationController.pushViewController(showViewController, animated: true) + let pageViewController = PageViewController(id: .show(show)) + navigationController.pushViewController(pageViewController, animated: true) SRGDataProvider.current!.increaseSearchResultsViewCount(for: show) .sink { _ in } receiveValue: { _ in } .store(in: &cancellables) case let .topic(topic): guard let navigationController else { return } - let topicViewController = PageViewController.topicViewController(for: topic) - navigationController.pushViewController(topicViewController, animated: true) + let pageViewController = PageViewController(id: .topic(topic)) + navigationController.pushViewController(pageViewController, animated: true) case .loading: break } diff --git a/Application/Sources/Settings/SettingsView.swift b/Application/Sources/Settings/SettingsView.swift index a894d7b90..a15160ebd 100644 --- a/Application/Sources/Settings/SettingsView.swift +++ b/Application/Sources/Settings/SettingsView.swift @@ -429,8 +429,21 @@ struct SettingsView: View { if let showSourceCode = model.showSourceCode { Button(NSLocalizedString("Source code", comment: "Label of the button to access the source code"), action: showSourceCode) } +#if NIGHTLY || BETA + if let switchVersion = model.switchVersion { + Button("\(NSLocalizedString("Switch version", comment: "Label of the button to open Apple TestFlight application and see other testable builds")) (TestFlight)", action: switchVersion) + } +#else if let becomeBetaTester = model.becomeBetaTester { - Button(NSLocalizedString("Become a beta tester", comment: "Label of the button to become beta tester"), action: becomeBetaTester) + let title = Bundle.main.play_isTestFlightDistribution ? + "\(NSLocalizedString("Switch version", comment: "Label of the button to open Apple TestFlight application and see other testable builds")) (TestFlight)" : + NSLocalizedString("Become a beta tester", comment: "Label of the button to become beta tester") + Button(title, action: becomeBetaTester) + } +#endif +#else + if let switchVersion = model.switchVersion { + Button("\(NSLocalizedString("Switch version", comment: "Label of the button to open Apple TestFlight application and see other testable builds")) (TestFlight)", action: switchVersion) } #endif VersionCell(model: model) @@ -802,9 +815,14 @@ struct SettingsView: View { var body: some View { PlaySection { + if let switchVersion = model.switchVersion { + Button("\(NSLocalizedString("Switch version", comment: "Label of the button to open Apple TestFlight application and see other testable builds")) (TestFlight)", action: switchVersion) + } InformationSection.VersionCell(model: model) } header: { Text(NSLocalizedString("Information", comment: "Information section header")) + } footer: { + Text(NSLocalizedString("This section is only available in nightly and beta versions, and won't appear in the production version.", comment: "Bottom additional information section footer")) } } } diff --git a/Application/Sources/Settings/SettingsViewModel.swift b/Application/Sources/Settings/SettingsViewModel.swift index 8c7986c28..ba01153e7 100644 --- a/Application/Sources/Settings/SettingsViewModel.swift +++ b/Application/Sources/Settings/SettingsViewModel.swift @@ -243,6 +243,30 @@ final class SettingsViewModel: ObservableObject { #endif } + var switchVersion: (() -> Void)? { + guard !Bundle.main.play_isAppStoreRelease else { return nil } + + guard let appStoreAppleId = Bundle.main.object(forInfoDictionaryKey: "AppStoreAppleId") as? String, !appStoreAppleId.isEmpty else { return nil } + + if let url = URL(string: "itms-beta://beta.itunes.apple.com/v1/app/\(appStoreAppleId)"), UIApplication.shared.canOpenURL(url) { + return { + UIApplication.shared.open(url) + } + } + else if let url = URL(string: "https://beta.itunes.apple.com/v1/app/\(appStoreAppleId)"), UIApplication.shared.canOpenURL(url) { +#if os(iOS) + return { + UIApplication.shared.open(url) + } +#else + return nil +#endif + } + else { + return nil + } + } + #if DEBUG || NIGHTLY || BETA func simulateMemoryWarning() { let selector = Selector("_p39e45r2f435o6r7837m12M34e5m6o67r8y8W9a9r66654n43i3n2g".unobfuscated()) diff --git a/Application/Sources/UI/Controllers/TabBarController.m b/Application/Sources/UI/Controllers/TabBarController.m index f1f02deff..31076d150 100755 --- a/Application/Sources/UI/Controllers/TabBarController.m +++ b/Application/Sources/UI/Controllers/TabBarController.m @@ -272,8 +272,8 @@ - (UIViewController *)videosTabViewController tag:TabBarItemIdentifierVideos]; videosTabBarItem.accessibilityIdentifier = [AccessibilityIdentifierObjC identifier:AccessibilityIdentifierVideosTabBarItem].value; - UIViewController *videosViewController = [PageViewController videosViewController]; - NavigationController *videosNavigationController = [[NavigationController alloc] initWithRootViewController:videosViewController]; + PageViewController *pageViewController = [PageViewController videosViewController]; + NavigationController *videosNavigationController = [[NavigationController alloc] initWithRootViewController:pageViewController]; videosNavigationController.tabBarItem = videosTabBarItem; return videosNavigationController; } @@ -300,8 +300,8 @@ - (UIViewController *)audiosTabViewController } else if (radioChannels.count == 1) { RadioChannel *radioChannel = radioChannels.firstObject; - UIViewController *audiosViewController = [PageViewController audiosViewControllerForRadioChannel:radioChannel]; - NavigationController *audiosNavigationController = [[NavigationController alloc] initWithRootViewController:audiosViewController]; + PageViewController *pageViewController = [PageViewController audiosViewControllerForRadioChannel:radioChannel]; + NavigationController *audiosNavigationController = [[NavigationController alloc] initWithRootViewController:pageViewController]; audiosNavigationController.tabBarItem = [self audiosTabBarItem]; [audiosNavigationController updateWithRadioChannel:radioChannel animated:NO]; return audiosNavigationController; @@ -322,8 +322,8 @@ - (UIViewController *)livestreamsTabViewController tag:TabBarItemIdentifierLivestreams]; liveTabBarItem.accessibilityIdentifier = [AccessibilityIdentifierObjC identifier:AccessibilityIdentifierLivestreamsTabBarItem].value; - UIViewController *liveViewController = [PageViewController liveViewController]; - NavigationController *liveNavigationController = [[NavigationController alloc] initWithRootViewController:liveViewController]; + PageViewController *pageViewController = [PageViewController liveViewController]; + NavigationController *liveNavigationController = [[NavigationController alloc] initWithRootViewController:pageViewController]; liveNavigationController.tabBarItem = liveTabBarItem; return liveNavigationController; } diff --git a/Application/Sources/UI/Helpers/ContextMenu.swift b/Application/Sources/UI/Helpers/ContextMenu.swift index 9d452249a..aae9b22f0 100644 --- a/Application/Sources/UI/Helpers/ContextMenu.swift +++ b/Application/Sources/UI/Helpers/ContextMenu.swift @@ -217,14 +217,14 @@ extension ContextMenu { guard !ApplicationConfiguration.shared.areShowsUnavailable, let show = media.show, let navigationController = viewController.navigationController else { return nil } - if let sectionViewController = viewController as? SectionViewController, - let displayedShow = sectionViewController.model.configuration.properties.displayedShow { + if let pageViewController = viewController as? PageViewController, + let displayedShow = pageViewController.id.displayedShow { guard !show.isEqual(displayedShow) else { return nil } } return UIAction(title: NSLocalizedString("More episodes", comment: "Context menu action to open more episodes associated with a media"), image: UIImage(resource: .episodes)) { _ in - let showViewController = SectionViewController.showViewController(for: show) - navigationController.pushViewController(showViewController, animated: true) + let pageViewController = PageViewController(id: .show(show)) + navigationController.pushViewController(pageViewController, animated: true) } } } @@ -234,7 +234,7 @@ extension ContextMenu { extension ContextMenu { static func configuration(for show: SRGShow, identifier: NSCopying?, in viewController: UIViewController) -> UIContextMenuConfiguration? { return UIContextMenuConfiguration(identifier: identifier) { - return SectionViewController.showViewController(for: show) + return PageViewController(id: .show(show)) } actionProvider: { _ in return menu(for: show, in: viewController) } diff --git a/Makefile b/Makefile index 712e65e8f..c18c9c753 100755 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ #!/usr/bin/xcrun make -f CONFIGURATION_REPOSITORY_URL=https://github.com/SRGSSR/playsrg-apple-configuration.git -CONFIGURATION_COMMIT_SHA1=cf878c955afb0fd424cce6678bc7e5c3f7a817a1 +CONFIGURATION_COMMIT_SHA1=ca02311d62e5896b485d02e4c1ee8fb8392a40a6 CONFIGURATION_FOLDER=Configuration .PHONY: all @@ -41,6 +41,12 @@ fix-quality: @Scripts/fix-quality.sh @echo "... done.\n" +.PHONY: pull-translations +pull-translations: + @echo "Pulling translations..." + @Scripts/pullCrowdin.sh + @echo "... done.\n" + .PHONY: git-hook-install git-hook-install: @echo "Installing git hooks..." @@ -72,6 +78,8 @@ help: @echo " check-quality Run quality checks" @echo " fix-quality Fix quality automatically (if possible)" @echo "" + @echo " pull-translations Pull new translations from Crowdin" + @echo "" @echo " git-hook-install Use hooks located in ./hooks" @echo " git-hook-uninstall Use default hooks located in .git/hooks" @echo "" diff --git a/PlaySRG.xcodeproj/project.pbxproj b/PlaySRG.xcodeproj/project.pbxproj index 680f30574..733e008ce 100644 --- a/PlaySRG.xcodeproj/project.pbxproj +++ b/PlaySRG.xcodeproj/project.pbxproj @@ -19259,7 +19259,7 @@ repositoryURL = "https://github.com/SRGSSR/srganalytics-apple.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 9.0.1; + minimumVersion = 9.0.2; }; }; 6F3AED322614C5B6007D591F /* XCRemoteSwiftPackageReference "srgappearance-apple" */ = { @@ -19307,7 +19307,7 @@ repositoryURL = "https://github.com/SRGSSR/srgdataprovider-apple.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 18.0.0; + minimumVersion = 18.1.0; }; }; 6F7269A72836CFE90072BA0B /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { diff --git a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved index 08c13b085..404d9e673 100644 --- a/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PlaySRG.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -275,8 +275,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SRGSSR/srganalytics-apple.git", "state" : { - "revision" : "5fbe14159af7f61c86b8e02470cea97a133040f4", - "version" : "9.0.1" + "revision" : "45343b66e5bc2626197d314dfff88d50f9a96388", + "version" : "9.0.2" } }, { @@ -302,8 +302,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SRGSSR/srgdataprovider-apple.git", "state" : { - "revision" : "93a46a26d9ea068e3d812755e5a44c7402b2ad04", - "version" : "18.0.0" + "revision" : "a7380a80cab17b9c3f84e1b32d6c403b0bedc2be", + "version" : "18.1.0" } }, { diff --git a/Podfile.lock b/Podfile.lock index 94515bf8b..1105bcb6b 100755 --- a/Podfile.lock +++ b/Podfile.lock @@ -64,4 +64,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a1e77ad2481071cb2575ad02ea8084be964a3861 -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.3 diff --git a/Scripts/pullCrowdin.sh b/Scripts/pullCrowdin.sh new file mode 100755 index 000000000..c14589569 --- /dev/null +++ b/Scripts/pullCrowdin.sh @@ -0,0 +1,188 @@ +#! /bin/sh + +if ! command -v crowdin > /dev/null; then + echo "crowdin CLI is not available. Please install it. https://crowdin.github.io/crowdin-cli/installation" + exit 0 +fi + +# Get the directory of the script +script_dir="$(dirname "$(readlink -f "$0")")" + +# Load the repository .env file +ENV_FILE="$script_dir/../.env" +if [ -f "$ENV_FILE" ]; then +# shellcheck source=/dev/null + . "$ENV_FILE" +fi + +if [ -z "$CROWDIN_API_TOKEN" ]; then + echo "CROWDIN_API_TOKEN environment variable is not set. Skipping Crowdin pulling." + exit 0 +fi + +rm -rf /tmp/playsrg-crowdin +mkdir /tmp/playsrg-crowdin + +# Use the repository configuration file +CROWDIN_CONFIG_FILE="$script_dir/../crowdin.yml" + +# crowdin CLI needs sources in the current directory to get translations. +echo "Downloading sources from Crowdin..." +crowdin pull sources -c "$CROWDIN_CONFIG_FILE" --token "$CROWDIN_API_TOKEN" --no-progress + +# crowdin CLI builds ZIP archive with the latest translations automatically. +echo "Downloading the latest translations..." +crowdin pull -c "$CROWDIN_CONFIG_FILE" --token "$CROWDIN_API_TOKEN" --no-progress + +for i in "$@" +do + if [ "$i" = "--skip-copies" ]; then + exit 0 + fi +done + +if [ -z "$CROWDIN_PLAY_PATH" ]; then + CROWDIN_PLAY_PATH="." + echo "Use default CROWDIN_PLAY_PATH variable: \".\"" +fi + +if [ -z "$CROWDIN_MEDIA_PLAYER_PATH" ]; then + CROWDIN_MEDIA_PLAYER_PATH="../srgmediaplayer-apple" + echo "Use default CROWDIN_MEDIA_PLAYER_PATH variable: \"$CROWDIN_MEDIA_PLAYER_PATH\"" +fi + +if [ -z "$CROWDIN_NETWORK_PATH" ]; then + CROWDIN_NETWORK_PATH="../srgnetwork-apple" + echo "Use default CROWDIN_NETWORK_PATH variable: \"$CROWDIN_NETWORK_PATH\"" +fi + +if [ -z "$CROWDIN_IDENTITY_PATH" ]; then + CROWDIN_IDENTITY_PATH="../srgidentity-apple" + echo "Use default CROWDIN_IDENTITY_PATH variable: \"$CROWDIN_IDENTITY_PATH\"" +fi + +if [ -z "$CROWDIN_CONTENT_PROTECTION_PATH" ]; then + CROWDIN_CONTENT_PROTECTION_PATH="../srgcontentprotection-apple" + echo "Use default CROWDIN_CONTENT_PROTECTION_PATH variable: \"$CROWDIN_CONTENT_PROTECTION_PATH\"" +fi + +if [ -z "$CROWDIN_DATA_PROVIDER_PATH" ]; then + CROWDIN_DATA_PROVIDER_PATH="../srgdataprovider-apple" + echo "Use default CROWDIN_DATA_PROVIDER_PATH variable: \"$CROWDIN_DATA_PROVIDER_PATH\"" +fi + +if [ -z "$CROWDIN_LETTERBOX_PATH" ]; then + CROWDIN_LETTERBOX_PATH="../srgletterbox-apple" + echo "Use default CROWDIN_LETTERBOX_PATH variable: \"$CROWDIN_LETTERBOX_PATH\"" +fi + +if [ -z "$CROWDIN_USER_DATA_PATH" ]; then + CROWDIN_USER_DATA_PATH="../srguserdata-apple" + echo "Use default CROWDIN_USER_DATA_PATH variable: \"$CROWDIN_USER_DATA_PATH\"" +fi + +# Play applications +echo "Update Play SRG translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/Play App/Localizable.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SRF/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/Play App/Localizable.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTS/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/Play App/Localizable.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RSI/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/Play App/Localizable.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTR/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/Play App/Localizable.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SWI/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/Play App/Accessibility.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SRF/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/Play App/Accessibility.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTS/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/Play App/Accessibility.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RSI/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/Play App/Accessibility.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTR/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/Play App/Accessibility.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SWI/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/Play App/Onboarding.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SRF/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/Play App/Onboarding.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTS/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/Play App/Onboarding.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RSI/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/Play App/Onboarding.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTR/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/Play App/Onboarding.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SWI/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/Play App/InfoPlist.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SRF/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/Play App/InfoPlist.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTS/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/Play App/InfoPlist.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RSI/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/Play App/InfoPlist.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play RTR/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/Play App/InfoPlist.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Apps/Play SWI/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/Play App/Settings.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Settings.bundle/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/Play App/Settings.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Settings.bundle/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/Play App/Settings.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Settings.bundle/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/Play App/Settings.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Settings.bundle/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/Play App/Settings.strings" "$CROWDIN_PLAY_PATH/Application/Resources/Settings.bundle/en.lproj/" + +# SRG Media Player library +Echo "Update SRG Media Player translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGMediaPlayer Library/Localizable.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGMediaPlayer Library/Localizable.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGMediaPlayer Library/Localizable.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGMediaPlayer Library/Localizable.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGMediaPlayer Library/Localizable.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGMediaPlayer Library/Accessibility.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGMediaPlayer Library/Accessibility.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGMediaPlayer Library/Accessibility.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGMediaPlayer Library/Accessibility.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGMediaPlayer Library/Accessibility.strings" "$CROWDIN_MEDIA_PLAYER_PATH/Sources/SRGMediaPlayer/Resources/en.lproj/" + +# SRG Network library +echo "Update SRG Network translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGNetwork Library/Localizable.strings" "$CROWDIN_NETWORK_PATH/Sources/SRGNetwork/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGNetwork Library/Localizable.strings" "$CROWDIN_NETWORK_PATH/Sources/SRGNetwork/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGNetwork Library/Localizable.strings" "$CROWDIN_NETWORK_PATH/Sources/SRGNetwork/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGNetwork Library/Localizable.strings" "$CROWDIN_NETWORK_PATH/Sources/SRGNetwork/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGNetwork Library/Localizable.strings" "$CROWDIN_NETWORK_PATH/Sources/SRGNetwork/Resources/en.lproj/" + +# SRG Identity library +echo "Update SRG Identity translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGIdentity Library/Localizable.strings" "$CROWDIN_IDENTITY_PATH/Sources/SRGIdentity/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGIdentity Library/Localizable.strings" "$CROWDIN_IDENTITY_PATH/Sources/SRGIdentity/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGIdentity Library/Localizable.strings" "$CROWDIN_IDENTITY_PATH/Sources/SRGIdentity/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGIdentity Library/Localizable.strings" "$CROWDIN_IDENTITY_PATH/Sources/SRGIdentity/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGIdentity Library/Localizable.strings" "$CROWDIN_IDENTITY_PATH/Sources/SRGIdentity/Resources/en.lproj/" + +# SRG Content Protection library +echo "Update SRG Content Protection translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGContentProtection Library/Localizable.strings" "$CROWDIN_CONTENT_PROTECTION_PATH/Sources/SRGContentProtection/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGContentProtection Library/Localizable.strings" "$CROWDIN_CONTENT_PROTECTION_PATH/Sources/SRGContentProtection/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGContentProtection Library/Localizable.strings" "$CROWDIN_CONTENT_PROTECTION_PATH/Sources/SRGContentProtection/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGContentProtection Library/Localizable.strings" "$CROWDIN_CONTENT_PROTECTION_PATH/Sources/SRGContentProtection/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGContentProtection Library/Localizable.strings" "$CROWDIN_CONTENT_PROTECTION_PATH/Sources/SRGContentProtection/Resources/en.lproj/" + +# SRG Data Provider library +echo "Update SRG Data Provider translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGDataprovider Library/Localizable.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGDataprovider Library/Localizable.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGDataprovider Library/Localizable.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGDataprovider Library/Localizable.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGDataprovider Library/Localizable.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGDataprovider Library/Accessibility.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGDataprovider Library/Accessibility.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGDataprovider Library/Accessibility.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGDataprovider Library/Accessibility.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGDataprovider Library/Accessibility.strings" "$CROWDIN_DATA_PROVIDER_PATH/Sources/SRGDataProviderModel/Resources/en.lproj/" + +# SRG Letterbox library +Echo "Update SRG Letterbox translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGLetterbox Library/Localizable.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGLetterbox Library/Localizable.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGLetterbox Library/Localizable.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGLetterbox Library/Localizable.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGLetterbox Library/Localizable.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/en.lproj/" + +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGLetterbox Library/Accessibility.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGLetterbox Library/Accessibility.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGLetterbox Library/Accessibility.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGLetterbox Library/Accessibility.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGLetterbox Library/Accessibility.strings" "$CROWDIN_LETTERBOX_PATH/Sources/SRGLetterbox/Resources/en.lproj/" + +# SRG User Data library +echo "Update SRG User Data translations files." +cp -f "/tmp/playsrg-crowdin/de-CH/Apple/SRGUserData Library/Localizable.strings" "$CROWDIN_USER_DATA_PATH/Sources/SRGUserData/Resources/de.lproj/" +cp -f "/tmp/playsrg-crowdin/fr-CH/Apple/SRGUserData Library/Localizable.strings" "$CROWDIN_USER_DATA_PATH/Sources/SRGUserData/Resources/fr.lproj/" +cp -f "/tmp/playsrg-crowdin/it-CH/Apple/SRGUserData Library/Localizable.strings" "$CROWDIN_USER_DATA_PATH/Sources/SRGUserData/Resources/it.lproj/" +cp -f "/tmp/playsrg-crowdin/rm-CH/Apple/SRGUserData Library/Localizable.strings" "$CROWDIN_USER_DATA_PATH/Sources/SRGUserData/Resources/rm.lproj/" +cp -f "/tmp/playsrg-crowdin/en/Apple/SRGUserData Library/Localizable.strings" "$CROWDIN_USER_DATA_PATH/Sources/SRGUserData/Resources/en.lproj/" diff --git a/TV Application/Sources/SceneDelegate.swift b/TV Application/Sources/SceneDelegate.swift index 74344e085..af14b6b7e 100644 --- a/TV Application/Sources/SceneDelegate.swift +++ b/TV Application/Sources/SceneDelegate.swift @@ -54,27 +54,27 @@ final class SceneDelegate: UIResponder { private static func applicationRootViewController() -> UIViewController { var viewControllers = [UIViewController]() - let videosViewController = PageViewController(id: .video) - videosViewController.tabBarItem = UITabBarItem(title: NSLocalizedString("Home", comment: "Home tab title"), image: nil, tag: 0) - videosViewController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifier.videosTabBarItem.value - viewControllers.append(videosViewController) + let pageViewController = PageViewController(id: .video) + pageViewController.tabBarItem = UITabBarItem(title: NSLocalizedString("Home", comment: "Home tab title"), image: nil, tag: 0) + pageViewController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifier.videosTabBarItem.value + viewControllers.append(pageViewController) let configuration = ApplicationConfiguration.shared #if DEBUG if let firstChannel = configuration.radioHomepageChannels.first { - let audiosViewController = PageViewController(id: .audio(channel: firstChannel)) - audiosViewController.tabBarItem = UITabBarItem(title: NSLocalizedString("Audios", comment: "Audios tab title"), image: nil, tag: 1) - audiosViewController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifier.audiosTabBarItem.value - viewControllers.append(audiosViewController) + let pageViewController = PageViewController(id: .audio(channel: firstChannel)) + pageViewController.tabBarItem = UITabBarItem(title: NSLocalizedString("Audios", comment: "Audios tab title"), image: nil, tag: 1) + pageViewController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifier.audiosTabBarItem.value + viewControllers.append(pageViewController) } #endif if !configuration.liveHomeSections.isEmpty { - let liveViewController = PageViewController(id: .live) - liveViewController.tabBarItem = UITabBarItem(title: NSLocalizedString("Livestreams", comment: "Livestreams tab title"), image: nil, tag: 2) - liveViewController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifier.livestreamsTabBarItem.value - viewControllers.append(liveViewController) + let pageViewController = PageViewController(id: .live) + pageViewController.tabBarItem = UITabBarItem(title: NSLocalizedString("Livestreams", comment: "Livestreams tab title"), image: nil, tag: 2) + pageViewController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifier.livestreamsTabBarItem.value + viewControllers.append(pageViewController) } if !configuration.isTvGuideUnavailable { diff --git a/TV Application/TV-Application-Info.plist b/TV Application/TV-Application-Info.plist index b94365d52..01d6786d4 100644 --- a/TV Application/TV-Application-Info.plist +++ b/TV Application/TV-Application-Info.plist @@ -53,8 +53,41 @@ + LSApplicationQueriesSchemes + + srgtraffic + srf + srfsport + srfplayer + srfmeteo + srfradio3 + srfvirus + srfjass + rtsinfo + rtssport + playrts + couleur3 + dtqc3 + rtsradio + rsinews + rsisport + rtskids + playrsi + rsich + rsiibazaar + rsizerovero + rsipeo + playrtr + ch.swissinfo.news + playswi + tvsvizzera + itms-beta + itms-appss + AppCenterSecret $(CONFIG__APPCENTER_SECRET) + AppStoreAppleId + $(CONFIG__APPSTORE_APPLE_ID) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/WhatsNew-iOS-beta.json b/WhatsNew-iOS-beta.json index 82bb3b4b1..f10cfb0ec 100755 --- a/WhatsNew-iOS-beta.json +++ b/WhatsNew-iOS-beta.json @@ -211,5 +211,10 @@ "3.7.9-432": "- Maintenance build.\n- Analytic events use a new server.", "3.7.9-433": "- Allow playing video and audio when displaying user consent banners.\n- Analytic events have language property.", "3.7.9-434": "- Increase player skip gesture area (double taps).", - "3.7.9-435": "- Patch analytics migration." + "3.7.9-435": "- Patch analytics migration.", + "3.7.10-436": "- Discover programs from all SRG SSR channels in the TV Guide.\n- Always display subscribe buttons in show page and profile page.\n- Update RTS Info channel logo. [RTS]", + "3.7.10-437": "Branch beta\n- TV show pages are now editorialized.\n- Display page status (empty, network error, …) in show pages.", + "3.7.10-438": "Beta from a branch\n\n- TV show pages are now editorialized.\n- Fix iOS 17 layout on iPad.", + "3.8.0-439": "- Discover programs from all SRG channels in the TV Guide. [RSI, RTR, RTS, SRF]\n- Video show pages can be editorialized, like series or documentaries pages. [RSI, RTR, RTS, SRF]", + "3.8.0-440": "- Shorter wording to \"add to favorites\" buttons. [RSI, SRF]\n- Switch version button for all iOS and tvOS TestFlight buids (private and public)." } \ No newline at end of file diff --git a/WhatsNew-tvOS-beta.json b/WhatsNew-tvOS-beta.json index 91f05763c..942278ce3 100755 --- a/WhatsNew-tvOS-beta.json +++ b/WhatsNew-tvOS-beta.json @@ -78,5 +78,9 @@ "1.7.7-432": "- Maintenance build.\n- Analytic events use a new server.", "1.7.7-433": "- Allow playing video and audio when displaying user consent banners.\n- Analytic events have language property.", "1.7.7-434": "- Better image resolution on program guide.", - "1.7.7-435": "- Patch analytics migration." + "1.7.7-435": "- Patch analytics migration.", + "1.7.8-436": "- Discover programs from all SRG SSR channels in the TV Guide.\n- Update RTS Info channel logo. [RTS]", + "1.7.8-437": "Branch beta\n- TV show pages are now editorialized.\n- Display page status (empty, network error, …) in show pages.", + "1.8.0-439": "- Discover programs from all SRG channels in the TV Guide. [RSI, RTR, RTS, SRF]\n- Video show pages can be editorialized, like series or documentaries pages. [RSI, RTR, RTS, SRF]", + "1.8.0-440": "- Shorter wording to \"add to favorites\" buttons. [RSI, SRF]\n- Switch version button for all iOS and tvOS TestFlight buids (private and public)." } \ No newline at end of file diff --git a/Xcode/Shared/Common.xcconfig b/Xcode/Shared/Common.xcconfig index c3250f60a..37cb2e468 100755 --- a/Xcode/Shared/Common.xcconfig +++ b/Xcode/Shared/Common.xcconfig @@ -2,7 +2,7 @@ PRODUCT_BUNDLE_IDENTIFIER = $(BU__BUNDLE_IDENTIFIER_PREFIX)$(BU__BUNDLE_IDENTIFI PRODUCT_NAME = $(BU__PRODUCT_NAME)$(TARGET__PRODUCT_NAME_SUFFIX) // Version information -CURRENT_PROJECT_VERSION = 435 +CURRENT_PROJECT_VERSION = 440 GCC_PREPROCESSOR_DEFINITIONS[config=Beta] = BETA=1 GCC_PREPROCESSOR_DEFINITIONS[config=Beta_AppCenter] = BETA=1 APPCENTER=1 diff --git a/Xcode/Shared/Targets/iOS/Common.xcconfig b/Xcode/Shared/Targets/iOS/Common.xcconfig index fbb7d22d5..25bb61c9e 100755 --- a/Xcode/Shared/Targets/iOS/Common.xcconfig +++ b/Xcode/Shared/Targets/iOS/Common.xcconfig @@ -1,7 +1,7 @@ #include "Xcode/Shared/Common.xcconfig" // Version information -MARKETING_VERSION = 3.7.9 +MARKETING_VERSION = 3.8.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY=1,2 diff --git a/Xcode/Shared/Targets/tvOS/Common.xcconfig b/Xcode/Shared/Targets/tvOS/Common.xcconfig index e4d1ef16a..bb0359066 100755 --- a/Xcode/Shared/Targets/tvOS/Common.xcconfig +++ b/Xcode/Shared/Targets/tvOS/Common.xcconfig @@ -1,7 +1,7 @@ #include "Xcode/Shared/Common.xcconfig" // Version information -MARKETING_VERSION = 1.7.7 +MARKETING_VERSION = 1.8.0 SDKROOT = appletvos TARGETED_DEVICE_FAMILY=3 diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..c16aacb25 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,47 @@ +# +# Crowdin configuration (PlaySRG project) +# +"project_id": "126201" +# "api_token": "" # use --token option instead so that it is work on all computers. +"base_path": "/tmp/playsrg-crowdin" +"base_url": "https://api.crowdin.com" + +"preserve_hierarchy": true +files: [ + { + "source": "/Apple/Play App/*.strings", + "translation": "/%locale%/Apple/Play App/%original_file_name%" + }, + { + "source": "/Apple/Play App/*.csv", + "translation": "/%locale%/Apple/Play App/%original_file_name%" + }, + { + "source": "/Apple/SRGContentProtection Library/*.strings", + "translation": "/%locale%/Apple/SRGContentProtection Library/%original_file_name%" + }, + { + "source": "/Apple/SRGDataprovider Library/*.strings", + "translation": "/%locale%/Apple/SRGDataprovider Library/%original_file_name%" + }, + { + "source": "/Apple/SRGIdentity Library/*.strings", + "translation": "/%locale%/Apple/SRGIdentity Library/%original_file_name%" + }, + { + "source": "/Apple/SRGLetterbox Library/*.strings", + "translation": "/%locale%/Apple/SRGLetterbox Library/%original_file_name%" + }, + { + "source": "/Apple/SRGMediaPlayer Library/*.strings", + "translation": "/%locale%/Apple/SRGMediaPlayer Library/%original_file_name%" + }, + { + "source": "/Apple/SRGNetwork Library/*.strings", + "translation": "/%locale%/Apple/SRGNetwork Library/%original_file_name%" + }, + { + "source": "/Apple/SRGUserData Library/*.strings", + "translation": "/%locale%/Apple/SRGUserData Library/%original_file_name%" + } +] diff --git a/docs/README.md b/docs/README.md index c3fbbb157..ccb1cb996 100755 --- a/docs/README.md +++ b/docs/README.md @@ -89,6 +89,18 @@ The project can be built without private settings but some features might not be Simply open the project with Xcode and wait until all dependencies have been retrieved. Then build and run the project. +## Translations + +Pushing new translations needs to upload source files on [crowdin.com](https://crowdin.com/project/play-srg/sources/files). + +Pulling new translations: + +``` +make pull-translations +``` + +The script needs [Crowdin CLI](https://crowdin.github.io/crowdin-cli/). + ## Releasing binaries The proprietary project uses [fastlane](https://fastlane.tools/) to [release binaries](RELEASE_CHECKLIST.md), either for internal purposes or for the AppStore. diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 451146328..540d856ab 100755 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -4,7 +4,7 @@ |:--:|:--:|:--:|:--:|:--:|:--:| | Edit SPM / Podfile dependencies to point at tagged versions |||||| | Verify that Package.resolved / Podfile.lock only contain tagged versions |||||| -| Update application translations (with pullCrowdin .sh) |||||| +| Update application translations (with make pull-translations) |||||| | Perform global diff with last release |||||| | Submit what's new for translation |||||| | Start git-flow release branch for new version |||||| @@ -30,6 +30,7 @@ | Finish git-flow release, tags, Bump patch / build version numbers and push (with fastlane\*) |||||| | Close milestone and issues on github |||||| | Create github release |||||| +| Add release date on Jira release |||||| | Update status page on Confluence (Release date, old versions section) |||||| ### \*Fastlane on PlayCity CI: diff --git a/docs/REMOTE_CONFIGURATION.md b/docs/REMOTE_CONFIGURATION.md index 3d5acdffd..7a21e4f2e 100755 --- a/docs/REMOTE_CONFIGURATION.md +++ b/docs/REMOTE_CONFIGURATION.md @@ -30,7 +30,7 @@ If a remote configuration is found to be invalid (usually a mandatory parameter * `identityWebsiteURL` (optional, string): The URL of the identity web portal. * `userDataServiceURL` (optional, string): The URL of the service with which user data can be synchronized (history, preferences, playlists). * `middlewareURL` (mandatory, string): The URL of the Play application middleware. -* `playURL` (mandatory, string): The base URL of the Play web portal, used when building sharing URLs. +* `playURLs` (mandatory, JSON): A JSON dictionary describing all base URLs of the Play web portals, used when building sharing URLs. Key (string) is the identifier of the business unit, value (string) is the base URL of the Play web portal. The `businessUnit` property value MUST be in the keys. * `playServiceURL` (mandatory, string): The base URL of the Play web service. * `sourceCodeURL` (optional, string); The URL where the application source code can be found. * `termsAndConditionsURL` (optional, string): The URL of the terms and conditions page. @@ -74,6 +74,7 @@ The radio channel JSON dictionaries have one more key: ## Shows +* `predefinedShowPagePreferred` (optional, boolean): Set to `true` iff show pages need to be displayed with the predefined layout (ie: only one predefined section with available episodes). If omitted, `false`. * `showLeadPreferred` (optional, boolean): Set to `true` iff show pages and show elements should display lead instead of description. If omitted, `false`. ## Audio homepage @@ -133,6 +134,15 @@ Feeds * `continuousPlaybackForegroundTransitionDuration` (optional, number): Duration in seconds for continuous playback when the application runs in foreground and the player view is not displayed. If empty, continuous playback is disabled; if equal to 0, upcoming media playback starts immediately. * `continuousPlaybackBackgroundTransitionDuration` (optional, number): Duration in seconds for continuous playback when the application runs in background. If empty, continuous playback is disabled; if equal to 0, upcoming media playback starts immediately. +## TV guide + +* `tvGuideUnavailable` (optional, boolean): If set to `true`, TV guide access is removed and replaced with the legacy _by date_ access. +* `tvGuideOtherBouquets` (optional, string, multiple): TV guide other bouquets to display below the main vendor bouquet. Available values: + * `thirdparty`: Third party bouquet delivered for the vendor. + * `rsi`: RSI vendor bouquet. + * `rts`: RT vendor bouquet. + * `srf`: SR vendor bouquet. + ## Other functionalities * `audioDescriptionAvailabilityHidden` (optional, boolean): Set to `true` to hide audio description availability setting. @@ -144,5 +154,3 @@ Feeds * `showsUnavailable` (optional, boolean): If set to `true`, all features related to shows are removed. * `subtitleAvailabilityHidden` (optional, boolean): Set to `true` to hide the subtitle availability setting. * `discoverySubtitleOptionLanguage` (optional, string): Set system subtitle language to this value once and at the beginning to help user discover that content is subtitled in that language. -* `tvGuideUnavailable` (optional, boolean): If set to `true`, TV guide access is removed and replaced with the legacy _by date_ access. -* `tvThirdPartyChannelsAvailable` (optional, boolean): if set to `true`, third-party TV channel content is available in the TV guide. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 937c39086..e29433995 100755 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -76,7 +76,7 @@ platform :ios do appcenter_lane( appname: appcenter_ios_nightly_appcenter_names[index], destinations: ENV.fetch('PLAY_NIGHTLY_APPCENTER_DESTINATIONS', nil), - notes: nightly_changelog(platform, service) + notes: nightly_changelog(platform, service, branch_name) ) clean_build_artifacts @@ -130,7 +130,7 @@ platform :ios do appcenter_lane( appname: appcenter_ios_beta_appcenter_names[index], destinations: ENV.fetch('PLAY_BETA_APPCENTER_DESTINATIONS', nil), - notes: what_s_new_for_beta(platform, nil), + notes: what_s_new_for_beta(platform, tag_version), notify_testers: true ) @@ -293,6 +293,9 @@ platform :ios do app = spaceship_bu_appstore_build_app(business_unit) next unless app + UI.message '-----' + UI.command_output "Switch to Play #{business_unit}" + UI.message '-----' ['iOS', 'tvOS'].map do |platform| live_version = spaceship_app_live_version(platform, app) @@ -314,6 +317,55 @@ platform :ios do UI.message '-----' end + desc 'Get AppStore TestFlight App status for iOS and tvOS, lastest version' + lane :appStoreTestFlightAppStatus do + UI.message '-----' + business_units.map do |business_unit| + spaceship_login_with_api_key(business_unit) + app = spaceship_bu_appstore_build_app(business_unit) + next unless app + + UI.message '-----' + UI.command_output "Switch to Play #{business_unit}" + + group_builds = spaceship_app_beta_group_builds(app) + + UI.message '-----' + ['iOS', 'tvOS'].map do |platform| + UI.message "Builds for Play #{business_unit} #{platform}" + + latest_version = spaceship_app_latest_known_version(platform, app) + next unless latest_version + + version = latest_version.version_string + builds = spaceship_app_builds(app.id, version, platform) + + builds.filter { |build| !build.expired }.each do |build| + message = "Build Version: #{version}-#{build.version} (#{build.processing_state})" + build.processing_state == 'VALID' ? UI.success(message) : UI.important(message) + + beta_detail = build.build_beta_detail + if beta_detail + beta_state = 'IN_BETA_TESTING' + + int_state = spaceship_app_internal_groups_state(beta_detail, group_builds, build.id) + int_message = int_state[:message] + int_state[:state] == beta_state ? UI.success(int_message) : UI.important(int_message) + + ext_state = spaceship_app_external_groups_state(beta_detail, group_builds, build.id) + ext_message = ext_state[:message] + ext_state[:state] == beta_state ? UI.success(ext_message) : UI.important(ext_message) + + else + UI.important('Internal: NaN / External: NaN') + end + end + UI.message '-----' + end + end + UI.message '-----' + end + # Public Release notes desc 'Publish release notes for iOS and tvOS on Github pages' @@ -776,7 +828,7 @@ platform :ios do clean_build_artifacts end - changelog = nightly_changelog(platform, service) + changelog = nightly_changelog(platform, service, branch_name) schemes.each_index do |index| app_identifier = appstore_nightly_identifiers[index] @@ -1231,7 +1283,11 @@ def what_s_new_for_beta(platform, tag_version) json = what_s_new_for_beta_json(platform) what_s_new = json[tag_version] - what_s_new || '' + what_s_new ||= '' + if !build_name(git_branch_name).empty? && !what_s_new.include?('Beta from a branch') + what_s_new = "Beta from a branch\n\n#{what_s_new}" + end + what_s_new end def build_number_for_version(platform, version) @@ -1400,8 +1456,20 @@ def build_name(branch_name) end end +def changelog_build_name(branch_name) + if branch_name.include? 'feature/' + branch_name.sub('feature/', '').strip + elsif branch_name.include? 'release/' + branch_name.sub('release/', '').strip + elsif branch_name.include? 'hotfix/' + branch_name.sub('hotfix/', '').strip + else + branch_name + end +end + # Return a nightly changelog from git commit messages -def nightly_changelog(platform, service) +def nightly_changelog(platform, service, branch_name) last_commit_hash = last_nightlies_success_git_commit_hash(platform, service) last_commit_hash = 'HEAD^^^^^' if last_commit_hash.length < 12 @@ -1413,7 +1481,9 @@ def nightly_changelog(platform, service) # HAX: strip emoji from changelog changelog = changelog ? changelog.sub(/[\u{1F300}-\u{1F6FF}]/, '').lstrip : '' - changelog.empty? ? 'No change log found for this build.' : changelog + changelog = 'No change log found for this build.' unless changelog && !changelog.empty? + + "Built from #{changelog_build_name(branch_name)} branch\n\n#{changelog}" end # Save the git commit hash in a local text file for nightlies @@ -1814,7 +1884,7 @@ def skip_pull_translations end def pull_translations - Dir.chdir('..') { sh 'Configuration/Scripts/pullCrowdin.sh --skip-copies' } + Dir.chdir('..') { sh 'Scripts/pullCrowdin.sh --skip-copies' } ENV['CROWDIN_TRANSLATIONS_PULLLED'] = '1' end @@ -2094,6 +2164,50 @@ def can_run_deliver(business_unit, platform) false end +def spaceship_app_beta_group_builds(app) + group_builds = [] + app.get_beta_groups.each do |group| + state = group.is_internal_group ? 'Internal' : 'External' + UI.message "TestFlight group: #{group.name} (#{state}). Getting builds..." + + builds = group.fetch_builds.filter { |build| !build.expired } + group_builds.push({ group:, builds: }) + end + group_builds +end + +def spaceship_app_builds(app_id, version, platform) + Spaceship::ConnectAPI::Build.all( + app_id:, + version:, + platform: spaceship_platform(platform) + ) +end + +def spaceship_app_internal_groups_state(beta_detail, group_builds, build_id) + build_filter = ->(group_build) { group_build[:builds].map(&:id).include?(build_id) } + filter_group_builds = group_builds.filter(&build_filter) + beta_groups = filter_group_builds.map { |group_build| group_build[:group] } + + internal_groups = beta_groups.filter(&:is_internal_group) + group_names = internal_groups.map(&:name).join(', ') + state = beta_detail.internal_build_state + message = "- Internal: #{group_names} (#{state})" + { state:, message: } +end + +def spaceship_app_external_groups_state(beta_detail, group_builds, build_id) + build_filter = ->(group_build) { group_build[:builds].map(&:id).include?(build_id) } + filter_group_builds = group_builds.filter(&build_filter) + beta_groups = filter_group_builds.map { |group_build| group_build[:group] } + + external_groups = beta_groups.reject(&:is_internal_group) + group_names = external_groups.map(&:name).join(', ') + state = beta_detail.external_build_state + message = "- External: #{group_names} (#{state})" + { state:, message: } +end + def publish_on_github_pages(output_directory, releases_directory) branch_name = git_branch_name diff --git a/fastlane/README.md b/fastlane/README.md index f0f7f21fa..d12ad0f99 100755 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -167,6 +167,14 @@ Prepare AppStore tvOS releases on App Store Connect with the current version and Get AppStore App status for iOS and tvOS +### ios appStoreTestFlightAppStatus + +```sh +[bundle exec] fastlane ios appStoreTestFlightAppStatus +``` + +Get AppStore TestFlight App status for iOS and tvOS, lastest version + ### ios publishReleaseNotes ```sh