iOS/iPadOS - Transition library views to Collection Views (#536)
This commit is contained in:
parent
8f42e20f0b
commit
5d0f933a2c
|
@ -42,7 +42,7 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
LibraryView(viewModel: self.viewModel, title: title)
|
LibraryView(viewModel: self.viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
|
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
|
||||||
|
|
|
@ -46,7 +46,7 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func poster(type: PosterType, width: CGFloat) -> some View {
|
func posterStyle(type: PosterType, width: CGFloat) -> some View {
|
||||||
Group {
|
Group {
|
||||||
switch type {
|
switch type {
|
||||||
case .portrait:
|
case .portrait:
|
||||||
|
|
|
@ -7,12 +7,21 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import SwiftUI
|
||||||
|
|
||||||
enum PosterType: String, CaseIterable, Defaults.Serializable {
|
enum PosterType: String, CaseIterable, Defaults.Serializable {
|
||||||
case portrait
|
case portrait
|
||||||
case landscape
|
case landscape
|
||||||
|
|
||||||
|
var width: CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .portrait:
|
||||||
|
return Width.portrait
|
||||||
|
case .landscape:
|
||||||
|
return Width.landscape
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var localizedName: String {
|
var localizedName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .portrait:
|
case .portrait:
|
||||||
|
@ -21,4 +30,18 @@ enum PosterType: String, CaseIterable, Defaults.Serializable {
|
||||||
return "Landscape"
|
return "Landscape"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Width {
|
||||||
|
#if os(tvOS)
|
||||||
|
static let portrait = 250.0
|
||||||
|
|
||||||
|
static let landscape = 490.0
|
||||||
|
#else
|
||||||
|
@ScaledMetric(relativeTo: .largeTitle)
|
||||||
|
static var portrait = 100.0
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .largeTitle)
|
||||||
|
static var landscape = 200.0
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,12 @@ import JellyfinAPI
|
||||||
|
|
||||||
struct LibraryFilters: Codable, Hashable {
|
struct LibraryFilters: Codable, Hashable {
|
||||||
var filters: [ItemFilter] = []
|
var filters: [ItemFilter] = []
|
||||||
var sortOrder: [APISortOrder] = [.descending]
|
var sortOrder: [APISortOrder] = [.ascending]
|
||||||
var withGenres: [NameGuidPair] = []
|
var withGenres: [NameGuidPair] = []
|
||||||
var tags: [String] = []
|
var tags: [String] = []
|
||||||
var sortBy: [SortBy] = [.name]
|
var sortBy: [SortBy] = [.name]
|
||||||
|
|
||||||
|
static let `default` = LibraryFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortBy: String, Codable, CaseIterable {
|
public enum SortBy: String, Codable, CaseIterable {
|
||||||
|
|
|
@ -36,6 +36,7 @@ extension Defaults.Keys {
|
||||||
static let recentlyAddedPosterType = Key<PosterType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite)
|
static let recentlyAddedPosterType = Key<PosterType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite)
|
||||||
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
|
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
|
||||||
static let recommendedPosterType = Key<PosterType>("recommendedPosterType", default: .portrait, suite: .generalSuite)
|
static let recommendedPosterType = Key<PosterType>("recommendedPosterType", default: .portrait, suite: .generalSuite)
|
||||||
|
static let libraryPosterType = Key<PosterType>("libraryPosterType", default: .portrait, suite: .generalSuite)
|
||||||
|
|
||||||
enum Episodes {
|
enum Episodes {
|
||||||
static let useSeriesLandscapeBackdrop = Key<Bool>("useSeriesBackdrop", default: true, suite: .generalSuite)
|
static let useSeriesLandscapeBackdrop = Key<Bool>("useSeriesBackdrop", default: true, suite: .generalSuite)
|
||||||
|
|
|
@ -8,26 +8,16 @@
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUICollection
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
typealias LibraryRow = CollectionRow<Int, LibraryRowCell>
|
|
||||||
|
|
||||||
struct LibraryRowCell: Hashable {
|
|
||||||
let id = UUID()
|
|
||||||
let item: BaseItemDto?
|
|
||||||
var loadingCell: Bool = false
|
|
||||||
}
|
|
||||||
|
|
||||||
final class LibraryViewModel: ViewModel {
|
final class LibraryViewModel: ViewModel {
|
||||||
|
|
||||||
|
@Default(.Customization.libraryPosterType)
|
||||||
|
var libraryPosterType
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var items: [BaseItemDto] = []
|
var items: [BaseItemDto] = []
|
||||||
@Published
|
|
||||||
var rows: [LibraryRow] = []
|
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var totalPages = 0
|
var totalPages = 0
|
||||||
@Published
|
@Published
|
||||||
|
@ -43,8 +33,11 @@ final class LibraryViewModel: ViewModel {
|
||||||
var person: BaseItemPerson?
|
var person: BaseItemPerson?
|
||||||
var genre: NameGuidPair?
|
var genre: NameGuidPair?
|
||||||
var studio: NameGuidPair?
|
var studio: NameGuidPair?
|
||||||
private let columns: Int
|
|
||||||
private let pageItemSize: Int
|
private var pageItemSize: Int {
|
||||||
|
let height = libraryPosterType == .portrait ? libraryPosterType.width * 1.5 : libraryPosterType.width / 1.77
|
||||||
|
return UIScreen.itemsFillableOnScreen(width: libraryPosterType.width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
var enabledFilterType: [FilterType] {
|
var enabledFilterType: [FilterType] {
|
||||||
if genre == nil {
|
if genre == nil {
|
||||||
|
@ -59,18 +52,13 @@ final class LibraryViewModel: ViewModel {
|
||||||
person: BaseItemPerson? = nil,
|
person: BaseItemPerson? = nil,
|
||||||
genre: NameGuidPair? = nil,
|
genre: NameGuidPair? = nil,
|
||||||
studio: NameGuidPair? = nil,
|
studio: NameGuidPair? = nil,
|
||||||
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]),
|
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name])
|
||||||
columns: Int = 7
|
|
||||||
) {
|
) {
|
||||||
self.parentID = parentID
|
self.parentID = parentID
|
||||||
self.person = person
|
self.person = person
|
||||||
self.genre = genre
|
self.genre = genre
|
||||||
self.studio = studio
|
self.studio = studio
|
||||||
self.filters = filters
|
self.filters = filters
|
||||||
self.columns = columns
|
|
||||||
|
|
||||||
// Size is typical size of portrait items
|
|
||||||
self.pageItemSize = UIScreen.itemsFillableOnScreen(width: 130, height: 185)
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
@ -137,7 +125,6 @@ final class LibraryViewModel: ViewModel {
|
||||||
self.totalPages = Int(totalPages)
|
self.totalPages = Int(totalPages)
|
||||||
self.hasNextPage = self.currentPage < self.totalPages - 1
|
self.hasNextPage = self.currentPage < self.totalPages - 1
|
||||||
self.items.append(contentsOf: response.items ?? [])
|
self.items.append(contentsOf: response.items ?? [])
|
||||||
self.rows = self.calculateRows(for: self.items)
|
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
@ -146,50 +133,13 @@ final class LibraryViewModel: ViewModel {
|
||||||
currentPage += 1
|
currentPage += 1
|
||||||
requestItemsAsync(with: filters)
|
requestItemsAsync(with: filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tvOS calculations for collection view
|
|
||||||
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
|
|
||||||
guard !itemList.isEmpty 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 ..< lastItemIndex] {
|
|
||||||
let newCell = LibraryRowCell(item: item)
|
|
||||||
rowCells.append(newCell)
|
|
||||||
}
|
|
||||||
if i == rowCount, hasNextPage {
|
|
||||||
var loadingCell = LibraryRowCell(item: nil)
|
|
||||||
loadingCell.loadingCell = true
|
|
||||||
rowCells.append(loadingCell)
|
|
||||||
}
|
|
||||||
|
|
||||||
calculatedRows.append(LibraryRow(
|
|
||||||
section: i,
|
|
||||||
items: rowCells
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return calculatedRows
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UIScreen {
|
extension UIScreen {
|
||||||
|
|
||||||
static func itemsFillableOnScreen(width: CGFloat, height: CGFloat) -> Int {
|
static func itemsFillableOnScreen(width: CGFloat, height: CGFloat) -> Int {
|
||||||
|
|
||||||
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
|
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
|
||||||
let itemSize = width * height
|
let itemSize = width * height
|
||||||
|
return Int(screenSize / itemSize)
|
||||||
#if os(tvOS)
|
|
||||||
return Int(screenSize / itemSize) * 2
|
|
||||||
#else
|
|
||||||
return Int(screenSize / itemSize)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,14 @@ import JellyfinAPI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
import SwiftUICollection
|
import SwiftUICollection
|
||||||
|
|
||||||
|
typealias LibraryRow = CollectionRow<Int, LibraryRowCell>
|
||||||
|
|
||||||
|
struct LibraryRowCell: Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
let item: BaseItemDto?
|
||||||
|
var loadingCell: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
final class TVLibrariesViewModel: ViewModel {
|
final class TVLibrariesViewModel: ViewModel {
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.765",
|
|
||||||
"green" : "0.361",
|
|
||||||
"red" : "0.667"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,10 +8,12 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
|
enum PosterButtonWidth {
|
||||||
|
static let landscape = 490.0
|
||||||
|
static let portrait = 250.0
|
||||||
|
}
|
||||||
|
|
||||||
private let landscapePosterWidth = 490.0
|
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
|
||||||
private let portraitPosterWidth = 250.0
|
|
||||||
|
|
||||||
private let item: Item
|
private let item: Item
|
||||||
private let type: PosterType
|
private let type: PosterType
|
||||||
|
@ -26,9 +28,9 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
|
||||||
private var itemWidth: CGFloat {
|
private var itemWidth: CGFloat {
|
||||||
switch type {
|
switch type {
|
||||||
case .portrait:
|
case .portrait:
|
||||||
return portraitPosterWidth * itemScale
|
return PosterButtonWidth.portrait * itemScale
|
||||||
case .landscape:
|
case .landscape:
|
||||||
return landscapePosterWidth * itemScale
|
return PosterButtonWidth.landscape * itemScale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,10 +64,10 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
|
||||||
switch type {
|
switch type {
|
||||||
case .portrait:
|
case .portrait:
|
||||||
ImageView(item.portraitPosterImageSource(maxWidth: itemWidth))
|
ImageView(item.portraitPosterImageSource(maxWidth: itemWidth))
|
||||||
.poster(type: type, width: itemWidth)
|
.posterStyle(type: type, width: itemWidth)
|
||||||
case .landscape:
|
case .landscape:
|
||||||
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
||||||
.poster(type: type, width: itemWidth)
|
.posterStyle(type: type, width: itemWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(CardButtonStyle())
|
.buttonStyle(CardButtonStyle())
|
||||||
|
@ -74,7 +76,7 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
|
||||||
})
|
})
|
||||||
.overlay {
|
.overlay {
|
||||||
imageOverlay(item)
|
imageOverlay(item)
|
||||||
.poster(type: type, width: itemWidth)
|
.posterStyle(type: type, width: itemWidth)
|
||||||
}
|
}
|
||||||
.posterShadow()
|
.posterShadow()
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ struct LatestInLibraryView: View {
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.poster(type: .portrait, width: 250)
|
.posterStyle(type: .portrait, width: 250)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,95 +6,65 @@
|
||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import JellyfinAPI
|
import CollectionView
|
||||||
|
import Defaults
|
||||||
|
import Introspect
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUICollection
|
|
||||||
|
|
||||||
struct LibraryView: View {
|
struct LibraryView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var libraryRouter: LibraryCoordinator.Router
|
private var libraryRouter: LibraryCoordinator.Router
|
||||||
@StateObject
|
@ObservedObject
|
||||||
var viewModel: LibraryViewModel
|
var viewModel: LibraryViewModel
|
||||||
var title: String
|
|
||||||
|
|
||||||
// MARK: tracks for grid
|
|
||||||
|
|
||||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
var isShowingSearchView = false
|
private var scrollViewOffset: CGPoint = .zero
|
||||||
@State
|
|
||||||
var isShowingFilterView = false
|
@Default(.Customization.libraryPosterType)
|
||||||
|
var libraryPosterType
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var loadingView: some View {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var noResultsView: some View {
|
||||||
|
L10n.noResults.text
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var libraryItemsView: some View {
|
||||||
|
CollectionView(items: viewModel.items) { _, item, _ in
|
||||||
|
PosterButton(item: item, type: libraryPosterType)
|
||||||
|
.onSelect { item in
|
||||||
|
libraryRouter.route(to: \.item, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layout { _, layoutEnvironment in
|
||||||
|
.grid(
|
||||||
|
layoutEnvironment: layoutEnvironment,
|
||||||
|
layoutMode: .fixedNumberOfColumns(6),
|
||||||
|
lineSpacing: 50
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.willReachEdge(insets: .init(top: 0, leading: 0, bottom: 600, trailing: 0)) { edge in
|
||||||
|
if !viewModel.isLoading && edge == .bottom {
|
||||||
|
viewModel.requestNextPageAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollViewOffset($scrollViewOffset)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if viewModel.rows.isEmpty && viewModel.isLoading == true {
|
Group {
|
||||||
ProgressView()
|
if viewModel.isLoading && viewModel.items.isEmpty {
|
||||||
} else if !viewModel.rows.isEmpty {
|
loadingView
|
||||||
CollectionView(rows: viewModel.rows) { _, _ in
|
} else if viewModel.items.isEmpty {
|
||||||
let itemSize = NSCollectionLayoutSize(
|
noResultsView
|
||||||
widthDimension: .fractionalWidth(1),
|
} else {
|
||||||
heightDimension: .fractionalHeight(1)
|
libraryItemsView
|
||||||
)
|
|
||||||
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 {
|
|
||||||
Button {
|
|
||||||
libraryRouter.route(to: \.item, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} supplementaryView: { _, indexPath in
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
}.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
} else {
|
|
||||||
VStack {
|
|
||||||
L10n.noResults.text
|
|
||||||
Button {} label: {
|
|
||||||
L10n.refresh.text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,7 +223,6 @@
|
||||||
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||||
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
||||||
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
|
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
|
||||||
C4D0CE4B2848570700345D11 /* ASCollectionView in Frameworks */ = {isa = PBXBuildFile; productRef = C4D0CE4A2848570700345D11 /* ASCollectionView */; };
|
|
||||||
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
||||||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
||||||
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; };
|
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; };
|
||||||
|
@ -250,7 +249,6 @@
|
||||||
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
|
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
|
||||||
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
|
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
|
||||||
E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; };
|
E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; };
|
||||||
E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */; };
|
|
||||||
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */; };
|
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */; };
|
||||||
E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; };
|
E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; };
|
||||||
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; };
|
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; };
|
||||||
|
@ -321,6 +319,8 @@
|
||||||
E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; };
|
E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; };
|
||||||
E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0F289A4162001A6922 /* HomeErrorView.swift */; };
|
E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0F289A4162001A6922 /* HomeErrorView.swift */; };
|
||||||
E16AA60828A364A6009A983C /* PosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16AA60728A364A6009A983C /* PosterButton.swift */; };
|
E16AA60828A364A6009A983C /* PosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16AA60728A364A6009A983C /* PosterButton.swift */; };
|
||||||
|
E1734D7C28B9577700C66367 /* CollectionView in Frameworks */ = {isa = PBXBuildFile; productRef = E1734D7B28B9577700C66367 /* CollectionView */; };
|
||||||
|
E1734D7E28B9578100C66367 /* CollectionView in Frameworks */ = {isa = PBXBuildFile; productRef = E1734D7D28B9578100C66367 /* CollectionView */; };
|
||||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
||||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
||||||
|
@ -741,7 +741,6 @@
|
||||||
E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = "<group>"; };
|
E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = "<group>"; };
|
||||||
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
||||||
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
||||||
E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectBottomScrollView.swift; sourceTree = "<group>"; };
|
|
||||||
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = "<group>"; };
|
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = "<group>"; };
|
||||||
E118959C289312020042947B /* BaseItemPerson+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemPerson+Poster.swift"; sourceTree = "<group>"; };
|
E118959C289312020042947B /* BaseItemPerson+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemPerson+Poster.swift"; sourceTree = "<group>"; };
|
||||||
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetModifier.swift; sourceTree = "<group>"; };
|
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetModifier.swift; sourceTree = "<group>"; };
|
||||||
|
@ -944,6 +943,7 @@
|
||||||
62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */,
|
62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */,
|
||||||
62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */,
|
62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */,
|
||||||
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
||||||
|
E1734D7E28B9578100C66367 /* CollectionView in Frameworks */,
|
||||||
62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */,
|
62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */,
|
||||||
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
||||||
62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */,
|
62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */,
|
||||||
|
@ -961,12 +961,12 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
C4D0CE4B2848570700345D11 /* ASCollectionView in Frameworks */,
|
|
||||||
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
|
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
|
||||||
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
|
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
|
||||||
E13DD3D327168E65009D4DAF /* Defaults in Frameworks */,
|
E13DD3D327168E65009D4DAF /* Defaults in Frameworks */,
|
||||||
E1101177281B1E8A006A3584 /* Puppy in Frameworks */,
|
E1101177281B1E8A006A3584 /* Puppy in Frameworks */,
|
||||||
E1002B682793CFBA00E47059 /* Algorithms in Frameworks */,
|
E1002B682793CFBA00E47059 /* Algorithms in Frameworks */,
|
||||||
|
E1734D7C28B9577700C66367 /* CollectionView in Frameworks */,
|
||||||
62666E1127E501B900EC0ECD /* UIKit.framework in Frameworks */,
|
62666E1127E501B900EC0ECD /* UIKit.framework in Frameworks */,
|
||||||
62666DF727E5012C00EC0ECD /* MobileVLCKit.xcframework in Frameworks */,
|
62666DF727E5012C00EC0ECD /* MobileVLCKit.xcframework in Frameworks */,
|
||||||
62666E0327E5017100EC0ECD /* CoreMedia.framework in Frameworks */,
|
62666E0327E5017100EC0ECD /* CoreMedia.framework in Frameworks */,
|
||||||
|
@ -1409,7 +1409,6 @@
|
||||||
53F866422687A45400DCD1D7 /* Components */ = {
|
53F866422687A45400DCD1D7 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */,
|
|
||||||
E18E01A7288746AF0022598C /* DotHStack.swift */,
|
E18E01A7288746AF0022598C /* DotHStack.swift */,
|
||||||
E18E01A5288746AF0022598C /* PillHStack.swift */,
|
E18E01A5288746AF0022598C /* PillHStack.swift */,
|
||||||
E16AA60728A364A6009A983C /* PosterButton.swift */,
|
E16AA60728A364A6009A983C /* PosterButton.swift */,
|
||||||
|
@ -2155,6 +2154,7 @@
|
||||||
E13AF3B728A0C598009093AB /* NukeExtensions */,
|
E13AF3B728A0C598009093AB /* NukeExtensions */,
|
||||||
E13AF3B928A0C598009093AB /* NukeUI */,
|
E13AF3B928A0C598009093AB /* NukeUI */,
|
||||||
E13AF3BB28A0C59E009093AB /* BlurHashKit */,
|
E13AF3BB28A0C59E009093AB /* BlurHashKit */,
|
||||||
|
E1734D7D28B9578100C66367 /* CollectionView */,
|
||||||
);
|
);
|
||||||
productName = "JellyfinPlayer tvOS";
|
productName = "JellyfinPlayer tvOS";
|
||||||
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
|
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
|
||||||
|
@ -2190,11 +2190,11 @@
|
||||||
E1002B672793CFBA00E47059 /* Algorithms */,
|
E1002B672793CFBA00E47059 /* Algorithms */,
|
||||||
62666E3827E502CE00EC0ECD /* SwizzleSwift */,
|
62666E3827E502CE00EC0ECD /* SwizzleSwift */,
|
||||||
E1101176281B1E8A006A3584 /* Puppy */,
|
E1101176281B1E8A006A3584 /* Puppy */,
|
||||||
C4D0CE4A2848570700345D11 /* ASCollectionView */,
|
|
||||||
C409CE9D285044C800CABC12 /* SwiftUICollection */,
|
C409CE9D285044C800CABC12 /* SwiftUICollection */,
|
||||||
E19E6E0428A0B958005C10C8 /* Nuke */,
|
E19E6E0428A0B958005C10C8 /* Nuke */,
|
||||||
E19E6E0628A0B958005C10C8 /* NukeUI */,
|
E19E6E0628A0B958005C10C8 /* NukeUI */,
|
||||||
E19E6E0928A0BEFF005C10C8 /* BlurHashKit */,
|
E19E6E0928A0BEFF005C10C8 /* BlurHashKit */,
|
||||||
|
E1734D7B28B9577700C66367 /* CollectionView */,
|
||||||
);
|
);
|
||||||
productName = JellyfinPlayer;
|
productName = JellyfinPlayer;
|
||||||
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
|
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
|
||||||
|
@ -2257,10 +2257,10 @@
|
||||||
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
|
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
|
||||||
62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */,
|
62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */,
|
||||||
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */,
|
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */,
|
||||||
C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */,
|
|
||||||
C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
||||||
E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */,
|
E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||||
E19E6E0828A0BEFF005C10C8 /* XCRemoteSwiftPackageReference "BlurHashKit" */,
|
E19E6E0828A0BEFF005C10C8 /* XCRemoteSwiftPackageReference "BlurHashKit" */,
|
||||||
|
E1734D7A28B9577700C66367 /* XCRemoteSwiftPackageReference "CollectionView" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -2610,7 +2610,6 @@
|
||||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
||||||
E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */,
|
E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */,
|
||||||
E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */,
|
|
||||||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
||||||
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
|
@ -3225,14 +3224,6 @@
|
||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/apptekstudios/ASCollectionView";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 2.0.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
|
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/apple/swift-algorithms.git";
|
repositoryURL = "https://github.com/apple/swift-algorithms.git";
|
||||||
|
@ -3297,6 +3288,14 @@
|
||||||
minimumVersion = 6.0.0;
|
minimumVersion = 6.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
E1734D7A28B9577700C66367 /* XCRemoteSwiftPackageReference "CollectionView" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/LePips/CollectionView";
|
||||||
|
requirement = {
|
||||||
|
branch = main;
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/kean/Nuke";
|
repositoryURL = "https://github.com/kean/Nuke";
|
||||||
|
@ -3369,11 +3368,6 @@
|
||||||
package = C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
package = C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
||||||
productName = SwiftUICollection;
|
productName = SwiftUICollection;
|
||||||
};
|
};
|
||||||
C4D0CE4A2848570700345D11 /* ASCollectionView */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */;
|
|
||||||
productName = ASCollectionView;
|
|
||||||
};
|
|
||||||
E1002B672793CFBA00E47059 /* Algorithms */ = {
|
E1002B672793CFBA00E47059 /* Algorithms */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
|
package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
|
||||||
|
@ -3454,6 +3448,16 @@
|
||||||
package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */;
|
package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
productName = Defaults;
|
productName = Defaults;
|
||||||
};
|
};
|
||||||
|
E1734D7B28B9577700C66367 /* CollectionView */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E1734D7A28B9577700C66367 /* XCRemoteSwiftPackageReference "CollectionView" */;
|
||||||
|
productName = CollectionView;
|
||||||
|
};
|
||||||
|
E1734D7D28B9578100C66367 /* CollectionView */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E1734D7A28B9577700C66367 /* XCRemoteSwiftPackageReference "CollectionView" */;
|
||||||
|
productName = CollectionView;
|
||||||
|
};
|
||||||
E178857C278037FD0094FBCF /* JellyfinAPI */ = {
|
E178857C278037FD0094FBCF /* JellyfinAPI */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */;
|
package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */;
|
||||||
|
|
|
@ -18,15 +18,6 @@
|
||||||
"version" : "0.6.5"
|
"version" : "0.6.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "ascollectionview",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apptekstudios/ASCollectionView",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4288744ba484c1062c109c0f28d72b629d321d55",
|
|
||||||
"version" : "2.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "blurhashkit",
|
"identity" : "blurhashkit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -36,6 +27,15 @@
|
||||||
"version" : "1.1.0"
|
"version" : "1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "collectionview",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/LePips/CollectionView",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "1dbf31d860626f8debdbb08201517a4684d226c6"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "combineext",
|
"identity" : "combineext",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -63,15 +63,6 @@
|
||||||
"version" : "6.3.0"
|
"version" : "6.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "differencekit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/ra1028/DifferenceKit",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "073b9671ce2b9b5b96398611427a1f929927e428",
|
|
||||||
"version" : "1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "jellyfin-sdk-swift",
|
"identity" : "jellyfin-sdk-swift",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/56573373/swiftui-get-size-of-child
|
|
||||||
|
|
||||||
struct ChildSizeReader<Content: View>: View {
|
|
||||||
@Binding
|
|
||||||
var size: CGSize
|
|
||||||
let content: () -> Content
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
content()
|
|
||||||
.background(GeometryReader { proxy in
|
|
||||||
Color.clear
|
|
||||||
.preference(key: SizePreferenceKey.self, value: proxy.size)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.onPreferenceChange(SizePreferenceKey.self) { preferences in
|
|
||||||
self.size = preferences
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SizePreferenceKey: PreferenceKey {
|
|
||||||
typealias Value = CGSize
|
|
||||||
static var defaultValue: Value = .zero
|
|
||||||
|
|
||||||
static func reduce(value _: inout Value, nextValue: () -> Value) {
|
|
||||||
_ = nextValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ViewOffsetKey: PreferenceKey {
|
|
||||||
typealias Value = CGFloat
|
|
||||||
static var defaultValue = CGFloat.zero
|
|
||||||
static func reduce(value: inout Value, nextValue: () -> Value) {
|
|
||||||
value += nextValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DetectBottomScrollView<Content: View>: View {
|
|
||||||
private let spaceName = "scroll"
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var wholeSize: CGSize = .zero
|
|
||||||
@State
|
|
||||||
private var scrollViewSize: CGSize = .zero
|
|
||||||
@State
|
|
||||||
private var previousDidReachBottom = false
|
|
||||||
let content: () -> Content
|
|
||||||
let didReachBottom: (Bool) -> Void
|
|
||||||
|
|
||||||
init(
|
|
||||||
content: @escaping () -> Content,
|
|
||||||
didReachBottom: @escaping (Bool) -> Void
|
|
||||||
) {
|
|
||||||
self.content = content
|
|
||||||
self.didReachBottom = didReachBottom
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ChildSizeReader(size: $wholeSize) {
|
|
||||||
ScrollView {
|
|
||||||
ChildSizeReader(size: $scrollViewSize) {
|
|
||||||
content()
|
|
||||||
.background(GeometryReader { proxy in
|
|
||||||
Color.clear.preference(
|
|
||||||
key: ViewOffsetKey.self,
|
|
||||||
value: -1 * proxy.frame(in: .named(spaceName)).origin.y
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.onPreferenceChange(
|
|
||||||
ViewOffsetKey.self,
|
|
||||||
perform: { value in
|
|
||||||
|
|
||||||
if value >= scrollViewSize.height - wholeSize.height {
|
|
||||||
if !previousDidReachBottom {
|
|
||||||
previousDidReachBottom = true
|
|
||||||
didReachBottom(true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if previousDidReachBottom {
|
|
||||||
previousDidReachBottom = false
|
|
||||||
didReachBottom(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.coordinateSpace(name: spaceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,12 +10,6 @@ import SwiftUI
|
||||||
|
|
||||||
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
|
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle)
|
|
||||||
private var landscapePosterWidth = 200.0
|
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .largeTitle)
|
|
||||||
private var portraitPosterWidth = 100.0
|
|
||||||
|
|
||||||
private let item: Item
|
private let item: Item
|
||||||
private let type: PosterType
|
private let type: PosterType
|
||||||
private let itemScale: CGFloat
|
private let itemScale: CGFloat
|
||||||
|
@ -27,12 +21,7 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
|
||||||
private let singleImage: Bool
|
private let singleImage: Bool
|
||||||
|
|
||||||
private var itemWidth: CGFloat {
|
private var itemWidth: CGFloat {
|
||||||
switch type {
|
type.width * itemScale
|
||||||
case .portrait:
|
|
||||||
return portraitPosterWidth * itemScale
|
|
||||||
case .landscape:
|
|
||||||
return landscapePosterWidth * itemScale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(
|
private init(
|
||||||
|
@ -72,10 +61,10 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
|
||||||
.contextMenu(menuItems: {
|
.contextMenu(menuItems: {
|
||||||
contextMenu(item)
|
contextMenu(item)
|
||||||
})
|
})
|
||||||
.poster(type: type, width: itemWidth)
|
.posterStyle(type: type, width: itemWidth)
|
||||||
.overlay {
|
.overlay {
|
||||||
imageOverlay(item)
|
imageOverlay(item)
|
||||||
.poster(type: type, width: itemWidth)
|
.posterStyle(type: type, width: itemWidth)
|
||||||
}
|
}
|
||||||
.posterShadow()
|
.posterShadow()
|
||||||
|
|
||||||
|
|
|
@ -6,30 +6,19 @@
|
||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import Stinsen
|
import CollectionView
|
||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryView: View {
|
struct LibraryView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var libraryRouter: LibraryCoordinator.Router
|
private var libraryRouter: LibraryCoordinator.Router
|
||||||
@StateObject
|
@ObservedObject
|
||||||
var viewModel: LibraryViewModel
|
var viewModel: LibraryViewModel
|
||||||
var title: String
|
|
||||||
|
|
||||||
// MARK: tracks for grid
|
@Default(.Customization.libraryPosterType)
|
||||||
|
var libraryPosterType
|
||||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var tracks: [GridItem] = Array(
|
|
||||||
repeating: .init(.flexible(), alignment: .top),
|
|
||||||
count: Int(UIScreen.main.bounds.size.width) / 125
|
|
||||||
)
|
|
||||||
|
|
||||||
func recalcTracks() {
|
|
||||||
tracks = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var loadingView: some View {
|
private var loadingView: some View {
|
||||||
|
@ -41,42 +30,48 @@ struct LibraryView: View {
|
||||||
L10n.noResults.text
|
L10n.noResults.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var gridLayout: NSCollectionLayoutSection.GridLayoutMode {
|
||||||
|
if libraryPosterType == .landscape && UIDevice.isPhone {
|
||||||
|
return .fixedNumberOfColumns(2)
|
||||||
|
} else {
|
||||||
|
return .adaptive(withMinItemSize: libraryPosterType.width + (UIDevice.isIPad ? 10 : 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var libraryItemsView: some View {
|
private var libraryItemsView: some View {
|
||||||
DetectBottomScrollView {
|
CollectionView(items: viewModel.items) { _, item, _ in
|
||||||
VStack {
|
PosterButton(item: item, type: libraryPosterType)
|
||||||
LazyVGrid(columns: tracks) {
|
.onSelect { item in
|
||||||
ForEach(viewModel.items, id: \.id) { item in
|
libraryRouter.route(to: \.item, item)
|
||||||
PosterButton(item: item, type: .portrait)
|
|
||||||
.onSelect { item in
|
|
||||||
libraryRouter.route(to: \.item, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.scaleItem(libraryPosterType == .landscape && UIDevice.isPhone ? 0.8 : 1)
|
||||||
.listRowSeparator(.hidden)
|
}
|
||||||
.onRotate { _ in
|
.layout { _, layoutEnvironment in
|
||||||
recalcTracks()
|
.grid(
|
||||||
}
|
layoutEnvironment: layoutEnvironment,
|
||||||
|
layoutMode: gridLayout
|
||||||
Spacer()
|
)
|
||||||
.frame(height: 30)
|
}
|
||||||
}
|
.willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in
|
||||||
} didReachBottom: { newValue in
|
if !viewModel.isLoading && edge == .bottom {
|
||||||
if newValue && viewModel.hasNextPage {
|
|
||||||
viewModel.requestNextPageAsync()
|
viewModel.requestNextPageAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.configure { configuration in
|
||||||
|
configuration.showsVerticalScrollIndicator = false
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if viewModel.isLoading && viewModel.items.isEmpty {
|
if viewModel.isLoading && viewModel.items.isEmpty {
|
||||||
ProgressView()
|
loadingView
|
||||||
} else if !viewModel.items.isEmpty {
|
} else if viewModel.items.isEmpty {
|
||||||
libraryItemsView
|
|
||||||
} else {
|
|
||||||
noResultsView
|
noResultsView
|
||||||
|
} else {
|
||||||
|
libraryItemsView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
@ -93,7 +88,7 @@ struct LibraryView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "line.horizontal.3.decrease.circle")
|
Image(systemName: "line.horizontal.3.decrease.circle")
|
||||||
}
|
}
|
||||||
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
.foregroundColor(viewModel.filters == .default ? .accentColor : Color(UIColor.systemOrange))
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
|
libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import ASCollectionView
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
@ -37,29 +36,30 @@ struct LiveTVChannelsView: View {
|
||||||
if viewModel.isLoading == true {
|
if viewModel.isLoading == true {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if !viewModel.channelPrograms.isEmpty {
|
} else if !viewModel.channelPrograms.isEmpty {
|
||||||
ASCollectionView(data: viewModel.channelPrograms, dataID: \.self) { channelProgram, _ in
|
Text("nothing")
|
||||||
makeCellView(channelProgram)
|
// ASCollectionView(data: viewModel.channelPrograms, dataID: \.self) { channelProgram, _ in
|
||||||
}
|
// makeCellView(channelProgram)
|
||||||
.layout {
|
// }
|
||||||
.grid(
|
// .layout {
|
||||||
layoutMode: .fixedNumberOfColumns(columns),
|
// .grid(
|
||||||
itemSpacing: 16,
|
// layoutMode: .fixedNumberOfColumns(columns),
|
||||||
lineSpacing: 4,
|
// itemSpacing: 16,
|
||||||
itemSize: .absolute(144)
|
// lineSpacing: 4,
|
||||||
)
|
// itemSize: .absolute(144)
|
||||||
}
|
// )
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
// }
|
||||||
.ignoresSafeArea()
|
// .frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.onAppear {
|
// .ignoresSafeArea()
|
||||||
viewModel.startScheduleCheckTimer()
|
// .onAppear {
|
||||||
self.checkOrientation()
|
// viewModel.startScheduleCheckTimer()
|
||||||
}
|
// self.checkOrientation()
|
||||||
.onDisappear {
|
// }
|
||||||
viewModel.stopScheduleCheckTimer()
|
// .onDisappear {
|
||||||
}
|
// viewModel.stopScheduleCheckTimer()
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
// }
|
||||||
self.checkOrientation()
|
// .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||||
}
|
// self.checkOrientation()
|
||||||
|
// }
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
Text("No results.")
|
Text("No results.")
|
||||||
|
|
|
@ -31,6 +31,8 @@ struct CustomizeViewsSettings: View {
|
||||||
var latestInLibraryPosterType
|
var latestInLibraryPosterType
|
||||||
@Default(.Customization.recommendedPosterType)
|
@Default(.Customization.recommendedPosterType)
|
||||||
var recommendedPosterType
|
var recommendedPosterType
|
||||||
|
@Default(.Customization.libraryPosterType)
|
||||||
|
var libraryPosterType
|
||||||
|
|
||||||
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
|
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
|
||||||
var useSeriesLandscapeBackdrop
|
var useSeriesLandscapeBackdrop
|
||||||
|
@ -74,7 +76,7 @@ struct CustomizeViewsSettings: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker(L10n.library, selection: $latestInLibraryPosterType) {
|
Picker(L10n.latestWithString(L10n.library), selection: $latestInLibraryPosterType) {
|
||||||
ForEach(PosterType.allCases, id: \.self) { type in
|
ForEach(PosterType.allCases, id: \.self) { type in
|
||||||
Text(type.localizedName).tag(type.rawValue)
|
Text(type.localizedName).tag(type.rawValue)
|
||||||
}
|
}
|
||||||
|
@ -87,6 +89,11 @@ struct CustomizeViewsSettings: View {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
Picker(L10n.library, selection: $libraryPosterType) {
|
||||||
|
ForEach(PosterType.allCases, id: \.self) { type in
|
||||||
|
Text(type.localizedName).tag(type.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
// TODO: localize after organization
|
// TODO: localize after organization
|
||||||
Text("Posters")
|
Text("Posters")
|
||||||
|
|
Loading…
Reference in New Issue