diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 6e6ea374..3705fd7b 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -68,9 +68,9 @@ internal enum L10n { internal static let genres = L10n.tr("Localizable", "genres") /// Home internal static let home = L10n.tr("Localizable", "home") - /// Latest %@ - internal static func latestWithString(_ p1: Any) -> String { - return L10n.tr("Localizable", "latestWithString", String(describing: p1)) + /// Latest in %@ + internal static func latestInWithString(_ p1: Any) -> String { + return L10n.tr("Localizable", "latestInWithString", String(describing: p1)) } /// Library internal static let library = L10n.tr("Localizable", "library") diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index e00f07c5..8207fedd 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -14,10 +14,11 @@ import JellyfinAPI final class HomeViewModel: ViewModel { - @Published var librariesShowRecentlyAddedIDs: [String] = [] - @Published var libraries: [BaseItemDto] = [] + @Published var latestAddedItems: [BaseItemDto] = [] @Published var resumeItems: [BaseItemDto] = [] @Published var nextUpItems: [BaseItemDto] = [] + @Published var librariesShowRecentlyAddedIDs: [String] = [] + @Published var libraries: [BaseItemDto] = [] // temp var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) @@ -59,6 +60,7 @@ final class HomeViewModel: ViewModel { LogManager.shared.log.debug("Refresh called.") refreshLibrariesLatest() + refreshLatestAddedItems() refreshResumeItems() refreshNextUpItems() } @@ -111,13 +113,34 @@ final class HomeViewModel: ViewModel { .store(in: &cancellables) } + // MARK: Latest Added Items + private func refreshLatestAddedItems() { + UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], + enableImageTypes: [.primary, .backdrop, .thumb], + enableUserData: true, + limit: 8) + .sink { completion in + switch completion { + case .finished: () + case .failure: + self.nextUpItems = [] + self.handleAPIRequestError(completion: completion) + } + } receiveValue: { items in + LogManager.shared.log.debug("Retrieved \(String(items.count)) resume items") + + self.latestAddedItems = items + } + .store(in: &cancellables) + } + // MARK: Resume Items private func refreshResumeItems() { - ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12, + ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, + limit: 6, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], - mediaTypes: ["Video"], - imageTypeLimit: 1, - enableImageTypes: [.primary, .backdrop, .thumb]) + enableUserData: true) .trackActivity(loading) .sink(receiveCompletion: { completion in switch completion { @@ -136,8 +159,10 @@ final class HomeViewModel: ViewModel { // MARK: Next Up Items private func refreshNextUpItems() { - TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters]) + TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, + limit: 6, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], + enableUserData: true) .trackActivity(loading) .sink(receiveCompletion: { completion in switch completion { diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift new file mode 100644 index 00000000..91cb3c5e --- /dev/null +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift @@ -0,0 +1,55 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import JellyfinAPI +import Nuke +import SwiftUI +import UIKit + +class DynamicCinematicBackgroundViewModel: ObservableObject { + + @Published var currentItem: BaseItemDto? + @Published var currentImageView: UIImageView? + + func select(item: BaseItemDto) { + + guard item.id != currentItem?.id else { return } + + currentItem = item + + let itemImageView = UIImageView() + + let backdropImage: URL + + if item.itemType == .episode { + backdropImage = item.getSeriesBackdropImage(maxWidth: 1920) + } else { + backdropImage = item.getBackdropImage(maxWidth: 1920) + } + + let options = ImageLoadingOptions(transition: .fadeIn(duration: 0.2)) + + Nuke.loadImage(with: backdropImage, options: options, into: itemImageView, completion: { _ in }) + + currentImageView = itemImageView + } +} + +struct CinematicBackgroundView: UIViewRepresentable { + + @ObservedObject var viewModel: DynamicCinematicBackgroundViewModel + + func updateUIView(_ uiView: UICinematicBackgroundView, context: Context) { + uiView.update(imageView: viewModel.currentImageView ?? UIImageView()) + } + + func makeUIView(context: Context) -> UICinematicBackgroundView { + return UICinematicBackgroundView(initialImageView: viewModel.currentImageView ?? UIImageView()) + } +} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift new file mode 100644 index 00000000..414f8342 --- /dev/null +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift @@ -0,0 +1,62 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import JellyfinAPI +import SwiftUI + +struct CinematicNextUpCardView: View { + + @EnvironmentObject var homeRouter: HomeCoordinator.Router + let item: BaseItemDto + let showOverlay: Bool + + var body: some View { + VStack(alignment: .leading) { + Button { + homeRouter.route(to: \.modalItem, item) + } label: { + ZStack(alignment: .bottomLeading) { + + if item.itemType == .episode { + ImageView(src: item.getSeriesBackdropImage(maxWidth: 350)) + .frame(width: 350, height: 210) + } else { + ImageView(src: item.getBackdropImage(maxWidth: 350)) + .frame(width: 350, height: 210) + } + + LinearGradient(colors: [.clear, .black], + startPoint: .top, + endPoint: .bottom) + .frame(height: 105) + .ignoresSafeArea() + + if showOverlay { + VStack(alignment: .leading, spacing: 0) { + Text("Next") + .font(.subheadline) + .padding(.vertical, 5) + .padding(.leading, 10) + .foregroundColor(.white) + + HStack { + Color.clear + .frame(width: 1, height: 7) + } + } + } + } + .frame(width: 350, height: 210) + } + .buttonStyle(CardButtonStyle()) + .padding(.top) + } + .padding(.vertical) + } +} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift new file mode 100644 index 00000000..9529976c --- /dev/null +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift @@ -0,0 +1,61 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import JellyfinAPI +import SwiftUI + +struct CinematicResumeCardView: View { + + @EnvironmentObject var homeRouter: HomeCoordinator.Router + let item: BaseItemDto + + var body: some View { + VStack(alignment: .leading) { + Button { + homeRouter.route(to: \.modalItem, item) + } label: { + ZStack(alignment: .bottom) { + + if item.itemType == .episode { + ImageView(src: item.getSeriesBackdropImage(maxWidth: 350)) + .frame(width: 350, height: 210) + } else { + ImageView(src: item.getBackdropImage(maxWidth: 350)) + .frame(width: 350, height: 210) + } + + LinearGradient(colors: [.clear, .black], + startPoint: .top, + endPoint: .bottom) + .frame(height: 105) + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + Text(item.getItemProgressString() ?? "") + .font(.subheadline) + .padding(.vertical, 5) + .padding(.leading, 10) + .foregroundColor(.white) + + HStack { + Color(UIColor.systemPurple) + .frame(width: 350 * (item.userData?.playedPercentage ?? 0) / 100, height: 7) + + Spacer(minLength: 0) + } + } + } + .frame(width: 350, height: 210) + } + .buttonStyle(CardButtonStyle()) + .padding(.top) + } + .padding(.vertical) + } +} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift b/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift new file mode 100644 index 00000000..37edaa8d --- /dev/null +++ b/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift @@ -0,0 +1,123 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import UIKit +import JellyfinAPI + +struct HomeCinematicViewItem: Hashable { + + enum TopRowType { + case resume + case nextUp + case plain + } + + let item: BaseItemDto + let type: TopRowType + + func hash(into hasher: inout Hasher) { + hasher.combine(item) + hasher.combine(type) + } +} + +struct HomeCinematicView: View { + + @FocusState var selectedItem: BaseItemDto? + @State private var updatedSelectedItem: BaseItemDto? + @State private var initiallyAppeared = false + private let forcedItemSubtitle: String? + private let items: [HomeCinematicViewItem] + private let backgroundViewModel = DynamicCinematicBackgroundViewModel() + + init(items: [HomeCinematicViewItem], forcedItemSubtitle: String? = nil) { + self.items = items + self.forcedItemSubtitle = forcedItemSubtitle + } + + var body: some View { + + ZStack(alignment: .bottom) { + + CinematicBackgroundView(viewModel: backgroundViewModel) + .frame(height: UIScreen.main.bounds.height - 10) + + LinearGradient(stops: [.init(color: .clear, location: 0.5), + .init(color: .black.opacity(0.6), location: 0.7), + .init(color: .black, location: 1)], + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + + VStack(alignment: .leading, spacing: 0) { + + if let forcedItemSubtitle = forcedItemSubtitle { + Text(forcedItemSubtitle) + .font(.callout) + .fontWeight(.medium) + .foregroundColor(Color.secondary) + } else { + if updatedSelectedItem?.itemType == .episode { + Text(updatedSelectedItem?.getEpisodeLocator() ?? "") + .font(.callout) + .fontWeight(.medium) + .foregroundColor(Color.secondary) + } else { + Text("") + } + } + + Text("\(updatedSelectedItem?.seriesName ?? updatedSelectedItem?.name ?? "")") + .font(.title) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.horizontal, 50) + + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(items, id: \.self) { item in + switch item.type { + case .nextUp: + CinematicNextUpCardView(item: item.item, showOverlay: true) + .focused($selectedItem, equals: item.item) + case .resume: + CinematicResumeCardView(item: item.item) + .focused($selectedItem, equals: item.item) + case .plain: + CinematicNextUpCardView(item: item.item, showOverlay: false) + .focused($selectedItem, equals: item.item) + } + } + } + .padding(.horizontal, 50) + .padding(.bottom) + } + .focusSection() + } + } + .onChange(of: selectedItem) { newValue in + if let newItem = newValue { + backgroundViewModel.select(item: newItem) + updatedSelectedItem = newItem + } + } + .onAppear { + guard !initiallyAppeared else { return } + selectedItem = items.first?.item + updatedSelectedItem = items.first?.item + initiallyAppeared = true + } + } +} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift b/Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift new file mode 100644 index 00000000..86c77b11 --- /dev/null +++ b/Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift @@ -0,0 +1,71 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import UIKit + +class UICinematicBackgroundView: UIView { + + private var currentImageView: UIView? + + private var selectDelayTimer: Timer? + + init(initialImageView: UIImageView) { + super.init(frame: .zero) + + initialImageView.translatesAutoresizingMaskIntoConstraints = false + initialImageView.alpha = 0 + + addSubview(initialImageView) + NSLayoutConstraint.activate([ + initialImageView.topAnchor.constraint(equalTo: topAnchor), + initialImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + initialImageView.leftAnchor.constraint(equalTo: leftAnchor), + initialImageView.rightAnchor.constraint(equalTo: rightAnchor) + ]) + + self.currentImageView = initialImageView + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(imageView: UIImageView) { + + selectDelayTimer?.invalidate() + + selectDelayTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(delayTimerTimed), userInfo: imageView, repeats: false) + + } + + @objc private func delayTimerTimed(timer: Timer) { + let newImageView = timer.userInfo as! UIImageView + + newImageView.translatesAutoresizingMaskIntoConstraints = false + newImageView.alpha = 0 + + addSubview(newImageView) + NSLayoutConstraint.activate([ + newImageView.topAnchor.constraint(equalTo: topAnchor), + newImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + newImageView.leftAnchor.constraint(equalTo: leftAnchor), + newImageView.rightAnchor.constraint(equalTo: rightAnchor) + ]) + + UIView.animate(withDuration: 0.2) { + newImageView.alpha = 1 + self.currentImageView?.alpha = 0 + } completion: { _ in + self.currentImageView?.removeFromSuperview() + self.currentImageView = newImageView + } + + } +} diff --git a/Swiftfin tvOS/Views/HomeView.swift b/Swiftfin tvOS/Views/HomeView.swift index 0c67b482..78fcbfa5 100644 --- a/Swiftfin tvOS/Views/HomeView.swift +++ b/Swiftfin tvOS/Views/HomeView.swift @@ -7,13 +7,16 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Defaults import Foundation import SwiftUI +import JellyfinAPI struct HomeView: View { @EnvironmentObject var homeRouter: HomeCoordinator.Router @ObservedObject var viewModel = HomeViewModel() + @Default(.showPosterLabels) var showPosterLabels @State var showingSettings = false @@ -24,16 +27,33 @@ struct HomeView: View { } else { ScrollView { LazyVStack(alignment: .leading) { - if !viewModel.resumeItems.isEmpty { - ContinueWatchingView(items: viewModel.resumeItems) - } - if !viewModel.nextUpItems.isEmpty { - NextUpView(items: viewModel.nextUpItems) + if viewModel.resumeItems.isEmpty { + HomeCinematicView(items: viewModel.latestAddedItems.map({ .init(item: $0, type: .plain) }), + forcedItemSubtitle: "Recently Added") + + if !viewModel.nextUpItems.isEmpty { + NextUpView(items: viewModel.nextUpItems) + .focusSection() + } + } else { + HomeCinematicView(items: viewModel.resumeItems.map({ .init(item: $0, type: .resume) })) + + if !viewModel.nextUpItems.isEmpty { + NextUpView(items: viewModel.nextUpItems) + .focusSection() + } + + PortraitItemsRowView(rowTitle: "Recently Added", + items: viewModel.latestAddedItems, + showItemTitles: showPosterLabels) { item in + homeRouter.route(to: \.item, item) + } } ForEach(viewModel.libraries, id: \.self) { library in LatestMediaView(viewModel: LatestMediaViewModel(library: library)) + .focusSection() } Spacer(minLength: 100) @@ -52,6 +72,7 @@ struct HomeView: View { .focusSection() } } + .edgesIgnoringSafeArea(.top) .edgesIgnoringSafeArea(.horizontal) } } diff --git a/Swiftfin tvOS/Views/LatestMediaView.swift b/Swiftfin tvOS/Views/LatestMediaView.swift index 68fbe395..cc5e96a7 100644 --- a/Swiftfin tvOS/Views/LatestMediaView.swift +++ b/Swiftfin tvOS/Views/LatestMediaView.swift @@ -18,7 +18,7 @@ struct LatestMediaView: View { var body: some View { VStack(alignment: .leading) { - L10n.latestWithString(viewModel.library.name ?? "").text + L10n.latestInWithString(viewModel.library.name ?? "").text .font(.title3) .padding(.horizontal, 50) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index e75290c9..9c5b8dd2 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -246,6 +246,11 @@ C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; + E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */; }; + E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */; }; + E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */; }; + E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */; }; + E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */; }; E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; }; @@ -638,6 +643,11 @@ C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; + E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICinematicBackgroundView.swift; sourceTree = ""; }; + E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicBackgroundView.swift; sourceTree = ""; }; + E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = ""; }; + E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = ""; }; + E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = ""; }; E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = ""; }; E107BB952788104100354E07 /* CinematicCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicCollectionItemView.swift; sourceTree = ""; }; E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = ""; }; @@ -977,6 +987,7 @@ E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */, 536D3D87267C17350004248C /* PublicUserButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, + E103A6A1278A7EB500820EC7 /* HomeCinematicView */, ); path = Components; sourceTree = ""; @@ -1312,6 +1323,18 @@ path = Pods; sourceTree = ""; }; + E103A6A1278A7EB500820EC7 /* HomeCinematicView */ = { + isa = PBXGroup; + children = ( + E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */, + E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */, + E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */, + E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */, + E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */, + ); + path = HomeCinematicView; + sourceTree = ""; + }; E107BB9127880A4000354E07 /* ItemViewModel */ = { isa = PBXGroup; children = ( @@ -2039,6 +2062,7 @@ E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, + E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */, E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, @@ -2064,10 +2088,12 @@ 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, + E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, + E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */, E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */, @@ -2098,6 +2124,7 @@ C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */, E10D87DF278510E400BD264C /* PosterSize.swift in Sources */, + E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */, E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */, E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */, @@ -2122,6 +2149,7 @@ 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */, + E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index a22e6cfc..d5a9e338 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ