Poster Display and Button Refactor (#1038)
This commit is contained in:
parent
ad8f4bbefd
commit
384e80805e
|
@ -25,7 +25,6 @@ private let imagePipeline = {
|
|||
// - 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.
|
||||
// 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 {
|
||||
|
||||
@State
|
||||
|
@ -80,7 +79,7 @@ extension ImageView {
|
|||
sources: [source].compacted(using: \.url),
|
||||
image: { $0 },
|
||||
placeholder: nil,
|
||||
failure: { DefaultFailureView() }
|
||||
failure: { EmptyView() }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -89,7 +88,7 @@ extension ImageView {
|
|||
sources: sources.compacted(using: \.url),
|
||||
image: { $0 },
|
||||
placeholder: nil,
|
||||
failure: { DefaultFailureView() }
|
||||
failure: { EmptyView() }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -98,7 +97,7 @@ extension ImageView {
|
|||
sources: [ImageSource(url: source)],
|
||||
image: { $0 },
|
||||
placeholder: nil,
|
||||
failure: { DefaultFailureView() }
|
||||
failure: { EmptyView() }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -111,7 +110,7 @@ extension ImageView {
|
|||
sources: imageSources,
|
||||
image: { $0 },
|
||||
placeholder: nil,
|
||||
failure: { DefaultFailureView() }
|
||||
failure: { EmptyView() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -124,10 +123,6 @@ extension ImageView {
|
|||
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 {
|
||||
copy(modifying: \.placeholder, with: content)
|
||||
}
|
||||
|
@ -156,9 +151,6 @@ extension ImageView {
|
|||
var body: some View {
|
||||
if let blurHash {
|
||||
BlurHashView(blurHash: blurHash, size: .Square(length: 8))
|
||||
} else {
|
||||
Color.secondarySystemFill
|
||||
.opacity(0.75)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,24 +14,24 @@ struct SystemImageContentView: View {
|
|||
|
||||
@State
|
||||
private var contentSize: CGSize = .zero
|
||||
@State
|
||||
private var labelSize: CGSize = .zero
|
||||
|
||||
private var backgroundColor: Color
|
||||
private var heightRatio: CGFloat
|
||||
private let systemName: String
|
||||
private let title: String?
|
||||
private var widthRatio: CGFloat
|
||||
|
||||
init(systemName: String?) {
|
||||
init(title: String? = nil, systemName: String?) {
|
||||
self.backgroundColor = Color.secondarySystemFill
|
||||
self.heightRatio = 3
|
||||
self.systemName = systemName ?? "circle"
|
||||
self.title = title
|
||||
self.widthRatio = 3.5
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
backgroundColor
|
||||
.opacity(0.5)
|
||||
|
||||
private var imageView: some View {
|
||||
Image(systemName: systemName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
@ -39,7 +39,33 @@ struct SystemImageContentView: View {
|
|||
.accessibilityHidden(true)
|
||||
.frame(width: contentSize.width / widthRatio, height: contentSize.height / heightRatio)
|
||||
}
|
||||
.size($contentSize)
|
||||
|
||||
@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 {
|
||||
ZStack {
|
||||
backgroundColor
|
||||
.opacity(0.5)
|
||||
|
||||
imageView
|
||||
.frame(width: contentSize.width)
|
||||
.overlay(alignment: .bottom) {
|
||||
label
|
||||
.padding(.horizontal, 4)
|
||||
.offset(y: labelSize.height)
|
||||
}
|
||||
}
|
||||
.trackingSize($contentSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ final class ItemCoordinator: NavigationCoordinatable {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -10,10 +10,10 @@ import SwiftUI
|
|||
|
||||
extension EdgeInsets {
|
||||
|
||||
// TODO: tvOS
|
||||
/// The default padding for View's against contextual edges,
|
||||
// TODO: finalize tvOS
|
||||
/// The padding for Views against contextual edges,
|
||||
/// typically the edges of the View's scene
|
||||
static let defaultEdgePadding: CGFloat = {
|
||||
static let edgePadding: CGFloat = {
|
||||
#if os(tvOS)
|
||||
50
|
||||
#else
|
||||
|
@ -25,7 +25,7 @@ extension EdgeInsets {
|
|||
#endif
|
||||
}()
|
||||
|
||||
static let DefaultEdgeInsets: EdgeInsets = .init(defaultEdgePadding)
|
||||
static let edgeInsets: EdgeInsets = .init(edgePadding)
|
||||
|
||||
init(_ constant: CGFloat) {
|
||||
self.init(top: constant, leading: constant, bottom: constant, trailing: constant)
|
||||
|
|
|
@ -11,28 +11,26 @@ import Foundation
|
|||
import JellyfinAPI
|
||||
import UIKit
|
||||
|
||||
// TODO: figure out what to do about screen scaling with .main being deprecated
|
||||
// - maxWidth assume already scaled?
|
||||
|
||||
extension BaseItemDto {
|
||||
|
||||
// MARK: Item Images
|
||||
|
||||
func imageURL(
|
||||
_ type: ImageType,
|
||||
maxWidth: Int? = nil,
|
||||
maxHeight: Int? = nil
|
||||
maxWidth: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil
|
||||
) -> URL? {
|
||||
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "")
|
||||
}
|
||||
|
||||
func imageURL(
|
||||
_ type: ImageType,
|
||||
maxWidth: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil
|
||||
) -> URL? {
|
||||
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: id ?? "")
|
||||
}
|
||||
|
||||
// TODO: will server actually only have a single blurhash per type?
|
||||
// - makes `firstBlurHash` redundant
|
||||
func blurHash(_ type: ImageType) -> String? {
|
||||
guard type != .logo else { return nil }
|
||||
|
||||
if let tag = imageTags?[type.rawValue], let taggedBlurHash = imageBlurHashes?[type]?[tag] {
|
||||
return taggedBlurHash
|
||||
} else if let firstBlurHash = imageBlurHashes?[type]?.values.first {
|
||||
|
@ -42,49 +40,62 @@ extension BaseItemDto {
|
|||
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 {
|
||||
_imageSource(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight))
|
||||
_imageSource(
|
||||
type,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Series Images
|
||||
|
||||
func seriesImageURL(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> URL? {
|
||||
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "")
|
||||
}
|
||||
|
||||
/// - 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 seriesImageURL(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> URL? {
|
||||
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: seriesID ?? "")
|
||||
}
|
||||
|
||||
func seriesImageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource {
|
||||
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "")
|
||||
return ImageSource(url: url, blurHash: nil)
|
||||
_imageURL(
|
||||
type,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
itemID: seriesID ?? "",
|
||||
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 {
|
||||
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 {
|
||||
seriesImageSource(type, maxWidth: Int(maxWidth))
|
||||
}
|
||||
// MARK: private
|
||||
|
||||
// MARK: Fileprivate
|
||||
|
||||
fileprivate func _imageURL(
|
||||
private func _imageURL(
|
||||
_ type: ImageType,
|
||||
maxWidth: Int?,
|
||||
maxHeight: Int?,
|
||||
itemID: String
|
||||
maxWidth: CGFloat?,
|
||||
maxHeight: CGFloat?,
|
||||
itemID: String,
|
||||
force: Bool = false
|
||||
) -> URL? {
|
||||
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
|
||||
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 parameters = Paths.GetItemImageParameters(
|
||||
|
@ -105,17 +116,21 @@ extension BaseItemDto {
|
|||
private func getImageTag(for type: ImageType) -> String? {
|
||||
switch type {
|
||||
case .backdrop:
|
||||
return backdropImageTags?.first
|
||||
backdropImageTags?.first
|
||||
case .screenshot:
|
||||
return screenshotImageTags?.first
|
||||
screenshotImageTags?.first
|
||||
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 blurHash = blurHash(type)
|
||||
return ImageSource(url: url, blurHash: blurHash)
|
||||
|
||||
return ImageSource(
|
||||
url: url,
|
||||
blurHash: blurHash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,94 +15,82 @@ import UIKit
|
|||
|
||||
extension BaseItemDto: Poster {
|
||||
|
||||
var title: String {
|
||||
switch type {
|
||||
case .episode:
|
||||
return seriesName ?? displayTitle
|
||||
default:
|
||||
return displayTitle
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String? {
|
||||
switch type {
|
||||
case .episode:
|
||||
return seasonEpisodeLabel
|
||||
seasonEpisodeLabel
|
||||
case .video:
|
||||
return extraType?.displayTitle
|
||||
extraType?.displayTitle
|
||||
default:
|
||||
return nil
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
var showTitle: Bool {
|
||||
switch type {
|
||||
case .episode, .series, .movie, .boxSet, .collectionFolder:
|
||||
return Defaults[.Customization.showPosterLabels]
|
||||
Defaults[.Customization.showPosterLabels]
|
||||
default:
|
||||
return true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
var typeSystemImage: String? {
|
||||
var systemImage: String {
|
||||
switch type {
|
||||
case .boxSet:
|
||||
"film.stack"
|
||||
case .channel, .tvChannel, .liveTvChannel, .program:
|
||||
"tv"
|
||||
case .episode, .movie, .series:
|
||||
"film"
|
||||
case .folder:
|
||||
"folder.fill"
|
||||
case .person:
|
||||
"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:
|
||||
return imageSource(.primary, maxWidth: maxWidth)
|
||||
"circle"
|
||||
}
|
||||
}
|
||||
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool = false) -> [ImageSource] {
|
||||
func portraitImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
|
||||
switch type {
|
||||
case .episode:
|
||||
if single || !Defaults[.Customization.Episodes.useSeriesLandscapeBackdrop] {
|
||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||
} else {
|
||||
return [
|
||||
[seriesImageSource(.primary, maxWidth: maxWidth)]
|
||||
case .channel, .tvChannel, .liveTvChannel, .movie, .series:
|
||||
[imageSource(.primary, maxWidth: maxWidth)]
|
||||
default:
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
func landscapeImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
|
||||
switch type {
|
||||
case .episode:
|
||||
if Defaults[.Customization.Episodes.useSeriesLandscapeBackdrop] {
|
||||
[
|
||||
seriesImageSource(.thumb, maxWidth: maxWidth),
|
||||
seriesImageSource(.backdrop, maxWidth: maxWidth),
|
||||
imageSource(.primary, maxWidth: maxWidth),
|
||||
]
|
||||
} else {
|
||||
[imageSource(.primary, maxWidth: maxWidth)]
|
||||
}
|
||||
case .folder:
|
||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||
case .program:
|
||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||
case .video:
|
||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||
case .folder, .program, .video:
|
||||
[imageSource(.primary, maxWidth: maxWidth)]
|
||||
default:
|
||||
return [
|
||||
[
|
||||
imageSource(.thumb, maxWidth: maxWidth),
|
||||
imageSource(.backdrop, maxWidth: maxWidth),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func cinematicPosterImageSources() -> [ImageSource] {
|
||||
func cinematicImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
|
||||
switch type {
|
||||
case .episode:
|
||||
return [seriesImageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)]
|
||||
[seriesImageSource(.backdrop, maxWidth: maxWidth)]
|
||||
default:
|
||||
return [imageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)]
|
||||
[imageSource(.backdrop, maxWidth: maxWidth)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
var alternateTitle: String? {
|
||||
originalTitle != displayTitle ? originalTitle : nil
|
||||
|
|
|
@ -17,19 +17,19 @@ extension BaseItemPerson: Poster {
|
|||
firstRole
|
||||
}
|
||||
|
||||
var showTitle: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
var typeSystemImage: String? {
|
||||
var systemImage: String {
|
||||
"person.fill"
|
||||
}
|
||||
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
||||
let scaleWidth = UIScreen.main.scale(maxWidth)
|
||||
func portraitImageSources(maxWidth: CGFloat? = nil) -> [ImageSource] {
|
||||
|
||||
// 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 imageRequestParameters = Paths.GetItemImageParameters(
|
||||
maxWidth: scaleWidth,
|
||||
maxWidth: scaleWidth ?? Int(maxWidth),
|
||||
tag: primaryImageTag
|
||||
)
|
||||
|
||||
|
@ -40,17 +40,11 @@ extension BaseItemPerson: Poster {
|
|||
)
|
||||
|
||||
let url = client.fullURL(with: imageRequest)
|
||||
let blurHash: String? = imageBlurHashes?.primary?[primaryImageTag]
|
||||
|
||||
var blurHash: String?
|
||||
|
||||
if let tag = primaryImageTag, let taggedBlurHash = imageBlurHashes?.primary?[tag] {
|
||||
blurHash = taggedBlurHash
|
||||
}
|
||||
|
||||
return ImageSource(url: url, blurHash: blurHash)
|
||||
}
|
||||
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
|
||||
[]
|
||||
return [ImageSource(
|
||||
url: url,
|
||||
blurHash: blurHash
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ extension ChapterInfo {
|
|||
chapterInfo.displayTitle
|
||||
}
|
||||
|
||||
let typeSystemImage: String? = "film"
|
||||
let systemImage: String = "film"
|
||||
var subtitle: String?
|
||||
var showTitle: Bool = true
|
||||
|
||||
|
@ -59,11 +59,7 @@ extension ChapterInfo {
|
|||
self.secondsRange = secondsRange
|
||||
}
|
||||
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
||||
.init()
|
||||
}
|
||||
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
|
||||
func landscapeImageSources(maxWidth: CGFloat?) -> [ImageSource] {
|
||||
[imageSource]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ extension JellyfinClient {
|
|||
|
||||
guard let path = request.url?.path else { return configuration.url }
|
||||
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) } ?? []
|
||||
|
||||
return components.url ?? fullPath
|
||||
|
|
|
@ -26,6 +26,6 @@ extension UserDto {
|
|||
|
||||
let profileImageURL = client.fullURL(with: request)
|
||||
|
||||
return ImageSource(url: profileImageURL, blurHash: nil)
|
||||
return ImageSource(url: profileImageURL)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ extension Backport where Content: View {
|
|||
.lineLimit(limit, reservesSpace: reservesSpace)
|
||||
} else {
|
||||
ZStack(alignment: .top) {
|
||||
Text(String(repeating: "\n", count: limit - 1))
|
||||
Text(String(repeating: " \n", count: limit))
|
||||
|
||||
content
|
||||
.lineLimit(limit)
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: remove as a `ViewModifier` and instead a wrapper view
|
||||
|
||||
struct AttributeViewModifier: ViewModifier {
|
||||
|
||||
enum Style {
|
||||
|
|
|
@ -34,7 +34,7 @@ struct BackgroundParallaxHeaderModifier<Header: View>: ViewModifier {
|
|||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.size($contentSize)
|
||||
.trackingSize($contentSize)
|
||||
.background(alignment: .top) {
|
||||
header()
|
||||
.offset(y: scrollViewOffset > 0 ? -scrollViewOffset * multiplier : 0)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct ScenePhaseChangeModifier: ViewModifier {
|
||||
struct OnScenePhaseChangedModifier: ViewModifier {
|
||||
|
||||
@Environment(\.scenePhase)
|
||||
private var scenePhase
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct AfterLastDisappearModifier: ViewModifier {
|
||||
struct SinceLastDisappearModifier: ViewModifier {
|
||||
|
||||
@State
|
||||
private var lastDisappear: Date? = nil
|
|
@ -9,22 +9,24 @@
|
|||
import SwiftUI
|
||||
|
||||
struct FramePreferenceKey: PreferenceKey {
|
||||
|
||||
static var defaultValue: CGRect = .zero
|
||||
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
|
||||
}
|
||||
|
||||
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 {
|
||||
let size: CGSize
|
||||
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 {
|
||||
|
||||
static var defaultValue: CGPoint = .zero
|
||||
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
func posterStyle(_ type: PosterType, contentMode: ContentMode = .fill) -> some View {
|
||||
func posterStyle(_ type: PosterDisplayType, contentMode: ContentMode = .fill) -> some View {
|
||||
switch type {
|
||||
case .portrait:
|
||||
aspectRatio(2 / 3, contentMode: contentMode)
|
||||
#if !os(tvOS)
|
||||
.cornerRadius(ratio: 0.0375, of: \.width)
|
||||
#endif
|
||||
case .landscape:
|
||||
aspectRatio(1.77, contentMode: contentMode)
|
||||
#if !os(tvOS)
|
||||
.posterBorder(ratio: 1 / 30, of: \.width)
|
||||
.cornerRadius(ratio: 1 / 30, of: \.width)
|
||||
#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
|
||||
@inlinable
|
||||
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
|
||||
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 {
|
||||
|
@ -152,8 +169,7 @@ extension View {
|
|||
.onPreferenceChange(FramePreferenceKey.self, perform: onChange)
|
||||
}
|
||||
|
||||
// TODO: probably rename since this doesn't set the frame but tracks it
|
||||
func frame(_ binding: Binding<CGRect>) -> some View {
|
||||
func trackingFrame(_ binding: Binding<CGRect>) -> some View {
|
||||
onFrameChanged { newFrame in
|
||||
binding.wrappedValue = newFrame
|
||||
}
|
||||
|
@ -174,8 +190,7 @@ extension View {
|
|||
.onPreferenceChange(LocationPreferenceKey.self, perform: onChange)
|
||||
}
|
||||
|
||||
// TODO: probably rename since this doesn't set the location but tracks it
|
||||
func location(_ binding: Binding<CGPoint>) -> some View {
|
||||
func trackingLocation(_ binding: Binding<CGPoint>) -> some View {
|
||||
onLocationChanged { newLocation in
|
||||
binding.wrappedValue = newLocation
|
||||
}
|
||||
|
@ -204,8 +219,7 @@ extension View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: probably rename since this doesn't set the size but tracks it
|
||||
func size(_ binding: Binding<CGSize>) -> some View {
|
||||
func trackingSize(_ binding: Binding<CGSize>) -> some View {
|
||||
onSizeChanged { newSize in
|
||||
binding.wrappedValue = newSize
|
||||
}
|
||||
|
@ -272,11 +286,11 @@ extension 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 {
|
||||
padding(edges, EdgeInsets.defaultEdgePadding)
|
||||
padding(edges, EdgeInsets.edgePadding)
|
||||
}
|
||||
|
||||
var backport: Backport<Self> {
|
||||
|
@ -293,10 +307,10 @@ extension View {
|
|||
modifier(OnFirstAppearModifier(action: action))
|
||||
}
|
||||
|
||||
/// Perform an action as a view appears given the time interval
|
||||
/// from when this view last disappeared.
|
||||
func afterLastDisappear(perform action: @escaping (TimeInterval) -> Void) -> some View {
|
||||
modifier(AfterLastDisappearModifier(action: action))
|
||||
/// Perform an action as the view appears given the time interval
|
||||
/// since the view last disappeared.
|
||||
func sinceLastDisappear(perform action: @escaping (TimeInterval) -> Void) -> some View {
|
||||
modifier(SinceLastDisappearModifier(action: action))
|
||||
}
|
||||
|
||||
func topBarTrailing(@ViewBuilder content: @escaping () -> some View) -> some View {
|
||||
|
|
|
@ -37,6 +37,9 @@ struct CaseIterablePicker<Element: CaseIterable & Displayable & Hashable>: View
|
|||
@Binding
|
||||
private var selection: Element?
|
||||
|
||||
@ViewBuilder
|
||||
private var label: (Element) -> any View
|
||||
|
||||
private let title: String
|
||||
private let hasNone: Bool
|
||||
private var noneStyle: NoneStyle
|
||||
|
@ -50,18 +53,22 @@ struct CaseIterablePicker<Element: CaseIterable & Displayable & Hashable>: View
|
|||
}
|
||||
|
||||
ForEach(Element.allCases.asArray, id: \.hashValue) {
|
||||
Text($0.displayTitle)
|
||||
label($0)
|
||||
.eraseToAnyView()
|
||||
.tag($0 as Element?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Text
|
||||
|
||||
extension CaseIterablePicker {
|
||||
|
||||
init(title: String, selection: Binding<Element?>) {
|
||||
self.init(
|
||||
selection: selection,
|
||||
label: { Text($0.displayTitle) },
|
||||
title: title,
|
||||
hasNone: true,
|
||||
noneStyle: .text
|
||||
|
@ -69,8 +76,6 @@ extension CaseIterablePicker {
|
|||
}
|
||||
|
||||
init(title: String, selection: Binding<Element>) {
|
||||
self.title = title
|
||||
|
||||
let binding = Binding<Element?> {
|
||||
selection.wrappedValue
|
||||
} set: { newValue, _ in
|
||||
|
@ -78,13 +83,48 @@ extension CaseIterablePicker {
|
|||
selection.wrappedValue = newValue!
|
||||
}
|
||||
|
||||
self._selection = binding
|
||||
|
||||
self.hasNone = false
|
||||
self.noneStyle = .text
|
||||
self.init(
|
||||
selection: binding,
|
||||
label: { Text($0.displayTitle) },
|
||||
title: title,
|
||||
hasNone: false,
|
||||
noneStyle: .text
|
||||
)
|
||||
}
|
||||
|
||||
func noneStyle(_ newStyle: NoneStyle) -> Self {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,15 +46,7 @@ extension ChannelProgram: Poster {
|
|||
channel.displayTitle
|
||||
}
|
||||
|
||||
var subtitle: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var typeSystemImage: String? {
|
||||
"tv"
|
||||
}
|
||||
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
||||
channel.imageSource(.primary, maxWidth: maxWidth)
|
||||
var systemImage: String {
|
||||
channel.systemImage
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/// A type that is displayed with a title
|
||||
protocol Displayable {
|
||||
|
||||
var displayTitle: String { get }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
|
||||
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 {
|
||||
|
||||
let url: URL?
|
||||
|
|
|
@ -10,7 +10,7 @@ import Defaults
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum LibraryViewType: String, CaseIterable, Displayable, Defaults.Serializable {
|
||||
enum LibraryDisplayType: String, CaseIterable, Displayable, Defaults.Serializable, SystemImageable {
|
||||
|
||||
case grid
|
||||
case list
|
||||
|
@ -24,4 +24,13 @@ enum LibraryViewType: String, CaseIterable, Displayable, Defaults.Serializable {
|
|||
"List"
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .grid:
|
||||
"square.grid.2x2.fill"
|
||||
case .list:
|
||||
"square.fill.text.grid.1x2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,35 +8,53 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
// TODO: remove `showTitle` and `subtitle` since the PosterButton can define custom supplementary views?
|
||||
// TODO: instead of the below image functions, have functions that match `ImageType`
|
||||
// - allows caller to choose images
|
||||
protocol Poster: Displayable, Hashable, Identifiable {
|
||||
/// A type that is displayed as a poster
|
||||
protocol Poster: Displayable, Hashable, Identifiable, SystemImageable {
|
||||
|
||||
/// Optional subtitle when used as a poster
|
||||
var subtitle: String? { get }
|
||||
var showTitle: Bool { get }
|
||||
var typeSystemImage: String? { get }
|
||||
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource]
|
||||
func cinematicPosterImageSources() -> [ImageSource]
|
||||
/// Show the title
|
||||
var showTitle: Bool { get }
|
||||
|
||||
func portraitImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource]
|
||||
|
||||
func landscapeImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource]
|
||||
|
||||
func cinematicImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource]
|
||||
}
|
||||
|
||||
extension Poster {
|
||||
|
||||
var subtitle: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var showTitle: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
||||
.init()
|
||||
}
|
||||
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
|
||||
func portraitImageSources(
|
||||
maxWidth: CGFloat? = nil
|
||||
) -> [ImageSource] {
|
||||
[]
|
||||
}
|
||||
|
||||
func cinematicPosterImageSources() -> [ImageSource] {
|
||||
func landscapeImageSources(
|
||||
maxWidth: CGFloat? = nil
|
||||
) -> [ImageSource] {
|
||||
[]
|
||||
}
|
||||
|
||||
func cinematicImageSources(
|
||||
maxWidth: CGFloat?
|
||||
) -> [ImageSource] {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -34,15 +34,15 @@ extension Defaults.Keys {
|
|||
static let itemViewType = Key<ItemViewType>("itemViewType", default: .compactLogo, suite: .generalSuite)
|
||||
|
||||
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: .generalSuite)
|
||||
static let nextUpPosterType = Key<PosterType>("nextUpPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let recentlyAddedPosterType = Key<PosterType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let nextUpPosterType = Key<PosterDisplayType>("nextUpPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let recentlyAddedPosterType = Key<PosterDisplayType>("recentlyAddedPosterType", 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 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
|
||||
static let searchPosterType = Key<PosterType>("searchPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let searchPosterType = Key<PosterDisplayType>("searchPosterType", default: .portrait, suite: .generalSuite)
|
||||
|
||||
enum CinematicItemViewType {
|
||||
|
||||
|
@ -74,12 +74,12 @@ extension Defaults.Keys {
|
|||
default: ItemFilterType.allCases,
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let viewType = Key<LibraryViewType>(
|
||||
static let viewType = Key<LibraryDisplayType>(
|
||||
"libraryViewType",
|
||||
default: .grid,
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let posterType = Key<PosterType>(
|
||||
static let posterType = Key<PosterDisplayType>(
|
||||
"libraryPosterType",
|
||||
default: .portrait,
|
||||
suite: .generalSuite
|
||||
|
|
|
@ -16,10 +16,6 @@ import UIKit
|
|||
/// Magic number for page sizes
|
||||
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
|
||||
// - 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
|
||||
|
@ -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? {
|
||||
elements.randomElement()
|
||||
}
|
||||
|
|
|
@ -10,10 +10,6 @@ import Combine
|
|||
import Foundation
|
||||
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 {
|
||||
|
||||
enum ProgramSection: CaseIterable {
|
||||
|
@ -42,34 +38,34 @@ final class ProgramsViewModel: ViewModel, Stateful {
|
|||
}
|
||||
|
||||
@Published
|
||||
private(set) var kids: [ChannelProgram] = []
|
||||
private(set) var kids: [BaseItemDto] = []
|
||||
@Published
|
||||
private(set) var movies: [ChannelProgram] = []
|
||||
private(set) var movies: [BaseItemDto] = []
|
||||
@Published
|
||||
private(set) var news: [ChannelProgram] = []
|
||||
private(set) var news: [BaseItemDto] = []
|
||||
@Published
|
||||
private(set) var recommended: [ChannelProgram] = []
|
||||
private(set) var recommended: [BaseItemDto] = []
|
||||
@Published
|
||||
private(set) var series: [ChannelProgram] = []
|
||||
private(set) var series: [BaseItemDto] = []
|
||||
@Published
|
||||
private(set) var sports: [ChannelProgram] = []
|
||||
private(set) var sports: [BaseItemDto] = []
|
||||
|
||||
@Published
|
||||
final var lastAction: Action? = nil
|
||||
@Published
|
||||
final var state: State = .initial
|
||||
|
||||
private var programChannels: [BaseItemDto] = []
|
||||
|
||||
private var currentRefreshTask: AnyCancellable?
|
||||
|
||||
var hasNoResults: Bool {
|
||||
kids.isEmpty &&
|
||||
movies.isEmpty &&
|
||||
news.isEmpty &&
|
||||
recommended.isEmpty &&
|
||||
series.isEmpty &&
|
||||
sports.isEmpty
|
||||
[
|
||||
kids,
|
||||
movies,
|
||||
news,
|
||||
recommended,
|
||||
series,
|
||||
sports,
|
||||
].allSatisfy(\.isEmpty)
|
||||
}
|
||||
|
||||
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(
|
||||
of: (ProgramSection, [BaseItemDto]).self,
|
||||
returning: [ProgramSection: [ChannelProgram]].self
|
||||
returning: [ProgramSection: [BaseItemDto]].self
|
||||
) { group in
|
||||
|
||||
// sections
|
||||
|
@ -137,18 +133,7 @@ final class ProgramsViewModel: ViewModel, Stateful {
|
|||
programs[items.0] = items.1
|
||||
}
|
||||
|
||||
// get channels for all programs at once to
|
||||
// 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
|
||||
return programs
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +143,7 @@ final class ProgramsViewModel: ViewModel, Stateful {
|
|||
parameters.fields = .MinimumFields
|
||||
.appending(.channelInfo)
|
||||
parameters.isAiring = true
|
||||
parameters.limit = 10
|
||||
parameters.limit = 20
|
||||
parameters.userID = userSession.user.id
|
||||
|
||||
let request = Paths.getRecommendedPrograms(parameters: parameters)
|
||||
|
@ -173,7 +158,7 @@ final class ProgramsViewModel: ViewModel, Stateful {
|
|||
parameters.fields = .MinimumFields
|
||||
.appending(.channelInfo)
|
||||
parameters.hasAired = false
|
||||
parameters.limit = 10
|
||||
parameters.limit = 20
|
||||
parameters.userID = userSession.user.id
|
||||
|
||||
parameters.isKids = section == .kids
|
||||
|
@ -187,19 +172,4 @@ final class ProgramsViewModel: ViewModel, Stateful {
|
|||
|
||||
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 ?? []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ final class QuickConnectViewModel: ViewModel, Stateful {
|
|||
// MARK: State
|
||||
|
||||
// The typical quick connect lifecycle is as follows:
|
||||
enum State: Equatable {
|
||||
enum State: Hashable {
|
||||
// 0. User has not interacted with quick connect
|
||||
case initial
|
||||
// 1. User clicks quick connect
|
||||
|
@ -53,6 +53,7 @@ final class QuickConnectViewModel: ViewModel, Stateful {
|
|||
|
||||
@Published
|
||||
var state: State = .initial
|
||||
var lastAction: Action? = nil
|
||||
|
||||
let client: JellyfinClient
|
||||
|
||||
|
|
|
@ -14,9 +14,10 @@ import JellyfinAPI
|
|||
import Pulse
|
||||
|
||||
final class UserSignInViewModel: ViewModel, Stateful {
|
||||
|
||||
// MARK: Action
|
||||
|
||||
enum Action {
|
||||
enum Action: Equatable {
|
||||
case signInWithUserPass(username: String, password: String)
|
||||
case signInWithQuickConnect(authSecret: String)
|
||||
case cancelSignIn
|
||||
|
@ -24,7 +25,7 @@ final class UserSignInViewModel: ViewModel, Stateful {
|
|||
|
||||
// MARK: State
|
||||
|
||||
enum State: Equatable {
|
||||
enum State: Hashable {
|
||||
case initial
|
||||
case signingIn
|
||||
case signedIn
|
||||
|
@ -38,6 +39,8 @@ final class UserSignInViewModel: ViewModel, Stateful {
|
|||
|
||||
@Published
|
||||
var state: State = .initial
|
||||
var lastAction: Action? = nil
|
||||
|
||||
@Published
|
||||
private(set) var publicUsers: [UserDto] = []
|
||||
@Published
|
||||
|
|
|
@ -10,8 +10,6 @@ import Combine
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: better name
|
||||
|
||||
struct CinematicBackgroundView<Item: Poster>: View {
|
||||
|
||||
@ObservedObject
|
||||
|
@ -26,8 +24,8 @@ struct CinematicBackgroundView<Item: Poster>: View {
|
|||
RotateContentView(proxy: proxy)
|
||||
.onChange(of: viewModel.currentItem) { newItem in
|
||||
proxy.update {
|
||||
ImageView(newItem?.cinematicPosterImageSources() ?? [])
|
||||
.placeholder {
|
||||
ImageView(newItem?.cinematicImageSources(maxWidth: nil) ?? [])
|
||||
.placeholder { _ in
|
||||
Color.clear
|
||||
}
|
||||
.failure {
|
||||
|
@ -49,6 +47,7 @@ struct CinematicBackgroundView<Item: Poster>: View {
|
|||
init() {
|
||||
currentItemSubject
|
||||
.debounce(for: 0.5, scheduler: DispatchQueue.main)
|
||||
.removeDuplicates()
|
||||
.sink { newItem in
|
||||
self.currentItem = newItem
|
||||
}
|
||||
|
@ -56,7 +55,6 @@ struct CinematicBackgroundView<Item: Poster>: View {
|
|||
}
|
||||
|
||||
func select(item: Item) {
|
||||
guard currentItem != item else { return }
|
||||
currentItemSubject.send(item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: make new protocol for cinematic view image provider
|
||||
// TODO: better name
|
||||
|
||||
struct CinematicItemSelector<Item: Poster>: View {
|
||||
|
@ -56,7 +57,10 @@ struct CinematicItemSelector<Item: Poster>: View {
|
|||
}
|
||||
.background(alignment: .top) {
|
||||
ZStack {
|
||||
CinematicBackgroundView(viewModel: viewModel, initialItem: items.first)
|
||||
CinematicBackgroundView(
|
||||
viewModel: viewModel,
|
||||
initialItem: items.first
|
||||
)
|
||||
|
||||
LinearGradient(
|
||||
stops: [
|
||||
|
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct NonePosterButton: View {
|
||||
|
||||
let type: PosterType
|
||||
let type: PosterDisplayType
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
|
|
|
@ -88,8 +88,8 @@ struct PagingLibraryView<Element: Poster>: View {
|
|||
// MARK: layout
|
||||
|
||||
private static func makeLayout(
|
||||
posterType: PosterType,
|
||||
viewType: LibraryViewType
|
||||
posterType: PosterDisplayType,
|
||||
viewType: LibraryDisplayType
|
||||
) -> CollectionVGridLayout {
|
||||
switch (posterType, viewType) {
|
||||
case (.landscape, .grid):
|
||||
|
|
|
@ -18,13 +18,12 @@ struct PosterButton<Item: Poster>: View {
|
|||
private var isFocused: Bool
|
||||
|
||||
private var item: Item
|
||||
private var type: PosterType
|
||||
private var type: PosterDisplayType
|
||||
private var horizontalAlignment: HorizontalAlignment
|
||||
private var content: () -> any View
|
||||
private var imageOverlay: () -> any View
|
||||
private var contextMenu: () -> any View
|
||||
private var onSelect: () -> Void
|
||||
private var singleImage: Bool
|
||||
|
||||
// Setting the .focused() modifier causes significant performance issues.
|
||||
// Only set if desiring focus changes
|
||||
|
@ -33,9 +32,9 @@ struct PosterButton<Item: Poster>: View {
|
|||
private func imageView(from item: Item) -> ImageView {
|
||||
switch type {
|
||||
case .portrait:
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: 500))
|
||||
ImageView(item.portraitImageSources(maxWidth: 500))
|
||||
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)
|
||||
.failure {
|
||||
SystemImageContentView(systemName: item.typeSystemImage)
|
||||
if item.showTitle {
|
||||
SystemImageContentView(systemName: item.systemImage)
|
||||
} else {
|
||||
SystemImageContentView(
|
||||
title: item.displayTitle,
|
||||
systemName: item.systemImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
imageOverlay()
|
||||
|
@ -80,7 +86,7 @@ struct PosterButton<Item: Poster>: View {
|
|||
|
||||
extension PosterButton {
|
||||
|
||||
init(item: Item, type: PosterType, singleImage: Bool = false) {
|
||||
init(item: Item, type: PosterDisplayType) {
|
||||
self.init(
|
||||
item: item,
|
||||
type: type,
|
||||
|
@ -89,7 +95,6 @@ extension PosterButton {
|
|||
imageOverlay: { DefaultOverlay(item: item) },
|
||||
contextMenu: { EmptyView() },
|
||||
onSelect: {},
|
||||
singleImage: singleImage,
|
||||
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 {
|
||||
|
||||
|
@ -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
|
||||
struct DefaultOverlay: View {
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import SwiftUI
|
|||
struct PosterHStack<Item: Poster>: View {
|
||||
|
||||
private var title: String?
|
||||
private var type: PosterType
|
||||
private var type: PosterDisplayType
|
||||
private var items: Binding<OrderedSet<Item>>
|
||||
private var content: (Item) -> any View
|
||||
private var imageOverlay: (Item) -> any View
|
||||
|
@ -58,8 +58,8 @@ struct PosterHStack<Item: Poster>: View {
|
|||
}
|
||||
.clipsToBounds(false)
|
||||
.dataPrefix(20)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: 20)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding - 20)
|
||||
.insets(horizontal: EdgeInsets.edgePadding, vertical: 20)
|
||||
.itemSpacing(EdgeInsets.edgePadding - 20)
|
||||
.scrollBehavior(.continuousLeadingEdge)
|
||||
}
|
||||
.focusSection()
|
||||
|
@ -85,7 +85,7 @@ extension PosterHStack {
|
|||
|
||||
init(
|
||||
title: String? = nil,
|
||||
type: PosterType,
|
||||
type: PosterDisplayType,
|
||||
items: Binding<OrderedSet<Item>>
|
||||
) {
|
||||
self.init(
|
||||
|
@ -103,7 +103,7 @@ extension PosterHStack {
|
|||
|
||||
init<S: Sequence<Item>>(
|
||||
title: String? = nil,
|
||||
type: PosterType,
|
||||
type: PosterDisplayType,
|
||||
items: S
|
||||
) {
|
||||
self.init(
|
||||
|
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
|
||||
struct SeeAllPosterButton: View {
|
||||
|
||||
private let type: PosterType
|
||||
private let type: PosterDisplayType
|
||||
private var onSelect: () -> Void
|
||||
|
||||
var body: some View {
|
||||
|
@ -37,7 +37,7 @@ struct SeeAllPosterButton: View {
|
|||
|
||||
extension SeeAllPosterButton {
|
||||
|
||||
init(type: PosterType) {
|
||||
init(type: PosterDisplayType) {
|
||||
self.init(
|
||||
type: type,
|
||||
onSelect: {}
|
||||
|
|
|
@ -59,7 +59,7 @@ struct ChannelLibraryView: View {
|
|||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
.afterLastDisappear { interval in
|
||||
.sinceLastDisappear { interval in
|
||||
// refresh after 3 hours
|
||||
if interval >= 10800 {
|
||||
viewModel.send(.refresh)
|
||||
|
|
|
@ -30,16 +30,16 @@ extension ChannelLibraryView {
|
|||
ZStack {
|
||||
Color.clear
|
||||
|
||||
ImageView(channel.portraitPosterImageSource(maxWidth: 110))
|
||||
ImageView(channel.portraitImageSources(maxWidth: 110))
|
||||
.image {
|
||||
$0.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
.failure {
|
||||
SystemImageContentView(systemName: channel.typeSystemImage)
|
||||
SystemImageContentView(systemName: channel.systemImage)
|
||||
.background(color: .clear)
|
||||
.imageFrameRatio(width: 1.5, height: 1.5)
|
||||
}
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ extension ChannelLibraryView {
|
|||
|
||||
@ViewBuilder
|
||||
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) {
|
||||
Text("00:00 AM")
|
||||
.monospacedDigit()
|
||||
|
@ -104,7 +104,7 @@ extension ChannelLibraryView {
|
|||
Button {
|
||||
onSelect()
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding / 2) {
|
||||
HStack(alignment: .center, spacing: EdgeInsets.edgePadding / 2) {
|
||||
|
||||
channelLogo
|
||||
.frame(width: 110)
|
||||
|
@ -127,7 +127,7 @@ extension ChannelLibraryView {
|
|||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.horizontal, EdgeInsets.defaultEdgePadding / 2)
|
||||
.padding(.horizontal, EdgeInsets.edgePadding / 2)
|
||||
}
|
||||
.buttonStyle(.card)
|
||||
.frame(height: 200)
|
||||
|
|
|
@ -39,7 +39,7 @@ extension HomeView {
|
|||
CinematicItemSelector(items: viewModel.elements.elements)
|
||||
.topContent { item in
|
||||
ImageView(itemSelectorImageSource(for: item))
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.failure {
|
||||
|
|
|
@ -39,7 +39,7 @@ extension HomeView {
|
|||
CinematicItemSelector(items: viewModel.resumeItems.elements)
|
||||
.topContent { item in
|
||||
ImageView(itemSelectorImageSource(for: item))
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.failure {
|
||||
|
@ -52,12 +52,11 @@ extension HomeView {
|
|||
.frame(height: 200, alignment: .bottomLeading)
|
||||
}
|
||||
.content { item in
|
||||
if let subtitle = item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
// TODO: clean up
|
||||
if item.type == .episode {
|
||||
PosterButton<BaseItemDto>.EpisodeContentSubtitleContent.Subtitle(item: item)
|
||||
} else {
|
||||
Text(" ")
|
||||
}
|
||||
}
|
||||
.itemImageOverlay { item in
|
||||
|
|
|
@ -61,7 +61,7 @@ struct HomeView: View {
|
|||
viewModel.send(.refresh)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.afterLastDisappear { interval in
|
||||
.sinceLastDisappear { interval in
|
||||
if interval > 60 || viewModel.notificationsReceived.contains(.itemMetadataDidChange) {
|
||||
viewModel.send(.backgroundRefresh)
|
||||
viewModel.notificationsReceived.remove(.itemMetadataDidChange)
|
||||
|
|
|
@ -23,8 +23,7 @@ extension SeriesEpisodeSelector {
|
|||
var body: some View {
|
||||
PosterButton(
|
||||
item: episode,
|
||||
type: .landscape,
|
||||
singleImage: true
|
||||
type: .landscape
|
||||
)
|
||||
.content {
|
||||
let content: String = if episode.isUnaired {
|
||||
|
|
|
@ -43,8 +43,8 @@ extension SeriesEpisodeSelector {
|
|||
.focused($focusedEpisodeID, equals: episode.id)
|
||||
}
|
||||
.scrollBehavior(.continuousLeadingEdge)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
.proxy(proxy)
|
||||
.onFirstAppear {
|
||||
guard !didScrollToPlayButtonItem else { return }
|
||||
|
@ -106,8 +106,8 @@ extension SeriesEpisodeSelector {
|
|||
}
|
||||
}
|
||||
.allowScrolling(false)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,8 +121,8 @@ extension SeriesEpisodeSelector {
|
|||
SeriesEpisodeSelector.LoadingCard()
|
||||
}
|
||||
.allowScrolling(false)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ extension ItemView {
|
|||
maxWidth: UIScreen.main.bounds.width * 0.4,
|
||||
maxHeight: 250
|
||||
))
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.failure {
|
||||
|
|
|
@ -93,7 +93,8 @@ extension MediaView {
|
|||
titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash))
|
||||
}
|
||||
.failure {
|
||||
ImageView.DefaultFailureView()
|
||||
Color.secondarySystemFill
|
||||
.opacity(0.75)
|
||||
.overlay {
|
||||
titleLabel
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
@ -54,7 +54,7 @@ struct ProgramsView: View {
|
|||
@ViewBuilder
|
||||
private func programsSection(
|
||||
title: String,
|
||||
keyPath: KeyPath<ProgramsViewModel, [ChannelProgram]>
|
||||
keyPath: KeyPath<ProgramsViewModel, [BaseItemDto]>
|
||||
) -> some View {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
|
@ -62,18 +62,18 @@ struct ProgramsView: View {
|
|||
items: programsViewModel[keyPath: keyPath]
|
||||
)
|
||||
.content {
|
||||
ProgramButtonContent(program: $0.programs[0])
|
||||
ProgramButtonContent(program: $0)
|
||||
}
|
||||
.imageOverlay {
|
||||
ProgramProgressOverlay(program: $0.programs[0])
|
||||
}
|
||||
.onSelect { channelProgram in
|
||||
guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
|
||||
router.route(
|
||||
to: \.liveVideoPlayer,
|
||||
LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource)
|
||||
)
|
||||
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)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -97,7 +97,7 @@ struct SearchView: View {
|
|||
private func itemsSection(
|
||||
title: String,
|
||||
keyPath: KeyPath<SearchViewModel, [BaseItemDto]>,
|
||||
posterType: PosterType
|
||||
posterType: PosterDisplayType
|
||||
) -> some View {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
|
|
|
@ -26,9 +26,9 @@ struct ServerListView: View {
|
|||
viewModel.servers,
|
||||
layout: .columns(
|
||||
1,
|
||||
insets: EdgeInsets.DefaultEdgeInsets,
|
||||
itemSpacing: EdgeInsets.defaultEdgePadding,
|
||||
lineSpacing: EdgeInsets.defaultEdgePadding
|
||||
insets: EdgeInsets.edgeInsets,
|
||||
itemSpacing: EdgeInsets.edgePadding,
|
||||
lineSpacing: EdgeInsets.edgePadding
|
||||
)
|
||||
) { server in
|
||||
ServerButton(server: server)
|
||||
|
|
|
@ -32,9 +32,9 @@ struct UserListView: View {
|
|||
viewModel.users,
|
||||
layout: .minWidth(
|
||||
250,
|
||||
insets: EdgeInsets.DefaultEdgeInsets,
|
||||
itemSpacing: EdgeInsets.defaultEdgePadding,
|
||||
lineSpacing: EdgeInsets.defaultEdgePadding
|
||||
insets: EdgeInsets.edgeInsets,
|
||||
itemSpacing: EdgeInsets.edgePadding,
|
||||
lineSpacing: EdgeInsets.edgePadding
|
||||
)
|
||||
) { user in
|
||||
UserProfileButton(user: user)
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */; };
|
||||
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.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 */; };
|
||||
E10231442BCF8A51009D71FC /* 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 */; };
|
||||
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.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 */; };
|
||||
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10E842B29A589860064EA49 /* NonePosterButton.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 */; };
|
||||
E1153DD02BBB634F00424D36 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DCF2BBB634F00424D36 /* 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 */; };
|
||||
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.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 */; };
|
||||
E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CC2BD2179C0058A2DF /* CollectionVGrid */; };
|
||||
E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CE2BD217AA0058A2DF /* CollectionVGrid */; };
|
||||
E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; };
|
||||
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; };
|
||||
E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */; };
|
||||
E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */; };
|
||||
E133328829538D8D00EE76AB /* 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 */; };
|
||||
|
@ -341,7 +344,7 @@
|
|||
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; };
|
||||
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.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 */; };
|
||||
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1401C9F2937DFF500E8B599 /* AppIconSelectorView.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 */; };
|
||||
E1575E72293E77B5001665B1 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8429229340B8300D1041A /* Utilities.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 */; };
|
||||
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C8CE7B28FF015000DF5D7B /* TrailingTimestampType.swift */; };
|
||||
E1575E7A293E77B5001665B1 /* TimeStampType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129428F28F0BDC300796AC6 /* TimeStampType.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 */; };
|
||||
E1575E7F293E77B5001665B1 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.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 */; };
|
||||
E15D4F0A2B1BD88900442DB8 /* 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 */; };
|
||||
E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */; };
|
||||
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 */; };
|
||||
E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; };
|
||||
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 */; };
|
||||
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.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 */; };
|
||||
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CAF6612BA363840087D991 /* UIHostingController.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 */; };
|
||||
E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.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 */; };
|
||||
E1E2F8422B757E0900B75998 /* 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 */; };
|
||||
E1E2F8462B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* AfterLastDisappearModifier.swift */; };
|
||||
E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */; };
|
||||
E1E2F8462B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */; };
|
||||
E1E306CD28EF6E8000537998 /* TimerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E306CC28EF6E8000537998 /* TimerProxy.swift */; };
|
||||
E1E5D5492783CDD700692DFE /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* VideoPlayerSettingsView.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 */; };
|
||||
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; };
|
||||
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; };
|
||||
E43918662AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */; };
|
||||
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.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 */; };
|
||||
E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */; };
|
||||
E43918672AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
@ -979,7 +984,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -999,6 +1004,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1060,7 +1067,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1083,7 +1090,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1125,6 +1132,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1237,6 +1246,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1285,7 +1295,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1334,7 +1344,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1371,9 +1381,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScenePhaseChangedModifier.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -1523,6 +1531,7 @@
|
|||
E1CAF65C2BA345830087D991 /* MediaViewModel */,
|
||||
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */,
|
||||
6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */,
|
||||
E10B1E892BD76FA900A92EAF /* QuickConnectViewModel.swift */,
|
||||
62E632DB267D2E130063E547 /* SearchViewModel.swift */,
|
||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */,
|
||||
E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */,
|
||||
|
@ -1532,7 +1541,6 @@
|
|||
BD0BA2292AD6501300306A8D /* VideoPlayerManager */,
|
||||
E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */,
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||
EA2073A02BAE35A400D8C78F /* QuickConnectViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1626,13 +1634,13 @@
|
|||
E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */,
|
||||
E14EDECA2B8FB66F000F00A4 /* ItemFilter */,
|
||||
E1C925F328875037002A7A66 /* ItemViewType.swift */,
|
||||
E13F05EB28BC9000003499D2 /* LibraryDisplayType.swift */,
|
||||
E1DE2B4E2B983F3200F6715F /* LibraryParent */,
|
||||
E13F05EB28BC9000003499D2 /* LibraryViewType.swift */,
|
||||
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
||||
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
|
||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
||||
E1937A60288F32DB00CB80AA /* Poster.swift */,
|
||||
E1CCF12D28ABF989006CAC9E /* PosterType.swift */,
|
||||
E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */,
|
||||
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
|
||||
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
|
||||
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
|
||||
|
@ -1641,6 +1649,7 @@
|
|||
E11042742B8013DF00821020 /* Stateful.swift */,
|
||||
E1EF4C402911B783008CC695 /* StreamType.swift */,
|
||||
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */,
|
||||
E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */,
|
||||
E1A1528428FD191A00600579 /* TextPair.swift */,
|
||||
E1E306CC28EF6E8000537998 /* TimerProxy.swift */,
|
||||
E129428F28F0BDC300796AC6 /* TimeStampType.swift */,
|
||||
|
@ -1923,6 +1932,7 @@
|
|||
6267B3D526710B8900A7371D /* Collection.swift */,
|
||||
E173DA5126D04AAF00CC4EB7 /* Color.swift */,
|
||||
E1B490462967E2E500D3EDCE /* CoreStore.swift */,
|
||||
E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */,
|
||||
E15756312935642A00976E1F /* Double.swift */,
|
||||
E15D4F092B1BD88900442DB8 /* Edge.swift */,
|
||||
E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */,
|
||||
|
@ -2085,18 +2095,19 @@
|
|||
path = ProgramsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E10231362BCF8A3C009D71FC /* Component */ = {
|
||||
E10231362BCF8A3C009D71FC /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */,
|
||||
E15D63EC2BD622A700AA665D /* CompactChannelView.swift */,
|
||||
E10231352BCF8A3C009D71FC /* DetailedChannelView.swift */,
|
||||
);
|
||||
path = Component;
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E10231382BCF8A3C009D71FC /* ChannelLibraryView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E10231362BCF8A3C009D71FC /* Component */,
|
||||
E10231362BCF8A3C009D71FC /* Components */,
|
||||
E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */,
|
||||
);
|
||||
path = ChannelLibraryView;
|
||||
|
@ -2326,6 +2337,7 @@
|
|||
E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */,
|
||||
E103DF932BCF31C5000229B2 /* MediaView */,
|
||||
E10231572BCF8AF8009D71FC /* ProgramsView */,
|
||||
E10B1E8C2BD7708900A92EAF /* QuickConnectView.swift */,
|
||||
E1E1643928BAC2EF00323B0A /* SearchView.swift */,
|
||||
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
||||
E193D54A271941D300900D82 /* ServerListView.swift */,
|
||||
|
@ -2333,7 +2345,6 @@
|
|||
E193D546271941C500900D82 /* UserListView.swift */,
|
||||
E193D548271941CC00900D82 /* UserSignInView.swift */,
|
||||
5310694F2684E7EE00CFFDBA /* VideoPlayer */,
|
||||
EADD26FC2BAE4A6C002F05DE /* QuickConnectView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2504,17 +2515,17 @@
|
|||
E170D101294CE4C10017224C /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1E2F8442B757E3400B75998 /* AfterLastDisappearModifier.swift */,
|
||||
E18E0202288749200022598C /* AttributeStyleModifier.swift */,
|
||||
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */,
|
||||
E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */,
|
||||
E1E2F83E2B757DFA00B75998 /* OnFinalDisappearModifier.swift */,
|
||||
E1E2F8412B757E0900B75998 /* OnFirstAppearModifier.swift */,
|
||||
E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */,
|
||||
E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */,
|
||||
E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */,
|
||||
E1DE2B4B2B98389E00F6715F /* PaletteOverlayRenderingModifier.swift */,
|
||||
E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */,
|
||||
E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */,
|
||||
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */,
|
||||
E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */,
|
||||
);
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3528,7 +3539,6 @@
|
|||
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
|
||||
E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
|
||||
E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
|
||||
EADD26FD2BAE4A6C002F05DE /* QuickConnectView.swift in Sources */,
|
||||
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
|
||||
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
|
||||
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
|
||||
|
@ -3561,6 +3571,7 @@
|
|||
E1153D9A2BBA3E9800424D36 /* ErrorCard.swift in Sources */,
|
||||
E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
|
||||
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */,
|
||||
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
|
||||
E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */,
|
||||
E1EA9F6B28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
|
||||
|
@ -3606,7 +3617,7 @@
|
|||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
||||
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */,
|
||||
E1E2F8462B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */,
|
||||
E1E2F8462B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */,
|
||||
E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */,
|
||||
E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */,
|
||||
E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */,
|
||||
|
@ -3618,22 +3629,22 @@
|
|||
E12376B02A33D6AE001F5B44 /* AboutViewCard.swift in Sources */,
|
||||
E12A9EF929499E0100731C3A /* JellyfinClient.swift in Sources */,
|
||||
E148128328C1443D003B8787 /* NameGuidPair.swift in Sources */,
|
||||
EA2073A22BAE35A400D8C78F /* QuickConnectViewModel.swift in Sources */,
|
||||
E1579EA82B97DC1500A31CA1 /* Eventful.swift in Sources */,
|
||||
E185920828CDAAA200326F80 /* SimilarItemsHStack.swift in Sources */,
|
||||
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */,
|
||||
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */,
|
||||
E10B1E8B2BD76FA900A92EAF /* QuickConnectViewModel.swift in Sources */,
|
||||
E1DC9842296DEBD800982F06 /* WatchedIndicator.swift in Sources */,
|
||||
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */,
|
||||
E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */,
|
||||
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */,
|
||||
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
||||
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */,
|
||||
E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */,
|
||||
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */,
|
||||
E43918672AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
|
||||
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
||||
E1D37F562B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */,
|
||||
E1575E75293E77B5001665B1 /* LibraryViewType.swift in Sources */,
|
||||
E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */,
|
||||
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
|
||||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
||||
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */,
|
||||
|
@ -3687,7 +3698,7 @@
|
|||
C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */,
|
||||
E1575E9F293E7B1E001665B1 /* Int.swift in Sources */,
|
||||
E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */,
|
||||
E1575E7D293E77B5001665B1 /* PosterType.swift in Sources */,
|
||||
E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */,
|
||||
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */,
|
||||
E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */,
|
||||
E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */,
|
||||
|
@ -3717,6 +3728,7 @@
|
|||
E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */,
|
||||
E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */,
|
||||
E154967C296CBB1A00C4EF88 /* FontPickerView.swift in Sources */,
|
||||
E15D63F02BD6DFC200AA665D /* SystemImageable.swift in Sources */,
|
||||
E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */,
|
||||
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
E1575E86293E7A00001665B1 /* AppIcons.swift in Sources */,
|
||||
|
@ -3788,6 +3800,7 @@
|
|||
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */,
|
||||
E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */,
|
||||
E18ACA8D2A14773500BB4F35 /* (null) in Sources */,
|
||||
E10B1E8E2BD7708900A92EAF /* QuickConnectView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -3821,7 +3834,7 @@
|
|||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||
62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */,
|
||||
E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */,
|
||||
E13316FE2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */,
|
||||
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
||||
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
|
||||
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
|
||||
|
@ -3847,7 +3860,6 @@
|
|||
E1E1644128BB301900323B0A /* Array.swift in Sources */,
|
||||
E18CE0AF28A222240092E7F1 /* PublicUserSignInView.swift in Sources */,
|
||||
E129429828F4785200796AC6 /* CaseIterablePicker.swift in Sources */,
|
||||
EA2073A12BAE35A400D8C78F /* QuickConnectViewModel.swift in Sources */,
|
||||
E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */,
|
||||
E154965E296CA2EF00C4EF88 /* DownloadTask.swift in Sources */,
|
||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||
|
@ -3859,6 +3871,7 @@
|
|||
E18E01FA288747580022598C /* AboutAppView.swift in Sources */,
|
||||
E170D103294CE8BF0017224C /* LoadingView.swift in Sources */,
|
||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */,
|
||||
E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */,
|
||||
E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */,
|
||||
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */,
|
||||
|
@ -3932,6 +3945,7 @@
|
|||
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
E10B1E8F2BD7728400A92EAF /* QuickConnectView.swift in Sources */,
|
||||
E1DD55372B6EE533007501C0 /* Task.swift in Sources */,
|
||||
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */,
|
||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
||||
|
@ -3940,7 +3954,6 @@
|
|||
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||
E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */,
|
||||
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */,
|
||||
E1DC9819296DD1CD00982F06 /* CinematicBackgroundView.swift in Sources */,
|
||||
C46DD8E72A8FA77F0046A504 /* LiveBottomBarView.swift in Sources */,
|
||||
E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */,
|
||||
|
@ -3956,10 +3969,11 @@
|
|||
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
|
||||
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
|
||||
E1401CB129386C9200E8B599 /* UIColor.swift in Sources */,
|
||||
E1E2F8452B757E3400B75998 /* AfterLastDisappearModifier.swift in Sources */,
|
||||
E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */,
|
||||
E18E01AB288746AF0022598C /* PillHStack.swift in Sources */,
|
||||
E1401CAB2938140A00E8B599 /* LightAppIcon.swift in Sources */,
|
||||
E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */,
|
||||
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */,
|
||||
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */,
|
||||
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
|
||||
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
|
||||
|
@ -3992,6 +4006,7 @@
|
|||
6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */,
|
||||
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
|
||||
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
|
||||
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */,
|
||||
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
||||
E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
||||
E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
|
||||
|
@ -4000,11 +4015,11 @@
|
|||
C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */,
|
||||
E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */,
|
||||
E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */,
|
||||
E43918662AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */,
|
||||
E43918662AD5C8310045A18C /* OnScenePhaseChangedModifier.swift in Sources */,
|
||||
E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */,
|
||||
E1B33ED128EB860A0073B0FD /* LargePlaybackButtons.swift in Sources */,
|
||||
E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */,
|
||||
E102313F2BCF8A3C009D71FC /* WideChannelGridItem.swift in Sources */,
|
||||
E102313F2BCF8A3C009D71FC /* DetailedChannelView.swift in Sources */,
|
||||
E113133228BDC72000930F75 /* FilterView.swift in Sources */,
|
||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||
E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */,
|
||||
|
@ -4020,6 +4035,7 @@
|
|||
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */,
|
||||
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
|
||||
E1D37F4E2B9CEDC400343D2B /* DeviceProfile.swift in Sources */,
|
||||
E10B1E8A2BD76FA900A92EAF /* QuickConnectViewModel.swift in Sources */,
|
||||
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */,
|
||||
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
||||
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */,
|
||||
|
@ -4040,7 +4056,7 @@
|
|||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
|
||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */,
|
||||
E1CCF12E28ABF989006CAC9E /* PosterDisplayType.swift in Sources */,
|
||||
E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */,
|
||||
E1D842912933F87500D1041A /* ItemFields.swift in Sources */,
|
||||
E1BDF2F729524ECD00CC0294 /* PlaybackSpeedActionButton.swift in Sources */,
|
||||
|
@ -4152,7 +4168,7 @@
|
|||
E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */,
|
||||
E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */,
|
||||
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
|
||||
E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */,
|
||||
E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,
|
||||
E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* SwiftfinApp.swift in Sources */,
|
||||
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
||||
|
|
|
@ -12,23 +12,26 @@ import SwiftUI
|
|||
|
||||
// TODO: expose `ImageView.image` modifier for image aspect fill/fit
|
||||
// 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 {
|
||||
|
||||
private var item: Item
|
||||
private var type: PosterType
|
||||
private var type: PosterDisplayType
|
||||
private var content: () -> any View
|
||||
private var imageOverlay: () -> any View
|
||||
private var contextMenu: () -> any View
|
||||
private var onSelect: () -> Void
|
||||
private var singleImage: Bool
|
||||
|
||||
private func imageView(from item: Item) -> ImageView {
|
||||
switch type {
|
||||
case .portrait:
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: 200))
|
||||
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)
|
||||
.failure {
|
||||
SystemImageContentView(systemName: item.typeSystemImage)
|
||||
if item.showTitle {
|
||||
SystemImageContentView(systemName: item.systemImage)
|
||||
} else {
|
||||
SystemImageContentView(
|
||||
title: item.displayTitle,
|
||||
systemName: item.systemImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
imageOverlay()
|
||||
|
@ -50,6 +60,7 @@ struct PosterButton<Item: Poster>: View {
|
|||
}
|
||||
.posterStyle(type)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu(menuItems: {
|
||||
contextMenu()
|
||||
.eraseToAnyView()
|
||||
|
@ -66,8 +77,7 @@ extension PosterButton {
|
|||
|
||||
init(
|
||||
item: Item,
|
||||
type: PosterType,
|
||||
singleImage: Bool = false
|
||||
type: PosterDisplayType
|
||||
) {
|
||||
self.init(
|
||||
item: item,
|
||||
|
@ -75,8 +85,7 @@ extension PosterButton {
|
|||
content: { TitleSubtitleContentView(item: item) },
|
||||
imageOverlay: { DefaultOverlay(item: item) },
|
||||
contextMenu: { EmptyView() },
|
||||
onSelect: {},
|
||||
singleImage: singleImage
|
||||
onSelect: {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -97,7 +106,8 @@ extension PosterButton {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Shared default content?
|
||||
// TODO: Shared default content with tvOS?
|
||||
// - check if content is generally same
|
||||
|
||||
extension PosterButton {
|
||||
|
||||
|
@ -119,7 +129,7 @@ extension PosterButton {
|
|||
let item: Item
|
||||
|
||||
var body: some View {
|
||||
Text(item.subtitle ?? "")
|
||||
Text(item.subtitle ?? " ")
|
||||
.font(.caption.weight(.medium))
|
||||
.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
|
||||
|
||||
struct DefaultOverlay: View {
|
||||
|
|
|
@ -14,9 +14,8 @@ struct PosterHStack<Item: Poster>: View {
|
|||
|
||||
private var header: () -> any View
|
||||
private var title: String?
|
||||
private var type: PosterType
|
||||
private var type: PosterDisplayType
|
||||
private var items: Binding<OrderedSet<Item>>
|
||||
private var singleImage: Bool
|
||||
private var content: (Item) -> any View
|
||||
private var imageOverlay: (Item) -> any View
|
||||
private var contextMenu: (Item) -> any View
|
||||
|
@ -31,8 +30,7 @@ struct PosterHStack<Item: Poster>: View {
|
|||
) { item in
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
singleImage: singleImage
|
||||
type: type
|
||||
)
|
||||
.content { content(item).eraseToAnyView() }
|
||||
.imageOverlay { imageOverlay(item).eraseToAnyView() }
|
||||
|
@ -41,8 +39,8 @@ struct PosterHStack<Item: Poster>: View {
|
|||
}
|
||||
.clipsToBounds(false)
|
||||
.dataPrefix(20)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
.scrollBehavior(.continuousLeadingEdge)
|
||||
}
|
||||
|
||||
|
@ -54,8 +52,7 @@ struct PosterHStack<Item: Poster>: View {
|
|||
) { item in
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
singleImage: singleImage
|
||||
type: type
|
||||
)
|
||||
.content { content(item).eraseToAnyView() }
|
||||
.imageOverlay { imageOverlay(item).eraseToAnyView() }
|
||||
|
@ -64,8 +61,8 @@ struct PosterHStack<Item: Poster>: View {
|
|||
}
|
||||
.clipsToBounds(false)
|
||||
.dataPrefix(20)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
.scrollBehavior(.continuousLeadingEdge)
|
||||
}
|
||||
|
||||
|
@ -96,16 +93,14 @@ extension PosterHStack {
|
|||
|
||||
init(
|
||||
title: String? = nil,
|
||||
type: PosterType,
|
||||
items: Binding<OrderedSet<Item>>,
|
||||
singleImage: Bool = false
|
||||
type: PosterDisplayType,
|
||||
items: Binding<OrderedSet<Item>>
|
||||
) {
|
||||
self.init(
|
||||
header: { DefaultHeader(title: title) },
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
singleImage: singleImage,
|
||||
content: { PosterButton.TitleSubtitleContentView(item: $0) },
|
||||
imageOverlay: { PosterButton.DefaultOverlay(item: $0) },
|
||||
contextMenu: { _ in EmptyView() },
|
||||
|
@ -116,15 +111,13 @@ extension PosterHStack {
|
|||
|
||||
init<S: Sequence<Item>>(
|
||||
title: String? = nil,
|
||||
type: PosterType,
|
||||
items: S,
|
||||
singleImage: Bool = false
|
||||
type: PosterDisplayType,
|
||||
items: S
|
||||
) {
|
||||
self.init(
|
||||
title: title,
|
||||
type: type,
|
||||
items: .constant(OrderedSet(items)),
|
||||
singleImage: singleImage
|
||||
items: .constant(OrderedSet(items))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,36 +12,69 @@ import Foundation
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: wide + narrow view toggling
|
||||
// - after `PosterType` has been refactored and with customizable toggle button
|
||||
// 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 {
|
||||
|
||||
@EnvironmentObject
|
||||
private var mainRouter: MainCoordinator.Router
|
||||
|
||||
@State
|
||||
private var channelDisplayType: LibraryDisplayType = .list
|
||||
@State
|
||||
private var layout: CollectionVGridLayout
|
||||
|
||||
@StateObject
|
||||
private var viewModel = ChannelLibraryViewModel()
|
||||
|
||||
// MARK: init
|
||||
|
||||
init() {
|
||||
if UIDevice.isPhone {
|
||||
layout = .columns(1)
|
||||
layout = Self.padlayout(channelDisplayType: .list)
|
||||
} else {
|
||||
layout = .minWidth(250)
|
||||
layout = Self.phonelayout(channelDisplayType: .list)
|
||||
}
|
||||
}
|
||||
|
||||
private var contentView: some View {
|
||||
CollectionVGrid(
|
||||
$viewModel.elements,
|
||||
layout: layout
|
||||
) { channel in
|
||||
WideChannelGridItem(channel: channel)
|
||||
// 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(
|
||||
|
@ -50,6 +83,30 @@ struct ChannelLibraryView: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
CollectionVGrid(
|
||||
$viewModel.elements,
|
||||
layout: $layout
|
||||
) { channel in
|
||||
switch channelDisplayType {
|
||||
case .grid:
|
||||
narrowChannelView(channel: channel)
|
||||
case .list:
|
||||
wideChannelView(channel: channel)
|
||||
}
|
||||
}
|
||||
.onReachedBottomEdge(offset: .offset(300)) {
|
||||
viewModel.send(.getNextPage)
|
||||
}
|
||||
|
@ -79,12 +136,19 @@ struct ChannelLibraryView: View {
|
|||
}
|
||||
.navigationTitle(L10n.channels)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onChange(of: channelDisplayType) { newValue in
|
||||
if UIDevice.isPhone {
|
||||
layout = Self.phonelayout(channelDisplayType: newValue)
|
||||
} else {
|
||||
layout = Self.padlayout(channelDisplayType: newValue)
|
||||
}
|
||||
}
|
||||
.onFirstAppear {
|
||||
if viewModel.state == .initial {
|
||||
viewModel.send(.refresh)
|
||||
}
|
||||
}
|
||||
.afterLastDisappear { interval in
|
||||
.sinceLastDisappear { interval in
|
||||
// refresh after 3 hours
|
||||
if interval >= 10800 {
|
||||
viewModel.send(.refresh)
|
||||
|
@ -95,6 +159,23 @@ struct ChannelLibraryView: View {
|
|||
if viewModel.backgroundStates.contains(.gettingNextPage) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ import SwiftUI
|
|||
|
||||
extension ChannelLibraryView {
|
||||
|
||||
struct WideChannelGridItem: View {
|
||||
struct DetailedChannelView: View {
|
||||
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
@ -39,21 +39,22 @@ extension ChannelLibraryView {
|
|||
.opacity(colorScheme == .dark ? 0.5 : 1)
|
||||
.posterShadow()
|
||||
|
||||
ImageView(channel.portraitPosterImageSource(maxWidth: 80))
|
||||
ImageView(channel.channel.imageSource(.primary, maxWidth: 120))
|
||||
.image {
|
||||
$0.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
.failure {
|
||||
SystemImageContentView(systemName: channel.typeSystemImage)
|
||||
SystemImageContentView(systemName: channel.systemImage)
|
||||
.background(color: .clear)
|
||||
.imageFrameRatio(width: 2, height: 2)
|
||||
}
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.padding(2)
|
||||
.padding(5)
|
||||
}
|
||||
.aspectRatio(1.0, contentMode: .fill)
|
||||
.posterBorder(ratio: 0.0375, of: \.width)
|
||||
.cornerRadius(ratio: 0.0375, of: \.width)
|
||||
|
||||
Text(channel.channel.number ?? "")
|
||||
|
@ -116,7 +117,7 @@ extension ChannelLibraryView {
|
|||
Button {
|
||||
onSelect()
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding) {
|
||||
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
|
||||
|
||||
channelLogo
|
||||
.frame(width: 80)
|
||||
|
@ -138,7 +139,7 @@ extension ChannelLibraryView {
|
|||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.size($contentSize)
|
||||
.trackingSize($contentSize)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
@ -154,7 +155,7 @@ extension ChannelLibraryView {
|
|||
}
|
||||
}
|
||||
|
||||
extension ChannelLibraryView.WideChannelGridItem {
|
||||
extension ChannelLibraryView.DetailedChannelView {
|
||||
|
||||
init(channel: ChannelProgram) {
|
||||
self.init(
|
|
@ -36,7 +36,7 @@ extension DownloadTaskView {
|
|||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
ImageView(downloadTask.item.landscapePosterImageSources(maxWidth: 600, single: true))
|
||||
ImageView(downloadTask.item.landscapeImageSources(maxWidth: 600))
|
||||
.frame(maxHeight: 300)
|
||||
.aspectRatio(1.77, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
|
|
|
@ -38,6 +38,13 @@ extension HomeView {
|
|||
columns: columnCount
|
||||
) { item in
|
||||
PosterButton(item: item, type: .landscape)
|
||||
.content {
|
||||
if item.type == .episode {
|
||||
PosterButton.EpisodeContentSubtitleContent(item: item)
|
||||
} else {
|
||||
PosterButton.TitleSubtitleContentView(item: item)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
viewModel.send(.setIsPlayed(true, item))
|
||||
|
|
|
@ -31,10 +31,11 @@ extension HomeView {
|
|||
type: nextUpPosterType,
|
||||
items: $homeViewModel.nextUpViewModel.elements
|
||||
)
|
||||
.trailing {
|
||||
SeeAllButton()
|
||||
.onSelect {
|
||||
router.route(to: \.library, homeViewModel.nextUpViewModel)
|
||||
.content { item in
|
||||
if item.type == .episode {
|
||||
PosterButton.EpisodeContentSubtitleContent(item: item)
|
||||
} else {
|
||||
PosterButton.TitleSubtitleContentView(item: item)
|
||||
}
|
||||
}
|
||||
.contextMenu { item in
|
||||
|
@ -47,6 +48,12 @@ extension HomeView {
|
|||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
.trailing {
|
||||
SeeAllButton()
|
||||
.onSelect {
|
||||
router.route(to: \.library, homeViewModel.nextUpViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ struct HomeView: View {
|
|||
.accessibilityLabel(L10n.settings)
|
||||
}
|
||||
}
|
||||
.afterLastDisappear { interval in
|
||||
.sinceLastDisappear { interval in
|
||||
if interval > 60 || viewModel.notificationsReceived.contains(.itemMetadataDidChange) {
|
||||
viewModel.send(.backgroundRefresh)
|
||||
viewModel.notificationsReceived.remove(.itemMetadataDidChange)
|
||||
|
|
|
@ -44,8 +44,7 @@ extension SeriesEpisodeSelector {
|
|||
var body: some View {
|
||||
PosterButton(
|
||||
item: episode,
|
||||
type: .landscape,
|
||||
singleImage: true
|
||||
type: .landscape
|
||||
)
|
||||
.content {
|
||||
let content: String = if episode.isUnaired {
|
||||
|
|
|
@ -36,8 +36,8 @@ extension SeriesEpisodeSelector {
|
|||
SeriesEpisodeSelector.EpisodeCard(episode: episode)
|
||||
}
|
||||
.scrollBehavior(.continuousLeadingEdge)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
.proxy(proxy)
|
||||
.onFirstAppear {
|
||||
guard !didScrollToPlayButtonItem else { return }
|
||||
|
@ -77,8 +77,8 @@ extension SeriesEpisodeSelector {
|
|||
SeriesEpisodeSelector.EmptyCard()
|
||||
}
|
||||
.allowScrolling(false)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,8 +101,8 @@ extension SeriesEpisodeSelector {
|
|||
}
|
||||
}
|
||||
.allowScrolling(false)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,8 +116,8 @@ extension SeriesEpisodeSelector {
|
|||
SeriesEpisodeSelector.LoadingCard()
|
||||
}
|
||||
.allowScrolling(false)
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding)
|
||||
.itemSpacing(EdgeInsets.defaultEdgePadding / 2)
|
||||
.insets(horizontal: EdgeInsets.edgePadding)
|
||||
.itemSpacing(EdgeInsets.edgePadding / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,9 @@ extension EpisodeItemView {
|
|||
VStack(alignment: .center) {
|
||||
ImageView(viewModel.item.imageSource(.primary, maxWidth: 600))
|
||||
.frame(maxHeight: 300)
|
||||
.aspectRatio(1.77, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
.padding(.horizontal)
|
||||
.posterStyle(.landscape)
|
||||
.posterShadow()
|
||||
.padding(.horizontal)
|
||||
|
||||
ShelfView(viewModel: viewModel)
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ extension ItemView.CinematicScrollView {
|
|||
VStack(alignment: .center, spacing: 10) {
|
||||
if !cinematicItemViewTypeUsePrimaryImage {
|
||||
ImageView(viewModel.item.imageURL(.logo, maxHeight: 100))
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.failure {
|
||||
|
|
|
@ -99,7 +99,7 @@ extension ItemView.CompactLogoScrollView {
|
|||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
ImageView(viewModel.item.imageURL(.logo, maxHeight: 70))
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.failure {
|
||||
|
|
|
@ -143,7 +143,7 @@ extension ItemView.CompactPosterScrollView {
|
|||
|
||||
ImageView(viewModel.item.imageSource(.primary, maxWidth: 130))
|
||||
.failure {
|
||||
SystemImageContentView(systemName: viewModel.item.typeSystemImage)
|
||||
SystemImageContentView(systemName: viewModel.item.systemImage)
|
||||
}
|
||||
.posterStyle(.portrait, contentMode: .fit)
|
||||
.frame(width: 130)
|
||||
|
|
|
@ -68,7 +68,7 @@ extension ItemView {
|
|||
content()
|
||||
.edgePadding(.vertical)
|
||||
}
|
||||
.size($globalSize)
|
||||
.trackingSize($globalSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ extension ItemView.iPadOSCinematicScrollView {
|
|||
maxWidth: UIScreen.main.bounds.width * 0.4,
|
||||
maxHeight: 130
|
||||
))
|
||||
.placeholder {
|
||||
.placeholder { _ in
|
||||
EmptyView()
|
||||
}
|
||||
.failure {
|
||||
|
|
|
@ -11,6 +11,8 @@ import SwiftUI
|
|||
|
||||
// Note: the design reason to not have a local label always on top
|
||||
// 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 {
|
||||
|
||||
|
@ -101,7 +103,8 @@ extension MediaView {
|
|||
titleLabelOverlay(with: ImageView.DefaultPlaceholderView(blurHash: imageSource.blurHash))
|
||||
}
|
||||
.failure {
|
||||
ImageView.DefaultFailureView()
|
||||
Color.secondarySystemFill
|
||||
.opacity(0.75)
|
||||
.overlay {
|
||||
titleLabel
|
||||
.foregroundColor(.primary)
|
||||
|
@ -110,6 +113,7 @@ extension MediaView {
|
|||
.id(imageSources.hashValue)
|
||||
}
|
||||
.posterStyle(.landscape)
|
||||
.posterShadow()
|
||||
}
|
||||
.onFirstAppear(perform: setImageSources)
|
||||
.onChange(of: useRandomImage) { _ in
|
||||
|
|
|
@ -19,14 +19,14 @@ extension PagingLibraryView {
|
|||
|
||||
private let item: Element
|
||||
private var onSelect: () -> Void
|
||||
private let posterType: PosterType
|
||||
private let posterType: PosterDisplayType
|
||||
|
||||
private func imageView(from element: Element) -> ImageView {
|
||||
switch posterType {
|
||||
case .portrait:
|
||||
ImageView(element.portraitPosterImageSource(maxWidth: 60))
|
||||
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 {
|
||||
onSelect()
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding) {
|
||||
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
|
||||
ZStack {
|
||||
Color.clear
|
||||
|
||||
imageView(from: item)
|
||||
.failure {
|
||||
SystemImageContentView(systemName: item.typeSystemImage)
|
||||
SystemImageContentView(systemName: item.systemImage)
|
||||
}
|
||||
}
|
||||
.posterStyle(posterType)
|
||||
|
@ -122,7 +122,7 @@ extension PagingLibraryView {
|
|||
|
||||
extension PagingLibraryView.LibraryRow {
|
||||
|
||||
init(item: Element, posterType: PosterType) {
|
||||
init(item: Element, posterType: PosterDisplayType) {
|
||||
self.init(
|
||||
item: item,
|
||||
onSelect: {},
|
||||
|
|
|
@ -16,13 +16,13 @@ extension PagingLibraryView {
|
|||
@Binding
|
||||
private var listColumnCount: Int
|
||||
@Binding
|
||||
private var posterType: PosterType
|
||||
private var posterType: PosterDisplayType
|
||||
@Binding
|
||||
private var viewType: LibraryViewType
|
||||
private var viewType: LibraryDisplayType
|
||||
|
||||
init(
|
||||
posterType: Binding<PosterType>,
|
||||
viewType: Binding<LibraryViewType>,
|
||||
posterType: Binding<PosterDisplayType>,
|
||||
viewType: Binding<LibraryDisplayType>,
|
||||
listColumnCount: Binding<Int>
|
||||
) {
|
||||
self._listColumnCount = listColumnCount
|
||||
|
@ -62,7 +62,7 @@ extension PagingLibraryView {
|
|||
if viewType == .grid {
|
||||
Label("Grid", systemImage: "checkmark")
|
||||
} else {
|
||||
Label("Grid", systemImage: "square.grid.2x2")
|
||||
Label("Grid", systemImage: "square.grid.2x2.fill")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ extension PagingLibraryView {
|
|||
} label: {
|
||||
switch viewType {
|
||||
case .grid:
|
||||
Label("Layout", systemImage: "square.grid.2x2")
|
||||
Label("Layout", systemImage: "square.grid.2x2.fill")
|
||||
case .list:
|
||||
Label("Layout", systemImage: "square.fill.text.grid.1x2")
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ import Defaults
|
|||
import JellyfinAPI
|
||||
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.
|
||||
// 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
|
||||
|
@ -98,8 +102,8 @@ struct PagingLibraryView<Element: Poster>: View {
|
|||
// MARK: layout
|
||||
|
||||
private static func padLayout(
|
||||
posterType: PosterType,
|
||||
viewType: LibraryViewType,
|
||||
posterType: PosterDisplayType,
|
||||
viewType: LibraryDisplayType,
|
||||
listColumnCount: Int
|
||||
) -> CollectionVGridLayout {
|
||||
switch (posterType, viewType) {
|
||||
|
@ -113,8 +117,8 @@ struct PagingLibraryView<Element: Poster>: View {
|
|||
}
|
||||
|
||||
private static func phoneLayout(
|
||||
posterType: PosterType,
|
||||
viewType: LibraryViewType
|
||||
posterType: PosterDisplayType,
|
||||
viewType: LibraryDisplayType
|
||||
) -> CollectionVGridLayout {
|
||||
switch (posterType, viewType) {
|
||||
case (.landscape, .grid):
|
||||
|
@ -128,6 +132,9 @@ struct PagingLibraryView<Element: Poster>: 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 {
|
||||
PosterButton(item: item, type: .landscape)
|
||||
.content {
|
||||
|
@ -135,6 +142,11 @@ struct PagingLibraryView<Element: Poster>: View {
|
|||
PosterButton.TitleContentView(item: item)
|
||||
.backport
|
||||
.lineLimit(1, reservesSpace: true)
|
||||
} else if viewModel.parent?.libraryType == .folder {
|
||||
PosterButton.TitleContentView(item: item)
|
||||
.backport
|
||||
.lineLimit(1, reservesSpace: true)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
.onSelect {
|
||||
|
@ -149,6 +161,11 @@ struct PagingLibraryView<Element: Poster>: View {
|
|||
PosterButton.TitleContentView(item: item)
|
||||
.backport
|
||||
.lineLimit(1, reservesSpace: true)
|
||||
} else if viewModel.parent?.libraryType == .folder {
|
||||
PosterButton.TitleContentView(item: item)
|
||||
.backport
|
||||
.lineLimit(1, reservesSpace: true)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
.onSelect {
|
||||
|
@ -291,7 +308,11 @@ struct PagingLibraryView<Element: Poster>: View {
|
|||
|
||||
Menu {
|
||||
|
||||
LibraryViewTypeToggle(posterType: $posterType, viewType: $viewType, listColumnCount: $listColumnCount)
|
||||
LibraryViewTypeToggle(
|
||||
posterType: $posterType,
|
||||
viewType: $viewType,
|
||||
listColumnCount: $listColumnCount
|
||||
)
|
||||
|
||||
Button(L10n.random, systemImage: "dice.fill") {
|
||||
viewModel.send(.getRandomItem)
|
||||
|
|
|
@ -103,7 +103,7 @@ struct ProgramsView: View {
|
|||
@ViewBuilder
|
||||
private func programsSection(
|
||||
title: String,
|
||||
keyPath: KeyPath<ProgramsViewModel, [ChannelProgram]>
|
||||
keyPath: KeyPath<ProgramsViewModel, [BaseItemDto]>
|
||||
) -> some View {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
|
@ -111,16 +111,15 @@ struct ProgramsView: View {
|
|||
items: programsViewModel[keyPath: keyPath]
|
||||
)
|
||||
.content {
|
||||
ProgramButtonContent(program: $0.programs[0])
|
||||
ProgramButtonContent(program: $0)
|
||||
}
|
||||
.imageOverlay {
|
||||
ProgramProgressOverlay(program: $0.programs[0])
|
||||
ProgramProgressOverlay(program: $0)
|
||||
}
|
||||
.onSelect { channelProgram in
|
||||
guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
|
||||
.onSelect {
|
||||
mainRouter.route(
|
||||
to: \.liveVideoPlayer,
|
||||
LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource)
|
||||
LiveVideoPlayerManager(program: $0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ struct SearchView: View {
|
|||
private func itemsSection(
|
||||
title: String,
|
||||
keyPath: KeyPath<SearchViewModel, [BaseItemDto]>,
|
||||
posterType: PosterType
|
||||
posterType: PosterDisplayType
|
||||
) -> some View {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
|
|
|
@ -10,6 +10,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct UserSignInView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: UserSignInCoordinator.Router
|
||||
|
||||
|
|
|
@ -139,12 +139,14 @@ class UILiveNativeVideoPlayerViewController: AVPlayerViewController {
|
|||
}
|
||||
|
||||
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(
|
||||
|
|
|
@ -53,15 +53,15 @@ extension LiveVideoPlayer.Overlay {
|
|||
.tint(Color.white)
|
||||
.foregroundColor(Color.white)
|
||||
|
||||
if let subtitle = viewModel.item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
.alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
|
||||
dimensions[.leading]
|
||||
}
|
||||
.offset(y: -10)
|
||||
}
|
||||
// if let subtitle = viewModel.item.subtitle {
|
||||
// Text(subtitle)
|
||||
// .font(.subheadline)
|
||||
// .foregroundColor(.white)
|
||||
// .alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
|
||||
// dimensions[.leading]
|
||||
// }
|
||||
// .offset(y: -10)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,12 +143,14 @@ class UINativeVideoPlayerViewController: AVPlayerViewController {
|
|||
}
|
||||
|
||||
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(
|
||||
|
|
|
@ -38,6 +38,9 @@ extension VideoPlayer.Overlay {
|
|||
@EnvironmentObject
|
||||
private var viewModel: VideoPlayerViewModel
|
||||
|
||||
@State
|
||||
private var size: CGSize = .zero
|
||||
|
||||
@StateObject
|
||||
private var collectionHStackProxy: CollectionHStackProxy<ChapterInfo.FullInfo> = .init()
|
||||
|
||||
|
@ -75,7 +78,7 @@ extension VideoPlayer.Overlay {
|
|||
) { chapter in
|
||||
ChapterButton(chapter: chapter)
|
||||
}
|
||||
.insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: EdgeInsets.defaultEdgePadding)
|
||||
.insets(horizontal: EdgeInsets.edgePadding, vertical: EdgeInsets.edgePadding)
|
||||
.proxy(collectionHStackProxy)
|
||||
.onChange(of: currentOverlayType) { newValue in
|
||||
guard newValue == .chapters else { return }
|
||||
|
@ -84,6 +87,8 @@ extension VideoPlayer.Overlay {
|
|||
collectionHStackProxy.scrollTo(element: currentChapter, animated: false)
|
||||
}
|
||||
}
|
||||
.trackingSize($size)
|
||||
.id(size.width)
|
||||
}
|
||||
.background {
|
||||
LinearGradient(
|
||||
|
@ -132,20 +137,32 @@ extension VideoPlayer.Overlay.ChapterOverlay {
|
|||
ZStack {
|
||||
Color.black
|
||||
|
||||
ImageView(chapter.landscapePosterImageSources(maxWidth: 500, single: false))
|
||||
ImageView(chapter.landscapeImageSources(maxWidth: 500))
|
||||
.failure {
|
||||
SystemImageContentView(systemName: chapter.typeSystemImage)
|
||||
SystemImageContentView(systemName: chapter.systemImage)
|
||||
}
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
.posterStyle(.landscape)
|
||||
.overlay {
|
||||
modifier(OnSizeChangedModifier { size in
|
||||
if chapter.secondsRange.contains(currentProgressHandler.seconds) {
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.stroke(accentColor, lineWidth: 5)
|
||||
RoundedRectangle(cornerRadius: size.width * (1 / 30))
|
||||
.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) {
|
||||
Text(chapter.chapterInfo.displayTitle)
|
||||
|
|
|
@ -54,15 +54,15 @@ extension VideoPlayer.Overlay {
|
|||
.tint(Color.white)
|
||||
.foregroundColor(Color.white)
|
||||
|
||||
if let subtitle = viewModel.item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white)
|
||||
.alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
|
||||
dimensions[.leading]
|
||||
}
|
||||
.offset(y: -10)
|
||||
}
|
||||
// if let subtitle = viewModel.item.subtitle {
|
||||
// Text(subtitle)
|
||||
// .font(.subheadline)
|
||||
// .foregroundColor(.white)
|
||||
// .alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
|
||||
// dimensions[.leading]
|
||||
// }
|
||||
// .offset(y: -10)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue