diff --git a/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift b/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift index ad793923..fd59664a 100644 --- a/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift +++ b/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift @@ -9,11 +9,14 @@ import SwiftUI import JellyfinAPI import Combine +import Stinsen struct ContinueWatchingView: View { var items: [BaseItemDto] @Namespace private var namespace + var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() + var body: some View { VStack(alignment: .leading) { if items.count > 0 { @@ -25,7 +28,9 @@ struct ContinueWatchingView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemView(item: item) }) { + Button { + self.homeRouter?.route(to: \.modalItem, item) + } label: { LandscapeItemElement(item: item) } .buttonStyle(PlainNavigationLinkButtonStyle()) diff --git a/JellyfinPlayer tvOS/Views/HomeView.swift b/JellyfinPlayer tvOS/Views/HomeView.swift index 8642a1a6..6ab1d03f 100644 --- a/JellyfinPlayer tvOS/Views/HomeView.swift +++ b/JellyfinPlayer tvOS/Views/HomeView.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI struct HomeView: View { + @EnvironmentObject var homeRouter: HomeCoordinator.Router @StateObject var viewModel = HomeViewModel() @State var showingSettings = false @@ -33,9 +34,9 @@ struct HomeView: View { VStack(alignment: .leading) { let library = viewModel.libraries.first(where: { $0.id == libraryID }) - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") - }) { + Button { + self.homeRouter.route(to: \.modalLibrary, (.init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")) + } label: { HStack { Text("Latest \(library?.name ?? "")") .font(.headline) diff --git a/JellyfinPlayer tvOS/Views/LibraryListView.swift b/JellyfinPlayer tvOS/Views/LibraryListView.swift index 7096ba08..051dcbbf 100644 --- a/JellyfinPlayer tvOS/Views/LibraryListView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryListView.swift @@ -16,47 +16,11 @@ struct LibraryListView: View { var body: some View { ScrollView { LazyVStack { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites") - }) { - ZStack { - HStack { - Spacer() - Text("Your Favorites") - .font(.subheadline) - .fontWeight(.semibold) - Spacer() - } - } - .padding(16) - .frame(minWidth: 100, maxWidth: .infinity) - } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 5) - - NavigationLink(destination: LazyView { - Text("WIP") - }) { - ZStack { - HStack { - Spacer() - Text("All Genres") - .font(.subheadline) - .fontWeight(.semibold) - Spacer() - } - } - .padding(16) - .frame(minWidth: 100, maxWidth: .infinity) - } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 15) - if !viewModel.isLoading { ForEach(viewModel.libraries, id: \.id) { library in if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { + EmptyView() + } else { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") }) { @@ -80,8 +44,6 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 5) - } else { - EmptyView() } } } else { diff --git a/JellyfinPlayer tvOS/Views/LibraryView.swift b/JellyfinPlayer tvOS/Views/LibraryView.swift index e7035067..d4f05aeb 100644 --- a/JellyfinPlayer tvOS/Views/LibraryView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryView.swift @@ -11,80 +11,85 @@ import SwiftUICollection import JellyfinAPI struct LibraryView: View { - @StateObject var viewModel: LibraryViewModel - var title: String + @EnvironmentObject var libraryRouter: LibraryCoordinator.Router + @StateObject var viewModel: LibraryViewModel + var title: String - // MARK: tracks for grid - var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) + // MARK: tracks for grid + var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) - @State var isShowingSearchView = false - @State var isShowingFilterView = false - - var body: some View { + @State var isShowingSearchView = false + @State var isShowingFilterView = false + + var body: some View { if viewModel.isLoading == true { ProgressView() - } else if !viewModel.items.isEmpty { - CollectionView(rows: viewModel.rows) { _, _ in - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) - let groupSize = NSCollectionLayoutSize( - widthDimension: .absolute(200), - heightDimension: .absolute(300) - ) - let group = NSCollectionLayoutGroup.horizontal( - layoutSize: groupSize, - subitems: [item] - ) + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(200), + heightDimension: .absolute(300) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) - let header = - NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .absolute(44) - ), - elementKind: UICollectionView.elementKindSectionHeader, - alignment: .topLeading - ) + let header = + NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(44) + ), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) - let section = NSCollectionLayoutSection(group: group) + let section = NSCollectionLayoutSection(group: group) - section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) - section.interGroupSpacing = 48 - section.orthogonalScrollingBehavior = .continuous - section.boundarySupplementaryItems = [header] - return section - } cell: { _, cell in - GeometryReader { _ in - if let item = cell.item { - if item.type != "Folder" { - NavigationLink(destination: LazyView { ItemView(item: item) }) { - PortraitItemElement(item: item) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - .onAppear { - if item == viewModel.items.last && viewModel.hasNextPage { - viewModel.requestNextPageAsync() + section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) + section.interGroupSpacing = 48 + section.orthogonalScrollingBehavior = .continuous + section.boundarySupplementaryItems = [header] + return section + } cell: { _, cell in + GeometryReader { _ in + if let item = cell.item { + if item.type != "Folder" { + Button { + libraryRouter.route(to: \.modalItem, item) + } label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + .onAppear { + if item == viewModel.items.last && viewModel.hasNextPage { + viewModel.requestNextPageAsync() + } + } + } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) } - } } - } else if cell.loadingCell { - ProgressView() - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) - } + } supplementaryView: { _, indexPath in + HStack { + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") } - } supplementaryView: { _, indexPath in - HStack { - Spacer() - }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .ignoresSafeArea(.all) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) } else { - Text("No results.") + Button { } label: { + Text("No results.") + } } } } diff --git a/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift b/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift new file mode 100644 index 00000000..4a78d1cb --- /dev/null +++ b/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift @@ -0,0 +1,82 @@ +/* + * JellyfinPlayer/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 SwiftUICollection +import JellyfinAPI + +struct MovieLibrariesView: View { + @EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router + @StateObject var viewModel: MovieLibrariesViewModel + var title: String + + var body: some View { + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(200), + heightDimension: .absolute(300) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let header = + NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(44) + ), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) + + let section = NSCollectionLayoutSection(group: group) + + section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) + section.interGroupSpacing = 48 + section.orthogonalScrollingBehavior = .continuous + section.boundarySupplementaryItems = [header] + return section + } cell: { _, cell in + GeometryReader { _ in + if let item = cell.item { + if item.type != "Folder" { + Button { + self.movieLibrariesRouter.route(to: \.library, item) + } label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + } + } + } supplementaryView: { _, indexPath in + HStack { + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } else { + Text("No results.") + } + } +} diff --git a/JellyfinPlayer tvOS/Views/NextUpView.swift b/JellyfinPlayer tvOS/Views/NextUpView.swift index 1db5d360..8a61a3e5 100644 --- a/JellyfinPlayer tvOS/Views/NextUpView.swift +++ b/JellyfinPlayer tvOS/Views/NextUpView.swift @@ -9,9 +9,12 @@ import SwiftUI import JellyfinAPI import Combine +import Stinsen struct NextUpView: View { var items: [BaseItemDto] + + var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() var body: some View { VStack(alignment: .leading) { @@ -24,7 +27,9 @@ struct NextUpView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemView(item: item) }) { + Button { + self.homeRouter?.route(to: \.modalItem, item) + } label: { LandscapeItemElement(item: item) }.buttonStyle(PlainNavigationLinkButtonStyle()) } diff --git a/JellyfinPlayer tvOS/Views/TVLibrariesView.swift b/JellyfinPlayer tvOS/Views/TVLibrariesView.swift new file mode 100644 index 00000000..725a13d6 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/TVLibrariesView.swift @@ -0,0 +1,80 @@ +/* + * JellyfinPlayer/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 SwiftUICollection +import JellyfinAPI + +struct TVLibrariesView: View { + @EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router + @StateObject var viewModel: TVLibrariesViewModel + var title: String + + var body: some View { + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(200), + heightDimension: .absolute(300) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let header = + NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(44) + ), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) + + let section = NSCollectionLayoutSection(group: group) + + section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) + section.interGroupSpacing = 48 + section.orthogonalScrollingBehavior = .continuous + section.boundarySupplementaryItems = [header] + return section + } cell: { _, cell in + GeometryReader { _ in + if let item = cell.item { + if item.type != "Folder" { + Button {} label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + } + } + } supplementaryView: { _, indexPath in + HStack { + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } else { + Text("No results.") + } + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 85605874..63a1911f 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -219,7 +219,19 @@ 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; + C40CD922271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; }; + C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; }; + C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; }; + C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; }; + C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; + C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; + C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; + C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; + C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; + C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; + C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; + C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; @@ -522,6 +534,12 @@ 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = ""; }; + C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = ""; }; + C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = ""; }; + C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = ""; }; + C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = ""; }; + C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = ""; }; + C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = ""; }; C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = ""; }; @@ -677,6 +695,8 @@ 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, + C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, + C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */, 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */, 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, @@ -1050,6 +1070,8 @@ 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, + C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, + C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, @@ -1120,6 +1142,8 @@ C4E508172703E8190045C9AB /* LibraryListView.swift */, C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, + C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, + C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, 531690EE267ABF72005D8AB9 /* NextUpView.swift */, E193D54F2719430400900D82 /* ServerDetailView.swift */, E193D54A271941D300900D82 /* ServerListView.swift */, @@ -1632,6 +1656,7 @@ E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, + C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, 531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */, C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, @@ -1640,6 +1665,7 @@ 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, + C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, @@ -1672,6 +1698,7 @@ 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */, E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */, @@ -1706,6 +1733,7 @@ C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, 531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, + C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, @@ -1731,7 +1759,9 @@ 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, + C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, + C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, @@ -1761,6 +1791,7 @@ E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, + C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, @@ -1781,6 +1812,7 @@ 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, + C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, @@ -1789,6 +1821,7 @@ 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, + C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, @@ -1828,12 +1861,14 @@ 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, + C40CD922271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, + C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, @@ -1848,6 +1883,7 @@ 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, + C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index 31837271..e30c79af 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -20,6 +20,8 @@ final class HomeCoordinator: NavigationCoordinatable { @Route(.modal) var settings = makeSettings @Route(.push) var library = makeLibrary @Route(.push) var item = makeItem + @Route(.modal) var modalItem = makeModalItem + @Route(.modal) var modalLibrary = makeModalLibrary func makeSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(SettingsCoordinator()) @@ -32,6 +34,14 @@ final class HomeCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } + + func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator { + return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) + } @ViewBuilder func makeStart() -> some View { HomeView() diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index c90234c5..a5ef3495 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -22,6 +22,7 @@ final class LibraryCoordinator: NavigationCoordinatable { @Route(.push) var search = makeSearch @Route(.modal) var filter = makeFilter @Route(.push) var item = makeItem + @Route(.modal) var modalItem = makeModalItem let viewModel: LibraryViewModel let title: String @@ -48,4 +49,8 @@ final class LibraryCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } } diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 82edaedf..8be5a5c6 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -14,12 +14,16 @@ import Stinsen final class MainTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ \MainTabCoordinator.home, - \MainTabCoordinator.allMedia, + \MainTabCoordinator.tv, + \MainTabCoordinator.movies, + \MainTabCoordinator.other, \MainTabCoordinator.settings ]) @Route(tabItem: makeHomeTab) var home = makeHome - @Route(tabItem: makeAllMediaTab) var allMedia = makeAllMedia + @Route(tabItem: makeTvTab) var tv = makeTv + @Route(tabItem: makeMoviesTab) var movies = makeMovies + @Route(tabItem: makeOtherTab) var other = makeOther @Route(tabItem: makeSettingsTab) var settings = makeSettings func makeHome() -> NavigationViewCoordinator { @@ -32,15 +36,37 @@ final class MainTabCoordinator: TabCoordinatable { Text("Home") } } + + func makeTv() -> NavigationViewCoordinator { + return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows")) + } - func makeAllMedia() -> NavigationViewCoordinator { + @ViewBuilder func makeTvTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "tv") + Text("TV Shows") + } + } + + func makeMovies() -> NavigationViewCoordinator { + return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies")) + } + + @ViewBuilder func makeMoviesTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "film") + Text("Movies") + } + } + + func makeOther() -> NavigationViewCoordinator { return NavigationViewCoordinator(LibraryListCoordinator()) } - @ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View { + @ViewBuilder func makeOtherTab(isActive: Bool) -> some View { HStack { Image(systemName: "folder") - Text("All Media") + Text("Other") } } diff --git a/Shared/Coordinators/MoviesLibrariesCoordinator.swift b/Shared/Coordinators/MoviesLibrariesCoordinator.swift new file mode 100644 index 00000000..9c530d3c --- /dev/null +++ b/Shared/Coordinators/MoviesLibrariesCoordinator.swift @@ -0,0 +1,37 @@ +// +/* + * 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 Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class MovieLibrariesCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start) + + @Root var start = makeStart + @Route(.push) var library = makeLibrary + + let viewModel: MovieLibrariesViewModel + let title: String + + init(viewModel: MovieLibrariesViewModel, title: String) { + self.viewModel = viewModel + self.title = title + } + + @ViewBuilder func makeStart() -> some View { + MovieLibrariesView(viewModel: self.viewModel, title: title) + } + + func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { + LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + } +} diff --git a/Shared/Coordinators/TVLibrariesCoordinator.swift b/Shared/Coordinators/TVLibrariesCoordinator.swift new file mode 100644 index 00000000..2ad50744 --- /dev/null +++ b/Shared/Coordinators/TVLibrariesCoordinator.swift @@ -0,0 +1,37 @@ +// +/* + * 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 Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class TVLibrariesCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \TVLibrariesCoordinator.start) + + @Root var start = makeStart + @Route(.push) var library = makeLibrary + + let viewModel: TVLibrariesViewModel + let title: String + + init(viewModel: TVLibrariesViewModel, title: String) { + self.viewModel = viewModel + self.title = title + } + + @ViewBuilder func makeStart() -> some View { + TVLibrariesView(viewModel: self.viewModel, title: title) + } + + func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { + LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + } +} diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 1df7ac24..f0839708 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -15,9 +15,9 @@ import SwiftUICollection typealias LibraryRow = CollectionRow struct LibraryRowCell: Hashable { - let id = UUID() - let item: BaseItemDto? - var loadingCell: Bool = false + let id = UUID() + let item: BaseItemDto? + var loadingCell: Bool = false } final class LibraryViewModel: ViewModel { @@ -38,6 +38,7 @@ final class LibraryViewModel: ViewModel { @Published var filters: LibraryFilters private let columns: Int + private var libraries = [BaseItemDto]() var enabledFilterType: [FilterType] { if genre == nil { @@ -48,12 +49,12 @@ final class LibraryViewModel: ViewModel { } init( - parentID: String? = nil, - person: BaseItemPerson? = nil, - genre: NameGuidPair? = nil, - studio: NameGuidPair? = nil, - filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]), - columns: Int = 7 + parentID: String? = nil, + person: BaseItemPerson? = nil, + genre: NameGuidPair? = nil, + studio: NameGuidPair? = nil, + filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]), + columns: Int = 7 ) { self.parentID = parentID self.person = person @@ -63,9 +64,11 @@ final class LibraryViewModel: ViewModel { self.columns = columns super.init() + $filters .sink(receiveValue: requestItems(with:)) .store(in: &cancellables) + } func requestItems(with filters: LibraryFilters) { @@ -95,7 +98,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items = response.items ?? [] - self.rows = self.calculateRows() + self.rows = self.calculateRows(for: self.items) }) .store(in: &cancellables) } @@ -125,7 +128,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items.append(contentsOf: response.items ?? []) - self.rows = self.calculateRows() + self.rows = self.calculateRows(for: self.items) }) .store(in: &cancellables) } @@ -145,37 +148,35 @@ final class LibraryViewModel: ViewModel { requestItems(with: filters) } - private func calculateRows() -> [LibraryRow] { - guard items.count > 0 else { return [] } - let rowCount = items.count / columns - var calculatedRows = [LibraryRow]() - for i in (0...rowCount) { - - let firstItemIndex = i * columns - var lastItemIndex = firstItemIndex + columns - if lastItemIndex > items.count { - lastItemIndex = items.count - } - - var rowCells = [LibraryRowCell]() - for item in items[firstItemIndex.. [LibraryRow] { + guard itemList.count > 0 else { return [] } + let rowCount = itemList.count / columns + var calculatedRows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex > itemList.count { + lastItemIndex = itemList.count + } + + var rowCells = [LibraryRowCell]() + for item in itemList[firstItemIndex.. [LibraryRow] { + guard libraries.count > 0 else { return [] } + let rowCount = libraries.count / columns + var calculatedRows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex > libraries.count { + lastItemIndex = libraries.count + } + + var rowCells = [LibraryRowCell]() + for item in libraries[firstItemIndex.. [LibraryRow] { + guard libraries.count > 0 else { return [] } + let rowCount = libraries.count / columns + var calculatedRows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex > libraries.count { + lastItemIndex = libraries.count + } + + var rowCells = [LibraryRowCell]() + for item in libraries[firstItemIndex..