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