diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift index f347c744..d8c26246 100644 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -7,57 +7,95 @@ */ import SwiftUI +import SwiftUICollection +import JellyfinAPI struct LibraryView: View { - @StateObject var viewModel: LibraryViewModel - var title: String + @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 - - @State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 250) - - var body: some View { - Group { - if viewModel.isLoading == true { - ProgressView() - } else if !viewModel.items.isEmpty { - ScrollView(.vertical) { - LazyVGrid(columns: tracks) { - ForEach(viewModel.items, id: \.id) { item in - if item.type != "Folder" { - NavigationLink(destination: LazyView { ItemView(item: item) }) { - PortraitItemElement(item: item) - }.buttonStyle(PlainNavigationLinkButtonStyle()) - .onAppear { - if item == viewModel.items.last && viewModel.hasNextPage { - print("Last item visible, load more items.") - viewModel.requestNextPageAsync() - } - } - } - } - }.padding() - } - } else { - Text("No results.") - } - } - /* - .sheet(isPresented: $isShowingFilterView) { - LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "") - } - .background( - NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)), - isActive: $isShowingSearchView) { - EmptyView() - } + @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) + + 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: { _, item in + GeometryReader { _ in + if item.type != "Folder" { + NavigationLink(destination: LazyView { ItemView(item: item) }) { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + .onAppear { + if item == viewModel.items.last && viewModel.hasNextPage { + print("Last item visible, load more items.") + viewModel.requestNextPageAsync() + } + } + } + } + } supplementaryView: { _, indexPath in + HStack { + Text("Supp View") + .font(.title3) + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } else { + Text("No results.") } + /* + .sheet(isPresented: $isShowingFilterView) { + LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "") + } + .background( + NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)), + isActive: $isShowingSearchView) { + EmptyView() + } + ) + */ + } } // stream BM^S by nicki! diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 3b1434b8..f5e8e28d 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -252,6 +252,8 @@ 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; + C49FB6592717A06300AAEABB /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C49FB6582717A06300AAEABB /* SwiftUICollection */; }; + C4BFD4E527167B63007739E3 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C4BFD4E427167B63007739E3 /* SwiftUICollection */; }; 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 */; }; @@ -528,6 +530,7 @@ 53272535268BF9710035FBF1 /* SwiftUIFocusGuide in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, + C49FB6592717A06300AAEABB /* SwiftUICollection in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */, 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, @@ -542,6 +545,7 @@ 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, + C4BFD4E527167B63007739E3 /* SwiftUICollection in Frameworks */, 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, @@ -1151,6 +1155,7 @@ 53649AAE269CFAF600A2D8B7 /* Puppy */, 6261A0DF26A0AB710072EF1C /* CombineExt */, 6220D0C826D63F3700B8E046 /* Stinsen */, + C49FB6582717A06300AAEABB /* SwiftUICollection */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -1186,6 +1191,7 @@ 53649AAC269CFAEA00A2D8B7 /* Puppy */, 6260FFF826A09754003FA968 /* CombineExt */, 62C29E9B26D0FE4200C1D2E7 /* Stinsen */, + C4BFD4E427167B63007739E3 /* SwiftUICollection */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -1275,6 +1281,7 @@ 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */, 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */, 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, + C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -2268,6 +2275,14 @@ kind = branch; }; }; + C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ABJC/SwiftUICollection"; + requirement = { + branch = master; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2406,6 +2421,16 @@ package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; + C49FB6582717A06300AAEABB /* SwiftUICollection */ = { + isa = XCSwiftPackageProductDependency; + package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */; + productName = SwiftUICollection; + }; + C4BFD4E427167B63007739E3 /* SwiftUICollection */ = { + isa = XCSwiftPackageProductDependency; + package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */; + productName = SwiftUICollection; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index d601cf81..1050d673 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -136,6 +136,15 @@ "version": "0.1.3" } }, + { + "package": "SwiftUICollection", + "repositoryURL": "https://github.com/ABJC/SwiftUICollection", + "state": { + "branch": "master", + "revision": "e27149382ce8ec21995069c8aab7ca83d61a3120", + "version": null + } + }, { "package": "SwiftUIFocusGuide", "repositoryURL": "https://github.com/rmnblm/SwiftUIFocusGuide", diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index f73d2803..921bd2ce 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -10,6 +10,9 @@ import Combine import Foundation import JellyfinAPI +import SwiftUICollection + +typealias LibraryRow = CollectionRow final class LibraryViewModel: ViewModel { var parentID: String? @@ -18,6 +21,7 @@ final class LibraryViewModel: ViewModel { var studio: NameGuidPair? @Published var items = [BaseItemDto]() + @Published var rows = [LibraryRow]() @Published var totalPages = 0 @Published var currentPage = 0 @@ -26,6 +30,8 @@ final class LibraryViewModel: ViewModel { // temp @Published var filters: LibraryFilters + + private let columns: Int var enabledFilterType: [FilterType] { if genre == nil { @@ -35,16 +41,20 @@ 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])) { + 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 + ) { self.parentID = parentID self.person = person self.genre = genre self.studio = studio self.filters = filters + self.columns = columns super.init() $filters @@ -79,6 +89,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items = response.items ?? [] + self.calculateRows() }) .store(in: &cancellables) } @@ -108,6 +119,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items.append(contentsOf: response.items ?? []) + self.calculateRows() }) .store(in: &cancellables) } @@ -126,4 +138,22 @@ final class LibraryViewModel: ViewModel { currentPage -= 1 requestItems(with: filters) } + + private func calculateRows() { + let rowCount = items.count / columns + rows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex >= items.count { + lastItemIndex = items.count - 1 + } + rows.append( + LibraryRow( + section: i, + items: Array(items[firstItemIndex...lastItemIndex]) + ) + ) + } + } }