diff --git a/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings b/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings index 32ab0e212..bbb88eca0 100755 --- a/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play RSI/it.lproj/Accessibility.strings @@ -60,6 +60,9 @@ /* Introductory title for information notifications */ "Information" = "Informazioni"; +/* Label on recently played livestreams */ +"Last played" = "Ultimo contenuto visionato"; + /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Utente connesso: %@"; diff --git a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings index 29485195a..dc95247d7 100755 --- a/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RSI/it.lproj/Localizable.strings @@ -97,6 +97,7 @@ /* Label of the button to close the media long-press menu Label of the button to close the media sharing menu + Label of the button to close the module long-press menu Label of the button to close the show long-press menu Title of a cancel button Title of the cancel button in the alert view when deleting a download in the player view */ @@ -198,6 +199,9 @@ /* Title of the alert view to keep autoplay permanently */ "Keep autoplay?" = "Mantenere l'autoplay attivo?"; +/* Label on recently played livestreams */ +"Last played" = "Ultimo contenuto visionato"; + /* Introductory text for the most recent data synchronization date */ "Last synchronization: %@" = "Ultima sincronizzazione: %@"; @@ -312,6 +316,7 @@ /* Button label to open a media from the start from the long-press menu Button label to open a media from the start from the preview window + Button label to open a module from the from the long-press menu Button label to open a module from the preview window Button label to open a show from the from the long-press menu Button label to open a show from the preview window */ diff --git a/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings b/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings index c6735d64d..e272bfd2d 100755 --- a/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play RTR/rm.lproj/Accessibility.strings @@ -60,6 +60,9 @@ /* Introductory title for information notifications */ "Information" = "Infurmaziun"; +/* Label on recently played livestreams */ +"Last played" = "Ultim guardà"; + /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Utilisader annunzià: %@"; diff --git a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings index 867340df3..4b87f7daf 100755 --- a/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RTR/rm.lproj/Localizable.strings @@ -97,6 +97,7 @@ /* Label of the button to close the media long-press menu Label of the button to close the media sharing menu + Label of the button to close the module long-press menu Label of the button to close the show long-press menu Title of a cancel button Title of the cancel button in the alert view when deleting a download in the player view */ @@ -198,6 +199,9 @@ /* Title of the alert view to keep autoplay permanently */ "Keep autoplay?" = "Tegnair autoplay?"; +/* Label on recently played livestreams */ +"Last played" = "Ultim guardà"; + /* Introductory text for the most recent data synchronization date */ "Last synchronization: %@" = "Ultima sincronisaziun: %@"; @@ -312,6 +316,7 @@ /* Button label to open a media from the start from the long-press menu Button label to open a media from the start from the preview window + Button label to open a module from the from the long-press menu Button label to open a module from the preview window Button label to open a show from the from the long-press menu Button label to open a show from the preview window */ @@ -537,7 +542,7 @@ "Total space used: %@" = "Spazi: %@"; /* Title label used to present trending TV videos */ -"Trending videos" = "Videos da trend"; +"Trending videos" = "Videos en trend"; /* Title label to present main TV livestreams */ "TV channels" = "Chanals da tv"; diff --git a/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings b/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings index 9cd50ee53..ac239cd08 100644 --- a/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play RTS/fr.lproj/Accessibility.strings @@ -60,6 +60,9 @@ /* Introductory title for information notifications */ "Information" = "Information"; +/* Label on recently played livestreams */ +"Last played" = "Joué en dernier"; + /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Utilisateur connecté : %@"; diff --git a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings index d316213b6..545f84931 100644 --- a/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play RTS/fr.lproj/Localizable.strings @@ -97,6 +97,7 @@ /* Label of the button to close the media long-press menu Label of the button to close the media sharing menu + Label of the button to close the module long-press menu Label of the button to close the show long-press menu Title of a cancel button Title of the cancel button in the alert view when deleting a download in the player view */ @@ -198,6 +199,9 @@ /* Title of the alert view to keep autoplay permanently */ "Keep autoplay?" = "Garder la lecture automatique ?"; +/* Label on recently played livestreams */ +"Last played" = "Joué en dernier"; + /* Introductory text for the most recent data synchronization date */ "Last synchronization: %@" = "Dernière synchronisation : %@"; @@ -312,6 +316,7 @@ /* Button label to open a media from the start from the long-press menu Button label to open a media from the start from the preview window + Button label to open a module from the from the long-press menu Button label to open a module from the preview window Button label to open a show from the from the long-press menu Button label to open a show from the preview window */ diff --git a/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings b/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings index 9e9ce4583..eed75c80f 100755 --- a/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play SRF/de.lproj/Accessibility.strings @@ -60,6 +60,9 @@ /* Introductory title for information notifications */ "Information" = "Information"; +/* Label on recently played livestreams */ +"Last played" = "Zuletzt abgespielt"; + /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Angemeldeter Nutzer: %@"; diff --git a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings index 8ae9294a9..023e58c7e 100755 --- a/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play SRF/de.lproj/Localizable.strings @@ -1,5 +1,5 @@ /* The amount of episodes available for a show */ -"%@ episodes" = "%@ Sendungen"; +"%@ episodes" = "%@ Folgen"; /* Message displayed at the top of the screen when adding a media to the watch later list. Quotes around the content placeholder are managed by the application. */ "%@ has been added to \"Watch later\"" = "%@ wurde zu Später schauen hinzugefügt"; @@ -97,6 +97,7 @@ /* Label of the button to close the media long-press menu Label of the button to close the media sharing menu + Label of the button to close the module long-press menu Label of the button to close the show long-press menu Title of a cancel button Title of the cancel button in the alert view when deleting a download in the player view */ @@ -198,6 +199,9 @@ /* Title of the alert view to keep autoplay permanently */ "Keep autoplay?" = "Autoplay aktivert behalten?"; +/* Label on recently played livestreams */ +"Last played" = "Zuletzt abgespielt"; + /* Introductory text for the most recent data synchronization date */ "Last synchronization: %@" = "Letzte Synchronisierung: %@"; @@ -312,6 +316,7 @@ /* Button label to open a media from the start from the long-press menu Button label to open a media from the start from the preview window + Button label to open a module from the from the long-press menu Button label to open a module from the preview window Button label to open a show from the from the long-press menu Button label to open a show from the preview window */ diff --git a/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings b/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings index 8f28304e8..272ee7e0d 100755 --- a/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings +++ b/Application/Resources/Apps/Play SWI/en.lproj/Accessibility.strings @@ -60,6 +60,9 @@ /* Introductory title for information notifications */ "Information" = "Information"; +/* Label on recently played livestreams */ +"Last played" = "Last played"; + /* Accessibility introductory text for the logged in user */ "Logged in user: %@" = "Logged in user: %@"; diff --git a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings index 62010ba16..ebcfaee71 100755 --- a/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings +++ b/Application/Resources/Apps/Play SWI/en.lproj/Localizable.strings @@ -97,6 +97,7 @@ /* Label of the button to close the media long-press menu Label of the button to close the media sharing menu + Label of the button to close the module long-press menu Label of the button to close the show long-press menu Title of a cancel button Title of the cancel button in the alert view when deleting a download in the player view */ @@ -198,6 +199,9 @@ /* Title of the alert view to keep autoplay permanently */ "Keep autoplay?" = "Keep autoplay?"; +/* Label on recently played livestreams */ +"Last played" = "Last played"; + /* Introductory text for the most recent data synchronization date */ "Last synchronization: %@" = "Last synchronization: %@"; @@ -312,6 +316,7 @@ /* Button label to open a media from the start from the long-press menu Button label to open a media from the start from the preview window + Button label to open a module from the from the long-press menu Button label to open a module from the preview window Button label to open a show from the from the long-press menu Button label to open a show from the preview window */ diff --git a/Application/Sources/Application/PlayAppDelegate.m b/Application/Sources/Application/PlayAppDelegate.m index 8d0692ef6..a2af47604 100755 --- a/Application/Sources/Application/PlayAppDelegate.m +++ b/Application/Sources/Application/PlayAppDelegate.m @@ -163,8 +163,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [Download removeUnusedDownloadedFiles]; // Setup view controller hierarchy - self.window.rootViewController = [[TabBarController alloc] init]; [self.window makeKeyAndVisible]; + self.window.rootViewController = [[TabBarController alloc] init]; [self checkForForcedUpdates]; diff --git a/Application/Sources/Helpers/ApplicationSectionInfo.h b/Application/Sources/Helpers/ApplicationSectionInfo.h index 10571470d..be0b46fbc 100755 --- a/Application/Sources/Helpers/ApplicationSectionInfo.h +++ b/Application/Sources/Helpers/ApplicationSectionInfo.h @@ -32,9 +32,9 @@ OBJC_EXPORT ApplicationSectionOptionKey const ApplicationSectionOptionShowByDate + (ApplicationSectionInfo *)applicationSectionInfoWithApplicationSection:(ApplicationSection)applicationSection radioChannel:(nullable RadioChannel *)radioChannel options:(nullable NSDictionary *)options; /** - * Return the profile section infos available for the current configuration. + * Return the profile section infos available for the current configuration (with optional inlined notification preview). */ -@property (class, nonatomic, readonly) NSArray *profileApplicationSectionInfos; ++ (NSArray *)profileApplicationSectionInfosWithNotificationPreview:(BOOL)notificationPreview; /** * Properties. diff --git a/Application/Sources/Helpers/ApplicationSectionInfo.m b/Application/Sources/Helpers/ApplicationSectionInfo.m index 513f8f2f8..cb211af9e 100755 --- a/Application/Sources/Helpers/ApplicationSectionInfo.m +++ b/Application/Sources/Helpers/ApplicationSectionInfo.m @@ -50,17 +50,19 @@ + (ApplicationSectionInfo *)applicationSectionInfoWithNotification:(Notification options:@{ ApplicationSectionOptionNotificationKey : notification }]; } -+ (NSArray *)profileApplicationSectionInfos ++ (NSArray *)profileApplicationSectionInfosWithNotificationPreview:(BOOL)notificationPreview { NSMutableArray *sectionInfos = [NSMutableArray array]; if (@available(iOS 10, *)) { if (PushService.sharedService.enabled) { [sectionInfos addObject:[self applicationSectionInfoWithApplicationSection:ApplicationSectionNotifications radioChannel:nil]]; - NSArray *unreadNotifications = Notification.unreadNotifications; - NSArray *previewNotifications = [unreadNotifications subarrayWithRange:NSMakeRange(0, MIN(3, unreadNotifications.count))]; - for (Notification *notification in previewNotifications) { - [sectionInfos addObject:[self applicationSectionInfoWithNotification:notification]]; + if (notificationPreview) { + NSArray *unreadNotifications = Notification.unreadNotifications; + NSArray *previewNotifications = [unreadNotifications subarrayWithRange:NSMakeRange(0, MIN(3, unreadNotifications.count))]; + for (Notification *notification in previewNotifications) { + [sectionInfos addObject:[self applicationSectionInfoWithNotification:notification]]; + } } } } diff --git a/Application/Sources/Helpers/Categories/UICollectionView+PlaySRG.h b/Application/Sources/Helpers/Categories/UICollectionView+PlaySRG.h deleted file mode 100755 index 1453f365a..000000000 --- a/Application/Sources/Helpers/Categories/UICollectionView+PlaySRG.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface UICollectionView (PlaySRG) - -@property (nonatomic, readonly) CGPoint play_maximumContentOffset; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Application/Sources/Helpers/Categories/UICollectionView+PlaySRG.m b/Application/Sources/Helpers/Categories/UICollectionView+PlaySRG.m deleted file mode 100755 index faa1bb201..000000000 --- a/Application/Sources/Helpers/Categories/UICollectionView+PlaySRG.m +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -#import "UICollectionView+PlaySRG.h" - -@implementation UICollectionView (PlaySRG) - -- (CGPoint)play_maximumContentOffset -{ - return CGPointMake(fmaxf(self.contentSize.width - CGRectGetWidth(self.frame), 0.f), - fmaxf(self.contentSize.height - CGRectGetHeight(self.frame), 0.f)); -} - -@end diff --git a/Application/Sources/Home/HomeMediaListTableViewCell.m b/Application/Sources/Home/HomeMediaListTableViewCell.m index 1fd1bb70d..876ee1966 100755 --- a/Application/Sources/Home/HomeMediaListTableViewCell.m +++ b/Application/Sources/Home/HomeMediaListTableViewCell.m @@ -6,13 +6,14 @@ #import "HomeMediaListTableViewCell.h" +#import "ApplicationSettings.h" #import "HomeLiveMediaCollectionViewCell.h" #import "HomeMediaCollectionHeaderView.h" #import "HomeMediaCollectionViewCell.h" #import "Layout.h" #import "MediaPlayerViewController.h" #import "SRGModule+PlaySRG.h" -#import "UICollectionView+PlaySRG.h" +#import "SwimlaneCollectionViewLayout.h" #import "UIColor+PlaySRG.h" #import "UIViewController+PlaySRG.h" @@ -87,7 +88,7 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr [self.contentView addSubview:wrapperView]; self.wrapperView = wrapperView; - UICollectionViewFlowLayout *collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; + SwimlaneCollectionViewLayout *collectionViewLayout = [[SwimlaneCollectionViewLayout alloc] init]; collectionViewLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; collectionViewLayout.minimumLineSpacing = LayoutStandardMargin; collectionViewLayout.minimumInteritemSpacing = LayoutStandardMargin; @@ -98,6 +99,7 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite; collectionView.alwaysBounceHorizontal = YES; collectionView.directionalLockEnabled = YES; + collectionView.decelerationRate = UIScrollViewDecelerationRateFast; // Important. If > 1 view on-screen is found on iPhone with this property enabled, none will scroll to top collectionView.scrollsToTop = NO; collectionView.delegate = self; @@ -148,16 +150,12 @@ - (void)setHomeSectionInfo:(HomeSectionInfo *)homeSectionInfo featured:(BOOL)fea self.moduleBackgroundView.backgroundColor = homeSectionInfo.module.play_backgroundColor; - dispatch_async(dispatch_get_main_queue(), ^{ - if (homeSectionInfo) { - // Restore position in rows when scrolling vertically and returning to a previously scrolled row - CGPoint maxContentOffset = self.collectionView.play_maximumContentOffset; - CGPoint contentOffset = CGPointMake(fmaxf(fminf(homeSectionInfo.contentOffset.x, maxContentOffset.x), 0.f), - homeSectionInfo.contentOffset.y); - [self.collectionView setContentOffset:contentOffset animated:NO]; - } - self.collectionView.scrollEnabled = (homeSectionInfo.items.count != 0); - }); + if (homeSectionInfo) { + // Restore position in rows when scrolling vertically and returning to a previously scrolled row + CGPoint contentOffset = [self.collectionView.collectionViewLayout targetContentOffsetForProposedContentOffset:homeSectionInfo.contentOffset]; + [self.collectionView setContentOffset:contentOffset animated:NO]; + } + self.collectionView.scrollEnabled = (homeSectionInfo.items.count != 0); } #pragma mark UICollectionViewDataSource protocol @@ -216,7 +214,20 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa { if (! [self isEmpty]) { SRGMedia *media = self.homeSectionInfo.items[indexPath.row]; - [self.nearestViewController play_presentMediaPlayerWithMedia:media position:nil airPlaySuggestions:YES fromPushNotification:NO animated:YES completion:nil]; + + HomeSection homeSection = self.homeSectionInfo.homeSection; + if (homeSection == HomeSectionTVLive) { + ApplicationSettingSetLastSelectedTVLivestreamURN(media.URN); + } + else if (homeSection == HomeSectionRadioLive) { + ApplicationSettingSetLastSelectedRadioLivestreamURN(media.URN); + } + [self.nearestViewController play_presentMediaPlayerWithMedia:media position:nil airPlaySuggestions:YES fromPushNotification:NO animated:YES completion:^(PlayerType playerType) { + // Reset scrolling to the origin after playing a livestream, as the last played item is presented first + if (homeSection == HomeSectionTVLive || homeSection == HomeSectionRadioLive) { + self.collectionView.contentOffset = CGPointMake(0.f, self.collectionView.contentOffset.y); + } + }]; } } diff --git a/Application/Sources/Home/HomeSectionInfo.m b/Application/Sources/Home/HomeSectionInfo.m index 784d76680..1b1ec7f35 100755 --- a/Application/Sources/Home/HomeSectionInfo.m +++ b/Application/Sources/Home/HomeSectionInfo.m @@ -17,6 +17,23 @@ #import #import +static NSArray *HomeSectionReorderedMedias(NSArray *medias, NSString *firstURN) +{ + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(SRGMedia * _Nullable media, NSDictionary * _Nullable bindings) { + return [media.URN isEqualToString:firstURN]; + }]; + SRGMedia *firstMedia = [medias filteredArrayUsingPredicate:predicate].firstObject; + if (firstMedia) { + NSMutableArray *reorderedMedias = medias.mutableCopy; + [reorderedMedias removeObject:firstMedia]; + [reorderedMedias insertObject:firstMedia atIndex:0]; + return reorderedMedias.copy; + } + else { + return medias; + } +} + @interface HomeSectionInfo () @property (nonatomic) HomeSection homeSection; @@ -143,10 +160,10 @@ - (void)refreshRadioLivestreamsForVendor:(SRGVendor)vendor withRequestQueue:(SRG }; for (SRGMedia *originalMedia in originalMedias) { - NSString *selectedLiveStreamURN = ApplicationSettingSelectedLiveStreamURNForChannelUid(originalMedia.channel.uid); + NSString *selectedLivestreamURN = ApplicationSettingSelectedLivestreamURNForChannelUid(originalMedia.channel.uid); // If a regional stream has been selected by the user, replace the main channel media with it - if (selectedLiveStreamURN && ! [originalMedia.URN isEqual:selectedLiveStreamURN]) { + if (selectedLivestreamURN && ! [originalMedia.URN isEqual:selectedLivestreamURN]) { [self.pendingMedias addObject:originalMedia]; SRGRequest *request = [SRGDataProvider.currentDataProvider radioLivestreamsForVendor:vendor channelUid:originalMedia.channel.uid withCompletionBlock:^(NSArray * _Nullable channelMedias, NSHTTPURLResponse * _Nullable channelMediasHTTPResponse, NSError * _Nullable error) { @@ -242,7 +259,9 @@ - (void)refreshWithRequestQueue:(SRGRequestQueue *)requestQueue page:(SRGPage *) case HomeSectionTVLive: { SRGBaseRequest *request = [SRGDataProvider.currentDataProvider tvLivestreamsForVendor:vendor withCompletionBlock:^(NSArray * _Nullable medias, NSHTTPURLResponse * _Nullable HTTPResponse, NSError * _Nullable error) { [requestQueue reportError:error]; - paginatedItemListCompletionBlock(medias, [SRGPage new] /* The request does not support pagination, but we need to return a page */, nil, HTTPResponse, error); + + NSString *lastSelectedURN = ApplicationSettingLastSelectedTVLivestreamURN(); + paginatedItemListCompletionBlock(HomeSectionReorderedMedias(medias, lastSelectedURN), [SRGPage new] /* The request does not support pagination, but we need to return a page */, nil, HTTPResponse, error); }]; [requestQueue addRequest:request resume:YES]; break; @@ -330,14 +349,17 @@ - (void)refreshWithRequestQueue:(SRGRequestQueue *)requestQueue page:(SRGPage *) if (self.identifier) { SRGRequest *request = [SRGDataProvider.currentDataProvider radioLivestreamsForVendor:vendor channelUid:self.identifier withCompletionBlock:^(NSArray * _Nullable medias, NSHTTPURLResponse * _Nullable HTTPResponse, NSError * _Nullable error) { [requestQueue reportError:error]; - paginatedItemListCompletionBlock(medias, [SRGPage new] /* The request does not support pagination, but we need to return a page */, nil, HTTPResponse, error); + + NSString *lastSelectedURN = ApplicationSettingLastSelectedRadioLivestreamURN(); + paginatedItemListCompletionBlock(HomeSectionReorderedMedias(medias, lastSelectedURN), [SRGPage new] /* The request does not support pagination, but we need to return a page */, nil, HTTPResponse, error); }]; [requestQueue addRequest:request resume:YES]; } else { [self refreshRadioLivestreamsForVendor:vendor withRequestQueue:requestQueue completionBlock:^(NSArray * _Nullable medias, NSHTTPURLResponse * _Nullable HTTPResponse, NSError * _Nullable error) { // Error reporting is done by the refresh method directly, do not report twice here - paginatedItemListCompletionBlock(medias, [SRGPage new] /* The request does not support pagination, but we need to return a page */, nil, HTTPResponse, error); + NSString *lastSelectedURN = ApplicationSettingLastSelectedRadioLivestreamURN(); + paginatedItemListCompletionBlock(HomeSectionReorderedMedias(medias, lastSelectedURN), [SRGPage new] /* The request does not support pagination, but we need to return a page */, nil, HTTPResponse, error); }]; } break; diff --git a/Application/Sources/Home/HomeShowListTableViewCell.m b/Application/Sources/Home/HomeShowListTableViewCell.m index 641d7b7f5..c7e05e543 100755 --- a/Application/Sources/Home/HomeShowListTableViewCell.m +++ b/Application/Sources/Home/HomeShowListTableViewCell.m @@ -9,7 +9,7 @@ #import "HomeShowCollectionViewCell.h" #import "Layout.h" #import "ShowViewController.h" -#import "UICollectionView+PlaySRG.h" +#import "SwimlaneCollectionViewLayout.h" #import #import @@ -61,7 +61,7 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr [self.contentView addSubview:wrapperView]; self.wrapperView = wrapperView; - UICollectionViewFlowLayout *collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; + SwimlaneCollectionViewLayout *collectionViewLayout = [[SwimlaneCollectionViewLayout alloc] init]; collectionViewLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; collectionViewLayout.minimumLineSpacing = LayoutStandardMargin; collectionViewLayout.minimumInteritemSpacing = LayoutStandardMargin; @@ -72,6 +72,7 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite; collectionView.alwaysBounceHorizontal = YES; collectionView.directionalLockEnabled = YES; + collectionView.decelerationRate = UIScrollViewDecelerationRateFast; // Important. If > 1 view on-screen is found on iPhone with this property enabled, none will scroll to top collectionView.scrollsToTop = NO; collectionView.delegate = self; @@ -112,16 +113,12 @@ - (void)setHomeSectionInfo:(HomeSectionInfo *)homeSectionInfo featured:(BOOL)fea { [super setHomeSectionInfo:homeSectionInfo featured:featured]; - dispatch_async(dispatch_get_main_queue(), ^{ - if (homeSectionInfo) { - // Restore position in rows when scrolling vertically and returning to a previously scrolled row - CGPoint maxContentOffset = self.collectionView.play_maximumContentOffset; - CGPoint contentOffset = CGPointMake(fmaxf(fminf(homeSectionInfo.contentOffset.x, maxContentOffset.x), 0.f), - homeSectionInfo.contentOffset.y); - [self.collectionView setContentOffset:contentOffset animated:NO]; - } - self.collectionView.scrollEnabled = (homeSectionInfo.items.count != 0); - }); + if (homeSectionInfo) { + // Restore position in rows when scrolling vertically and returning to a previously scrolled row + CGPoint contentOffset = [self.collectionView.collectionViewLayout targetContentOffsetForProposedContentOffset:homeSectionInfo.contentOffset]; + [self.collectionView setContentOffset:contentOffset animated:NO]; + } + self.collectionView.scrollEnabled = (homeSectionInfo.items.count != 0); } #pragma mark UICollectionViewDataSource protocol diff --git a/Application/Sources/Home/HomeViewController.h b/Application/Sources/Home/HomeViewController.h index b38c2afba..a1c2e6442 100755 --- a/Application/Sources/Home/HomeViewController.h +++ b/Application/Sources/Home/HomeViewController.h @@ -9,14 +9,14 @@ #import "PlayApplicationNavigation.h" #import "RequestViewController.h" #import "RadioChannel.h" -#import "Scrollable.h" +#import "TabBarActionable.h" #import #import NS_ASSUME_NONNULL_BEGIN -@interface HomeViewController : RequestViewController +@interface HomeViewController : RequestViewController /** * Instantiate a home for the specified application section, displayed the provided home sections. diff --git a/Application/Sources/Home/HomeViewController.m b/Application/Sources/Home/HomeViewController.m index 3e2ea7003..a2165e4d7 100755 --- a/Application/Sources/Home/HomeViewController.m +++ b/Application/Sources/Home/HomeViewController.m @@ -267,6 +267,8 @@ - (void)prepareRefreshWithRequestQueue:(SRGRequestQueue *)requestQueue - (void)refreshDidStart { self.lastRequestError = nil; + + [self.tableView reloadData]; } - (void)refreshDidFinishWithError:(NSError *)error @@ -433,31 +435,30 @@ - (UIEdgeInsets)play_paddingContentInsets - (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView { + NSString *title = nil; + NSError *error = self.lastRequestError; if (error) { // Multiple errors. Pick the first ones if ([error hasCode:SRGNetworkErrorMultiple withinDomain:SRGNetworkErrorDomain]) { error = [error.userInfo[SRGNetworkErrorsKey] firstObject]; } - return [[NSAttributedString alloc] initWithString:error.localizedDescription - attributes:@{ NSFontAttributeName : [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleTitle], - NSForegroundColorAttributeName : UIColor.play_lightGrayColor }]; + title = error.localizedDescription; } else { - return nil; + title = NSLocalizedString(@"No results", @"Default text displayed when no results are available"); } + + return [[NSAttributedString alloc] initWithString:title + attributes:@{ NSFontAttributeName : [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleTitle], + NSForegroundColorAttributeName : UIColor.play_lightGrayColor }]; } - (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView { - if (self.lastRequestError) { - return [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Pull to reload", @"Text displayed to inform the user she can pull a list to reload it") - attributes:@{ NSFontAttributeName : [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleSubtitle], - NSForegroundColorAttributeName : UIColor.play_lightGrayColor }]; - } - else { - return nil; - } + return [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Pull to reload", @"Text displayed to inform the user she can pull a list to reload it") + attributes:@{ NSFontAttributeName : [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleSubtitle], + NSForegroundColorAttributeName : UIColor.play_lightGrayColor }]; } - (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView @@ -466,7 +467,7 @@ - (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView return [UIImage imageNamed:@"error-90"]; } else { - return nil; + return [UIImage imageNamed:@"media-90"]; } } @@ -511,13 +512,6 @@ - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionI } } -#pragma mark Scrollable protocol - -- (void)scrollToTopAnimated:(BOOL)animated -{ - [self.tableView play_scrollToTopAnimated:animated]; -} - #pragma mark SRGAnalyticsViewTracking protocol - (NSString *)srg_pageViewTitle @@ -538,6 +532,13 @@ - (NSString *)srg_pageViewTitle } } +#pragma mark TabBarActionable protocol + +- (void)performActiveTabActionAnimated:(BOOL)animated +{ + [self.tableView play_scrollToTopAnimated:animated]; +} + #pragma mark UITableViewDataSource protocol - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView diff --git a/Application/Sources/Library/ProfileTableViewCell.m b/Application/Sources/Library/ProfileTableViewCell.m index 475bdeb5f..e9b095cd5 100755 --- a/Application/Sources/Library/ProfileTableViewCell.m +++ b/Application/Sources/Library/ProfileTableViewCell.m @@ -43,8 +43,9 @@ - (void)awakeFromNib self.backgroundColor = UIColor.clearColor; - // Cell highlighting is custom - self.selectionStyle = UITableViewCellSelectionStyleNone; + UIView *selectedBackgroundView = [[UIView alloc] init]; + selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0.15f alpha:1.f]; + self.selectedBackgroundView = selectedBackgroundView; } - (void)prepareForReuse @@ -74,13 +75,20 @@ - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { [super setHighlighted:highlighted animated:animated]; - UIColor *color = highlighted ? UIColor.play_grayColor : UIColor.whiteColor; + UIColor *color = (highlighted && self.selectionStyle == UITableViewCellAccessoryNone) ? UIColor.play_grayColor : UIColor.whiteColor; self.titleLabel.textColor = color; self.iconImageView.tintColor = color; [self updateIconImageViewAnimation]; } +- (void)setSelected:(BOOL)selected animated:(BOOL)animated +{ + [super setSelected:selected animated:animated]; + + [self updateIconImageViewAnimation]; +} + #pragma mark User interface - (void)updateIconImageViewAnimation diff --git a/Application/Sources/Library/ProfileViewController.h b/Application/Sources/Library/ProfileViewController.h index e4b08ec37..7452d0c68 100755 --- a/Application/Sources/Library/ProfileViewController.h +++ b/Application/Sources/Library/ProfileViewController.h @@ -7,13 +7,13 @@ #import "BaseViewController.h" #import "ContentInsets.h" #import "PlayApplicationNavigation.h" -#import "Scrollable.h" +#import "TabBarActionable.h" #import NS_ASSUME_NONNULL_BEGIN -@interface ProfileViewController : BaseViewController +@interface ProfileViewController : BaseViewController @end diff --git a/Application/Sources/Library/ProfileViewController.m b/Application/Sources/Library/ProfileViewController.m index 44d8ee8e3..4b00ddffd 100755 --- a/Application/Sources/Library/ProfileViewController.m +++ b/Application/Sources/Library/ProfileViewController.m @@ -31,6 +31,7 @@ @interface ProfileViewController () @property (nonatomic) NSArray *sectionInfos; +@property (nonatomic) ApplicationSectionInfo *currentSectionInfo; @property (nonatomic, weak) IBOutlet UITableView *tableView; @@ -104,8 +105,14 @@ - (void)viewWillAppear:(BOOL)animated [PushService.sharedService resetApplicationBadge]; - // Ensure correct latest notifications displayed + // Ensure latest notifications are displayed [self reloadData]; + + // On iPad where split screen can be used, load the secondary view afterwards (if loaded too early it will be collapsed + // automatically onto the primary at startup for narrow layouts, which is not what we want). + if ([self play_isMovingToParentViewController] && UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + [self openApplicationSectionInfo:self.sectionInfos.firstObject interactive:NO animated:NO]; + } } - (void)viewWillDisappear:(BOOL)animated @@ -137,15 +144,21 @@ - (void)updateForContentSizeCategory { [super updateForContentSizeCategory]; - [self.tableView reloadData]; + [self reloadTableView]; } #pragma mark Data -- (void)reloadData +- (void)reloadTableView { - self.sectionInfos = ApplicationSectionInfo.profileApplicationSectionInfos; [self.tableView reloadData]; + [self updateSelection]; +} + +- (void)reloadData +{ + self.sectionInfos = [ApplicationSectionInfo profileApplicationSectionInfosWithNotificationPreview:self.splitViewController.collapsed]; + [self reloadTableView]; dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView flashScrollIndicators]; @@ -164,8 +177,12 @@ - (Notification *)notificationAtIndexPath:(NSIndexPath *)indexPath return applicationSectionInfo.options[ApplicationSectionOptionNotificationKey]; } -- (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionInfo animated:(BOOL)animated +- (UIViewController *)viewControllerForSectionInfo:(ApplicationSectionInfo *)applicationSectionInfo { + if (! applicationSectionInfo) { + return nil; + } + UIViewController *viewController = nil; switch (applicationSectionInfo.applicationSection) { case ApplicationSectionNotifications: { @@ -198,8 +215,63 @@ - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionI } } + if (! viewController) { + return nil; + } + + // Always wrap into a navigation controller. The split view takes care of moving view controllers between navigation + // controllers when collapsing or expanding + return [[NavigationController alloc] initWithRootViewController:viewController]; +} + +- (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionInfo interactive:(BOOL)interactive animated:(BOOL)animated +{ + if (! applicationSectionInfo) { + return NO; + } + + // Do not reload a section if already the current one + if (! self.splitViewController.collapsed && [applicationSectionInfo isEqual:self.currentSectionInfo]) { + return YES; + } + + UIViewController *viewController = [self viewControllerForSectionInfo:applicationSectionInfo]; if (viewController) { - [self.navigationController pushViewController:viewController animated:animated]; + self.currentSectionInfo = applicationSectionInfo; + + if (interactive) { + void (^showDetail)(void) = ^{ + [self.splitViewController showDetailViewController:viewController sender:self]; + }; + + if (animated) { + showDetail(); + } + else { + [UIView performWithoutAnimation:showDetail]; + } + } + else { + // Adding the details view controller on-the-fly avoids automatic collapsing (i.e. starting with the details + // on top of the primary) when starting in compact layout. + NSMutableArray *viewControllers = self.splitViewController.viewControllers.mutableCopy; + if (viewControllers.count == 1) { + [viewControllers addObject:viewController]; + } + else if (viewControllers.count == 2) { + [viewControllers replaceObjectAtIndex:1 withObject:viewController]; + } + else { + return NO; + } + self.splitViewController.viewControllers = viewControllers.copy; + [self updateSelection]; + } + + // Transfer the VoiceOver focus automatically, as is for example done in the Settings application. + if (! self.splitViewController.collapsed) { + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, viewController.view); + } return YES; } else { @@ -207,6 +279,25 @@ - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionI } } +- (void)updateSelection +{ + if (! self.currentSectionInfo) { + return; + } + + NSUInteger index = [self.sectionInfos indexOfObject:self.currentSectionInfo]; + if (index != NSNotFound) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; + } + else { + NSIndexPath *indexPath = self.tableView.indexPathForSelectedRow; + if (indexPath) { + [self.tableView deselectRowAtIndexPath:indexPath animated:NO]; + } + } +} + #pragma mark ContentInsets protocol - (NSArray *)play_contentScrollViews @@ -216,21 +307,14 @@ - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionI - (UIEdgeInsets)play_paddingContentInsets { - return UIEdgeInsetsZero; + return SRGIdentityService.currentIdentityService ? UIEdgeInsetsZero : LayoutStandardTableViewPaddingInsets; } #pragma mark PlayApplicationNavigation protocol - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionInfo { - return [self openApplicationSectionInfo:applicationSectionInfo animated:NO]; -} - -#pragma mark Scrollable protocol - -- (void)scrollToTopAnimated:(BOOL)animated -{ - [self.tableView play_scrollToTopAnimated:animated]; + return [self openApplicationSectionInfo:applicationSectionInfo interactive:YES animated:NO]; } #pragma mark SRGAnalyticsViewTracking protocol @@ -245,6 +329,13 @@ - (NSString *)srg_pageViewTitle return @[ AnalyticsPageLevelPlay, AnalyticsPageLevelUser ]; } +#pragma mark TabBarActionable protocol + +- (void)performActiveTabActionAnimated:(BOOL)animated +{ + [self.tableView play_scrollToTopAnimated:animated]; +} + #pragma mark UITableViewDataSource protocol - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView @@ -289,6 +380,7 @@ - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)ce else { ProfileTableViewCell *profileTableViewCell = (ProfileTableViewCell *)cell; profileTableViewCell.applicationSectionInfo = self.sectionInfos[indexPath.row]; + profileTableViewCell.selectionStyle = self.splitViewController.collapsed ? UITableViewCellSelectionStyleNone : UITableViewCellSelectionStyleDefault; } } @@ -299,11 +391,11 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath [NotificationsViewController openNotification:notification fromViewController:self]; // Update the cell dot right away - [tableView reloadData]; + [self reloadTableView]; } else { ApplicationSectionInfo *applicationSectionInfo = self.sectionInfos[indexPath.row]; - [self openApplicationSectionInfo:applicationSectionInfo animated:YES]; + [self openApplicationSectionInfo:applicationSectionInfo interactive:YES animated:YES]; } } diff --git a/Application/Sources/MiniPlayer/PlayMiniPlayerView.m b/Application/Sources/MiniPlayer/PlayMiniPlayerView.m index e2c4ff4b7..cf84679e9 100755 --- a/Application/Sources/MiniPlayer/PlayMiniPlayerView.m +++ b/Application/Sources/MiniPlayer/PlayMiniPlayerView.m @@ -352,7 +352,7 @@ - (void)playbackButton:(SRGPlaybackButton *)playbackButton didPressInState:(SRGP [SRGLetterboxService.sharedService enableWithController:controller pictureInPictureDelegate:nil]; } - if (media.mediaType == SRGMediaTypeVideo && ! ApplicationSettingBackgroundVideoPlaybackEnabled() + if (state == SRGPlaybackButtonStatePlay && media.mediaType == SRGMediaTypeVideo && ! ApplicationSettingBackgroundVideoPlaybackEnabled() && ! AVAudioSession.srg_isAirPlayActive && ! controller.pictureInPictureActive) { [self.nearestViewController play_presentMediaPlayerFromLetterboxController:controller withAirPlaySuggestions:YES fromPushNotification:NO animated:YES completion:nil]; } diff --git a/Application/Sources/Player/MediaPlayerViewController.m b/Application/Sources/Player/MediaPlayerViewController.m index 9854b6655..d24796c6a 100755 --- a/Application/Sources/Player/MediaPlayerViewController.m +++ b/Application/Sources/Player/MediaPlayerViewController.m @@ -1726,7 +1726,8 @@ - (IBAction)selectLivestreamMedia:(id)sender [self.livestreamMedias enumerateObjectsUsingBlock:^(SRGMedia * _Nonnull media, NSUInteger idx, BOOL * _Nonnull stop) { [alertController addAction:[UIAlertAction actionWithTitle:media.title style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - ApplicationSettingSetSelectedLiveStreamURNForChannelUid(media.channel.uid, media.URN); + ApplicationSettingSetLastSelectedRadioLivestreamURN(media.URN); + ApplicationSettingSetSelectedLivestreamURNForChannelUid(media.channel.uid, media.URN); // Use the playback state if playing SRGMediaPlayerPlaybackState currentPlaybackState = self.letterboxController.playbackState; diff --git a/Application/Sources/Settings/ApplicationSettings.h b/Application/Sources/Settings/ApplicationSettings.h index df6403bea..c61a14729 100755 --- a/Application/Sources/Settings/ApplicationSettings.h +++ b/Application/Sources/Settings/ApplicationSettings.h @@ -72,8 +72,14 @@ OBJC_EXPORT BOOL ApplicationSettingBackgroundVideoPlaybackEnabled(void); OBJC_EXPORT BOOL ApplicationSettingSubtitleAvailabilityDisplayed(void); OBJC_EXPORT BOOL ApplicationSettingAudioDescriptionAvailabilityDisplayed(void); -OBJC_EXPORT NSString * _Nullable ApplicationSettingSelectedLiveStreamURNForChannelUid(NSString * _Nullable channelUid); -OBJC_EXPORT void ApplicationSettingSetSelectedLiveStreamURNForChannelUid(NSString * channelUid, NSString * _Nullable mediaURN); +OBJC_EXPORT NSString * _Nullable ApplicationSettingLastSelectedTVLivestreamURN(void); +OBJC_EXPORT void ApplicationSettingSetLastSelectedTVLivestreamURN(NSString * _Nullable mediaURN); + +OBJC_EXPORT NSString * _Nullable ApplicationSettingLastSelectedRadioLivestreamURN(void); +OBJC_EXPORT void ApplicationSettingSetLastSelectedRadioLivestreamURN(NSString * _Nullable mediaURN); + +OBJC_EXPORT NSString * _Nullable ApplicationSettingSelectedLivestreamURNForChannelUid(NSString * _Nullable channelUid); +OBJC_EXPORT void ApplicationSettingSetSelectedLivestreamURNForChannelUid(NSString * channelUid, NSString * _Nullable mediaURN); OBJC_EXPORT SRGMedia * _Nullable ApplicationSettingSelectedLivestreamMediaForChannelUid(NSString * _Nullable channelUid, NSArray * _Nullable medias); diff --git a/Application/Sources/Settings/ApplicationSettings.m b/Application/Sources/Settings/ApplicationSettings.m index 77e82f868..c2aa62012 100755 --- a/Application/Sources/Settings/ApplicationSettings.m +++ b/Application/Sources/Settings/ApplicationSettings.m @@ -30,7 +30,9 @@ NSString * const PlaySRGSettingLastLoggedInEmailAddress = @"PlaySRGSettingLastLoggedInEmailAddress"; NSString * const PlaySRGSettingLastOpenedRadioChannelUid = @"PlaySRGSettingLastOpenedRadioChannelUid"; NSString * const PlaySRGSettingLastOpenedTabBarItem = @"PlaySRGSettingLastOpenedTabBarItem"; -NSString * const PlaySRGSettingSelectedLiveStreamURNForChannels = @"PlaySRGSettingSelectedLiveStreamURNForChannels"; +NSString * const PlaySRGSettingSelectedLivestreamURNForChannels = @"PlaySRGSettingSelectedLiveStreamURNForChannels"; +NSString * const PlaySRGSettingSelectedTVLivestreamURN = @"PlaySRGSettingSelectedTVLivestreamURN"; +NSString * const PlaySRGSettingSelectedRadioLivestreamURN = @"PlaySRGSettingSelectedRadioLivestreamURN"; NSString * const PlaySRGSettingServiceURL = @"PlaySRGSettingServiceURL"; NSString * const PlaySRGSettingUserLocation = @"PlaySRGSettingUserLocation"; @@ -207,22 +209,46 @@ BOOL ApplicationSettingAudioDescriptionAvailabilityDisplayed(void) return UIAccessibilityIsVoiceOverRunning() || [NSUserDefaults.standardUserDefaults boolForKey:PlaySRGSettingAudioDescriptionAvailabilityDisplayed]; } -NSString *ApplicationSettingSelectedLiveStreamURNForChannelUid(NSString *channelUid) +NSString *ApplicationSettingLastSelectedTVLivestreamURN(void) { - NSDictionary *selectedLiveStreamURNForChannels = [NSUserDefaults.standardUserDefaults dictionaryForKey:PlaySRGSettingSelectedLiveStreamURNForChannels]; - return selectedLiveStreamURNForChannels[channelUid]; + return [NSUserDefaults.standardUserDefaults stringForKey:PlaySRGSettingSelectedTVLivestreamURN]; } -void ApplicationSettingSetSelectedLiveStreamURNForChannelUid(NSString *channelUid, NSString *mediaURN) +void ApplicationSettingSetLastSelectedTVLivestreamURN(NSString *mediaURN) +{ + NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults; + [userDefaults setObject:mediaURN forKey:PlaySRGSettingSelectedTVLivestreamURN]; + [userDefaults synchronize]; +} + +NSString *ApplicationSettingLastSelectedRadioLivestreamURN(void) +{ + return [NSUserDefaults.standardUserDefaults stringForKey:PlaySRGSettingSelectedRadioLivestreamURN]; +} + +void ApplicationSettingSetLastSelectedRadioLivestreamURN(NSString *mediaURN) +{ + NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults; + [userDefaults setObject:mediaURN forKey:PlaySRGSettingSelectedRadioLivestreamURN]; + [userDefaults synchronize]; +} + +NSString *ApplicationSettingSelectedLivestreamURNForChannelUid(NSString *channelUid) +{ + NSDictionary *selectedLivestreamURNForChannels = [NSUserDefaults.standardUserDefaults dictionaryForKey:PlaySRGSettingSelectedLivestreamURNForChannels]; + return selectedLivestreamURNForChannels[channelUid]; +} + +void ApplicationSettingSetSelectedLivestreamURNForChannelUid(NSString *channelUid, NSString *mediaURN) { if (channelUid) { - NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;; + NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults; - NSDictionary *selectedLiveStreamURNForChannels = [userDefaults dictionaryForKey:PlaySRGSettingSelectedLiveStreamURNForChannels]; - NSMutableDictionary *mutableSelectedLiveStreamURNForChannels = selectedLiveStreamURNForChannels.mutableCopy ?: NSMutableDictionary.new; - mutableSelectedLiveStreamURNForChannels[channelUid] = mediaURN; + NSDictionary *selectedLivestreamURNForChannels = [userDefaults dictionaryForKey:PlaySRGSettingSelectedLivestreamURNForChannels]; + NSMutableDictionary *mutableSelectedLivestreamURNForChannels = selectedLivestreamURNForChannels.mutableCopy ?: NSMutableDictionary.new; + mutableSelectedLivestreamURNForChannels[channelUid] = mediaURN; - [userDefaults setObject:mutableSelectedLiveStreamURNForChannels.copy forKey:PlaySRGSettingSelectedLiveStreamURNForChannels]; + [userDefaults setObject:mutableSelectedLivestreamURNForChannels.copy forKey:PlaySRGSettingSelectedLivestreamURNForChannels]; [userDefaults synchronize]; } } @@ -233,8 +259,8 @@ void ApplicationSettingSetSelectedLiveStreamURNForChannelUid(NSString *channelUi return nil; } - NSString *selectedLiveStreamURN = ApplicationSettingSelectedLiveStreamURNForChannelUid(channelUid); - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @keypath(SRGMedia.new, URN), selectedLiveStreamURN]; + NSString *selectedLivestreamURN = ApplicationSettingSelectedLivestreamURNForChannelUid(channelUid); + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @keypath(SRGMedia.new, URN), selectedLivestreamURN]; return [medias filteredArrayUsingPredicate:predicate].firstObject; } diff --git a/Application/Sources/UI/Controllers/BaseViewController.m b/Application/Sources/UI/Controllers/BaseViewController.m index 07a6563c3..0e62fa4ba 100755 --- a/Application/Sources/UI/Controllers/BaseViewController.m +++ b/Application/Sources/UI/Controllers/BaseViewController.m @@ -346,6 +346,7 @@ - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)) { UIViewController *viewController = animator.previewViewController; + animator.preferredCommitStyle = UIContextMenuInteractionCommitStylePop; [animator addCompletion:^{ if ([viewController isKindOfClass:MediaPreviewViewController.class]) { MediaPreviewViewController *mediaPreviewViewController = (MediaPreviewViewController *)viewController; @@ -359,6 +360,13 @@ - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willPerfo }]; } +- (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction previewForHighlightingMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)) +{ + UIPreviewParameters *previewParameters = [[UIPreviewParameters alloc] init]; + previewParameters.backgroundColor = self.view.backgroundColor; + return [[UITargetedPreview alloc] initWithView:interaction.view parameters:previewParameters]; +} + #pragma mark UIViewControllerPreviewingDelegate protocol - (UIViewController *)previewingContext:(id)previewingContext viewControllerForLocation:(CGPoint)location diff --git a/Application/Sources/UI/Controllers/CollectionRequestViewController.h b/Application/Sources/UI/Controllers/CollectionRequestViewController.h index eb3623751..7f5d5fd04 100755 --- a/Application/Sources/UI/Controllers/CollectionRequestViewController.h +++ b/Application/Sources/UI/Controllers/CollectionRequestViewController.h @@ -6,7 +6,7 @@ #import "ContentInsets.h" #import "ListRequestViewController.h" -#import "Scrollable.h" +#import "TabBarActionable.h" #import @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN * - Automatic display of a load more footer if more data is available * - Clean display of empty collections, whether because of an error or because no items are available */ -@interface CollectionRequestViewController : ListRequestViewController +@interface CollectionRequestViewController : ListRequestViewController /** * The collection view. A flow layout is required diff --git a/Application/Sources/UI/Controllers/CollectionRequestViewController.m b/Application/Sources/UI/Controllers/CollectionRequestViewController.m index 6c4c6b599..d210d3d34 100755 --- a/Application/Sources/UI/Controllers/CollectionRequestViewController.m +++ b/Application/Sources/UI/Controllers/CollectionRequestViewController.m @@ -259,9 +259,9 @@ - (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView return VerticalOffsetForEmptyDataSet(scrollView); } -#pragma mark Scrollable protocol +#pragma mark TabBarActionable protocol -- (void)scrollToTopAnimated:(BOOL)animated +- (void)performActiveTabActionAnimated:(BOOL)animated { [self.collectionView play_scrollToTopAnimated:animated]; } diff --git a/Application/Sources/UI/Controllers/NavigationController.h b/Application/Sources/UI/Controllers/NavigationController.h index f986bf238..0ec1df8ec 100755 --- a/Application/Sources/UI/Controllers/NavigationController.h +++ b/Application/Sources/UI/Controllers/NavigationController.h @@ -7,7 +7,7 @@ #import "RadioChannel.h" #import "PlayApplicationNavigation.h" -#import "Scrollable.h" +#import "TabBarActionable.h" #import @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Standard navigation controller with Play look-and-feel and behavior. */ -@interface NavigationController : UINavigationController +@interface NavigationController : UINavigationController /** * Create a navigation controller with standard customizable look-and-feel. diff --git a/Application/Sources/UI/Controllers/NavigationController.m b/Application/Sources/UI/Controllers/NavigationController.m index 427a7a9f1..c90476b29 100755 --- a/Application/Sources/UI/Controllers/NavigationController.m +++ b/Application/Sources/UI/Controllers/NavigationController.m @@ -175,17 +175,22 @@ - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionI } } -#pragma mark Scrollable protocol +#pragma mark TabBarActionable protocol -- (void)scrollToTopAnimated:(BOOL)animated +- (void)performActiveTabActionAnimated:(BOOL)animated { if (self.viewControllers.count == 1) { UIViewController *rootViewController = self.viewControllers.firstObject; - if ([rootViewController conformsToProtocol:@protocol(Scrollable)]) { - UIViewController *scrollableRootViewController = (UIViewController *)rootViewController; - [scrollableRootViewController scrollToTopAnimated:animated]; + if ([rootViewController conformsToProtocol:@protocol(TabBarActionable)]) { + UIViewController *actionableRootViewController = (UIViewController *)rootViewController; + [actionableRootViewController performActiveTabActionAnimated:animated]; } } + else { + // Natively performed when a navigation controller is directly embedded in a tab bar controller, but here triggered + // explicitly for all other kinds of embedding as well (e.g. tab bar -> split view -> navigation). + [self popToRootViewControllerAnimated:animated]; + } } @end diff --git a/Application/Sources/UI/Controllers/PageViewController.h b/Application/Sources/UI/Controllers/PageViewController.h index df5fc95a2..9d054f828 100755 --- a/Application/Sources/UI/Controllers/PageViewController.h +++ b/Application/Sources/UI/Controllers/PageViewController.h @@ -5,7 +5,7 @@ // #import "ContentInsets.h" -#import "Scrollable.h" +#import "TabBarActionable.h" #import #import @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN * * To use `PageViewController`, bind its `placeholderViews` property to a single view where pages will be displayed. */ -@interface PageViewController : HLSPlaceholderViewController +@interface PageViewController : HLSPlaceholderViewController /** * Create an instance displaying the supplied view controllers, and starting at the specified page. diff --git a/Application/Sources/UI/Controllers/PageViewController.m b/Application/Sources/UI/Controllers/PageViewController.m index c591cbe57..9739ad8c2 100755 --- a/Application/Sources/UI/Controllers/PageViewController.m +++ b/Application/Sources/UI/Controllers/PageViewController.m @@ -237,22 +237,22 @@ - (void)tabBar:(MDCTabBar *)tabBar didSelectItem:(UITabBarItem *)item [self displayPageAtIndex:index animated:YES]; } -#pragma mark Scrollable protocol +#pragma mark SRGAnalyticsContainerViewTracking protocol -- (void)scrollToTopAnimated:(BOOL)animated +- (NSArray *)srg_activeChildViewControllers { - UIViewController *currentViewController = self.pageViewController.viewControllers.firstObject; - if ([currentViewController conformsToProtocol:@protocol(Scrollable)]) { - UIViewController *scrollableCurrentViewController = (UIViewController *)currentViewController; - [scrollableCurrentViewController scrollToTopAnimated:animated]; - } + return self.pageViewController ? @[self.pageViewController] : @[]; } -#pragma mark SRGAnalyticsContainerViewTracking protocol +#pragma mark TabBarActionable protocol -- (NSArray *)srg_activeChildViewControllers +- (void)performActiveTabActionAnimated:(BOOL)animated { - return self.pageViewController ? @[self.pageViewController] : @[]; + UIViewController *currentViewController = self.pageViewController.viewControllers.firstObject; + if ([currentViewController conformsToProtocol:@protocol(TabBarActionable)]) { + UIViewController *actionableCurrentViewController = (UIViewController *)currentViewController; + [actionableCurrentViewController performActiveTabActionAnimated:animated]; + } } #pragma mark UIPageViewControllerDataSource protocol diff --git a/Application/Sources/UI/Controllers/SplitViewController.h b/Application/Sources/UI/Controllers/SplitViewController.h new file mode 100644 index 000000000..58e5a599c --- /dev/null +++ b/Application/Sources/UI/Controllers/SplitViewController.h @@ -0,0 +1,21 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +#import "PlayApplicationNavigation.h" +#import "TabBarActionable.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Lightweight split view controller subclass with standard behavior. + */ +@interface SplitViewController : UISplitViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/Application/Sources/UI/Controllers/SplitViewController.m b/Application/Sources/UI/Controllers/SplitViewController.m new file mode 100644 index 000000000..702c4acbe --- /dev/null +++ b/Application/Sources/UI/Controllers/SplitViewController.m @@ -0,0 +1,74 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +#import "SplitViewController.h" + +@implementation SplitViewController + +#pragma mark Rotation + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + UIInterfaceOrientationMask supportedInterfaceOrientations = [super supportedInterfaceOrientations]; + for (UIViewController *viewController in self.viewControllers) { + supportedInterfaceOrientations &= viewController.supportedInterfaceOrientations; + } + return supportedInterfaceOrientations; +} + +#pragma mark Status bar + +- (BOOL)prefersStatusBarHidden +{ + return [self.viewControllers.firstObject prefersStatusBarHidden]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return [self.viewControllers.firstObject preferredStatusBarStyle]; +} + +- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation +{ + return [self.viewControllers.firstObject preferredStatusBarUpdateAnimation]; +} + +#pragma mark PlayApplicationNavigation protocol + +- (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionInfo +{ + UIViewController *primaryViewController = self.viewControllers.firstObject; + if ([primaryViewController conformsToProtocol:@protocol(PlayApplicationNavigation)]) { + UIViewController *navigablePrimaryViewController = (UIViewController *)primaryViewController; + return [navigablePrimaryViewController openApplicationSectionInfo:applicationSectionInfo]; + } + else { + return NO; + } +} + +#pragma mark TabBarActionable protocol + +- (void)performActiveTabActionAnimated:(BOOL)animated +{ + void (^performActiveTabAction)(UIViewController *) = ^(UIViewController *viewController) { + if ([viewController conformsToProtocol:@protocol(TabBarActionable)]) { + UIViewController *actionableViewController = (UIViewController *)viewController; + [actionableViewController performActiveTabActionAnimated:animated]; + } + }; + + if (self.viewControllers.count == 2) { + UIViewController *secondaryViewController = self.viewControllers[1]; + performActiveTabAction(secondaryViewController); + } + else { + UIViewController *primaryViewController = self.viewControllers.firstObject; + performActiveTabAction(primaryViewController); + } +} + +@end diff --git a/Application/Sources/UI/Controllers/TabBarController.m b/Application/Sources/UI/Controllers/TabBarController.m index 213303aa6..14e9cdbf1 100755 --- a/Application/Sources/UI/Controllers/TabBarController.m +++ b/Application/Sources/UI/Controllers/TabBarController.m @@ -15,8 +15,9 @@ #import "ProfileViewController.h" #import "PushService.h" #import "RadioChannelsViewController.h" -#import "Scrollable.h" #import "SearchViewController.h" +#import "SplitViewController.h" +#import "TabBarActionable.h" #import "UIColor+PlaySRG.h" #import @@ -43,73 +44,28 @@ - (instancetype)init if (self = [super init]) { self.delegate = self; - ApplicationConfiguration *applicationConfiguration = ApplicationConfiguration.sharedApplicationConfiguration; - NSMutableArray *viewControllers = NSMutableArray.array; - NSMutableArray *tabBarItems = NSMutableArray.array; - - ApplicationSectionInfo *videosApplicationSectionInfo = [ApplicationSectionInfo applicationSectionInfoWithApplicationSection:ApplicationSectionOverview radioChannel:nil]; - UIViewController *videosViewController = [[HomeViewController alloc] initWithApplicationSectionInfo:videosApplicationSectionInfo homeSections:applicationConfiguration.videoHomeSections]; - videosViewController.title = NSLocalizedString(@"Videos", @"Title displayed at the top of the video view"); - [viewControllers addObject:videosViewController]; - UITabBarItem *videosTabBarItem = [[UITabBarItem alloc] initWithTitle:videosViewController.title image:[UIImage imageNamed:@"videos-24"] tag:TabBarItemIdentifierVideos]; - videosTabBarItem.accessibilityIdentifier = AccessibilityIdentifierVideosTabBarItem; - [tabBarItems addObject:videosTabBarItem]; - UIViewController *audiosViewController = nil; - NSArray *radioChannels = applicationConfiguration.radioChannels; - if (radioChannels.count > 1) { - audiosViewController = [[RadioChannelsViewController alloc] initWithRadioChannels:radioChannels]; - } - else if (radioChannels.count == 1) { - RadioChannel *radioChannel = radioChannels.firstObject; - ApplicationSectionInfo *audiosApplicationSectionInfo = [ApplicationSectionInfo applicationSectionInfoWithApplicationSection:ApplicationSectionOverview radioChannel:radioChannel]; - audiosViewController = [[HomeViewController alloc] initWithApplicationSectionInfo:audiosApplicationSectionInfo homeSections:radioChannel.homeSections]; - audiosViewController.title = NSLocalizedString(@"Audios", @"Title displayed at the top of the audio view"); - } + UIViewController *videosTabViewController = [self videosTabViewController]; + [viewControllers addObject:videosTabViewController]; - if (audiosViewController) { - [viewControllers addObject:audiosViewController]; - UITabBarItem *audiosTabBarItem = [[UITabBarItem alloc] initWithTitle:audiosViewController.title image:[UIImage imageNamed:@"audios-24"] tag:TabBarItemIdentifierAudios]; - audiosTabBarItem.accessibilityIdentifier = AccessibilityIdentifierAudiosTabBarItem; - [tabBarItems addObject:audiosTabBarItem]; + UIViewController *audiosTabViewController = [self audiosTabViewController]; + if (audiosTabViewController) { + [viewControllers addObject:audiosTabViewController]; } - NSArray *liveHomeSections = ApplicationConfiguration.sharedApplicationConfiguration.liveHomeSections; - if (liveHomeSections.count != 0) { - ApplicationSectionInfo *liveApplicationSectionInfo = [ApplicationSectionInfo applicationSectionInfoWithApplicationSection:ApplicationSectionLive radioChannel:nil]; - UIViewController *liveHomeViewController = [[HomeViewController alloc] initWithApplicationSectionInfo:liveApplicationSectionInfo homeSections:liveHomeSections]; - liveHomeViewController.title = NSLocalizedString(@"Livestreams", @"Title displayed at the top of the livestream view"); - [viewControllers addObject:liveHomeViewController]; - UITabBarItem *liveTabBarItem = [[UITabBarItem alloc] initWithTitle:liveHomeViewController.title image:[UIImage imageNamed:@"livestreams-24"] tag:TabBarItemIdentifierLivestreams]; - liveTabBarItem.accessibilityIdentifier = AccessibilityIdentifierLivestreamsTabBarItem; - [tabBarItems addObject:liveTabBarItem]; + UIViewController *livestreamsTabViewController = [self livestreamsTabViewController]; + if (livestreamsTabViewController) { + [viewControllers addObject:livestreamsTabViewController]; } - UIViewController *searchViewController = [[SearchViewController alloc] init]; - [viewControllers addObject:searchViewController]; - UITabBarItem *searchTabBarItem = [[UITabBarItem alloc] initWithTitle:searchViewController.title image:[UIImage imageNamed:@"search-24"] tag:TabBarItemIdentifierSearch]; - searchTabBarItem.accessibilityIdentifier = AccessibilityIdentifierSearchTabBarItem; - [tabBarItems addObject:searchTabBarItem]; + UIViewController *searchTabViewController = [self searchTabViewController]; + [viewControllers addObject:searchTabViewController]; - UIViewController *profileViewController = [[ProfileViewController alloc] init]; - [viewControllers addObject:profileViewController]; - UITabBarItem *profileTabBarItem = [[UITabBarItem alloc] initWithTitle:profileViewController.title image:[UIImage imageNamed:@"profile-24"] tag:TabBarItemIdentifierProfile]; - profileTabBarItem.accessibilityIdentifier = AccessibilityIdentifierProfileTabBarItem; - [tabBarItems addObject:profileTabBarItem]; + UIViewController *profileTabViewController = [self profileTabViewController]; + [viewControllers addObject:profileTabViewController]; - NSMutableArray *navigationControllers = NSMutableArray.array; - [viewControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { - NavigationController *navigationController = [[NavigationController alloc] initWithRootViewController:viewController]; - navigationController.tabBarItem = tabBarItems[idx]; - [navigationControllers addObject:navigationController]; - - if ([viewController isKindOfClass:HomeViewController.class]) { - HomeViewController *homeViewController = (HomeViewController *)viewController; - [navigationController updateWithRadioChannel:homeViewController.radioChannel animated:NO]; - } - }]; - self.viewControllers = navigationControllers.copy; + self.viewControllers = viewControllers.copy; if (@available(iOS 13, *)) { self.tabBar.barTintColor = nil; @@ -248,6 +204,107 @@ - (void)setSelectedViewController:(UIViewController *)selectedViewController ApplicationSettingSetLastOpenedTabBarItemIdentifier(selectedViewController.tabBarItem.tag); } +#pragma mark View controllers + +- (UIViewController *)videosTabViewController +{ + ApplicationConfiguration *applicationConfiguration = ApplicationConfiguration.sharedApplicationConfiguration; + + ApplicationSectionInfo *videosApplicationSectionInfo = [ApplicationSectionInfo applicationSectionInfoWithApplicationSection:ApplicationSectionOverview radioChannel:nil]; + UIViewController *videosViewController = [[HomeViewController alloc] initWithApplicationSectionInfo:videosApplicationSectionInfo homeSections:applicationConfiguration.videoHomeSections]; + videosViewController.title = NSLocalizedString(@"Videos", @"Title displayed at the top of the video view"); + + UITabBarItem *videosTabBarItem = [[UITabBarItem alloc] initWithTitle:videosViewController.title image:[UIImage imageNamed:@"videos-24"] tag:TabBarItemIdentifierVideos]; + videosTabBarItem.accessibilityIdentifier = AccessibilityIdentifierVideosTabBarItem; + + NavigationController *videosNavigationController = [[NavigationController alloc] initWithRootViewController:videosViewController]; + videosNavigationController.tabBarItem = videosTabBarItem; + return videosNavigationController; +} + +- (UIViewController *)audiosTabViewController +{ + ApplicationConfiguration *applicationConfiguration = ApplicationConfiguration.sharedApplicationConfiguration; + + NSArray *radioChannels = applicationConfiguration.radioChannels; + if (radioChannels.count > 1) { + UIViewController *radioChannelsViewController = [[RadioChannelsViewController alloc] initWithRadioChannels:radioChannels]; + + UITabBarItem *audiosTabBarItem = [[UITabBarItem alloc] initWithTitle:radioChannelsViewController.title image:[UIImage imageNamed:@"audios-24"] tag:TabBarItemIdentifierAudios]; + audiosTabBarItem.accessibilityIdentifier = AccessibilityIdentifierAudiosTabBarItem; + + NavigationController *audiosNavigationController = [[NavigationController alloc] initWithRootViewController:radioChannelsViewController]; + audiosNavigationController.tabBarItem = audiosTabBarItem; + return audiosNavigationController; + } + else if (radioChannels.count == 1) { + RadioChannel *radioChannel = radioChannels.firstObject; + ApplicationSectionInfo *audiosApplicationSectionInfo = [ApplicationSectionInfo applicationSectionInfoWithApplicationSection:ApplicationSectionOverview radioChannel:radioChannel]; + UIViewController *audiosViewController = [[HomeViewController alloc] initWithApplicationSectionInfo:audiosApplicationSectionInfo homeSections:radioChannel.homeSections]; + audiosViewController.title = NSLocalizedString(@"Audios", @"Title displayed at the top of the audio view"); + + UITabBarItem *audiosTabBarItem = [[UITabBarItem alloc] initWithTitle:audiosViewController.title image:[UIImage imageNamed:@"audios-24"] tag:TabBarItemIdentifierAudios]; + audiosTabBarItem.accessibilityIdentifier = AccessibilityIdentifierAudiosTabBarItem; + + NavigationController *audiosNavigationController = [[NavigationController alloc] initWithRootViewController:audiosViewController]; + audiosNavigationController.tabBarItem = audiosTabBarItem; + [audiosNavigationController updateWithRadioChannel:radioChannel animated:NO]; + return audiosNavigationController; + } + else { + return nil; + } +} + +- (UIViewController *)livestreamsTabViewController +{ + ApplicationConfiguration *applicationConfiguration = ApplicationConfiguration.sharedApplicationConfiguration; + + NSArray *liveHomeSections = applicationConfiguration.liveHomeSections; + if (liveHomeSections.count != 0) { + ApplicationSectionInfo *liveApplicationSectionInfo = [ApplicationSectionInfo applicationSectionInfoWithApplicationSection:ApplicationSectionLive radioChannel:nil]; + UIViewController *liveHomeViewController = [[HomeViewController alloc] initWithApplicationSectionInfo:liveApplicationSectionInfo homeSections:liveHomeSections]; + liveHomeViewController.title = NSLocalizedString(@"Livestreams", @"Title displayed at the top of the livestream view"); + + UITabBarItem *liveTabBarItem = [[UITabBarItem alloc] initWithTitle:liveHomeViewController.title image:[UIImage imageNamed:@"livestreams-24"] tag:TabBarItemIdentifierLivestreams]; + liveTabBarItem.accessibilityIdentifier = AccessibilityIdentifierLivestreamsTabBarItem; + + NavigationController *liveNavigationController = [[NavigationController alloc] initWithRootViewController:liveHomeViewController]; + liveNavigationController.tabBarItem = liveTabBarItem; + return liveNavigationController; + } + else { + return nil; + } +} + +- (UIViewController *)searchTabViewController +{ + UIViewController *searchViewController = [[SearchViewController alloc] init]; + + UITabBarItem *searchTabBarItem = [[UITabBarItem alloc] initWithTitle:searchViewController.title image:[UIImage imageNamed:@"search-24"] tag:TabBarItemIdentifierSearch]; + searchTabBarItem.accessibilityIdentifier = AccessibilityIdentifierSearchTabBarItem; + + NavigationController *searchNavigationController = [[NavigationController alloc] initWithRootViewController:searchViewController]; + searchNavigationController.tabBarItem = searchTabBarItem; + return searchNavigationController; +} + +- (UIViewController *)profileTabViewController +{ + UIViewController *profileViewController = [[ProfileViewController alloc] init]; + NavigationController *profileNavigationController = [[NavigationController alloc] initWithRootViewController:profileViewController]; + + UITabBarItem *profileTabBarItem = [[UITabBarItem alloc] initWithTitle:profileViewController.title image:[UIImage imageNamed:@"profile-24"] tag:TabBarItemIdentifierProfile]; + profileTabBarItem.accessibilityIdentifier = AccessibilityIdentifierProfileTabBarItem; + + SplitViewController *profileSplitViewController = [[SplitViewController alloc] init]; + profileSplitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; + profileSplitViewController.viewControllers = @[ profileNavigationController ]; + profileSplitViewController.tabBarItem = profileTabBarItem; + return profileSplitViewController; +} + #pragma mark Layout - (void)updateLayoutAnimated:(BOOL)animated @@ -375,9 +432,9 @@ - (BOOL)openApplicationSectionInfo:(ApplicationSectionInfo *)applicationSectionI - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { if (viewController == self.selectedViewController) { - if ([viewController conformsToProtocol:@protocol(Scrollable)]) { - UIViewController *scrollableViewController = (UIViewController *)viewController; - [scrollableViewController scrollToTopAnimated:YES]; + if ([viewController conformsToProtocol:@protocol(TabBarActionable)]) { + UIViewController *actionableViewController = (UIViewController *)viewController; + [actionableViewController performActiveTabActionAnimated:YES]; } } return YES; diff --git a/Application/Sources/UI/Controllers/TableRequestViewController.h b/Application/Sources/UI/Controllers/TableRequestViewController.h index ed3e71701..62eabd15e 100755 --- a/Application/Sources/UI/Controllers/TableRequestViewController.h +++ b/Application/Sources/UI/Controllers/TableRequestViewController.h @@ -6,7 +6,7 @@ #import "ContentInsets.h" #import "ListRequestViewController.h" -#import "Scrollable.h" +#import "TabBarActionable.h" #import @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN * - Automatic display of a load more footer if more data is available * - Clean display of empty tables, whether because of an error or because no items are available */ -@interface TableRequestViewController : ListRequestViewController +@interface TableRequestViewController : ListRequestViewController /** * The table view. diff --git a/Application/Sources/UI/Controllers/TableRequestViewController.m b/Application/Sources/UI/Controllers/TableRequestViewController.m index a03a7b2fb..241552d9f 100755 --- a/Application/Sources/UI/Controllers/TableRequestViewController.m +++ b/Application/Sources/UI/Controllers/TableRequestViewController.m @@ -293,9 +293,9 @@ - (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView return VerticalOffsetForEmptyDataSet(scrollView); } -#pragma mark Scrollable protocol +#pragma mark TabBarActionable protocol -- (void)scrollToTopAnimated:(BOOL)animated +- (void)performActiveTabActionAnimated:(BOOL)animated { [self.tableView play_scrollToTopAnimated:animated]; } diff --git a/Application/Sources/UI/Helpers/Scrollable.h b/Application/Sources/UI/Helpers/Scrollable.h deleted file mode 100644 index 2e3e98233..000000000 --- a/Application/Sources/UI/Helpers/Scrollable.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * Protocol for view controllers with 'scroll to the top' support. - */ -@protocol Scrollable - -- (void)scrollToTopAnimated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Application/Sources/UI/Helpers/SwimlaneCollectionViewLayout.h b/Application/Sources/UI/Helpers/SwimlaneCollectionViewLayout.h new file mode 100644 index 000000000..d1ae01bbd --- /dev/null +++ b/Application/Sources/UI/Helpers/SwimlaneCollectionViewLayout.h @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Standard flow layout for use in swimlanes, implementing snapping at item boundaries. + * + * @discussion Can be used in horizontal direction only. + */ +@interface SwimlaneCollectionViewLayout : UICollectionViewFlowLayout + +@end + +NS_ASSUME_NONNULL_END diff --git a/Application/Sources/UI/Helpers/SwimlaneCollectionViewLayout.m b/Application/Sources/UI/Helpers/SwimlaneCollectionViewLayout.m new file mode 100644 index 000000000..3a2823393 --- /dev/null +++ b/Application/Sources/UI/Helpers/SwimlaneCollectionViewLayout.m @@ -0,0 +1,94 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +#import "SwimlaneCollectionViewLayout.h" + +@implementation SwimlaneCollectionViewLayout + +#pragma mark Overrides + +- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity +{ + NSAssert(self.scrollDirection == UICollectionViewScrollDirectionHorizontal, @"Currently only implemented for horizontal layout direction"); + + // If already at the beginning or the end, stays there + CGFloat maxX = fmaxf(self.collectionViewContentSize.width - CGRectGetWidth(self.collectionView.frame), 0.f); + if (proposedContentOffset.x >= maxX) { + return CGPointMake(maxX, proposedContentOffset.y); + } + else if (proposedContentOffset.x <= 0.f) { + return CGPointMake(0.f, proposedContentOffset.y); + } + + // Extract attributes for all items which would be displayed at the proposed offset (sort them to have cells + // and supplementary views correctly ordered altogether) + CGRect proposedRect = CGRectMake(proposedContentOffset.x, + proposedContentOffset.y, + CGRectGetWidth(self.collectionView.bounds), + CGRectGetHeight(self.collectionView.bounds)); + NSArray *layoutAttributesInProposedRect = [[self layoutAttributesForElementsInRect:proposedRect] sortedArrayUsingComparator:^NSComparisonResult(UICollectionViewLayoutAttributes * _Nonnull layoutAttributes1, UICollectionViewLayoutAttributes * _Nonnull layoutAttributes2) { + CGFloat x1 = CGRectGetMinX(layoutAttributes1.frame); + CGFloat x2 = CGRectGetMinX(layoutAttributes2.frame); + + if (x1 == x2) { + return NSOrderedSame; + } + else if (x1 < x2) { + return NSOrderedAscending; + } + else { + return NSOrderedDescending; + } + }]; + + // No item displayed in the rect + if (layoutAttributesInProposedRect.count == 0) { + return proposedContentOffset; + } + + UICollectionViewLayoutAttributes *proposedLayoutAttributes = nil; + + // Decide on which one of the first two items we should snap (if more than two items) + UICollectionViewLayoutAttributes *layoutAttributes0 = layoutAttributesInProposedRect.firstObject; + if (layoutAttributesInProposedRect.count > 1) { + UICollectionViewLayoutAttributes *layoutAttributes1 = layoutAttributesInProposedRect[1]; + + // Moving to the right. Snap on the second item + if (velocity.x > 0.f) { + proposedLayoutAttributes = layoutAttributes1; + } + // Moving to the left. Snap on the first item + else if (velocity.x < 0.f) { + proposedLayoutAttributes = layoutAttributes0; + } + // Still. Snap on the first item is at least half of it is visible + else { + CGRect visibleRect0 = CGRectIntersection(layoutAttributes0.frame, proposedRect); + + if (CGRectGetWidth(visibleRect0) < 0.5f * CGRectGetWidth(layoutAttributes0.frame)) { + proposedLayoutAttributes = layoutAttributes1; + } + else { + proposedLayoutAttributes = layoutAttributes0; + } + } + } + // Snap on the only available item + else { + proposedLayoutAttributes = layoutAttributes0; + } + + // Use twice the margin to snap not at item boundaries but a little before, so that previous items are slightly visible + CGFloat snapXOffset = fmaxf(CGRectGetMinX(proposedLayoutAttributes.frame) - 2 * self.minimumInteritemSpacing, 0.f); + return CGPointMake(snapXOffset, proposedContentOffset.y); +} + +- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset +{ + return [self targetContentOffsetForProposedContentOffset:proposedContentOffset withScrollingVelocity:CGPointZero]; +} + +@end diff --git a/Application/Sources/UI/Helpers/TabBarActionable.h b/Application/Sources/UI/Helpers/TabBarActionable.h new file mode 100644 index 000000000..2f61f62b3 --- /dev/null +++ b/Application/Sources/UI/Helpers/TabBarActionable.h @@ -0,0 +1,23 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Protocol implemented by view controllers to respond to tab bar actions. + */ +@protocol TabBarActionable + +/** + * Called when the currently active tab has been tapped again. + */ +- (void)performActiveTabActionAnimated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.m b/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.m index d5f7661e8..645a13a87 100755 --- a/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.m +++ b/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.m @@ -7,6 +7,7 @@ #import "HomeLiveMediaCollectionViewCell.h" #import "AnalyticsConstants.h" +#import "ApplicationSettings.h" #import "ChannelService.h" #import "Layout.h" #import "NSBundle+PlaySRG.h" @@ -33,6 +34,7 @@ @interface HomeLiveMediaCollectionViewCell () @property (nonatomic, weak) IBOutlet UIImageView *placeholderImageView; @property (nonatomic, weak) IBOutlet UIImageView *logoImageView; +@property (nonatomic, weak) IBOutlet UILabel *recentLabel; @property (nonatomic, weak) IBOutlet UILabel *titleLabel; @property (nonatomic, weak) IBOutlet UILabel *subtitleLabel; @property (nonatomic, weak) IBOutlet UIImageView *thumbnailImageView; @@ -73,6 +75,12 @@ - (void)awakeFromNib self.thumbnailImageView.layer.cornerRadius = LayoutStandardViewCornerRadius; self.thumbnailImageView.layer.masksToBounds = YES; + self.recentLabel.layer.cornerRadius = LayoutStandardLabelCornerRadius; + self.recentLabel.layer.masksToBounds = YES; + self.recentLabel.backgroundColor = UIColor.play_blackDurationLabelBackgroundColor; + self.recentLabel.text = [NSString stringWithFormat:@" %@ ", NSLocalizedString(@"Last played", @"Label on recently played livestreams").uppercaseString]; + self.recentLabel.hidden = YES; + self.durationLabel.backgroundColor = UIColor.play_blackDurationLabelBackgroundColor; self.blockingOverlayView.hidden = YES; @@ -136,6 +144,9 @@ - (NSString *)accessibilityLabel { if (self.media.contentType == SRGContentTypeLivestream) { NSMutableString *accessibilityLabel = [NSMutableString stringWithFormat:PlaySRGAccessibilityLocalizedString(@"%@ live", @"Live content label, with a channel title"), self.channel.title]; + if (! self.recentLabel.hidden) { + [accessibilityLabel appendFormat:@", %@", PlaySRGAccessibilityLocalizedString(@"Last played", @"Label on recently played livestreams")]; + } if (self.channel.currentProgram) { [accessibilityLabel appendFormat:@", %@", self.channel.currentProgram.title]; } @@ -207,6 +218,10 @@ - (void)reloadData self.titleLabel.font = [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleBody]; self.durationLabel.font = [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleCaption]; + self.recentLabel.font = [UIFont srg_mediumFontWithTextStyle:SRGAppearanceFontTextStyleCaption]; + self.recentLabel.hidden = ! [self.media.URN isEqualToString:ApplicationSettingLastSelectedTVLivestreamURN()] + && ! [self.media.URN isEqualToString:ApplicationSettingLastSelectedRadioLivestreamURN()]; + SRGBlockingReason blockingReason = [self.media blockingReasonAtDate:NSDate.date]; if (blockingReason == SRGBlockingReasonNone || blockingReason == SRGBlockingReasonStartDate) { self.blockingOverlayView.hidden = YES; diff --git a/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.xib b/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.xib index d799c4977..e10483cdd 100755 --- a/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.xib +++ b/Application/Sources/UI/Views/HomeLiveMediaCollectionViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -147,9 +147,22 @@ + + @@ -158,6 +171,7 @@ + @@ -188,6 +202,7 @@ + diff --git a/Cartfile b/Cartfile index 610cb1cdd..c38360338 100755 --- a/Cartfile +++ b/Cartfile @@ -1,5 +1,5 @@ github "defagos/CoconutKit" "3.4" -github "Flipboard/FLEX" "6d489e72c52401839386ee41aeefe1f5105c212e" +github "Flipboard/FLEX" "4.1.1" github "mapbox/Fingertips" "cdffabac5506103a2c7cc5aedeed4021df2501da" github "microsoft/appcenter-sdk-apple" ~> 3.1.0 github "SRGSSR/DZNEmptyDataSet" "v1.8.1_srg1" diff --git a/Cartfile.resolved.proprietary b/Cartfile.resolved.proprietary index 0dd947123..3f0763e50 100755 --- a/Cartfile.resolved.proprietary +++ b/Cartfile.resolved.proprietary @@ -1,4 +1,4 @@ -github "Flipboard/FLEX" "6d489e72c52401839386ee41aeefe1f5105c212e" +github "Flipboard/FLEX" "4.1.1" github "Mantle/Mantle" "2.1.0" github "SRGSSR/ComScore-iOS-watchOS-tvOS" "6.2.0" github "SRGSSR/DZNEmptyDataSet" "v1.8.1_srg1" diff --git a/Cartfile.resolved.public b/Cartfile.resolved.public index 7823b1214..b95bca851 100755 --- a/Cartfile.resolved.public +++ b/Cartfile.resolved.public @@ -1,4 +1,4 @@ -github "Flipboard/FLEX" "6d489e72c52401839386ee41aeefe1f5105c212e" +github "Flipboard/FLEX" "4.1.1" github "Mantle/Mantle" "2.1.0" github "SRGSSR/ComScore-iOS-watchOS-tvOS" "6.2.0" github "SRGSSR/DZNEmptyDataSet" "v1.8.1_srg1" diff --git a/PlaySRG.xcodeproj/project.pbxproj b/PlaySRG.xcodeproj/project.pbxproj index 048f6a479..221168ab6 100644 --- a/PlaySRG.xcodeproj/project.pbxproj +++ b/PlaySRG.xcodeproj/project.pbxproj @@ -475,6 +475,11 @@ 6F00875F219AA888004BD6FF /* StoreReview.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F00875C219AA888004BD6FF /* StoreReview.m */; }; 6F008760219AA888004BD6FF /* StoreReview.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F00875C219AA888004BD6FF /* StoreReview.m */; }; 6F008761219AA888004BD6FF /* StoreReview.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F00875C219AA888004BD6FF /* StoreReview.m */; }; + 6F0506E7245468EE0053253E /* SplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0506E6245468EE0053253E /* SplitViewController.m */; }; + 6F0506E8245468EE0053253E /* SplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0506E6245468EE0053253E /* SplitViewController.m */; }; + 6F0506E9245468EE0053253E /* SplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0506E6245468EE0053253E /* SplitViewController.m */; }; + 6F0506EA245468EE0053253E /* SplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0506E6245468EE0053253E /* SplitViewController.m */; }; + 6F0506EB245468EE0053253E /* SplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0506E6245468EE0053253E /* SplitViewController.m */; }; 6F06E0A41DB7791300220FC6 /* ApplicationSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F06E0A31DB7791300220FC6 /* ApplicationSettings.m */; }; 6F06E0A51DB7791300220FC6 /* ApplicationSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F06E0A31DB7791300220FC6 /* ApplicationSettings.m */; }; 6F06E0A61DB7791300220FC6 /* ApplicationSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F06E0A31DB7791300220FC6 /* ApplicationSettings.m */; }; @@ -879,11 +884,6 @@ 6F4760361EB37BD1003021EA /* NSDateFormatter+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F47601C1EB37BD1003021EA /* NSDateFormatter+PlaySRG.m */; }; 6F4760371EB37BD1003021EA /* NSDateFormatter+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F47601C1EB37BD1003021EA /* NSDateFormatter+PlaySRG.m */; }; 6F4760381EB37BD1003021EA /* NSDateFormatter+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F47601C1EB37BD1003021EA /* NSDateFormatter+PlaySRG.m */; }; - 6F47603E1EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */; }; - 6F47603F1EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */; }; - 6F4760401EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */; }; - 6F4760411EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */; }; - 6F4760421EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */; }; 6F4760781EB37D60003021EA /* UIColor+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F47606B1EB37D60003021EA /* UIColor+PlaySRG.m */; }; 6F4760791EB37D60003021EA /* UIDevice+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F47606D1EB37D60003021EA /* UIDevice+PlaySRG.m */; }; 6F47607A1EB37D60003021EA /* UIImage+PlaySRG.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F47606F1EB37D60003021EA /* UIImage+PlaySRG.m */; }; @@ -1376,6 +1376,11 @@ 6FF7BBB422A7901900FA758A /* SearchSettingsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF7BBB122A7901900FA758A /* SearchSettingsViewController.storyboard */; }; 6FF7BBB522A7901900FA758A /* SearchSettingsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF7BBB122A7901900FA758A /* SearchSettingsViewController.storyboard */; }; 6FF7BBB622A7901900FA758A /* SearchSettingsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF7BBB122A7901900FA758A /* SearchSettingsViewController.storyboard */; }; + 6FFCB62424504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */; }; + 6FFCB62524504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */; }; + 6FFCB62624504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */; }; + 6FFCB62724504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */; }; + 6FFCB62824504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */; }; 7ACE7CA000370D1208243956 /* libPods-PlaySRG-Play SRF.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D6E859054BE4FCA7A62352E /* libPods-PlaySRG-Play SRF.a */; }; E1D8F87EB9409ADB326F446F /* libPods-PlaySRG-Play RTR.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 866AF638938EFA7338C23B03 /* libPods-PlaySRG-Play RTR.a */; }; E60178521D635E130000362E /* ComScore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E60178511D635E130000362E /* ComScore.framework */; }; @@ -1880,6 +1885,8 @@ 5CA73886C1B816E3660CB5E6 /* Pods-PlaySRG-Play SRF.nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlaySRG-Play SRF.nightly.xcconfig"; path = "Pods/Target Support Files/Pods-PlaySRG-Play SRF/Pods-PlaySRG-Play SRF.nightly.xcconfig"; sourceTree = ""; }; 6F00875B219AA888004BD6FF /* StoreReview.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StoreReview.h; sourceTree = ""; }; 6F00875C219AA888004BD6FF /* StoreReview.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StoreReview.m; sourceTree = ""; }; + 6F0506E5245468EE0053253E /* SplitViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SplitViewController.h; sourceTree = ""; }; + 6F0506E6245468EE0053253E /* SplitViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SplitViewController.m; sourceTree = ""; }; 6F05F254203C2F0200EEA894 /* UIViewController+PlaySRG_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+PlaySRG_Private.h"; sourceTree = ""; }; 6F06E0A21DB7791300220FC6 /* ApplicationSettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApplicationSettings.h; sourceTree = ""; }; 6F06E0A31DB7791300220FC6 /* ApplicationSettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApplicationSettings.m; sourceTree = ""; }; @@ -2027,8 +2034,6 @@ 6F47601A1EB37BD1003021EA /* PlayDurationFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayDurationFormatter.m; sourceTree = ""; }; 6F47601B1EB37BD1003021EA /* NSDateFormatter+PlaySRG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDateFormatter+PlaySRG.h"; sourceTree = ""; }; 6F47601C1EB37BD1003021EA /* NSDateFormatter+PlaySRG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDateFormatter+PlaySRG.m"; sourceTree = ""; }; - 6F47601F1EB37BD1003021EA /* UICollectionView+PlaySRG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+PlaySRG.h"; sourceTree = ""; }; - 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+PlaySRG.m"; sourceTree = ""; }; 6F47606A1EB37D60003021EA /* UIColor+PlaySRG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+PlaySRG.h"; sourceTree = ""; }; 6F47606B1EB37D60003021EA /* UIColor+PlaySRG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+PlaySRG.m"; sourceTree = ""; }; 6F47606C1EB37D60003021EA /* UIDevice+PlaySRG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIDevice+PlaySRG.h"; sourceTree = ""; }; @@ -2138,7 +2143,7 @@ 6FB0BB3C20AEF5C4007C5D87 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Onboarding.strings; sourceTree = ""; }; 6FB0BB3F20AEF5D1007C5D87 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Onboarding.strings; sourceTree = ""; }; 6FB0BB4220AEF5DC007C5D87 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Onboarding.strings; sourceTree = ""; }; - 6FB340D823E1A21500BC83BF /* Scrollable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Scrollable.h; sourceTree = ""; }; + 6FB340D823E1A21500BC83BF /* TabBarActionable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TabBarActionable.h; sourceTree = ""; }; 6FB3C1A51DBCE783008160B4 /* MediaPreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaPreviewViewController.h; sourceTree = ""; }; 6FB3C1A61DBCE783008160B4 /* MediaPreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaPreviewViewController.m; sourceTree = ""; }; 6FB3C1A71DBCE783008160B4 /* MediaPreviewViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MediaPreviewViewController.storyboard; sourceTree = ""; }; @@ -2230,6 +2235,8 @@ 6FF7BB9522A78DE400FA758A /* SearchSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SearchSettingsViewController.m; sourceTree = ""; }; 6FF7BB9622A78DE400FA758A /* SearchSettingSelectorCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SearchSettingSelectorCell.m; sourceTree = ""; }; 6FF7BBB122A7901900FA758A /* SearchSettingsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SearchSettingsViewController.storyboard; sourceTree = ""; }; + 6FFCB62224504C6C00D16466 /* SwimlaneCollectionViewLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwimlaneCollectionViewLayout.h; sourceTree = ""; }; + 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SwimlaneCollectionViewLayout.m; sourceTree = ""; }; 866AF638938EFA7338C23B03 /* libPods-PlaySRG-Play RTR.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PlaySRG-Play RTR.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9BDBB65E7B9D845CB1576750 /* Pods-PlaySRG-Play RTS.nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlaySRG-Play RTS.nightly.xcconfig"; path = "Pods/Target Support Files/Pods-PlaySRG-Play RTS/Pods-PlaySRG-Play RTS.nightly.xcconfig"; sourceTree = ""; }; A0923E657497DE17CE057F6E /* Pods-PlaySRG-Play RTR.nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlaySRG-Play RTR.nightly.xcconfig"; path = "Pods/Target Support Files/Pods-PlaySRG-Play RTR/Pods-PlaySRG-Play RTR.nightly.xcconfig"; sourceTree = ""; }; @@ -3225,6 +3232,8 @@ 6F475F9A1EB37BC6003021EA /* PageViewController.m */, 6F475F9C1EB37BC6003021EA /* RequestViewController.h */, 6F475F9D1EB37BC6003021EA /* RequestViewController.m */, + 6F0506E5245468EE0053253E /* SplitViewController.h */, + 6F0506E6245468EE0053253E /* SplitViewController.m */, 082910AC239E90D200D168F4 /* TabBarController.h */, 082910AD239E90D200D168F4 /* TabBarController.m */, 6F80E9C021A682E60027CA2F /* TableRequestViewController.h */, @@ -3240,9 +3249,12 @@ 6F475FA01EB37BC6003021EA /* ModalTransition.m */, 6F475FA11EB37BC6003021EA /* Previewing.h */, 6F4091FF22DCF43D005F3850 /* Previewing.m */, + 6FB340D823E1A21500BC83BF /* Scrollable.h */, + 6FFCB62224504C6C00D16466 /* SwimlaneCollectionViewLayout.h */, + 6FFCB62324504C6C00D16466 /* SwimlaneCollectionViewLayout.m */, 6FF4D4DE23D5B07E008B981A /* UIVisualEffectView+PlaySRG.h */, 6FF4D4DF23D5B07E008B981A /* UIVisualEffectView+PlaySRG.m */, - 6FB340D823E1A21500BC83BF /* Scrollable.h */, + 6FB340D823E1A21500BC83BF /* TabBarActionable.h */, ); path = Helpers; sourceTree = ""; @@ -3311,8 +3323,6 @@ 6F7C35B423708E8A00259BE7 /* SRGResource+PlaySRG.m */, 0852539522073D5200BCF0B1 /* UIApplication+PlaySRG.h */, 0852539622073D5200BCF0B1 /* UIApplication+PlaySRG.m */, - 6F47601F1EB37BD1003021EA /* UICollectionView+PlaySRG.h */, - 6F4760201EB37BD1003021EA /* UICollectionView+PlaySRG.m */, 6F47606A1EB37D60003021EA /* UIColor+PlaySRG.h */, 6F47606B1EB37D60003021EA /* UIColor+PlaySRG.m */, 6F47606C1EB37D60003021EA /* UIDevice+PlaySRG.h */, @@ -5895,6 +5905,7 @@ 6F9D2743203AD99C00FDE899 /* Playlist.m in Sources */, 086321052258C6D000C719A6 /* WatchLaterTableViewCell.m in Sources */, E66BEC1C1DA7FCED00AD4450 /* MediaPlayerViewController.m in Sources */, + 6FFCB62424504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */, 6F475FC31EB37BC6003021EA /* ListRequestViewController.m in Sources */, 6F475FFF1EB37BC6003021EA /* MediaCollectionViewCell.m in Sources */, 6F12E4E222D8676600BC1718 /* SearchHeaderView.m in Sources */, @@ -5907,6 +5918,7 @@ 08AA55BB1D49EFBD00C5026E /* HomeTableViewCell.m in Sources */, 0806E7B81D50D918002ED406 /* SettingsViewController.m in Sources */, 081220C11DD0ADAC00BF8326 /* DownloadSession.m in Sources */, + 6F0506E7245468EE0053253E /* SplitViewController.m in Sources */, E6E2DF1A1D6ACA8B00791EDE /* ShowViewController.m in Sources */, 6F475FDC1EB37BC6003021EA /* RequestViewController.m in Sources */, 6F475FF51EB37BC6003021EA /* CollectionLoadMoreFooterView.m in Sources */, @@ -6010,7 +6022,6 @@ 6F475FCD1EB37BC6003021EA /* NavigationController.m in Sources */, 6FDF08FA218B126700B2AF2C /* DeprecatedFavorite.m in Sources */, 08209308208F522A00711DE4 /* PushService.m in Sources */, - 6F47603E1EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */, 6F4760091EB37BC6003021EA /* TranslucentTitleHeaderView.m in Sources */, 08C68F8A1D38DEA100BB8AAA /* PlayAppDelegate.m in Sources */, 6F40920822DCFE2B005F3850 /* MostSearchedShowCollectionViewCell.m in Sources */, @@ -6172,7 +6183,6 @@ 6F7C35B623708E8A00259BE7 /* SRGResource+PlaySRG.m in Sources */, 081220AE1DD07B7A00BF8326 /* DownloadsViewController.m in Sources */, 087BC6641EDF1B7C00EED89B /* UILabel+PlaySRG.m in Sources */, - 6F47603F1EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */, 082910B6239EA69C00D168F4 /* RadioChannelsViewController.m in Sources */, E694A9F61D65F02700372DF0 /* CalendarViewController.m in Sources */, 6FD88F8B22D4BC41008859EF /* UISearchBar+PlaySRG.m in Sources */, @@ -6182,6 +6192,7 @@ E65FB20C1D66D69700820696 /* DailyMediasViewController.m in Sources */, 6FEC91AA21A6B39A00AA50C8 /* TableLoadMoreFooterView.m in Sources */, 6F4760831EB37DF1003021EA /* UIStackView+PlaySRG.m in Sources */, + 6FFCB62524504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */, 6F06E0A51DB7791300220FC6 /* ApplicationSettings.m in Sources */, 08564B1E1D41111A00381549 /* HomeSectionHeaderView.m in Sources */, 6FE686E21EB9D57400067D40 /* ChannelService.m in Sources */, @@ -6191,6 +6202,7 @@ 08C68F8B1D38DEA100BB8AAA /* PlayAppDelegate.m in Sources */, 08B77AF1240A86FD00A3BC3B /* AccessibilityIdentifierConstants.m in Sources */, 0875851423A0025F00FA7207 /* ProfileAccountHeaderView.m in Sources */, + 6F0506E8245468EE0053253E /* SplitViewController.m in Sources */, 6F4760821EB37DF1003021EA /* UIImageView+PlaySRG.m in Sources */, 087584F023A0008500FA7207 /* ApplicationSectionInfo.m in Sources */, 6F4760301EB37BD1003021EA /* PlayDurationFormatter.m in Sources */, @@ -6336,7 +6348,6 @@ 6F7C35B723708E8A00259BE7 /* SRGResource+PlaySRG.m in Sources */, 081220B21DD07B7B00BF8326 /* DownloadsViewController.m in Sources */, 087BC6651EDF1B7C00EED89B /* UILabel+PlaySRG.m in Sources */, - 6F4760401EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */, 082910B7239EA69C00D168F4 /* RadioChannelsViewController.m in Sources */, E694A9F71D65F02700372DF0 /* CalendarViewController.m in Sources */, 6FD88F8C22D4BC41008859EF /* UISearchBar+PlaySRG.m in Sources */, @@ -6346,6 +6357,7 @@ E65FB20D1D66D69700820696 /* DailyMediasViewController.m in Sources */, 6FEC91AB21A6B39A00AA50C8 /* TableLoadMoreFooterView.m in Sources */, 6F47608A1EB37DF1003021EA /* UIStackView+PlaySRG.m in Sources */, + 6FFCB62624504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */, 6F06E0A61DB7791300220FC6 /* ApplicationSettings.m in Sources */, 08564B1F1D41111A00381549 /* HomeSectionHeaderView.m in Sources */, 6FE686E31EB9D57400067D40 /* ChannelService.m in Sources */, @@ -6355,6 +6367,7 @@ 08C68F8C1D38DEA100BB8AAA /* PlayAppDelegate.m in Sources */, 08B77AF2240A86FD00A3BC3B /* AccessibilityIdentifierConstants.m in Sources */, 0875851523A0025F00FA7207 /* ProfileAccountHeaderView.m in Sources */, + 6F0506E9245468EE0053253E /* SplitViewController.m in Sources */, 6F4760891EB37DF1003021EA /* UIImageView+PlaySRG.m in Sources */, 087584F123A0008500FA7207 /* ApplicationSectionInfo.m in Sources */, 6F4760311EB37BD1003021EA /* PlayDurationFormatter.m in Sources */, @@ -6500,7 +6513,6 @@ 6F7C35B823708E8A00259BE7 /* SRGResource+PlaySRG.m in Sources */, 081220B61DD07B7B00BF8326 /* DownloadsViewController.m in Sources */, 087BC6661EDF1B7C00EED89B /* UILabel+PlaySRG.m in Sources */, - 6F4760411EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */, 082910B8239EA69C00D168F4 /* RadioChannelsViewController.m in Sources */, E694A9F81D65F02700372DF0 /* CalendarViewController.m in Sources */, 6FD88F8D22D4BC41008859EF /* UISearchBar+PlaySRG.m in Sources */, @@ -6510,6 +6522,7 @@ E65FB20E1D66D69700820696 /* DailyMediasViewController.m in Sources */, 6FEC91AC21A6B39A00AA50C8 /* TableLoadMoreFooterView.m in Sources */, 6F4760911EB37DF1003021EA /* UIStackView+PlaySRG.m in Sources */, + 6FFCB62724504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */, 6F06E0A71DB7791300220FC6 /* ApplicationSettings.m in Sources */, 08564B201D41111A00381549 /* HomeSectionHeaderView.m in Sources */, 6FE686E41EB9D57400067D40 /* ChannelService.m in Sources */, @@ -6519,6 +6532,7 @@ 08C68F8D1D38DEA100BB8AAA /* PlayAppDelegate.m in Sources */, 08B77AF3240A86FD00A3BC3B /* AccessibilityIdentifierConstants.m in Sources */, 0875851623A0025F00FA7207 /* ProfileAccountHeaderView.m in Sources */, + 6F0506EA245468EE0053253E /* SplitViewController.m in Sources */, 6F4760901EB37DF1003021EA /* UIImageView+PlaySRG.m in Sources */, 087584F223A0008500FA7207 /* ApplicationSectionInfo.m in Sources */, 6F4760321EB37BD1003021EA /* PlayDurationFormatter.m in Sources */, @@ -6664,7 +6678,6 @@ 6F7C35B923708E8A00259BE7 /* SRGResource+PlaySRG.m in Sources */, 081220BA1DD07B7B00BF8326 /* DownloadsViewController.m in Sources */, 087BC6671EDF1B7D00EED89B /* UILabel+PlaySRG.m in Sources */, - 6F4760421EB37BD1003021EA /* UICollectionView+PlaySRG.m in Sources */, 082910B9239EA69C00D168F4 /* RadioChannelsViewController.m in Sources */, E694A9F91D65F02700372DF0 /* CalendarViewController.m in Sources */, 6FD88F8E22D4BC41008859EF /* UISearchBar+PlaySRG.m in Sources */, @@ -6674,6 +6687,7 @@ E65FB20F1D66D69700820696 /* DailyMediasViewController.m in Sources */, 6FEC91AD21A6B39A00AA50C8 /* TableLoadMoreFooterView.m in Sources */, 6F4760981EB37DF2003021EA /* UIStackView+PlaySRG.m in Sources */, + 6FFCB62824504C6C00D16466 /* SwimlaneCollectionViewLayout.m in Sources */, 6F06E0A81DB7791300220FC6 /* ApplicationSettings.m in Sources */, 08564B211D41111A00381549 /* HomeSectionHeaderView.m in Sources */, 6FE686E51EB9D57400067D40 /* ChannelService.m in Sources */, @@ -6683,6 +6697,7 @@ 08C68F8E1D38DEA100BB8AAA /* PlayAppDelegate.m in Sources */, 08B77AF4240A86FD00A3BC3B /* AccessibilityIdentifierConstants.m in Sources */, 0875851723A0025F00FA7207 /* ProfileAccountHeaderView.m in Sources */, + 6F0506EB245468EE0053253E /* SplitViewController.m in Sources */, 6F4760971EB37DF2003021EA /* UIImageView+PlaySRG.m in Sources */, 087584F323A0008500FA7207 /* ApplicationSectionInfo.m in Sources */, 6F4760331EB37BD1003021EA /* PlayDurationFormatter.m in Sources */, @@ -7605,7 +7620,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 329; + CURRENT_PROJECT_VERSION = 332; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7624,7 +7639,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 3.0.1; + MARKETING_VERSION = 3.0.2; MARKETING_VERSION_SUFFIX = "-debug"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -7673,7 +7688,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 329; + CURRENT_PROJECT_VERSION = 332; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -7686,7 +7701,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 3.0.1; + MARKETING_VERSION = 3.0.2; MARKETING_VERSION_SUFFIX = ""; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; @@ -8028,7 +8043,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 329; + CURRENT_PROJECT_VERSION = 332; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8042,7 +8057,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 3.0.1; + MARKETING_VERSION = 3.0.2; MARKETING_VERSION_SUFFIX = "-beta"; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; @@ -8240,7 +8255,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 329; + CURRENT_PROJECT_VERSION = 332; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8254,7 +8269,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 3.0.1; + MARKETING_VERSION = 3.0.2; MARKETING_VERSION_SUFFIX = "-nightly"; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; diff --git a/WhatsNew-beta.json b/WhatsNew-beta.json index df193458e..0c9840eb9 100755 --- a/WhatsNew-beta.json +++ b/WhatsNew-beta.json @@ -105,5 +105,8 @@ "3.0.0-326": "- Event module swimlane redesign.\n- Tiny UI margin adjustments.\n- Fixed titles timeline on player (2 lines).", "3.0.1-327": "- Medias with various aspect ratios are better displayed.\n- Homepage glitches and performance issues have been fixed.", "3.0.1-328": "- Allow Handoff with downloaded content.\n- Fixes an issue preventing the first item in lists to be accessed with VoiceOver.", - "3.0.1-329": "- Fixes sometimes erratic scroll position when returning on a homepage.\n- Fixes double tap on tab which sometimes would not return to the top of a list." + "3.0.1-329": "- Fixes sometimes erratic scroll position when returning on a homepage.\n- Fixes double tap on tab which sometimes would not return to the top of a list.", + "3.0.2-330": "- Latest TV / Radio livestream restoration on livestream tab.\n- Snap content and on horizontal swimlanes.\n- Split view for the iPad profile tab.\n- Fix the mini player when using with Picture in Picture on iPad.\n- Fix preview background color on iOS 13.\n- Fix scroll position coupling between swimlanes\n- Avoid displaying empty screen when no swimlanes have been loaded.", + "3.0.2-331": "- Fix profile view rotation on iPhone.\n- Improve empty home screens when no swimlanes have been loaded.\n- Improve VoiceOver for latest TV / Radio livestream items moved to the front on livestream tab.", + "3.0.2-332": "- AppStore release." } \ No newline at end of file