Poster Display and Button Refactor (#1038)

This commit is contained in:
Ethan Pippin 2024-04-23 11:22:07 -06:00 committed by GitHub
parent ad8f4bbefd
commit 384e80805e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 999 additions and 600 deletions

View File

@ -25,7 +25,6 @@ private let imagePipeline = {
// - instead of removing first source on failure, just safe index into sources // - instead of removing first source on failure, just safe index into sources
// TODO: currently SVGs are only supported for logos, which are only used in a few places. // TODO: currently SVGs are only supported for logos, which are only used in a few places.
// make it so when displaying an SVG there is a unified `image` caller modifier // make it so when displaying an SVG there is a unified `image` caller modifier
// TODO: probably don't need both `placeholder` modifiers
struct ImageView: View { struct ImageView: View {
@State @State
@ -80,7 +79,7 @@ extension ImageView {
sources: [source].compacted(using: \.url), sources: [source].compacted(using: \.url),
image: { $0 }, image: { $0 },
placeholder: nil, placeholder: nil,
failure: { DefaultFailureView() } failure: { EmptyView() }
) )
} }
@ -89,7 +88,7 @@ extension ImageView {
sources: sources.compacted(using: \.url), sources: sources.compacted(using: \.url),
image: { $0 }, image: { $0 },
placeholder: nil, placeholder: nil,
failure: { DefaultFailureView() } failure: { EmptyView() }
) )
} }
@ -98,7 +97,7 @@ extension ImageView {
sources: [ImageSource(url: source)], sources: [ImageSource(url: source)],
image: { $0 }, image: { $0 },
placeholder: nil, placeholder: nil,
failure: { DefaultFailureView() } failure: { EmptyView() }
) )
} }
@ -111,7 +110,7 @@ extension ImageView {
sources: imageSources, sources: imageSources,
image: { $0 }, image: { $0 },
placeholder: nil, placeholder: nil,
failure: { DefaultFailureView() } failure: { EmptyView() }
) )
} }
} }
@ -124,10 +123,6 @@ extension ImageView {
copy(modifying: \.image, with: content) copy(modifying: \.image, with: content)
} }
func placeholder(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.placeholder, with: { _ in content() })
}
func placeholder(@ViewBuilder _ content: @escaping (ImageSource) -> any View) -> Self { func placeholder(@ViewBuilder _ content: @escaping (ImageSource) -> any View) -> Self {
copy(modifying: \.placeholder, with: content) copy(modifying: \.placeholder, with: content)
} }
@ -156,9 +151,6 @@ extension ImageView {
var body: some View { var body: some View {
if let blurHash { if let blurHash {
BlurHashView(blurHash: blurHash, size: .Square(length: 8)) BlurHashView(blurHash: blurHash, size: .Square(length: 8))
} else {
Color.secondarySystemFill
.opacity(0.75)
} }
} }
} }

View File

@ -14,32 +14,58 @@ struct SystemImageContentView: View {
@State @State
private var contentSize: CGSize = .zero private var contentSize: CGSize = .zero
@State
private var labelSize: CGSize = .zero
private var backgroundColor: Color private var backgroundColor: Color
private var heightRatio: CGFloat private var heightRatio: CGFloat
private let systemName: String private let systemName: String
private let title: String?
private var widthRatio: CGFloat private var widthRatio: CGFloat
init(systemName: String?) { init(title: String? = nil, systemName: String?) {
self.backgroundColor = Color.secondarySystemFill self.backgroundColor = Color.secondarySystemFill
self.heightRatio = 3 self.heightRatio = 3
self.systemName = systemName ?? "circle" self.systemName = systemName ?? "circle"
self.title = title
self.widthRatio = 3.5 self.widthRatio = 3.5
} }
private var imageView: some View {
Image(systemName: systemName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.secondary)
.accessibilityHidden(true)
.frame(width: contentSize.width / widthRatio, height: contentSize.height / heightRatio)
}
@ViewBuilder
private var label: some View {
if let title {
Text(title)
.lineLimit(2)
.multilineTextAlignment(.center)
.font(.footnote.weight(.regular))
.foregroundColor(.secondary)
.trackingSize($labelSize)
}
}
var body: some View { var body: some View {
ZStack { ZStack {
backgroundColor backgroundColor
.opacity(0.5) .opacity(0.5)
Image(systemName: systemName) imageView
.resizable() .frame(width: contentSize.width)
.aspectRatio(contentMode: .fit) .overlay(alignment: .bottom) {
.foregroundColor(.secondary) label
.accessibilityHidden(true) .padding(.horizontal, 4)
.frame(width: contentSize.width / widthRatio, height: contentSize.height / heightRatio) .offset(y: labelSize.height)
}
} }
.size($contentSize) .trackingSize($contentSize)
} }
} }

View File

@ -57,7 +57,7 @@ final class ItemCoordinator: NavigationCoordinatable {
} }
func makeCastAndCrew(people: [BaseItemPerson]) -> LibraryCoordinator<BaseItemPerson> { func makeCastAndCrew(people: [BaseItemPerson]) -> LibraryCoordinator<BaseItemPerson> {
let viewModel = PagingLibraryViewModel(people, parent: BaseItemDto(name: L10n.castAndCrew)) let viewModel = PagingLibraryViewModel(title: L10n.castAndCrew, people)
return LibraryCoordinator(viewModel: viewModel) return LibraryCoordinator(viewModel: viewModel)
} }

View File

@ -0,0 +1,17 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Dictionary {
subscript(key: Key?) -> Value? {
guard let key else { return nil }
return self[key]
}
}

View File

@ -10,10 +10,10 @@ import SwiftUI
extension EdgeInsets { extension EdgeInsets {
// TODO: tvOS // TODO: finalize tvOS
/// The default padding for View's against contextual edges, /// The padding for Views against contextual edges,
/// typically the edges of the View's scene /// typically the edges of the View's scene
static let defaultEdgePadding: CGFloat = { static let edgePadding: CGFloat = {
#if os(tvOS) #if os(tvOS)
50 50
#else #else
@ -25,7 +25,7 @@ extension EdgeInsets {
#endif #endif
}() }()
static let DefaultEdgeInsets: EdgeInsets = .init(defaultEdgePadding) static let edgeInsets: EdgeInsets = .init(edgePadding)
init(_ constant: CGFloat) { init(_ constant: CGFloat) {
self.init(top: constant, leading: constant, bottom: constant, trailing: constant) self.init(top: constant, leading: constant, bottom: constant, trailing: constant)

View File

@ -11,28 +11,26 @@ import Foundation
import JellyfinAPI import JellyfinAPI
import UIKit import UIKit
// TODO: figure out what to do about screen scaling with .main being deprecated
// - maxWidth assume already scaled?
extension BaseItemDto { extension BaseItemDto {
// MARK: Item Images // MARK: Item Images
func imageURL( func imageURL(
_ type: ImageType, _ type: ImageType,
maxWidth: Int? = nil, maxWidth: CGFloat? = nil,
maxHeight: Int? = nil maxHeight: CGFloat? = nil
) -> URL? { ) -> URL? {
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "") _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "")
} }
func imageURL( // TODO: will server actually only have a single blurhash per type?
_ type: ImageType, // - makes `firstBlurHash` redundant
maxWidth: CGFloat? = nil,
maxHeight: CGFloat? = nil
) -> URL? {
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: id ?? "")
}
func blurHash(_ type: ImageType) -> String? { func blurHash(_ type: ImageType) -> String? {
guard type != .logo else { return nil } guard type != .logo else { return nil }
if let tag = imageTags?[type.rawValue], let taggedBlurHash = imageBlurHashes?[type]?[tag] { if let tag = imageTags?[type.rawValue], let taggedBlurHash = imageBlurHashes?[type]?[tag] {
return taggedBlurHash return taggedBlurHash
} else if let firstBlurHash = imageBlurHashes?[type]?.values.first { } else if let firstBlurHash = imageBlurHashes?[type]?.values.first {
@ -42,49 +40,62 @@ extension BaseItemDto {
return nil return nil
} }
func imageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource {
_imageSource(type, maxWidth: maxWidth, maxHeight: maxHeight)
}
func imageSource(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> ImageSource { func imageSource(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> ImageSource {
_imageSource(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight)) _imageSource(
type,
maxWidth: maxWidth,
maxHeight: maxHeight
)
} }
// MARK: Series Images // MARK: Series Images
func seriesImageURL(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> URL? { /// - Note: Will force the creation of an image source even if it doesn't have a tag, due
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "") /// to episodes also retrieving series images in some areas. This may cause more 404s.
}
func seriesImageURL(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> URL? { func seriesImageURL(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> URL? {
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: seriesID ?? "") _imageURL(
} type,
maxWidth: maxWidth,
func seriesImageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource { maxHeight: maxHeight,
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "") itemID: seriesID ?? "",
return ImageSource(url: url, blurHash: nil) force: true
)
} }
/// - Note: Will force the creation of an image source even if it doesn't have a tag, due
/// to episodes also retrieving series images in some areas. This may cause more 404s.
func seriesImageSource(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> ImageSource { func seriesImageSource(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> ImageSource {
seriesImageSource(type, maxWidth: Int(maxWidth), maxHeight: Int(maxWidth)) let url = _imageURL(
type,
maxWidth: maxWidth,
maxHeight: maxHeight,
itemID: seriesID ?? "",
force: true
)
return ImageSource(
url: url,
blurHash: nil
)
} }
func seriesImageSource(_ type: ImageType, maxWidth: CGFloat) -> ImageSource { // MARK: private
seriesImageSource(type, maxWidth: Int(maxWidth))
}
// MARK: Fileprivate private func _imageURL(
fileprivate func _imageURL(
_ type: ImageType, _ type: ImageType,
maxWidth: Int?, maxWidth: CGFloat?,
maxHeight: Int?, maxHeight: CGFloat?,
itemID: String itemID: String,
force: Bool = false
) -> URL? { ) -> URL? {
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!) let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
let scaleHeight = maxHeight == nil ? nil : UIScreen.main.scale(maxHeight!) let scaleHeight = maxHeight == nil ? nil : UIScreen.main.scale(maxHeight!)
guard let tag = getImageTag(for: type) else { return nil } let tag = getImageTag(for: type)
if tag == nil && !force {
return nil
}
let client = Container.userSession().client let client = Container.userSession().client
let parameters = Paths.GetItemImageParameters( let parameters = Paths.GetItemImageParameters(
@ -105,17 +116,21 @@ extension BaseItemDto {
private func getImageTag(for type: ImageType) -> String? { private func getImageTag(for type: ImageType) -> String? {
switch type { switch type {
case .backdrop: case .backdrop:
return backdropImageTags?.first backdropImageTags?.first
case .screenshot: case .screenshot:
return screenshotImageTags?.first screenshotImageTags?.first
default: default:
return imageTags?[type.rawValue] imageTags?[type.rawValue]
} }
} }
private func _imageSource(_ type: ImageType, maxWidth: Int?, maxHeight: Int?) -> ImageSource { private func _imageSource(_ type: ImageType, maxWidth: CGFloat?, maxHeight: CGFloat?) -> ImageSource {
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "") let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "")
let blurHash = blurHash(type) let blurHash = blurHash(type)
return ImageSource(url: url, blurHash: blurHash)
return ImageSource(
url: url,
blurHash: blurHash
)
} }
} }

View File

@ -15,94 +15,82 @@ import UIKit
extension BaseItemDto: Poster { extension BaseItemDto: Poster {
var title: String {
switch type {
case .episode:
return seriesName ?? displayTitle
default:
return displayTitle
}
}
var subtitle: String? { var subtitle: String? {
switch type { switch type {
case .episode: case .episode:
return seasonEpisodeLabel seasonEpisodeLabel
case .video: case .video:
return extraType?.displayTitle extraType?.displayTitle
default: default:
return nil nil
} }
} }
var showTitle: Bool { var showTitle: Bool {
switch type { switch type {
case .episode, .series, .movie, .boxSet, .collectionFolder: case .episode, .series, .movie, .boxSet, .collectionFolder:
return Defaults[.Customization.showPosterLabels] Defaults[.Customization.showPosterLabels]
default: default:
return true true
} }
} }
var typeSystemImage: String? { var systemImage: String {
switch type { switch type {
case .boxSet: case .boxSet:
"film.stack" "film.stack"
case .channel, .tvChannel, .liveTvChannel, .program:
"tv"
case .episode, .movie, .series: case .episode, .movie, .series:
"film" "film"
case .folder: case .folder:
"folder.fill" "folder.fill"
case .person: case .person:
"person.fill" "person.fill"
case .program:
"tv"
default: nil
}
}
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
switch type {
case .episode:
return seriesImageSource(.primary, maxWidth: maxWidth)
case .folder:
return ImageSource()
default: default:
return imageSource(.primary, maxWidth: maxWidth) "circle"
} }
} }
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool = false) -> [ImageSource] { func portraitImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
switch type { switch type {
case .episode: case .episode:
if single || !Defaults[.Customization.Episodes.useSeriesLandscapeBackdrop] { [seriesImageSource(.primary, maxWidth: maxWidth)]
return [imageSource(.primary, maxWidth: maxWidth)] case .channel, .tvChannel, .liveTvChannel, .movie, .series:
} else { [imageSource(.primary, maxWidth: maxWidth)]
return [ default:
[]
}
}
func landscapeImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
switch type {
case .episode:
if Defaults[.Customization.Episodes.useSeriesLandscapeBackdrop] {
[
seriesImageSource(.thumb, maxWidth: maxWidth), seriesImageSource(.thumb, maxWidth: maxWidth),
seriesImageSource(.backdrop, maxWidth: maxWidth), seriesImageSource(.backdrop, maxWidth: maxWidth),
imageSource(.primary, maxWidth: maxWidth), imageSource(.primary, maxWidth: maxWidth),
] ]
} else {
[imageSource(.primary, maxWidth: maxWidth)]
} }
case .folder: case .folder, .program, .video:
return [imageSource(.primary, maxWidth: maxWidth)] [imageSource(.primary, maxWidth: maxWidth)]
case .program:
return [imageSource(.primary, maxWidth: maxWidth)]
case .video:
return [imageSource(.primary, maxWidth: maxWidth)]
default: default:
return [ [
imageSource(.thumb, maxWidth: maxWidth), imageSource(.thumb, maxWidth: maxWidth),
imageSource(.backdrop, maxWidth: maxWidth), imageSource(.backdrop, maxWidth: maxWidth),
] ]
} }
} }
func cinematicPosterImageSources() -> [ImageSource] { func cinematicImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
switch type { switch type {
case .episode: case .episode:
return [seriesImageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)] [seriesImageSource(.backdrop, maxWidth: maxWidth)]
default: default:
return [imageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)] [imageSource(.backdrop, maxWidth: maxWidth)]
} }
} }
} }

View File

@ -244,28 +244,6 @@ extension BaseItemDto {
} }
} }
// TODO: move as extension on `BaseItemKind`
// TODO: remove when `collectionType` becomes an enum
func includedItemTypesForCollectionType() -> [BaseItemKind]? {
guard let collectionType else { return nil }
var itemTypes: [BaseItemKind]?
switch collectionType {
case "movies":
itemTypes = [.movie]
case "tvshows":
itemTypes = [.series]
case "mixed":
itemTypes = [.movie, .series]
default:
itemTypes = nil
}
return itemTypes
}
/// Returns `originalTitle` if it is not the same as `displayTitle` /// Returns `originalTitle` if it is not the same as `displayTitle`
var alternateTitle: String? { var alternateTitle: String? {
originalTitle != displayTitle ? originalTitle : nil originalTitle != displayTitle ? originalTitle : nil

View File

@ -17,19 +17,19 @@ extension BaseItemPerson: Poster {
firstRole firstRole
} }
var showTitle: Bool { var systemImage: String {
true
}
var typeSystemImage: String? {
"person.fill" "person.fill"
} }
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource { func portraitImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
let scaleWidth = UIScreen.main.scale(maxWidth)
// TODO: figure out what to do about screen scaling with .main being deprecated
// - maxWidth assume already scaled?
let scaleWidth: Int? = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
let client = Container.userSession().client let client = Container.userSession().client
let imageRequestParameters = Paths.GetItemImageParameters( let imageRequestParameters = Paths.GetItemImageParameters(
maxWidth: scaleWidth, maxWidth: scaleWidth ?? Int(maxWidth),
tag: primaryImageTag tag: primaryImageTag
) )
@ -40,17 +40,11 @@ extension BaseItemPerson: Poster {
) )
let url = client.fullURL(with: imageRequest) let url = client.fullURL(with: imageRequest)
let blurHash: String? = imageBlurHashes?.primary?[primaryImageTag]
var blurHash: String? return [ImageSource(
url: url,
if let tag = primaryImageTag, let taggedBlurHash = imageBlurHashes?.primary?[tag] { blurHash: blurHash
blurHash = taggedBlurHash )]
}
return ImageSource(url: url, blurHash: blurHash)
}
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
[]
} }
} }

View File

@ -45,7 +45,7 @@ extension ChapterInfo {
chapterInfo.displayTitle chapterInfo.displayTitle
} }
let typeSystemImage: String? = "film" let systemImage: String = "film"
var subtitle: String? var subtitle: String?
var showTitle: Bool = true var showTitle: Bool = true
@ -59,11 +59,7 @@ extension ChapterInfo {
self.secondsRange = secondsRange self.secondsRange = secondsRange
} }
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource { func landscapeImageSources(maxWidth: CGFloat?) -> [ImageSource] {
.init()
}
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
[imageSource] [imageSource]
} }
} }

View File

@ -16,8 +16,8 @@ extension JellyfinClient {
guard let path = request.url?.path else { return configuration.url } guard let path = request.url?.path else { return configuration.url }
guard let fullPath = fullURL(with: path) else { return nil } guard let fullPath = fullURL(with: path) else { return nil }
guard var components = URLComponents(string: fullPath.absoluteString) else { return nil }
var components = URLComponents(string: fullPath.absoluteString)!
components.queryItems = request.query?.map { URLQueryItem(name: $0.0, value: $0.1) } ?? [] components.queryItems = request.query?.map { URLQueryItem(name: $0.0, value: $0.1) } ?? []
return components.url ?? fullPath return components.url ?? fullPath

View File

@ -26,6 +26,6 @@ extension UserDto {
let profileImageURL = client.fullURL(with: request) let profileImageURL = client.fullURL(with: request)
return ImageSource(url: profileImageURL, blurHash: nil) return ImageSource(url: profileImageURL)
} }
} }

View File

@ -22,7 +22,7 @@ extension Backport where Content: View {
.lineLimit(limit, reservesSpace: reservesSpace) .lineLimit(limit, reservesSpace: reservesSpace)
} else { } else {
ZStack(alignment: .top) { ZStack(alignment: .top) {
Text(String(repeating: "\n", count: limit - 1)) Text(String(repeating: " \n", count: limit))
content content
.lineLimit(limit) .lineLimit(limit)

View File

@ -8,6 +8,8 @@
import SwiftUI import SwiftUI
// TODO: remove as a `ViewModifier` and instead a wrapper view
struct AttributeViewModifier: ViewModifier { struct AttributeViewModifier: ViewModifier {
enum Style { enum Style {

View File

@ -34,7 +34,7 @@ struct BackgroundParallaxHeaderModifier<Header: View>: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.size($contentSize) .trackingSize($contentSize)
.background(alignment: .top) { .background(alignment: .top) {
header() header()
.offset(y: scrollViewOffset > 0 ? -scrollViewOffset * multiplier : 0) .offset(y: scrollViewOffset > 0 ? -scrollViewOffset * multiplier : 0)

View File

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct ScenePhaseChangeModifier: ViewModifier { struct OnScenePhaseChangedModifier: ViewModifier {
@Environment(\.scenePhase) @Environment(\.scenePhase)
private var scenePhase private var scenePhase

View File

@ -0,0 +1,23 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct OnSizeChangedModifier<Wrapped: View>: ViewModifier {
@State
private var size: CGSize = .zero
@ViewBuilder
var wrapped: (CGSize) -> Wrapped
func body(content: Content) -> some View {
wrapped(size)
.trackingSize($size)
}
}

View File

@ -1,27 +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) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct RatioCornerRadiusModifier: ViewModifier {
@State
private var cornerRadius: CGFloat = 0
let corners: UIRectCorner
let ratio: CGFloat
let side: KeyPath<CGSize, CGFloat>
func body(content: Content) -> some View {
content
.cornerRadius(cornerRadius, corners: corners)
.onSizeChanged { newSize in
cornerRadius = newSize[keyPath: side] * ratio
}
}
}

View File

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct AfterLastDisappearModifier: ViewModifier { struct SinceLastDisappearModifier: ViewModifier {
@State @State
private var lastDisappear: Date? = nil private var lastDisappear: Date? = nil

View File

@ -9,22 +9,24 @@
import SwiftUI import SwiftUI
struct FramePreferenceKey: PreferenceKey { struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {} static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
} }
struct GeometryPrefenceKey: PreferenceKey { struct GeometryPrefenceKey: PreferenceKey {
static var defaultValue: Value = Value(size: .zero, safeAreaInsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0))
static func reduce(value: inout Value, nextValue: () -> Value) {}
struct Value: Equatable { struct Value: Equatable {
let size: CGSize let size: CGSize
let safeAreaInsets: EdgeInsets let safeAreaInsets: EdgeInsets
} }
static var defaultValue: Value = Value(size: .zero, safeAreaInsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0))
static func reduce(value: inout Value, nextValue: () -> Value) {}
} }
struct LocationPreferenceKey: PreferenceKey { struct LocationPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {} static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
} }

View File

@ -89,23 +89,38 @@ extension View {
} }
} }
/// Applies the aspect ratio and corner radius for the given `PosterType` /// Applies the aspect ratio, corner radius, and border for the given `PosterType`
@ViewBuilder @ViewBuilder
func posterStyle(_ type: PosterType, contentMode: ContentMode = .fill) -> some View { func posterStyle(_ type: PosterDisplayType, contentMode: ContentMode = .fill) -> some View {
switch type { switch type {
case .portrait:
aspectRatio(2 / 3, contentMode: contentMode)
#if !os(tvOS)
.cornerRadius(ratio: 0.0375, of: \.width)
#endif
case .landscape: case .landscape:
aspectRatio(1.77, contentMode: contentMode) aspectRatio(1.77, contentMode: contentMode)
#if !os(tvOS) #if !os(tvOS)
.posterBorder(ratio: 1 / 30, of: \.width)
.cornerRadius(ratio: 1 / 30, of: \.width) .cornerRadius(ratio: 1 / 30, of: \.width)
#endif #endif
case .portrait:
aspectRatio(2 / 3, contentMode: contentMode)
#if !os(tvOS)
.posterBorder(ratio: 0.0375, of: \.width)
.cornerRadius(ratio: 0.0375, of: \.width)
#endif
} }
} }
func posterBorder(ratio: CGFloat, of side: KeyPath<CGSize, CGFloat>) -> some View {
modifier(OnSizeChangedModifier { size in
overlay {
RoundedRectangle(cornerRadius: size[keyPath: side] * ratio)
.stroke(
.white.opacity(0.10),
lineWidth: 2
)
.clipped()
}
})
}
// TODO: remove // TODO: remove
@inlinable @inlinable
func padding2(_ edges: Edge.Set = .all) -> some View { func padding2(_ edges: Edge.Set = .all) -> some View {
@ -139,7 +154,9 @@ extension View {
/// Apply a corner radius as a ratio of a view's side /// Apply a corner radius as a ratio of a view's side
func cornerRadius(ratio: CGFloat, of side: KeyPath<CGSize, CGFloat>, corners: UIRectCorner = .allCorners) -> some View { func cornerRadius(ratio: CGFloat, of side: KeyPath<CGSize, CGFloat>, corners: UIRectCorner = .allCorners) -> some View {
modifier(RatioCornerRadiusModifier(corners: corners, ratio: ratio, side: side)) modifier(OnSizeChangedModifier { size in
cornerRadius(size[keyPath: side] * ratio, corners: corners)
})
} }
func onFrameChanged(_ onChange: @escaping (CGRect) -> Void) -> some View { func onFrameChanged(_ onChange: @escaping (CGRect) -> Void) -> some View {
@ -152,8 +169,7 @@ extension View {
.onPreferenceChange(FramePreferenceKey.self, perform: onChange) .onPreferenceChange(FramePreferenceKey.self, perform: onChange)
} }
// TODO: probably rename since this doesn't set the frame but tracks it func trackingFrame(_ binding: Binding<CGRect>) -> some View {
func frame(_ binding: Binding<CGRect>) -> some View {
onFrameChanged { newFrame in onFrameChanged { newFrame in
binding.wrappedValue = newFrame binding.wrappedValue = newFrame
} }
@ -174,8 +190,7 @@ extension View {
.onPreferenceChange(LocationPreferenceKey.self, perform: onChange) .onPreferenceChange(LocationPreferenceKey.self, perform: onChange)
} }
// TODO: probably rename since this doesn't set the location but tracks it func trackingLocation(_ binding: Binding<CGPoint>) -> some View {
func location(_ binding: Binding<CGPoint>) -> some View {
onLocationChanged { newLocation in onLocationChanged { newLocation in
binding.wrappedValue = newLocation binding.wrappedValue = newLocation
} }
@ -204,8 +219,7 @@ extension View {
} }
} }
// TODO: probably rename since this doesn't set the size but tracks it func trackingSize(_ binding: Binding<CGSize>) -> some View {
func size(_ binding: Binding<CGSize>) -> some View {
onSizeChanged { newSize in onSizeChanged { newSize in
binding.wrappedValue = newSize binding.wrappedValue = newSize
} }
@ -272,11 +286,11 @@ extension View {
} }
func onScenePhase(_ phase: ScenePhase, _ action: @escaping () -> Void) -> some View { func onScenePhase(_ phase: ScenePhase, _ action: @escaping () -> Void) -> some View {
modifier(ScenePhaseChangeModifier(phase: phase, action: action)) modifier(OnScenePhaseChangedModifier(phase: phase, action: action))
} }
func edgePadding(_ edges: Edge.Set = .all) -> some View { func edgePadding(_ edges: Edge.Set = .all) -> some View {
padding(edges, EdgeInsets.defaultEdgePadding) padding(edges, EdgeInsets.edgePadding)
} }
var backport: Backport<Self> { var backport: Backport<Self> {
@ -293,10 +307,10 @@ extension View {
modifier(OnFirstAppearModifier(action: action)) modifier(OnFirstAppearModifier(action: action))
} }
/// Perform an action as a view appears given the time interval /// Perform an action as the view appears given the time interval
/// from when this view last disappeared. /// since the view last disappeared.
func afterLastDisappear(perform action: @escaping (TimeInterval) -> Void) -> some View { func sinceLastDisappear(perform action: @escaping (TimeInterval) -> Void) -> some View {
modifier(AfterLastDisappearModifier(action: action)) modifier(SinceLastDisappearModifier(action: action))
} }
func topBarTrailing(@ViewBuilder content: @escaping () -> some View) -> some View { func topBarTrailing(@ViewBuilder content: @escaping () -> some View) -> some View {

View File

@ -37,6 +37,9 @@ struct CaseIterablePicker<Element: CaseIterable & Displayable & Hashable>: View
@Binding @Binding
private var selection: Element? private var selection: Element?
@ViewBuilder
private var label: (Element) -> any View
private let title: String private let title: String
private let hasNone: Bool private let hasNone: Bool
private var noneStyle: NoneStyle private var noneStyle: NoneStyle
@ -50,18 +53,22 @@ struct CaseIterablePicker<Element: CaseIterable & Displayable & Hashable>: View
} }
ForEach(Element.allCases.asArray, id: \.hashValue) { ForEach(Element.allCases.asArray, id: \.hashValue) {
Text($0.displayTitle) label($0)
.eraseToAnyView()
.tag($0 as Element?) .tag($0 as Element?)
} }
} }
} }
} }
// MARK: Text
extension CaseIterablePicker { extension CaseIterablePicker {
init(title: String, selection: Binding<Element?>) { init(title: String, selection: Binding<Element?>) {
self.init( self.init(
selection: selection, selection: selection,
label: { Text($0.displayTitle) },
title: title, title: title,
hasNone: true, hasNone: true,
noneStyle: .text noneStyle: .text
@ -69,8 +76,6 @@ extension CaseIterablePicker {
} }
init(title: String, selection: Binding<Element>) { init(title: String, selection: Binding<Element>) {
self.title = title
let binding = Binding<Element?> { let binding = Binding<Element?> {
selection.wrappedValue selection.wrappedValue
} set: { newValue, _ in } set: { newValue, _ in
@ -78,13 +83,48 @@ extension CaseIterablePicker {
selection.wrappedValue = newValue! selection.wrappedValue = newValue!
} }
self._selection = binding self.init(
selection: binding,
self.hasNone = false label: { Text($0.displayTitle) },
self.noneStyle = .text title: title,
hasNone: false,
noneStyle: .text
)
} }
func noneStyle(_ newStyle: NoneStyle) -> Self { func noneStyle(_ newStyle: NoneStyle) -> Self {
copy(modifying: \.noneStyle, with: newStyle) copy(modifying: \.noneStyle, with: newStyle)
} }
} }
// MARK: Label
extension CaseIterablePicker where Element: SystemImageable {
init(title: String, selection: Binding<Element?>) {
self.init(
selection: selection,
label: { Label($0.displayTitle, systemImage: $0.systemImage) },
title: title,
hasNone: true,
noneStyle: .text
)
}
init(title: String, selection: Binding<Element>) {
let binding = Binding<Element?> {
selection.wrappedValue
} set: { newValue, _ in
precondition(newValue != nil, "Should not have nil new value with non-optional binding")
selection.wrappedValue = newValue!
}
self.init(
selection: binding,
label: { Label($0.displayTitle, systemImage: $0.systemImage) },
title: title,
hasNone: false,
noneStyle: .text
)
}
}

View File

@ -46,15 +46,7 @@ extension ChannelProgram: Poster {
channel.displayTitle channel.displayTitle
} }
var subtitle: String? { var systemImage: String {
nil channel.systemImage
}
var typeSystemImage: String? {
"tv"
}
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
channel.imageSource(.primary, maxWidth: maxWidth)
} }
} }

View File

@ -8,6 +8,8 @@
import Foundation import Foundation
/// A type that is displayed with a title
protocol Displayable { protocol Displayable {
var displayTitle: String { get } var displayTitle: String { get }
} }

View File

@ -8,6 +8,10 @@
import Foundation import Foundation
/// Represents an image source along with a blur hash and a system image
/// to act as placeholders.
///
/// If `blurHash` is `nil`, the given system image is used instead.
struct ImageSource: Hashable { struct ImageSource: Hashable {
let url: URL? let url: URL?

View File

@ -10,7 +10,7 @@ import Defaults
import Foundation import Foundation
import UIKit import UIKit
enum LibraryViewType: String, CaseIterable, Displayable, Defaults.Serializable { enum LibraryDisplayType: String, CaseIterable, Displayable, Defaults.Serializable, SystemImageable {
case grid case grid
case list case list
@ -24,4 +24,13 @@ enum LibraryViewType: String, CaseIterable, Displayable, Defaults.Serializable {
"List" "List"
} }
} }
var systemImage: String {
switch self {
case .grid:
"square.grid.2x2.fill"
case .list:
"square.fill.text.grid.1x2"
}
}
} }

View File

@ -8,35 +8,53 @@
import Foundation import Foundation
// TODO: remove `showTitle` and `subtitle` since the PosterButton can define custom supplementary views? /// A type that is displayed as a poster
// TODO: instead of the below image functions, have functions that match `ImageType` protocol Poster: Displayable, Hashable, Identifiable, SystemImageable {
// - allows caller to choose images
protocol Poster: Displayable, Hashable, Identifiable {
/// Optional subtitle when used as a poster
var subtitle: String? { get } var subtitle: String? { get }
var showTitle: Bool { get }
var typeSystemImage: String? { get }
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource /// Show the title
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] var showTitle: Bool { get }
func cinematicPosterImageSources() -> [ImageSource]
func portraitImageSources(
maxWidth: CGFloat?
) -> [ImageSource]
func landscapeImageSources(
maxWidth: CGFloat?
) -> [ImageSource]
func cinematicImageSources(
maxWidth: CGFloat?
) -> [ImageSource]
} }
extension Poster { extension Poster {
var subtitle: String? {
nil
}
var showTitle: Bool { var showTitle: Bool {
true true
} }
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource { func portraitImageSources(
.init() maxWidth: CGFloat? = nil
} ) -> [ImageSource] {
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
[] []
} }
func cinematicPosterImageSources() -> [ImageSource] { func landscapeImageSources(
maxWidth: CGFloat? = nil
) -> [ImageSource] {
[]
}
func cinematicImageSources(
maxWidth: CGFloat?
) -> [ImageSource] {
[] []
} }
} }

View File

@ -0,0 +1,26 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
enum PosterDisplayType: String, CaseIterable, Displayable, Defaults.Serializable {
case landscape
case portrait
// TODO: localize
var displayTitle: String {
switch self {
case .landscape:
"Landscape"
case .portrait:
"Portrait"
}
}
}

View File

@ -1,34 +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) 2024 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
// TODO: Refactor to `ItemDisplayType`
// - this is to move away from video specific to generalizing all media types. However,
// media is still able to use grammar for their own contexts.
// - move landscape/portrait to wide/narrow
// - add `square`/something similar
// TODO: after no longer experimental, nest under `Poster`?
// tracker: https://github.com/apple/swift-evolution/blob/main/proposals/0404-nested-protocols.md
enum PosterType: String, CaseIterable, Displayable, Defaults.Serializable {
case landscape
case portrait
// TODO: localize
var displayTitle: String {
switch self {
case .landscape:
"Landscape"
case .portrait:
"Portrait"
}
}
}

View File

@ -0,0 +1,14 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import Foundation
protocol SystemImageable {
var systemImage: String { get }
}

View File

@ -34,15 +34,15 @@ extension Defaults.Keys {
static let itemViewType = Key<ItemViewType>("itemViewType", default: .compactLogo, suite: .generalSuite) static let itemViewType = Key<ItemViewType>("itemViewType", default: .compactLogo, suite: .generalSuite)
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: .generalSuite) static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: .generalSuite)
static let nextUpPosterType = Key<PosterType>("nextUpPosterType", default: .portrait, suite: .generalSuite) static let nextUpPosterType = Key<PosterDisplayType>("nextUpPosterType", default: .portrait, suite: .generalSuite)
static let recentlyAddedPosterType = Key<PosterType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite) static let recentlyAddedPosterType = Key<PosterDisplayType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite)
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite) static let latestInLibraryPosterType = Key<PosterDisplayType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
static let shouldShowMissingSeasons = Key<Bool>("shouldShowMissingSeasons", default: true, suite: .generalSuite) static let shouldShowMissingSeasons = Key<Bool>("shouldShowMissingSeasons", default: true, suite: .generalSuite)
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: .generalSuite) static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: .generalSuite)
static let similarPosterType = Key<PosterType>("similarPosterType", default: .portrait, suite: .generalSuite) static let similarPosterType = Key<PosterDisplayType>("similarPosterType", default: .portrait, suite: .generalSuite)
// TODO: have search poster type by types of items if applicable // TODO: have search poster type by types of items if applicable
static let searchPosterType = Key<PosterType>("searchPosterType", default: .portrait, suite: .generalSuite) static let searchPosterType = Key<PosterDisplayType>("searchPosterType", default: .portrait, suite: .generalSuite)
enum CinematicItemViewType { enum CinematicItemViewType {
@ -74,12 +74,12 @@ extension Defaults.Keys {
default: ItemFilterType.allCases, default: ItemFilterType.allCases,
suite: .generalSuite suite: .generalSuite
) )
static let viewType = Key<LibraryViewType>( static let viewType = Key<LibraryDisplayType>(
"libraryViewType", "libraryViewType",
default: .grid, default: .grid,
suite: .generalSuite suite: .generalSuite
) )
static let posterType = Key<PosterType>( static let posterType = Key<PosterDisplayType>(
"libraryPosterType", "libraryPosterType",
default: .portrait, default: .portrait,
suite: .generalSuite suite: .generalSuite

View File

@ -16,10 +16,6 @@ import UIKit
/// Magic number for page sizes /// Magic number for page sizes
private let DefaultPageSize = 50 private let DefaultPageSize = 50
// TODO: frankly this is just generic because we also view `BaseItemPerson` elements
// and I don't want additional views for it. Is there a way we can transform a
// `BaseItemPerson` into a `BaseItemDto` and just use the concrete type?
// TODO: fix how `hasNextPage` is determined // TODO: fix how `hasNextPage` is determined
// - some subclasses might not have "paging" and only have one call. This can be solved with // - some subclasses might not have "paging" and only have one call. This can be solved with
// a check if elements were actually appended to the set but that requires a redundant get // a check if elements were actually appended to the set but that requires a redundant get
@ -301,6 +297,8 @@ class PagingLibraryViewModel<Element: Poster>: ViewModel, Eventful, Stateful {
[] []
} }
/// Gets a random item from `elements`. Override if item should
/// come from another source instead.
func getRandomItem() async throws -> Element? { func getRandomItem() async throws -> Element? {
elements.randomElement() elements.randomElement()
} }

View File

@ -10,10 +10,6 @@ import Combine
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
// TODO: is current program-channel requesting best way to do it?
// Note: section item limit is low so that total channel amount is not too much
final class ProgramsViewModel: ViewModel, Stateful { final class ProgramsViewModel: ViewModel, Stateful {
enum ProgramSection: CaseIterable { enum ProgramSection: CaseIterable {
@ -42,34 +38,34 @@ final class ProgramsViewModel: ViewModel, Stateful {
} }
@Published @Published
private(set) var kids: [ChannelProgram] = [] private(set) var kids: [BaseItemDto] = []
@Published @Published
private(set) var movies: [ChannelProgram] = [] private(set) var movies: [BaseItemDto] = []
@Published @Published
private(set) var news: [ChannelProgram] = [] private(set) var news: [BaseItemDto] = []
@Published @Published
private(set) var recommended: [ChannelProgram] = [] private(set) var recommended: [BaseItemDto] = []
@Published @Published
private(set) var series: [ChannelProgram] = [] private(set) var series: [BaseItemDto] = []
@Published @Published
private(set) var sports: [ChannelProgram] = [] private(set) var sports: [BaseItemDto] = []
@Published @Published
final var lastAction: Action? = nil final var lastAction: Action? = nil
@Published @Published
final var state: State = .initial final var state: State = .initial
private var programChannels: [BaseItemDto] = []
private var currentRefreshTask: AnyCancellable? private var currentRefreshTask: AnyCancellable?
var hasNoResults: Bool { var hasNoResults: Bool {
kids.isEmpty && [
movies.isEmpty && kids,
news.isEmpty && movies,
recommended.isEmpty && news,
series.isEmpty && recommended,
sports.isEmpty series,
sports,
].allSatisfy(\.isEmpty)
} }
func respond(to action: Action) -> State { func respond(to action: Action) -> State {
@ -111,10 +107,10 @@ final class ProgramsViewModel: ViewModel, Stateful {
} }
} }
private func getItemSections() async throws -> [ProgramSection: [ChannelProgram]] { private func getItemSections() async throws -> [ProgramSection: [BaseItemDto]] {
try await withThrowingTaskGroup( try await withThrowingTaskGroup(
of: (ProgramSection, [BaseItemDto]).self, of: (ProgramSection, [BaseItemDto]).self,
returning: [ProgramSection: [ChannelProgram]].self returning: [ProgramSection: [BaseItemDto]].self
) { group in ) { group in
// sections // sections
@ -137,18 +133,7 @@ final class ProgramsViewModel: ViewModel, Stateful {
programs[items.0] = items.1 programs[items.0] = items.1
} }
// get channels for all programs at once to return programs
// avoid going back and forth too much
let channels = try await Set(self.getChannels(for: programs.values.flatMap { $0 }))
let result: [ProgramSection: [ChannelProgram]] = programs.mapValues { programs in
programs.compactMap { program in
guard let channel = channels.first(where: { channel in channel.id == program.channelID }) else { return nil }
return ChannelProgram(channel: channel, programs: [program])
}
}
return result
} }
} }
@ -158,7 +143,7 @@ final class ProgramsViewModel: ViewModel, Stateful {
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
.appending(.channelInfo) .appending(.channelInfo)
parameters.isAiring = true parameters.isAiring = true
parameters.limit = 10 parameters.limit = 20
parameters.userID = userSession.user.id parameters.userID = userSession.user.id
let request = Paths.getRecommendedPrograms(parameters: parameters) let request = Paths.getRecommendedPrograms(parameters: parameters)
@ -173,7 +158,7 @@ final class ProgramsViewModel: ViewModel, Stateful {
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
.appending(.channelInfo) .appending(.channelInfo)
parameters.hasAired = false parameters.hasAired = false
parameters.limit = 10 parameters.limit = 20
parameters.userID = userSession.user.id parameters.userID = userSession.user.id
parameters.isKids = section == .kids parameters.isKids = section == .kids
@ -187,19 +172,4 @@ final class ProgramsViewModel: ViewModel, Stateful {
return response.value.items ?? [] return response.value.items ?? []
} }
private func getChannels(for programs: [BaseItemDto]) async throws -> [BaseItemDto] {
var parameters = Paths.GetItemsByUserIDParameters()
parameters.fields = .MinimumFields
parameters.ids = programs.compactMap(\.channelID)
let request = Paths.getItemsByUserID(
userID: userSession.user.id,
parameters: parameters
)
let response = try await userSession.client.send(request)
return response.value.items ?? []
}
} }

View File

@ -27,7 +27,7 @@ final class QuickConnectViewModel: ViewModel, Stateful {
// MARK: State // MARK: State
// The typical quick connect lifecycle is as follows: // The typical quick connect lifecycle is as follows:
enum State: Equatable { enum State: Hashable {
// 0. User has not interacted with quick connect // 0. User has not interacted with quick connect
case initial case initial
// 1. User clicks quick connect // 1. User clicks quick connect
@ -53,6 +53,7 @@ final class QuickConnectViewModel: ViewModel, Stateful {
@Published @Published
var state: State = .initial var state: State = .initial
var lastAction: Action? = nil
let client: JellyfinClient let client: JellyfinClient

View File

@ -14,9 +14,10 @@ import JellyfinAPI
import Pulse import Pulse
final class UserSignInViewModel: ViewModel, Stateful { final class UserSignInViewModel: ViewModel, Stateful {
// MARK: Action // MARK: Action
enum Action { enum Action: Equatable {
case signInWithUserPass(username: String, password: String) case signInWithUserPass(username: String, password: String)
case signInWithQuickConnect(authSecret: String) case signInWithQuickConnect(authSecret: String)
case cancelSignIn case cancelSignIn
@ -24,7 +25,7 @@ final class UserSignInViewModel: ViewModel, Stateful {
// MARK: State // MARK: State
enum State: Equatable { enum State: Hashable {
case initial case initial
case signingIn case signingIn
case signedIn case signedIn
@ -38,6 +39,8 @@ final class UserSignInViewModel: ViewModel, Stateful {
@Published @Published
var state: State = .initial var state: State = .initial
var lastAction: Action? = nil
@Published @Published
private(set) var publicUsers: [UserDto] = [] private(set) var publicUsers: [UserDto] = []
@Published @Published

View File

@ -10,8 +10,6 @@ import Combine
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: better name
struct CinematicBackgroundView<Item: Poster>: View { struct CinematicBackgroundView<Item: Poster>: View {
@ObservedObject @ObservedObject
@ -26,8 +24,8 @@ struct CinematicBackgroundView<Item: Poster>: View {
RotateContentView(proxy: proxy) RotateContentView(proxy: proxy)
.onChange(of: viewModel.currentItem) { newItem in .onChange(of: viewModel.currentItem) { newItem in
proxy.update { proxy.update {
ImageView(newItem?.cinematicPosterImageSources() ?? []) ImageView(newItem?.cinematicImageSources(maxWidth: nil) ?? [])
.placeholder { .placeholder { _ in
Color.clear Color.clear
} }
.failure { .failure {
@ -49,6 +47,7 @@ struct CinematicBackgroundView<Item: Poster>: View {
init() { init() {
currentItemSubject currentItemSubject
.debounce(for: 0.5, scheduler: DispatchQueue.main) .debounce(for: 0.5, scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { newItem in .sink { newItem in
self.currentItem = newItem self.currentItem = newItem
} }
@ -56,7 +55,6 @@ struct CinematicBackgroundView<Item: Poster>: View {
} }
func select(item: Item) { func select(item: Item) {
guard currentItem != item else { return }
currentItemSubject.send(item) currentItemSubject.send(item)
} }
} }

View File

@ -10,6 +10,7 @@ import Combine
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: make new protocol for cinematic view image provider
// TODO: better name // TODO: better name
struct CinematicItemSelector<Item: Poster>: View { struct CinematicItemSelector<Item: Poster>: View {
@ -56,7 +57,10 @@ struct CinematicItemSelector<Item: Poster>: View {
} }
.background(alignment: .top) { .background(alignment: .top) {
ZStack { ZStack {
CinematicBackgroundView(viewModel: viewModel, initialItem: items.first) CinematicBackgroundView(
viewModel: viewModel,
initialItem: items.first
)
LinearGradient( LinearGradient(
stops: [ stops: [

View File

@ -10,7 +10,7 @@ import SwiftUI
struct NonePosterButton: View { struct NonePosterButton: View {
let type: PosterType let type: PosterDisplayType
var body: some View { var body: some View {
Button { Button {

View File

@ -88,8 +88,8 @@ struct PagingLibraryView<Element: Poster>: View {
// MARK: layout // MARK: layout
private static func makeLayout( private static func makeLayout(
posterType: PosterType, posterType: PosterDisplayType,
viewType: LibraryViewType viewType: LibraryDisplayType
) -> CollectionVGridLayout { ) -> CollectionVGridLayout {
switch (posterType, viewType) { switch (posterType, viewType) {
case (.landscape, .grid): case (.landscape, .grid):

View File

@ -18,13 +18,12 @@ struct PosterButton<Item: Poster>: View {
private var isFocused: Bool private var isFocused: Bool
private var item: Item private var item: Item
private var type: PosterType private var type: PosterDisplayType
private var horizontalAlignment: HorizontalAlignment private var horizontalAlignment: HorizontalAlignment
private var content: () -> any View private var content: () -> any View
private var imageOverlay: () -> any View private var imageOverlay: () -> any View
private var contextMenu: () -> any View private var contextMenu: () -> any View
private var onSelect: () -> Void private var onSelect: () -> Void
private var singleImage: Bool
// Setting the .focused() modifier causes significant performance issues. // Setting the .focused() modifier causes significant performance issues.
// Only set if desiring focus changes // Only set if desiring focus changes
@ -33,9 +32,9 @@ struct PosterButton<Item: Poster>: View {
private func imageView(from item: Item) -> ImageView { private func imageView(from item: Item) -> ImageView {
switch type { switch type {
case .portrait: case .portrait:
ImageView(item.portraitPosterImageSource(maxWidth: 500)) ImageView(item.portraitImageSources(maxWidth: 500))
case .landscape: case .landscape:
ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage)) ImageView(item.landscapeImageSources(maxWidth: 500))
} }
} }
@ -49,7 +48,14 @@ struct PosterButton<Item: Poster>: View {
imageView(from: item) imageView(from: item)
.failure { .failure {
SystemImageContentView(systemName: item.typeSystemImage) if item.showTitle {
SystemImageContentView(systemName: item.systemImage)
} else {
SystemImageContentView(
title: item.displayTitle,
systemName: item.systemImage
)
}
} }
imageOverlay() imageOverlay()
@ -80,7 +86,7 @@ struct PosterButton<Item: Poster>: View {
extension PosterButton { extension PosterButton {
init(item: Item, type: PosterType, singleImage: Bool = false) { init(item: Item, type: PosterDisplayType) {
self.init( self.init(
item: item, item: item,
type: type, type: type,
@ -89,7 +95,6 @@ extension PosterButton {
imageOverlay: { DefaultOverlay(item: item) }, imageOverlay: { DefaultOverlay(item: item) },
contextMenu: { EmptyView() }, contextMenu: { EmptyView() },
onSelect: {}, onSelect: {},
singleImage: singleImage,
onFocusChanged: nil onFocusChanged: nil
) )
} }
@ -122,7 +127,8 @@ extension PosterButton {
} }
} }
// TODO: Shared default content? // TODO: Shared default content with iOS?
// - check if content is generally same
extension PosterButton { extension PosterButton {
@ -169,6 +175,58 @@ extension PosterButton {
} }
} }
// TODO: clean up
// Content specific for BaseItemDto episode items
struct EpisodeContentSubtitleContent: View {
let item: Item
var body: some View {
if let item = item as? BaseItemDto {
// Unsure why this needs 0 spacing
// compared to other default content
VStack(alignment: .leading, spacing: 0) {
if item.showTitle, let seriesName = item.seriesName {
Text(seriesName)
.font(.footnote.weight(.regular))
.foregroundColor(.primary)
.backport
.lineLimit(1, reservesSpace: true)
}
Subtitle(item: item)
}
}
}
struct Subtitle: View {
let item: BaseItemDto
var body: some View {
SeparatorHStack {
Text(item.seasonEpisodeLabel ?? .emptyDash)
if item.showTitle {
Text(item.displayTitle)
} else if let seriesName = item.seriesName {
Text(seriesName)
}
}
.separator {
Circle()
.frame(width: 2, height: 2)
.padding(.horizontal, 3)
}
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
// TODO: Find better way for these indicators, see EpisodeCard // TODO: Find better way for these indicators, see EpisodeCard
struct DefaultOverlay: View { struct DefaultOverlay: View {

View File

@ -15,7 +15,7 @@ import SwiftUI
struct PosterHStack<Item: Poster>: View { struct PosterHStack<Item: Poster>: View {
private var title: String? private var title: String?
private var type: PosterType private var type: PosterDisplayType
private var items: Binding<OrderedSet<Item>> private var items: Binding<OrderedSet<Item>>
private var content: (Item) -> any View private var content: (Item) -> any View
private var imageOverlay: (Item) -> any View private var imageOverlay: (Item) -> any View
@ -58,8 +58,8 @@ struct PosterHStack<Item: Poster>: View {
} }
.clipsToBounds(false) .clipsToBounds(false)
.dataPrefix(20) .dataPrefix(20)
.insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: 20) .insets(horizontal: EdgeInsets.edgePadding, vertical: 20)
.itemSpacing(EdgeInsets.defaultEdgePadding - 20) .itemSpacing(EdgeInsets.edgePadding - 20)
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
} }
.focusSection() .focusSection()
@ -85,7 +85,7 @@ extension PosterHStack {
init( init(
title: String? = nil, title: String? = nil,
type: PosterType, type: PosterDisplayType,
items: Binding<OrderedSet<Item>> items: Binding<OrderedSet<Item>>
) { ) {
self.init( self.init(
@ -103,7 +103,7 @@ extension PosterHStack {
init<S: Sequence<Item>>( init<S: Sequence<Item>>(
title: String? = nil, title: String? = nil,
type: PosterType, type: PosterDisplayType,
items: S items: S
) { ) {
self.init( self.init(

View File

@ -10,7 +10,7 @@ import SwiftUI
struct SeeAllPosterButton: View { struct SeeAllPosterButton: View {
private let type: PosterType private let type: PosterDisplayType
private var onSelect: () -> Void private var onSelect: () -> Void
var body: some View { var body: some View {
@ -37,7 +37,7 @@ struct SeeAllPosterButton: View {
extension SeeAllPosterButton { extension SeeAllPosterButton {
init(type: PosterType) { init(type: PosterDisplayType) {
self.init( self.init(
type: type, type: type,
onSelect: {} onSelect: {}

View File

@ -59,7 +59,7 @@ struct ChannelLibraryView: View {
viewModel.send(.refresh) viewModel.send(.refresh)
} }
} }
.afterLastDisappear { interval in .sinceLastDisappear { interval in
// refresh after 3 hours // refresh after 3 hours
if interval >= 10800 { if interval >= 10800 {
viewModel.send(.refresh) viewModel.send(.refresh)

View File

@ -30,16 +30,16 @@ extension ChannelLibraryView {
ZStack { ZStack {
Color.clear Color.clear
ImageView(channel.portraitPosterImageSource(maxWidth: 110)) ImageView(channel.portraitImageSources(maxWidth: 110))
.image { .image {
$0.aspectRatio(contentMode: .fit) $0.aspectRatio(contentMode: .fit)
} }
.failure { .failure {
SystemImageContentView(systemName: channel.typeSystemImage) SystemImageContentView(systemName: channel.systemImage)
.background(color: .clear) .background(color: .clear)
.imageFrameRatio(width: 1.5, height: 1.5) .imageFrameRatio(width: 1.5, height: 1.5)
} }
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
} }
@ -54,7 +54,7 @@ extension ChannelLibraryView {
@ViewBuilder @ViewBuilder
private func programLabel(for program: BaseItemDto) -> some View { private func programLabel(for program: BaseItemDto) -> some View {
HStack(alignment: .top, spacing: EdgeInsets.defaultEdgePadding / 2) { HStack(alignment: .top, spacing: EdgeInsets.edgePadding / 2) {
AlternateLayoutView(alignment: .leading) { AlternateLayoutView(alignment: .leading) {
Text("00:00 AM") Text("00:00 AM")
.monospacedDigit() .monospacedDigit()
@ -104,7 +104,7 @@ extension ChannelLibraryView {
Button { Button {
onSelect() onSelect()
} label: { } label: {
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding / 2) { HStack(alignment: .center, spacing: EdgeInsets.edgePadding / 2) {
channelLogo channelLogo
.frame(width: 110) .frame(width: 110)
@ -127,7 +127,7 @@ extension ChannelLibraryView {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, EdgeInsets.defaultEdgePadding / 2) .padding(.horizontal, EdgeInsets.edgePadding / 2)
} }
.buttonStyle(.card) .buttonStyle(.card)
.frame(height: 200) .frame(height: 200)

View File

@ -39,7 +39,7 @@ extension HomeView {
CinematicItemSelector(items: viewModel.elements.elements) CinematicItemSelector(items: viewModel.elements.elements)
.topContent { item in .topContent { item in
ImageView(itemSelectorImageSource(for: item)) ImageView(itemSelectorImageSource(for: item))
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.failure { .failure {

View File

@ -39,7 +39,7 @@ extension HomeView {
CinematicItemSelector(items: viewModel.resumeItems.elements) CinematicItemSelector(items: viewModel.resumeItems.elements)
.topContent { item in .topContent { item in
ImageView(itemSelectorImageSource(for: item)) ImageView(itemSelectorImageSource(for: item))
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.failure { .failure {
@ -52,12 +52,11 @@ extension HomeView {
.frame(height: 200, alignment: .bottomLeading) .frame(height: 200, alignment: .bottomLeading)
} }
.content { item in .content { item in
if let subtitle = item.subtitle { // TODO: clean up
Text(subtitle) if item.type == .episode {
.font(.caption) PosterButton<BaseItemDto>.EpisodeContentSubtitleContent.Subtitle(item: item)
.fontWeight(.medium) } else {
.foregroundColor(.secondary) Text(" ")
.lineLimit(2)
} }
} }
.itemImageOverlay { item in .itemImageOverlay { item in

View File

@ -61,7 +61,7 @@ struct HomeView: View {
viewModel.send(.refresh) viewModel.send(.refresh)
} }
.ignoresSafeArea() .ignoresSafeArea()
.afterLastDisappear { interval in .sinceLastDisappear { interval in
if interval > 60 || viewModel.notificationsReceived.contains(.itemMetadataDidChange) { if interval > 60 || viewModel.notificationsReceived.contains(.itemMetadataDidChange) {
viewModel.send(.backgroundRefresh) viewModel.send(.backgroundRefresh)
viewModel.notificationsReceived.remove(.itemMetadataDidChange) viewModel.notificationsReceived.remove(.itemMetadataDidChange)

View File

@ -23,8 +23,7 @@ extension SeriesEpisodeSelector {
var body: some View { var body: some View {
PosterButton( PosterButton(
item: episode, item: episode,
type: .landscape, type: .landscape
singleImage: true
) )
.content { .content {
let content: String = if episode.isUnaired { let content: String = if episode.isUnaired {

View File

@ -43,8 +43,8 @@ extension SeriesEpisodeSelector {
.focused($focusedEpisodeID, equals: episode.id) .focused($focusedEpisodeID, equals: episode.id)
} }
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
.proxy(proxy) .proxy(proxy)
.onFirstAppear { .onFirstAppear {
guard !didScrollToPlayButtonItem else { return } guard !didScrollToPlayButtonItem else { return }
@ -106,8 +106,8 @@ extension SeriesEpisodeSelector {
} }
} }
.allowScrolling(false) .allowScrolling(false)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
} }
} }
@ -121,8 +121,8 @@ extension SeriesEpisodeSelector {
SeriesEpisodeSelector.LoadingCard() SeriesEpisodeSelector.LoadingCard()
} }
.allowScrolling(false) .allowScrolling(false)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
} }
} }
} }

View File

@ -67,7 +67,7 @@ extension ItemView {
maxWidth: UIScreen.main.bounds.width * 0.4, maxWidth: UIScreen.main.bounds.width * 0.4,
maxHeight: 250 maxHeight: 250
)) ))
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.failure { .failure {

View File

@ -93,7 +93,8 @@ extension MediaView {
titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash)) titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash))
} }
.failure { .failure {
ImageView.DefaultFailureView() Color.secondarySystemFill
.opacity(0.75)
.overlay { .overlay {
titleLabel titleLabel
.foregroundColor(.primary) .foregroundColor(.primary)

View File

@ -54,7 +54,7 @@ struct ProgramsView: View {
@ViewBuilder @ViewBuilder
private func programsSection( private func programsSection(
title: String, title: String,
keyPath: KeyPath<ProgramsViewModel, [ChannelProgram]> keyPath: KeyPath<ProgramsViewModel, [BaseItemDto]>
) -> some View { ) -> some View {
PosterHStack( PosterHStack(
title: title, title: title,
@ -62,18 +62,18 @@ struct ProgramsView: View {
items: programsViewModel[keyPath: keyPath] items: programsViewModel[keyPath: keyPath]
) )
.content { .content {
ProgramButtonContent(program: $0.programs[0]) ProgramButtonContent(program: $0)
} }
.imageOverlay { .imageOverlay {
ProgramProgressOverlay(program: $0.programs[0]) ProgramProgressOverlay(program: $0)
}
.onSelect { channelProgram in
guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
router.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource)
)
} }
// .onSelect { channelProgram in
// guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
// router.route(
// to: \.liveVideoPlayer,
// LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource)
// )
// }
} }
var body: some View { var body: some View {

View File

@ -97,7 +97,7 @@ struct SearchView: View {
private func itemsSection( private func itemsSection(
title: String, title: String,
keyPath: KeyPath<SearchViewModel, [BaseItemDto]>, keyPath: KeyPath<SearchViewModel, [BaseItemDto]>,
posterType: PosterType posterType: PosterDisplayType
) -> some View { ) -> some View {
PosterHStack( PosterHStack(
title: title, title: title,

View File

@ -26,9 +26,9 @@ struct ServerListView: View {
viewModel.servers, viewModel.servers,
layout: .columns( layout: .columns(
1, 1,
insets: EdgeInsets.DefaultEdgeInsets, insets: EdgeInsets.edgeInsets,
itemSpacing: EdgeInsets.defaultEdgePadding, itemSpacing: EdgeInsets.edgePadding,
lineSpacing: EdgeInsets.defaultEdgePadding lineSpacing: EdgeInsets.edgePadding
) )
) { server in ) { server in
ServerButton(server: server) ServerButton(server: server)

View File

@ -32,9 +32,9 @@ struct UserListView: View {
viewModel.users, viewModel.users,
layout: .minWidth( layout: .minWidth(
250, 250,
insets: EdgeInsets.DefaultEdgeInsets, insets: EdgeInsets.edgeInsets,
itemSpacing: EdgeInsets.defaultEdgePadding, itemSpacing: EdgeInsets.edgePadding,
lineSpacing: EdgeInsets.defaultEdgePadding lineSpacing: EdgeInsets.edgePadding
) )
) { user in ) { user in
UserProfileButton(user: user) UserProfileButton(user: user)

View File

@ -178,7 +178,7 @@
E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */; }; E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */; };
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */; }; E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */; };
E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231332BCF8A3C009D71FC /* ProgramsView.swift */; }; E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231332BCF8A3C009D71FC /* ProgramsView.swift */; };
E102313F2BCF8A3C009D71FC /* WideChannelGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */; }; E102313F2BCF8A3C009D71FC /* DetailedChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231352BCF8A3C009D71FC /* DetailedChannelView.swift */; };
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */; }; E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */; };
E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231432BCF8A51009D71FC /* ChannelProgram.swift */; }; E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231432BCF8A51009D71FC /* ChannelProgram.swift */; };
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231432BCF8A51009D71FC /* ChannelProgram.swift */; }; E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231432BCF8A51009D71FC /* ChannelProgram.swift */; };
@ -208,6 +208,10 @@
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
E1092F4C29106F9F00163F57 /* GestureAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1092F4B29106F9F00163F57 /* GestureAction.swift */; }; E1092F4C29106F9F00163F57 /* GestureAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1092F4B29106F9F00163F57 /* GestureAction.swift */; };
E10B1E8A2BD76FA900A92EAF /* QuickConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B1E892BD76FA900A92EAF /* QuickConnectViewModel.swift */; };
E10B1E8B2BD76FA900A92EAF /* QuickConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B1E892BD76FA900A92EAF /* QuickConnectViewModel.swift */; };
E10B1E8E2BD7708900A92EAF /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10B1E8C2BD7708900A92EAF /* QuickConnectView.swift */; };
E10B1E8F2BD7728400A92EAF /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */; };
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842929A587110064EA49 /* LoadingView.swift */; }; E10E842A29A587110064EA49 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842929A587110064EA49 /* LoadingView.swift */; };
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842B29A589860064EA49 /* NonePosterButton.swift */; }; E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842B29A589860064EA49 /* NonePosterButton.swift */; };
E10EAA4F277BBCC4000269ED /* CGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSize.swift */; }; E10EAA4F277BBCC4000269ED /* CGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSize.swift */; };
@ -244,7 +248,6 @@
E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153DCB2BBB633B00424D36 /* FastSVGView.swift */; }; E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153DCB2BBB633B00424D36 /* FastSVGView.swift */; };
E1153DD02BBB634F00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DCF2BBB634F00424D36 /* SVGKit */; }; E1153DD02BBB634F00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DCF2BBB634F00424D36 /* SVGKit */; };
E1153DD22BBB649C00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DD12BBB649C00424D36 /* SVGKit */; }; E1153DD22BBB649C00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DD12BBB649C00424D36 /* SVGKit */; };
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 */; };
E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; }; E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; };
@ -304,8 +307,8 @@
E132D3C82BD200C10058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3C72BD200C10058A2DF /* CollectionVGrid */; }; E132D3C82BD200C10058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3C72BD200C10058A2DF /* CollectionVGrid */; };
E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CC2BD2179C0058A2DF /* CollectionVGrid */; }; E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CC2BD2179C0058A2DF /* CollectionVGrid */; };
E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CE2BD217AA0058A2DF /* CollectionVGrid */; }; E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CE2BD217AA0058A2DF /* CollectionVGrid */; };
E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; }; E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */; };
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; }; E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */; };
E133328829538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; }; E133328829538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; };
E133328929538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; }; E133328929538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; };
E133328D2953AE4B00EE76AB /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328C2953AE4B00EE76AB /* CircularProgressView.swift */; }; E133328D2953AE4B00EE76AB /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328C2953AE4B00EE76AB /* CircularProgressView.swift */; };
@ -341,7 +344,7 @@
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; };
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; }; E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; };
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; }; E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; };
E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryViewType.swift */; }; E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */; };
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryRow.swift */; }; E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryRow.swift */; };
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1401C9F2937DFF500E8B599 /* AppIconSelectorView.swift */; }; E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1401C9F2937DFF500E8B599 /* AppIconSelectorView.swift */; };
E1401CA22938122C00E8B599 /* AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1401CA12938122C00E8B599 /* AppIcons.swift */; }; E1401CA22938122C00E8B599 /* AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1401CA12938122C00E8B599 /* AppIcons.swift */; };
@ -418,12 +421,12 @@
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; }; E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; };
E1575E72293E77B5001665B1 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8429229340B8300D1041A /* Utilities.swift */; }; E1575E72293E77B5001665B1 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8429229340B8300D1041A /* Utilities.swift */; };
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */; }; E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */; };
E1575E75293E77B5001665B1 /* LibraryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryViewType.swift */; }; E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */; };
E1575E76293E77B5001665B1 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15756352936856700976E1F /* VideoPlayerType.swift */; }; E1575E76293E77B5001665B1 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15756352936856700976E1F /* VideoPlayerType.swift */; };
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C8CE7B28FF015000DF5D7B /* TrailingTimestampType.swift */; }; E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C8CE7B28FF015000DF5D7B /* TrailingTimestampType.swift */; };
E1575E7A293E77B5001665B1 /* TimeStampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129428F28F0BDC300796AC6 /* TimeStampType.swift */; }; E1575E7A293E77B5001665B1 /* TimeStampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129428F28F0BDC300796AC6 /* TimeStampType.swift */; };
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E306CC28EF6E8000537998 /* TimerProxy.swift */; }; E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E306CC28EF6E8000537998 /* TimerProxy.swift */; };
E1575E7D293E77B5001665B1 /* PosterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterType.swift */; }; E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */; };
E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* ItemFilterCollection.swift */; }; E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* ItemFilterCollection.swift */; };
E1575E7F293E77B5001665B1 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; E1575E7F293E77B5001665B1 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */; }; E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */; };
@ -461,6 +464,9 @@
E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F062B1B12C300442DB8 /* Backport.swift */; }; E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F062B1B12C300442DB8 /* Backport.swift */; };
E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; }; E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; };
E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; }; E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; };
E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D63EC2BD622A700AA665D /* CompactChannelView.swift */; };
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */; };
E15D63F02BD6DFC200AA665D /* SystemImageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */; };
E15EFA842BA167350080E926 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA832BA167350080E926 /* CollectionHStack */; }; E15EFA842BA167350080E926 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA832BA167350080E926 /* CollectionHStack */; };
E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */; }; E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */; };
E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; }; E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; };
@ -621,6 +627,8 @@
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; }; E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; };
E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; }; E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; };
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; }; E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButton.swift */; }; E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButton.swift */; };
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDto.swift */; }; E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDto.swift */; };
@ -682,7 +690,7 @@
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.swift */; }; E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.swift */; };
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.swift */; }; E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.swift */; };
E1CCC3D228C858A50020ED54 /* UserProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */; }; E1CCC3D228C858A50020ED54 /* UserProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */; };
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterType.swift */; }; E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */; };
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */; }; E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */; };
E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */; }; E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */; };
E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; }; E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; };
@ -751,8 +759,8 @@
E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */; }; E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */; };
E1E2F8422B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */; }; E1E2F8422B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */; };
E1E2F8432B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */; }; E1E2F8432B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */; };
E1E2F8452B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* AfterLastDisappearModifier.swift */; }; E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */; };
E1E2F8462B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* AfterLastDisappearModifier.swift */; }; E1E2F8462B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */; };
E1E306CD28EF6E8000537998 /* TimerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E306CC28EF6E8000537998 /* TimerProxy.swift */; }; E1E306CD28EF6E8000537998 /* TimerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E306CC28EF6E8000537998 /* TimerProxy.swift */; };
E1E5D5492783CDD700692DFE /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* VideoPlayerSettingsView.swift */; }; E1E5D5492783CDD700692DFE /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* VideoPlayerSettingsView.swift */; };
E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */; }; E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */; };
@ -800,11 +808,8 @@
E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; }; E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; };
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; }; E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; };
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; }; E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; };
E43918662AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */; }; E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */; };
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */; }; E43918672AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */; };
EA2073A12BAE35A400D8C78F /* QuickConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2073A02BAE35A400D8C78F /* QuickConnectViewModel.swift */; };
EA2073A22BAE35A400D8C78F /* QuickConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2073A02BAE35A400D8C78F /* QuickConnectViewModel.swift */; };
EADD26FD2BAE4A6C002F05DE /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADD26FC2BAE4A6C002F05DE /* QuickConnectView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@ -979,7 +984,7 @@
E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramButtonContent.swift; sourceTree = "<group>"; }; E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramButtonContent.swift; sourceTree = "<group>"; };
E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramProgressOverlay.swift; sourceTree = "<group>"; }; E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramProgressOverlay.swift; sourceTree = "<group>"; };
E10231332BCF8A3C009D71FC /* ProgramsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramsView.swift; sourceTree = "<group>"; }; E10231332BCF8A3C009D71FC /* ProgramsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramsView.swift; sourceTree = "<group>"; };
E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WideChannelGridItem.swift; sourceTree = "<group>"; }; E10231352BCF8A3C009D71FC /* DetailedChannelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailedChannelView.swift; sourceTree = "<group>"; };
E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryView.swift; sourceTree = "<group>"; }; E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryView.swift; sourceTree = "<group>"; };
E10231432BCF8A51009D71FC /* ChannelProgram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelProgram.swift; sourceTree = "<group>"; }; E10231432BCF8A51009D71FC /* ChannelProgram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelProgram.swift; sourceTree = "<group>"; };
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryViewModel.swift; sourceTree = "<group>"; }; E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryViewModel.swift; sourceTree = "<group>"; };
@ -999,6 +1004,8 @@
E104DC952B9E7E29008F506D /* AssertionFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertionFailureView.swift; sourceTree = "<group>"; }; E104DC952B9E7E29008F506D /* AssertionFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertionFailureView.swift; sourceTree = "<group>"; };
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; }; E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
E1092F4B29106F9F00163F57 /* GestureAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureAction.swift; sourceTree = "<group>"; }; E1092F4B29106F9F00163F57 /* GestureAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureAction.swift; sourceTree = "<group>"; };
E10B1E892BD76FA900A92EAF /* QuickConnectViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickConnectViewModel.swift; sourceTree = "<group>"; };
E10B1E8C2BD7708900A92EAF /* QuickConnectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = "<group>"; };
E10E842929A587110064EA49 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; }; E10E842929A587110064EA49 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
E10E842B29A589860064EA49 /* NonePosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonePosterButton.swift; sourceTree = "<group>"; }; E10E842B29A589860064EA49 /* NonePosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonePosterButton.swift; sourceTree = "<group>"; };
E10EAA4E277BBCC4000269ED /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = "<group>"; }; E10EAA4E277BBCC4000269ED /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = "<group>"; };
@ -1060,7 +1067,7 @@
E12E30F229638B140022FAC9 /* ChevronButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronButton.swift; sourceTree = "<group>"; }; E12E30F229638B140022FAC9 /* ChevronButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronButton.swift; sourceTree = "<group>"; };
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = "<group>"; }; E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = "<group>"; };
E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = "<group>"; }; E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = "<group>"; };
E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatioCornerRadiusModifier.swift; sourceTree = "<group>"; }; E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnSizeChangedModifier.swift; sourceTree = "<group>"; };
E133328729538D8D00EE76AB /* Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = "<group>"; }; E133328729538D8D00EE76AB /* Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = "<group>"; };
E133328C2953AE4B00EE76AB /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; }; E133328C2953AE4B00EE76AB /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
E133328E2953B71000EE76AB /* DownloadTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskView.swift; sourceTree = "<group>"; }; E133328E2953B71000EE76AB /* DownloadTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskView.swift; sourceTree = "<group>"; };
@ -1083,7 +1090,7 @@
E13DD3F82717E961009D4DAF /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = "<group>"; }; E13DD3F82717E961009D4DAF /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = "<group>"; };
E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = "<group>"; }; E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = "<group>"; };
E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = "<group>"; }; E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = "<group>"; };
E13F05EB28BC9000003499D2 /* LibraryViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryViewType.swift; sourceTree = "<group>"; }; E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryDisplayType.swift; sourceTree = "<group>"; };
E13F05EF28BC9016003499D2 /* LibraryRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; }; E13F05EF28BC9016003499D2 /* LibraryRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; };
E1401C9F2937DFF500E8B599 /* AppIconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSelectorView.swift; sourceTree = "<group>"; }; E1401C9F2937DFF500E8B599 /* AppIconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSelectorView.swift; sourceTree = "<group>"; };
E1401CA12938122C00E8B599 /* AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcons.swift; sourceTree = "<group>"; }; E1401CA12938122C00E8B599 /* AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcons.swift; sourceTree = "<group>"; };
@ -1125,6 +1132,8 @@
E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceInfoView.swift; sourceTree = "<group>"; }; E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceInfoView.swift; sourceTree = "<group>"; };
E15D4F062B1B12C300442DB8 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; }; E15D4F062B1B12C300442DB8 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
E15D4F092B1BD88900442DB8 /* Edge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Edge.swift; sourceTree = "<group>"; }; E15D4F092B1BD88900442DB8 /* Edge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Edge.swift; sourceTree = "<group>"; };
E15D63EC2BD622A700AA665D /* CompactChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactChannelView.swift; sourceTree = "<group>"; };
E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImageable.swift; sourceTree = "<group>"; };
E168BD08289A4162001A6922 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; E168BD08289A4162001A6922 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; }; E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = "<group>"; }; E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = "<group>"; };
@ -1237,6 +1246,7 @@
E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = "<group>"; }; E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = "<group>"; };
E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; }; E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; };
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; }; E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; }; E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = "<group>"; }; E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = "<group>"; };
E1AD104C26D96CE3003E4A08 /* BaseItemDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDto.swift; sourceTree = "<group>"; }; E1AD104C26D96CE3003E4A08 /* BaseItemDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDto.swift; sourceTree = "<group>"; };
@ -1285,7 +1295,7 @@
E1CAF65B2BA345830087D991 /* MediaViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaViewModel.swift; sourceTree = "<group>"; }; E1CAF65B2BA345830087D991 /* MediaViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaViewModel.swift; sourceTree = "<group>"; };
E1CAF6612BA363840087D991 /* UIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingController.swift; sourceTree = "<group>"; }; E1CAF6612BA363840087D991 /* UIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingController.swift; sourceTree = "<group>"; };
E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileButton.swift; sourceTree = "<group>"; }; E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileButton.swift; sourceTree = "<group>"; };
E1CCF12D28ABF989006CAC9E /* PosterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterType.swift; sourceTree = "<group>"; }; E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterDisplayType.swift; sourceTree = "<group>"; };
E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; }; E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; };
E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectOrientationModifier.swift; sourceTree = "<group>"; }; E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectOrientationModifier.swift; sourceTree = "<group>"; };
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; }; E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
@ -1334,7 +1344,7 @@
E1E1E24C28DF8A2E000DF5FD /* PreferenceKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceKeys.swift; sourceTree = "<group>"; }; E1E1E24C28DF8A2E000DF5FD /* PreferenceKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceKeys.swift; sourceTree = "<group>"; };
E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFinalDisappearModifier.swift; sourceTree = "<group>"; }; E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFinalDisappearModifier.swift; sourceTree = "<group>"; };
E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFirstAppearModifier.swift; sourceTree = "<group>"; }; E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFirstAppearModifier.swift; sourceTree = "<group>"; };
E1E2F8442B757E3400B75998 /* AfterLastDisappearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AfterLastDisappearModifier.swift; sourceTree = "<group>"; }; E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinceLastDisappearModifier.swift; sourceTree = "<group>"; };
E1E306CC28EF6E8000537998 /* TimerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerProxy.swift; sourceTree = "<group>"; }; E1E306CC28EF6E8000537998 /* TimerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerProxy.swift; sourceTree = "<group>"; };
E1E5D5472783CCF900692DFE /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; }; E1E5D5472783CCF900692DFE /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = "<group>"; }; E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = "<group>"; };
@ -1371,9 +1381,7 @@
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; }; E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; };
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; }; E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenePhaseChangeModifier.swift; sourceTree = "<group>"; }; E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScenePhaseChangedModifier.swift; sourceTree = "<group>"; };
EA2073A02BAE35A400D8C78F /* QuickConnectViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectViewModel.swift; sourceTree = "<group>"; };
EADD26FC2BAE4A6C002F05DE /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -1523,6 +1531,7 @@
E1CAF65C2BA345830087D991 /* MediaViewModel */, E1CAF65C2BA345830087D991 /* MediaViewModel */,
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */, E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */,
6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */, 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */,
E10B1E892BD76FA900A92EAF /* QuickConnectViewModel.swift */,
62E632DB267D2E130063E547 /* SearchViewModel.swift */, 62E632DB267D2E130063E547 /* SearchViewModel.swift */,
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */,
E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */,
@ -1532,7 +1541,6 @@
BD0BA2292AD6501300306A8D /* VideoPlayerManager */, BD0BA2292AD6501300306A8D /* VideoPlayerManager */,
E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */, E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */,
625CB57B2678CE1000530A6E /* ViewModel.swift */, 625CB57B2678CE1000530A6E /* ViewModel.swift */,
EA2073A02BAE35A400D8C78F /* QuickConnectViewModel.swift */,
); );
path = ViewModels; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1626,13 +1634,13 @@
E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */, E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */,
E14EDECA2B8FB66F000F00A4 /* ItemFilter */, E14EDECA2B8FB66F000F00A4 /* ItemFilter */,
E1C925F328875037002A7A66 /* ItemViewType.swift */, E1C925F328875037002A7A66 /* ItemViewType.swift */,
E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */,
E1DE2B4E2B983F3200F6715F /* LibraryParent */, E1DE2B4E2B983F3200F6715F /* LibraryParent */,
E13F05EB28BC9000003499D2 /* LibraryViewType.swift */,
E1AA331E2782639D00F6439C /* OverlayType.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */,
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
E1937A60288F32DB00CB80AA /* Poster.swift */, E1937A60288F32DB00CB80AA /* Poster.swift */,
E1CCF12D28ABF989006CAC9E /* PosterType.swift */, E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */,
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */, E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */, E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */, E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
@ -1641,6 +1649,7 @@
E11042742B8013DF00821020 /* Stateful.swift */, E11042742B8013DF00821020 /* Stateful.swift */,
E1EF4C402911B783008CC695 /* StreamType.swift */, E1EF4C402911B783008CC695 /* StreamType.swift */,
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */, E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */,
E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */,
E1A1528428FD191A00600579 /* TextPair.swift */, E1A1528428FD191A00600579 /* TextPair.swift */,
E1E306CC28EF6E8000537998 /* TimerProxy.swift */, E1E306CC28EF6E8000537998 /* TimerProxy.swift */,
E129428F28F0BDC300796AC6 /* TimeStampType.swift */, E129428F28F0BDC300796AC6 /* TimeStampType.swift */,
@ -1923,6 +1932,7 @@
6267B3D526710B8900A7371D /* Collection.swift */, 6267B3D526710B8900A7371D /* Collection.swift */,
E173DA5126D04AAF00CC4EB7 /* Color.swift */, E173DA5126D04AAF00CC4EB7 /* Color.swift */,
E1B490462967E2E500D3EDCE /* CoreStore.swift */, E1B490462967E2E500D3EDCE /* CoreStore.swift */,
E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */,
E15756312935642A00976E1F /* Double.swift */, E15756312935642A00976E1F /* Double.swift */,
E15D4F092B1BD88900442DB8 /* Edge.swift */, E15D4F092B1BD88900442DB8 /* Edge.swift */,
E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */, E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */,
@ -2085,18 +2095,19 @@
path = ProgramsView; path = ProgramsView;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E10231362BCF8A3C009D71FC /* Component */ = { E10231362BCF8A3C009D71FC /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */, E15D63EC2BD622A700AA665D /* CompactChannelView.swift */,
E10231352BCF8A3C009D71FC /* DetailedChannelView.swift */,
); );
path = Component; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E10231382BCF8A3C009D71FC /* ChannelLibraryView */ = { E10231382BCF8A3C009D71FC /* ChannelLibraryView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E10231362BCF8A3C009D71FC /* Component */, E10231362BCF8A3C009D71FC /* Components */,
E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */, E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */,
); );
path = ChannelLibraryView; path = ChannelLibraryView;
@ -2326,6 +2337,7 @@
E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */, E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */,
E103DF932BCF31C5000229B2 /* MediaView */, E103DF932BCF31C5000229B2 /* MediaView */,
E10231572BCF8AF8009D71FC /* ProgramsView */, E10231572BCF8AF8009D71FC /* ProgramsView */,
E10B1E8C2BD7708900A92EAF /* QuickConnectView.swift */,
E1E1643928BAC2EF00323B0A /* SearchView.swift */, E1E1643928BAC2EF00323B0A /* SearchView.swift */,
E193D54F2719430400900D82 /* ServerDetailView.swift */, E193D54F2719430400900D82 /* ServerDetailView.swift */,
E193D54A271941D300900D82 /* ServerListView.swift */, E193D54A271941D300900D82 /* ServerListView.swift */,
@ -2333,7 +2345,6 @@
E193D546271941C500900D82 /* UserListView.swift */, E193D546271941C500900D82 /* UserListView.swift */,
E193D548271941CC00900D82 /* UserSignInView.swift */, E193D548271941CC00900D82 /* UserSignInView.swift */,
5310694F2684E7EE00CFFDBA /* VideoPlayer */, 5310694F2684E7EE00CFFDBA /* VideoPlayer */,
EADD26FC2BAE4A6C002F05DE /* QuickConnectView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2504,17 +2515,17 @@
E170D101294CE4C10017224C /* Modifiers */ = { E170D101294CE4C10017224C /* Modifiers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1E2F8442B757E3400B75998 /* AfterLastDisappearModifier.swift */,
E18E0202288749200022598C /* AttributeStyleModifier.swift */, E18E0202288749200022598C /* AttributeStyleModifier.swift */,
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */, E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */,
E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */, E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */,
E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */, E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */,
E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */, E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */,
E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */, E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */,
E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */,
E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */,
E1DE2B4B2B98389E00F6715F /* PaletteOverlayRenderingModifier.swift */, E1DE2B4B2B98389E00F6715F /* PaletteOverlayRenderingModifier.swift */,
E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */,
E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */,
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */, E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */,
E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */,
); );
path = Modifiers; path = Modifiers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3528,7 +3539,6 @@
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */, E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
E1DD55382B6EE533007501C0 /* Task.swift in Sources */, E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
E1575EA1293E7B1E001665B1 /* String.swift in Sources */, E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
EADD26FD2BAE4A6C002F05DE /* QuickConnectView.swift in Sources */,
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */, E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */, E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */, E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
@ -3561,6 +3571,7 @@
E1153D9A2BBA3E9800424D36 /* ErrorCard.swift in Sources */, E1153D9A2BBA3E9800424D36 /* ErrorCard.swift in Sources */,
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */, E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */, E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */,
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */, E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */, E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */,
E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */, E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
@ -3606,7 +3617,7 @@
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */, E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */, E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */,
E1E2F8462B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */, E1E2F8462B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */,
E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */, E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */,
E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */, E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */,
E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */, E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */,
@ -3618,22 +3629,22 @@
E12376B02A33D6AE001F5B44 /* AboutViewCard.swift in Sources */, E12376B02A33D6AE001F5B44 /* AboutViewCard.swift in Sources */,
E12A9EF929499E0100731C3A /* JellyfinClient.swift in Sources */, E12A9EF929499E0100731C3A /* JellyfinClient.swift in Sources */,
E148128328C1443D003B8787 /* NameGuidPair.swift in Sources */, E148128328C1443D003B8787 /* NameGuidPair.swift in Sources */,
EA2073A22BAE35A400D8C78F /* QuickConnectViewModel.swift in Sources */,
E1579EA82B97DC1500A31CA1 /* Eventful.swift in Sources */, E1579EA82B97DC1500A31CA1 /* Eventful.swift in Sources */,
E185920828CDAAA200326F80 /* SimilarItemsHStack.swift in Sources */, E185920828CDAAA200326F80 /* SimilarItemsHStack.swift in Sources */,
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */, E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */,
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */, E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */,
E10B1E8B2BD76FA900A92EAF /* QuickConnectViewModel.swift in Sources */,
E1DC9842296DEBD800982F06 /* WatchedIndicator.swift in Sources */, E1DC9842296DEBD800982F06 /* WatchedIndicator.swift in Sources */,
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */, E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */,
E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */, E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */,
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */, E10E842A29A587110064EA49 /* LoadingView.swift in Sources */,
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */, E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */,
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */, E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */, E43918672AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */, E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */,
E1D37F562B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */, E1D37F562B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */,
E1575E75293E77B5001665B1 /* LibraryViewType.swift in Sources */, E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */,
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */, E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */,
@ -3687,7 +3698,7 @@
C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */, C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */,
E1575E9F293E7B1E001665B1 /* Int.swift in Sources */, E1575E9F293E7B1E001665B1 /* Int.swift in Sources */,
E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */, E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */,
E1575E7D293E77B5001665B1 /* PosterType.swift in Sources */, E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */,
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */,
E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */, E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */,
E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */, E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */,
@ -3717,6 +3728,7 @@
E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */,
E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */, E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */,
E154967C296CBB1A00C4EF88 /* FontPickerView.swift in Sources */, E154967C296CBB1A00C4EF88 /* FontPickerView.swift in Sources */,
E15D63F02BD6DFC200AA665D /* SystemImageable.swift in Sources */,
E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */,
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
E1575E86293E7A00001665B1 /* AppIcons.swift in Sources */, E1575E86293E7A00001665B1 /* AppIcons.swift in Sources */,
@ -3788,6 +3800,7 @@
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */, E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */,
E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */, E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */,
E18ACA8D2A14773500BB4F35 /* (null) in Sources */, E18ACA8D2A14773500BB4F35 /* (null) in Sources */,
E10B1E8E2BD7708900A92EAF /* QuickConnectView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -3821,7 +3834,7 @@
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */, 62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */,
E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */, E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */,
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */, E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */, E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
@ -3847,7 +3860,6 @@
E1E1644128BB301900323B0A /* Array.swift in Sources */, E1E1644128BB301900323B0A /* Array.swift in Sources */,
E18CE0AF28A222240092E7F1 /* PublicUserSignInView.swift in Sources */, E18CE0AF28A222240092E7F1 /* PublicUserSignInView.swift in Sources */,
E129429828F4785200796AC6 /* CaseIterablePicker.swift in Sources */, E129429828F4785200796AC6 /* CaseIterablePicker.swift in Sources */,
EA2073A12BAE35A400D8C78F /* QuickConnectViewModel.swift in Sources */,
E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */, E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */,
E154965E296CA2EF00C4EF88 /* DownloadTask.swift in Sources */, E154965E296CA2EF00C4EF88 /* DownloadTask.swift in Sources */,
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
@ -3859,6 +3871,7 @@
E18E01FA288747580022598C /* AboutAppView.swift in Sources */, E18E01FA288747580022598C /* AboutAppView.swift in Sources */,
E170D103294CE8BF0017224C /* LoadingView.swift in Sources */, E170D103294CE8BF0017224C /* LoadingView.swift in Sources */,
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */,
E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */, E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */,
E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */,
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */, E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */,
@ -3932,6 +3945,7 @@
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */, E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
E10B1E8F2BD7728400A92EAF /* QuickConnectView.swift in Sources */,
E1DD55372B6EE533007501C0 /* Task.swift in Sources */, E1DD55372B6EE533007501C0 /* Task.swift in Sources */,
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */, E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
@ -3940,7 +3954,6 @@
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */, E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */, E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */,
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */,
E1DC9819296DD1CD00982F06 /* CinematicBackgroundView.swift in Sources */, E1DC9819296DD1CD00982F06 /* CinematicBackgroundView.swift in Sources */,
C46DD8E72A8FA77F0046A504 /* LiveBottomBarView.swift in Sources */, C46DD8E72A8FA77F0046A504 /* LiveBottomBarView.swift in Sources */,
E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */, E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */,
@ -3956,10 +3969,11 @@
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */, E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
E168BD10289A4162001A6922 /* HomeView.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
E1401CB129386C9200E8B599 /* UIColor.swift in Sources */, E1401CB129386C9200E8B599 /* UIColor.swift in Sources */,
E1E2F8452B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */, E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */,
E18E01AB288746AF0022598C /* PillHStack.swift in Sources */, E18E01AB288746AF0022598C /* PillHStack.swift in Sources */,
E1401CAB2938140A00E8B599 /* LightAppIcon.swift in Sources */, E1401CAB2938140A00E8B599 /* LightAppIcon.swift in Sources */,
E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */, E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */,
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */,
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */, E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */,
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
@ -3992,6 +4006,7 @@
6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */, 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */,
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */, E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */, E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */,
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */, E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
@ -4000,11 +4015,11 @@
C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */, C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */,
E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */, E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */,
E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */, E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */,
E43918662AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */, E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */, E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */,
E1B33ED128EB860A0073B0FD /* LargePlaybackButtons.swift in Sources */, E1B33ED128EB860A0073B0FD /* LargePlaybackButtons.swift in Sources */,
E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */, E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */,
E102313F2BCF8A3C009D71FC /* WideChannelGridItem.swift in Sources */, E102313F2BCF8A3C009D71FC /* DetailedChannelView.swift in Sources */,
E113133228BDC72000930F75 /* FilterView.swift in Sources */, E113133228BDC72000930F75 /* FilterView.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */, E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */,
@ -4020,6 +4035,7 @@
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */,
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
E1D37F4E2B9CEDC400343D2B /* DeviceProfile.swift in Sources */, E1D37F4E2B9CEDC400343D2B /* DeviceProfile.swift in Sources */,
E10B1E8A2BD76FA900A92EAF /* QuickConnectViewModel.swift in Sources */,
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */, E1EF4C412911B783008CC695 /* StreamType.swift in Sources */,
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */, E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */,
@ -4040,7 +4056,7 @@
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */, E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */, E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */,
E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */, E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */,
E1D842912933F87500D1041A /* ItemFields.swift in Sources */, E1D842912933F87500D1041A /* ItemFields.swift in Sources */,
E1BDF2F729524ECD00CC0294 /* PlaybackSpeedActionButton.swift in Sources */, E1BDF2F729524ECD00CC0294 /* PlaybackSpeedActionButton.swift in Sources */,
@ -4152,7 +4168,7 @@
E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */, E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */,
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */, E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */, E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,
E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */, E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */,
5377CBF5263B596A003A4E83 /* SwiftfinApp.swift in Sources */, 5377CBF5263B596A003A4E83 /* SwiftfinApp.swift in Sources */,
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */, E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,

View File

@ -12,23 +12,26 @@ import SwiftUI
// TODO: expose `ImageView.image` modifier for image aspect fill/fit // TODO: expose `ImageView.image` modifier for image aspect fill/fit
// TODO: allow `content` to trigger `onSelect`? // TODO: allow `content` to trigger `onSelect`?
// - not in button label to avoid context menu visual oddities
// TODO: get width/height for images from layout size?
// TODO: why don't shadows work with failure image views?
// - due to `Color`?
struct PosterButton<Item: Poster>: View { struct PosterButton<Item: Poster>: View {
private var item: Item private var item: Item
private var type: PosterType private var type: PosterDisplayType
private var content: () -> any View private var content: () -> any View
private var imageOverlay: () -> any View private var imageOverlay: () -> any View
private var contextMenu: () -> any View private var contextMenu: () -> any View
private var onSelect: () -> Void private var onSelect: () -> Void
private var singleImage: Bool
private func imageView(from item: Item) -> ImageView { private func imageView(from item: Item) -> ImageView {
switch type { switch type {
case .portrait:
ImageView(item.portraitPosterImageSource(maxWidth: 200))
case .landscape: case .landscape:
ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage)) ImageView(item.landscapeImageSources(maxWidth: 500))
case .portrait:
ImageView(item.portraitImageSources(maxWidth: 200))
} }
} }
@ -42,7 +45,14 @@ struct PosterButton<Item: Poster>: View {
imageView(from: item) imageView(from: item)
.failure { .failure {
SystemImageContentView(systemName: item.typeSystemImage) if item.showTitle {
SystemImageContentView(systemName: item.systemImage)
} else {
SystemImageContentView(
title: item.displayTitle,
systemName: item.systemImage
)
}
} }
imageOverlay() imageOverlay()
@ -50,6 +60,7 @@ struct PosterButton<Item: Poster>: View {
} }
.posterStyle(type) .posterStyle(type)
} }
.buttonStyle(.plain)
.contextMenu(menuItems: { .contextMenu(menuItems: {
contextMenu() contextMenu()
.eraseToAnyView() .eraseToAnyView()
@ -66,8 +77,7 @@ extension PosterButton {
init( init(
item: Item, item: Item,
type: PosterType, type: PosterDisplayType
singleImage: Bool = false
) { ) {
self.init( self.init(
item: item, item: item,
@ -75,8 +85,7 @@ extension PosterButton {
content: { TitleSubtitleContentView(item: item) }, content: { TitleSubtitleContentView(item: item) },
imageOverlay: { DefaultOverlay(item: item) }, imageOverlay: { DefaultOverlay(item: item) },
contextMenu: { EmptyView() }, contextMenu: { EmptyView() },
onSelect: {}, onSelect: {}
singleImage: singleImage
) )
} }
@ -97,7 +106,8 @@ extension PosterButton {
} }
} }
// TODO: Shared default content? // TODO: Shared default content with tvOS?
// - check if content is generally same
extension PosterButton { extension PosterButton {
@ -119,7 +129,7 @@ extension PosterButton {
let item: Item let item: Item
var body: some View { var body: some View {
Text(item.subtitle ?? "") Text(item.subtitle ?? " ")
.font(.caption.weight(.medium)) .font(.caption.weight(.medium))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
@ -144,6 +154,49 @@ extension PosterButton {
} }
} }
// Content specific for BaseItemDto episode items
struct EpisodeContentSubtitleContent: View {
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
private var useSeriesLandscapeBackdrop
let item: Item
var body: some View {
if let item = item as? BaseItemDto {
// Unsure why this needs 0 spacing
// compared to other default content
VStack(alignment: .leading, spacing: 0) {
if item.showTitle, let seriesName = item.seriesName {
Text(seriesName)
.font(.footnote.weight(.regular))
.foregroundColor(.primary)
.backport
.lineLimit(1, reservesSpace: true)
}
SeparatorHStack {
Text(item.seasonEpisodeLabel ?? .emptyDash)
if item.showTitle || useSeriesLandscapeBackdrop {
Text(item.displayTitle)
} else if let seriesName = item.seriesName {
Text(seriesName)
}
}
.separator {
Circle()
.frame(width: 2, height: 2)
.padding(.horizontal, 3)
}
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
}
// MARK: Default Overlay // MARK: Default Overlay
struct DefaultOverlay: View { struct DefaultOverlay: View {

View File

@ -14,9 +14,8 @@ struct PosterHStack<Item: Poster>: View {
private var header: () -> any View private var header: () -> any View
private var title: String? private var title: String?
private var type: PosterType private var type: PosterDisplayType
private var items: Binding<OrderedSet<Item>> private var items: Binding<OrderedSet<Item>>
private var singleImage: Bool
private var content: (Item) -> any View private var content: (Item) -> any View
private var imageOverlay: (Item) -> any View private var imageOverlay: (Item) -> any View
private var contextMenu: (Item) -> any View private var contextMenu: (Item) -> any View
@ -31,8 +30,7 @@ struct PosterHStack<Item: Poster>: View {
) { item in ) { item in
PosterButton( PosterButton(
item: item, item: item,
type: type, type: type
singleImage: singleImage
) )
.content { content(item).eraseToAnyView() } .content { content(item).eraseToAnyView() }
.imageOverlay { imageOverlay(item).eraseToAnyView() } .imageOverlay { imageOverlay(item).eraseToAnyView() }
@ -41,8 +39,8 @@ struct PosterHStack<Item: Poster>: View {
} }
.clipsToBounds(false) .clipsToBounds(false)
.dataPrefix(20) .dataPrefix(20)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
} }
@ -54,8 +52,7 @@ struct PosterHStack<Item: Poster>: View {
) { item in ) { item in
PosterButton( PosterButton(
item: item, item: item,
type: type, type: type
singleImage: singleImage
) )
.content { content(item).eraseToAnyView() } .content { content(item).eraseToAnyView() }
.imageOverlay { imageOverlay(item).eraseToAnyView() } .imageOverlay { imageOverlay(item).eraseToAnyView() }
@ -64,8 +61,8 @@ struct PosterHStack<Item: Poster>: View {
} }
.clipsToBounds(false) .clipsToBounds(false)
.dataPrefix(20) .dataPrefix(20)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
} }
@ -96,16 +93,14 @@ extension PosterHStack {
init( init(
title: String? = nil, title: String? = nil,
type: PosterType, type: PosterDisplayType,
items: Binding<OrderedSet<Item>>, items: Binding<OrderedSet<Item>>
singleImage: Bool = false
) { ) {
self.init( self.init(
header: { DefaultHeader(title: title) }, header: { DefaultHeader(title: title) },
title: title, title: title,
type: type, type: type,
items: items, items: items,
singleImage: singleImage,
content: { PosterButton.TitleSubtitleContentView(item: $0) }, content: { PosterButton.TitleSubtitleContentView(item: $0) },
imageOverlay: { PosterButton.DefaultOverlay(item: $0) }, imageOverlay: { PosterButton.DefaultOverlay(item: $0) },
contextMenu: { _ in EmptyView() }, contextMenu: { _ in EmptyView() },
@ -116,15 +111,13 @@ extension PosterHStack {
init<S: Sequence<Item>>( init<S: Sequence<Item>>(
title: String? = nil, title: String? = nil,
type: PosterType, type: PosterDisplayType,
items: S, items: S
singleImage: Bool = false
) { ) {
self.init( self.init(
title: title, title: title,
type: type, type: type,
items: .constant(OrderedSet(items)), items: .constant(OrderedSet(items))
singleImage: singleImage
) )
} }

View File

@ -12,43 +12,100 @@ import Foundation
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: wide + narrow view toggling
// - after `PosterType` has been refactored and with customizable toggle button
// TODO: sorting by number/filtering // TODO: sorting by number/filtering
// - should be able to use normal filter view model, but how to add custom filters for data context? // - see if can use normal filter view model?
// - how to add custom filters for data context?
// TODO: saving item display type/detailed column count
// - wait until after user refactor
// Note: Repurposes `LibraryDisplayType` to save from creating a new type.
// If there are other places where detailed/compact contextually differ
// from the library types, then create a new type and use it here.
// - list: detailed
// - grid: compact
struct ChannelLibraryView: View { struct ChannelLibraryView: View {
@EnvironmentObject @EnvironmentObject
private var mainRouter: MainCoordinator.Router private var mainRouter: MainCoordinator.Router
@State
private var channelDisplayType: LibraryDisplayType = .list
@State @State
private var layout: CollectionVGridLayout private var layout: CollectionVGridLayout
@StateObject @StateObject
private var viewModel = ChannelLibraryViewModel() private var viewModel = ChannelLibraryViewModel()
// MARK: init
init() { init() {
if UIDevice.isPhone { if UIDevice.isPhone {
layout = .columns(1) layout = Self.padlayout(channelDisplayType: .list)
} else { } else {
layout = .minWidth(250) layout = Self.phonelayout(channelDisplayType: .list)
} }
} }
// MARK: layout
private static func padlayout(
channelDisplayType: LibraryDisplayType
) -> CollectionVGridLayout {
switch channelDisplayType {
case .grid:
.minWidth(150)
case .list:
.minWidth(250)
}
}
private static func phonelayout(
channelDisplayType: LibraryDisplayType
) -> CollectionVGridLayout {
switch channelDisplayType {
case .grid:
.columns(3)
case .list:
.columns(1)
}
}
// MARK: item view
private func narrowChannelView(channel: ChannelProgram) -> some View {
CompactChannelView(channel: channel.channel)
.onSelect {
guard let mediaSource = channel.channel.mediaSources?.first else { return }
mainRouter.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
)
}
}
private func wideChannelView(channel: ChannelProgram) -> some View {
DetailedChannelView(channel: channel)
.onSelect {
guard let mediaSource = channel.channel.mediaSources?.first else { return }
mainRouter.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
)
}
}
private var contentView: some View { private var contentView: some View {
CollectionVGrid( CollectionVGrid(
$viewModel.elements, $viewModel.elements,
layout: layout layout: $layout
) { channel in ) { channel in
WideChannelGridItem(channel: channel) switch channelDisplayType {
.onSelect { case .grid:
guard let mediaSource = channel.channel.mediaSources?.first else { return } narrowChannelView(channel: channel)
mainRouter.route( case .list:
to: \.liveVideoPlayer, wideChannelView(channel: channel)
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource) }
)
}
} }
.onReachedBottomEdge(offset: .offset(300)) { .onReachedBottomEdge(offset: .offset(300)) {
viewModel.send(.getNextPage) viewModel.send(.getNextPage)
@ -79,12 +136,19 @@ struct ChannelLibraryView: View {
} }
.navigationTitle(L10n.channels) .navigationTitle(L10n.channels)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.onChange(of: channelDisplayType) { newValue in
if UIDevice.isPhone {
layout = Self.phonelayout(channelDisplayType: newValue)
} else {
layout = Self.padlayout(channelDisplayType: newValue)
}
}
.onFirstAppear { .onFirstAppear {
if viewModel.state == .initial { if viewModel.state == .initial {
viewModel.send(.refresh) viewModel.send(.refresh)
} }
} }
.afterLastDisappear { interval in .sinceLastDisappear { interval in
// refresh after 3 hours // refresh after 3 hours
if interval >= 10800 { if interval >= 10800 {
viewModel.send(.refresh) viewModel.send(.refresh)
@ -95,6 +159,23 @@ struct ChannelLibraryView: View {
if viewModel.backgroundStates.contains(.gettingNextPage) { if viewModel.backgroundStates.contains(.gettingNextPage) {
ProgressView() ProgressView()
} }
Menu {
// We repurposed `LibraryDisplayType` but want different labels
Picker("Channel Display", selection: $channelDisplayType) {
Label("Compact", systemImage: LibraryDisplayType.grid.systemImage)
.tag(LibraryDisplayType.grid)
Label("Detailed", systemImage: LibraryDisplayType.list.systemImage)
.tag(LibraryDisplayType.list)
}
} label: {
Label(
channelDisplayType.displayTitle,
systemImage: channelDisplayType.systemImage
)
}
} }
} }
} }

View File

@ -0,0 +1,75 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
extension ChannelLibraryView {
struct CompactChannelView: View {
@Environment(\.colorScheme)
private var colorScheme
let channel: BaseItemDto
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
VStack(alignment: .leading) {
ZStack {
Color.secondarySystemFill
.opacity(colorScheme == .dark ? 0.5 : 1)
.posterShadow()
ImageView(channel.imageSource(.primary, maxWidth: 120))
.image {
$0.aspectRatio(contentMode: .fit)
}
.failure {
SystemImageContentView(systemName: channel.systemImage)
.background(color: .clear)
.imageFrameRatio(width: 2, height: 2)
}
.placeholder { _ in
EmptyView()
}
.padding(5)
}
.aspectRatio(1.0, contentMode: .fill)
.cornerRadius(ratio: 0.0375, of: \.width)
.posterBorder(ratio: 0.0375, of: \.width)
Text(channel.displayTitle)
.font(.footnote.weight(.regular))
.foregroundColor(.primary)
.backport
.lineLimit(1, reservesSpace: true)
}
}
.buttonStyle(.plain)
}
}
}
extension ChannelLibraryView.CompactChannelView {
init(channel: BaseItemDto) {
self.init(
channel: channel,
onSelect: {}
)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -14,7 +14,7 @@ import SwiftUI
extension ChannelLibraryView { extension ChannelLibraryView {
struct WideChannelGridItem: View { struct DetailedChannelView: View {
@Default(.accentColor) @Default(.accentColor)
private var accentColor private var accentColor
@ -39,21 +39,22 @@ extension ChannelLibraryView {
.opacity(colorScheme == .dark ? 0.5 : 1) .opacity(colorScheme == .dark ? 0.5 : 1)
.posterShadow() .posterShadow()
ImageView(channel.portraitPosterImageSource(maxWidth: 80)) ImageView(channel.channel.imageSource(.primary, maxWidth: 120))
.image { .image {
$0.aspectRatio(contentMode: .fit) $0.aspectRatio(contentMode: .fit)
} }
.failure { .failure {
SystemImageContentView(systemName: channel.typeSystemImage) SystemImageContentView(systemName: channel.systemImage)
.background(color: .clear) .background(color: .clear)
.imageFrameRatio(width: 2, height: 2) .imageFrameRatio(width: 2, height: 2)
} }
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.padding(2) .padding(5)
} }
.aspectRatio(1.0, contentMode: .fill) .aspectRatio(1.0, contentMode: .fill)
.posterBorder(ratio: 0.0375, of: \.width)
.cornerRadius(ratio: 0.0375, of: \.width) .cornerRadius(ratio: 0.0375, of: \.width)
Text(channel.channel.number ?? "") Text(channel.channel.number ?? "")
@ -116,7 +117,7 @@ extension ChannelLibraryView {
Button { Button {
onSelect() onSelect()
} label: { } label: {
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding) { HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
channelLogo channelLogo
.frame(width: 80) .frame(width: 80)
@ -138,7 +139,7 @@ extension ChannelLibraryView {
Spacer() Spacer()
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.size($contentSize) .trackingSize($contentSize)
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -154,7 +155,7 @@ extension ChannelLibraryView {
} }
} }
extension ChannelLibraryView.WideChannelGridItem { extension ChannelLibraryView.DetailedChannelView {
init(channel: ChannelProgram) { init(channel: ChannelProgram) {
self.init( self.init(

View File

@ -36,7 +36,7 @@ extension DownloadTaskView {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .center) { VStack(alignment: .center) {
ImageView(downloadTask.item.landscapePosterImageSources(maxWidth: 600, single: true)) ImageView(downloadTask.item.landscapeImageSources(maxWidth: 600))
.frame(maxHeight: 300) .frame(maxHeight: 300)
.aspectRatio(1.77, contentMode: .fill) .aspectRatio(1.77, contentMode: .fill)
.cornerRadius(10) .cornerRadius(10)

View File

@ -38,6 +38,13 @@ extension HomeView {
columns: columnCount columns: columnCount
) { item in ) { item in
PosterButton(item: item, type: .landscape) PosterButton(item: item, type: .landscape)
.content {
if item.type == .episode {
PosterButton.EpisodeContentSubtitleContent(item: item)
} else {
PosterButton.TitleSubtitleContentView(item: item)
}
}
.contextMenu { .contextMenu {
Button { Button {
viewModel.send(.setIsPlayed(true, item)) viewModel.send(.setIsPlayed(true, item))

View File

@ -31,11 +31,12 @@ extension HomeView {
type: nextUpPosterType, type: nextUpPosterType,
items: $homeViewModel.nextUpViewModel.elements items: $homeViewModel.nextUpViewModel.elements
) )
.trailing { .content { item in
SeeAllButton() if item.type == .episode {
.onSelect { PosterButton.EpisodeContentSubtitleContent(item: item)
router.route(to: \.library, homeViewModel.nextUpViewModel) } else {
} PosterButton.TitleSubtitleContentView(item: item)
}
} }
.contextMenu { item in .contextMenu { item in
Button { Button {
@ -47,6 +48,12 @@ extension HomeView {
.onSelect { item in .onSelect { item in
router.route(to: \.item, item) router.route(to: \.item, item)
} }
.trailing {
SeeAllButton()
.onSelect {
router.route(to: \.library, homeViewModel.nextUpViewModel)
}
}
} }
} }
} }

View File

@ -83,7 +83,7 @@ struct HomeView: View {
.accessibilityLabel(L10n.settings) .accessibilityLabel(L10n.settings)
} }
} }
.afterLastDisappear { interval in .sinceLastDisappear { interval in
if interval > 60 || viewModel.notificationsReceived.contains(.itemMetadataDidChange) { if interval > 60 || viewModel.notificationsReceived.contains(.itemMetadataDidChange) {
viewModel.send(.backgroundRefresh) viewModel.send(.backgroundRefresh)
viewModel.notificationsReceived.remove(.itemMetadataDidChange) viewModel.notificationsReceived.remove(.itemMetadataDidChange)

View File

@ -44,8 +44,7 @@ extension SeriesEpisodeSelector {
var body: some View { var body: some View {
PosterButton( PosterButton(
item: episode, item: episode,
type: .landscape, type: .landscape
singleImage: true
) )
.content { .content {
let content: String = if episode.isUnaired { let content: String = if episode.isUnaired {

View File

@ -36,8 +36,8 @@ extension SeriesEpisodeSelector {
SeriesEpisodeSelector.EpisodeCard(episode: episode) SeriesEpisodeSelector.EpisodeCard(episode: episode)
} }
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
.proxy(proxy) .proxy(proxy)
.onFirstAppear { .onFirstAppear {
guard !didScrollToPlayButtonItem else { return } guard !didScrollToPlayButtonItem else { return }
@ -77,8 +77,8 @@ extension SeriesEpisodeSelector {
SeriesEpisodeSelector.EmptyCard() SeriesEpisodeSelector.EmptyCard()
} }
.allowScrolling(false) .allowScrolling(false)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
} }
} }
@ -101,8 +101,8 @@ extension SeriesEpisodeSelector {
} }
} }
.allowScrolling(false) .allowScrolling(false)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
} }
} }
@ -116,8 +116,8 @@ extension SeriesEpisodeSelector {
SeriesEpisodeSelector.LoadingCard() SeriesEpisodeSelector.LoadingCard()
} }
.allowScrolling(false) .allowScrolling(false)
.insets(horizontal: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.edgePadding / 2)
} }
} }
} }

View File

@ -25,10 +25,9 @@ extension EpisodeItemView {
VStack(alignment: .center) { VStack(alignment: .center) {
ImageView(viewModel.item.imageSource(.primary, maxWidth: 600)) ImageView(viewModel.item.imageSource(.primary, maxWidth: 600))
.frame(maxHeight: 300) .frame(maxHeight: 300)
.aspectRatio(1.77, contentMode: .fill) .posterStyle(.landscape)
.cornerRadius(10)
.padding(.horizontal)
.posterShadow() .posterShadow()
.padding(.horizontal)
ShelfView(viewModel: viewModel) ShelfView(viewModel: viewModel)
} }

View File

@ -109,7 +109,7 @@ extension ItemView.CinematicScrollView {
VStack(alignment: .center, spacing: 10) { VStack(alignment: .center, spacing: 10) {
if !cinematicItemViewTypeUsePrimaryImage { if !cinematicItemViewTypeUsePrimaryImage {
ImageView(viewModel.item.imageURL(.logo, maxHeight: 100)) ImageView(viewModel.item.imageURL(.logo, maxHeight: 100))
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.failure { .failure {

View File

@ -99,7 +99,7 @@ extension ItemView.CompactLogoScrollView {
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 10) { VStack(alignment: .center, spacing: 10) {
ImageView(viewModel.item.imageURL(.logo, maxHeight: 70)) ImageView(viewModel.item.imageURL(.logo, maxHeight: 70))
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.failure { .failure {

View File

@ -143,7 +143,7 @@ extension ItemView.CompactPosterScrollView {
ImageView(viewModel.item.imageSource(.primary, maxWidth: 130)) ImageView(viewModel.item.imageSource(.primary, maxWidth: 130))
.failure { .failure {
SystemImageContentView(systemName: viewModel.item.typeSystemImage) SystemImageContentView(systemName: viewModel.item.systemImage)
} }
.posterStyle(.portrait, contentMode: .fit) .posterStyle(.portrait, contentMode: .fit)
.frame(width: 130) .frame(width: 130)

View File

@ -68,7 +68,7 @@ extension ItemView {
content() content()
.edgePadding(.vertical) .edgePadding(.vertical)
} }
.size($globalSize) .trackingSize($globalSize)
} }
} }
} }
@ -93,7 +93,7 @@ extension ItemView.iPadOSCinematicScrollView {
maxWidth: UIScreen.main.bounds.width * 0.4, maxWidth: UIScreen.main.bounds.width * 0.4,
maxHeight: 130 maxHeight: 130
)) ))
.placeholder { .placeholder { _ in
EmptyView() EmptyView()
} }
.failure { .failure {

View File

@ -11,6 +11,8 @@ import SwiftUI
// Note: the design reason to not have a local label always on top // Note: the design reason to not have a local label always on top
// is to have the same failure/empty color for all views // is to have the same failure/empty color for all views
// TODO: why don't shadows work with failure image views?
// - due to `Color`?
extension MediaView { extension MediaView {
@ -101,7 +103,8 @@ extension MediaView {
titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash)) titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash))
} }
.failure { .failure {
ImageView.DefaultFailureView() Color.secondarySystemFill
.opacity(0.75)
.overlay { .overlay {
titleLabel titleLabel
.foregroundColor(.primary) .foregroundColor(.primary)
@ -110,6 +113,7 @@ extension MediaView {
.id(imageSources.hashValue) .id(imageSources.hashValue)
} }
.posterStyle(.landscape) .posterStyle(.landscape)
.posterShadow()
} }
.onFirstAppear(perform: setImageSources) .onFirstAppear(perform: setImageSources)
.onChange(of: useRandomImage) { _ in .onChange(of: useRandomImage) { _ in

View File

@ -19,14 +19,14 @@ extension PagingLibraryView {
private let item: Element private let item: Element
private var onSelect: () -> Void private var onSelect: () -> Void
private let posterType: PosterType private let posterType: PosterDisplayType
private func imageView(from element: Element) -> ImageView { private func imageView(from element: Element) -> ImageView {
switch posterType { switch posterType {
case .portrait:
ImageView(element.portraitPosterImageSource(maxWidth: 60))
case .landscape: case .landscape:
ImageView(element.landscapePosterImageSources(maxWidth: 110, single: false)) ImageView(element.landscapeImageSources(maxWidth: 110))
case .portrait:
ImageView(element.portraitImageSources(maxWidth: 60))
} }
} }
@ -75,13 +75,13 @@ extension PagingLibraryView {
Button { Button {
onSelect() onSelect()
} label: { } label: {
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding) { HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
ZStack { ZStack {
Color.clear Color.clear
imageView(from: item) imageView(from: item)
.failure { .failure {
SystemImageContentView(systemName: item.typeSystemImage) SystemImageContentView(systemName: item.systemImage)
} }
} }
.posterStyle(posterType) .posterStyle(posterType)
@ -122,7 +122,7 @@ extension PagingLibraryView {
extension PagingLibraryView.LibraryRow { extension PagingLibraryView.LibraryRow {
init(item: Element, posterType: PosterType) { init(item: Element, posterType: PosterDisplayType) {
self.init( self.init(
item: item, item: item,
onSelect: {}, onSelect: {},

View File

@ -16,13 +16,13 @@ extension PagingLibraryView {
@Binding @Binding
private var listColumnCount: Int private var listColumnCount: Int
@Binding @Binding
private var posterType: PosterType private var posterType: PosterDisplayType
@Binding @Binding
private var viewType: LibraryViewType private var viewType: LibraryDisplayType
init( init(
posterType: Binding<PosterType>, posterType: Binding<PosterDisplayType>,
viewType: Binding<LibraryViewType>, viewType: Binding<LibraryDisplayType>,
listColumnCount: Binding<Int> listColumnCount: Binding<Int>
) { ) {
self._listColumnCount = listColumnCount self._listColumnCount = listColumnCount
@ -62,7 +62,7 @@ extension PagingLibraryView {
if viewType == .grid { if viewType == .grid {
Label("Grid", systemImage: "checkmark") Label("Grid", systemImage: "checkmark")
} else { } else {
Label("Grid", systemImage: "square.grid.2x2") Label("Grid", systemImage: "square.grid.2x2.fill")
} }
} }
@ -83,7 +83,7 @@ extension PagingLibraryView {
} label: { } label: {
switch viewType { switch viewType {
case .grid: case .grid:
Label("Layout", systemImage: "square.grid.2x2") Label("Layout", systemImage: "square.grid.2x2.fill")
case .list: case .list:
Label("Layout", systemImage: "square.fill.text.grid.1x2") Label("Layout", systemImage: "square.fill.text.grid.1x2")
} }

View File

@ -11,6 +11,10 @@ import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: need to think about better design for views that may not support current library display type
// - ex: channels/albums when in portrait/landscape
// - just have the supported view embedded in a container view?
// Note: Currently, it is a conscious decision to not have grid posters have subtitle content. // Note: Currently, it is a conscious decision to not have grid posters have subtitle content.
// This is due to episodes, which have their `S_E_` subtitles, and these can be alongside // This is due to episodes, which have their `S_E_` subtitles, and these can be alongside
// other items that don't have a subtitle which requires the entire library to implement // other items that don't have a subtitle which requires the entire library to implement
@ -98,8 +102,8 @@ struct PagingLibraryView<Element: Poster>: View {
// MARK: layout // MARK: layout
private static func padLayout( private static func padLayout(
posterType: PosterType, posterType: PosterDisplayType,
viewType: LibraryViewType, viewType: LibraryDisplayType,
listColumnCount: Int listColumnCount: Int
) -> CollectionVGridLayout { ) -> CollectionVGridLayout {
switch (posterType, viewType) { switch (posterType, viewType) {
@ -113,8 +117,8 @@ struct PagingLibraryView<Element: Poster>: View {
} }
private static func phoneLayout( private static func phoneLayout(
posterType: PosterType, posterType: PosterDisplayType,
viewType: LibraryViewType viewType: LibraryDisplayType
) -> CollectionVGridLayout { ) -> CollectionVGridLayout {
switch (posterType, viewType) { switch (posterType, viewType) {
case (.landscape, .grid): case (.landscape, .grid):
@ -128,6 +132,9 @@ struct PagingLibraryView<Element: Poster>: View {
// MARK: item view // MARK: item view
// Note: if parent is a folders then other items will have labels,
// so an empty content view is necessary
private func landscapeGridItemView(item: Element) -> some View { private func landscapeGridItemView(item: Element) -> some View {
PosterButton(item: item, type: .landscape) PosterButton(item: item, type: .landscape)
.content { .content {
@ -135,6 +142,11 @@ struct PagingLibraryView<Element: Poster>: View {
PosterButton.TitleContentView(item: item) PosterButton.TitleContentView(item: item)
.backport .backport
.lineLimit(1, reservesSpace: true) .lineLimit(1, reservesSpace: true)
} else if viewModel.parent?.libraryType == .folder {
PosterButton.TitleContentView(item: item)
.backport
.lineLimit(1, reservesSpace: true)
.hidden()
} }
} }
.onSelect { .onSelect {
@ -149,6 +161,11 @@ struct PagingLibraryView<Element: Poster>: View {
PosterButton.TitleContentView(item: item) PosterButton.TitleContentView(item: item)
.backport .backport
.lineLimit(1, reservesSpace: true) .lineLimit(1, reservesSpace: true)
} else if viewModel.parent?.libraryType == .folder {
PosterButton.TitleContentView(item: item)
.backport
.lineLimit(1, reservesSpace: true)
.hidden()
} }
} }
.onSelect { .onSelect {
@ -291,7 +308,11 @@ struct PagingLibraryView<Element: Poster>: View {
Menu { Menu {
LibraryViewTypeToggle(posterType: $posterType, viewType: $viewType, listColumnCount: $listColumnCount) LibraryViewTypeToggle(
posterType: $posterType,
viewType: $viewType,
listColumnCount: $listColumnCount
)
Button(L10n.random, systemImage: "dice.fill") { Button(L10n.random, systemImage: "dice.fill") {
viewModel.send(.getRandomItem) viewModel.send(.getRandomItem)

View File

@ -103,7 +103,7 @@ struct ProgramsView: View {
@ViewBuilder @ViewBuilder
private func programsSection( private func programsSection(
title: String, title: String,
keyPath: KeyPath<ProgramsViewModel, [ChannelProgram]> keyPath: KeyPath<ProgramsViewModel, [BaseItemDto]>
) -> some View { ) -> some View {
PosterHStack( PosterHStack(
title: title, title: title,
@ -111,16 +111,15 @@ struct ProgramsView: View {
items: programsViewModel[keyPath: keyPath] items: programsViewModel[keyPath: keyPath]
) )
.content { .content {
ProgramButtonContent(program: $0.programs[0]) ProgramButtonContent(program: $0)
} }
.imageOverlay { .imageOverlay {
ProgramProgressOverlay(program: $0.programs[0]) ProgramProgressOverlay(program: $0)
} }
.onSelect { channelProgram in .onSelect {
guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
mainRouter.route( mainRouter.route(
to: \.liveVideoPlayer, to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource) LiveVideoPlayerManager(program: $0)
) )
} }
} }

View File

@ -110,7 +110,7 @@ struct SearchView: View {
private func itemsSection( private func itemsSection(
title: String, title: String,
keyPath: KeyPath<SearchViewModel, [BaseItemDto]>, keyPath: KeyPath<SearchViewModel, [BaseItemDto]>,
posterType: PosterType posterType: PosterDisplayType
) -> some View { ) -> some View {
PosterHStack( PosterHStack(
title: title, title: title,

View File

@ -10,6 +10,7 @@ import Stinsen
import SwiftUI import SwiftUI
struct UserSignInView: View { struct UserSignInView: View {
@EnvironmentObject @EnvironmentObject
private var router: UserSignInCoordinator.Router private var router: UserSignInCoordinator.Router

View File

@ -139,12 +139,14 @@ class UILiveNativeVideoPlayerViewController: AVPlayerViewController {
} }
private func createMetadata() -> [AVMetadataItem] { private func createMetadata() -> [AVMetadataItem] {
let allMetadata: [AVMetadataIdentifier: Any?] = [ []
.commonIdentifierTitle: videoPlayerManager.currentViewModel.item.displayTitle,
.iTunesMetadataTrackSubTitle: videoPlayerManager.currentViewModel.item.subtitle,
]
return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) } // let allMetadata: [AVMetadataIdentifier: Any?] = [
// .commonIdentifierTitle: videoPlayerManager.currentViewModel.item.displayTitle,
// .iTunesMetadataTrackSubTitle: videoPlayerManager.currentViewModel.item.subtitle,
// ]
//
// return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) }
} }
private func createMetadataItem( private func createMetadataItem(

View File

@ -53,15 +53,15 @@ extension LiveVideoPlayer.Overlay {
.tint(Color.white) .tint(Color.white)
.foregroundColor(Color.white) .foregroundColor(Color.white)
if let subtitle = viewModel.item.subtitle { // if let subtitle = viewModel.item.subtitle {
Text(subtitle) // Text(subtitle)
.font(.subheadline) // .font(.subheadline)
.foregroundColor(.white) // .foregroundColor(.white)
.alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in // .alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
dimensions[.leading] // dimensions[.leading]
} // }
.offset(y: -10) // .offset(y: -10)
} // }
} }
} }
} }

View File

@ -143,12 +143,14 @@ class UINativeVideoPlayerViewController: AVPlayerViewController {
} }
private func createMetadata() -> [AVMetadataItem] { private func createMetadata() -> [AVMetadataItem] {
let allMetadata: [AVMetadataIdentifier: Any?] = [ []
.commonIdentifierTitle: videoPlayerManager.currentViewModel.item.displayTitle,
.iTunesMetadataTrackSubTitle: videoPlayerManager.currentViewModel.item.subtitle,
]
return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) } // let allMetadata: [AVMetadataIdentifier: Any?] = [
// .commonIdentifierTitle: videoPlayerManager.currentViewModel.item.displayTitle,
// .iTunesMetadataTrackSubTitle: videoPlayerManager.currentViewModel.item.subtitle,
// ]
//
// return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) }
} }
private func createMetadataItem( private func createMetadataItem(

View File

@ -38,6 +38,9 @@ extension VideoPlayer.Overlay {
@EnvironmentObject @EnvironmentObject
private var viewModel: VideoPlayerViewModel private var viewModel: VideoPlayerViewModel
@State
private var size: CGSize = .zero
@StateObject @StateObject
private var collectionHStackProxy: CollectionHStackProxy<ChapterInfo.FullInfo> = .init() private var collectionHStackProxy: CollectionHStackProxy<ChapterInfo.FullInfo> = .init()
@ -75,7 +78,7 @@ extension VideoPlayer.Overlay {
) { chapter in ) { chapter in
ChapterButton(chapter: chapter) ChapterButton(chapter: chapter)
} }
.insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.edgePadding, vertical: EdgeInsets.edgePadding)
.proxy(collectionHStackProxy) .proxy(collectionHStackProxy)
.onChange(of: currentOverlayType) { newValue in .onChange(of: currentOverlayType) { newValue in
guard newValue == .chapters else { return } guard newValue == .chapters else { return }
@ -84,6 +87,8 @@ extension VideoPlayer.Overlay {
collectionHStackProxy.scrollTo(element: currentChapter, animated: false) collectionHStackProxy.scrollTo(element: currentChapter, animated: false)
} }
} }
.trackingSize($size)
.id(size.width)
} }
.background { .background {
LinearGradient( LinearGradient(
@ -132,20 +137,32 @@ extension VideoPlayer.Overlay.ChapterOverlay {
ZStack { ZStack {
Color.black Color.black
ImageView(chapter.landscapePosterImageSources(maxWidth: 500, single: false)) ImageView(chapter.landscapeImageSources(maxWidth: 500))
.failure { .failure {
SystemImageContentView(systemName: chapter.typeSystemImage) SystemImageContentView(systemName: chapter.systemImage)
} }
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
} }
.posterStyle(.landscape)
.overlay { .overlay {
if chapter.secondsRange.contains(currentProgressHandler.seconds) { modifier(OnSizeChangedModifier { size in
RoundedRectangle(cornerRadius: 1) if chapter.secondsRange.contains(currentProgressHandler.seconds) {
.stroke(accentColor, lineWidth: 5) RoundedRectangle(cornerRadius: size.width * (1 / 30))
.transition(.opacity.animation(.linear(duration: 0.1))) .stroke(accentColor, lineWidth: 8)
} .transition(.opacity.animation(.linear(duration: 0.1)))
.clipped()
} else {
RoundedRectangle(cornerRadius: size.width * (1 / 30))
.stroke(
.white.opacity(0.10),
lineWidth: 1.5
)
.clipped()
}
})
} }
.aspectRatio(1.77, contentMode: .fill)
.posterBorder(ratio: 1 / 30, of: \.width)
.cornerRadius(ratio: 1 / 30, of: \.width)
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text(chapter.chapterInfo.displayTitle) Text(chapter.chapterInfo.displayTitle)

View File

@ -54,15 +54,15 @@ extension VideoPlayer.Overlay {
.tint(Color.white) .tint(Color.white)
.foregroundColor(Color.white) .foregroundColor(Color.white)
if let subtitle = viewModel.item.subtitle { // if let subtitle = viewModel.item.subtitle {
Text(subtitle) // Text(subtitle)
.font(.subheadline) // .font(.subheadline)
.foregroundColor(.white) // .foregroundColor(.white)
.alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in // .alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
dimensions[.leading] // dimensions[.leading]
} // }
.offset(y: -10) // .offset(y: -10)
} // }
} }
} }
} }