diff --git a/.swiftformat b/.swiftformat
new file mode 100644
index 000000000..3b1a6767f
--- /dev/null
+++ b/.swiftformat
@@ -0,0 +1,8 @@
+# format options
+--swiftversion 5.10
+
+# file options
+--exclude Pods,Generated,build,archive,fastlane,vendor
+
+# rules
+--disable trailingCommas,wrapMultilineStatementBraces
\ No newline at end of file
diff --git a/.xcode-version b/.xcode-version
index 8e0f41140..441e3fb38 100644
--- a/.xcode-version
+++ b/.xcode-version
@@ -1 +1 @@
-15.3
\ No newline at end of file
+15.4
\ No newline at end of file
diff --git a/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json b/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json
index ee972a097..b2928811c 100755
--- a/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json
+++ b/Application/Resources/Apps/Play RSI/ApplicationConfiguration.json
@@ -21,11 +21,11 @@
"radioChannels": "[{\"uid\":\"rete-uno\",\"name\":\"Rete Uno\",\"resourceUid\":\"rete_uno\",\"songsViewStyle\":\"collapsed\",\"color\":\"#0074C2\",\"secondColor\":\"#54B8EF\"},{\"uid\":\"rete-due\",\"name\":\"Rete Due\",\"resourceUid\":\"rete_due\",\"songsViewStyle\":\"collapsed\",\"color\":\"#06A73B\",\"secondColor\":\"#30E96B\"},{\"uid\":\"rete-tre\",\"name\":\"Rete Tre\",\"resourceUid\":\"rete_tre\",\"songsViewStyle\":\"collapsed\",\"color\":\"#A4BB1B\",\"secondColor\":\"#DEF355\"},{\"uid\":\"podcast\",\"name\":\"Podcast\",\"resourceUid\":\"rsi_podcast\",\"color\":\"#333333\",\"homeSections\":\"radioLatest,radioFavoriteShows,radioLatestEpisodesFromFavorites,radioResumePlayback,radioMostPopular,radioWatchLater,radioAllShows\"}]",
"tvChannels": "[{\"uid\":\"la1\",\"name\":\"LA 1\",\"resourceUid\":\"la1\",\"color\":\"#FF9120\",\"secondColor\":\"#E15100\"},{\"uid\":\"la2\",\"name\":\"LA 2\",\"resourceUid\":\"la2\",\"color\":\"#FFCF2F\",\"secondColor\":\"#F38A0D\"},{\"uid\":\"143932a79bb5a123a646b68b1d1188d7ae493e5b\",\"name\":\"RTS 1\",\"resourceUid\":\"rts_un\",\"color\":\"#00D6F3\",\"secondColor\":\"#00B6F0\",\"titleColor\":\"#161616\"},{\"uid\":\"d7dfff28deee44e1d3c49a3d37d36d492b29671b\",\"name\":\"RTS 2\",\"resourceUid\":\"rts_deux\",\"color\":\"#BB66FF\",\"secondColor\":\"#782EB5\"},{\"uid\":\"5d332a26e06d08eec8ad385d566187df72955623\",\"name\":\"RTS Info\",\"resourceUid\":\"rts_info\",\"color\":\"#3787FF\",\"secondColor\":\"#153567\"},{\"uid\":\"23FFBE1B-65CE-4188-ADD2-C724186C2C9F\",\"name\":\"SRF 1\",\"resourceUid\":\"tv_srf1\",\"color\":\"#C91024\",\"secondColor\":\"#8D0614\"},{\"uid\":\"E4D5AD08-C1E8-46A3-BB58-4875051D60D2\",\"name\":\"SRF zwei\",\"resourceUid\":\"tv_srf2\",\"color\":\"#FFB600\",\"secondColor\":\"#ED7004\",\"titleColor\":\"#161616\",\"hasDarkStatusBar\":true},{\"uid\":\"34c2819e-e715-43d7-9026-40a443152a97\",\"name\":\"SRF info\",\"resourceUid\":\"tv_srf_info\",\"color\":\"#AF001E\",\"secondColor\":\"#830512\"}]",
"satelliteRadioChannels": "[{\"uid\":\"rsp\",\"name\":\"Radio Swiss Pop\",\"resourceUid\":\"rsp\",\"songsViewStyle\":\"expanded\",\"color\":\"#F01F73\",\"secondColor\":\"#D31A3C\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswisspop.ch/it\"},{\"uid\":\"rsc-it\",\"name\":\"Radio Swiss Classic\",\"resourceUid\":\"rsc\",\"songsViewStyle\":\"expanded\",\"color\":\"#09A1DE\",\"secondColor\":\"#036E99\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissclassic.ch/it\"},{\"uid\":\"rsj\",\"name\":\"Radio Swiss Jazz\",\"resourceUid\":\"rsj\",\"songsViewStyle\":\"expanded\",\"color\":\"#F7B222\",\"secondColor\":\"#CC7A00\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissjazz.ch/it\"}]",
- "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#c01232\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#d7b447\",\"secondColor\":\"#b62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#da2146\",\"secondColor\":\"#2d38c0\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#cd4023\",\"secondColor\":\"#90062e\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#dea706\",\"secondColor\":\"#bd2e5e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44bda8\",\"secondColor\":\"#00324e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1f509d\",\"secondColor\":\"#121a37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#738dae\",\"secondColor\":\"#3a465e\"},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#d75959\",\"secondColor\":\"#29336c\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#27DCF9\",\"secondColor\":\"#932387\"},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#02cde9\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
+ "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#B5344E\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#D7B447\",\"secondColor\":\"#B62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#546591\",\"secondColor\":\"#2C3A50\"},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#CD4023\",\"secondColor\":\"#90062E\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#DEA706\",\"secondColor\":\"#E92466\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44BDA8\",\"secondColor\":\"#00324E\"},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1F509D\",\"secondColor\":\"#121A37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#8B96A5\",\"secondColor\":\"#4F5562\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#D75959\",\"secondColor\":\"#29336C\"},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#23B6CD\",\"secondColor\":\"#7C3184\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#017EB3\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
"continuousPlaybackPlayerViewTransitionDuration": 10,
"continuousPlaybackForegroundTransitionDuration": 0,
"continuousPlaybackBackgroundTransitionDuration": 0,
- "endToleranceRatio": 0.02,
+ "endToleranceRatio": 0.07,
"hiddenOnboardings": "account,favorites_account,resume_playback_account,watch_later_account",
"searchSettingSubtitledHidden": true,
"subtitleAvailabilityHidden": true,
diff --git a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings
index 0dc770275..e297e4b64 100755
--- a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings
+++ b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings
@@ -95,6 +95,10 @@
/* Audio description availability setting label */
"Audio description availability" = "Audiodescrizione disponibile";
+/* Audio home page selection view title
+ Label of the button for audio homepage option selection */
+"Audio home page" = "Audio home page";
+
/* Audios tab title
Header for audio search results
Search setting option
@@ -159,7 +163,9 @@
/* Search setting */
"Date" = "Data";
-/* Poster images setting state */
+/* Audio homepage option setting state
+ Poster images setting state
+ Square images setting state */
"Default (current configuration)" = "Default (configurazione attuale)";
/* User location setting state */
@@ -270,7 +276,8 @@
/* Explanation displayed in the alert asking the user to enable notifications */
"For the application to inform you when a new episode is available, notifications must be enabled." = "Affinché l'app ti possa informare quando è disponibile un nuovo episodio, è necessario attivare le notifiche.";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Force" = "Forzare";
/* Total free space size displayed as a list footer */
@@ -299,7 +306,8 @@
/* Home tab title */
"Home" = "Home";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Ignore" = "Ignorare";
/* User location setting state */
@@ -381,6 +389,12 @@
/* Message title displayed when the user is forced to update the application. */
"Mandatory update" = "È necessario aggiornare";
+/* Many curated audio homepages option setting state */
+"Many curated pages (PAC landing pages)" = "Many curated pages (PAC landing pages)";
+
+/* Many predefined audio homepage option setting state */
+"Many predefined pages" = "Many predefined pages";
+
/* Message on top screen when trying to open a media in the download list and the media is not downloaded. */
"Media not available yet" = "Media non disponibile";
@@ -473,6 +487,9 @@
Title of the search settings button to apply settings */
"OK" = "OK";
+/* One curated audio homepage option setting state */
+"One curated page (PAC Audio)" = "One curated page (PAC Audio)";
+
/* Label of the button opening system settings */
"Open system settings" = "Aprire le impostazioni del sistema";
@@ -661,6 +678,10 @@
Title label used to present sport scheduled livestream medias */
"Sport livestreams" = "Sport in diretta";
+/* Label of the button for Podcast square image format selection
+ Podcast square image format selection view title */
+"Square images" = "Square images";
+
/* Server setting name */
"Stage" = "Stage";
diff --git a/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json b/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json
index e7c91787e..f8f4cf1aa 100755
--- a/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json
+++ b/Application/Resources/Apps/Play RTR/ApplicationConfiguration.json
@@ -19,11 +19,11 @@
"radioChannels": "[{\"uid\":\"12fb886e-b7aa-4e55-beb2-45dbc619f3c4\",\"name\":\"Radio RTR\",\"resourceUid\":\"radio_rtr\",\"songsViewStyle\":\"expanded\",\"color\":\"#AF001D\",\"secondColor\":\"#9B001B\"}]",
"tvChannels": "[{\"uid\":\"la1\",\"name\":\"LA 1\",\"resourceUid\":\"la1\",\"color\":\"#FF9120\",\"secondColor\":\"#E15100\"},{\"uid\":\"la2\",\"name\":\"LA 2\",\"resourceUid\":\"la2\",\"color\":\"#FFCF2F\",\"secondColor\":\"#F38A0D\"},{\"uid\":\"f5dc82ed-4564-4223-903f-0bf6a13c5620\",\"name\":\"RTR auf SRF 1\",\"resourceUid\":\"rtr_srf1\",\"color\":\"#C91024\",\"secondColor\":\"#8D0614\"},{\"uid\":\"80bdf859-b58d-421d-bb27-ce1fba4637a7\",\"name\":\"RTR auf SRF Info\",\"resourceUid\":\"rtr_srf_info\",\"color\":\"#AF001E\",\"secondColor\":\"#830512\"},{\"uid\":\"2541c864-f883-4b80-9459-e1026e0e692e\",\"name\":\"RTR auf SRF 2\",\"resourceUid\":\"rtr_srf2\",\"color\":\"#FFB600\",\"secondColor\":\"#ED7004\",\"titleColor\":\"#333333\",\"hasDarkStatusBar\":true},{\"uid\":\"143932a79bb5a123a646b68b1d1188d7ae493e5b\",\"name\":\"RTS 1\",\"resourceUid\":\"rts_un\",\"color\":\"#00D6F3\",\"secondColor\":\"#00B6F0\",\"titleColor\":\"#161616\"},{\"uid\":\"d7dfff28deee44e1d3c49a3d37d36d492b29671b\",\"name\":\"RTS 2\",\"resourceUid\":\"rts_deux\",\"color\":\"#BB66FF\",\"secondColor\":\"#782EB5\"},{\"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, \"shareURL\":\"https://www.radioswisspop.ch/de\"},{\"uid\":\"rsc-de\",\"name\":\"Radio Swiss Classic\",\"resourceUid\":\"rsc\",\"songsViewStyle\":\"expanded\",\"color\":\"#09A1DE\",\"secondColor\":\"#036E99\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissclassic.ch/de\"},{\"uid\":\"rsj\",\"name\":\"Radio Swiss Jazz\",\"resourceUid\":\"rsj\",\"songsViewStyle\":\"expanded\",\"color\":\"#F7B222\",\"secondColor\":\"#CC7A00\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissjazz.ch/de\"}]",
- "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#c01232\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#d7b447\",\"secondColor\":\"#b62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#da2146\",\"secondColor\":\"#2d38c0\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#cd4023\",\"secondColor\":\"#90062e\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#dea706\",\"secondColor\":\"#bd2e5e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44bda8\",\"secondColor\":\"#00324e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1f509d\",\"secondColor\":\"#121a37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#738dae\",\"secondColor\":\"#3a465e\"},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#d75959\",\"secondColor\":\"#29336c\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#27DCF9\",\"secondColor\":\"#932387\"},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#02cde9\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
+ "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#B5344E\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#D7B447\",\"secondColor\":\"#B62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#546591\",\"secondColor\":\"#2C3A50\"},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#CD4023\",\"secondColor\":\"#90062E\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#DEA706\",\"secondColor\":\"#E92466\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44BDA8\",\"secondColor\":\"#00324E\"},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1F509D\",\"secondColor\":\"#121A37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#8B96A5\",\"secondColor\":\"#4F5562\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#D75959\",\"secondColor\":\"#29336C\"},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#23B6CD\",\"secondColor\":\"#7C3184\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#017EB3\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
"continuousPlaybackPlayerViewTransitionDuration": 10,
"continuousPlaybackForegroundTransitionDuration": 0,
"continuousPlaybackBackgroundTransitionDuration": 0,
- "endToleranceRatio": 0.02,
+ "endToleranceRatio": 0.07,
"hiddenOnboardings": "account,favorites_account,resume_playback_account,watch_later_account",
"discoverySubtitleOptionLanguage": "de",
"audioDescriptionAvailabilityHidden": true,
diff --git a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings
index 1e23768a3..90d61faa2 100755
--- a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings
+++ b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings
@@ -95,6 +95,10 @@
/* Audio description availability setting label */
"Audio description availability" = "Disponibilitad dad audio description";
+/* Audio home page selection view title
+ Label of the button for audio homepage option selection */
+"Audio home page" = "Padina da audio";
+
/* Audios tab title
Header for audio search results
Search setting option
@@ -159,7 +163,9 @@
/* Search setting */
"Date" = "Data";
-/* Poster images setting state */
+/* Audio homepage option setting state
+ Poster images setting state
+ Square images setting state */
"Default (current configuration)" = "Standard (configuraziun actuala)";
/* User location setting state */
@@ -270,7 +276,8 @@
/* Explanation displayed in the alert asking the user to enable notifications */
"For the application to inform you when a new episode is available, notifications must be enabled." = "Vegn utilisa da la applicaziun per infurmar vus cura che novas episodas stattan a disposiziun, la funcziunalitad da notificaziun sto esser activada.";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Force" = "Sfurzar";
/* Total free space size displayed as a list footer */
@@ -299,7 +306,8 @@
/* Home tab title */
"Home" = "Home";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Ignore" = "Ignorar";
/* User location setting state */
@@ -381,6 +389,12 @@
/* Message title displayed when the user is forced to update the application. */
"Mandatory update" = "Dovra update";
+/* Many curated audio homepages option setting state */
+"Many curated pages (PAC landing pages)" = "Pliras paginas curatadas (PAC landing pages)";
+
+/* Many predefined audio homepage option setting state */
+"Many predefined pages" = "Pliras paginas predefinadas";
+
/* Message on top screen when trying to open a media in the download list and the media is not downloaded. */
"Media not available yet" = "Media betg disponibel";
@@ -473,6 +487,9 @@
Title of the search settings button to apply settings */
"OK" = "OK";
+/* One curated audio homepage option setting state */
+"One curated page (PAC Audio)" = "Ina pagina curatada (PAC audio)";
+
/* Label of the button opening system settings */
"Open system settings" = "Avrir las preferenzas dal sistem";
@@ -661,6 +678,10 @@
Title label used to present sport scheduled livestream medias */
"Sport livestreams" = "RTR livestreams";
+/* Label of the button for Podcast square image format selection
+ Podcast square image format selection view title */
+"Square images" = "Maletgs quadratics";
+
/* Server setting name */
"Stage" = "Stage";
diff --git a/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json b/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json
index 53c478300..ab5040243 100755
--- a/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json
+++ b/Application/Resources/Apps/Play RTS/ApplicationConfiguration.json
@@ -24,11 +24,11 @@
"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\":\"la1\",\"name\":\"LA 1\",\"resourceUid\":\"la1\",\"color\":\"#FF9120\",\"secondColor\":\"#E15100\"},{\"uid\":\"la2\",\"name\":\"LA 2\",\"resourceUid\":\"la2\",\"color\":\"#FFCF2F\",\"secondColor\":\"#F38A0D\"},{\"uid\":\"143932a79bb5a123a646b68b1d1188d7ae493e5b\",\"name\":\"RTS 1\",\"resourceUid\":\"rts_un\",\"color\":\"#00D6F3\",\"secondColor\":\"#00B6F0\",\"titleColor\":\"#161616\"},{\"uid\":\"d7dfff28deee44e1d3c49a3d37d36d492b29671b\",\"name\":\"RTS 2\",\"resourceUid\":\"rts_deux\",\"color\":\"#BB66FF\",\"secondColor\":\"#782EB5\"},{\"uid\":\"5d332a26e06d08eec8ad385d566187df72955623\",\"name\":\"RTS Info\",\"resourceUid\":\"rts_info\",\"color\":\"#3787FF\",\"secondColor\":\"#153567\"},{\"uid\":\"23FFBE1B-65CE-4188-ADD2-C724186C2C9F\",\"name\":\"SRF 1\",\"resourceUid\":\"tv_srf1\",\"color\":\"#C91024\",\"secondColor\":\"#8D0614\"},{\"uid\":\"E4D5AD08-C1E8-46A3-BB58-4875051D60D2\",\"name\":\"SRF zwei\",\"resourceUid\":\"tv_srf2\",\"color\":\"#FFB600\",\"secondColor\":\"#ED7004\",\"titleColor\":\"#161616\",\"hasDarkStatusBar\":true},{\"uid\":\"34c2819e-e715-43d7-9026-40a443152a97\",\"name\":\"SRF info\",\"resourceUid\":\"tv_srf_info\",\"color\":\"#AF001E\",\"secondColor\":\"#830512\"}]",
"satelliteRadioChannels": "[{\"uid\":\"rsp\",\"name\":\"Radio Swiss Pop\",\"resourceUid\":\"rsp\",\"songsViewStyle\":\"expanded\",\"color\":\"#F01F73\",\"secondColor\":\"#D31A3C\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswisspop.ch/fr\"},{\"uid\":\"rsc-fr\",\"name\":\"Radio Swiss Classic\",\"resourceUid\":\"rsc\",\"songsViewStyle\":\"expanded\",\"color\":\"#09A1DE\",\"secondColor\":\"#036E99\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissclassic.ch/fr\"},{\"uid\":\"rsj\",\"name\":\"Radio Swiss Jazz\",\"resourceUid\":\"rsj\",\"songsViewStyle\":\"expanded\",\"color\":\"#F7B222\",\"secondColor\":\"#CC7A00\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissjazz.ch/fr\"}]",
- "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#c01232\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#d7b447\",\"secondColor\":\"#b62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#da2146\",\"secondColor\":\"#2d38c0\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#cd4023\",\"secondColor\":\"#90062e\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#dea706\",\"secondColor\":\"#bd2e5e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44bda8\",\"secondColor\":\"#00324e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1f509d\",\"secondColor\":\"#121a37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#738dae\",\"secondColor\":\"#3a465e\"},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#d75959\",\"secondColor\":\"#29336c\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#27DCF9\",\"secondColor\":\"#932387\"},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#02cde9\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
+ "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#B5344E\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#D7B447\",\"secondColor\":\"#B62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#546591\",\"secondColor\":\"#2C3A50\"},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#CD4023\",\"secondColor\":\"#90062E\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#DEA706\",\"secondColor\":\"#E92466\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44BDA8\",\"secondColor\":\"#00324E\"},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1F509D\",\"secondColor\":\"#121A37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#8B96A5\",\"secondColor\":\"#4F5562\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#D75959\",\"secondColor\":\"#29336C\"},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#23B6CD\",\"secondColor\":\"#7C3184\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#017EB3\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
"continuousPlaybackPlayerViewTransitionDuration": 10,
"continuousPlaybackForegroundTransitionDuration": 0,
"continuousPlaybackBackgroundTransitionDuration": 0,
- "endToleranceRatio": 0.02,
+ "endToleranceRatio": 0.07,
"hiddenOnboardings": "favorites,resume_playback,watch_later",
"showLeadPreferred": true,
"tvGuideOtherBouquets": "srf,rsi",
diff --git a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings
index 631783823..470c85a64 100644
--- a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings
+++ b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings
@@ -95,6 +95,10 @@
/* Audio description availability setting label */
"Audio description availability" = "Disponibilité de l'audiodescription";
+/* Audio home page selection view title
+ Label of the button for audio homepage option selection */
+"Audio home page" = "Page Audios";
+
/* Audios tab title
Header for audio search results
Search setting option
@@ -159,7 +163,9 @@
/* Search setting */
"Date" = "Date";
-/* Poster images setting state */
+/* Audio homepage option setting state
+ Poster images setting state
+ Square images setting state */
"Default (current configuration)" = "Défaut (configuration actuelle)";
/* User location setting state */
@@ -270,7 +276,8 @@
/* Explanation displayed in the alert asking the user to enable notifications */
"For the application to inform you when a new episode is available, notifications must be enabled." = "Pour que l'application puisse vous informer quand un nouvel épisode est disponible, les notifications doivent être activées.";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Force" = "Forcer";
/* Total free space size displayed as a list footer */
@@ -299,7 +306,8 @@
/* Home tab title */
"Home" = "Accueil";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Ignore" = "Ignorer";
/* User location setting state */
@@ -381,6 +389,12 @@
/* Message title displayed when the user is forced to update the application. */
"Mandatory update" = "Mise à jour obligatoire";
+/* Many curated audio homepages option setting state */
+"Many curated pages (PAC landing pages)" = "Plusieurs pages éditorialisées (Pages d'accueil PAC)";
+
+/* Many predefined audio homepage option setting state */
+"Many predefined pages" = "Plusieurs pages préfinies";
+
/* Message on top screen when trying to open a media in the download list and the media is not downloaded. */
"Media not available yet" = "Contenu non disponible";
@@ -473,6 +487,9 @@
Title of the search settings button to apply settings */
"OK" = "OK";
+/* One curated audio homepage option setting state */
+"One curated page (PAC Audio)" = "Une page éditorialisée (PAC Audio)";
+
/* Label of the button opening system settings */
"Open system settings" = "Ouvrir Réglages";
@@ -661,6 +678,10 @@
Title label used to present sport scheduled livestream medias */
"Sport livestreams" = "Sports en direct";
+/* Label of the button for Podcast square image format selection
+ Podcast square image format selection view title */
+"Square images" = "Square images";
+
/* Server setting name */
"Stage" = "Stage";
diff --git a/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json b/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json
index 51c611713..7f18c43aa 100755
--- a/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json
+++ b/Application/Resources/Apps/Play SRF/ApplicationConfiguration.json
@@ -20,11 +20,11 @@
"radioChannels": "[{\"uid\":\"69e8ac16-4327-4af4-b873-fd5cd6e895a7\",\"name\":\"Radio SRF 1\",\"resourceUid\":\"srf1\",\"songsViewStyle\":\"collapsed\",\"color\":\"#F7A600\",\"secondColor\":\"#FFD651\",\"titleColor\":\"#161616\",\"hasDarkStatusBar\":true,\"numberOfLivePlaceholders\":8},{\"uid\":\"c8537421-c9c5-4461-9c9c-c15816458b46\",\"name\":\"Radio SRF 2 Kultur\",\"resourceUid\":\"srf2\",\"songsViewStyle\":\"collapsed\",\"color\":\"#CA3DAB\",\"secondColor\":\"#8C1D60\"},{\"uid\":\"dd0fa1ba-4ff6-4e1a-ab74-d7e49057d96f\",\"name\":\"Radio SRF 3\",\"resourceUid\":\"srf3\",\"songsViewStyle\":\"expanded\",\"color\":\"#464646\",\"secondColor\":\"#000000\"},{\"uid\":\"ee1fb348-2b6a-4958-9aac-ec6c87e190da\",\"name\":\"Radio SRF 4 News\",\"resourceUid\":\"srf4\",\"color\":\"#E31F2B\",\"secondColor\":\"#6A0B0C\"},{\"uid\":\"a9c5c070-8899-46c7-ac27-f04f1be902fd\",\"name\":\"Radio SRF Musikwelle\",\"resourceUid\":\"srf_musikwelle\",\"songsViewStyle\":\"expanded\",\"color\":\"#42A3F1\",\"secondColor\":\"#0066B0\"},{\"uid\":\"66815fe2-9008-4853-80a5-f9caaffdf3a9\",\"name\":\"Radio SRF Virus\",\"resourceUid\":\"virus\",\"songsViewStyle\":\"expanded\",\"color\":\"#A5FF00\",\"secondColor\":\"#BDFF44\",\"titleColor\":\"#161616\",\"hasDarkStatusBar\":true,\"homepageHidden\":true}]",
"tvChannels": "[{\"uid\":\"la1\",\"name\":\"LA 1\",\"resourceUid\":\"la1\",\"color\":\"#FF9120\",\"secondColor\":\"#E15100\"},{\"uid\":\"la2\",\"name\":\"LA 2\",\"resourceUid\":\"la2\",\"color\":\"#FFCF2F\",\"secondColor\":\"#F38A0D\"},{\"uid\":\"143932a79bb5a123a646b68b1d1188d7ae493e5b\",\"name\":\"RTS 1\",\"resourceUid\":\"rts_un\",\"color\":\"#00D6F3\",\"secondColor\":\"#00B6F0\",\"titleColor\":\"#161616\"},{\"uid\":\"d7dfff28deee44e1d3c49a3d37d36d492b29671b\",\"name\":\"RTS 2\",\"resourceUid\":\"rts_deux\",\"color\":\"#BB66FF\",\"secondColor\":\"#782EB5\"},{\"uid\":\"5d332a26e06d08eec8ad385d566187df72955623\",\"name\":\"RTS Info\",\"resourceUid\":\"rts_info\",\"color\":\"#3787FF\",\"secondColor\":\"#153567\"},{\"uid\":\"23FFBE1B-65CE-4188-ADD2-C724186C2C9F\",\"name\":\"SRF 1\",\"resourceUid\":\"tv_srf1\",\"color\":\"#C91024\",\"secondColor\":\"#8D0614\"},{\"uid\":\"E4D5AD08-C1E8-46A3-BB58-4875051D60D2\",\"name\":\"SRF zwei\",\"resourceUid\":\"tv_srf2\",\"color\":\"#FFB600\",\"secondColor\":\"#ED7004\",\"titleColor\":\"#161616\",\"hasDarkStatusBar\":true},{\"uid\":\"34c2819e-e715-43d7-9026-40a443152a97\",\"name\":\"SRF info\",\"resourceUid\":\"tv_srf_info\",\"color\":\"#AF001E\",\"secondColor\":\"#830512\"}]",
"satelliteRadioChannels": "[{\"uid\":\"rsp\",\"name\":\"Radio Swiss Pop\",\"resourceUid\":\"rsp\",\"songsViewStyle\":\"expanded\",\"color\":\"#F01F73\",\"secondColor\":\"#D31A3C\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswisspop.ch/de\"},{\"uid\":\"rsc-de\",\"name\":\"Radio Swiss Classic\",\"resourceUid\":\"rsc\",\"songsViewStyle\":\"expanded\",\"color\":\"#09A1DE\",\"secondColor\":\"#036E99\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissclassic.ch/de\"},{\"uid\":\"rsj\",\"name\":\"Radio Swiss Jazz\",\"resourceUid\":\"rsj\",\"songsViewStyle\":\"expanded\",\"color\":\"#F7B222\",\"secondColor\":\"#CC7A00\",\"homepageHidden\":true, \"shareURL\":\"https://www.radioswissjazz.ch/de\"}]",
- "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#c01232\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#d7b447\",\"secondColor\":\"#b62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#da2146\",\"secondColor\":\"#2d38c0\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#cd4023\",\"secondColor\":\"#90062e\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#dea706\",\"secondColor\":\"#bd2e5e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44bda8\",\"secondColor\":\"#00324e\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1f509d\",\"secondColor\":\"#121a37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#738dae\",\"secondColor\":\"#3a465e\"},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#d75959\",\"secondColor\":\"#29336c\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#27DCF9\",\"secondColor\":\"#932387\"},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#02cde9\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
+ "topicColors": "{\"urn:rsi:topic:tv:1\":{\"firstColor\":\"#B5344E\",\"secondColor\":\"#480010\"},\"urn:rsi:topic:tv:4\":{\"firstColor\":\"#D7B447\",\"secondColor\":\"#B62019\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:7\":{\"firstColor\":\"#546591\",\"secondColor\":\"#2C3A50\"},\"urn:rsi:topic:tv:8\":{\"firstColor\":\"#CD4023\",\"secondColor\":\"#90062E\"},\"urn:rsi:topic:tv:11\":{\"firstColor\":\"#DEA706\",\"secondColor\":\"#E92466\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:40\":{\"firstColor\":\"#44BDA8\",\"secondColor\":\"#00324E\"},\"urn:rsi:topic:tv:80\":{\"firstColor\":\"#1F509D\",\"secondColor\":\"#121A37\"},\"urn:rsi:topic:tv:90\":{\"firstColor\":\"#8B96A5\",\"secondColor\":\"#4F5562\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:100\":{\"firstColor\":\"#D75959\",\"secondColor\":\"#29336C\"},\"urn:rsi:topic:tv:600\":{\"firstColor\":\"#23B6CD\",\"secondColor\":\"#7C3184\",\"reduceBrightness\":true},\"urn:rsi:topic:tv:6000\":{\"firstColor\":\"#017EB3\",\"secondColor\":\"#011844\"},\"urn:rtr:topic:tv:2d48ba80-566c-4359-9e8d-8d9b2d570e0a\":{\"firstColor\":\"#00A1A1\",\"secondColor\":\"#04575B\"},\"urn:rtr:topic:tv:7d7f21be-6727-4939-9126-5bca25eb3a49\":{\"firstColor\":\"#80D2E3\",\"secondColor\":\"#003D58\"},\"urn:rtr:topic:tv:20e7478f-1ea1-49c3-81c2-5f157d6ff092\":{\"firstColor\":\"#340101\",\"secondColor\":\"#8F0E0F\"},\"urn:rtr:topic:tv:50bb90d6-41af-4bbd-b92c-6ef5db16a9b3\":{\"firstColor\":\"#8A0533\",\"secondColor\":\"#812626\"},\"urn:rtr:topic:tv:c50140e7-5740-4c44-abd0-0f7d9ea68da7\":{\"firstColor\":\"#A6A6A7\",\"secondColor\":\"#2C2B2D\"},\"urn:rtr:topic:tv:dfb7ae6d-cb73-431b-a817-b1663ec2f58a\":{\"firstColor\":\"#00F8CC\",\"secondColor\":\"#018864\"},\"urn:rts:topic:tv:623\":{\"firstColor\":\"#5C845B\",\"secondColor\":\"#16280F\"},\"urn:rts:topic:tv:665\":{\"firstColor\":\"#3787FF\",\"secondColor\":\"#0A1C33\"},\"urn:rts:topic:tv:1095\":{\"firstColor\":\"#F5F500\",\"secondColor\":\"#BEB405\",\"reduceBrightness\":true},\"urn:rts:topic:tv:1353\":{\"firstColor\":\"#084165\",\"secondColor\":\"#140953\"},\"urn:rts:topic:tv:2743\":{\"firstColor\":\"#BCF6FF\",\"secondColor\":\"#00D0EF\",\"reduceBrightness\":true},\"urn:rts:topic:tv:10193\":{\"firstColor\":\"#EB2350\",\"secondColor\":\"#A61637\"},\"urn:rts:topic:tv:54537\":{\"firstColor\":\"#FFE03E\",\"secondColor\":\"#F98E73\",\"reduceBrightness\":true},\"urn:rts:topic:tv:59220\":{\"firstColor\":\"#492b63\",\"secondColor\":\"#271633\"},\"urn:rts:topic:tv:67132\":{\"firstColor\":\"#415FAF\",\"secondColor\":\"#23376B\"},\"urn:srf:topic:tv:1d7d9cfb-6682-4d5b-9e36-322e8fa93c03\":{\"firstColor\":\"#00A4B3\",\"secondColor\":\"#006973\"},\"urn:srf:topic:tv:4acf86dd-7ff7-45d3-baf8-33375340d976\":{\"firstColor\":\"#3f4b70\",\"secondColor\":\"#131a2d\"},\"urn:srf:topic:tv:9a79b1de-cde8-4528-b304-d1ae1363f52f\":{\"firstColor\":\"#836fcd\",\"secondColor\":\"#36343f\"},\"urn:srf:topic:tv:63f937e4-859e-42c4-a430-bdb74dd09645\":{\"firstColor\":\"#4480a2\",\"secondColor\":\"#20182c\"},\"urn:srf:topic:tv:67f812fd-19a3-4c22-9e6b-ec36e65a4703\":{\"firstColor\":\"#bb3966\",\"secondColor\":\"#190406\"},\"urn:srf:topic:tv:593eb926-d892-41ba-8b1f-eccbcfd7f15f\":{\"firstColor\":\"#2bbf9b\",\"secondColor\":\"#02291e\"},\"urn:srf:topic:tv:649e36d7-ff57-41c8-9c1b-7892daf15e78\":{\"firstColor\":\"#FF0037\",\"secondColor\":\"#AF001E\"},\"urn:srf:topic:tv:882cb264-cf81-4a9c-b660-d42519b7ce28\":{\"firstColor\":\"#c91d7d\",\"secondColor\":\"#31041f\"},\"urn:srf:topic:tv:43741c59-317e-458b-ac38-c2b1c065c865\":{\"firstColor\":\"#0075ad\",\"secondColor\":\"#000022\"},\"urn:srf:topic:tv:516421f0-ec89-43ba-823b-1b5ceec262f3\":{\"firstColor\":\"#5FB281\",\"secondColor\":\"#154e60\"},\"urn:srf:topic:tv:641223fa-f112-4d98-8aec-cb22262a1182\":{\"firstColor\":\"#c55cee\",\"secondColor\":\"#0c1c68\"},\"urn:srf:topic:tv:a2d97206-0b85-4226-8afe-06e86ebd05b2\":{\"firstColor\":\"#9fc885\",\"secondColor\":\"#20281a\"},\"urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712\":{\"firstColor\":\"#b3131d\",\"secondColor\":\"#3e0b14\"},\"urn:srf:topic:tv:b58dcf14-96ac-4046-8676-fd8a942c0e88\":{\"firstColor\":\"#7081b0\",\"secondColor\":\"#202020\"},\"urn:srf:topic:tv:bb7b21e0-1056-4e28-bac3-c610393b5b0f\":{\"firstColor\":\"#3c788e\",\"secondColor\":\"#1b3e48\"},\"urn:srf:topic:tv:e52080fc-f36b-481e-955f-071b6c8d6dc3\":{\"firstColor\":\"#ff6778\",\"secondColor\":\"#920a1a\",\"reduceBrightness\":true},\"urn:srf:topic:tv:fa793c13-bebc-41b9-9710-bf8a34192c15\":{\"firstColor\":\"#baead5\",\"secondColor\":\"#010b40\",\"reduceBrightness\":true}}",
"continuousPlaybackPlayerViewTransitionDuration": 10,
"continuousPlaybackForegroundTransitionDuration": 0,
"continuousPlaybackBackgroundTransitionDuration": 0,
- "endToleranceRatio": 0.02,
+ "endToleranceRatio": 0.07,
"hiddenOnboardings": "account,favorites_account,resume_playback_account,watch_later_account",
"audioDescriptionAvailabilityHidden": true,
"posterImagesEnabled": true,
diff --git a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings
index 5e929500e..de13187aa 100755
--- a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings
+++ b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings
@@ -95,6 +95,10 @@
/* Audio description availability setting label */
"Audio description availability" = "Audiodeskription verfügbar";
+/* Audio home page selection view title
+ Label of the button for audio homepage option selection */
+"Audio home page" = "Audio home page";
+
/* Audios tab title
Header for audio search results
Search setting option
@@ -159,7 +163,9 @@
/* Search setting */
"Date" = "Datum";
-/* Poster images setting state */
+/* Audio homepage option setting state
+ Poster images setting state
+ Square images setting state */
"Default (current configuration)" = "Standard (aktuelle Konfiguration)";
/* User location setting state */
@@ -270,7 +276,8 @@
/* Explanation displayed in the alert asking the user to enable notifications */
"For the application to inform you when a new episode is available, notifications must be enabled." = "Damit Play SRF Sie über neue Episoden informieren kann, müssen Push-Mitteilungen aktiviert werden.";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Force" = "Erzwingen";
/* Total free space size displayed as a list footer */
@@ -299,7 +306,8 @@
/* Home tab title */
"Home" = "Home";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Ignore" = "Ignorieren";
/* User location setting state */
@@ -381,6 +389,12 @@
/* Message title displayed when the user is forced to update the application. */
"Mandatory update" = "Update erforderlich";
+/* Many curated audio homepages option setting state */
+"Many curated pages (PAC landing pages)" = "Many curated pages (PAC landing pages)";
+
+/* Many predefined audio homepage option setting state */
+"Many predefined pages" = "Many predefined pages";
+
/* Message on top screen when trying to open a media in the download list and the media is not downloaded. */
"Media not available yet" = "Inhalt noch nicht verfügbar";
@@ -473,6 +487,9 @@
Title of the search settings button to apply settings */
"OK" = "OK";
+/* One curated audio homepage option setting state */
+"One curated page (PAC Audio)" = "One curated page (PAC Audio)";
+
/* Label of the button opening system settings */
"Open system settings" = "Einstellungen anzeigen";
@@ -661,6 +678,10 @@
Title label used to present sport scheduled livestream medias */
"Sport livestreams" = "Sport-Livestreams";
+/* Label of the button for Podcast square image format selection
+ Podcast square image format selection view title */
+"Square images" = "Square images";
+
/* Server setting name */
"Stage" = "Stage";
diff --git a/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json b/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json
index e5a499552..5612d9e11 100755
--- a/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json
+++ b/Application/Resources/Apps/Play SWI/ApplicationConfiguration.json
@@ -20,7 +20,7 @@
"continuousPlaybackPlayerViewTransitionDuration": 10,
"continuousPlaybackForegroundTransitionDuration": 0,
"continuousPlaybackBackgroundTransitionDuration": 0,
- "endToleranceRatio": 0.02,
+ "endToleranceRatio": 0.07,
"searchSettingsHidden": true,
"showsSearchHidden": true,
"hiddenOnboardings": "account,favorites_account,resume_playback_account,watch_later_account",
diff --git a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings
index fa67e5be8..6eddef361 100755
--- a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings
+++ b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings
@@ -95,6 +95,10 @@
/* Audio description availability setting label */
"Audio description availability" = "Audio description availability";
+/* Audio home page selection view title
+ Label of the button for audio homepage option selection */
+"Audio home page" = "Audio home page";
+
/* Audios tab title
Header for audio search results
Search setting option
@@ -159,7 +163,9 @@
/* Search setting */
"Date" = "Date";
-/* Poster images setting state */
+/* Audio homepage option setting state
+ Poster images setting state
+ Square images setting state */
"Default (current configuration)" = "Default (current configuration)";
/* User location setting state */
@@ -270,7 +276,8 @@
/* Explanation displayed in the alert asking the user to enable notifications */
"For the application to inform you when a new episode is available, notifications must be enabled." = "For the application to inform you when a new episode is available, notifications must be enabled.";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Force" = "Force";
/* Total free space size displayed as a list footer */
@@ -299,7 +306,8 @@
/* Home tab title */
"Home" = "Home";
-/* Poster images setting state */
+/* Poster images setting state
+ Square images setting state */
"Ignore" = "Ignore";
/* User location setting state */
@@ -381,6 +389,12 @@
/* Message title displayed when the user is forced to update the application. */
"Mandatory update" = "Mandatory update";
+/* Many curated audio homepages option setting state */
+"Many curated pages (PAC landing pages)" = "Many curated pages (PAC landing pages)";
+
+/* Many predefined audio homepage option setting state */
+"Many predefined pages" = "Many predefined pages";
+
/* Message on top screen when trying to open a media in the download list and the media is not downloaded. */
"Media not available yet" = "Media not available yet";
@@ -473,6 +487,9 @@
Title of the search settings button to apply settings */
"OK" = "OK";
+/* One curated audio homepage option setting state */
+"One curated page (PAC Audio)" = "One curated page (PAC Audio)";
+
/* Label of the button opening system settings */
"Open system settings" = "Open system settings";
@@ -661,6 +678,10 @@
Title label used to present sport scheduled livestream medias */
"Sport livestreams" = "Sport livestreams";
+/* Label of the button for Podcast square image format selection
+ Podcast square image format selection view title */
+"Square images" = "Square images";
+
/* Server setting name */
"Stage" = "Stage";
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 d936d543d..5ddb3b792 100755
--- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt
+++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt
@@ -28,94 +28,19 @@ body: AutoCoding
Copyrigh…
version: 2.2.3
-name: MaterialComponents, nameSpecified:
-body:
- …
-version: 118.2.0
-
-name: MaterialComponents, nameSpecified:
-body:
- …
-version: 118.2.0
-
-name: MaterialComponents, nameSpecified:
-body:
- …
-version: 118.2.0
-
-name: MaterialComponents, nameSpecified:
-body:
- …
-version: 118.2.0
-
-name: MaterialComponents, nameSpecified:
-body:
- …
-version: 118.2.0
-
-name: MDFInternationalization, nameSpecified:
-body:
- …
-version: 3.0.0
-
-name: MDFInternationalization, nameSpecified:
-body:
- …
-version: 3.0.0
-
-name: MDFInternationalization, nameSpecified:
-body:
- …
-version: 3.0.0
-
-name: MDFInternationalization, nameSpecified:
-body:
- …
-version: 3.0.0
-
-name: MDFInternationalization, nameSpecified:
-body:
- …
-version: 3.0.0
-
-name: MDFTextAccessibility, nameSpecified:
-body:
- …
-version: 2.0.1
-
-name: MDFTextAccessibility, nameSpecified:
-body:
- …
-version: 2.0.1
-
-name: MDFTextAccessibility, nameSpecified:
-body:
- …
-version: 2.0.1
-
-name: MDFTextAccessibility, nameSpecified:
-body:
- …
-version: 2.0.1
-
-name: MDFTextAccessibility, nameSpecified:
-body:
- …
-version: 2.0.1
-
-name: abseil-cpp-binary, nameSpecified: abseil, owner: google, version: 1.2024011601.0, source: https://github.com/google/abseil-cpp-binary
+name: abseil-cpp-binary, nameSpecified: abseil, owner: google, version: 1.2024011601.1, source: https://github.com/google/abseil-cpp-binary
name: Aiolos, nameSpecified: Aiolos, owner: IdeasOnCanvas, version: 1.9.8, source: https://github.com/IdeasOnCanvas/Aiolos
-name: app-check, nameSpecified: AppCheck, owner: google, version: 10.18.1, source: https://github.com/google/app-check
+name: app-check, nameSpecified: AppCheck, owner: google, version: 10.19.1, source: https://github.com/google/app-check
-name: appcenter-sdk-apple, nameSpecified: AppCenter, owner: microsoft, version: 5.0.4, source: https://github.com/microsoft/appcenter-sdk-apple
+name: appcenter-sdk-apple, nameSpecified: AppCenter, owner: microsoft, version: 5.0.5, source: https://github.com/microsoft/appcenter-sdk-apple
name: Comscore-Swift-Package-Manager, nameSpecified: Comscore-Swift-Package-Manager, owner: comScore, version: 6.11.0, source: https://github.com/comScore/Comscore-Swift-Package-Manager
name: DZNEmptyDataSet, nameSpecified: DZNEmptyDataSet, owner: dzenbot, version: , source: https://github.com/dzenbot/DZNEmptyDataSet
-name: firebase-ios-sdk, nameSpecified: Firebase, owner: firebase, version: 10.23.1, source: https://github.com/firebase/firebase-ios-sdk
+name: firebase-ios-sdk, nameSpecified: Firebase, owner: firebase, version: 10.27.0, source: https://github.com/firebase/firebase-ios-sdk
name: FSCalendar, nameSpecified: FSCalendar, owner: WenchaoD, version: 2.8.4, source: https://github.com/WenchaoD/FSCalendar
@@ -123,25 +48,25 @@ name: FXReachability, nameSpecified: FXReachability, owner: SRGSSR, version: 1.3
name: Gifu, nameSpecified: Gifu, owner: kaishin, version: 3.4.1, source: https://github.com/kaishin/Gifu
-name: GoogleAppMeasurement, nameSpecified: GoogleAppMeasurement, owner: google, version: 10.23.1, source: https://github.com/google/GoogleAppMeasurement
+name: GoogleAppMeasurement, nameSpecified: GoogleAppMeasurement, owner: google, version: 10.27.0, source: https://github.com/google/GoogleAppMeasurement
name: GoogleCastSDK-no-bluetooth, nameSpecified: GoogleCastSDK-no-bluetooth, owner: SRGSSR, version: 4.8.0, source: https://github.com/SRGSSR/GoogleCastSDK-no-bluetooth
name: GoogleDataTransport, nameSpecified: GoogleDataTransport, owner: google, version: 9.4.0, source: https://github.com/google/GoogleDataTransport
-name: GoogleUtilities, nameSpecified: GoogleUtilities, owner: google, version: 7.13.1, source: https://github.com/google/GoogleUtilities
+name: GoogleUtilities, nameSpecified: GoogleUtilities, owner: google, version: 7.13.3, source: https://github.com/google/GoogleUtilities
-name: grpc-binary, nameSpecified: gRPC, owner: google, version: 1.62.1, source: https://github.com/google/grpc-binary
+name: grpc-binary, nameSpecified: gRPC, owner: google, version: 1.62.2, source: https://github.com/google/grpc-binary
-name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 3.3.2, source: https://github.com/google/gtm-session-fetcher
+name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 3.4.1, source: https://github.com/google/gtm-session-fetcher
name: interop-ios-for-google-sdks, nameSpecified: InteropForGoogle, owner: google, version: 100.0.0, source: https://github.com/google/interop-ios-for-google-sdks
-name: ios-library, nameSpecified: Airship, owner: urbanairship, version: 16.12.6, source: https://github.com/urbanairship/ios-library
+name: ios-library, nameSpecified: Airship, owner: urbanairship, version: 16.12.7, source: https://github.com/urbanairship/ios-library
-name: iOSV5, nameSpecified: TagCommander SDK V5, owner: CommandersAct, version: 5.4.6, source: https://github.com/CommandersAct/iOSV5
+name: iOSV5, nameSpecified: TagCommander SDK V5, owner: CommandersAct, version: 5.4.9, source: https://github.com/CommandersAct/iOSV5
-name: leveldb, nameSpecified: leveldb, owner: firebase, version: 1.22.4, source: https://github.com/firebase/leveldb
+name: leveldb, nameSpecified: leveldb, owner: firebase, version: 1.22.5, source: https://github.com/firebase/leveldb
name: libextobjc, nameSpecified: libextobjc, owner: SRGSSR, version: 0.6.0-srg4, source: https://github.com/SRGSSR/libextobjc
@@ -155,9 +80,11 @@ name: Nuke, nameSpecified: Nuke, owner: kean, version: 10.11.2, source: https://
name: NukeUI, nameSpecified: NukeUI, owner: kean, version: 0.8.3, source: https://github.com/kean/NukeUI
+name: Pageboy, nameSpecified: Pageboy, owner: uias, version: 4.2.0, source: https://github.com/uias/Pageboy
+
name: paper-onboarding, nameSpecified: PaperOnboarding, owner: Ramotion, version: 6.1.5, source: https://github.com/Ramotion/paper-onboarding
-name: PLCrashReporter, nameSpecified: PLCrashReporter, owner: microsoft, version: 1.11.1, source: https://github.com/microsoft/PLCrashReporter
+name: PLCrashReporter, nameSpecified: PLCrashReporter, owner: microsoft, version: 1.11.2, source: https://github.com/microsoft/PLCrashReporter
name: promises, nameSpecified: Promises, owner: google, version: 2.4.0, source: https://github.com/google/promises
@@ -167,13 +94,13 @@ name: srgappearance-apple, nameSpecified: SRGAppearance, owner: SRGSSR, version:
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: 19.0.3, source: https://github.com/SRGSSR/srgdataprovider-apple
+name: srgdataprovider-apple, nameSpecified: SRGDataProvider, owner: SRGSSR, version: 19.0.4, 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
name: srgidentity-apple, nameSpecified: SRGIdentity, owner: SRGSSR, version: 3.3.0, source: https://github.com/SRGSSR/srgidentity-apple
-name: srgletterbox-apple, nameSpecified: SRGLetterbox, owner: SRGSSR, version: 9.3.0, source: https://github.com/SRGSSR/srgletterbox-apple
+name: srgletterbox-apple, nameSpecified: SRGLetterbox, owner: SRGSSR, version: 9.3.1, source: https://github.com/SRGSSR/srgletterbox-apple
name: srglogger-apple, nameSpecified: SRGLogger, owner: SRGSSR, version: 3.1.0, source: https://github.com/SRGSSR/srglogger-apple
@@ -183,7 +110,7 @@ name: srgnetwork-apple, nameSpecified: SRGNetwork, owner: SRGSSR, version: 3.1.0
name: srguserdata-apple, nameSpecified: SRGUserData, owner: SRGSSR, version: 3.3.1, source: https://github.com/SRGSSR/srguserdata-apple
-name: swift-collections, nameSpecified: swift-collections, owner: apple, version: 1.1.0, source: https://github.com/apple/swift-collections
+name: swift-collections, nameSpecified: swift-collections, owner: apple, version: 1.1.1, source: https://github.com/apple/swift-collections
name: swift-protobuf, nameSpecified: SwiftProtobuf, owner: apple, version: 1.26.0, source: https://github.com/apple/swift-protobuf
@@ -191,6 +118,8 @@ name: SwiftMessages, nameSpecified: SwiftMessages, owner: SwiftKickMobile, versi
name: SwiftUI-Introspect, nameSpecified: Introspect, owner: siteline, version: 0.12.0, source: https://github.com/siteline/SwiftUI-Introspect
+name: Tabman, nameSpecified: Tabman, owner: uias, version: 3.2.0, source: https://github.com/uias/Tabman
+
name: UICKeyChainStore, nameSpecified: UICKeyChainStore, owner: kishikawakatsumi, version: 2.2.1, source: https://github.com/kishikawakatsumi/UICKeyChainStore
name: YYWebImage, nameSpecified: YYWebImage, owner: SRGSSR, version: 1.0.5-srg3, source: https://github.com/SRGSSR/YYWebImage
diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist
index c3e5a1ec9..635e2e1aa 100755
--- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist
+++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist.plist
@@ -14,7 +14,7 @@
File
com.mono0926.LicensePlist/abseil-cpp-binary
Title
- abseil (1.2024011601.0)
+ abseil (1.2024011601.1)
Type
PSChildPaneSpecifier
@@ -30,7 +30,7 @@
File
com.mono0926.LicensePlist/app-check
Title
- AppCheck (10.18.1)
+ AppCheck (10.19.1)
Type
PSChildPaneSpecifier
@@ -38,7 +38,7 @@
File
com.mono0926.LicensePlist/appcenter-sdk-apple
Title
- AppCenter (5.0.4)
+ AppCenter (5.0.5)
Type
PSChildPaneSpecifier
@@ -70,7 +70,7 @@
File
com.mono0926.LicensePlist/firebase-ios-sdk
Title
- Firebase (10.23.1)
+ Firebase (10.27.0)
Type
PSChildPaneSpecifier
@@ -102,7 +102,7 @@
File
com.mono0926.LicensePlist/GoogleAppMeasurement
Title
- GoogleAppMeasurement (10.23.1)
+ GoogleAppMeasurement (10.27.0)
Type
PSChildPaneSpecifier
@@ -126,7 +126,7 @@
File
com.mono0926.LicensePlist/GoogleUtilities
Title
- GoogleUtilities (7.13.1)
+ GoogleUtilities (7.13.3)
Type
PSChildPaneSpecifier
@@ -134,7 +134,7 @@
File
com.mono0926.LicensePlist/grpc-binary
Title
- gRPC (1.62.1)
+ gRPC (1.62.2)
Type
PSChildPaneSpecifier
@@ -142,7 +142,7 @@
File
com.mono0926.LicensePlist/gtm-session-fetcher
Title
- gtm-session-fetcher (3.3.2)
+ gtm-session-fetcher (3.4.1)
Type
PSChildPaneSpecifier
@@ -158,7 +158,7 @@
File
com.mono0926.LicensePlist/ios-library
Title
- Airship (16.12.6)
+ Airship (16.12.7)
Type
PSChildPaneSpecifier
@@ -166,7 +166,7 @@
File
com.mono0926.LicensePlist/iOSV5
Title
- TagCommander SDK V5 (5.4.6)
+ TagCommander SDK V5 (5.4.9)
Type
PSChildPaneSpecifier
@@ -174,7 +174,7 @@
File
com.mono0926.LicensePlist/leveldb
Title
- leveldb (1.22.4)
+ leveldb (1.22.5)
Type
PSChildPaneSpecifier
@@ -202,30 +202,6 @@
Type
PSChildPaneSpecifier
-
- File
- com.mono0926.LicensePlist/MaterialComponents
- Title
- MaterialComponents (118.2.0)
- Type
- PSChildPaneSpecifier
-
-
- File
- com.mono0926.LicensePlist/MDFInternationalization
- Title
- MDFInternationalization (3.0.0)
- Type
- PSChildPaneSpecifier
-
-
- File
- com.mono0926.LicensePlist/MDFTextAccessibility
- Title
- MDFTextAccessibility (2.0.1)
- Type
- PSChildPaneSpecifier
-
File
com.mono0926.LicensePlist/nanopb
@@ -250,6 +226,14 @@
Type
PSChildPaneSpecifier
+
+ File
+ com.mono0926.LicensePlist/Pageboy
+ Title
+ Pageboy (4.2.0)
+ Type
+ PSChildPaneSpecifier
+
File
com.mono0926.LicensePlist/paper-onboarding
@@ -262,7 +246,7 @@
File
com.mono0926.LicensePlist/PLCrashReporter
Title
- PLCrashReporter (1.11.1)
+ PLCrashReporter (1.11.2)
Type
PSChildPaneSpecifier
@@ -302,7 +286,7 @@
File
com.mono0926.LicensePlist/srgdataprovider-apple
Title
- SRGDataProvider (19.0.3)
+ SRGDataProvider (19.0.4)
Type
PSChildPaneSpecifier
@@ -326,7 +310,7 @@
File
com.mono0926.LicensePlist/srgletterbox-apple
Title
- SRGLetterbox (9.3.0)
+ SRGLetterbox (9.3.1)
Type
PSChildPaneSpecifier
@@ -366,7 +350,7 @@
File
com.mono0926.LicensePlist/swift-collections
Title
- swift-collections (1.1.0)
+ swift-collections (1.1.1)
Type
PSChildPaneSpecifier
@@ -394,6 +378,14 @@
Type
PSChildPaneSpecifier
+
+ File
+ com.mono0926.LicensePlist/Tabman
+ Title
+ Tabman (3.2.0)
+ Type
+ PSChildPaneSpecifier
+
File
com.mono0926.LicensePlist/UICKeyChainStore
diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MDFInternationalization.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MDFInternationalization.plist
deleted file mode 100644
index b8fcbc147..000000000
--- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MDFInternationalization.plist
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
- PreferenceSpecifiers
-
-
- FooterText
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- License
- unknown
- Type
- PSGroupSpecifier
-
-
-
-
diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MDFTextAccessibility.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MDFTextAccessibility.plist
deleted file mode 100644
index b8fcbc147..000000000
--- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MDFTextAccessibility.plist
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
- PreferenceSpecifiers
-
-
- FooterText
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- License
- unknown
- Type
- PSGroupSpecifier
-
-
-
-
diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MaterialComponents.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MaterialComponents.plist
deleted file mode 100644
index 9a8234436..000000000
--- a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/MaterialComponents.plist
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
- PreferenceSpecifiers
-
-
- FooterText
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- License
- Apache-2.0
- Type
- PSGroupSpecifier
-
-
-
-
diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/Pageboy.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/Pageboy.plist
new file mode 100644
index 000000000..0dcc1c200
--- /dev/null
+++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/Pageboy.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ PreferenceSpecifiers
+
+
+ FooterText
+ MIT License
+
+Copyright (c) 2022 UI At Six
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+ License
+ MIT
+ Type
+ PSGroupSpecifier
+
+
+
+
diff --git a/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/Tabman.plist b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/Tabman.plist
new file mode 100644
index 000000000..0dcc1c200
--- /dev/null
+++ b/Application/Resources/Settings.bundle/com.mono0926.LicensePlist/Tabman.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ PreferenceSpecifiers
+
+
+ FooterText
+ MIT License
+
+Copyright (c) 2022 UI At Six
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+ License
+ MIT
+ Type
+ PSGroupSpecifier
+
+
+
+
diff --git a/Application/Sources/Application/AppDelegate.m b/Application/Sources/Application/AppDelegate.m
index 833466a63..5285a7ccb 100755
--- a/Application/Sources/Application/AppDelegate.m
+++ b/Application/Sources/Application/AppDelegate.m
@@ -339,7 +339,7 @@ - (void)playbackDidContinueAutomatically:(NSNotification *)notification
SRGMedia *media = notification.userInfo[SRGLetterboxMediaKey];
if (media) {
[[AnalyticsEventObjC continuousPlaybackWithAction:AnalyticsContiniousPlaybackActionPlayAutomatic
- mediaUrn:media.URN]
+ mediaUrn:media.URN]
send];
}
}
diff --git a/Application/Sources/Application/Navigation.swift b/Application/Sources/Application/Navigation.swift
index 9d6331377..0942f608b 100644
--- a/Application/Sources/Application/Navigation.swift
+++ b/Application/Sources/Application/Navigation.swift
@@ -10,313 +10,307 @@ import SRGAppearanceSwift
import SRGDataProviderModel
import SwiftUI
#if os(tvOS)
-import TvOSTextViewer
+ import TvOSTextViewer
#endif
import UIKit
private var cancellable: AnyCancellable?
#if os(tvOS)
-private var isPresenting = false
-private var cancellables = Set()
-
-extension UIViewController {
- func navigateToMedia(_ media: SRGMedia, play: Bool = false, mediaAnalyticsClickEvent: AnalyticsClickEvent? = nil, playAnalyticsClickEvent: AnalyticsClickEvent? = nil, from program: SRGProgram? = nil, animated: Bool = true, completion: (() -> Void)? = nil) {
- if !play && media.contentType != .livestream {
- mediaAnalyticsClickEvent?.send()
-
- let hostController = UIHostingController(rootView: MediaDetailView(media: media, playAnalyticsClickEvent: playAnalyticsClickEvent))
- present(hostController, animated: animated, completion: completion)
- }
- else {
- playAnalyticsClickEvent?.send()
-
- let letterboxViewController = SRGLetterboxViewController()
- letterboxViewController.delegate = LetterboxDelegate.shared
-
- let controller = letterboxViewController.controller
- let playlist = PlaylistForURN(media.urn)
- controller.playlistDataSource = playlist
- controller.playbackTransitionDelegate = playlist
- ApplicationConfigurationApplyControllerSettings(controller)
-
- controller.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: Int32(NSEC_PER_SEC)), queue: nil) { _ in
- HistoryUpdateLetterboxPlaybackProgress(controller)
- }
-
- controller.publisher(for: \.continuousPlaybackUpcomingMedia)
- .sink { upcomingMedia in
- guard let upcomingMedia else { return }
-
- AnalyticsEvent.continuousPlayback(action: .display,
- mediaUrn: upcomingMedia.urn)
- .send()
+ private var isPresenting = false
+ private var cancellables = Set()
+
+ extension UIViewController {
+ func navigateToMedia(_ media: SRGMedia, play: Bool = false, mediaAnalyticsClickEvent: AnalyticsClickEvent? = nil, playAnalyticsClickEvent: AnalyticsClickEvent? = nil, from _: SRGProgram? = nil, animated: Bool = true, completion: (() -> Void)? = nil) {
+ if !play, media.contentType != .livestream {
+ mediaAnalyticsClickEvent?.send()
+
+ let hostController = UIHostingController(rootView: MediaDetailView(media: media, playAnalyticsClickEvent: playAnalyticsClickEvent))
+ present(hostController, animated: animated, completion: completion)
+ } else {
+ playAnalyticsClickEvent?.send()
+
+ let letterboxViewController = SRGLetterboxViewController()
+ letterboxViewController.delegate = LetterboxDelegate.shared
+
+ let controller = letterboxViewController.controller
+ let playlist = PlaylistForURN(media.urn)
+ controller.playlistDataSource = playlist
+ controller.playbackTransitionDelegate = playlist
+ ApplicationConfigurationApplyControllerSettings(controller)
+
+ controller.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: Int32(NSEC_PER_SEC)), queue: nil) { _ in
+ HistoryUpdateLetterboxPlaybackProgress(controller)
}
- .store(in: &cancellables)
-
- let position = HistoryResumePlaybackPositionForMedia(media)
- controller.playMedia(media, at: position, withPreferredSettings: nil)
-
- present(letterboxViewController, animated: animated) {
- SRGAnalyticsTracker.shared.trackPageView(withTitle: AnalyticsPageTitle.player.rawValue, type: AnalyticsPageType.detail.rawValue, levels: [AnalyticsPageLevel.play.rawValue])
- if let completion {
- completion()
+
+ controller.publisher(for: \.continuousPlaybackUpcomingMedia)
+ .sink { upcomingMedia in
+ guard let upcomingMedia else { return }
+
+ AnalyticsEvent.continuousPlayback(action: .display,
+ mediaUrn: upcomingMedia.urn)
+ .send()
+ }
+ .store(in: &cancellables)
+
+ let position = HistoryResumePlaybackPositionForMedia(media)
+ controller.playMedia(media, at: position, withPreferredSettings: nil)
+
+ present(letterboxViewController, animated: animated) {
+ SRGAnalyticsTracker.shared.trackPageView(withTitle: AnalyticsPageTitle.player.rawValue, type: AnalyticsPageType.detail.rawValue, levels: [AnalyticsPageLevel.play.rawValue])
+ if let completion {
+ completion()
+ }
}
}
}
- }
-
- func navigateToShow(_ show: SRGShow, animated: Bool = true, completion: (() -> Void)? = nil) {
- let pageViewController = PageViewController(id: .show(show))
- present(pageViewController, animated: animated, completion: completion)
- }
-
- func navigateToTopic(_ topic: SRGTopic, animated: Bool = true, completion: (() -> Void)? = nil) {
- let pageViewController = PageViewController(id: .topic(topic))
- present(pageViewController, animated: animated, completion: completion)
- }
-
- func navigateToProgram(_ program: SRGProgram, in channel: SRGChannel, animated: Bool = true, completion: (() -> Void)? = nil) {
- cancellable = mediaPublisher(for: program, in: channel)?
- .receive(on: DispatchQueue.main)
- .sink { _ in
- // No error banners displayed on tvOS yet
- } receiveValue: { [weak self] media in
- let playAnalyticsClickEvent = media.contentType == .livestream ?
- AnalyticsClickEvent.tvGuidePlayLivestream(program: program, channel: channel, source: .grid) :
- AnalyticsClickEvent.tvGuidePlayMedia(media: media, programIsLive: (program.startDate...program.endDate).contains(Date()), channel: channel)
- let mediaAnalyticsClickEvent = AnalyticsClickEvent.tvGuideOpenInfoBox(program: program, programGuideLayout: .grid)
-
- self?.navigateToMedia(media, mediaAnalyticsClickEvent: mediaAnalyticsClickEvent, playAnalyticsClickEvent: playAnalyticsClickEvent, from: program, animated: animated, completion: completion)
- }
- }
-
- func navigateToPage(_ page: SRGContentPage, animated: Bool = true, completion: (() -> Void)? = nil) {
- let pageViewController = PageViewController(id: .page(page))
- present(pageViewController, animated: animated, completion: completion)
- }
-
- func navigateToSection(_ section: Content.Section, filter: SectionFiltering?, animated: Bool = true, completion: (() -> Void)? = nil) {
- let sectionViewController = SectionViewController(section: section, filter: filter)
- present(sectionViewController, animated: animated, completion: completion)
- }
-
- func navigateToApplicationSection(_ applicationSection: ApplicationSection, animated: Bool = true, completion: (() -> Void)? = nil) {
- switch applicationSection {
- case .history:
- present(SectionViewController.historyViewController(), animated: animated, completion: completion)
- case .favorites:
- present(SectionViewController.favoriteShowsViewController(), animated: animated, completion: completion)
- case .watchLater:
- present(SectionViewController.watchLaterViewController(), animated: animated, completion: completion)
- default:
- break
+
+ func navigateToShow(_ show: SRGShow, animated: Bool = true, completion: (() -> Void)? = nil) {
+ let pageViewController = PageViewController(id: .show(show))
+ present(pageViewController, animated: animated, completion: completion)
}
- }
-
- func navigateToText(_ text: String, animated: Bool = true, completion: (() -> Void)? = nil) {
- let textViewController = TvOSTextViewerViewController()
- textViewController.text = text
- textViewController.textAttributes = [
- .foregroundColor: UIColor.white,
- .font: SRGFont.font(.body) as UIFont
- ]
- textViewController.textEdgeInsets = UIEdgeInsets(top: 100, left: 250, bottom: 100, right: 250)
- textViewController.modalPresentationStyle = .overFullScreen
- present(textViewController, animated: animated, completion: completion)
- }
-
- private func mediaPublisher(for program: SRGProgram, in channel: SRGChannel) -> AnyPublisher? {
- if program.play_containsDate(Date()) {
- return SRGDataProvider.current!.tvLivestreams(for: channel.vendor)
- .compactMap { $0.first(where: { $0.channel == channel }) }
- .eraseToAnyPublisher()
+
+ func navigateToTopic(_ topic: SRGTopic, animated: Bool = true, completion: (() -> Void)? = nil) {
+ let pageViewController = PageViewController(id: .topic(topic))
+ present(pageViewController, animated: animated, completion: completion)
+ }
+
+ func navigateToProgram(_ program: SRGProgram, in channel: SRGChannel, animated: Bool = true, completion: (() -> Void)? = nil) {
+ cancellable = mediaPublisher(for: program, in: channel)?
+ .receive(on: DispatchQueue.main)
+ .sink { _ in
+ // No error banners displayed on tvOS yet
+ } receiveValue: { [weak self] media in
+ let playAnalyticsClickEvent = media.contentType == .livestream ?
+ AnalyticsClickEvent.tvGuidePlayLivestream(program: program, channel: channel, source: .grid) :
+ AnalyticsClickEvent.tvGuidePlayMedia(media: media, programIsLive: (program.startDate ... program.endDate).contains(Date()), channel: channel)
+ let mediaAnalyticsClickEvent = AnalyticsClickEvent.tvGuideOpenInfoBox(program: program, programGuideLayout: .grid)
+
+ self?.navigateToMedia(media, mediaAnalyticsClickEvent: mediaAnalyticsClickEvent, playAnalyticsClickEvent: playAnalyticsClickEvent, from: program, animated: animated, completion: completion)
+ }
}
- else if let mediaUrn = program.mediaURN {
- return SRGDataProvider.current!.media(withUrn: mediaUrn)
+
+ func navigateToPage(_ page: SRGContentPage, animated: Bool = true, completion: (() -> Void)? = nil) {
+ let pageViewController = PageViewController(id: .page(page))
+ present(pageViewController, animated: animated, completion: completion)
}
- else {
- return nil
+
+ func navigateToSection(_ section: Content.Section, filter: SectionFiltering?, animated: Bool = true, completion: (() -> Void)? = nil) {
+ let sectionViewController = SectionViewController(section: section, filter: filter)
+ present(sectionViewController, animated: animated, completion: completion)
+ }
+
+ func navigateToApplicationSection(_ applicationSection: ApplicationSection, animated: Bool = true, completion: (() -> Void)? = nil) {
+ switch applicationSection {
+ case .history:
+ present(SectionViewController.historyViewController(), animated: animated, completion: completion)
+ case .favorites:
+ present(SectionViewController.favoriteShowsViewController(), animated: animated, completion: completion)
+ case .watchLater:
+ present(SectionViewController.watchLaterViewController(), animated: animated, completion: completion)
+ default:
+ break
+ }
+ }
+
+ func navigateToText(_ text: String, animated: Bool = true, completion: (() -> Void)? = nil) {
+ let textViewController = TvOSTextViewerViewController()
+ textViewController.text = text
+ textViewController.textAttributes = [
+ .foregroundColor: UIColor.white,
+ .font: SRGFont.font(.body) as UIFont
+ ]
+ textViewController.textEdgeInsets = UIEdgeInsets(top: 100, left: 250, bottom: 100, right: 250)
+ textViewController.modalPresentationStyle = .overFullScreen
+ present(textViewController, animated: animated, completion: completion)
+ }
+
+ private func mediaPublisher(for program: SRGProgram, in channel: SRGChannel) -> AnyPublisher? {
+ if program.play_containsDate(Date()) {
+ SRGDataProvider.current!.tvLivestreams(for: channel.vendor)
+ .compactMap { $0.first(where: { $0.channel == channel }) }
+ .eraseToAnyPublisher()
+ } else if let mediaUrn = program.mediaURN {
+ SRGDataProvider.current!.media(withUrn: mediaUrn)
+ } else {
+ nil
+ }
}
}
-}
-func navigateToMedia(_ media: SRGMedia, play: Bool = false, mediaAnalyticsClickEvent: AnalyticsClickEvent? = nil, playAnalyticsClickEvent: AnalyticsClickEvent? = nil, animated: Bool = true) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToMedia(media, play: play, mediaAnalyticsClickEvent: mediaAnalyticsClickEvent, playAnalyticsClickEvent: playAnalyticsClickEvent, animated: animated) {
- isPresenting = false
+ func navigateToMedia(_ media: SRGMedia, play: Bool = false, mediaAnalyticsClickEvent: AnalyticsClickEvent? = nil, playAnalyticsClickEvent: AnalyticsClickEvent? = nil, animated: Bool = true) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToMedia(media, play: play, mediaAnalyticsClickEvent: mediaAnalyticsClickEvent, playAnalyticsClickEvent: playAnalyticsClickEvent, animated: animated) {
+ isPresenting = false
+ }
}
-}
-func navigateToShow(_ show: SRGShow, animated: Bool = true) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToShow(show, animated: animated) {
- isPresenting = false
+ func navigateToShow(_ show: SRGShow, animated: Bool = true) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToShow(show, animated: animated) {
+ isPresenting = false
+ }
}
-}
-func navigateToPage(_ page: SRGContentPage, animated: Bool = true) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToPage(page, animated: animated) {
- isPresenting = false
+ func navigateToPage(_ page: SRGContentPage, animated: Bool = true) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToPage(page, animated: animated) {
+ isPresenting = false
+ }
}
-}
-func navigateToSection(_ section: Content.Section, filter: SectionFiltering?, animated: Bool = true) {
- if let microPageId = section.properties.openContentPageId {
- openContentPage(id: microPageId, animated: animated)
- } else {
- openSectionPage(section: section, filter: filter, animated: animated)
+ func navigateToSection(_ section: Content.Section, filter: SectionFiltering?, animated: Bool = true) {
+ if let microPageId = section.properties.openContentPageId {
+ openContentPage(id: microPageId, animated: animated)
+ } else {
+ openSectionPage(section: section, filter: filter, animated: animated)
+ }
}
-}
-private func openSectionPage(section: Content.Section, filter: SectionFiltering?, animated: Bool) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToSection(section, filter: filter, animated: animated) {
- isPresenting = false
+ private func openSectionPage(section: Content.Section, filter: SectionFiltering?, animated: Bool) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToSection(section, filter: filter, animated: animated) {
+ isPresenting = false
+ }
}
-}
-private func openContentPage(id: String, animated: Bool) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
+ private func openContentPage(id: String, animated: Bool) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
- SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, uid: id)
- .receive(on: DispatchQueue.main)
- .sink { _ in
- // No error banners displayed on tvOS yet
- isPresenting = false
- } receiveValue: { contentPage in
- topViewController.navigateToPage(contentPage, animated: animated) {
+ SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, uid: id)
+ .receive(on: DispatchQueue.main)
+ .sink { _ in
+ // No error banners displayed on tvOS yet
isPresenting = false
+ } receiveValue: { contentPage in
+ topViewController.navigateToPage(contentPage, animated: animated) {
+ isPresenting = false
+ }
}
+ .store(in: &cancellables)
+ }
+
+ func navigateToTopic(_ topic: SRGTopic, animated: Bool = true) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToTopic(topic, animated: animated) {
+ isPresenting = false
}
- .store(in: &cancellables)
-}
-
-func navigateToTopic(_ topic: SRGTopic, animated: Bool = true) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToTopic(topic, animated: animated) {
- isPresenting = false
}
-}
-func navigateToApplicationSection(_ applicationSection: ApplicationSection, animated: Bool = true) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToApplicationSection(applicationSection, animated: animated) {
- isPresenting = false
+ func navigateToApplicationSection(_ applicationSection: ApplicationSection, animated: Bool = true) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToApplicationSection(applicationSection, animated: animated) {
+ isPresenting = false
+ }
}
-}
-func navigateToText(_ text: String, animated: Bool = true) {
- guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
- isPresenting = true
- topViewController.navigateToText(text, animated: animated) {
- isPresenting = false
+ func navigateToText(_ text: String, animated: Bool = true) {
+ guard !isPresenting, let topViewController = UIApplication.shared.mainTopViewController else { return }
+ isPresenting = true
+ topViewController.navigateToText(text, animated: animated) {
+ isPresenting = false
+ }
}
-}
#endif
#if os(iOS)
-extension UIViewController {
- @objc func navigateToNotification(_ notification: UserNotification, animated: Bool = true) {
- UserNotification.saveNotification(notification, read: true)
-
- if let mediaUrn = notification.mediaURN {
- UserConsentHelper.waitCollectingConsentRetain()
- cancellable = SRGDataProvider.current!.media(withUrn: mediaUrn)
- .receive(on: DispatchQueue.main)
- .sink { result in
- if case let .failure(error) = result {
- Banner.showError(error as NSError)
- UserConsentHelper.waitCollectingConsentRelease()
+ extension UIViewController {
+ @objc func navigateToNotification(_ notification: UserNotification, animated: Bool = true) {
+ UserNotification.saveNotification(notification, read: true)
+
+ if let mediaUrn = notification.mediaURN {
+ UserConsentHelper.waitCollectingConsentRetain()
+ cancellable = SRGDataProvider.current!.media(withUrn: mediaUrn)
+ .receive(on: DispatchQueue.main)
+ .sink { result in
+ if case let .failure(error) = result {
+ Banner.showError(error as NSError)
+ UserConsentHelper.waitCollectingConsentRelease()
+ }
+ } receiveValue: { [weak self] media in
+ guard let self else { return }
+ play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated) { _ in
+ AnalyticsEvent.notification(action: .playMedia,
+ from: .application,
+ uid: mediaUrn,
+ overrideSource: notification.showURN,
+ overrideType: UserNotificationTypeString(notification.type))
+ .send()
+ UserConsentHelper.waitCollectingConsentRelease()
+ }
}
- } receiveValue: { [weak self] media in
- guard let self else { return }
- self.play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated) { _ in
- AnalyticsEvent.notification(action: .playMedia,
+ } else if let showUrn = notification.showURN {
+ UserConsentHelper.waitCollectingConsentRetain()
+ cancellable = SRGDataProvider.current!.show(withUrn: showUrn)
+ .receive(on: DispatchQueue.main)
+ .sink { result in
+ if case let .failure(error) = result {
+ Banner.showError(error as NSError)
+ UserConsentHelper.waitCollectingConsentRelease()
+ }
+ } receiveValue: { [weak self] show in
+ guard let navigationController = self?.navigationController else { return }
+ let pageViewController = PageViewController(id: .show(show))
+ navigationController.pushViewController(pageViewController, animated: animated)
+
+ AnalyticsEvent.notification(action: .displayShow,
from: .application,
- uid: mediaUrn,
- overrideSource: notification.showURN,
+ uid: showUrn,
overrideType: UserNotificationTypeString(notification.type))
- .send()
+ .send()
UserConsentHelper.waitCollectingConsentRelease()
}
- }
- }
- else if let showUrn = notification.showURN {
- UserConsentHelper.waitCollectingConsentRetain()
- cancellable = SRGDataProvider.current!.show(withUrn: showUrn)
- .receive(on: DispatchQueue.main)
- .sink { result in
- if case let .failure(error) = result {
- Banner.showError(error as NSError)
- UserConsentHelper.waitCollectingConsentRelease()
- }
- } receiveValue: { [weak self] show in
- guard let navigationController = self?.navigationController else { return }
- let pageViewController = PageViewController(id: .show(show))
- navigationController.pushViewController(pageViewController, animated: animated)
-
- AnalyticsEvent.notification(action: .displayShow,
- from: .application,
- uid: showUrn,
- overrideType: UserNotificationTypeString(notification.type))
+ } else {
+ AnalyticsEvent.notification(action: .alert,
+ from: .application,
+ uid: notification.body,
+ overrideType: UserNotificationTypeString(notification.type))
.send()
- UserConsentHelper.waitCollectingConsentRelease()
- }
- }
- else {
- AnalyticsEvent.notification(action: .alert,
- from: .application,
- uid: notification.body,
- overrideType: UserNotificationTypeString(notification.type))
- .send()
- }
- }
-
- func navigateToDownload(_ download: Download, animated: Bool = true) {
- if let media = download.media {
- play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated, completion: nil)
+ }
}
- else {
- let error = NSError(
- domain: PlayErrorDomain,
- code: PlayErrorCode.notFound.rawValue,
- userInfo: [
- NSLocalizedDescriptionKey: NSLocalizedString("Media not available yet", comment: "Message on top screen when trying to open a media in the download list and the media is not downloaded.")
- ]
- )
- Banner.showError(error)
+
+ func navigateToDownload(_ download: Download, animated: Bool = true) {
+ if let media = download.media {
+ play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated, completion: nil)
+ } else {
+ let error = NSError(
+ domain: PlayErrorDomain,
+ code: PlayErrorCode.notFound.rawValue,
+ userInfo: [
+ NSLocalizedDescriptionKey: NSLocalizedString("Media not available yet", comment: "Message on top screen when trying to open a media in the download list and the media is not downloaded.")
+ ]
+ )
+ Banner.showError(error)
+ }
}
- }
-
- func navigateToItem(_ item: Content.Item, animated: Bool = true) {
- switch item {
- case let .media(media):
- play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated, completion: nil)
- case let .show(show):
- guard let navigationController else { return }
- 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))
- navigationController.pushViewController(pageViewController, animated: animated)
- case let .download(download):
- navigateToDownload(download, animated: animated)
- case let .notification(notification):
- navigateToNotification(notification, animated: animated)
- default:
- break
+
+ func navigateToItem(_ item: Content.Item, animated: Bool = true) {
+ switch item {
+ case let .media(media):
+ play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: animated, completion: nil)
+ case let .show(show):
+ guard let navigationController else { return }
+ 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))
+ navigationController.pushViewController(pageViewController, animated: animated)
+ case let .download(download):
+ navigateToDownload(download, animated: animated)
+ case let .notification(notification):
+ navigateToNotification(notification, animated: animated)
+ default:
+ break
+ }
}
}
-}
#endif
diff --git a/Application/Sources/Application/SceneDelegate.m b/Application/Sources/Application/SceneDelegate.m
index d131940c9..e0590a6c5 100644
--- a/Application/Sources/Application/SceneDelegate.m
+++ b/Application/Sources/Application/SceneDelegate.m
@@ -61,6 +61,8 @@ - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session op
[defaults addObserver:self forKeyPath:PlaySRGSettingServiceIdentifier options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:s_kvoContext];
[defaults addObserver:self forKeyPath:PlaySRGSettingUserLocation options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:s_kvoContext];
[defaults addObserver:self forKeyPath:PlaySRGSettingPosterImages options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:s_kvoContext];
+ [defaults addObserver:self forKeyPath:PlaySRGSettingSquareImages options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:s_kvoContext];
+ [defaults addObserver:self forKeyPath:PlaySRGSettingAudioHomepageOption options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:s_kvoContext];
#endif
}
@@ -71,6 +73,8 @@ - (void)sceneDidDisconnect:(UIScene *)scene
[defaults removeObserver:self forKeyPath:PlaySRGSettingServiceIdentifier];
[defaults removeObserver:self forKeyPath:PlaySRGSettingUserLocation];
[defaults removeObserver:self forKeyPath:PlaySRGSettingPosterImages];
+ [defaults removeObserver:self forKeyPath:PlaySRGSettingSquareImages];
+ [defaults removeObserver:self forKeyPath:PlaySRGSettingAudioHomepageOption];
#endif
}
@@ -537,7 +541,8 @@ - (void)openSectionUid:(NSString *)sectionUid
[UserConsentHelper waitCollectingConsentRetain];
[[SRGDataProvider.currentDataProvider contentSectionForVendor:ApplicationConfiguration.sharedApplicationConfiguration.vendor uid:sectionUid published:YES withCompletionBlock:^(SRGContentSection * _Nullable contentSection, NSHTTPURLResponse * _Nullable HTTPResponse, NSError * _Nullable error) {
if (contentSection) {
- SectionViewController *sectionViewController = [SectionViewController viewControllerForContentSection:contentSection];
+ // FIXME: is section always videoOrTV content type?
+ SectionViewController *sectionViewController = [SectionViewController viewControllerForContentSection:contentSection contentType:ContentTypeVideoOrTV];
[self.rootTabBarController pushViewController:sectionViewController animated:YES];
}
else {
@@ -555,7 +560,7 @@ - (void)openSectionUid:(NSString *)sectionUid
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (s_kvoContext == context) {
- if ([keyPath isEqualToString:PlaySRGSettingServiceIdentifier] || [keyPath isEqualToString:PlaySRGSettingUserLocation] || [keyPath isEqualToString:PlaySRGSettingPosterImages]) {
+ if ([keyPath isEqualToString:PlaySRGSettingServiceIdentifier] || [keyPath isEqualToString:PlaySRGSettingUserLocation] || [keyPath isEqualToString:PlaySRGSettingPosterImages] || [keyPath isEqualToString:PlaySRGSettingSquareImages] || [keyPath isEqualToString:PlaySRGSettingAudioHomepageOption]) {
// Entirely reload the view controller hierarchy to ensure all configuration changes are reflected in the
// user interface. Scheduled for the next run loop to have the same code in the app delegate (updating the
// data provider) executed first.
diff --git a/Application/Sources/Bridges/PlaySRG-ObjectiveC.h b/Application/Sources/Bridges/PlaySRG-ObjectiveC.h
index da4a358cd..237744b38 100755
--- a/Application/Sources/Bridges/PlaySRG-ObjectiveC.h
+++ b/Application/Sources/Bridges/PlaySRG-ObjectiveC.h
@@ -20,7 +20,6 @@
#import "Favorites.h"
#import "History.h"
#import "Layout.h"
-#import "MaterialTabs.h"
#import "MediaPlayerViewController.h"
#import "MediaPlayerViewController+Private.h"
#import "MediaPreviewViewController.h"
diff --git a/Application/Sources/Bridges/SwiftMessagesBridge.swift b/Application/Sources/Bridges/SwiftMessagesBridge.swift
index a78a28a05..4ce086504 100755
--- a/Application/Sources/Bridges/SwiftMessagesBridge.swift
+++ b/Application/Sources/Bridges/SwiftMessagesBridge.swift
@@ -24,31 +24,30 @@ final class SwiftMessagesBridge: NSObject {
*/
@objc static func show(_ message: String, accessibilityPrefix: String?, image: UIImage?, backgroundColor: UIColor?, foregroundColor: UIColor?, sticky: Bool) {
SwiftMessages.hideAll()
-
+
let messageView = MessageView.viewFromNib(layout: .cardView)
messageView.button?.isHidden = true
messageView.bodyLabel?.font = SRGFont.font(.body)
messageView.configureDropShadow()
-
+
messageView.configureContent(title: nil, body: message, iconImage: nil, iconText: nil, buttonImage: nil, buttonTitle: nil, buttonTapHandler: nil)
messageView.configureTheme(backgroundColor: backgroundColor ?? UIColor.white, foregroundColor: foregroundColor ?? UIColor.black)
-
+
messageView.accessibilityPrefix = accessibilityPrefix
-
+
messageView.iconImageView?.image = image
messageView.iconImageView?.isHidden = (image == nil)
-
+
messageView.tapHandler = { _ in SwiftMessages.hide() }
-
+
var config = SwiftMessages.defaultConfig
if sticky {
config.duration = .forever
- }
- else {
+ } else {
config.duration = .seconds(seconds: 4)
}
config.presentationStyle = .bottom
-
+
// Set a presentation context (with a preference for navigation controllers). A context is required so that
// the notification rotation behavior matches the one of the currently visible view controller.
var presentationController = UIApplication.shared.mainTopViewController
@@ -58,16 +57,16 @@ final class SwiftMessagesBridge: NSObject {
}
presentationController = presentationController?.parent
}
-
+
if let presentationController {
config.presentationContext = .viewController(presentationController)
}
-
+
// Remark: VoiceOver is supported natively, but with the system language (not the one we might set on the
// UIApplication instance)
SwiftMessages.show(config: config, view: messageView)
}
-
+
/**
* Hide all notification messages.
*/
diff --git a/Application/Sources/Browser/WebViewController.m b/Application/Sources/Browser/WebViewController.m
index 22a0c6a79..64d32e164 100755
--- a/Application/Sources/Browser/WebViewController.m
+++ b/Application/Sources/Browser/WebViewController.m
@@ -67,7 +67,7 @@ - (void)loadView
{
UIView *view = [[UIView alloc] initWithFrame:UIScreen.mainScreen.bounds];
view.backgroundColor = UIColor.srg_gray16Color;
-
+
// WKWebView cannot be instantiated in storyboards, do it programmatically
WKWebView *webView = [[WKWebView alloc] init];
webView.opaque = NO;
@@ -77,7 +77,7 @@ - (void)loadView
webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
webView.scrollView.delegate = self;
[view insertSubview:webView atIndex:0];
-
+
webView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[webView.topAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.topAnchor],
@@ -86,56 +86,56 @@ - (void)loadView
[webView.trailingAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.trailingAnchor]
]];
self.webView = webView;
-
+
if (self.customizationBlock) {
self.customizationBlock(webView);
}
-
+
UIImageView *loadingImageView = [UIImageView play_largeLoadingImageViewWithTintColor:UIColor.srg_grayD2Color];
loadingImageView.hidden = YES;
[view insertSubview:loadingImageView atIndex:0];
self.loadingImageView = loadingImageView;
-
+
loadingImageView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[loadingImageView.centerXAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.centerXAnchor],
[loadingImageView.centerYAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.centerYAnchor]
]];
-
+
UILabel *errorLabel = [[UILabel alloc] init];
errorLabel.textColor = UIColor.whiteColor;
errorLabel.font = [SRGFont fontWithStyle:SRGFontStyleBody];
[view addSubview:errorLabel];
self.errorLabel = errorLabel;
-
+
errorLabel.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[errorLabel.leadingAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.leadingAnchor constant:40.f],
[errorLabel.trailingAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.trailingAnchor constant:-40.f],
[errorLabel.centerYAnchor constraintEqualToAnchor:view.centerYAnchor]
]];
-
+
UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar];
progressView.progressTintColor = UIColor.srg_redColor;
[view addSubview:progressView];
self.progressView = progressView;
-
+
progressView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[progressView.leadingAnchor constraintEqualToAnchor:view.leadingAnchor],
[progressView.trailingAnchor constraintEqualToAnchor:view.trailingAnchor],
self.progressTopConstraint = [progressView.topAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.topAnchor]
]];
-
+
self.view = view;
}
- (void)viewDidLoad
{
[super viewDidLoad];
-
+
[self.webView loadRequest:self.request];
-
+
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(webViewController_reachabilityDidChange:)
name:FXReachabilityStatusDidChangeNotification
@@ -213,7 +213,7 @@ - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation
{
self.loadingImageView.hidden = NO;
self.errorLabel.text = nil;
-
+
[UIView animateWithDuration:0.3 animations:^{
self.progressView.alpha = 1.f;
}];
@@ -223,7 +223,7 @@ - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigat
{
self.loadingImageView.hidden = YES;
self.errorLabel.text = nil;
-
+
[UIView animateWithDuration:0.3 animations:^{
self.webView.alpha = 1.f;
self.progressView.alpha = 0.f;
@@ -234,7 +234,7 @@ - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation
{
self.loadingImageView.hidden = YES;
NSError *updatedError = error;
-
+
NSURL *failingURL = ([error.domain isEqualToString:NSURLErrorDomain]) ? error.userInfo[NSURLErrorFailingURLErrorKey] : nil;
if (failingURL && ! [failingURL.scheme isEqualToString:@"http"] && ! [failingURL.scheme isEqualToString:@"https"] && ! [failingURL.scheme isEqualToString:@"file"]) {
updatedError = nil;
@@ -242,7 +242,7 @@ - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation
if ([updatedError.domain isEqualToString:NSURLErrorDomain]) {
self.errorLabel.text = [NSHTTPURLResponse srg_localizedStringForURLErrorCode:updatedError.code];
-
+
[UIView animateWithDuration:0.3 animations:^{
self.progressView.alpha = 0.f;
self.webView.alpha = 0.f;
@@ -250,9 +250,9 @@ - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation
}
else {
self.errorLabel.text = nil;
-
+
[webView goBack];
-
+
[UIView animateWithDuration:0.3 animations:^{
self.progressView.alpha = 0.f;
}];
diff --git a/Application/Sources/Calendar/CalendarViewController.h b/Application/Sources/Calendar/CalendarViewController.h
index 1c74e1951..837e541de 100755
--- a/Application/Sources/Calendar/CalendarViewController.h
+++ b/Application/Sources/Calendar/CalendarViewController.h
@@ -15,7 +15,7 @@
NS_ASSUME_NONNULL_BEGIN
@interface CalendarViewController : UIViewController
+Oriented, ScrollableContentContainer, SRGAnalyticsViewTracking, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIGestureRecognizerDelegate>
/**
* Instantiate for medias belonging to the specified radio channel. If no channel is provided, TV medias will be
diff --git a/Application/Sources/Calendar/CalendarViewController.m b/Application/Sources/Calendar/CalendarViewController.m
index 5285854ec..10075dc8b 100755
--- a/Application/Sources/Calendar/CalendarViewController.m
+++ b/Application/Sources/Calendar/CalendarViewController.m
@@ -314,7 +314,7 @@ - (void)calendarCurrentPageDidChange:(FSCalendar *)calendar
// Hidden if in the same page as today and current date is not today
BOOL hidden = [NSCalendar.srg_defaultCalendar compareDate:calendar.currentPage toDate:calendar.today toUnitGranularity:unitGranularity] == NSOrderedSame
- && [calendar.today isEqualToDate:dailyMediasViewController.date];
+ && [calendar.today isEqualToDate:dailyMediasViewController.date];
[self setNavigationBarItemsHidden:hidden];
}
diff --git a/Application/Sources/CarPlay/CarPlay+Extensions.swift b/Application/Sources/CarPlay/CarPlay+Extensions.swift
index 5e3528b80..8553fafb8 100644
--- a/Application/Sources/CarPlay/CarPlay+Extensions.swift
+++ b/Application/Sources/CarPlay/CarPlay+Extensions.swift
@@ -12,7 +12,7 @@ extension CPListTemplate {
template.controller = CarPlayTemplateListController(list: list, template: template, interfaceController: interfaceController)
return template
}
-
+
static var playbackRate: CPListTemplate {
let template = CPListTemplate(title: NSLocalizedString("Playback speed", comment: "Playback speed screen title"), sections: [])
template.controller = CarPlayPlaybackSpeedController(template: template)
@@ -26,19 +26,18 @@ extension CPInterfaceController {
if controller.play_mainMedia != media {
controller.playMedia(media, at: HistoryResumePlaybackPositionForMedia(media), withPreferredSettings: ApplicationSettingPlaybackSettings())
}
- }
- else {
+ } else {
let controller = SRGLetterboxController()
controller.playMedia(media, at: HistoryResumePlaybackPositionForMedia(media), withPreferredSettings: ApplicationSettingPlaybackSettings())
SRGLetterboxService.shared.enable(with: controller, pictureInPictureDelegate: nil)
}
-
+
if let controller = SRGLetterboxService.shared.controller {
let playlist = PlaylistForURN(media.urn)
controller.playlistDataSource = playlist
controller.playbackTransitionDelegate = playlist
}
-
+
let nowPlayingTemplate = CPNowPlayingTemplate.shared
pushTemplate(nowPlayingTemplate, animated: true) { _, _ in
completion()
diff --git a/Application/Sources/CarPlay/CarPlayList.swift b/Application/Sources/CarPlay/CarPlayList.swift
index 1a03e7346..611ef3c4c 100644
--- a/Application/Sources/CarPlay/CarPlayList.swift
+++ b/Application/Sources/CarPlay/CarPlayList.swift
@@ -5,8 +5,8 @@
//
import CarPlay
-import SRGDataProviderCombine
import Nuke
+import SRGDataProviderCombine
// MARK: Types
@@ -16,84 +16,84 @@ enum CarPlayList {
case mostPopular
case mostPopularMedias(radioChannel: RadioChannel)
case livePrograms(channel: SRGChannel, media: SRGMedia)
-
+
private static let pageSize: UInt = 20
-
+
var title: String? {
switch self {
case .latestEpisodesFromFavorites:
- return NSLocalizedString("Favorites", comment: "Tab title to present the latest episodes from favorite shows on CarPlay")
+ NSLocalizedString("Favorites", comment: "Tab title to present the latest episodes from favorite shows on CarPlay")
case .livestreams:
- return NSLocalizedString("Livestreams", comment: "Tab title to present the livestreams on CarPlay")
+ NSLocalizedString("Livestreams", comment: "Tab title to present the livestreams on CarPlay")
case .mostPopular:
- return NSLocalizedString("Trends", comment: "Tab title to present the most popular medias by channel on CarPlay")
+ NSLocalizedString("Trends", comment: "Tab title to present the most popular medias by channel on CarPlay")
case let .mostPopularMedias(radioChannel: radioChannel):
- return radioChannel.name
+ radioChannel.name
case .livePrograms:
- return NSLocalizedString("Previous shows", comment: "Livestream previous programs screen title")
+ NSLocalizedString("Previous shows", comment: "Livestream previous programs screen title")
}
}
-
+
var pageViewTitle: String? {
switch self {
case .latestEpisodesFromFavorites:
- return AnalyticsPageTitle.latestEpisodesFromFavorites.rawValue
+ AnalyticsPageTitle.latestEpisodesFromFavorites.rawValue
case .livestreams, .mostPopular:
- return AnalyticsPageTitle.home.rawValue
+ AnalyticsPageTitle.home.rawValue
case .mostPopularMedias:
- return AnalyticsPageTitle.mostPopular.rawValue
+ AnalyticsPageTitle.mostPopular.rawValue
case .livePrograms:
- return AnalyticsPageTitle.livePrograms.rawValue
+ AnalyticsPageTitle.livePrograms.rawValue
}
}
-
+
var pageViewType: String? {
switch self {
case .latestEpisodesFromFavorites, .livePrograms, .mostPopularMedias:
- return AnalyticsPageType.detail.rawValue
+ AnalyticsPageType.detail.rawValue
case .livestreams:
- return AnalyticsPageType.live.rawValue
+ AnalyticsPageType.live.rawValue
case .mostPopular:
- return AnalyticsPageType.overview.rawValue
+ AnalyticsPageType.overview.rawValue
}
}
-
+
var pageViewLevels: [String]? {
switch self {
case .latestEpisodesFromFavorites:
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue]
case .livestreams:
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, AnalyticsPageLevel.live.rawValue]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, AnalyticsPageLevel.live.rawValue]
case .mostPopular:
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, AnalyticsPageLevel.mostPopular.rawValue]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, AnalyticsPageLevel.mostPopular.rawValue]
case let .mostPopularMedias(radioChannel):
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, radioChannel.name]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, radioChannel.name]
case let .livePrograms(channel, _):
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, channel.title]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue, channel.title]
}
}
-
+
func publisher(with interfaceController: CPInterfaceController) -> AnyPublisher<[CPListSection], Error> {
switch self {
case .latestEpisodesFromFavorites:
- return Publishers.PublishAndRepeat(onOutputFrom: UserInteractionSignal.favoriteUpdates()) {
- return SRGDataProvider.current!.favoritesPublisher(filter: self)
+ Publishers.PublishAndRepeat(onOutputFrom: UserInteractionSignal.favoriteUpdates()) {
+ SRGDataProvider.current!.favoritesPublisher(filter: self)
.map { SRGDataProvider.current!.latestMediasForShowsPublisher(withUrns: $0.map(\.urn), pageSize: Self.pageSize) }
.switchToLatest()
}
.mapToSections(with: interfaceController)
case .livestreams:
- return Publishers.PublishAndRepeat(onOutputFrom: ApplicationSignal.settingUpdates(at: \.PlaySRGSettingSelectedLivestreamURNForChannels)) {
- return Self.livestreamsSections(for: .all, interfaceController: interfaceController)
+ Publishers.PublishAndRepeat(onOutputFrom: ApplicationSignal.settingUpdates(at: \.PlaySRGSettingSelectedLivestreamURNForChannels)) {
+ Self.livestreamsSections(for: .all, interfaceController: interfaceController)
}
case .mostPopular:
- return Self.mostPopular(interfaceController: interfaceController)
+ Self.mostPopular(interfaceController: interfaceController)
case let .mostPopularMedias(radioChannel: radioChannel):
- return SRGDataProvider.current!.radioMostPopularMedias(for: ApplicationConfiguration.shared.vendor, channelUid: radioChannel.uid, pageSize: Self.pageSize)
+ SRGDataProvider.current!.radioMostPopularMedias(for: ApplicationConfiguration.shared.vendor, channelUid: radioChannel.uid, pageSize: Self.pageSize)
.mapToSections(with: interfaceController)
case let .livePrograms(channel, media):
- return Publishers.PublishAndRepeat(onOutputFrom: Timer.publish(every: 30, on: .main, in: .common).autoconnect()) {
- return Self.liveProgramsSections(for: channel, media: media, interfaceController: interfaceController)
+ Publishers.PublishAndRepeat(onOutputFrom: Timer.publish(every: 30, on: .main, in: .common).autoconnect()) {
+ Self.liveProgramsSections(for: channel, media: media, interfaceController: interfaceController)
}
}
}
@@ -104,71 +104,70 @@ private extension CarPlayList {
let media: SRGMedia
let playing: Bool
}
-
+
struct LiveProgramData {
let program: SRGProgram
let image: UIImage
let playing: Bool
}
-
+
struct MediaData {
let media: SRGMedia
let image: UIImage
let playing: Bool
let progress: Double?
}
-
+
static func liveMediaDataPublisher(for media: SRGMedia) -> AnyPublisher {
- return playingPublisher(for: media.urn)
+ playingPublisher(for: media.urn)
.map { playing in
- return LiveMediaData(media: media, playing: playing)
+ LiveMediaData(media: media, playing: playing)
}
.eraseToAnyPublisher()
}
-
+
static func liveProgramDataPublisher(for program: SRGProgram) -> AnyPublisher {
- return Publishers.CombineLatest(
+ Publishers.CombineLatest(
imagePublisher(for: program),
playingPublisher(for: program.mediaURN)
)
.map { image, playing in
- return LiveProgramData(program: program, image: image, playing: playing)
+ LiveProgramData(program: program, image: image, playing: playing)
}
.eraseToAnyPublisher()
}
-
+
static func mediaDataPublisher(for media: SRGMedia) -> AnyPublisher {
- return Publishers.CombineLatest3(
+ Publishers.CombineLatest3(
imagePublisher(for: media),
playingPublisher(for: media.urn),
UserDataPublishers.playbackProgressPublisher(for: media)
)
.map { image, playing, progress in
- return MediaData(media: media, image: image, playing: playing, progress: progress)
+ MediaData(media: media, image: image, playing: playing, progress: progress)
}
.eraseToAnyPublisher()
}
-
+
private static func playingPublisher(for mediaUrn: String?) -> AnyPublisher {
if let mediaUrn {
- return nowPlayingMediaPublisher()
+ nowPlayingMediaPublisher()
.map { $0.map(\.urn).contains(mediaUrn) }
.eraseToAnyPublisher()
- }
- else {
- return Just(false)
+ } else {
+ Just(false)
.eraseToAnyPublisher()
}
}
-
+
private static func imagePublisher(for media: SRGMedia) -> AnyPublisher {
- return imagePublisher(for: media.image)
+ imagePublisher(for: media.image)
}
-
+
private static func imagePublisher(for program: SRGProgram) -> AnyPublisher {
- return imagePublisher(for: program.image)
+ imagePublisher(for: program.image)
}
-
+
private static func imagePublisher(for image: SRGImage?) -> AnyPublisher {
let imageSize = SRGImageSize.small
let placeholderImage = UIColor.placeholder.image(ofSize: SRGRecommendedImageCGSize(imageSize, .default))
@@ -178,16 +177,15 @@ private extension CarPlayList {
.replaceError(with: placeholderImage)
.prepend(placeholderImage)
.eraseToAnyPublisher()
- }
- else {
+ } else {
return Just(placeholderImage)
.eraseToAnyPublisher()
}
}
-
+
private static func nowPlayingMedia(for controller: SRGLetterboxController?) -> [SRGMedia] {
guard let controller else { return [] }
-
+
var medias: Set = []
if let mainMedia = controller.play_mainMedia {
medias.insert(mainMedia)
@@ -197,21 +195,20 @@ private extension CarPlayList {
}
return Array(medias)
}
-
+
private static func nowPlayingMediaPublisher() -> AnyPublisher<[SRGMedia], Never> {
- return SRGLetterboxService.shared.publisher(for: \.controller)
- .map { controller in
+ SRGLetterboxService.shared.publisher(for: \.controller)
+ .map { controller -> AnyPublisher<[SRGMedia], Never> in
if let controller {
- return NotificationCenter.default.weakPublisher(for: .SRGLetterboxMetadataDidChange, object: controller)
+ NotificationCenter.default.weakPublisher(for: .SRGLetterboxMetadataDidChange, object: controller)
.map { notification in
let controller = notification.object as? SRGLetterboxController
return nowPlayingMedia(for: controller)
}
.prepend(nowPlayingMedia(for: controller))
.eraseToAnyPublisher()
- }
- else {
- return Just([])
+ } else {
+ Just([])
.eraseToAnyPublisher()
}
}
@@ -219,25 +216,24 @@ private extension CarPlayList {
.removeDuplicates()
.eraseToAnyPublisher()
}
-
+
private static func liveProgramsPublisher(for channel: SRGChannel, media: SRGMedia) -> AnyPublisher<[SRGProgram], Error> {
if let controller = SRGLetterboxService.shared.controller,
let dateInterval = controller.play_dateInterval,
let segments = controller.mediaComposition?.mainChapter.segments, !segments.isEmpty {
- return SRGDataProvider.current!.radioLatestPrograms(for: ApplicationConfiguration.shared.vendor,
- channelUid: channel.uid,
- livestreamUid: media.uid,
- from: nil, to: nil,
- pageSize: 50, paginatedBy: nil)
- .map { _, programs in
- return programs
- .filter { $0.startDate >= dateInterval.start && $0.startDate <= dateInterval.end }
- .reversed()
- }
- .eraseToAnyPublisher()
- }
- else {
- return Just([])
+ SRGDataProvider.current!.radioLatestPrograms(for: ApplicationConfiguration.shared.vendor,
+ channelUid: channel.uid,
+ livestreamUid: media.uid,
+ from: nil, to: nil,
+ pageSize: 50, paginatedBy: nil)
+ .map { _, programs in
+ programs
+ .filter { $0.startDate >= dateInterval.start && $0.startDate <= dateInterval.end }
+ .reversed()
+ }
+ .eraseToAnyPublisher()
+ } else {
+ Just([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@@ -248,11 +244,11 @@ private extension CarPlayList {
extension CarPlayList: SectionFiltering {
func compatibleShows(_ shows: [SRGShow]) -> [SRGShow] {
- return shows.filter { $0.transmission == .radio }
+ shows.filter { $0.transmission == .radio }
}
-
+
func compatibleMedias(_ medias: [SRGMedia]) -> [SRGMedia] {
- return medias.filter { $0.mediaType == .audio }
+ medias.filter { $0.mediaType == .audio }
}
}
@@ -263,16 +259,16 @@ private extension CarPlayList {
guard let channel = media.channel, let radioChannel = ApplicationConfiguration.shared.radioChannel(forUid: channel.uid) else { return nil }
return logoImage(for: radioChannel)
}
-
+
private static func logoImage(for channel: RadioChannel) -> UIImage? {
- return RadioChannelLogoImageWithTraitCollection(channel, UITraitCollection(userInterfaceIdiom: .carPlay))
+ RadioChannelLogoImageWithTraitCollection(channel, UITraitCollection(userInterfaceIdiom: .carPlay))
}
-
+
static func livestreamsSections(for contentProviders: SRGContentProviders, interfaceController: CPInterfaceController) -> AnyPublisher<[CPListSection], Error> {
- return SRGDataProvider.current!.regionalizedRadioLivestreams(for: ApplicationConfiguration.shared.vendor, contentProviders: contentProviders)
+ SRGDataProvider.current!.regionalizedRadioLivestreams(for: ApplicationConfiguration.shared.vendor, contentProviders: contentProviders)
.map { medias in
- return Publishers.AccumulateLatestMany(medias.map { media in
- return liveMediaDataPublisher(for: media)
+ Publishers.AccumulateLatestMany(medias.map { media in
+ liveMediaDataPublisher(for: media)
})
}
.switchToLatest()
@@ -291,14 +287,13 @@ private extension CarPlayList {
}
.eraseToAnyPublisher()
}
-
+
static func mostPopular(interfaceController: CPInterfaceController) -> AnyPublisher<[CPListSection], Error> {
let radioChannels = ApplicationConfiguration.shared.radioHomepageChannels
if radioChannels.count == 1, let radioChannel = radioChannels.first {
return SRGDataProvider.current!.radioMostPopularMedias(for: ApplicationConfiguration.shared.vendor, channelUid: radioChannel.uid)
.mapToSections(with: interfaceController)
- }
- else {
+ } else {
return radioChannels.publisher
.map { radioChannel in
let item = CPListItem(text: radioChannel.name, detailText: nil, image: logoImage(for: radioChannel))
@@ -317,12 +312,12 @@ private extension CarPlayList {
.eraseToAnyPublisher()
}
}
-
- static func liveProgramsSections(for channel: SRGChannel, media: SRGMedia, interfaceController: CPInterfaceController) -> AnyPublisher<[CPListSection], Error> {
- return liveProgramsPublisher(for: channel, media: media)
+
+ static func liveProgramsSections(for channel: SRGChannel, media: SRGMedia, interfaceController _: CPInterfaceController) -> AnyPublisher<[CPListSection], Error> {
+ liveProgramsPublisher(for: channel, media: media)
.map { programs in
- return Publishers.AccumulateLatestMany(programs.map { program in
- return liveProgramDataPublisher(for: program)
+ Publishers.AccumulateLatestMany(programs.map { program in
+ liveProgramDataPublisher(for: program)
})
}
.switchToLatest()
@@ -337,8 +332,7 @@ private extension CarPlayList {
SRGLetterboxService.shared.controller?.switch(toURN: mediaUrn, withCompletionHandler: { _ in
completion()
})
- }
- else {
+ } else {
completion()
}
}
@@ -354,9 +348,9 @@ private extension CarPlayList {
private extension Publisher where Output == [SRGMedia] {
func mapToSections(with interfaceController: CPInterfaceController) -> AnyPublisher<[CPListSection], Failure> {
- return map { medias in
- return Publishers.AccumulateLatestMany(medias.map { media in
- return CarPlayList.mediaDataPublisher(for: media)
+ map { medias in
+ Publishers.AccumulateLatestMany(medias.map { media in
+ CarPlayList.mediaDataPublisher(for: media)
})
}
.switchToLatest()
diff --git a/Application/Sources/CarPlay/CarPlayNowPlayingController.swift b/Application/Sources/CarPlay/CarPlayNowPlayingController.swift
index f618f34e8..d8edbfb85 100644
--- a/Application/Sources/CarPlay/CarPlayNowPlayingController.swift
+++ b/Application/Sources/CarPlay/CarPlayNowPlayingController.swift
@@ -14,10 +14,10 @@ final class CarPlayNowPlayingController: NSObject {
private weak var interfaceController: CPInterfaceController?
private var popToRootCancellable: AnyCancellable
private var nowPlayingPropertiesCancellable: AnyCancellable?
-
+
init(interfaceController: CPInterfaceController) {
self.interfaceController = interfaceController
-
+
// If the player is closed on the iOS device return to the first level. A better result would inspect the
// template hierarchy to pop to the previous one but this might perform an IPC call. Popping to the root
// should be sufficient.
@@ -26,7 +26,7 @@ final class CarPlayNowPlayingController: NSObject {
.sink { [weak interfaceController] _ in
interfaceController?.popToRootTemplate(animated: true) { _, _ in }
}
-
+
CPNowPlayingTemplate.shared.upNextTitle = NSLocalizedString("Previous shows", comment: "Button title on CarPlay player for livestream previous programs")
}
}
@@ -35,33 +35,33 @@ private extension CarPlayNowPlayingController {
private struct NowPlayingProperties: Equatable {
let nowPlayingButtons: [CPNowPlayingButton]
let upNextButtonEnabled: Bool
-
+
init(for controller: SRGLetterboxController?, interfaceController: CPInterfaceController) {
nowPlayingButtons = Self.nowPlayingButtons(for: controller, interfaceController: interfaceController)
upNextButtonEnabled = Self.upNextButtonEnabled(for: controller)
}
-
+
private static func playbackRateButton(for interfaceController: CPInterfaceController) -> CPNowPlayingButton {
- return CPNowPlayingImageButton(image: UIImage(named: "playback_speed", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in
+ CPNowPlayingImageButton(image: UIImage(named: "playback_speed", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in
interfaceController.pushTemplate(CPListTemplate.playbackRate, animated: true) { _, _ in }
}
}
-
+
private static func startOverButton() -> CPNowPlayingButton {
- return CPNowPlayingImageButton(image: UIImage(named: "start_over", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in
+ CPNowPlayingImageButton(image: UIImage(named: "start_over", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in
SRGLetterboxService.shared.controller?.startOver()
}
}
-
+
private static func skipToLiveButton() -> CPNowPlayingButton {
- return CPNowPlayingImageButton(image: UIImage(named: "skip_to_live", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in
+ CPNowPlayingImageButton(image: UIImage(named: "skip_to_live", in: nil, compatibleWith: UITraitCollection(userInterfaceIdiom: .carPlay))!) { _ in
SRGLetterboxService.shared.controller?.skipToLive()
}
}
-
+
private static func nowPlayingButtons(for controller: SRGLetterboxController?, interfaceController: CPInterfaceController) -> [CPNowPlayingButton] {
- guard let controller = controller else { return [] }
-
+ guard let controller else { return [] }
+
var nowPlayingButtons = [playbackRateButton(for: interfaceController)]
if controller.canStartOver() {
nowPlayingButtons.insert(startOverButton(), at: 0)
@@ -71,36 +71,34 @@ private extension CarPlayNowPlayingController {
}
return nowPlayingButtons
}
-
+
private static func upNextButtonEnabled(for controller: SRGLetterboxController?) -> Bool {
if let mainChapter = controller?.mediaComposition?.mainChapter, mainChapter.contentType == .livestream,
let segments = mainChapter.segments {
- return !segments.isEmpty
- }
- else {
- return false
+ !segments.isEmpty
+ } else {
+ false
}
}
}
private static func nowPlayingPropertiesPublisher(interfaceController: CPInterfaceController) -> AnyPublisher {
- return SRGLetterboxService.shared.publisher(for: \.controller)
+ SRGLetterboxService.shared.publisher(for: \.controller)
.map { controller in
if let controller {
- return Publishers.CombineLatest3(
+ Publishers.CombineLatest3(
controller.mediaPlayerController.publisher(for: \.timeRange),
NotificationCenter.default.weakPublisher(for: .SRGLetterboxPlaybackStateDidChange, object: controller),
NotificationCenter.default.weakPublisher(for: .SRGLetterboxMetadataDidChange, object: controller)
)
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
.map { _ in
- return NowPlayingProperties(for: controller, interfaceController: interfaceController)
+ NowPlayingProperties(for: controller, interfaceController: interfaceController)
}
.prepend(NowPlayingProperties(for: controller, interfaceController: interfaceController))
.eraseToAnyPublisher()
- }
- else {
- return Just(NowPlayingProperties(for: controller, interfaceController: interfaceController))
+ } else {
+ Just(NowPlayingProperties(for: controller, interfaceController: interfaceController))
.eraseToAnyPublisher()
}
}
@@ -113,7 +111,7 @@ private extension CarPlayNowPlayingController {
// MARK: Protocols
extension CarPlayNowPlayingController: CarPlayTemplateController {
- func willAppear(animated: Bool) {
+ func willAppear(animated _: Bool) {
CPNowPlayingTemplate.shared.add(self)
nowPlayingPropertiesCancellable = Self.nowPlayingPropertiesPublisher(interfaceController: interfaceController!)
.sink { nowPlayingProperties in
@@ -122,28 +120,28 @@ extension CarPlayNowPlayingController: CarPlayTemplateController {
template.isUpNextButtonEnabled = nowPlayingProperties.upNextButtonEnabled
}
}
-
- func didAppear(animated: Bool) {
+
+ func didAppear(animated _: Bool) {
SRGAnalyticsTracker.shared.uncheckedTrackPageView(
withTitle: AnalyticsPageTitle.player.rawValue,
type: AnalyticsPageType.detail.rawValue,
levels: [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.automobile.rawValue]
)
}
-
- func willDisappear(animated: Bool) {}
-
- func didDisappear(animated: Bool) {
+
+ func willDisappear(animated _: Bool) {}
+
+ func didDisappear(animated _: Bool) {
nowPlayingPropertiesCancellable = nil
CPNowPlayingTemplate.shared.remove(self)
}
}
extension CarPlayNowPlayingController: CPNowPlayingTemplateObserver {
- func nowPlayingTemplateUpNextButtonTapped(_ nowPlayingTemplate: CPNowPlayingTemplate) {
+ func nowPlayingTemplateUpNextButtonTapped(_: CPNowPlayingTemplate) {
if let channel = SRGLetterboxService.shared.controller?.channel,
let media = SRGLetterboxService.shared.controller?.play_mainMedia,
- let interfaceController = interfaceController {
+ let interfaceController {
let template = CPListTemplate.list(.livePrograms(channel: channel, media: media), interfaceController: interfaceController)
interfaceController.pushTemplate(template, animated: true) { _, _ in }
}
diff --git a/Application/Sources/CarPlay/CarPlayPlaybackSpeedController.swift b/Application/Sources/CarPlay/CarPlayPlaybackSpeedController.swift
index 1d6f5bffa..92d19b145 100644
--- a/Application/Sources/CarPlay/CarPlayPlaybackSpeedController.swift
+++ b/Application/Sources/CarPlay/CarPlayPlaybackSpeedController.swift
@@ -12,7 +12,7 @@ import SRGLetterbox
final class CarPlayPlaybackSpeedController {
private var cancellables = Set()
-
+
private static func sections(template: CPListTemplate?) -> [CPListSection] {
guard let controller = SRGLetterboxService.shared.controller else { return [] }
let items = controller.supportedPlaybackRates
@@ -34,10 +34,10 @@ final class CarPlayPlaybackSpeedController {
}
return [CPListSection(items: items)]
}
-
+
init(template: CPListTemplate) {
template.emptyViewSubtitleVariants = [NSLocalizedString("No content", comment: "Default text displayed when no content is available")]
-
+
if let controller = SRGLetterboxService.shared.controller {
Self.playbackRateChangeSignal(for: controller)
.sink { [weak template] _ in
@@ -46,23 +46,22 @@ final class CarPlayPlaybackSpeedController {
.store(in: &cancellables)
}
}
-
+
private static func text(forPlaybackRate playbackRate: Float, controller: SRGLetterboxController) -> String {
let effectivePlaybackRate = controller.effectivePlaybackRate
- if playbackRate == controller.playbackRate && playbackRate != effectivePlaybackRate {
+ if playbackRate == controller.playbackRate, playbackRate != effectivePlaybackRate {
return String(format: NSLocalizedString("%1$@× (Currently: %2$@×)", comment: "Speed factor with current value if different from desired one"), playbackRate.minimalRepresentation, effectivePlaybackRate.minimalRepresentation)
- }
- else {
+ } else {
return String(format: NSLocalizedString("%@×", comment: "Speed factor"), playbackRate.minimalRepresentation)
}
}
-
+
private static func accessoryImage(forPlaybackRate playbackRate: Float, controller: SRGLetterboxController) -> UIImage? {
- return playbackRate == controller.playbackRate ? UIImage(systemName: "checkmark") : nil
+ playbackRate == controller.playbackRate ? UIImage(systemName: "checkmark") : nil
}
-
+
private static func playbackRateChangeSignal(for controller: SRGLetterboxController) -> AnyPublisher {
- return Publishers.Merge(
+ Publishers.Merge(
controller.publisher(for: \.playbackRate),
controller.publisher(for: \.effectivePlaybackRate)
)
@@ -74,11 +73,11 @@ final class CarPlayPlaybackSpeedController {
// MARK: Protocols
extension CarPlayPlaybackSpeedController: CarPlayTemplateController {
- func willAppear(animated: Bool) {}
-
- func didAppear(animated: Bool) {}
-
- func willDisappear(animated: Bool) {}
-
- func didDisappear(animated: Bool) {}
+ func willAppear(animated _: Bool) {}
+
+ func didAppear(animated _: Bool) {}
+
+ func willDisappear(animated _: Bool) {}
+
+ func didDisappear(animated _: Bool) {}
}
diff --git a/Application/Sources/CarPlay/CarPlaySceneDelegate.swift b/Application/Sources/CarPlay/CarPlaySceneDelegate.swift
index 9b2cd3d9c..1b9c3670d 100644
--- a/Application/Sources/CarPlay/CarPlaySceneDelegate.swift
+++ b/Application/Sources/CarPlay/CarPlaySceneDelegate.swift
@@ -19,34 +19,34 @@ extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate {
let nowPlayingTemplate = CPNowPlayingTemplate.shared
nowPlayingTemplate.controller = CarPlayNowPlayingController(interfaceController: interfaceController)
}
-
- func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) {
+
+ func templateApplicationScene(_: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) {
interfaceController.delegate = self
self.interfaceController = interfaceController
-
+
Self.configureNowPlayingTemplate(for: interfaceController)
-
+
let traitCollection = UITraitCollection(userInterfaceIdiom: .carPlay)
var templates = [CPTemplate]()
-
+
let livestreamsTemplate = CPListTemplate.list(.livestreams, interfaceController: interfaceController)
livestreamsTemplate.tabImage = UIImage(named: "livestreams_tab", in: nil, compatibleWith: traitCollection)
templates.append(livestreamsTemplate)
-
+
let favoriteEpisodesTemplate = CPListTemplate.list(.latestEpisodesFromFavorites, interfaceController: interfaceController)
favoriteEpisodesTemplate.tabImage = UIImage(named: "favorites_tab", in: nil, compatibleWith: traitCollection)
templates.append(favoriteEpisodesTemplate)
-
+
let mostPopularTemplate = CPListTemplate.list(.mostPopular, interfaceController: interfaceController)
mostPopularTemplate.tabImage = UIImage(named: "trends_tab", in: nil, compatibleWith: traitCollection)
templates.append(mostPopularTemplate)
-
+
let tabBarTemplate = CPTabBarTemplate(templates: templates)
interfaceController.setRootTemplate(tabBarTemplate, animated: true, completion: nil)
}
-
- private func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnect interfaceController: CPInterfaceController) {
- self.interfaceController = nil
+
+ private func templateApplicationScene(_: CPTemplateApplicationScene, didDisconnect _: CPInterfaceController) {
+ interfaceController = nil
}
}
@@ -54,15 +54,15 @@ extension CarPlaySceneDelegate: CPInterfaceControllerDelegate {
func templateWillAppear(_ aTemplate: CPTemplate, animated: Bool) {
aTemplate.notifyWillAppear(animated: animated)
}
-
+
func templateDidAppear(_ aTemplate: CPTemplate, animated: Bool) {
aTemplate.notifyDidAppear(animated: animated)
}
-
+
func templateWillDisappear(_ aTemplate: CPTemplate, animated: Bool) {
aTemplate.notifyWillDisappear(animated: animated)
}
-
+
func templateDidDisappear(_ aTemplate: CPTemplate, animated: Bool) {
aTemplate.notifyDidDisappear(animated: animated)
}
diff --git a/Application/Sources/CarPlay/CarPlayTemplateController.swift b/Application/Sources/CarPlay/CarPlayTemplateController.swift
index 7983796d6..8da0debe2 100644
--- a/Application/Sources/CarPlay/CarPlayTemplateController.swift
+++ b/Application/Sources/CarPlay/CarPlayTemplateController.swift
@@ -30,68 +30,68 @@ extension CPTemplate {
*/
var controller: CarPlayTemplateController? {
get {
- return objc_getAssociatedObject(self, &controllerKey) as? CarPlayTemplateController
+ objc_getAssociatedObject(self, &controllerKey) as? CarPlayTemplateController
}
set {
objc_setAssociatedObject(self, &controllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
-
+
private var appearedOnce: Bool {
get {
- return objc_getAssociatedObject(self, &appearedOnceKey) as? Bool ?? false
+ objc_getAssociatedObject(self, &appearedOnceKey) as? Bool ?? false
}
set {
objc_setAssociatedObject(self, &appearedOnceKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
-
+
func notifyWillAppear(animated: Bool) {
notifyWillAppear(animated: animated, recursive: false)
}
-
+
func notifyDidAppear(animated: Bool) {
notifyDidAppear(animated: animated, recursive: false)
}
-
+
func notifyWillDisappear(animated: Bool) {
notifyWillDisappear(animated: animated, recursive: false)
}
-
+
func notifyDidDisappear(animated: Bool) {
notifyDidDisappear(animated: animated, recursive: false)
}
-
- fileprivate func notifyWillAppear(animated: Bool, recursive: Bool) {
+
+ private func notifyWillAppear(animated: Bool, recursive: Bool) {
if recursive, let container = self as? CarPlayTemplateContainer, let activeChildTemplate = container.activeChildTemplate {
activeChildTemplate.notifyWillAppear(animated: animated, recursive: recursive)
}
-
+
if !recursive || appearedOnce {
controller?.willAppear(animated: animated)
}
}
-
- fileprivate func notifyDidAppear(animated: Bool, recursive: Bool) {
+
+ private func notifyDidAppear(animated: Bool, recursive: Bool) {
if recursive, let container = self as? CarPlayTemplateContainer, let activeChildTemplate = container.activeChildTemplate {
activeChildTemplate.notifyDidAppear(animated: animated, recursive: recursive)
}
-
+
if !recursive || appearedOnce {
controller?.didAppear(animated: animated)
}
-
+
appearedOnce = true
}
-
- fileprivate func notifyWillDisappear(animated: Bool, recursive: Bool) {
+
+ private func notifyWillDisappear(animated: Bool, recursive: Bool) {
if recursive, let container = self as? CarPlayTemplateContainer, let activeChildTemplate = container.activeChildTemplate {
activeChildTemplate.notifyWillDisappear(animated: animated, recursive: recursive)
}
controller?.willDisappear(animated: animated)
}
-
- fileprivate func notifyDidDisappear(animated: Bool, recursive: Bool) {
+
+ private func notifyDidDisappear(animated: Bool, recursive: Bool) {
if recursive, let container = self as? CarPlayTemplateContainer, let activeChildTemplate = container.activeChildTemplate {
activeChildTemplate.notifyDidDisappear(animated: animated, recursive: recursive)
}
@@ -101,6 +101,6 @@ extension CPTemplate {
extension CPTabBarTemplate: CarPlayTemplateContainer {
var activeChildTemplate: CPTemplate? {
- return selectedTemplate
+ selectedTemplate
}
}
diff --git a/Application/Sources/CarPlay/CarPlayTemplateListController.swift b/Application/Sources/CarPlay/CarPlayTemplateListController.swift
index 1a2a8a803..015e5897e 100644
--- a/Application/Sources/CarPlay/CarPlayTemplateListController.swift
+++ b/Application/Sources/CarPlay/CarPlayTemplateListController.swift
@@ -13,19 +13,19 @@ import SRGDataProviderCombine
final class CarPlayTemplateListController {
private let list: CarPlayList
private var cancellables = Set()
-
+
private let trigger = Trigger()
-
+
init(list: CarPlayList, template: CPListTemplate, interfaceController: CPInterfaceController) {
self.list = list
-
+
template.emptyViewSubtitleVariants = [NSLocalizedString("Loading…", comment: "Default text displayed when loading")]
-
+
Publishers.Publish(onOutputFrom: reloadSignal()) {
list.publisher(with: interfaceController)
.map { State.loaded(sections: $0) }
.catch { error in
- return Just(State.failed(error: error))
+ Just(State.failed(error: error))
}
}
.receive(on: DispatchQueue.main)
@@ -42,37 +42,37 @@ final class CarPlayTemplateListController {
}
.store(in: &cancellables)
}
-
+
private func reloadSignal() -> AnyPublisher {
- return Publishers.Merge(
+ Publishers.Merge(
trigger.signal(activatedBy: TriggerId.reload),
ApplicationSignal.wokenUp(.scene(filter: notificationFilter.self))
)
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false)
.eraseToAnyPublisher()
}
-
+
private func notificationFilter(notification: Notification) -> Bool {
- return notification.object is CPTemplateApplicationScene
+ notification.object is CPTemplateApplicationScene
}
}
// MARK: Protocols
extension CarPlayTemplateListController: CarPlayTemplateController {
- func willAppear(animated: Bool) {
+ func willAppear(animated _: Bool) {
trigger.activate(for: TriggerId.reload)
}
-
- func didAppear(animated: Bool) {
+
+ func didAppear(animated _: Bool) {
if let pageViewTitle = list.pageViewTitle, let pageViewType = list.pageViewType {
SRGAnalyticsTracker.shared.uncheckedTrackPageView(withTitle: pageViewTitle, type: pageViewType, levels: list.pageViewLevels)
}
}
-
- func willDisappear(animated: Bool) {}
-
- func didDisappear(animated: Bool) {}
+
+ func willDisappear(animated _: Bool) {}
+
+ func didDisappear(animated _: Bool) {}
}
// MARK: Types
@@ -82,7 +82,7 @@ extension CarPlayTemplateListController {
case failed(error: Error)
case loaded(sections: [CPListSection])
}
-
+
enum TriggerId {
case reload
}
diff --git a/Application/Sources/Configuration/ApplicationConfiguration.h b/Application/Sources/Configuration/ApplicationConfiguration.h
index 55d29d076..a343f69ca 100755
--- a/Application/Sources/Configuration/ApplicationConfiguration.h
+++ b/Application/Sources/Configuration/ApplicationConfiguration.h
@@ -66,11 +66,14 @@ OBJC_EXPORT NSString * const ApplicationConfigurationDidChangeNotification;
@property (nonatomic, readonly, copy, nullable) NSString *discoverySubtitleOptionLanguage;
@property (nonatomic, readonly, getter=arePosterImagesEnabled) BOOL posterImagesEnabled;
+@property (nonatomic, readonly, getter=areSquareImagesEnabled) BOOL squareImagesEnabled;
@property (nonatomic, readonly) NSArray *liveHomeSections; // wrap `HomeSection` values
@property (nonatomic, readonly) NSInteger minimumSocialViewCount; // minimum value to display social view count
+@property (nonatomic, readonly, getter=isAudioContentHomepagePreferred) BOOL audioContentHomepagePreferred;
+
@property (nonatomic, readonly) NSArray *radioChannels;
@property (nonatomic, readonly) NSArray *radioHomepageChannels; // radio channels having a corresponding homepage
@property (nonatomic, readonly) NSArray *tvChannels;
diff --git a/Application/Sources/Configuration/ApplicationConfiguration.m b/Application/Sources/Configuration/ApplicationConfiguration.m
index e9647e683..ad50d2124 100755
--- a/Application/Sources/Configuration/ApplicationConfiguration.m
+++ b/Application/Sources/Configuration/ApplicationConfiguration.m
@@ -151,11 +151,14 @@ @interface ApplicationConfiguration ()
@property (nonatomic, copy) NSString *discoverySubtitleOptionLanguage;
@property (nonatomic, getter=arePosterImagesEnabled) BOOL posterImagesEnabled;
+@property (nonatomic, getter=areSquareImagesEnabled) BOOL squareImagesEnabled;
@property (nonatomic) NSArray *liveHomeSections;
@property (nonatomic) NSInteger minimumSocialViewCount;
+@property (nonatomic, getter=isAudioContentHomepagePreferred) BOOL audioContentHomepagePreferred;
+
@property (nonatomic) NSArray *radioChannels;
@property (nonatomic) NSArray *audioHomeSections; // wrap `HomeSection` values
@@ -237,8 +240,8 @@ - (BOOL)isContinuousPlaybackAvailable
{
#if TARGET_OS_IOS
return self.continuousPlaybackBackgroundTransitionDuration != SRGLetterboxContinuousPlaybackDisabled
- || self.continuousPlaybackForegroundTransitionDuration != SRGLetterboxContinuousPlaybackDisabled
- || self.continuousPlaybackPlayerViewTransitionDuration != SRGLetterboxContinuousPlaybackDisabled;
+ || self.continuousPlaybackForegroundTransitionDuration != SRGLetterboxContinuousPlaybackDisabled
+ || self.continuousPlaybackPlayerViewTransitionDuration != SRGLetterboxContinuousPlaybackDisabled;
#else
return self.continuousPlaybackPlayerViewTransitionDuration != SRGLetterboxContinuousPlaybackDisabled;
#endif
@@ -266,6 +269,51 @@ - (BOOL)arePosterImagesEnabled
#endif
}
+- (BOOL)areSquareImagesEnabled
+{
+#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA)
+ switch (ApplicationSettingSquareImages()) {
+ case SettingSquareImagesForced: {
+ return YES;
+ break;
+ }
+ case SettingSquareImagesIgnored: {
+ return NO;
+ break;
+ }
+ default: {
+ return _squareImagesEnabled;
+ break;
+ }
+ }
+#else
+ return _squareImagesEnabled;
+#endif
+}
+
+- (BOOL)isAudioContentHomepagePreferred
+{
+#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA)
+ switch (ApplicationSettingAudioHomepageOption()) {
+ case SettingAudioHomepageOptionCuratedOne: {
+ return YES;
+ break;
+ }
+ case SettingAudioHomepageOptionCuratedMany:
+ case SettingAudioHomepageOptionPredefinedMany: {
+ return NO;
+ break;
+ }
+ default: {
+ return _audioContentHomepagePreferred;
+ break;
+ }
+ }
+#else
+ return _audioContentHomepagePreferred;
+#endif
+}
+
#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA)
- (void)setOverridePlayURLForVendorBasedOnServiceURL:(NSURL *)serviceURL
{
@@ -328,7 +376,7 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba
if (! analyticsBusinessUnitIdentifier) {
return NO;
}
-
+
#if defined(DEBUG) || defined(NIGHTLY) || defined(BETA)
NSString *analyticsSourceKey = @"39ae8f94-595c-4ca4-81f7-fb7748bd3f04";
#else
@@ -421,7 +469,7 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba
NSString *betaTestingURLString = [firebaseConfiguration stringForKey:@"betaTestingURL"];
self.betaTestingURL = betaTestingURLString ? [NSURL URLWithString:betaTestingURLString] : nil;
-
+
NSString *sourceCodeURLString = [firebaseConfiguration stringForKey:@"sourceCodeURL"];
self.sourceCodeURL = sourceCodeURLString ? [NSURL URLWithString:sourceCodeURLString] : nil;
@@ -447,6 +495,7 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba
self.discoverySubtitleOptionLanguage = [firebaseConfiguration stringForKey:@"discoverySubtitleOptionLanguage"];
self.posterImagesEnabled = [firebaseConfiguration boolForKey:@"posterImagesEnabled"];
+ self.squareImagesEnabled = [firebaseConfiguration boolForKey:@"squareImagesEnabled"];
#if TARGET_OS_IOS
self.liveHomeSections = [firebaseConfiguration homeSectionsForKey:@"liveHomeSections"];
@@ -456,6 +505,10 @@ - (BOOL)synchronizeWithFirebaseConfiguration:(PlayFirebaseConfiguration *)fireba
self.audioHomeSections = [firebaseConfiguration homeSectionsForKey:@"audioHomeSections"];
+#if DEBUG || NIGHTLY || BETA
+ self.audioContentHomepagePreferred = [firebaseConfiguration boolForKey:@"audioContentHomepagePreferred"];
+#endif
+
self.radioChannels = [firebaseConfiguration radioChannelsForKey:@"radioChannels" defaultHomeSections:self.audioHomeSections];
self.tvChannels = [firebaseConfiguration tvChannelsForKey:@"tvChannels"];
self.satelliteRadioChannels = [firebaseConfiguration radioChannelsForKey:@"satelliteRadioChannels" defaultHomeSections:nil];
diff --git a/Application/Sources/Configuration/ApplicationConfiguration.swift b/Application/Sources/Configuration/ApplicationConfiguration.swift
index 9964edd81..28e77102f 100644
--- a/Application/Sources/Configuration/ApplicationConfiguration.swift
+++ b/Application/Sources/Configuration/ApplicationConfiguration.swift
@@ -10,78 +10,76 @@ extension ApplicationConfiguration {
private static func configuredSection(from homeSection: HomeSection) -> ConfiguredSection? {
switch homeSection {
case .tvLive:
- return .tvLive
+ .tvLive
case .radioLive:
- return .radioLive
+ .radioLive
case .radioLiveSatellite:
- return .radioLiveSatellite
+ .radioLiveSatellite
case .tvLiveCenterScheduledLivestreams:
- return .tvLiveCenterScheduledLivestreams
+ .tvLiveCenterScheduledLivestreams
case .tvLiveCenterScheduledLivestreamsAll:
- return .tvLiveCenterScheduledLivestreamsAll
+ .tvLiveCenterScheduledLivestreamsAll
case .tvLiveCenterEpisodes:
- return .tvLiveCenterEpisodes
+ .tvLiveCenterEpisodes
case .tvLiveCenterEpisodesAll:
- return .tvLiveCenterEpisodesAll
+ .tvLiveCenterEpisodesAll
case .tvScheduledLivestreams:
- return .tvScheduledLivestreams
+ .tvScheduledLivestreams
case .tvScheduledLivestreamsNews:
- return .tvScheduledLivestreamsNews
+ .tvScheduledLivestreamsNews
case .tvScheduledLivestreamsSport:
- return .tvScheduledLivestreamsSport
+ .tvScheduledLivestreamsSport
case .tvScheduledLivestreamsSignLanguage:
- return .tvScheduledLivestreamsSignLanguage
+ .tvScheduledLivestreamsSignLanguage
default:
- return nil
+ nil
}
}
-
+
var liveConfiguredSections: [ConfiguredSection] {
- return liveHomeSections.compactMap { homeSection in
+ liveHomeSections.compactMap { homeSection in
guard let homeSection = HomeSection(rawValue: homeSection.intValue) else { return nil }
return Self.configuredSection(from: homeSection)
}
}
-
+
var serviceMessageUrl: URL {
- return URL(string: "v3/api/\(businessUnitIdentifier)/general-information-message", relativeTo: playServiceURL)!
+ URL(string: "v3/api/\(businessUnitIdentifier)/general-information-message", relativeTo: playServiceURL)!
}
-
+
func relatedContentUrl(for media: SRGMedia) -> URL {
- return URL(string: "api/v2/playlist/recommendation/relatedContent/\(media.urn)", relativeTo: self.middlewareURL)!
+ URL(string: "api/v2/playlist/recommendation/relatedContent/\(media.urn)", relativeTo: middlewareURL)!
}
-
+
func topicColors(for topic: SRGTopic) -> (Color, Color)? {
- guard let topicColorsArray = self.topicColors[topic.urn], topicColorsArray.count == 2 else { return nil }
-
+ guard let topicColorsArray = topicColors[topic.urn], topicColorsArray.count == 2 else { return nil }
+
let colors = topicColorsArray.map { Color($0) }
return (colors.first!, colors.last!)
}
-
+
private static var version: String {
- return Bundle.main.play_friendlyVersionNumber
+ Bundle.main.play_friendlyVersionNumber
}
-
+
private static var type: String {
if ProcessInfo.processInfo.isMacCatalystApp || ProcessInfo.processInfo.isiOSAppOnMac {
- return "desktop"
- }
- else if UIDevice.current.userInterfaceIdiom == .pad {
- return "tablet"
- }
- else {
- return "phone"
+ "desktop"
+ } else if UIDevice.current.userInterfaceIdiom == .pad {
+ "tablet"
+ } else {
+ "phone"
}
}
-
+
private static var identifier: String? {
- return UserDefaults.standard.string(forKey: "tc_unique_id")
+ UserDefaults.standard.string(forKey: "tc_unique_id")
}
-
+
private static func typeformUrlWithParameters(_ url: URL) -> URL {
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
guard let host = urlComponents.host, host.contains("typeform.") else { return url }
-
+
let typeformQueryItems = [
URLQueryItem(name: "platform", value: "iOS"),
URLQueryItem(name: "version", value: version),
@@ -90,36 +88,35 @@ extension ApplicationConfiguration {
]
if let queryItems = urlComponents.queryItems {
urlComponents.queryItems = typeformQueryItems.appending(contentsOf: queryItems)
- }
- else {
+ } else {
urlComponents.queryItems = typeformQueryItems
}
return urlComponents.url ?? url
}
-
+
var userSuggestionUrlWithParameters: URL? {
guard let feedbackUrl = feedbackURL else { return nil }
-
+
return Self.typeformUrlWithParameters(feedbackUrl)
}
-
+
var tvGuideOtherBouquets: [TVGuideBouquet] {
- return self.tvGuideOtherBouquetsObjc.map { number in
- return TVGuideBouquet(rawValue: number.intValue)!
+ tvGuideOtherBouquetsObjc.map { number in
+ TVGuideBouquet(rawValue: number.intValue)!
}
}
}
enum ConfiguredSection: Hashable {
case availableEpisodes(SRGShow)
-
- case favoriteShows
+
+ case favoriteShows(contentType: ContentType)
case history
case watchLater
-
+
case tvAllShows
case tvEpisodesForDay(_ day: SRGDay)
-
+
case radioAllShows(channelUid: String)
case radioEpisodesForDay(_ day: SRGDay, channelUid: String)
case radioFavoriteShows(channelUid: String)
@@ -130,11 +127,11 @@ enum ConfiguredSection: Hashable {
case radioMostPopular(channelUid: String)
case radioResumePlayback(channelUid: String)
case radioWatchLater(channelUid: String)
-
+
case tvLive
case radioLive
case radioLiveSatellite
-
+
case tvLiveCenterScheduledLivestreams
case tvLiveCenterScheduledLivestreamsAll
case tvLiveCenterEpisodes
@@ -143,10 +140,10 @@ enum ConfiguredSection: Hashable {
case tvScheduledLivestreamsNews
case tvScheduledLivestreamsSport
case tvScheduledLivestreamsSignLanguage
-
-#if os(iOS)
- case downloads
- case notifications
- case radioShowAccess(channelUid: String)
-#endif
+
+ #if os(iOS)
+ case downloads
+ case notifications
+ case radioShowAccess(channelUid: String)
+ #endif
}
diff --git a/Application/Sources/Configuration/Channel.h b/Application/Sources/Configuration/Channel.h
index cbc415b65..b07494feb 100644
--- a/Application/Sources/Configuration/Channel.h
+++ b/Application/Sources/Configuration/Channel.h
@@ -78,6 +78,11 @@ typedef NS_ENUM(NSInteger, SongsViewStyle) {
*/
@property (nonatomic, readonly) SongsViewStyle songsViewStyle;
+/**
+ * The channel content page identifier.
+ */
+@property (nonatomic, readonly, copy, nullable) NSString *contentPageId;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Application/Sources/Configuration/Channel.m b/Application/Sources/Configuration/Channel.m
index c87784aa4..ab1d7b138 100644
--- a/Application/Sources/Configuration/Channel.m
+++ b/Application/Sources/Configuration/Channel.m
@@ -31,6 +31,7 @@ @interface Channel ()
@property (nonatomic) UIColor *titleColor;
@property (nonatomic, getter=hasDarkStatusBar) BOOL darkStatusBar;
@property (nonatomic) SongsViewStyle songsViewStyle;
+@property (nonatomic, copy) NSString *contentPageId;
@end
@@ -91,6 +92,13 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary
if ([songsViewStyleValue isKindOfClass:NSString.class]) {
self.songsViewStyle = SongsViewStyleWithString(songsViewStyleValue);
}
+
+#if DEBUG || NIGHTLY || BETA
+ id contentPageIdValue = dictionary[@"contentPageId"];
+ if ([contentPageIdValue isKindOfClass:NSString.class]) {
+ self.contentPageId = contentPageIdValue;
+ }
+#endif
}
return self;
}
diff --git a/Application/Sources/Configuration/PlayFirebaseConfiguration.m b/Application/Sources/Configuration/PlayFirebaseConfiguration.m
index 8de93143c..4e3b956ea 100644
--- a/Application/Sources/Configuration/PlayFirebaseConfiguration.m
+++ b/Application/Sources/Configuration/PlayFirebaseConfiguration.m
@@ -68,9 +68,9 @@ static HomeSection HomeSectionWithString(NSString *string)
static NSDictionary *s_bouqets;
dispatch_once(&s_onceToken, ^{
s_bouqets = @{ @"thirdparty" : @(TVGuideBouquetThirdParty),
- @"rsi" : @(TVGuideBouquetRSI),
- @"rts" : @(TVGuideBouquetRTS),
- @"srf" : @(TVGuideBouquetSRF)
+ @"rsi" : @(TVGuideBouquetRSI),
+ @"rts" : @(TVGuideBouquetRTS),
+ @"srf" : @(TVGuideBouquetSRF)
};
});
return s_bouqets[string];
diff --git a/Application/Sources/Configuration/RadioChannel.swift b/Application/Sources/Configuration/RadioChannel.swift
index 18ec0a26e..772079441 100644
--- a/Application/Sources/Configuration/RadioChannel.swift
+++ b/Application/Sources/Configuration/RadioChannel.swift
@@ -25,17 +25,17 @@ extension RadioChannel {
return .radioResumePlayback(channelUid: channelUid)
case .radioWatchLater:
return .radioWatchLater(channelUid: channelUid)
-#if os(iOS)
- case .radioShowsAccess:
- return .radioShowAccess(channelUid: channelUid)
-#endif
+ #if os(iOS)
+ case .radioShowsAccess:
+ return .radioShowAccess(channelUid: channelUid)
+ #endif
default:
return nil
}
}
-
+
func configuredSections() -> [ConfiguredSection] {
- return homeSections.compactMap { homeSection in
+ homeSections.compactMap { homeSection in
guard let homeSection = HomeSection(rawValue: homeSection.intValue) else { return nil }
return Self.configuredSection(from: homeSection, withChannelUid: uid)
}
diff --git a/Application/Sources/Content/Content.swift b/Application/Sources/Content/Content.swift
index 7d61943b0..f9aab485d 100644
--- a/Application/Sources/Content/Content.swift
+++ b/Application/Sources/Content/Content.swift
@@ -9,44 +9,63 @@ import SRGDataProviderCombine
private let kDefaultNumberOfPlaceholders = 10
private let kDefaultNumberOfLivestreamPlaceholders = 4
+// MARK: Content types
+
+@objc enum ContentType: Int {
+ case videoOrTV
+ case audioOrRadio
+ case mixed
+
+ var imageVariant: SRGImageVariant {
+ switch self {
+ case .videoOrTV:
+ ApplicationConfiguration.shared.arePosterImagesEnabled ? .poster : .default
+ case .audioOrRadio:
+ ApplicationConfiguration.shared.areSquareImagesEnabled ? .podcast : .default
+ case .mixed:
+ .default
+ }
+ }
+}
+
// MARK: Types
enum Content {
enum Section: Hashable {
- case content(SRGContentSection, show: SRGShow? = nil)
+ case content(SRGContentSection, type: ContentType, show: SRGShow? = nil)
case configured(ConfiguredSection)
-
+
var properties: SectionProperties {
switch self {
- case let .content(section, show):
- return ContentSectionProperties(contentSection: section, show: show)
+ case let .content(section, type, show):
+ ContentSectionProperties(contentSection: section, contentType: type, show: show)
case let .configured(section):
- return ConfiguredSectionProperties(configuredSection: section)
+ ConfiguredSectionProperties(configuredSection: section)
}
}
}
-
+
indirect enum Item: Hashable {
case mediaPlaceholder(index: Int)
case media(_ media: SRGMedia)
-
+
case showPlaceholder(index: Int)
case show(_ show: SRGShow)
-
+
case topicPlaceholder(index: Int)
case topic(_ topic: SRGTopic)
-
-#if os(iOS)
- case download(_ download: Download)
- case notification(_ notification: UserNotification)
- case showAccess(radioChannel: RadioChannel?)
-#endif
-
+
+ #if os(iOS)
+ case download(_ download: Download)
+ case notification(_ notification: UserNotification)
+ case showAccess(radioChannel: RadioChannel?)
+ #endif
+
case highlightPlaceholder(index: Int)
case highlight(_ highlight: Highlight, item: Self?)
-
+
case transparent
-
+
private var title: String? {
switch self {
case let .media(media):
@@ -55,66 +74,62 @@ enum Content {
return show.title
case let .topic(topic):
return topic.title
-#if os(iOS)
- case let .download(download):
- return download.title
- case let .notification(notification):
- return notification.title
-#endif
+ #if os(iOS)
+ case let .download(download):
+ return download.title
+ case let .notification(notification):
+ return notification.title
+ #endif
case let .highlight(highlight, _):
return highlight.title
default:
return nil
}
}
-
+
static func groupAlphabetically(_ items: [Self]) -> [(key: Character, value: [Self])] {
- return items.groupedAlphabetically { $0.title }
+ items.groupedAlphabetically { $0.title }
}
}
-
+
static func medias(from items: [Self.Item]) -> [SRGMedia] {
- return items.compactMap { item in
+ items.compactMap { item in
if case let .media(media) = item {
- return media
- }
- else {
- return nil
+ media
+ } else {
+ nil
}
}
}
-
-#if os(iOS)
- static func downloads(from items: [Self.Item]) -> [Download] {
- return items.compactMap { item in
- if case let .download(download) = item {
- return download
- }
- else {
- return nil
+
+ #if os(iOS)
+ static func downloads(from items: [Self.Item]) -> [Download] {
+ items.compactMap { item in
+ if case let .download(download) = item {
+ download
+ } else {
+ nil
+ }
}
}
- }
-
- static func notifications(from items: [Self.Item]) -> [UserNotification] {
- return items.compactMap { item in
- if case let .notification(notification) = item {
- return notification
- }
- else {
- return nil
+
+ static func notifications(from items: [Self.Item]) -> [UserNotification] {
+ items.compactMap { item in
+ if case let .notification(notification) = item {
+ notification
+ } else {
+ nil
+ }
}
}
- }
-#endif
-
+ #endif
+
static func shows(from items: [Self.Item]) -> [SRGShow] {
- return items.compactMap { item in
+ items.compactMap { item in
if case let .show(show) = item {
- return show
- }
- else {
- return nil
+ show
+ } else {
+ nil
}
}
}
@@ -134,41 +149,42 @@ protocol SectionProperties {
var label: String? { get }
var image: SRGImage? { get }
var imageVariant: SRGImageVariant { get }
-
+
/// Properties for section detail display
var displaysTitle: Bool { get }
var supportsEdition: Bool { get }
var emptyType: EmptyContentView.`Type` { get }
var hasHighlightedItem: Bool { get }
-
+ var couldHaveHighlightedItem: Bool { get }
+
var displayedShow: SRGShow? { get }
-#if os(iOS)
- var sharingItem: SharingItem? { get }
- var canResetApplicationBadge: Bool { get }
-#endif
-
+ #if os(iOS)
+ var sharingItem: SharingItem? { get }
+ var canResetApplicationBadge: Bool { get }
+ #endif
+
/// Analytics information
var analyticsTitle: String? { get }
var analyticsType: String? { get }
var analyticsLevels: [String]? { get }
func analyticsDeletionHiddenEvent(source: AnalyticsListSource) -> AnalyticsEvent?
-
+
/// Properties for section displayed as a row
var rowHighlight: Highlight? { get }
var placeholderRowItems: [Content.Item] { get }
var displaysRowHeader: Bool { get }
var openContentPageId: String? { get }
-
+
/// Publisher providing content for the section. A single result must be delivered upon subscription. Further
/// results can be retrieved (if any) using a paginator, one page at a time.
func publisher(pageSize: UInt, paginatedBy paginator: Trigger.Signal?, filter: SectionFiltering?) -> AnyPublisher<[Content.Item], Error>
-
+
/// Publisher for interactive updates (addition / removal of items by the user).
func interactiveUpdatesPublisher() -> AnyPublisher<[Content.Item], Never>
-
+
/// Signal which can be used to trigger a section reload.
func reloadSignal() -> AnyPublisher?
-
+
/// Method to be called for removing the specified items from an editable section.
func remove(_ items: [Content.Item])
}
@@ -176,189 +192,193 @@ protocol SectionProperties {
private extension Content {
struct ContentSectionProperties: SectionProperties {
let contentSection: SRGContentSection
+ let contentType: ContentType
let show: SRGShow?
-
+
private var presentation: SRGContentPresentation {
- return contentSection.presentation
+ contentSection.presentation
}
-
+
var title: String? {
if let title = presentation.title {
- return title
- }
- else {
+ title
+ } else {
switch presentation.type {
case .favoriteShows:
- return NSLocalizedString("Favorites", comment: "Title label used to present the TV or radio favorite shows")
+ NSLocalizedString("Favorites", comment: "Title label used to present the TV or radio favorite shows")
case .myProgram:
- return NSLocalizedString("Latest episodes from your favorites", comment: "Title label used to present the latest episodes from TV favorite shows")
+ NSLocalizedString("Latest episodes from your favorites", comment: "Title label used to present the latest episodes from TV favorite shows")
case .livestreams:
- return NSLocalizedString("TV channels", comment: "Title label to present main TV livestreams")
+ NSLocalizedString("TV channels", comment: "Title label to present main TV livestreams")
case .continueWatching:
- return NSLocalizedString("Resume playback", comment: "Title label used to present medias whose playback can be resumed")
+ NSLocalizedString("Resume playback", comment: "Title label used to present medias whose playback can be resumed")
case .watchLater:
- return NSLocalizedString("Later", comment: "Title Label used to present the video later list")
+ NSLocalizedString("Later", comment: "Title Label used to present the video later list")
case .showAccess:
- return NSLocalizedString("Shows", comment: "Title label used to present the TV shows AZ and TV shows by date access buttons")
+ NSLocalizedString("Shows", comment: "Title label used to present the TV shows AZ and TV shows by date access buttons")
case .topicSelector:
- return NSLocalizedString("Topics", comment: "Title label used to present the topic list")
+ NSLocalizedString("Topics", comment: "Title label used to present the topic list")
default:
- return nil
+ nil
}
}
}
-
+
var summary: String? {
- return presentation.summary
+ presentation.summary
}
-
+
var label: String? {
- return presentation.label
+ presentation.label
}
-
+
var image: SRGImage? {
- return presentation.image
+ presentation.image
}
-
+
var imageVariant: SRGImageVariant {
- guard ApplicationConfiguration.shared.arePosterImagesEnabled else { return .default }
switch contentSection.type {
case .shows:
- return .poster
+ contentType.imageVariant
case .predefined:
switch presentation.type {
case .favoriteShows:
- return .poster
+ contentType.imageVariant
default:
- return .default
+ .default
}
default:
- return .default
+ .default
}
}
-
+
var displaysTitle: Bool {
switch contentSection.type {
case .showAndMedias:
- return false
+ false
default:
- return true
+ true
}
}
-
+
var supportsEdition: Bool {
switch contentSection.type {
case .predefined:
switch presentation.type {
case .favoriteShows, .continueWatching, .watchLater:
- return true
+ true
default:
- return false
+ false
}
default:
- return false
+ false
}
}
-
+
var emptyType: EmptyContentView.`Type` {
switch contentSection.type {
case .predefined:
switch contentSection.presentation.type {
case .favoriteShows:
- return .favoriteShows
+ .favoriteShows
case .myProgram:
- return .episodesFromFavorites
+ .episodesFromFavorites
case .continueWatching:
- return .resumePlayback
+ .resumePlayback
case .watchLater:
- return .watchLater
+ .watchLater
default:
- return .generic
+ .generic
}
default:
- return .generic
+ .generic
}
}
-
+
var hasHighlightedItem: Bool {
- return presentation.type == .showPromotion
- }
-
- var displayedShow: SRGShow? {
- return show
+ presentation.type == .showPromotion
}
-#if os(iOS)
- var sharingItem: SharingItem? {
- return SharingItem(for: contentSection)
+
+ var couldHaveHighlightedItem: Bool {
+ presentation.type == .highlight
}
-
- var canResetApplicationBadge: Bool {
- return false
+
+ var displayedShow: SRGShow? {
+ show
}
-#endif
-
+
+ #if os(iOS)
+ var sharingItem: SharingItem? {
+ SharingItem(for: contentSection)
+ }
+
+ var canResetApplicationBadge: Bool {
+ false
+ }
+ #endif
+
var analyticsTitle: String? {
switch contentSection.type {
case .medias, .showAndMedias, .shows:
- return contentSection.presentation.title ?? contentSection.uid
+ contentSection.presentation.title ?? contentSection.uid
case .predefined:
switch presentation.type {
case .favoriteShows:
- return AnalyticsPageTitle.favorites.rawValue
+ AnalyticsPageTitle.favorites.rawValue
case .myProgram:
- return AnalyticsPageTitle.latestEpisodesFromFavorites.rawValue
+ AnalyticsPageTitle.latestEpisodesFromFavorites.rawValue
case .continueWatching:
- return AnalyticsPageTitle.resumePlayback.rawValue
+ AnalyticsPageTitle.resumePlayback.rawValue
case .watchLater:
- return AnalyticsPageTitle.watchLater.rawValue
+ AnalyticsPageTitle.watchLater.rawValue
case .topicSelector:
- return AnalyticsPageTitle.topics.rawValue
+ AnalyticsPageTitle.topics.rawValue
default:
- return nil
+ nil
}
case .none:
- return nil
+ nil
}
}
-
+
var analyticsType: String? {
switch contentSection.type {
case .none:
- return nil
+ nil
default:
- return AnalyticsPageType.detail.rawValue
+ AnalyticsPageType.detail.rawValue
}
}
-
+
var analyticsLevels: [String]? {
switch contentSection.type {
case .medias, .showAndMedias, .shows:
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue, AnalyticsPageLevel.section.rawValue]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue, AnalyticsPageLevel.section.rawValue]
case .predefined:
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue]
+ [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue]
case .none:
- return nil
+ nil
}
}
-
+
func analyticsDeletionHiddenEvent(source: AnalyticsListSource) -> AnalyticsEvent? {
switch presentation.type {
case .favoriteShows:
- return AnalyticsEvent.favorite(action: .remove, source: source, urn: nil)
+ AnalyticsEvent.favorite(action: .remove, source: source, urn: nil)
case .watchLater:
- return AnalyticsEvent.watchLater(action: .remove, source: source, urn: nil)
+ AnalyticsEvent.watchLater(action: .remove, source: source, urn: nil)
case .continueWatching:
- return AnalyticsEvent.historyRemove(source: source, urn: nil)
+ AnalyticsEvent.historyRemove(source: source, urn: nil)
default:
- return nil
+ nil
}
}
-
+
var rowHighlight: Highlight? {
guard presentation.type == .highlight || presentation.type == .showPromotion else { return nil }
return Highlight(from: contentSection)
}
-
+
var placeholderRowItems: [Content.Item] {
switch presentation.type {
case .mediaElement:
@@ -366,43 +386,43 @@ private extension Content {
case .showElement:
return [.showPlaceholder(index: 0)]
case .topicSelector:
- return (0.. AnyPublisher<[Content.Item], Error> {
let dataProvider = SRGDataProvider.current!
-
+
switch contentSection.type {
case .medias:
return dataProvider.medias(for: contentSection.vendor, contentSectionUid: contentSection.uid, pageSize: pageSize, paginatedBy: paginator)
@@ -451,19 +471,18 @@ private extension Content {
return dataProvider.laterPublisher(pageSize: pageSize, paginatedBy: paginator, filter: filter)
.map { $0.map { .media($0) } }
.eraseToAnyPublisher()
-#if os(iOS)
- case .showAccess:
- return Just([.showAccess(radioChannel: nil)])
- .setFailureType(to: Error.self)
- .eraseToAnyPublisher()
-#endif
+ #if os(iOS)
+ case .showAccess:
+ return Just([.showAccess(radioChannel: nil)])
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
+ #endif
case .availableEpisodes:
if let show {
return dataProvider.latestMediasForShow(withUrn: show.urn, pageSize: pageSize, paginatedBy: paginator)
.map { $0.map { .media($0) } }
.eraseToAnyPublisher()
- }
- else {
+ } else {
return Just([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
@@ -479,38 +498,38 @@ private extension Content {
.eraseToAnyPublisher()
}
}
-
+
func interactiveUpdatesPublisher() -> AnyPublisher<[Content.Item], Never> {
switch contentSection.type {
case .predefined:
switch contentSection.presentation.type {
case .favoriteShows, .myProgram:
- return UserInteractionSignal.favoriteUpdates()
+ UserInteractionSignal.favoriteUpdates()
case .continueWatching:
- return UserInteractionSignal.historyUpdates()
+ UserInteractionSignal.historyUpdates()
case .watchLater:
- return UserInteractionSignal.watchLaterUpdates()
+ UserInteractionSignal.watchLaterUpdates()
default:
- return Just([]).eraseToAnyPublisher()
+ Just([]).eraseToAnyPublisher()
}
default:
- return Just([]).eraseToAnyPublisher()
+ Just([]).eraseToAnyPublisher()
}
}
-
+
func reloadSignal() -> AnyPublisher? {
switch presentation.type {
case .favoriteShows, .myProgram:
- return ThrottledSignal.preferenceUpdates()
+ ThrottledSignal.preferenceUpdates()
case .watchLater:
- return ThrottledSignal.watchLaterUpdates()
+ ThrottledSignal.watchLaterUpdates()
default:
// TODO: No history updates yet for battery consumption reasons. Fix when an efficient way to
// broadcast and apply history updates is available.
- return nil
+ nil
}
}
-
+
func remove(_ items: [Content.Item]) {
switch presentation.type {
case .favoriteShows:
@@ -523,17 +542,15 @@ private extension Content {
break
}
}
-
+
private func filterItems(_ items: [T]) -> [T] {
guard presentation.type == .mediaElement || presentation.type == .showElement else { return items }
-
+
if presentation.isRandomized, let item = items.randomElement() {
return [item]
- }
- else if !presentation.isRandomized, let item = items.first {
+ } else if !presentation.isRandomized, let item = items.first {
return [item]
- }
- else {
+ } else {
return []
}
}
@@ -545,7 +562,7 @@ private extension Content {
private extension Content {
struct ConfiguredSectionProperties: SectionProperties {
let configuredSection: ConfiguredSection
-
+
var title: String? {
switch configuredSection {
case .history:
@@ -586,58 +603,59 @@ private extension Content {
return NSLocalizedString("Sport livestreams", comment: "Title label used to present sport scheduled livestream medias")
case .tvScheduledLivestreamsSignLanguage:
return NSLocalizedString("Sign language livestreams", comment: "Title label used to present sign language scheduled livestream medias")
-#if os(iOS)
- case .downloads:
- return NSLocalizedString("Downloads", comment: "Label to present downloads")
- case .notifications:
- return NSLocalizedString("Notifications", comment: "Title label used to present notifications")
- case .radioShowAccess:
- return NSLocalizedString("Shows", comment: "Title label used to present the radio shows AZ and radio shows by date access buttons")
-#endif
+ #if os(iOS)
+ case .downloads:
+ return NSLocalizedString("Downloads", comment: "Label to present downloads")
+ case .notifications:
+ return NSLocalizedString("Notifications", comment: "Title label used to present notifications")
+ case .radioShowAccess:
+ return NSLocalizedString("Shows", comment: "Title label used to present the radio shows AZ and radio shows by date access buttons")
+ #endif
default:
return nil
}
}
-
+
var summary: String? {
- return nil
+ nil
}
-
+
var label: String? {
- return nil
+ nil
}
-
+
var image: SRGImage? {
- return nil
+ nil
}
-
+
var imageVariant: SRGImageVariant {
- guard ApplicationConfiguration.shared.arePosterImagesEnabled else { return .default }
switch configuredSection {
case .tvAllShows:
- return .poster
+ .default
+ case .radioAllShows:
+ ContentType.audioOrRadio.imageVariant
default:
- return .default
+ ContentType.mixed.imageVariant
}
}
-
+
var displaysTitle: Bool {
- return true
+ true
}
-
+
var supportsEdition: Bool {
switch configuredSection {
case .favoriteShows, .history, .radioFavoriteShows, .radioResumePlayback, .radioWatchLater, .watchLater:
return true
-#if os(iOS)
- case .downloads, .notifications:
- return true
-#endif
+ #if os(iOS)
+ case .downloads, .notifications:
+ return true
+ #endif
default:
return false
}
}
-
+
var emptyType: EmptyContentView.`Type` {
switch configuredSection {
case .favoriteShows, .radioFavoriteShows:
@@ -650,49 +668,53 @@ private extension Content {
return .history
case .radioResumePlayback:
return .resumePlayback
-#if os(iOS)
- case .downloads:
- return .downloads
- case .notifications:
- return .notifications
-#endif
+ #if os(iOS)
+ case .downloads:
+ return .downloads
+ case .notifications:
+ return .notifications
+ #endif
default:
return .generic
}
}
-
+
var hasHighlightedItem: Bool {
- return false
+ false
+ }
+
+ var couldHaveHighlightedItem: Bool {
+ false
}
-
+
var displayedShow: SRGShow? {
if case let .availableEpisodes(show) = configuredSection {
- return show
- }
- else {
- return nil
+ show
+ } else {
+ nil
}
}
-#if os(iOS)
- var sharingItem: SharingItem? {
- switch configuredSection {
- case let .availableEpisodes(show):
- return SharingItem(for: show)
- default:
- return nil
+
+ #if os(iOS)
+ var sharingItem: SharingItem? {
+ switch configuredSection {
+ case let .availableEpisodes(show):
+ SharingItem(for: show)
+ default:
+ nil
+ }
}
- }
-
- var canResetApplicationBadge: Bool {
- switch configuredSection {
- case .notifications:
- return true
- default:
- return false
+
+ var canResetApplicationBadge: Bool {
+ switch configuredSection {
+ case .notifications:
+ true
+ default:
+ false
+ }
}
- }
-#endif
-
+ #endif
+
var analyticsTitle: String? {
switch configuredSection {
case .history:
@@ -717,45 +739,44 @@ private extension Content {
return AnalyticsPageTitle.sports.rawValue
case .tvScheduledLivestreams, .tvScheduledLivestreamsNews, .tvScheduledLivestreamsSport, .tvScheduledLivestreamsSignLanguage:
return AnalyticsPageTitle.scheduledLivestreams.rawValue
-#if os(iOS)
- case .downloads:
- return AnalyticsPageTitle.downloads.rawValue
- case .notifications:
- return AnalyticsPageTitle.notifications.rawValue
-#endif
+ #if os(iOS)
+ case .downloads:
+ return AnalyticsPageTitle.downloads.rawValue
+ case .notifications:
+ return AnalyticsPageTitle.notifications.rawValue
+ #endif
default:
return nil
}
}
-
+
var analyticsType: String? {
switch configuredSection {
case .radioAllShows, .tvAllShows:
- return AnalyticsPageType.overview.rawValue
+ AnalyticsPageType.overview.rawValue
case .tvLiveCenterScheduledLivestreams, .tvLiveCenterScheduledLivestreamsAll, .tvLiveCenterEpisodes, .tvLiveCenterEpisodesAll,
- .tvScheduledLivestreams, .tvScheduledLivestreamsNews, .tvScheduledLivestreamsSport, .tvScheduledLivestreamsSignLanguage,
- .tvLive, .radioLive, .radioLiveSatellite:
- return AnalyticsPageType.live.rawValue
+ .tvScheduledLivestreams, .tvScheduledLivestreamsNews, .tvScheduledLivestreamsSport, .tvScheduledLivestreamsSignLanguage,
+ .tvLive, .radioLive, .radioLiveSatellite:
+ AnalyticsPageType.live.rawValue
default:
- return AnalyticsPageType.detail.rawValue
+ AnalyticsPageType.detail.rawValue
}
}
-
+
var analyticsLevels: [String]? {
switch configuredSection {
case let .radioAllShows(channelUid),
- let .radioFavoriteShows(channelUid: channelUid),
- let .radioLatest(channelUid: channelUid),
- let .radioLatestEpisodes(channelUid: channelUid),
- let .radioLatestEpisodesFromFavorites(channelUid: channelUid),
- let .radioLatestVideos(channelUid: channelUid),
- let .radioMostPopular(channelUid: channelUid),
- let .radioResumePlayback(channelUid: channelUid),
- let .radioWatchLater(channelUid: channelUid):
+ let .radioFavoriteShows(channelUid: channelUid),
+ let .radioLatest(channelUid: channelUid),
+ let .radioLatestEpisodes(channelUid: channelUid),
+ let .radioLatestEpisodesFromFavorites(channelUid: channelUid),
+ let .radioLatestVideos(channelUid: channelUid),
+ let .radioMostPopular(channelUid: channelUid),
+ let .radioResumePlayback(channelUid: channelUid),
+ let .radioWatchLater(channelUid: channelUid):
if let channel = ApplicationConfiguration.shared.radioChannel(forUid: channelUid) {
return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.audio.rawValue, channel.name]
- }
- else {
+ } else {
return nil
}
case .tvAllShows:
@@ -774,15 +795,15 @@ private extension Content {
return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.live.rawValue, AnalyticsPageLevel.signLanguage.rawValue]
case .favoriteShows, .history, .watchLater:
return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.user.rawValue]
-#if os(iOS)
- case .downloads, .notifications:
- return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.user.rawValue]
-#endif
+ #if os(iOS)
+ case .downloads, .notifications:
+ return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.user.rawValue]
+ #endif
default:
return nil
}
}
-
+
func analyticsDeletionHiddenEvent(source: AnalyticsListSource) -> AnalyticsEvent? {
switch configuredSection {
case .favoriteShows, .radioFavoriteShows:
@@ -791,52 +812,52 @@ private extension Content {
return AnalyticsEvent.watchLater(action: .remove, source: source, urn: nil)
case .history, .radioResumePlayback:
return AnalyticsEvent.historyRemove(source: source, urn: nil)
-#if os(iOS)
- case .downloads:
- return AnalyticsEvent.download(action: .remove, source: source, urn: nil)
-#endif
+ #if os(iOS)
+ case .downloads:
+ return AnalyticsEvent.download(action: .remove, source: source, urn: nil)
+ #endif
default:
return nil
}
}
-
+
var rowHighlight: Highlight? {
- return nil
+ nil
}
-
+
var placeholderRowItems: [Content.Item] {
switch configuredSection {
case .availableEpisodes, .history, .watchLater, .radioEpisodesForDay, .radioLatest, .radioLatestEpisodes, .radioLatestVideos,
- .radioMostPopular, .tvEpisodesForDay, .tvLiveCenterScheduledLivestreams, .tvLiveCenterScheduledLivestreamsAll,
- .tvLiveCenterEpisodes, .tvLiveCenterEpisodesAll, .tvScheduledLivestreams, .tvScheduledLivestreamsNews, .tvScheduledLivestreamsSport, .tvScheduledLivestreamsSignLanguage:
- return (0.. AnyPublisher<[Content.Item], Error> {
let dataProvider = SRGDataProvider.current!
-
+
let configuration = ApplicationConfiguration.shared
let vendor = configuration.vendor
-
+
switch configuredSection {
case let .availableEpisodes(show):
return dataProvider.latestMediasForShow(withUrn: show.urn, pageSize: pageSize, paginatedBy: paginator)
@@ -948,25 +969,25 @@ private extension Content {
return dataProvider.tvScheduledLivestreams(for: vendor, signLanguageOnly: true, pageSize: pageSize, paginatedBy: paginator)
.map { $0.map { .media($0) } }
.eraseToAnyPublisher()
-#if os(iOS)
- case .downloads:
- return Just(Download.downloads)
- .map { $0.map { .download($0) } }
- .setFailureType(to: Error.self)
- .eraseToAnyPublisher()
- case .notifications:
- return Just(UserNotification.notifications)
- .map { $0.map { .notification($0) } }
- .setFailureType(to: Error.self)
- .eraseToAnyPublisher()
- case let .radioShowAccess(channelUid):
- return Just([.showAccess(radioChannel: configuration.radioChannel(forUid: channelUid))])
- .setFailureType(to: Error.self)
- .eraseToAnyPublisher()
-#endif
+ #if os(iOS)
+ case .downloads:
+ return Just(Download.downloads)
+ .map { $0.map { .download($0) } }
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
+ case .notifications:
+ return Just(UserNotification.notifications)
+ .map { $0.map { .notification($0) } }
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
+ case let .radioShowAccess(channelUid):
+ return Just([.showAccess(radioChannel: configuration.radioChannel(forUid: channelUid))])
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
+ #endif
}
}
-
+
func interactiveUpdatesPublisher() -> AnyPublisher<[Content.Item], Never> {
switch configuredSection {
case .favoriteShows, .radioFavoriteShows, .radioLatestEpisodesFromFavorites:
@@ -975,17 +996,17 @@ private extension Content {
return UserInteractionSignal.historyUpdates()
case .radioWatchLater, .watchLater:
return UserInteractionSignal.watchLaterUpdates()
-#if os(iOS)
- case .downloads:
- return UserInteractionSignal.downloadUpdates()
- case .notifications:
- return UserInteractionSignal.notificationUpdates()
-#endif
+ #if os(iOS)
+ case .downloads:
+ return UserInteractionSignal.downloadUpdates()
+ case .notifications:
+ return UserInteractionSignal.notificationUpdates()
+ #endif
default:
return Just([]).eraseToAnyPublisher()
}
}
-
+
func reloadSignal() -> AnyPublisher? {
switch configuredSection {
case .favoriteShows, .radioFavoriteShows, .radioLatestEpisodesFromFavorites:
@@ -994,17 +1015,17 @@ private extension Content {
return ThrottledSignal.watchLaterUpdates()
case .radioLive:
return ApplicationSignal.settingUpdates(at: \.PlaySRGSettingSelectedLivestreamURNForChannels)
-#if os(iOS)
- case .downloads:
- return ThrottledSignal.downloadUpdates()
-#endif
+ #if os(iOS)
+ case .downloads:
+ return ThrottledSignal.downloadUpdates()
+ #endif
default:
// TODO: No history updates yet for battery consumption reasons. Fix when an efficient way to
// broadcast and apply history updates is available.
return nil
}
}
-
+
func remove(_ items: [Content.Item]) {
switch configuredSection {
case .favoriteShows, .radioFavoriteShows:
@@ -1013,12 +1034,12 @@ private extension Content {
Content.removeFromWatchLater(items)
case .history, .radioResumePlayback:
Content.removeFromHistory(items)
-#if os(iOS)
- case .downloads:
- Content.removeFromDownloads(items)
- case .notifications:
- Content.removeFromNotifications(items)
-#endif
+ #if os(iOS)
+ case .downloads:
+ Content.removeFromDownloads(items)
+ case .notifications:
+ Content.removeFromNotifications(items)
+ #endif
default:
break
}
@@ -1033,30 +1054,30 @@ private extension Content {
let shows = Content.shows(from: items)
FavoritesRemoveShows(shows)
}
-
+
static func removeFromHistory(_ items: [Content.Item]) {
let medias = Content.medias(from: items)
HistoryRemoveMedias(medias) { _ in }
}
-
-#if os(iOS)
- static func removeFromNotifications(_ items: [Content.Item]) {
- let notifications = Content.notifications(from: items)
- let updatedNotifications = Array(Set(UserNotification.notifications).subtracting(notifications))
- UserNotification.saveNotifications(updatedNotifications)
- UserInteractionEvent.removeFromNotifications(notifications)
- }
-#endif
-
+
+ #if os(iOS)
+ static func removeFromNotifications(_ items: [Content.Item]) {
+ let notifications = Content.notifications(from: items)
+ let updatedNotifications = Array(Set(UserNotification.notifications).subtracting(notifications))
+ UserNotification.saveNotifications(updatedNotifications)
+ UserInteractionEvent.removeFromNotifications(notifications)
+ }
+ #endif
+
static func removeFromWatchLater(_ items: [Content.Item]) {
let medias = Content.medias(from: items)
WatchLaterRemoveMedias(medias) { _ in }
}
-
-#if os(iOS)
- static func removeFromDownloads(_ items: [Content.Item]) {
- let downloads = Content.downloads(from: items)
- Download.removeDownloads(downloads)
- }
-#endif
+
+ #if os(iOS)
+ static func removeFromDownloads(_ items: [Content.Item]) {
+ let downloads = Content.downloads(from: items)
+ Download.removeDownloads(downloads)
+ }
+ #endif
}
diff --git a/Application/Sources/Content/PageViewController.swift b/Application/Sources/Content/PageViewController.swift
index d83fbaf1b..e82a44561 100644
--- a/Application/Sources/Content/PageViewController.swift
+++ b/Application/Sources/Content/PageViewController.swift
@@ -10,7 +10,7 @@ import SwiftUI
import UIKit
#if os(iOS)
-import GoogleCast
+ import GoogleCast
#endif
// MARK: View controller
@@ -18,32 +18,32 @@ import GoogleCast
final class PageViewController: UIViewController {
private let model: PageViewModel
private let fromPushNotification: Bool
-
+
private var cancellables = Set()
-
+
private var dataSource: UICollectionViewDiffableDataSource!
-
+
private weak var collectionView: UICollectionView!
private weak var emptyContentView: HostView!
private weak var topicGradientView: HostView!
private weak var topicGradientViewFixTopAnchor: NSLayoutConstraint!
private weak var topicGradientViewStickyTopAnchor: NSLayoutConstraint!
private weak var topicGradientViewHeightAnchor: NSLayoutConstraint!
-
-#if os(iOS)
- private weak var refreshControl: UIRefreshControl!
- private weak var googleCastButton: GoogleCastFloatingButton?
-
- private var isNavigationBarHidden: Bool {
- return model.isNavigationBarHidden && !UIAccessibility.isVoiceOverRunning
- }
-
- private var refreshTriggered = false
- private var headerWithTitleVisible = false
-#endif
-
+
+ #if os(iOS)
+ private weak var refreshControl: UIRefreshControl!
+ private weak var googleCastButton: GoogleCastFloatingButton?
+
+ private var isNavigationBarHidden: Bool {
+ model.isNavigationBarHidden && !UIAccessibility.isVoiceOverRunning
+ }
+
+ private var refreshTriggered = false
+ private var headerWithTitleVisible = false
+ #endif
+
private var analyticsPageViewTracked = false
-
+
private static func snapshot(from state: PageViewModel.State) -> NSDiffableDataSourceSnapshot {
var snapshot = NSDiffableDataSourceSnapshot()
if case let .loaded(rows: rows, _) = state {
@@ -54,55 +54,53 @@ final class PageViewController: UIViewController {
}
return snapshot
}
-
-#if os(iOS)
- private static func showByDateViewController(radioChannel: RadioChannel?, date: Date?) -> UIViewController {
- if let radioChannel {
- return CalendarViewController(radioChannel: radioChannel, date: date)
- }
- else if !ApplicationConfiguration.shared.isTvGuideUnavailable {
- return ProgramGuideViewController(date: date)
- }
- else {
- return CalendarViewController(radioChannel: nil, date: date)
+
+ #if os(iOS)
+ private static func showByDateViewController(radioChannel: RadioChannel?, date: Date?) -> UIViewController {
+ if let radioChannel {
+ CalendarViewController(radioChannel: radioChannel, date: date)
+ } else if !ApplicationConfiguration.shared.isTvGuideUnavailable {
+ ProgramGuideViewController(date: date)
+ } else {
+ CalendarViewController(radioChannel: nil, date: date)
+ }
}
- }
-#endif
-
+ #endif
+
init(id: PageViewModel.Id, fromPushNotification: Bool = false) {
model = PageViewModel(id: id)
self.fromPushNotification = fromPushNotification
super.init(nibName: nil, bundle: nil)
title = id.title
}
-
- required init?(coder: NSCoder) {
+
+ @available(*, unavailable)
+ required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
-
+
var displayedShow: SRGShow? {
- return model.displayedShow
+ model.displayedShow
}
-
+
@objc var radioChannel: RadioChannel? {
if case let .audio(channel: channel) = model.id {
- return channel
- }
- else {
- return nil
+ channel
+ } else {
+ nil
}
}
-
+
override func loadView() {
let view = UIView(frame: UIScreen.main.bounds)
view.backgroundColor = .srgGray16
-
+
let collectionView = CollectionView(frame: .zero, collectionViewLayout: layout(for: model))
collectionView.delegate = self
collectionView.backgroundColor = .clear
view.addSubview(collectionView)
self.collectionView = collectionView
-
+
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
@@ -110,20 +108,20 @@ final class PageViewController: UIViewController {
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
-
-#if os(iOS)
- if #available(iOS 17.0, *) {
- collectionView.registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (collectionView: CollectionView, _) in
- collectionView.collectionViewLayout.invalidateLayout()
-
- self.updateLayoutConfiguration()
- self.updateTopicGradientLayout()
+
+ #if os(iOS)
+ if #available(iOS 17.0, *) {
+ collectionView.registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (collectionView: CollectionView, _) in
+ collectionView.collectionViewLayout.invalidateLayout()
+
+ self.updateLayoutConfiguration()
+ self.updateTopicGradientLayout()
+ }
}
- }
-#endif
+ #endif
let backgroundView = UIView(frame: .zero)
collectionView.backgroundView = backgroundView
-
+
backgroundView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
@@ -131,17 +129,17 @@ final class PageViewController: UIViewController {
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
-
+
let topicGradientView = HostView(frame: .zero)
backgroundView.addSubview(topicGradientView)
self.topicGradientView = topicGradientView
-
+
topicGradientView.translatesAutoresizingMaskIntoConstraints = false
- let topicGradientViewFixTopAnchor = topicGradientView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 0 /* set in updateTopicGradientLayout() */)
+ let topicGradientViewFixTopAnchor = topicGradientView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 0 /* set in updateTopicGradientLayout() */ )
topicGradientViewFixTopAnchor.priority = .defaultHigh
- let topicGradientViewStickyTopAnchor = topicGradientView.topAnchor.constraint(equalTo: collectionView.topAnchor, constant: 0 /* set in updateTopicGradientLayout() */)
+ let topicGradientViewStickyTopAnchor = topicGradientView.topAnchor.constraint(equalTo: collectionView.topAnchor, constant: 0 /* set in updateTopicGradientLayout() */ )
topicGradientViewStickyTopAnchor.priority = .defaultLow
- let topicGradientViewHeightAnchor = topicGradientView.heightAnchor.constraint(equalToConstant: 0 /* set in updateTopicGradientLayout() */)
+ let topicGradientViewHeightAnchor = topicGradientView.heightAnchor.constraint(equalToConstant: 0 /* set in updateTopicGradientLayout() */ )
NSLayoutConstraint.activate([
topicGradientViewFixTopAnchor,
topicGradientViewStickyTopAnchor,
@@ -152,11 +150,11 @@ final class PageViewController: UIViewController {
self.topicGradientViewFixTopAnchor = topicGradientViewFixTopAnchor
self.topicGradientViewStickyTopAnchor = topicGradientViewStickyTopAnchor
self.topicGradientViewHeightAnchor = topicGradientViewHeightAnchor
-
+
let emptyContentView = HostView(frame: .zero)
backgroundView.addSubview(emptyContentView)
self.emptyContentView = emptyContentView
-
+
emptyContentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
emptyContentView.topAnchor.constraint(equalTo: backgroundView.topAnchor),
@@ -164,70 +162,68 @@ final class PageViewController: UIViewController {
emptyContentView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
emptyContentView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor)
])
-
-#if os(tvOS)
- tabBarObservedScrollView = collectionView
-#else
- let refreshControl = RefreshControl()
- refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
- collectionView.insertSubview(refreshControl, at: 0)
- self.refreshControl = refreshControl
-#endif
+
+ #if os(tvOS)
+ tabBarObservedScrollView = collectionView
+ #else
+ let refreshControl = RefreshControl()
+ refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
+ collectionView.insertSubview(refreshControl, at: 0)
+ self.refreshControl = refreshControl
+ #endif
self.view = view
}
-
+
override func viewDidLoad() {
super.viewDidLoad()
-
-#if os(iOS)
- navigationItem.largeTitleDisplayMode = model.isLargeTitleDisplayMode ? .always : .never
- headerWithTitleVisible = model.isHeaderWithTitle
-#endif
-
+
+ #if os(iOS)
+ navigationItem.largeTitleDisplayMode = model.isLargeTitleDisplayMode ? .always : .never
+ headerWithTitleVisible = model.isHeaderWithTitle
+ #endif
+
let cellRegistration = UICollectionView.CellRegistration, PageViewModel.Item> { [model] cell, _, item in
cell.content = ItemCell(item: item, id: model.id, primaryColor: model.primaryColor, secondaryColor: model.secondaryColor)
}
-
+
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in
- return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
+ collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
-
+
let titleHeaderViewRegistration = UICollectionView.SupplementaryRegistration>(elementKind: Header.titleHeader.rawValue) { [weak self] view, _, _ in
guard let self else { return }
view.content = TitleHeaderView(model.displayedTitle, description: model.displayedTitleDescription, titleTextAlignment: model.displayedTitleTextAlignment, topPadding: Self.layoutDisplayedTitleTopPadding(model.displayedTitleNeedsTopPadding)).primaryColor(model.primaryColor)
}
-
+
let showHeaderViewRegistration = UICollectionView.SupplementaryRegistration>(elementKind: Header.showHeader.rawValue) { [weak self] view, _, _ in
guard let self else { return }
view.content = ShowHeaderView(model.displayedShow, horizontalPadding: Self.layoutHorizontalMargin).primaryColor(model.primaryColor)
}
-
+
let sectionHeaderViewRegistration = UICollectionView.SupplementaryRegistration>(elementKind: UICollectionView.elementKindSectionHeader) { [weak self] view, _, indexPath in
guard let self else { return }
let snapshot = dataSource.snapshot()
let section = snapshot.sectionIdentifiers[indexPath.section]
view.content = SectionHeaderView(section: section, pageId: model.id).primaryColor(model.primaryColor)
}
-
+
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
if kind == Header.titleHeader.rawValue {
- return collectionView.dequeueConfiguredReusableSupplementary(using: titleHeaderViewRegistration, for: indexPath)
- }
- else if kind == Header.showHeader.rawValue {
- return collectionView.dequeueConfiguredReusableSupplementary(using: showHeaderViewRegistration, for: indexPath)
- }
- else {
- return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderViewRegistration, for: indexPath)
+ collectionView.dequeueConfiguredReusableSupplementary(using: titleHeaderViewRegistration, for: indexPath)
+ } else if kind == Header.showHeader.rawValue {
+ collectionView.dequeueConfiguredReusableSupplementary(using: showHeaderViewRegistration, for: indexPath)
+ } else {
+ collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderViewRegistration, for: indexPath)
}
}
-
+
model.$state
.sink { [weak self] state in
self?.trackPageView(state: state)
self?.reloadData(for: state)
}
.store(in: &cancellables)
-
+
model.$displayedShow
.dropFirst()
.sink { [weak self] _ in
@@ -240,63 +236,63 @@ final class PageViewController: UIViewController {
}
}
.store(in: &cancellables)
-
-#if os(iOS)
- model.$serviceMessage
- .sink { serviceMessage in
- guard let serviceMessage else { return }
- Banner.show(with: .error, message: serviceMessage.text, image: nil, sticky: true)
- }
- .store(in: &cancellables)
-
- NotificationCenter.default.weakPublisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
- .sink { [weak self] _ in
- guard let self, play_isViewCurrent else { return }
- updateNavigationBar(animated: true)
- }
- .store(in: &cancellables)
-#endif
+
+ #if os(iOS)
+ model.$serviceMessage
+ .sink { serviceMessage in
+ guard let serviceMessage else { return }
+ Banner.show(with: .error, message: serviceMessage.text, image: nil, sticky: true)
+ }
+ .store(in: &cancellables)
+
+ NotificationCenter.default.weakPublisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
+ .sink { [weak self] _ in
+ guard let self, play_isViewCurrent else { return }
+ updateNavigationBar(animated: true)
+ }
+ .store(in: &cancellables)
+ #endif
}
-
+
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
-
+
updateLayoutConfiguration()
updateTopicGradientLayout()
model.reload()
deselectItems(in: collectionView, animated: animated)
-#if os(iOS)
- updateNavigationBar(animated: animated)
-#endif
+ #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()
updateTopicGradientLayout()
}
-
+
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 {
+ if let collectionViewLayout = 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, +)
+ let supplementaryItemsHeight = configuration.boundarySupplementaryItems.map(\.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:
@@ -306,97 +302,93 @@ final class PageViewController: UIViewController {
case let .loaded(rows: rows, _):
emptyContentView.content = rows.isEmpty ? EmptyContentView(state: .empty(type: .generic), insets: emptyViewEdgeInsets()) : nil
}
-
+
if let topic = model.displayedGradientTopic, let style = model.displayedGradientTopicStyle {
- self.topicGradientView.content = TopicGradientView(topic, style: style)
- }
- else {
- self.topicGradientView.content = nil
+ topicGradientView.content = TopicGradientView(topic, style: style)
+ } else {
+ topicGradientView.content = nil
}
-
+
DispatchQueue.global(qos: .userInteractive).async {
// Can be triggered on a background thread. Layout is updated on the main thread.
self.dataSource.apply(Self.snapshot(from: state)) {
-#if os(iOS)
- // Avoid stopping scrolling
- // See http://stackoverflow.com/a/31681037/760435
- if self.refreshControl.isRefreshing {
- self.refreshControl.endRefreshing()
- }
-#endif
+ #if os(iOS)
+ // Avoid stopping scrolling
+ // See http://stackoverflow.com/a/31681037/760435
+ if self.refreshControl.isRefreshing {
+ self.refreshControl.endRefreshing()
+ }
+ #endif
}
}
}
-
-#if os(iOS)
- private func updateNavigationBar(animated: Bool) {
- if model.id.supportsCastButton {
- if !isNavigationBarHidden, let navigationBar = navigationController?.navigationBar {
- self.googleCastButton?.removeFromSuperview()
- navigationItem.rightBarButtonItem = GoogleCastBarButtonItem(for: navigationBar)
+
+ #if os(iOS)
+ private func updateNavigationBar(animated: Bool) {
+ if model.id.supportsCastButton {
+ if !isNavigationBarHidden, let navigationBar = navigationController?.navigationBar {
+ googleCastButton?.removeFromSuperview()
+ navigationItem.rightBarButtonItem = GoogleCastBarButtonItem(for: navigationBar)
+ } else if googleCastButton == nil {
+ let googleCastButton = GoogleCastFloatingButton(frame: .zero)
+ view.addSubview(googleCastButton)
+ self.googleCastButton = googleCastButton
+
+ // Place the button where it would appear if a navigation bar was available. An offset is needed on iPads for a perfect
+ // result (might be fragile but should be enough).
+ let topOffset: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad) ? 3 : 0
+ NSLayoutConstraint.activate([
+ googleCastButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: topOffset),
+ googleCastButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor)
+ ])
+ }
+ } else {
+ googleCastButton?.removeFromSuperview()
}
- else if self.googleCastButton == nil {
- let googleCastButton = GoogleCastFloatingButton(frame: .zero)
- view.addSubview(googleCastButton)
- self.googleCastButton = googleCastButton
-
- // Place the button where it would appear if a navigation bar was available. An offset is needed on iPads for a perfect
- // result (might be fragile but should be enough).
- let topOffset: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad) ? 3 : 0
- NSLayoutConstraint.activate([
- googleCastButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: topOffset),
- googleCastButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor)
- ])
+
+ navigationItem.title = !headerWithTitleVisible ? 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(shareContent(_:)))
+ shareButtonItem.accessibilityLabel = PlaySRGAccessibilityLocalizedString("Share", comment: "Share button label on content page view")
+ navigationItem.rightBarButtonItem = shareButtonItem
+ } else {
+ navigationItem.rightBarButtonItem = nil
}
}
- else {
- self.googleCastButton?.removeFromSuperview()
- }
-
- navigationItem.title = !headerWithTitleVisible ? 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) {
+ if refreshControl.isRefreshing {
+ refreshControl.endRefreshing()
+ }
+ refreshTriggered = true
}
- }
-
- @objc private func pullToRefresh(_ refreshControl: RefreshControl) {
- if refreshControl.isRefreshing {
- refreshControl.endRefreshing()
+
+ @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
+
+ present(activityViewController, animated: true, completion: nil)
}
- 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
-
+ #endif
+
private func trackPageView(state: PageViewModel.State) {
switch state {
case .loading:
break
case let .failed(_, pageUid), let .loaded(_, pageUid):
- guard !self.analyticsPageViewTracked else { return }
- self.analyticsPageViewTracked = true
-
+ guard !analyticsPageViewTracked else { return }
+ analyticsPageViewTracked = true
+
SRGAnalyticsTracker.shared.trackPageView(withTitle: model.id.analyticsPageViewTitle,
type: model.id.analyticsPageViewType,
levels: model.id.analyticsPageViewLevels,
@@ -413,39 +405,43 @@ private extension PageViewController {
case titleHeader
case showHeader
}
-
-#if os(iOS)
- private typealias CollectionView = DampedCollectionView
-#else
- private typealias CollectionView = UICollectionView
-#endif
+
+ #if os(iOS)
+ private typealias CollectionView = DampedCollectionView
+ #else
+ private typealias CollectionView = UICollectionView
+ #endif
}
// MARK: Objective-C API
extension PageViewController {
@objc static func videosViewController() -> PageViewController {
- return PageViewController(id: .video)
+ PageViewController(id: .video)
}
-
+
+ @objc static func audiosViewController() -> PageViewController {
+ PageViewController(id: .audio(channel: nil))
+ }
+
@objc static func audiosViewController(forRadioChannel channel: RadioChannel) -> PageViewController {
- return PageViewController(id: .audio(channel: channel))
+ PageViewController(id: .audio(channel: channel))
}
-
+
@objc static func liveViewController() -> PageViewController {
- return PageViewController(id: .live)
+ PageViewController(id: .live)
}
-
+
@objc static func topicViewController(for topic: SRGTopic) -> PageViewController {
- return PageViewController(id: .topic(topic))
+ PageViewController(id: .topic(topic))
}
-
+
@objc static func showViewController(for show: SRGShow, fromPushNotification: Bool = false) -> PageViewController {
- return PageViewController(id: .show(show), fromPushNotification: fromPushNotification)
+ PageViewController(id: .show(show), fromPushNotification: fromPushNotification)
}
-
+
@objc static func pageViewController(for page: SRGContentPage) -> PageViewController {
- return PageViewController(id: .page(page))
+ PageViewController(id: .page(page))
}
}
@@ -453,279 +449,280 @@ extension PageViewController {
extension PageViewController: ContentInsets {
var play_contentScrollViews: [UIScrollView]? {
- return collectionView != nil ? [collectionView] : nil
+ collectionView != nil ? [collectionView] : nil
}
-
+
var play_paddingContentInsets: UIEdgeInsets {
-#if os(iOS)
- let top = (isNavigationBarHidden || model.isHeaderWithTitle) ? 0 : Self.layoutVerticalMargin
-#else
- let top = Self.layoutVerticalMargin
-#endif
+ #if os(iOS)
+ let top = (isNavigationBarHidden || model.isHeaderWithTitle) ? 0 : Self.layoutVerticalMargin
+ #else
+ let top = Self.layoutVerticalMargin
+ #endif
return UIEdgeInsets(top: top, left: 0, bottom: Self.layoutVerticalMargin, right: 0)
}
}
#if os(iOS)
-extension PageViewController: Oriented {
-}
+ extension PageViewController: Oriented {}
#endif
extension PageViewController: ScrollableContent {
var play_scrollableView: UIScrollView? {
- return collectionView
+ collectionView
}
}
extension PageViewController: UICollectionViewDelegate {
-#if os(iOS)
- func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
- let snapshot = dataSource.snapshot()
- let section = snapshot.sectionIdentifiers[indexPath.section]
- let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
-
- switch item.wrappedValue {
- case let .item(wrappedItem):
- switch wrappedItem {
- case let .media(media):
- play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil)
- case let .show(show):
- if let navigationController {
- let pageViewController = PageViewController(id: .show(show))
- navigationController.pushViewController(pageViewController, animated: true)
- }
- case let .topic(topic):
- if let navigationController {
- let pageViewController = PageViewController(id: .topic(topic))
- navigationController.pushViewController(pageViewController, animated: true)
- }
- case let .highlight(_, highlightedItem):
- if let navigationController {
- if case let .show(show) = highlightedItem {
+ #if os(iOS)
+ func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+ let snapshot = dataSource.snapshot()
+ let section = snapshot.sectionIdentifiers[indexPath.section]
+ let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
+
+ switch item.wrappedValue {
+ case let .item(wrappedItem):
+ switch wrappedItem {
+ case let .media(media):
+ play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil)
+ case let .show(show):
+ if let navigationController {
let pageViewController = PageViewController(id: .show(show))
navigationController.pushViewController(pageViewController, animated: true)
}
- else {
- let sectionViewController = SectionViewController(section: section.wrappedValue, filter: model.id)
- navigationController.pushViewController(sectionViewController, animated: true)
+ case let .topic(topic):
+ if let navigationController {
+ let pageViewController = PageViewController(id: .topic(topic))
+ navigationController.pushViewController(pageViewController, animated: true)
+ }
+ case let .highlight(_, highlightedItem):
+ if let navigationController {
+ if case let .show(show) = highlightedItem {
+ let pageViewController = PageViewController(id: .show(show))
+ navigationController.pushViewController(pageViewController, animated: true)
+ } else if case let .media(media) = highlightedItem {
+ play_presentMediaPlayer(with: media, position: nil, airPlaySuggestions: true, fromPushNotification: false, animated: true, completion: nil)
+ } else {
+ let sectionViewController = SectionViewController(section: section.wrappedValue, filter: model.id)
+ navigationController.pushViewController(sectionViewController, animated: true)
+ }
}
+ default:
+ ()
}
+ case .more:
+ if let navigationController {
+ let sectionViewController = SectionViewController(section: section.wrappedValue, filter: model.id)
+ navigationController.pushViewController(sectionViewController, animated: true)
+ }
+ }
+ }
+
+ func collectionView(_: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point _: CGPoint) -> UIContextMenuConfiguration? {
+ let snapshot = dataSource.snapshot()
+ let section = snapshot.sectionIdentifiers[indexPath.section]
+ let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
+
+ switch item.wrappedValue {
+ case let .item(wrappedItem):
+ return ContextMenu.configuration(for: wrappedItem, at: indexPath, in: self)
default:
- ()
+ return nil
}
- case .more:
- if let navigationController {
- let sectionViewController = SectionViewController(section: section.wrappedValue, filter: model.id)
- navigationController.pushViewController(sectionViewController, animated: true)
+ }
+
+ func collectionView(_: UICollectionView, willPerformPreviewActionForMenuWith _: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
+ ContextMenu.commitPreview(in: self, animator: animator)
+ }
+
+ func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
+ preview(for: configuration, in: collectionView)
+ }
+
+ func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
+ preview(for: configuration, in: collectionView)
+ }
+
+ func collectionView(_: UICollectionView, willDisplaySupplementaryView _: UICollectionReusableView, forElementKind elementKind: String, at _: IndexPath) {
+ switch elementKind {
+ case Header.showHeader.rawValue, Header.titleHeader.rawValue:
+ headerWithTitleVisible = true
+ updateNavigationBar(animated: true)
+ default:
+ break
}
}
- }
-
- func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
- let snapshot = dataSource.snapshot()
- let section = snapshot.sectionIdentifiers[indexPath.section]
- let item = snapshot.itemIdentifiers(inSection: section)[indexPath.row]
-
- switch item.wrappedValue {
- case let .item(wrappedItem):
- return ContextMenu.configuration(for: wrappedItem, at: indexPath, in: self)
- default:
- return nil
+
+ func collectionView(_: UICollectionView, didEndDisplayingSupplementaryView _: UICollectionReusableView, forElementOfKind elementKind: String, at _: IndexPath) {
+ switch elementKind {
+ case Header.showHeader.rawValue, Header.titleHeader.rawValue:
+ headerWithTitleVisible = false
+ updateNavigationBar(animated: true)
+ default:
+ break
+ }
}
- }
-
- func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
- ContextMenu.commitPreview(in: self, animator: animator)
- }
-
- func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
- return preview(for: configuration, in: collectionView)
- }
-
- func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
- 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, Header.titleHeader.rawValue:
- headerWithTitleVisible = true
- 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()
+ parameters.backgroundColor = view.backgroundColor
+ return UITargetedPreview(view: interactionView, parameters: parameters)
}
- }
-
- func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {
- switch elementKind {
- case Header.showHeader.rawValue, Header.titleHeader.rawValue:
- headerWithTitleVisible = false
- updateNavigationBar(animated: true)
- default:
- break
+ #endif
+
+ #if os(tvOS)
+ func collectionView(_: UICollectionView, canFocusItemAt _: IndexPath) -> Bool {
+ false
}
- }
-
- 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()
- parameters.backgroundColor = view.backgroundColor
- return UITargetedPreview(view: interactionView, parameters: parameters)
- }
-#endif
-
-#if os(tvOS)
- func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
- return false
- }
-#endif
+ #endif
}
extension PageViewController: UIScrollViewDelegate {
-#if os(iOS)
- func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
- // Avoid the collection jumping when pulling to refresh. Only mark the refresh as being triggered.
- if refreshTriggered {
- model.reload(deep: true)
- refreshTriggered = false
+ #if os(iOS)
+ func scrollViewDidEndDecelerating(_: UIScrollView) {
+ // Avoid the collection jumping when pulling to refresh. Only mark the refresh as being triggered.
+ if refreshTriggered {
+ model.reload(deep: true)
+ refreshTriggered = false
+ }
}
- }
-
- // The system default behavior does not lead to correct results when large titles are displayed. Override.
- func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
- scrollView.play_scrollToTop(animated: true)
- return false
- }
-#endif
-
+
+ // The system default behavior does not lead to correct results when large titles are displayed. Override.
+ func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
+ scrollView.play_scrollToTop(animated: true)
+ return false
+ }
+ #endif
+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentSize.height > 0 else { return }
-
+
let numberOfScreens = 4
if scrollView.contentOffset.y > scrollView.contentSize.height - CGFloat(numberOfScreens) * scrollView.frame.height {
model.loadMore()
}
-
-#if os(iOS)
- if isShowHeaderVerticalLayout {
- topicGradientViewStickyTopAnchor.constant = showPageStickyTopAnchorConstant
- }
-#endif
+
+ #if os(iOS)
+ if isShowHeaderVerticalLayout {
+ topicGradientViewStickyTopAnchor.constant = showPageStickyTopAnchorConstant
+ }
+ #endif
}
}
#if os(iOS)
-extension PageViewController: PlayApplicationNavigation {
- func open(_ applicationSectionInfo: ApplicationSectionInfo) -> Bool {
- guard radioChannel === applicationSectionInfo.radioChannel || radioChannel == applicationSectionInfo.radioChannel else { return false }
-
- switch applicationSectionInfo.applicationSection {
- case .showByDate:
- let date = applicationSectionInfo.options?[ApplicationSectionOptionKey.showByDateDateKey] as? Date
- if let navigationController {
- let showByDateViewController = Self.showByDateViewController(radioChannel: radioChannel, date: date)
- navigationController.pushViewController(showByDateViewController, animated: false)
- }
- return true
- case .showAZ:
- if let navigationController {
- let initialSectionId = applicationSectionInfo.options?[ApplicationSectionOptionKey.showAZIndexKey] as? String
- let showsViewController = SectionViewController.showsViewController(forChannelUid: radioChannel?.uid, initialSectionId: initialSectionId)
- navigationController.pushViewController(showsViewController, animated: false)
- }
- return true
- default:
- switch self.model.id {
- case .live:
- return applicationSectionInfo.applicationSection == .live
+ extension PageViewController: PlayApplicationNavigation {
+ func open(_ applicationSectionInfo: ApplicationSectionInfo) -> Bool {
+ guard radioChannel === applicationSectionInfo.radioChannel || radioChannel == applicationSectionInfo.radioChannel else { return false }
+
+ switch applicationSectionInfo.applicationSection {
+ case .showByDate:
+ let date = applicationSectionInfo.options?[ApplicationSectionOptionKey.showByDateDateKey] as? Date
+ if let navigationController {
+ let showByDateViewController = Self.showByDateViewController(radioChannel: radioChannel, date: date)
+ navigationController.pushViewController(showByDateViewController, animated: false)
+ }
+ return true
+ case .showAZ:
+ if let navigationController {
+ let initialSectionId = applicationSectionInfo.options?[ApplicationSectionOptionKey.showAZIndexKey] as? String
+ let showsViewController = SectionViewController.showsViewController(forChannelUid: radioChannel?.uid, initialSectionId: initialSectionId)
+ navigationController.pushViewController(showsViewController, animated: false)
+ }
+ return true
default:
- return applicationSectionInfo.applicationSection == .overview
+ switch model.id {
+ case .live:
+ return applicationSectionInfo.applicationSection == .live
+ default:
+ return applicationSectionInfo.applicationSection == .overview
+ }
}
}
}
-}
-extension PageViewController: ShowAccessCellActions {
- func openShowAZ() {
- if let navigationController {
- let showsViewController = SectionViewController.showsViewController(forChannelUid: radioChannel?.uid)
- navigationController.pushViewController(showsViewController, animated: true)
- }
- }
-
- func openShowByDate() {
- if let navigationController {
- let showByDateViewController = Self.showByDateViewController(radioChannel: radioChannel, date: nil)
- navigationController.pushViewController(showByDateViewController, animated: true)
+ extension PageViewController: ShowAccessCellActions {
+ func openShowAZ() {
+ if let navigationController {
+ let showsViewController = SectionViewController.showsViewController(forChannelUid: radioChannel?.uid)
+ navigationController.pushViewController(showsViewController, animated: true)
+ }
}
- }
-}
-extension PageViewController: SectionHeaderViewAction {
- fileprivate func openSection(sender: Any?, event: OpenSectionEvent?) {
- if let event {
- if let microPageId = event.section.wrappedValue.properties.openContentPageId {
- openContentPage(id: microPageId)
- } else {
- openSectionPage(section: event.section, filter: model.id)
+ func openShowByDate() {
+ if let navigationController {
+ let showByDateViewController = Self.showByDateViewController(radioChannel: radioChannel, date: nil)
+ navigationController.pushViewController(showByDateViewController, animated: true)
}
}
}
-
- private func openSectionPage(section: PageViewModel.Section, filter: PageViewModel.Id) {
- guard let navigationController else { return }
-
- let sectionViewController = SectionViewController(section: section.wrappedValue, filter: filter)
- navigationController.pushViewController(sectionViewController, animated: true)
- }
-
- private func openContentPage(id: String) {
- guard let navigationController else { return }
-
- SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, uid: id)
- .receive(on: DispatchQueue.main)
- .sink { result in
- if case .failure = result {
- let error = NSError(
- domain: PlayErrorDomain,
- code: PlayErrorCode.notFound.rawValue,
- userInfo: [
- NSLocalizedDescriptionKey: NSLocalizedString("The page cannot be opened.", comment: "Error message when a page cannot be opened from a page section title")
- ])
- Banner.showError(error)
+
+ extension PageViewController: SectionHeaderViewAction {
+ fileprivate func openSection(sender _: Any?, event: OpenSectionEvent?) {
+ if let event {
+ if let microPageId = event.section.wrappedValue.properties.openContentPageId {
+ openContentPage(id: microPageId)
+ } else {
+ openSectionPage(section: event.section, filter: model.id)
}
- } receiveValue: { contentPage in
- let pageViewController = PageViewController.pageViewController(for: contentPage)
- navigationController.pushViewController(pageViewController, animated: true)
}
- .store(in: &cancellables)
+ }
+
+ private func openSectionPage(section: PageViewModel.Section, filter: PageViewModel.Id) {
+ guard let navigationController else { return }
+
+ let sectionViewController = SectionViewController(section: section.wrappedValue, filter: filter)
+ navigationController.pushViewController(sectionViewController, animated: true)
+ }
+
+ private func openContentPage(id: String) {
+ guard let navigationController else { return }
+
+ SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, uid: id)
+ .receive(on: DispatchQueue.main)
+ .sink { result in
+ if case .failure = result {
+ let error = NSError(
+ domain: PlayErrorDomain,
+ code: PlayErrorCode.notFound.rawValue,
+ userInfo: [
+ NSLocalizedDescriptionKey: NSLocalizedString("The page cannot be opened.", comment: "Error message when a page cannot be opened from a page section title")
+ ]
+ )
+ Banner.showError(error)
+ }
+ } receiveValue: { contentPage in
+ let pageViewController = PageViewController.pageViewController(for: contentPage)
+ navigationController.pushViewController(pageViewController, animated: true)
+ }
+ .store(in: &cancellables)
+ }
}
-}
-extension PageViewController: TabBarActionable {
- func performActiveTabAction(animated: Bool) {
- collectionView?.play_scrollToTop(animated: animated)
+ extension PageViewController: TabBarActionable {
+ func performActiveTabAction(animated: Bool) {
+ collectionView?.play_scrollToTop(animated: animated)
+ }
}
-}
#endif
extension PageViewController: ShowHeaderViewAction {
- func showMore(sender: Any?, event: ShowMoreEvent?) {
+ 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()]
+
+ #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
+ present(sheetTextViewController, animated: true, completion: nil)
+ #else
+ navigateToText(event.content)
+ #endif
}
}
@@ -737,198 +734,194 @@ private extension PageViewController {
private static let layoutVerticalMargin: CGFloat = constant(iOS: 8, tvOS: 0)
private static let layoutHorizontalConfigurationViewMargin: CGFloat = constant(iOS: 0, tvOS: 8)
private static let layoutTopicGradientViewHeight: CGFloat = 572
-
+
private static func layoutDisplayedTitleTopPadding(_ required: Bool) -> CGFloat {
- return required ? layoutVerticalMargin * 2 : 0
+ required ? layoutVerticalMargin * 2 : 0
}
-
+
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)
-
+
if let title = model.displayedTitle {
let titleHeaderSize = TitleHeaderViewSize.recommended(for: title, description: model.displayedTitleDescription,
topPadding: layoutDisplayedTitleTopPadding(model.displayedTitleNeedsTopPadding), layoutWidth: layoutWidth - layoutHorizontalConfigurationViewMargin * 2, horizontalSizeClass: horizontalSizeClass)
- configuration.boundarySupplementaryItems = [ NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleHeaderSize, elementKind: Header.titleHeader.rawValue, alignment: .topLeading, absoluteOffset: CGPoint(x: offsetX, y: 0)) ]
- }
- else if let show = model.displayedShow {
+ configuration.boundarySupplementaryItems = [NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleHeaderSize, elementKind: Header.titleHeader.rawValue, alignment: .topLeading, absoluteOffset: CGPoint(x: offsetX, y: 0))]
+ } else if let show = model.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)) ]
+ configuration.boundarySupplementaryItems = [NSCollectionLayoutBoundarySupplementaryItem(layoutSize: showHeaderSize, elementKind: Header.showHeader.rawValue, alignment: .topLeading, absoluteOffset: CGPoint(x: offsetX + layoutHorizontalConfigurationViewMargin, y: 0))]
}
-
+
return configuration
}
-
+
private func layout(for model: PageViewModel) -> UICollectionViewLayout {
- return UICollectionViewCompositionalLayout(sectionProvider: { [weak self] sectionIndex, layoutEnvironment in
+ UICollectionViewCompositionalLayout(sectionProvider: { [weak self] sectionIndex, layoutEnvironment in
let layoutWidth = layoutEnvironment.container.effectiveContentSize.width
let horizontalSizeClass = layoutEnvironment.traitCollection.horizontalSizeClass
-
- func sectionSupplementaryItems(for section: PageViewModel.Section, horizontalMargin: CGFloat) -> [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
+ #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 {
switch section.viewModelProperties.layout {
case .heroStage:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in
- return HeroMediaCellSize.recommended(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
+ HeroMediaCellSize.recommended(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
}
layoutSection.orthogonalScrollingBehavior = .groupPaging
return layoutSection
case .highlight:
return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in
- return HighlightCellSize.fullWidth(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
+ HighlightCellSize.fullWidth(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
}
case .headline:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in
- return FeaturedContentCellSize.headline(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
+ FeaturedContentCellSize.headline(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
}
layoutSection.orthogonalScrollingBehavior = .groupPaging
return layoutSection
case .element:
return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in
- return FeaturedContentCellSize.element(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
+ FeaturedContentCellSize.element(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
}
case .elementSwimlane:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, _ in
- return FeaturedContentCellSize.element(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
+ FeaturedContentCellSize.element(layoutWidth: layoutWidth, horizontalSizeClass: horizontalSizeClass)
}
layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return layoutSection
case .mediaSwimlane:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
- return MediaCellSize.swimlane()
+ MediaCellSize.swimlane()
}
layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return layoutSection
case .liveMediaSwimlane:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
- return LiveMediaCellSize.swimlane()
+ LiveMediaCellSize.swimlane()
}
layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return layoutSection
case .showSwimlane:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
- return ShowCellSize.swimlane(for: section.properties.imageVariant)
+ ShowCellSize.swimlane(for: section.properties.imageVariant)
}
layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return layoutSection
case .topicSelector:
let layoutSection = NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
- return TopicCellSize.swimlane()
+ TopicCellSize.swimlane()
}
layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return layoutSection
case .mediaGrid:
if horizontalSizeClass == .compact {
return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
- return MediaCellSize.fullWidth()
+ MediaCellSize.fullWidth()
}
- }
- else {
+ } else {
return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in
- return MediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing)
+ MediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing)
}
}
case .liveMediaGrid:
return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in
- return LiveMediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing)
+ LiveMediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing)
}
case .showGrid:
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)
+ ShowCellSize.grid(for: section.properties.imageVariant, layoutWidth: layoutWidth, spacing: spacing)
}
-#if os(iOS)
- case .showAccess:
- return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
- return ShowAccessCellSize.fullWidth()
- }
-#endif
+ #if os(iOS)
+ case .showAccess:
+ return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
+ 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
+ #if os(iOS)
+ return NSCollectionLayoutSection.horizontal(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { _, _ in
+ MediaCellSize.fullWidth(horizontalSizeClass: horizontalSizeClass)
+ }
+ #else
+ return NSCollectionLayoutSection.grid(layoutWidth: layoutWidth, horizontalMargin: horizontalMargin(for: section), spacing: Self.itemSpacing) { layoutWidth, spacing in
+ MediaCellSize.grid(layoutWidth: layoutWidth, spacing: spacing)
+ }
+ #endif
}
}
-
+
guard let self else { return nil }
-
- let snapshot = self.dataSource.snapshot()
+
+ let snapshot = dataSource.snapshot()
let section = snapshot.sectionIdentifiers[sectionIndex]
-
+
let layoutSection = layoutSection(for: section)
layoutSection.boundarySupplementaryItems = sectionSupplementaryItems(for: section, horizontalMargin: horizontalMargin(for: section))
return layoutSection
}, configuration: Self.layoutConfiguration(model: model, layoutWidth: 0, horizontalSizeClass: .unspecified, offsetX: 0))
}
-
+
private func updateTopicGradientLayout() {
let topScreenOffset = constant(iOS: collectionView.safeAreaInsets.top, tvOS: 0)
-
+
if case .show = model.id {
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, +)
+ let supplementaryItemsHeight = configuration.boundarySupplementaryItems.map(\.layoutSize.heightDimension.dimension).reduce(0, +)
let mediaCellHeight = MediaCellSize.height(horizontalSizeClass: traitCollection.horizontalSizeClass)
-
+
// Move the gradient view below the show image when displayed in compact horizontal size class
if isShowHeaderVerticalLayout {
topicGradientViewFixTopAnchor.priority = .defaultLow
topicGradientViewStickyTopAnchor.priority = .defaultHigh
-
+
topicGradientViewStickyTopAnchor.constant = showPageStickyTopAnchorConstant
let showImageOffset = view.safeAreaLayoutGuide.layoutFrame.width / ShowHeaderView.imageAspectRatio
topicGradientViewHeightAnchor.constant = supplementaryItemsHeight - showImageOffset + mediaCellHeight
- }
- else {
+ } else {
topicGradientViewStickyTopAnchor.priority = .defaultLow
topicGradientViewFixTopAnchor.priority = .defaultHigh
-
+
topicGradientViewFixTopAnchor.constant = topScreenOffset
topicGradientViewHeightAnchor.constant = supplementaryItemsHeight + mediaCellHeight
}
- }
- else {
+ } else {
topicGradientViewStickyTopAnchor.priority = .defaultLow
topicGradientViewFixTopAnchor.priority = .defaultHigh
-
+
topicGradientViewFixTopAnchor.constant = topScreenOffset
topicGradientViewHeightAnchor.constant = Self.layoutTopicGradientViewHeight
}
}
-
+
private var showPageStickyTopAnchorConstant: CGFloat {
let showImageOffset = view.safeAreaLayoutGuide.layoutFrame.width / ShowHeaderView.imageAspectRatio
let topScreenOffset = constant(iOS: collectionView.safeAreaInsets.top, tvOS: 0)
let offset = topScreenOffset + collectionView.contentOffset.y
return (offset < showImageOffset) ? showImageOffset : offset
}
-
+
private var isShowHeaderVerticalLayout: Bool {
guard case .show = model.id else { return false }
-
+
return ShowHeaderView.isVerticalLayout(
horizontalSizeClass: traitCollection.horizontalSizeClass,
isLandscape: UIApplication.shared.mainWindow?.isLandscape ?? false
@@ -944,7 +937,7 @@ private extension PageViewController {
let section: PageViewModel.Section
let primaryColor: Color
let secondaryColor: Color
-
+
var body: some View {
switch section.viewModelProperties.layout {
case .heroStage:
@@ -963,20 +956,20 @@ private extension PageViewController {
PlaySRG.MediaCell(media: media, style: haveSameShow(media: media, in: section) ? .date : .show, layout: .vertical).primaryColor(primaryColor).secondaryColor(secondaryColor)
}
}
-
+
private func haveSameShow(media: SRGMedia?, in section: PageViewModel.Section) -> Bool {
guard let displayedShow = section.properties.displayedShow, let mediaShow = media?.show else { return false }
-
+
return displayedShow.isEqual(mediaShow)
}
}
-
+
struct ShowCell: View {
let show: SRGShow?
let section: PageViewModel.Section
let primaryColor: Color
let secondaryColor: Color
-
+
var body: some View {
switch section.viewModelProperties.layout {
case .heroStage, .headline:
@@ -988,13 +981,13 @@ private extension PageViewController {
}
}
}
-
+
struct ItemCell: View {
let item: PageViewModel.Item
let id: PageViewModel.Id
let primaryColor: Color
let secondaryColor: Color
-
+
var body: some View {
switch item.wrappedValue {
case let .item(wrappedItem):
@@ -1011,20 +1004,20 @@ private extension PageViewController {
TopicCell(topic: nil)
case let .topic(topic):
TopicCell(topic: topic)
-#if os(iOS)
- case let .download(download):
- DownloadCell(download: download)
- case let .notification(notification):
- NotificationCell(notification: notification)
- case .showAccess:
- switch id {
- case .video:
- let style: ShowAccessCell.Style = !ApplicationConfiguration.shared.isTvGuideUnavailable ? .programGuide : .calendar
- ShowAccessCell(style: style).primaryColor(primaryColor)
- default:
- ShowAccessCell(style: .calendar).primaryColor(primaryColor)
- }
-#endif
+ #if os(iOS)
+ case let .download(download):
+ DownloadCell(download: download)
+ case let .notification(notification):
+ NotificationCell(notification: notification)
+ case .showAccess:
+ switch id {
+ case .video:
+ let style: ShowAccessCell.Style = !ApplicationConfiguration.shared.isTvGuideUnavailable ? .programGuide : .calendar
+ ShowAccessCell(style: style).primaryColor(primaryColor)
+ default:
+ ShowAccessCell(style: .calendar).primaryColor(primaryColor)
+ }
+ #endif
case .highlightPlaceholder:
HighlightCell(highlight: nil, section: item.section.wrappedValue, item: nil, filter: id)
case let .highlight(highlight, highlightedItem):
@@ -1047,12 +1040,12 @@ private extension PageViewController {
private class OpenSectionEvent: UIEvent {
let section: PageViewModel.Section
-
+
init(section: PageViewModel.Section) {
self.section = section
super.init()
}
-
+
override init() {
fatalError("init() is not available")
}
@@ -1062,54 +1055,53 @@ private extension PageViewController {
private struct SectionHeaderView: View, PrimaryColorSettable {
let section: PageViewModel.Section
let pageId: PageViewModel.Id
-
- internal var primaryColor: Color = .srgGrayD2
-
+
+ var primaryColor: Color = .srgGrayD2
+
@FirstResponder private var firstResponder
@AppStorage(PlaySRGSettingSectionWideSupportEnabled) var isSectionWideSupportEnabled = false
-
+
private static func title(for section: PageViewModel.Section) -> String? {
- return section.properties.title
+ section.properties.title
}
-
+
private static func subtitle(for section: PageViewModel.Section) -> String? {
- return section.properties.summary
+ section.properties.summary
}
-
+
private var hasDetailDisclosure: Bool {
- return section.viewModelProperties.canOpenPage || isSectionWideSupportEnabled
+ section.viewModelProperties.canOpenPage || isSectionWideSupportEnabled
}
-
+
var accessibilityLabel: String? {
- return Self.title(for: section)
+ Self.title(for: section)
}
-
+
var accessibilityHint: String? {
- return hasDetailDisclosure ? PlaySRGAccessibilityLocalizedString("Shows all contents.", comment: "Homepage header action hint") : nil
+ hasDetailDisclosure ? PlaySRGAccessibilityLocalizedString("Shows all contents.", comment: "Homepage header action hint") : nil
}
-
+
var body: some View {
if section.properties.displaysRowHeader, let title = Self.title(for: section) {
-#if os(tvOS)
- HeaderView(title: title, subtitle: Self.subtitle(for: section), hasDetailDisclosure: false, primaryColor: primaryColor)
-#else
- Button {
- firstResponder.sendAction(#selector(SectionHeaderViewAction.openSection(sender:event:)), for: OpenSectionEvent(section: section))
- } label: {
- HeaderView(title: title, subtitle: Self.subtitle(for: section), hasDetailDisclosure: hasDetailDisclosure, primaryColor: primaryColor)
- }
- .disabled(!hasDetailDisclosure)
- .responderChain(from: firstResponder)
-#endif
+ #if os(tvOS)
+ HeaderView(title: title, subtitle: Self.subtitle(for: section), hasDetailDisclosure: false, primaryColor: primaryColor)
+ #else
+ Button {
+ firstResponder.sendAction(#selector(SectionHeaderViewAction.openSection(sender:event:)), for: OpenSectionEvent(section: section))
+ } label: {
+ HeaderView(title: title, subtitle: Self.subtitle(for: section), hasDetailDisclosure: hasDetailDisclosure, primaryColor: primaryColor)
+ }
+ .disabled(!hasDetailDisclosure)
+ .responderChain(from: firstResponder)
+ #endif
}
}
-
+
static func size(section: PageViewModel.Section, layoutWidth: CGFloat) -> NSCollectionLayoutSize {
if section.properties.displaysRowHeader {
- return HeaderViewSize.recommended(forTitle: title(for: section), subtitle: subtitle(for: section), layoutWidth: layoutWidth)
- }
- else {
- return NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(LayoutHeaderHeightZero))
+ HeaderViewSize.recommended(forTitle: title(for: section), subtitle: subtitle(for: section), layoutWidth: layoutWidth)
+ } else {
+ NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(LayoutHeaderHeightZero))
}
}
}
diff --git a/Application/Sources/Content/PageViewModel.swift b/Application/Sources/Content/PageViewModel.swift
index 06cce8632..d46a6874d 100644
--- a/Application/Sources/Content/PageViewModel.swift
+++ b/Application/Sources/Content/PageViewModel.swift
@@ -11,29 +11,28 @@ import SwiftUI
final class PageViewModel: Identifiable, ObservableObject {
let id: Id
-
+
@Published private(set) var state: State = .loading
@Published private(set) var serviceMessage: ServiceMessage?
-
+
@Published private(set) var displayedShow: SRGShow?
-
+
private let trigger = Trigger()
-
+
init(id: Id) {
self.id = id
-
+
Publishers.Publish(onOutputFrom: reloadSignal()) { [weak self] in
- return Self.pagePublisher(id: id)
+ Self.pagePublisher(id: id)
.map { page in
- return Publishers.AccumulateLatestMany(page.sections.map { section in
- return Publishers.PublishAndRepeat(onOutputFrom: Self.rowReloadSignal(for: section, trigger: self?.trigger)) {
- return Self.rowPublisher(id: id,
- section: section,
- pageSize: Self.pageSize(for: section, in: page.sections),
- paginatedBy: self?.trigger.signal(activatedBy: TriggerId.loadMore(section: section))
- )
- .replaceError(with: Self.fallbackRow(for: section, state: self?.state))
- .prepend(Self.placeholderRow(for: section, state: self?.state))
+ Publishers.AccumulateLatestMany(page.sections.map { section in
+ Publishers.PublishAndRepeat(onOutputFrom: Self.rowReloadSignal(for: section, trigger: self?.trigger)) {
+ Self.rowPublisher(id: id,
+ section: section,
+ pageSize: Self.pageSize(for: section, in: page.sections),
+ paginatedBy: self?.trigger.signal(activatedBy: TriggerId.loadMore(section: section)))
+ .replaceError(with: Self.fallbackRow(for: section, state: self?.state))
+ .prepend(Self.placeholderRow(for: section, state: self?.state))
}
})
.map { (page, $0) }
@@ -44,12 +43,12 @@ final class PageViewModel: Identifiable, ObservableObject {
State.loaded(rows: rows.filter { !$0.isEmpty }, pageUid: page.uid)
}
.catch { error in
- return Just(State.failed(error: error, pageUid: self?.state.pageUid))
+ Just(State.failed(error: error, pageUid: self?.state.pageUid))
}
}
.receive(on: DispatchQueue.main)
.assign(to: &$state)
-
+
Publishers.PublishAndRepeat(onOutputFrom: reloadSignal()) {
URLSession.shared.dataTaskPublisher(for: ApplicationConfiguration.shared.serviceMessageUrl)
.map(\.data)
@@ -61,10 +60,10 @@ final class PageViewModel: Identifiable, ObservableObject {
.removeDuplicates()
.receive(on: DispatchQueue.main)
.assign(to: &$serviceMessage)
-
+
if case let .show(show) = id {
- self.displayedShow = show
-
+ displayedShow = show
+
// The show page needs `topics` which could be available only in the show request.
SRGDataProvider.current!.show(withUrn: show.urn)
.map { $0 }
@@ -73,26 +72,25 @@ final class PageViewModel: Identifiable, ObservableObject {
.assign(to: &$displayedShow)
}
}
-
+
func loadMore() {
if let lastSection = state.sections.last, Self.hasLoadMore(for: lastSection, in: state.sections) {
trigger.activate(for: TriggerId.loadMore(section: lastSection))
}
}
-
+
func reload(deep: Bool = false) {
if deep || state.sections.isEmpty {
trigger.activate(for: TriggerId.reload)
- }
- else {
+ } else {
for section in state.sections where !Self.hasLoadMore(for: section, in: state.sections) {
trigger.activate(for: TriggerId.reloadSection(section))
}
}
}
-
+
private func reloadSignal() -> AnyPublisher {
- return Publishers.Merge4(
+ Publishers.Merge4(
trigger.signal(activatedBy: TriggerId.reload),
ApplicationSignal.wokenUp()
.filter { [weak self] in
@@ -109,49 +107,46 @@ final class PageViewModel: Identifiable, ObservableObject {
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false)
.eraseToAnyPublisher()
}
-
+
private static func rowReloadSignal(for section: Section, trigger: Trigger?) -> AnyPublisher {
- return Publishers.Merge(
+ Publishers.Merge(
section.properties.reloadSignal() ?? PassthroughSubject().eraseToAnyPublisher(),
trigger?.signal(activatedBy: TriggerId.reloadSection(section)) ?? PassthroughSubject().eraseToAnyPublisher()
)
.eraseToAnyPublisher()
}
-
+
private static func hasLoadMore(for section: Section, in sections: [Section]) -> Bool {
- if section == sections.last && section.viewModelProperties.hasLoadMore {
- return true
- }
- else {
- return false
+ if section == sections.last, section.viewModelProperties.hasLoadMore {
+ true
+ } else {
+ false
}
}
-
+
private static func pageSize(for section: Section, in sections: [Section]) -> UInt {
let configuration = ApplicationConfiguration.shared
return hasLoadMore(for: section, in: sections) ? configuration.detailPageSize : configuration.pageSize
}
-
+
private static func placeholderRow(for section: Section, state: State?) -> Row {
if let row = state?.rows.first(where: { $0.section == section }) {
- return row
- }
- else {
- return Row(section: section, items: Self.placeholderRowItems(for: section))
+ row
+ } else {
+ Row(section: section, items: Self.placeholderRowItems(for: section))
}
}
-
+
private static func fallbackRow(for section: Section, state: State?) -> Row {
if let row = state?.rows.first(where: { $0.section == section }) {
- return row
- }
- else {
- return Row(section: section, items: [])
+ row
+ } else {
+ Row(section: section, items: [])
}
}
-
+
private static func placeholderRowItems(for section: Section) -> [Item] {
- return section.properties.placeholderRowItems.map { Item(.item($0), in: section) }
+ section.properties.placeholderRowItems.map { Item(.item($0), in: section) }
}
}
@@ -160,90 +155,90 @@ final class PageViewModel: Identifiable, ObservableObject {
extension PageViewModel {
enum Id: SectionFiltering {
case video
- case audio(channel: RadioChannel)
+ case audio(channel: RadioChannel?)
case live
case topic(_ topic: SRGTopic)
case show(_ show: SRGShow)
case page(_ page: SRGContentPage)
-
-#if os(iOS)
- var sharingItem: SharingItem? {
- switch self {
- case let .show(show):
- return SharingItem(for: show)
- case let .page(page):
- return SharingItem(for: page)
- default:
- return nil
+
+ #if os(iOS)
+ var sharingItem: SharingItem? {
+ switch self {
+ case let .show(show):
+ SharingItem(for: show)
+ case let .page(page):
+ SharingItem(for: page)
+ default:
+ nil
+ }
}
- }
-#endif
-
+ #endif
+
var supportsCastButton: Bool {
switch self {
case .video, .audio, .live:
- return true
+ true
default:
- return false
+ false
}
}
-
+
var isConfigured: Bool {
switch self {
case .audio, .live:
- return true
+ true
default:
- return false
+ false
}
}
-
+
var title: String? {
switch self {
case .video:
- return NSLocalizedString("Videos", comment: "Title displayed at the top of the video view")
+ 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")
+ 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")
+ NSLocalizedString("Livestreams", comment: "Title displayed at the top of the livestreams view")
case let .topic(topic):
- return topic.title
+ topic.title
case let .show(show):
- return show.title
+ show.title
case let .page(page):
- return page.title
+ page.title
}
}
-
+
var analyticsPageViewTitle: String {
switch self {
case .video, .audio, .live:
- return AnalyticsPageTitle.home.rawValue
+ AnalyticsPageTitle.home.rawValue
case let .topic(topic):
- return topic.title
+ topic.title
case let .show(show):
- return show.title
+ show.title
case let .page(page):
- return page.title
+ page.title
}
}
-
+
var analyticsPageViewType: String {
switch self {
case .video, .audio, .page:
- return AnalyticsPageType.landingPage.rawValue
- case .live:
- return AnalyticsPageType.live.rawValue
+ AnalyticsPageType.landingPage.rawValue
+ case .live:
+ AnalyticsPageType.live.rawValue
case .topic, .show:
- return AnalyticsPageType.overview.rawValue
+ 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]
+ return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.audio.rawValue, channel?.name].compactMap { $0 }
case .live:
return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.live.rawValue]
case .topic:
@@ -256,76 +251,83 @@ extension PageViewModel {
return [AnalyticsPageLevel.play.rawValue, AnalyticsPageLevel.video.rawValue, level3]
}
}
-
+
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:
- return show.transmission == .TV
+ show.transmission == .TV
case let .audio(channel: channel):
- return show.transmission == .radio && show.primaryChannelUid == channel.uid
+ if let channel {
+ show.transmission == .radio && show.primaryChannelUid == channel.uid
+ } else {
+ show.transmission == .radio
+ }
default:
- return false
+ false
}
}
-
+
func compatibleShows(_ shows: [SRGShow]) -> [SRGShow] {
- return shows.filter { canContain(show: $0) }
+ shows.filter { canContain(show: $0) }
}
-
+
func compatibleMedias(_ medias: [SRGMedia]) -> [SRGMedia] {
switch self {
case .video:
- return medias.filter { $0.mediaType == .video }
+ medias.filter { $0.mediaType == .video }
case let .audio(channel: channel):
- return medias.filter { $0.mediaType == .audio && ($0.channel?.uid == channel.uid || $0.show?.primaryChannelUid == channel.uid) }
+ if let channel {
+ medias.filter { $0.mediaType == .audio && ($0.channel?.uid == channel.uid || $0.show?.primaryChannelUid == channel.uid) }
+ } else {
+ medias.filter { $0.mediaType == .audio }
+ }
default:
- return medias
+ medias
}
}
}
-
+
enum State {
case loading
case failed(error: Error, pageUid: String?)
case loaded(rows: [Row], pageUid: String?)
-
+
var rows: [Row] {
if case let .loaded(rows: rows, _) = self {
- return rows
- }
- else {
- return []
+ rows
+ } else {
+ []
}
}
-
+
var sections: [Section] {
- return rows.map(\.section)
+ rows.map(\.section)
}
-
+
var isEmpty: Bool {
- return rows.isEmpty
+ rows.isEmpty
}
-
+
var pageUid: String? {
switch self {
case .loading:
- return nil
+ nil
case let .failed(_, pageUid: pageUid):
- return pageUid
+ pageUid
case let .loaded(_, pageUid: pageUid):
- return pageUid
+ pageUid
}
}
}
-
+
enum SectionLayout: Hashable {
case heroStage
case highlight
@@ -340,56 +342,56 @@ extension PageViewModel {
case showGrid
case showSwimlane
case topicSelector
-#if os(iOS)
- case showAccess
-#endif
+ #if os(iOS)
+ case showAccess
+ #endif
}
-
+
fileprivate struct Page: Hashable {
let uid: String?
let sections: [Section]
}
-
+
struct Section: Hashable {
let wrappedValue: Content.Section
- let index: Int // TODO: Remove when all pages are configured with PAC
-
+ let index: Int // TODO: Remove when all pages are configured with PAC
+
init(_ wrappedValue: Content.Section, index: Int) {
self.wrappedValue = wrappedValue
self.index = index
}
-
+
var properties: SectionProperties {
- return wrappedValue.properties
+ wrappedValue.properties
}
-
+
var viewModelProperties: PageViewModelProperties {
switch wrappedValue {
- case let .content(section, _):
- return ContentSectionProperties(contentSection: section)
+ case let .content(section, _, _):
+ ContentSectionProperties(contentSection: section)
case let .configured(section):
- return ConfiguredSectionProperties(configuredSection: section, index: index)
+ ConfiguredSectionProperties(configuredSection: section, index: index)
}
}
}
-
+
struct Item: Hashable {
enum WrappedValue: Hashable {
case item(Content.Item)
case more
}
-
+
let wrappedValue: WrappedValue
let section: Section
-
+
init(_ wrappedValue: WrappedValue, in section: Section) {
self.wrappedValue = wrappedValue
self.section = section
}
}
-
+
typealias Row = CollectionRow
-
+
enum TriggerId: Hashable {
case reload
case reloadSection(Section)
@@ -400,33 +402,34 @@ extension PageViewModel {
// MARK: Header and navigation
extension PageViewModel {
-#if os(iOS)
- var isHeaderWithTitle: Bool {
- return displayedTitle != nil || displayedShow != nil
- }
-
- var isLargeTitleDisplayMode: Bool {
- if isHeaderWithTitle {
- return false
+ #if os(iOS)
+ var isHeaderWithTitle: Bool {
+ displayedTitle != nil || displayedShow != nil
}
- 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 isLargeTitleDisplayMode: Bool {
+ if isHeaderWithTitle {
+ 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.
+ !isNavigationBarHidden
+ }
}
- }
-
- var isNavigationBarHidden: Bool {
- switch id {
- case .video:
- return true
- default:
- return false
+
+ var isNavigationBarHidden: Bool {
+ switch id {
+ case .video:
+ true
+ case let .audio(channel: channel):
+ channel == nil
+ default:
+ false
+ }
}
- }
-#endif
-
+ #endif
+
var primaryColor: Color {
switch id {
case let .topic(topic):
@@ -438,49 +441,46 @@ extension PageViewModel {
return .srgGrayD2
}
}
-
+
var secondaryColor: Color {
- return .srgGray96
+ .srgGray96
}
-
+
var displayedTitle: String? {
switch id {
case let .page(page):
- return page.title
+ page.title
case let .topic(topic):
- return topic.title
+ topic.title
default:
- return nil
+ nil
}
}
-
+
var displayedTitleDescription: String? {
if case let .page(page) = id {
- return page.summary
- }
- else {
- return nil
+ page.summary
+ } else {
+ nil
}
}
-
+
var displayedTitleTextAlignment: TextAlignment {
if case .topic = id {
- return constant(iOS: .leading, tvOS: .center)
- }
- else {
- return .leading
+ constant(iOS: .leading, tvOS: .center)
+ } else {
+ .leading
}
}
-
+
var displayedTitleNeedsTopPadding: Bool {
if case let .topic(topic) = id, ApplicationConfiguration.shared.topicColors(for: topic) != nil {
- return constant(iOS: true, tvOS: false)
- }
- else {
- return false
+ constant(iOS: true, tvOS: false)
+ } else {
+ false
}
}
-
+
var displayedGradientTopic: SRGTopic? {
switch id {
case let .topic(topic):
@@ -492,15 +492,15 @@ extension PageViewModel {
return nil
}
}
-
+
var displayedGradientTopicStyle: TopicGradientView.Style? {
switch id {
case .topic:
- return .topicPage
+ .topicPage
case .show:
- return .showPage
+ .showPage
default:
- return nil
+ nil
}
}
}
@@ -511,10 +511,11 @@ extension PageViewModel {
var userActivity: NSUserActivity? {
{
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
- let applicationVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") else {
+ 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"))
@@ -525,15 +526,14 @@ extension PageViewModel {
"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
+ #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 {
+ } else {
return nil
}
}()
@@ -546,53 +546,65 @@ private extension PageViewModel {
static func pagePublisher(id: Id) -> AnyPublisher {
switch id {
case .video:
- return SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, product: .playVideo)
- .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0), index: $1) }) }
+ SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, product: .playVideo)
+ .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, type: .videoOrTV), index: $1) }) }
.eraseToAnyPublisher()
case let .topic(topic):
- return SRGDataProvider.current!.contentPage(for: topic.vendor, topicWithUrn: topic.urn)
- .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0), index: $1) }) }
+ SRGDataProvider.current!.contentPage(for: topic.vendor, topicWithUrn: topic.urn)
+ // FIXME: is topic page always videoOrTV content type?
+ .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, type: .videoOrTV), index: $1) }) }
.eraseToAnyPublisher()
case let .show(show):
- if show.transmission == .TV && !ApplicationConfiguration.shared.isPredefinedShowPagePreferred {
- return SRGDataProvider.current!.contentPage(for: show.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) }) }
+ if show.transmission == .TV, !ApplicationConfiguration.shared.isPredefinedShowPagePreferred {
+ SRGDataProvider.current!.contentPage(for: show.vendor, product: show.transmission == .radio ? .playAudio : .playVideo, showWithUrn: show.urn)
+ .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, type: show.play_contentType, show: show), index: $1) }) }
.eraseToAnyPublisher()
- }
- else {
- return Just(Page(uid: nil, sections: [ Section(.configured(.availableEpisodes(show)), index: 0) ] ))
+ } else {
+ Just(Page(uid: nil, sections: [Section(.configured(.availableEpisodes(show)), index: 0)]))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
case let .page(page):
- return SRGDataProvider.current!.contentPage(for: page.vendor, uid: page.uid)
- .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0), index: $1) }) }
+ SRGDataProvider.current!.contentPage(for: page.vendor, uid: page.uid)
+ // FIXME: is page always videoOrTV content type?
+ .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, type: .videoOrTV), index: $1) }) }
.eraseToAnyPublisher()
case let .audio(channel: channel):
- return Just(Page(uid: nil, sections: channel.configuredSections().enumeratedMap { Section(.configured($0), index: $1) }))
- .setFailureType(to: Error.self)
- .eraseToAnyPublisher()
+ if let channel, let uid = channel.contentPageId, ApplicationSettingAudioHomepageOption() == .curatedMany {
+ SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, uid: uid)
+ .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, type: .audioOrRadio), index: $1) }) }
+ .eraseToAnyPublisher()
+ } else if let channel {
+ Just(Page(uid: nil, sections: channel.configuredSections().enumeratedMap { Section(.configured($0), index: $1) }))
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
+ } else {
+ SRGDataProvider.current!.contentPage(for: ApplicationConfiguration.shared.vendor, product: .playAudio)
+ .map { Page(uid: $0.uid, sections: $0.sections.enumeratedMap { Section(.content($0, type: .audioOrRadio), index: $1) }) }
+ .eraseToAnyPublisher()
+ }
case .live:
- return Just(Page(uid: nil, sections: ApplicationConfiguration.shared.liveConfiguredSections.enumeratedMap { Section(.configured($0), index: $1) }))
+ Just(Page(uid: nil, sections: ApplicationConfiguration.shared.liveConfiguredSections.enumeratedMap { Section(.configured($0), index: $1) }))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
-
+
static func rowPublisher(id: Id, section: Section, pageSize: UInt, paginatedBy paginator: Trigger.Signal?) -> AnyPublisher {
if let highlight = section.properties.rowHighlight {
- return section.properties.publisher(pageSize: pageSize, paginatedBy: paginator, filter: id)
+ section.properties.publisher(pageSize: pageSize, paginatedBy: paginator, filter: id)
.map { items in
guard let firstItem = items.first else { return Row(section: section, items: []) }
-
- let highlightedItem = section.properties.hasHighlightedItem ? firstItem : nil
+
+ let highlightedItem = section.properties.hasHighlightedItem ? firstItem :
+ section.properties.couldHaveHighlightedItem && items.count == 1 ? firstItem : nil
+
let item = Item(.item(.highlight(highlight, item: highlightedItem)), in: section)
return Row(section: section, items: [item])
}
.eraseToAnyPublisher()
- }
- else {
- return Publishers.CombineLatest(
+ } else {
+ Publishers.CombineLatest(
section.properties.publisher(pageSize: pageSize, paginatedBy: paginator, filter: id)
.scan([]) { $0 + $1 },
section.properties.interactiveUpdatesPublisher()
@@ -600,23 +612,23 @@ private extension PageViewModel {
.setFailureType(to: Error.self)
)
.map { items, removedItems in
- return items.filter { !removedItems.contains($0) }
+ items.filter { !removedItems.contains($0) }
}
.map { rowItems(removeDuplicates(in: $0), in: section) }
.map { Row(section: section, items: $0) }
.eraseToAnyPublisher()
}
}
-
+
static func rowItems(_ items: [Content.Item], in section: Section) -> [Item] {
var rowItems = items.map { Item(.item($0), in: section) }
-#if os(tvOS)
- if !rowItems.isEmpty
- && (section.viewModelProperties.canOpenPage || ApplicationSettingSectionWideSupportEnabled())
- && section.viewModelProperties.hasMoreRowItem {
- rowItems.append(Item(.more, in: section))
- }
-#endif
+ #if os(tvOS)
+ if !rowItems.isEmpty,
+ section.viewModelProperties.canOpenPage || ApplicationSettingSectionWideSupportEnabled(),
+ section.viewModelProperties.hasMoreRowItem {
+ rowItems.append(Item(.more, in: section))
+ }
+ #endif
return rowItems
}
}
@@ -629,23 +641,23 @@ protocol PageViewModelProperties {
}
extension PageViewModelProperties {
-#if os(tvOS)
- var hasMoreRowItem: Bool {
- switch layout {
- case .mediaSwimlane, .showSwimlane, .elementSwimlane:
- return true
- default:
- return false
+ #if os(tvOS)
+ var hasMoreRowItem: Bool {
+ switch layout {
+ case .mediaSwimlane, .showSwimlane, .elementSwimlane:
+ true
+ default:
+ false
+ }
}
- }
-#endif
-
+ #endif
+
var hasLoadMore: Bool {
switch layout {
case .mediaGrid, .mediaList, .showGrid, .liveMediaGrid:
- return true
+ true
default:
- return false
+ false
}
}
}
@@ -653,11 +665,11 @@ extension PageViewModelProperties {
private extension PageViewModel {
struct ContentSectionProperties: PageViewModelProperties {
let contentSection: SRGContentSection
-
+
private var presentation: SRGContentPresentation {
- return contentSection.presentation
+ contentSection.presentation
}
-
+
var layout: PageViewModel.SectionLayout {
switch presentation.type {
case .heroStage:
@@ -672,10 +684,10 @@ private extension PageViewModel {
return .elementSwimlane
case .topicSelector:
return .topicSelector
-#if os(iOS)
- case .showAccess:
- return .showAccess
-#endif
+ #if os(iOS)
+ case .showAccess:
+ return .showAccess
+ #endif
case .favoriteShows:
return .showSwimlane
case .swimlane:
@@ -683,67 +695,67 @@ private extension PageViewModel {
case .grid:
return (contentSection.type == .shows) ? .showGrid : .mediaGrid
case .availableEpisodes:
-#if os(iOS)
- return .mediaList
-#else
- return .mediaGrid
-#endif
+ #if os(iOS)
+ return .mediaList
+ #else
+ return .mediaGrid
+ #endif
case .livestreams:
return .liveMediaSwimlane
default:
return .mediaSwimlane
}
}
-
+
var canOpenPage: Bool {
switch presentation.type {
case .favoriteShows, .myProgram, .continueWatching, .topicSelector, .watchLater:
- return true
+ true
default:
if presentation.contentLink != nil {
- return true
+ true
} else {
- return false
+ false
}
}
}
}
-
+
struct ConfiguredSectionProperties: PageViewModelProperties {
let configuredSection: ConfiguredSection
- let index: Int // TODO: Remove when all pages are configured with PAC
-
+ let index: Int // TODO: Remove when all pages are configured with PAC
+
var layout: PageViewModel.SectionLayout {
switch configuredSection {
case .radioLatestEpisodes, .radioMostPopular, .radioLatest, .radioLatestVideos:
return index == 0 ? .headline : .mediaSwimlane
case .tvLive, .radioLive, .radioLiveSatellite:
-#if os(iOS)
- return .liveMediaGrid
-#else
- return .liveMediaSwimlane
-#endif
+ #if os(iOS)
+ return .liveMediaGrid
+ #else
+ return .liveMediaSwimlane
+ #endif
case .favoriteShows, .radioFavoriteShows:
return .showSwimlane
case .radioAllShows, .tvAllShows:
return .showGrid
-#if os(iOS)
- case .radioShowAccess:
- return .showAccess
-#endif
+ #if os(iOS)
+ case .radioShowAccess:
+ return .showAccess
+ #endif
case .availableEpisodes:
-#if os(iOS)
- return .mediaList
-#else
- return .mediaGrid
-#endif
+ #if os(iOS)
+ return .mediaList
+ #else
+ return .mediaGrid
+ #endif
default:
return .mediaSwimlane
}
}
-
+
var canOpenPage: Bool {
- return layout == .mediaSwimlane || layout == .showSwimlane
+ layout == .mediaSwimlane || layout == .showSwimlane
}
}
}
diff --git a/Application/Sources/Content/Publishers.swift b/Application/Sources/Content/Publishers.swift
index ffbc31b6f..c8d7d2707 100644
--- a/Application/Sources/Content/Publishers.swift
+++ b/Application/Sources/Content/Publishers.swift
@@ -15,7 +15,7 @@ extension NotificationCenter {
func weakPublisher(for name: Notification.Name, object: AnyObject? = nil) -> AnyPublisher {
publisher(for: name)
.filter { [weak object] notification in
- guard let object = object else { return true }
+ guard let object else { return true }
guard let notificationObject = notification.object as? AnyObject else { return false }
return notificationObject === object
}
@@ -26,74 +26,71 @@ extension NotificationCenter {
extension SRGDataProvider {
/// Publishes the latest 30 episodes for a show URN list.
func latestMediasForShowsPublisher(withUrns urns: [String], pageSize: UInt = SRGDataProviderDefaultPageSize) -> AnyPublisher<[SRGMedia], Error> {
- return urns.publisher
+ urns.publisher
.collect(3)
.flatMap { urns in
- return self.latestMediasForShows(withUrns: urns, filter: .episodesOnly, pageSize: 15)
+ self.latestMediasForShows(withUrns: urns, filter: .episodesOnly, pageSize: 15)
}
.reduce([]) { $0 + $1 }
.map { medias in
- return Array(medias.sorted(by: { $0.publicationDate > $1.publicationDate }).prefix(Int(pageSize)))
+ Array(medias.sorted(by: { $0.publicationDate > $1.publicationDate }).prefix(Int(pageSize)))
}
.eraseToAnyPublisher()
}
-
-#if os(iOS)
- /// Publishes the regional media which corresponds to the specified media, if any.
- private func regionalizedRadioLivestreamMedia(for media: SRGMedia) -> AnyPublisher {
- if let channelUid = media.channel?.uid,
- let selectedLivestreamUrn = ApplicationSettingSelectedLivestreamURNForChannelUid(channelUid),
- media.urn != selectedLivestreamUrn {
- return self.radioLivestreams(for: media.vendor, channelUid: channelUid)
- .map { medias in
- if let selectedMedia = ApplicationSettingSelectedLivestreamMediaForChannelUid(channelUid, medias) {
- return selectedMedia
- }
- else {
- return media
+
+ #if os(iOS)
+ /// Publishes the regional media which corresponds to the specified media, if any.
+ private func regionalizedRadioLivestreamMedia(for media: SRGMedia) -> AnyPublisher {
+ if let channelUid = media.channel?.uid,
+ let selectedLivestreamUrn = ApplicationSettingSelectedLivestreamURNForChannelUid(channelUid),
+ media.urn != selectedLivestreamUrn {
+ radioLivestreams(for: media.vendor, channelUid: channelUid)
+ .map { medias in
+ if let selectedMedia = ApplicationSettingSelectedLivestreamMediaForChannelUid(channelUid, medias) {
+ selectedMedia
+ } else {
+ media
+ }
}
- }
- .replaceError(with: media)
- .eraseToAnyPublisher()
- }
- else {
- return Just(media)
- .eraseToAnyPublisher()
+ .replaceError(with: media)
+ .eraseToAnyPublisher()
+ } else {
+ Just(media)
+ .eraseToAnyPublisher()
+ }
}
- }
-#endif
-
+ #endif
+
/// Publishes radio livestreams, replacing regional radio channels. Updates are published down the pipeline as they
/// are retrieved.
func regionalizedRadioLivestreams(for vendor: SRGVendor, contentProviders: SRGContentProviders = .default) -> AnyPublisher<[SRGMedia], Error> {
-#if os(iOS)
- return radioLivestreams(for: vendor, contentProviders: contentProviders)
- .map { medias in
- return Publishers.AccumulateLatestMany(medias.map { media in
- return self.regionalizedRadioLivestreamMedia(for: media)
- })
- .setFailureType(to: Error.self)
+ #if os(iOS)
+ return radioLivestreams(for: vendor, contentProviders: contentProviders)
+ .map { medias in
+ Publishers.AccumulateLatestMany(medias.map { media in
+ self.regionalizedRadioLivestreamMedia(for: media)
+ })
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
+ }
+ .switchToLatest()
.eraseToAnyPublisher()
- }
- .switchToLatest()
- .eraseToAnyPublisher()
-#else
- return radioLivestreams(for: vendor, contentProviders: contentProviders)
- .eraseToAnyPublisher()
-#endif
+ #else
+ return radioLivestreams(for: vendor, contentProviders: contentProviders)
+ .eraseToAnyPublisher()
+ #endif
}
-
+
func historyEntriesPublisher() -> AnyPublisher<[String], Error> {
// Use a deferred future to make it repeatable on-demand
// See https://heckj.github.io/swiftui-notes/#reference-future
- return Deferred {
+ Deferred {
Future<[String], Error> { promise in
let sortDescriptor = NSSortDescriptor(keyPath: \SRGHistoryEntry.date, ascending: false)
SRGUserData.current!.history.historyEntries(matching: nil, sortedWith: [sortDescriptor]) { historyEntries, error in
if let error {
promise(.failure(error))
- }
- else {
+ } else {
promise(.success(historyEntries?.compactMap(\.uid) ?? []))
}
}
@@ -101,21 +98,21 @@ extension SRGDataProvider {
}
.eraseToAnyPublisher()
}
-
+
func historyPublisher(pageSize: UInt = SRGDataProviderDefaultPageSize, paginatedBy paginator: Trigger.Signal?, filter: SectionFiltering?) -> AnyPublisher<[SRGMedia], Error> {
- return historyEntriesPublisher()
+ historyEntriesPublisher()
.map { urns in
- return self.medias(withUrns: urns, pageSize: pageSize, paginatedBy: paginator)
+ self.medias(withUrns: urns, pageSize: pageSize, paginatedBy: paginator)
.map { filter?.compatibleMedias($0) ?? $0 }
}
.switchToLatest()
.eraseToAnyPublisher()
}
-
+
func resumePlaybackPublisher(pageSize: UInt = SRGDataProviderDefaultPageSize, paginatedBy paginator: Trigger.Signal?, filter: SectionFiltering?) -> AnyPublisher<[SRGMedia], Error> {
func playbackPositions(for historyEntries: [SRGHistoryEntry]?) -> OrderedDictionary {
guard let historyEntries else { return [:] }
-
+
var playbackPositions = OrderedDictionary()
for historyEntry in historyEntries {
if let uid = historyEntry.uid {
@@ -124,7 +121,7 @@ extension SRGDataProvider {
}
return playbackPositions
}
-
+
// Use a deferred future to make it repeatable on-demand
// See https://heckj.github.io/swiftui-notes/#reference-future
return Deferred {
@@ -133,18 +130,17 @@ extension SRGDataProvider {
SRGUserData.current!.history.historyEntries(matching: nil, sortedWith: [sortDescriptor]) { historyEntries, error in
if let error {
promise(.failure(error))
- }
- else {
+ } else {
promise(.success(playbackPositions(for: historyEntries)))
}
}
}
}
.map { playbackPositions in
- return self.medias(withUrns: Array(playbackPositions.keys), pageSize: pageSize, paginatedBy: paginator)
+ self.medias(withUrns: Array(playbackPositions.keys), pageSize: pageSize, paginatedBy: paginator)
.map { filter?.compatibleMedias($0) ?? $0 }
.map {
- return $0.filter { media in
+ $0.filter { media in
guard let playbackPosition = playbackPositions[media.urn] else { return true }
return HistoryCanResumePlaybackForMediaAndPosition(playbackPosition, media)
}
@@ -153,18 +149,17 @@ extension SRGDataProvider {
.switchToLatest()
.eraseToAnyPublisher()
}
-
+
func laterEntriesPublisher() -> AnyPublisher<[String], Error> {
// Use a deferred future to make it repeatable on-demand
// See https://heckj.github.io/swiftui-notes/#reference-future
- return Deferred {
+ Deferred {
Future<[String], Error> { promise in
let sortDescriptor = NSSortDescriptor(keyPath: \SRGPlaylistEntry.date, ascending: false)
SRGUserData.current!.playlists.playlistEntriesInPlaylist(withUid: SRGPlaylistUid.watchLater.rawValue, matching: nil, sortedWith: [sortDescriptor]) { playlistEntries, error in
if let error {
promise(.failure(error))
- }
- else {
+ } else {
promise(.success(playlistEntries?.compactMap(\.uid) ?? []))
}
}
@@ -172,20 +167,20 @@ extension SRGDataProvider {
}
.eraseToAnyPublisher()
}
-
+
func laterPublisher(pageSize: UInt = SRGDataProviderDefaultPageSize, paginatedBy paginator: Trigger.Signal?, filter: SectionFiltering?) -> AnyPublisher<[SRGMedia], Error> {
- return laterEntriesPublisher()
+ laterEntriesPublisher()
.map { urns in
- return self.medias(withUrns: urns, pageSize: pageSize, paginatedBy: paginator)
+ self.medias(withUrns: urns, pageSize: pageSize, paginatedBy: paginator)
.map { filter?.compatibleMedias($0) ?? $0 }
}
.switchToLatest()
.eraseToAnyPublisher()
}
-
+
func showsPublisher(withUrns urns: [String]) -> AnyPublisher<[SRGShow], Error> {
let trigger = Trigger()
-
+
return shows(withUrns: urns, pageSize: 50 /* Use largest page size */, paginatedBy: trigger.signal(activatedBy: 1))
.handleEvents(receiveOutput: { _ in
// FIXME: There is probably a better way
@@ -197,21 +192,20 @@ extension SRGDataProvider {
.map { $0.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } }
.eraseToAnyPublisher()
}
-
+
func favoritesPublisher(filter: SectionFiltering?) -> AnyPublisher<[SRGShow], Error> {
- return self.showsPublisher(withUrns: FavoritesShowURNs().array as? [String] ?? [])
+ showsPublisher(withUrns: FavoritesShowURNs().array as? [String] ?? [])
.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) })) }
+ .map { Array($0.map { PlayProgramComposition(channel: $0.channel, programs: $0.programs, external: false) }) }
.eraseToAnyPublisher()
- }
- else {
+ } else {
let tvOtherPartyProgramsPublishers = applicationConfiguration.tvGuideOtherBouquets
.map { tvOtherPartyProgramsPublisher(day: day, bouquet: $0, minimal: minimal) }
return Publishers.concatenateMany(tvOtherPartyProgramsPublishers)
@@ -219,24 +213,24 @@ extension SRGDataProvider {
.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) })) }
+ 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) })) }
+ 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) })) }
+ 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) })) }
+ 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()
}
}
@@ -246,7 +240,7 @@ extension SRGDataProvider {
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
@@ -260,7 +254,7 @@ struct PlayChannel: Hashable {
extension Publishers {
static func concatenateMany