From 1c2b1879b4c3844aac363c0eb17ca10a919178ef Mon Sep 17 00:00:00 2001 From: jhays Date: Thu, 14 Oct 2021 09:59:35 -0500 Subject: [PATCH 1/6] use CollectionView on LibraryView --- JellyfinPlayer tvOS/LibraryView.swift | 128 ++++++++++++------ JellyfinPlayer.xcodeproj/project.pbxproj | 25 ++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ Shared/ViewModels/LibraryViewModel.swift | 40 +++++- 4 files changed, 152 insertions(+), 50 deletions(-) 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]) + ) + ) + } + } } From db6483ed5e9741db08617bc34f8c51a081ba0c1c Mon Sep 17 00:00:00 2001 From: jhays Date: Thu, 14 Oct 2021 11:54:19 -0500 Subject: [PATCH 2/6] remove unused label --- JellyfinPlayer tvOS/LibraryView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift index d8c26246..bc522420 100644 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -74,8 +74,6 @@ struct LibraryView: View { } } supplementaryView: { _, indexPath in HStack { - Text("Supp View") - .font(.title3) Spacer() }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") } From ffde1c468d84448e8bdcb6088eabb8f9e044fbdf Mon Sep 17 00:00:00 2001 From: jhays Date: Thu, 14 Oct 2021 14:06:23 -0500 Subject: [PATCH 3/6] feedback cleanup --- JellyfinPlayer tvOS/LibraryView.swift | 11 ----------- Shared/ViewModels/LibraryViewModel.swift | 13 ++++++++----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift index bc522420..ce9ccab0 100644 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -82,17 +82,6 @@ struct LibraryView: View { } 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() - } - ) - */ } } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 921bd2ce..6a6273a7 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -89,7 +89,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items = response.items ?? [] - self.calculateRows() + self.rows = self.calculateRows() }) .store(in: &cancellables) } @@ -119,7 +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() + self.rows = self.calculateRows() }) .store(in: &cancellables) } @@ -139,21 +139,24 @@ final class LibraryViewModel: ViewModel { requestItems(with: filters) } - private func calculateRows() { + private func calculateRows() -> [LibraryRow] { + guard items.count > 0 else { return [] } let rowCount = items.count / columns - rows = [LibraryRow]() + var calculatedRows = [LibraryRow]() for i in (0...rowCount) { let firstItemIndex = i * columns var lastItemIndex = firstItemIndex + columns if lastItemIndex >= items.count { lastItemIndex = items.count - 1 } - rows.append( + calculatedRows.append( LibraryRow( section: i, items: Array(items[firstItemIndex...lastItemIndex]) ) ) } + + return calculatedRows } } From 2cfc69640d401b27b0c12b450e0f79c4e8d561bf Mon Sep 17 00:00:00 2001 From: jhays Date: Thu, 14 Oct 2021 22:28:36 -0500 Subject: [PATCH 4/6] cleanup some row math --- JellyfinPlayer tvOS/LibraryView.swift | 7 +++---- Shared/ViewModels/LibraryViewModel.swift | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift index ce9ccab0..1e908ec3 100644 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -65,10 +65,9 @@ struct LibraryView: View { } .buttonStyle(PlainNavigationLinkButtonStyle()) .onAppear { - if item == viewModel.items.last && viewModel.hasNextPage { - print("Last item visible, load more items.") - viewModel.requestNextPageAsync() - } + if item == viewModel.items.last && viewModel.hasNextPage { + viewModel.requestNextPageAsync() + } } } } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 6a6273a7..828d32e3 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -144,15 +144,16 @@ final class LibraryViewModel: ViewModel { 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 - 1 + if lastItemIndex > items.count { + lastItemIndex = items.count } calculatedRows.append( LibraryRow( section: i, - items: Array(items[firstItemIndex...lastItemIndex]) + items: Array(items[firstItemIndex.. Date: Fri, 15 Oct 2021 08:22:08 -0500 Subject: [PATCH 5/6] add loading cell as last item --- JellyfinPlayer tvOS/LibraryView.swift | 23 ++++++++++++++--------- Shared/ViewModels/LibraryViewModel.swift | 22 ++++++++++++++++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift index 1e908ec3..e7035067 100644 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ b/JellyfinPlayer tvOS/LibraryView.swift @@ -57,18 +57,23 @@ struct LibraryView: View { section.orthogonalScrollingBehavior = .continuous section.boundarySupplementaryItems = [header] return section - } cell: { _, item in + } cell: { _, cell 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 { - viewModel.requestNextPageAsync() + 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() + } } } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) } } } supplementaryView: { _, indexPath in diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 828d32e3..48a0f5d4 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -12,7 +12,13 @@ import Foundation import JellyfinAPI import SwiftUICollection -typealias LibraryRow = CollectionRow +typealias LibraryRow = CollectionRow + +struct LibraryRowCell: Hashable { + let id = UUID() + let item: BaseItemDto? + var loadingCell: Bool = false +} final class LibraryViewModel: ViewModel { var parentID: String? @@ -150,10 +156,22 @@ final class LibraryViewModel: ViewModel { if lastItemIndex > items.count { lastItemIndex = items.count } + + var rowCells = [LibraryRowCell]() + for item in items[firstItemIndex.. Date: Fri, 15 Oct 2021 08:24:10 -0500 Subject: [PATCH 6/6] don't add loading cell after last page --- Shared/ViewModels/LibraryViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 48a0f5d4..7a969f24 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -162,7 +162,7 @@ final class LibraryViewModel: ViewModel { let newCell = LibraryRowCell(item: item) rowCells.append(newCell) } - if i == rowCount { + if i == rowCount && hasNextPage { var loadingCell = LibraryRowCell(item: nil) loadingCell.loadingCell = true rowCells.append(loadingCell)