diff --git a/podcasts.xcodeproj/project.pbxproj b/podcasts.xcodeproj/project.pbxproj index f12e65c9c0..f71b12d93a 100644 --- a/podcasts.xcodeproj/project.pbxproj +++ b/podcasts.xcodeproj/project.pbxproj @@ -1691,6 +1691,7 @@ F57D8F162C66CBB80004C4DF /* UnsafeTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57D8F152C66CBB80004C4DF /* UnsafeTransfer.swift */; }; F57D8F182C66CDF40004C4DF /* View+CVPixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57D8F172C66CDF40004C4DF /* View+CVPixelBuffer.swift */; }; F581ED422CC6F1A200A19860 /* ListeningTime2024Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = F581ED412CC6F19300A19860 /* ListeningTime2024Story.swift */; }; + F581ED442CC6F3A900A19860 /* Top5Podcasts2024Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = F581ED432CC6F3A400A19860 /* Top5Podcasts2024Story.swift */; }; F586959A2C04320100E0754A /* PodcastManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F58695992C04320100E0754A /* PodcastManagerTests.swift */; }; F59503B42C93590A007FB3C8 /* ActivityItemSourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59503B32C93590A007FB3C8 /* ActivityItemSourceItem.swift */; }; F5952FCA2C057C6400754BC3 /* FMDatabaseQueue+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5952FC92C057C6400754BC3 /* FMDatabaseQueue+Test.swift */; }; @@ -3627,6 +3628,7 @@ F57D8F152C66CBB80004C4DF /* UnsafeTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsafeTransfer.swift; sourceTree = ""; }; F57D8F172C66CDF40004C4DF /* View+CVPixelBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+CVPixelBuffer.swift"; sourceTree = ""; }; F581ED412CC6F19300A19860 /* ListeningTime2024Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListeningTime2024Story.swift; sourceTree = ""; }; + F581ED432CC6F3A400A19860 /* Top5Podcasts2024Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Top5Podcasts2024Story.swift; sourceTree = ""; }; F58695992C04320100E0754A /* PodcastManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastManagerTests.swift; sourceTree = ""; }; F59503B32C93590A007FB3C8 /* ActivityItemSourceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSourceItem.swift; sourceTree = ""; }; F5952FC92C057C6400754BC3 /* FMDatabaseQueue+Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FMDatabaseQueue+Test.swift"; sourceTree = ""; }; @@ -7872,6 +7874,7 @@ F53C3E822CC73538004F3581 /* TopSpotStory2024.swift */, F581ED412CC6F19300A19860 /* ListeningTime2024Story.swift */, F5D527372CC81AA700682CD5 /* EpilogueStory2024.swift */, + F581ED432CC6F3A400A19860 /* Top5Podcasts2024Story.swift */, ); path = 2024; sourceTree = ""; @@ -10128,6 +10131,7 @@ 10DFE9382C5A8D1300957D0A /* ABTestProviding.swift in Sources */, C7D8AE632A60818600C9EBAF /* ActionBarStyles.swift in Sources */, 8B633D2F2A57294E00526AF0 /* AutoplayWhatsNewHeader.swift in Sources */, + F581ED442CC6F3A900A19860 /* Top5Podcasts2024Story.swift in Sources */, FF91A0FA2B6BBFD1002A0590 /* UpgradeCard.swift in Sources */, C73215FB291EA13400AB1FE5 /* PlusPurchaseModal.swift in Sources */, 40B26AC7213FED4300386173 /* DiscoverPeekViewController.swift in Sources */, diff --git a/podcasts/End of Year/End of Year 2024/EndOfYear2024StoriesModel.swift b/podcasts/End of Year/End of Year 2024/EndOfYear2024StoriesModel.swift index 7c355fea60..e8d04d3891 100644 --- a/podcasts/End of Year/End of Year 2024/EndOfYear2024StoriesModel.swift +++ b/podcasts/End of Year/End of Year 2024/EndOfYear2024StoriesModel.swift @@ -12,6 +12,10 @@ class EndOfYear2024StoriesModel: StoryModel { func populate(with dataManager: DataManager) { // First, search for top 5 podcasts let topPodcasts = dataManager.topPodcasts(in: Self.year, limit: 5) + if !topPodcasts.isEmpty { + stories.append(.top5Podcasts) + data.topPodcasts = Array(topPodcasts.prefix(5)) + } // Listening time if let listeningTime = dataManager.listeningTime(in: Self.year), @@ -26,6 +30,8 @@ class EndOfYear2024StoriesModel: StoryModel { switch stories[storyNumber] { case .intro: return IntroStory2024() + case .top5Podcasts: + return Top5Podcasts2024Story(top5Podcasts: data.topPodcasts) case .listeningTime: return ListeningTime2024Story(listeningTime: data.listeningTime) case .epilogue: @@ -37,6 +43,8 @@ class EndOfYear2024StoriesModel: StoryModel { switch stories[storyNumber] { case .epilogue: return true + case .top5Podcasts: + return true default: return false } @@ -80,5 +88,7 @@ class EndOfYear2024StoriesModel: StoryModel { /// An entity that holds data to present EoY 2024 stories class EndOfYear2024StoriesData { + var topPodcasts: [TopPodcast] = [] + var listeningTime: Double = 0 } diff --git a/podcasts/End of Year/End of Year 2024/EndOfYear2024Story.swift b/podcasts/End of Year/End of Year 2024/EndOfYear2024Story.swift index 9b906a93b1..e226b896b7 100644 --- a/podcasts/End of Year/End of Year 2024/EndOfYear2024Story.swift +++ b/podcasts/End of Year/End of Year 2024/EndOfYear2024Story.swift @@ -1,5 +1,6 @@ enum EndOfYear2024Story: CaseIterable { case intro + case top5Podcasts case listeningTime case epilogue } diff --git a/podcasts/End of Year/Stories/2024/Top5Podcasts2024Story.swift b/podcasts/End of Year/Stories/2024/Top5Podcasts2024Story.swift new file mode 100644 index 0000000000..4e691a2964 --- /dev/null +++ b/podcasts/End of Year/Stories/2024/Top5Podcasts2024Story.swift @@ -0,0 +1,121 @@ +import SwiftUI +import PocketCastsDataModel + +struct Top5Podcasts2024Story: ShareableStory { + @Environment(\.renderForSharing) var renderForSharing: Bool + + let top5Podcasts: [TopPodcast] + + let identifier: String = "top_five_podcast" + + private let shapeColor = Color.green + + private let backgroundColor = Color(hex: "#E0EFAD") + private let shapeImages = ["playback-2024-shape-pentagon", + "playback-2024-shape-two-ovals", + "playback-2024-shape-wavy-circle"] + + @ObservedObject private var animationViewModel = PlayPauseAnimationViewModel(duration: 0.8, animation: Animation.spring(_:)) + + @State private var visible = false + + @State private var itemScale: Double = 0 + @State private var itemOpacity: Double = 0 + + var body: some View { + GeometryReader { geometry in + let isSmallScreen = geometry.size.height <= 700 + VStack(alignment: .leading) { + ScrollView(.vertical) { + VStack(alignment: .leading) { + podcastList() + .modifier(animationViewModel.animate($itemOpacity, to: 1, after: 0.1)) + .modifier(animationViewModel.animate($itemScale, to: 1)) + } + } + .modify { + if #available(iOS 16.4, *) { + $0.scrollIndicators(.never) + .scrollBounceBehavior(.basedOnSize) + } + } + .disabled(!isSmallScreen) // Disable scrolling on larger where we shouldn't be clipping. + .frame(height: geometry.size.height * 0.65) + + Text("And you were big on these shows too!") + .font(.system(size: 30, weight: .bold)) + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .ignoresSafeArea() + .enableProportionalValueScaling() + .background( + Rectangle() + .fill(backgroundColor) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + .onAppear { + animationViewModel.play() + } + } + + @ViewBuilder func podcastList() -> some View { + ForEach(Array(zip(top5Podcasts.indices, top5Podcasts)), id: \.1.podcast.uuid) { index, item in + listCell(index: index, item: item) + } + } + + @ViewBuilder func listCell(index: Int, item: TopPodcast) -> some View { + HStack { + let imageSize: Double = 72 + let textAnimationOffset = imageSize/2 + Text("#\(index + 1)") + .font(.system(size: 22, weight: .semibold)) + .opacity(itemOpacity) + .offset(x: (1 - itemScale) * textAnimationOffset) + + ZStack { + Image(shapeImages[index % shapeImages.count]) + .foregroundStyle(shapeColor) + PodcastImage(uuid: item.podcast.uuid, size: .grid) + .frame(width: imageSize, height: imageSize) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .scaleEffect(itemScale) + + VStack(alignment: .leading) { + if let author = item.podcast.author { + Text(author) + .font(.system(size: 15)) + } + if let title = item.podcast.title { + Text(title) + .font(.system(size: 18, weight: .medium)) + } + } + .opacity(itemOpacity) + .offset(x: (1 - itemScale) * -textAnimationOffset) + } + } + + func onAppear() { + Analytics.track(.endOfYearStoryShown, story: identifier) + } + + func onPause() { + animationViewModel.pause() + } + + func onResume() { + animationViewModel.play() + } + + func sharingAssets() -> [Any] { + [ + StoryShareableProvider.new(AnyView(self)), + StoryShareableText(L10n.eoyStoryTopPodcastsShareText("%1$@"), podcasts: top5Podcasts.map { $0.podcast }) + ] + } +} diff --git a/podcasts/EndOfYear.xcassets/playback-2024-shape-pentagon.imageset/Contents.json b/podcasts/EndOfYear.xcassets/playback-2024-shape-pentagon.imageset/Contents.json new file mode 100644 index 0000000000..a23d4d4194 --- /dev/null +++ b/podcasts/EndOfYear.xcassets/playback-2024-shape-pentagon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "playback-2024-shape-pentagon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/podcasts/EndOfYear.xcassets/playback-2024-shape-pentagon.imageset/playback-2024-shape-pentagon.svg b/podcasts/EndOfYear.xcassets/playback-2024-shape-pentagon.imageset/playback-2024-shape-pentagon.svg new file mode 100644 index 0000000000..3b5554864b --- /dev/null +++ b/podcasts/EndOfYear.xcassets/playback-2024-shape-pentagon.imageset/playback-2024-shape-pentagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/podcasts/EndOfYear.xcassets/playback-2024-shape-two-ovals.imageset/Contents.json b/podcasts/EndOfYear.xcassets/playback-2024-shape-two-ovals.imageset/Contents.json new file mode 100644 index 0000000000..68bc2c3e73 --- /dev/null +++ b/podcasts/EndOfYear.xcassets/playback-2024-shape-two-ovals.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "playback-2024-shape-two-ovals.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/podcasts/EndOfYear.xcassets/playback-2024-shape-two-ovals.imageset/playback-2024-shape-two-ovals.svg b/podcasts/EndOfYear.xcassets/playback-2024-shape-two-ovals.imageset/playback-2024-shape-two-ovals.svg new file mode 100644 index 0000000000..e166495223 --- /dev/null +++ b/podcasts/EndOfYear.xcassets/playback-2024-shape-two-ovals.imageset/playback-2024-shape-two-ovals.svg @@ -0,0 +1,3 @@ + + + diff --git a/podcasts/EndOfYear.xcassets/playback-2024-shape-wavy-circle.imageset/Contents.json b/podcasts/EndOfYear.xcassets/playback-2024-shape-wavy-circle.imageset/Contents.json new file mode 100644 index 0000000000..11d3a8975d --- /dev/null +++ b/podcasts/EndOfYear.xcassets/playback-2024-shape-wavy-circle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "playback-2024-shape-wavy-circle.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/podcasts/EndOfYear.xcassets/playback-2024-shape-wavy-circle.imageset/playback-2024-shape-wavy-circle.svg b/podcasts/EndOfYear.xcassets/playback-2024-shape-wavy-circle.imageset/playback-2024-shape-wavy-circle.svg new file mode 100644 index 0000000000..64efecfb4e --- /dev/null +++ b/podcasts/EndOfYear.xcassets/playback-2024-shape-wavy-circle.imageset/playback-2024-shape-wavy-circle.svg @@ -0,0 +1,3 @@ + + +