diff --git a/Shared/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift index 73071ac1..140b18e2 100644 --- a/Shared/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -17,12 +17,11 @@ final class SearchCoordinator: NavigationCoordinatable { @Root var start = makeStart - #if os(tvOS) @Route(.modal) var item = makeItem - #else @Route(.push) - var item = makeItem + var library = makeLibrary + #if !os(tvOS) @Route(.modal) var filter = makeFilter #endif @@ -31,11 +30,19 @@ final class SearchCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { NavigationViewCoordinator(ItemCoordinator(item: item)) } + + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(LibraryCoordinator(parameters: parameters)) + } #else func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { + LibraryCoordinator(parameters: parameters) + } + func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator { NavigationViewCoordinator(FilterCoordinator(parameters: parameters)) } diff --git a/Shared/Extensions/UIScreenExtensions.swift b/Shared/Extensions/UIScreenExtensions.swift index beeb5987..7c9218f0 100644 --- a/Shared/Extensions/UIScreenExtensions.swift +++ b/Shared/Extensions/UIScreenExtensions.swift @@ -16,4 +16,10 @@ extension UIScreen { func scale(_ x: CGFloat) -> Int { Int(nativeScale * x) } + + func maxChildren(width: CGFloat, height: CGFloat) -> Int { + let screenSize = bounds.height * bounds.width + let itemSize = width * height + return Int(screenSize / itemSize) + } } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index f0fc27de..a8177ed5 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -36,7 +36,7 @@ final class LibraryViewModel: ViewModel { filterViewModel.$currentFilters .sink { newFilters in - self.requestItemsAsync(with: newFilters, replaceCurrentItems: true) + self.requestItems(with: newFilters, replaceCurrentItems: true) } .store(in: &cancellables) } @@ -53,17 +53,17 @@ final class LibraryViewModel: ViewModel { filterViewModel.$currentFilters .sink { newFilters in - self.requestItemsAsync(with: newFilters, replaceCurrentItems: true) + self.requestItems(with: newFilters, replaceCurrentItems: true) } .store(in: &cancellables) } private var pageItemSize: Int { let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77 - return UIScreen.itemsFillableOnScreen(width: libraryGridPosterType.width, height: height) + return UIScreen.main.maxChildren(width: libraryGridPosterType.width, height: height) } - func requestItemsAsync(with filters: ItemFilters, replaceCurrentItems: Bool = false) { + private func requestItems(with filters: ItemFilters, replaceCurrentItems: Bool = false) { if replaceCurrentItems { self.items = [] @@ -156,18 +156,9 @@ final class LibraryViewModel: ViewModel { .store(in: &cancellables) } - func requestNextPageAsync() { + func requestNextPage() { guard hasNextPage else { return } currentPage += 1 - requestItemsAsync(with: filterViewModel.currentFilters) - } -} - -extension UIScreen { - - static func itemsFillableOnScreen(width: CGFloat, height: CGFloat) -> Int { - let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width - let itemSize = width * height - return Int(screenSize / itemSize) + requestItems(with: filterViewModel.currentFilters) } } diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift index 9282fce7..69a5e59a 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift @@ -12,18 +12,9 @@ extension ItemView { struct AboutView: View { - @EnvironmentObject - private var itemRouter: ItemCoordinator.Router @ObservedObject var viewModel: ItemViewModel - @State - private var presentOverviewAlert = false - @State - private var presentSubtitlesAlert = false - @State - private var presentAudioAlert = false - var body: some View { VStack(alignment: .leading) { @@ -33,7 +24,7 @@ extension ItemView { .padding(.leading, 50) ScrollView(.horizontal) { - HStack { + HStack(spacing: 30) { ImageView( viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel.item .imageSource(.primary, maxWidth: 300) @@ -43,26 +34,20 @@ extension ItemView { } .posterStyle(type: .portrait, width: 270) - AboutViewCard( - isShowingAlert: $presentOverviewAlert, + InformationCard( title: viewModel.item.displayName, - text: viewModel.item.overview ?? L10n.noOverviewAvailable + content: viewModel.item.overview ?? L10n.noOverviewAvailable ) if let subtitleStreams = viewModel.playButtonItem?.subtitleStreams, !subtitleStreams.isEmpty { - AboutViewCard( - isShowingAlert: $presentSubtitlesAlert, + InformationCard( title: L10n.subtitles, - text: subtitleStreams.compactMap(\.displayTitle).joined(separator: ", ") + content: subtitleStreams.compactMap(\.displayTitle).joined(separator: ", ") ) } if let audioStreams = viewModel.playButtonItem?.audioStreams, !audioStreams.isEmpty { - AboutViewCard( - isShowingAlert: $presentAudioAlert, - title: L10n.audio, - text: audioStreams.compactMap(\.displayTitle).joined(separator: ", ") - ) + InformationCard(title: L10n.audio, content: audioStreams.compactMap(\.displayTitle).joined(separator: ", ")) } } .padding(.horizontal, 50) @@ -71,39 +56,6 @@ extension ItemView { } } .focusSection() - .alert(viewModel.item.displayName, isPresented: $presentOverviewAlert) { - Button { - presentOverviewAlert = false - } label: { - L10n.close.text - } - } message: { - if let overview = viewModel.item.overview { - overview.text - } else { - L10n.noOverviewAvailable.text - } - } - .alert(L10n.subtitles, isPresented: $presentSubtitlesAlert) { - Button { - presentSubtitlesAlert = false - } label: { - L10n.close.text - } - } message: { - viewModel.item.subtitleStreams.compactMap(\.displayTitle).joined(separator: ", ") - .text - } - .alert(L10n.audio, isPresented: $presentAudioAlert) { - Button { - presentAudioAlert = false - } label: { - L10n.close.text - } - } message: { - viewModel.item.audioStreams.compactMap(\.displayTitle).joined(separator: ", ") - .text - } } } } diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift index 55107cf9..328028f8 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutViewCard.swift @@ -10,17 +10,17 @@ import SwiftUI extension ItemView.AboutView { - struct AboutViewCard: View { + struct InformationCard: View { - @Binding - var isShowingAlert: Bool + @State + private var presentingAlert: Bool = false let title: String - let text: String + let content: String var body: some View { Button { - isShowingAlert = true + presentingAlert = true } label: { VStack(alignment: .leading) { title.text @@ -30,7 +30,7 @@ extension ItemView.AboutView { Spacer() - TruncatedTextView(text: text, seeMoreAction: {}) + TruncatedTextView(text: content, seeMoreAction: {}) .font(.subheadline) .lineLimit(4) } @@ -38,6 +38,15 @@ extension ItemView.AboutView { .frame(width: 700, height: 405) } .buttonStyle(.card) + .alert(title, isPresented: $presentingAlert) { + Button { + presentingAlert = false + } label: { + L10n.close.text + } + } message: { + Text(content) + } } } } diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index c2a443f3..46d3c04c 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -29,8 +29,6 @@ struct ItemView: View { SeriesItemView(viewModel: .init(item: item)) case .boxSet: CollectionItemView(viewModel: .init(item: item)) - case .person: - LibraryView(viewModel: .init(parent: item, type: .person, filters: .init())) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift index 04ab72c2..a5774655 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodesView.swift @@ -14,8 +14,6 @@ struct SeriesEpisodesView: View { @ObservedObject var viewModel: SeriesItemViewModel - @FocusState - private var isFocused: Bool @EnvironmentObject private var parentFocusGuide: FocusGuide @@ -68,7 +66,7 @@ extension SeriesEpisodesView { focusGuide, tag: "seasons", onContentFocus: { focusedSeason = viewModel.selectedSeason }, - top: "mediaButtons", + top: "top", bottom: "episodes" ) .frame(height: 70) @@ -124,14 +122,28 @@ extension SeriesEpisodesView { .padding(.bottom, 50) .padding(.top) } + .mask { + VStack(spacing: 0) { + Color.white + + LinearGradient( + stops: [ + .init(color: .white, location: 0), + .init(color: .clear, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: 20) + } + } + .transition(.opacity) .focusGuide( focusGuide, tag: "episodes", onContentFocus: { focusedEpisodeID = lastFocusedEpisodeID }, - top: "seasons", - bottom: "recommended" + top: "seasons" ) - .transition(.opacity) .introspectScrollView { scrollView in wrappedScrollView = scrollView } diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift index 464e127d..e5908485 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift @@ -14,20 +14,23 @@ extension SeriesItemView { struct ContentView: View { + @ObservedObject + private var focusGuide = FocusGuide() @ObservedObject var viewModel: SeriesItemViewModel - @EnvironmentObject - private var itemRouter: ItemCoordinator.Router - var body: some View { VStack(spacing: 0) { ItemView.CinematicHeaderView(viewModel: viewModel) + .focusGuide(focusGuide, tag: "top", bottom: "seasons") .frame(height: UIScreen.main.bounds.height - 150) .padding(.bottom, 50) SeriesEpisodesView(viewModel: viewModel) + .environmentObject(focusGuide) + + ItemView.CastAndCrewHStack(people: viewModel.item.people?.filter(\.isDisplayed) ?? []) ItemView.SimilarItemsHStack(items: viewModel.similarItems) diff --git a/Swiftfin tvOS/Views/LibraryView.swift b/Swiftfin tvOS/Views/LibraryView.swift index c2cb01df..ad65bd0d 100644 --- a/Swiftfin tvOS/Views/LibraryView.swift +++ b/Swiftfin tvOS/Views/LibraryView.swift @@ -65,7 +65,7 @@ struct LibraryView: View { } .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 600, trailing: 0)) { edge in if !viewModel.isLoading && edge == .bottom { - viewModel.requestNextPageAsync() + viewModel.requestNextPage() } } .scrollViewOffset($scrollViewOffset) diff --git a/Swiftfin tvOS/Views/SearchView.swift b/Swiftfin tvOS/Views/SearchView.swift index 05de6c46..7cd97a50 100644 --- a/Swiftfin tvOS/Views/SearchView.swift +++ b/Swiftfin tvOS/Views/SearchView.swift @@ -63,6 +63,14 @@ struct SearchView: View { .ignoresSafeArea() } + private func baseItemOnSelect(_ item: BaseItemDto) { + if item.type == .person { + router.route(to: \.library, .init(parent: item, type: .person, filters: .init())) + } else { + router.route(to: \.item, item) + } + } + @ViewBuilder private func itemsSection( title: String, @@ -74,7 +82,7 @@ struct SearchView: View { items: viewModel[keyPath: keyPath] ) .onSelect { item in - router.route(to: \.item, item) + baseItemOnSelect(item) } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index b6f02309..6a9ee118 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -858,10 +858,6 @@ E18E01D8288747230022598C /* PlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButton.swift; sourceTree = ""; }; E18E01D9288747230022598C /* ActionButtonHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButtonHStack.swift; sourceTree = ""; }; E18E01F3288747580022598C /* AboutAppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = ""; }; - E18E01F5288747580022598C /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - E18E01F6288747580022598C /* HomeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = ""; }; - E18E01F8288747580022598C /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = ""; }; - E18E01F9288747580022598C /* HomeErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; E18E01FF288749200022598C /* Divider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Divider.swift; sourceTree = ""; }; E18E0200288749200022598C /* AppIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; E18E0201288749200022598C /* AttributeFillView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeFillView.swift; sourceTree = ""; }; @@ -872,7 +868,6 @@ E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Images.swift"; sourceTree = ""; }; E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScreenExtensions.swift; sourceTree = ""; }; E1937A60288F32DB00CB80AA /* Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poster.swift; sourceTree = ""; }; - E1937A63288F683300CB80AA /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = ""; }; E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainCoordinator.swift; sourceTree = ""; }; E193D546271941C500900D82 /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = ""; }; @@ -1103,11 +1098,13 @@ children = ( 53913BDA26D323FE00EB3286 /* de.lproj */, 53913BE326D323FE00EB3286 /* el.lproj */, + 534D4FE626A7D7CC000A7A48 /* en.lproj */, 53913BE026D323FE00EB3286 /* es.lproj */, 53913BC826D323FE00EB3286 /* fr.lproj */, 53913BE626D323FE00EB3286 /* he.lproj */, 53913BCE26D323FE00EB3286 /* it.lproj */, 53913BEC26D323FE00EB3286 /* kk.lproj */, + 534D4FEA26A7D7CC000A7A48 /* ko.lproj */, 53913BCB26D323FE00EB3286 /* ru.lproj */, 53913BE926D323FE00EB3286 /* sk.lproj */, 53913BD726D323FE00EB3286 /* sl.lproj */, @@ -1115,8 +1112,6 @@ 53913BDD26D323FE00EB3286 /* ta.lproj */, 53913BD126D323FE00EB3286 /* vi.lproj */, 534D4FED26A7D7CC000A7A48 /* zh-Hans.lproj */, - 534D4FE626A7D7CC000A7A48 /* en.lproj */, - 534D4FEA26A7D7CC000A7A48 /* ko.lproj */, ); path = Translations; sourceTree = ""; @@ -1234,13 +1229,12 @@ 5377CBE8263B596A003A4E83 = { isa = PBXGroup; children = ( - 534D4FE126A7D7CC000A7A48 /* Translations */, - 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* Swiftfin */, 535870612669D21600D05A09 /* Swiftfin tvOS */, - 5377CBF2263B596A003A4E83 /* Products */, 535870752669D60C00D05A09 /* Shared */, - E168BD06289A414B001A6922 /* Recovered References */, + 534D4FE126A7D7CC000A7A48 /* Translations */, + 5377CBF2263B596A003A4E83 /* Products */, + 53D5E3DB264B47EE00BADDC8 /* Frameworks */, ); sourceTree = ""; }; @@ -1256,12 +1250,12 @@ 5377CBF3263B596A003A4E83 /* Swiftfin */ = { isa = PBXGroup; children = ( - E1DD1127271E7D15005BE12F /* Objects */, E13DD3BB27163C3E009D4DAF /* App */, 62ECA01926FA6D6900E8EBB7 /* AppURLHandler */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 53F866422687A45400DCD1D7 /* Components */, 5377CC02263B596B003A4E83 /* Info.plist */, + E1DD1127271E7D15005BE12F /* Objects */, E13D02842788B634000FCB04 /* Swiftfin.entitlements */, E11CEB85289984F5003E74C7 /* Extensions */, 5377CBFA263B596B003A4E83 /* Preview Content */, @@ -1777,18 +1771,6 @@ path = CollectionItemView; sourceTree = ""; }; - E168BD06289A414B001A6922 /* Recovered References */ = { - isa = PBXGroup; - children = ( - E18E01F9288747580022598C /* HomeErrorView.swift */, - E18E01F8288747580022598C /* LatestInLibraryView.swift */, - E18E01F6288747580022598C /* HomeContentView.swift */, - E18E01F5288747580022598C /* HomeView.swift */, - E1937A63288F683300CB80AA /* ContinueWatchingCard.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; E168BD07289A4162001A6922 /* HomeView */ = { isa = PBXGroup; children = ( diff --git a/Swiftfin/Views/LibraryView/LibraryView.swift b/Swiftfin/Views/LibraryView/LibraryView.swift index 5520b19c..feb6fe4a 100644 --- a/Swiftfin/Views/LibraryView/LibraryView.swift +++ b/Swiftfin/Views/LibraryView/LibraryView.swift @@ -69,7 +69,7 @@ struct LibraryView: View { } .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in if !viewModel.isLoading && edge == .bottom { - viewModel.requestNextPageAsync() + viewModel.requestNextPage() } } .configure { configuration in @@ -96,7 +96,7 @@ struct LibraryView: View { } .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in if !viewModel.isLoading && edge == .bottom { - viewModel.requestNextPageAsync() + viewModel.requestNextPage() } } .configure { configuration in diff --git a/Swiftfin/Views/SearchView.swift b/Swiftfin/Views/SearchView.swift index 3c1c0f11..1737e407 100644 --- a/Swiftfin/Views/SearchView.swift +++ b/Swiftfin/Views/SearchView.swift @@ -66,6 +66,14 @@ struct SearchView: View { } } + private func baseItemOnSelect(_ item: BaseItemDto) { + if item.type == .person { + router.route(to: \.library, .init(parent: item, type: .person, filters: .init())) + } else { + router.route(to: \.item, item) + } + } + @ViewBuilder private func itemsSection( title: String, @@ -78,7 +86,7 @@ struct SearchView: View { items: viewModel[keyPath: keyPath] ) .onSelect { item in - router.route(to: \.item, item) + baseItemOnSelect(item) } }