Remove iOS `PosterButtonType` + cleanup (#883)
This commit is contained in:
parent
5a407410fb
commit
9266d53ae0
|
@ -9,6 +9,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// TODO: Look at name spacing
|
// TODO: Look at name spacing
|
||||||
|
// TODO: Consistent naming: ...Key
|
||||||
|
|
||||||
struct AudioOffset: EnvironmentKey {
|
struct AudioOffset: EnvironmentKey {
|
||||||
static let defaultValue: Binding<Int> = .constant(0)
|
static let defaultValue: Binding<Int> = .constant(0)
|
||||||
|
|
|
@ -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) 2023 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PaddingMultiplierModifier: ViewModifier {
|
||||||
|
|
||||||
|
let edges: Edge.Set
|
||||||
|
let multiplier: Int
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.if(multiplier > 0) { view in
|
||||||
|
view.padding()
|
||||||
|
.padding(multiplier: multiplier - 1, edges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// 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) 2023 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,57 +50,32 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Simplify plethora of calls
|
// TODO: Don't apply corner radius on tvOS because buttons handle themselves, add new modifier for setting corner radius of poster type
|
||||||
// TODO: Centralize math
|
@ViewBuilder
|
||||||
// TODO: Move poster stuff to own file
|
func posterStyle(_ type: PosterType) -> some View {
|
||||||
// TODO: Figure out proper handling of corner radius for tvOS buttons
|
switch type {
|
||||||
func posterStyle(type: PosterType, width: CGFloat) -> some View {
|
case .portrait:
|
||||||
Group {
|
aspectRatio(2 / 3, contentMode: .fit)
|
||||||
switch type {
|
.cornerRadius(ratio: 0.0375, of: \.width)
|
||||||
case .portrait:
|
case .landscape:
|
||||||
self.portraitPoster(width: width)
|
aspectRatio(1.77, contentMode: .fit)
|
||||||
case .landscape:
|
.cornerRadius(ratio: 1 / 30, of: \.width)
|
||||||
self.landscapePoster(width: width)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func posterStyle(type: PosterType, height: CGFloat) -> some View {
|
// TODO: switch to padding(multiplier: 2)
|
||||||
Group {
|
|
||||||
switch type {
|
|
||||||
case .portrait:
|
|
||||||
self.portraitPoster(height: height)
|
|
||||||
case .landscape:
|
|
||||||
self.landscapePoster(height: height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func portraitPoster(width: CGFloat) -> some View {
|
|
||||||
frame(width: width, height: width * 1.5)
|
|
||||||
.cornerRadius((width * 1.5) / 40)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func landscapePoster(width: CGFloat) -> some View {
|
|
||||||
frame(width: width, height: width / 1.77)
|
|
||||||
#if !os(tvOS)
|
|
||||||
.cornerRadius(width / 30)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private func portraitPoster(height: CGFloat) -> some View {
|
|
||||||
portraitPoster(width: height / 1.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func landscapePoster(height: CGFloat) -> some View {
|
|
||||||
landscapePoster(width: height * 1.77)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
func padding2(_ edges: Edge.Set = .all) -> some View {
|
func padding2(_ edges: Edge.Set = .all) -> some View {
|
||||||
padding(edges).padding(edges)
|
padding(edges).padding(edges)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies the default system padding a number of times with a multiplier
|
||||||
|
func padding(multiplier: Int, _ edges: Edge.Set = .all) -> some View {
|
||||||
|
precondition(multiplier > 0, "Multiplier must be > 0")
|
||||||
|
|
||||||
|
return modifier(PaddingMultiplierModifier(edges: edges, multiplier: multiplier))
|
||||||
|
}
|
||||||
|
|
||||||
func scrollViewOffset(_ scrollViewOffset: Binding<CGFloat>) -> some View {
|
func scrollViewOffset(_ scrollViewOffset: Binding<CGFloat>) -> some View {
|
||||||
modifier(ScrollViewOffsetModifier(scrollViewOffset: scrollViewOffset))
|
modifier(ScrollViewOffsetModifier(scrollViewOffset: scrollViewOffset))
|
||||||
}
|
}
|
||||||
|
@ -126,6 +101,11 @@ extension View {
|
||||||
clipShape(RoundedCorner(radius: radius, corners: corners))
|
clipShape(RoundedCorner(radius: radius, corners: corners))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a corner radius as a ratio of a side of the view's size
|
||||||
|
func cornerRadius(ratio: CGFloat, of side: KeyPath<CGSize, CGFloat>, corners: UIRectCorner = .allCorners) -> some View {
|
||||||
|
modifier(RatioCornerRadiusModifier(corners: corners, ratio: ratio, side: side))
|
||||||
|
}
|
||||||
|
|
||||||
func onFrameChanged(_ onChange: @escaping (CGRect) -> Void) -> some View {
|
func onFrameChanged(_ onChange: @escaping (CGRect) -> Void) -> some View {
|
||||||
background {
|
background {
|
||||||
GeometryReader { reader in
|
GeometryReader { reader in
|
||||||
|
@ -174,6 +154,7 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: rename isVisible
|
||||||
@inlinable
|
@inlinable
|
||||||
func visible(_ isVisible: Bool) -> some View {
|
func visible(_ isVisible: Bool) -> some View {
|
||||||
opacity(isVisible ? 1 : 0)
|
opacity(isVisible ? 1 : 0)
|
||||||
|
|
|
@ -15,7 +15,7 @@ protocol MenuPosterHStackModel: ObservableObject {
|
||||||
associatedtype Item: Poster
|
associatedtype Item: Poster
|
||||||
|
|
||||||
var menuSelection: Section? { get }
|
var menuSelection: Section? { get }
|
||||||
var menuSections: [Section: [PosterButtonType<Item>]] { get set }
|
var menuSections: [Section: [Item]] { get set }
|
||||||
var menuSectionSort: (Section, Section) -> Bool { get }
|
var menuSectionSort: (Section, Section) -> Bool { get }
|
||||||
|
|
||||||
func select(section: Section)
|
func select(section: Section)
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: find way to remove special `single` handling
|
||||||
|
// TODO: remove `showTitle` and `subtitle` since the PosterButton can define custom supplementary views?
|
||||||
protocol Poster: Displayable, Hashable {
|
protocol Poster: Displayable, Hashable {
|
||||||
|
|
||||||
var subtitle: String? { get }
|
var subtitle: String? { get }
|
||||||
|
@ -19,10 +21,6 @@ protocol Poster: Displayable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Poster {
|
extension Poster {
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(displayTitle)
|
|
||||||
hasher.combine(subtitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cinematicPosterImageSources() -> [ImageSource] {
|
func cinematicPosterImageSources() -> [ImageSource] {
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -1,36 +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) 2023 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
// TODO: Replace with better mechanism
|
|
||||||
|
|
||||||
enum PosterButtonType<Item: Poster>: Hashable, Identifiable {
|
|
||||||
|
|
||||||
case loading
|
|
||||||
case noResult
|
|
||||||
case item(Item)
|
|
||||||
|
|
||||||
var id: Int {
|
|
||||||
switch self {
|
|
||||||
case .loading, .noResult:
|
|
||||||
return UUID().hashValue
|
|
||||||
case let .item(item):
|
|
||||||
return item.hashValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _item: Item? {
|
|
||||||
switch self {
|
|
||||||
case let .item(item):
|
|
||||||
return item
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,7 +17,7 @@ final class SeriesItemViewModel: ItemViewModel, MenuPosterHStackModel {
|
||||||
@Published
|
@Published
|
||||||
var menuSelection: BaseItemDto?
|
var menuSelection: BaseItemDto?
|
||||||
@Published
|
@Published
|
||||||
var menuSections: [BaseItemDto: [PosterButtonType<BaseItemDto>]]
|
var menuSections: [BaseItemDto: [BaseItemDto]]
|
||||||
var menuSectionSort: (BaseItemDto, BaseItemDto) -> Bool
|
var menuSectionSort: (BaseItemDto, BaseItemDto) -> Bool
|
||||||
|
|
||||||
override init(item: BaseItemDto) {
|
override init(item: BaseItemDto) {
|
||||||
|
@ -117,14 +117,7 @@ final class SeriesItemViewModel: ItemViewModel, MenuPosterHStackModel {
|
||||||
func select(section: BaseItemDto) {
|
func select(section: BaseItemDto) {
|
||||||
self.menuSelection = section
|
self.menuSelection = section
|
||||||
|
|
||||||
if let existingItems = menuSections[section] {
|
if !menuSections.keys.contains(section) {
|
||||||
if existingItems.allSatisfy({ $0 == .loading }) {
|
|
||||||
getEpisodesForSeason(section)
|
|
||||||
} else if existingItems.allSatisfy({ $0 == .noResult }) {
|
|
||||||
menuSections[section] = PosterButtonType.loading.random(in: 3 ..< 8)
|
|
||||||
getEpisodesForSeason(section)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
getEpisodesForSeason(section)
|
getEpisodesForSeason(section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,12 +133,6 @@ final class SeriesItemViewModel: ItemViewModel, MenuPosterHStackModel {
|
||||||
|
|
||||||
guard let seasons = response.value.items else { return }
|
guard let seasons = response.value.items else { return }
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
seasons.forEach { season in
|
|
||||||
self.menuSections[season] = PosterButtonType.loading.random(in: 3 ..< 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let firstSeason = seasons.first {
|
if let firstSeason = seasons.first {
|
||||||
self.getEpisodesForSeason(firstSeason)
|
self.getEpisodesForSeason(firstSeason)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
@ -169,9 +156,7 @@ final class SeriesItemViewModel: ItemViewModel, MenuPosterHStackModel {
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
if let items = response.value.items {
|
if let items = response.value.items {
|
||||||
self.menuSections[season] = items.map { .item($0) }
|
self.menuSections[season] = items
|
||||||
} else {
|
|
||||||
self.menuSections[season] = [.noResult]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,10 @@ class SpecialFeaturesViewModel: ViewModel, MenuPosterHStackModel {
|
||||||
@Published
|
@Published
|
||||||
var menuSelection: SpecialFeatureType?
|
var menuSelection: SpecialFeatureType?
|
||||||
@Published
|
@Published
|
||||||
var menuSections: [SpecialFeatureType: [PosterButtonType<BaseItemDto>]]
|
var menuSections: [SpecialFeatureType: [BaseItemDto]]
|
||||||
var menuSectionSort: (SpecialFeatureType, SpecialFeatureType) -> Bool
|
var menuSectionSort: (SpecialFeatureType, SpecialFeatureType) -> Bool
|
||||||
|
|
||||||
init(sections: [SpecialFeatureType: [PosterButtonType<BaseItemDto>]]) {
|
init(sections: [SpecialFeatureType: [BaseItemDto]]) {
|
||||||
let comparator: (SpecialFeatureType, SpecialFeatureType) -> Bool = { i, j in i.rawValue < j.rawValue }
|
let comparator: (SpecialFeatureType, SpecialFeatureType) -> Bool = { i, j in i.rawValue < j.rawValue }
|
||||||
self.menuSelection = Array(sections.keys).sorted(by: comparator).first!
|
self.menuSelection = Array(sections.keys).sorted(by: comparator).first!
|
||||||
self.menuSections = sections
|
self.menuSections = sections
|
||||||
|
|
|
@ -29,7 +29,8 @@ struct NonePosterButton: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.posterStyle(type: type, width: type.width)
|
.posterStyle(type)
|
||||||
|
.frame(width: type.width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.card)
|
.buttonStyle(.card)
|
||||||
|
|
|
@ -47,19 +47,20 @@ struct PosterButton<Item: Poster>: View {
|
||||||
.failure {
|
.failure {
|
||||||
InitialFailureView(item.displayTitle.initials)
|
InitialFailureView(item.displayTitle.initials)
|
||||||
}
|
}
|
||||||
.posterStyle(type: type, width: itemWidth)
|
|
||||||
case .landscape:
|
case .landscape:
|
||||||
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
||||||
.failure {
|
.failure {
|
||||||
InitialFailureView(item.displayTitle.initials)
|
InitialFailureView(item.displayTitle.initials)
|
||||||
}
|
}
|
||||||
.posterStyle(type: type, width: itemWidth)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.posterStyle(type)
|
||||||
|
.frame(width: itemWidth)
|
||||||
.overlay {
|
.overlay {
|
||||||
imageOverlay()
|
imageOverlay()
|
||||||
.eraseToAnyView()
|
.eraseToAnyView()
|
||||||
.posterStyle(type: type, width: itemWidth)
|
.posterStyle(type)
|
||||||
|
.frame(width: itemWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.card)
|
.buttonStyle(.card)
|
||||||
|
|
|
@ -29,7 +29,8 @@ struct SeeAllPosterButton: View {
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.posterStyle(type: type, width: type.width)
|
.posterStyle(type)
|
||||||
|
.frame(width: type.width)
|
||||||
}
|
}
|
||||||
.buttonStyle(.card)
|
.buttonStyle(.card)
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@ extension SeriesEpisodeSelector {
|
||||||
private var items: [BaseItemDto] {
|
private var items: [BaseItemDto] {
|
||||||
guard let selection = viewModel.menuSelection,
|
guard let selection = viewModel.menuSelection,
|
||||||
let items = viewModel.menuSections[selection] else { return [.noResults] }
|
let items = viewModel.menuSections[selection] else { return [.noResults] }
|
||||||
return items.compactMap(\._item)
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
@ -93,7 +93,8 @@ extension MediaView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.posterStyle(type: .landscape, width: itemWidth)
|
.posterStyle(.landscape)
|
||||||
|
.frame(width: itemWidth)
|
||||||
}
|
}
|
||||||
.buttonStyle(.card)
|
.buttonStyle(.card)
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,6 +269,10 @@
|
||||||
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F229638B140022FAC9 /* ChevronButton.swift */; };
|
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F229638B140022FAC9 /* ChevronButton.swift */; };
|
||||||
E12E30F5296392EC0022FAC9 /* EnumPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */; };
|
E12E30F5296392EC0022FAC9 /* EnumPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */; };
|
||||||
E12F038C28F8B0B100976CC3 /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */; };
|
E12F038C28F8B0B100976CC3 /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */; };
|
||||||
|
E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; };
|
||||||
|
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; };
|
||||||
|
E13317012ADE4B8D009BF865 /* PaddingMultiplierModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13317002ADE4B8D009BF865 /* PaddingMultiplierModifier.swift */; };
|
||||||
|
E13317022ADE4B8D009BF865 /* PaddingMultiplierModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13317002ADE4B8D009BF865 /* PaddingMultiplierModifier.swift */; };
|
||||||
E133328829538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; };
|
E133328829538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; };
|
||||||
E133328929538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; };
|
E133328929538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; };
|
||||||
E133328D2953AE4B00EE76AB /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328C2953AE4B00EE76AB /* CircularProgressView.swift */; };
|
E133328D2953AE4B00EE76AB /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328C2953AE4B00EE76AB /* CircularProgressView.swift */; };
|
||||||
|
@ -370,7 +374,6 @@
|
||||||
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */; };
|
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */; };
|
||||||
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; };
|
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; };
|
||||||
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129429228F2845000796AC6 /* SliderType.swift */; };
|
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129429228F2845000796AC6 /* SliderType.swift */; };
|
||||||
E1575E6D293E77B5001665B1 /* PosterButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17665D828E80F0F00130507 /* PosterButtonType.swift */; };
|
|
||||||
E1575E6E293E77B5001665B1 /* SpecialFeatureType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */; };
|
E1575E6E293E77B5001665B1 /* SpecialFeatureType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */; };
|
||||||
E1575E6F293E77B5001665B1 /* GestureAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1092F4B29106F9F00163F57 /* GestureAction.swift */; };
|
E1575E6F293E77B5001665B1 /* GestureAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1092F4B29106F9F00163F57 /* GestureAction.swift */; };
|
||||||
E1575E70293E77B5001665B1 /* TextPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528428FD191A00600579 /* TextPair.swift */; };
|
E1575E70293E77B5001665B1 /* TextPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528428FD191A00600579 /* TextPair.swift */; };
|
||||||
|
@ -444,7 +447,6 @@
|
||||||
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; };
|
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; };
|
||||||
E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; };
|
E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; };
|
||||||
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */; };
|
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */; };
|
||||||
E17665D928E80F0F00130507 /* PosterButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17665D828E80F0F00130507 /* PosterButtonType.swift */; };
|
|
||||||
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; };
|
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; };
|
||||||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
||||||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.swift */; };
|
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.swift */; };
|
||||||
|
@ -988,6 +990,8 @@
|
||||||
E12E30F229638B140022FAC9 /* ChevronButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronButton.swift; sourceTree = "<group>"; };
|
E12E30F229638B140022FAC9 /* ChevronButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronButton.swift; sourceTree = "<group>"; };
|
||||||
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = "<group>"; };
|
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = "<group>"; };
|
||||||
E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = "<group>"; };
|
E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = "<group>"; };
|
||||||
|
E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatioCornerRadiusModifier.swift; sourceTree = "<group>"; };
|
||||||
|
E13317002ADE4B8D009BF865 /* PaddingMultiplierModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingMultiplierModifier.swift; sourceTree = "<group>"; };
|
||||||
E133328729538D8D00EE76AB /* Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = "<group>"; };
|
E133328729538D8D00EE76AB /* Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = "<group>"; };
|
||||||
E133328C2953AE4B00EE76AB /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
|
E133328C2953AE4B00EE76AB /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
|
||||||
E133328E2953B71000EE76AB /* DownloadTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskView.swift; sourceTree = "<group>"; };
|
E133328E2953B71000EE76AB /* DownloadTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1072,7 +1076,6 @@
|
||||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = "<group>"; };
|
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = "<group>"; };
|
||||||
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatable.swift; sourceTree = "<group>"; };
|
E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatable.swift; sourceTree = "<group>"; };
|
||||||
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = "<group>"; };
|
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = "<group>"; };
|
||||||
E17665D828E80F0F00130507 /* PosterButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterButtonType.swift; sourceTree = "<group>"; };
|
|
||||||
E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = "<group>"; };
|
E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = "<group>"; };
|
||||||
E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = "<group>"; };
|
E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = "<group>"; };
|
||||||
E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = "<group>"; };
|
E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1547,7 +1550,6 @@
|
||||||
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
|
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
|
||||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
||||||
E1937A60288F32DB00CB80AA /* Poster.swift */,
|
E1937A60288F32DB00CB80AA /* Poster.swift */,
|
||||||
E17665D828E80F0F00130507 /* PosterButtonType.swift */,
|
|
||||||
E1CCF12D28ABF989006CAC9E /* PosterType.swift */,
|
E1CCF12D28ABF989006CAC9E /* PosterType.swift */,
|
||||||
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
|
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
|
||||||
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
|
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
|
||||||
|
@ -2262,6 +2264,8 @@
|
||||||
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */,
|
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */,
|
||||||
E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */,
|
E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */,
|
||||||
E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */,
|
E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */,
|
||||||
|
E13317002ADE4B8D009BF865 /* PaddingMultiplierModifier.swift */,
|
||||||
|
E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */,
|
||||||
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */,
|
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */,
|
||||||
E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */,
|
E43918652AD5C8310045A18C /* ScenePhaseChangeModifier.swift */,
|
||||||
);
|
);
|
||||||
|
@ -3193,6 +3197,7 @@
|
||||||
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */,
|
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */,
|
||||||
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
||||||
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
||||||
|
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */,
|
||||||
E148128928C154BF003B8787 /* ItemFilter.swift in Sources */,
|
E148128928C154BF003B8787 /* ItemFilter.swift in Sources */,
|
||||||
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */,
|
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */,
|
||||||
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
E154966F296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
||||||
|
@ -3305,6 +3310,7 @@
|
||||||
535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */,
|
535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */,
|
||||||
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||||
E1575E9A293E7B1E001665B1 /* Array.swift in Sources */,
|
E1575E9A293E7B1E001665B1 /* Array.swift in Sources */,
|
||||||
|
E13317022ADE4B8D009BF865 /* PaddingMultiplierModifier.swift in Sources */,
|
||||||
E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */,
|
E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */,
|
||||||
E187A60329AB28F0008387E6 /* RotateContentView.swift in Sources */,
|
E187A60329AB28F0008387E6 /* RotateContentView.swift in Sources */,
|
||||||
E1575E94293E7B1E001665B1 /* VerticalAlignment.swift in Sources */,
|
E1575E94293E7B1E001665B1 /* VerticalAlignment.swift in Sources */,
|
||||||
|
@ -3322,7 +3328,6 @@
|
||||||
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */,
|
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */,
|
||||||
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */,
|
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */,
|
||||||
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
|
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
|
||||||
E1575E6D293E77B5001665B1 /* PosterButtonType.swift in Sources */,
|
|
||||||
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */,
|
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */,
|
||||||
E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */,
|
E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */,
|
||||||
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
|
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
|
||||||
|
@ -3368,6 +3373,7 @@
|
||||||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||||
62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */,
|
62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */,
|
||||||
|
E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */,
|
||||||
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
|
||||||
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
|
E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */,
|
||||||
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
|
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */,
|
||||||
|
@ -3385,7 +3391,6 @@
|
||||||
E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */,
|
E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */,
|
||||||
E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */,
|
E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */,
|
||||||
E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */,
|
E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */,
|
||||||
E17665D928E80F0F00130507 /* PosterButtonType.swift in Sources */,
|
|
||||||
E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */,
|
E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */,
|
||||||
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */,
|
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */,
|
||||||
E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */,
|
||||||
|
@ -3518,6 +3523,7 @@
|
||||||
E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */,
|
E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */,
|
||||||
E17AC9732955007A003D2BC2 /* DownloadTaskButton.swift in Sources */,
|
E17AC9732955007A003D2BC2 /* DownloadTaskButton.swift in Sources */,
|
||||||
E1A1528228FD126C00600579 /* VerticalAlignment.swift in Sources */,
|
E1A1528228FD126C00600579 /* VerticalAlignment.swift in Sources */,
|
||||||
|
E13317012ADE4B8D009BF865 /* PaddingMultiplierModifier.swift in Sources */,
|
||||||
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
|
||||||
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */,
|
||||||
E148128B28C15526003B8787 /* SortBy.swift in Sources */,
|
E148128B28C15526003B8787 /* SortBy.swift in Sources */,
|
||||||
|
|
|
@ -25,7 +25,8 @@ struct LibraryItemRow: View {
|
||||||
} label: {
|
} label: {
|
||||||
HStack(alignment: .bottom) {
|
HStack(alignment: .bottom) {
|
||||||
ImageView(item.portraitPosterImageSource(maxWidth: posterWidth))
|
ImageView(item.portraitPosterImageSource(maxWidth: posterWidth))
|
||||||
.posterStyle(type: .portrait, width: posterWidth)
|
.posterStyle(.portrait)
|
||||||
|
.frame(width: posterWidth)
|
||||||
.posterShadow()
|
.posterShadow()
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -17,9 +17,9 @@ struct MenuPosterHStack<Model: MenuPosterHStackModel>: View {
|
||||||
private let type: PosterType
|
private let type: PosterType
|
||||||
private var itemScale: CGFloat
|
private var itemScale: CGFloat
|
||||||
private let singleImage: Bool
|
private let singleImage: Bool
|
||||||
private var content: (PosterButtonType<Model.Item>) -> any View
|
private var content: (Model.Item) -> any View
|
||||||
private var imageOverlay: (PosterButtonType<Model.Item>) -> any View
|
private var imageOverlay: (Model.Item) -> any View
|
||||||
private var contextMenu: (PosterButtonType<Model.Item>) -> any View
|
private var contextMenu: (Model.Item) -> any View
|
||||||
private var onSelect: (Model.Item) -> Void
|
private var onSelect: (Model.Item) -> Void
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
@ -50,9 +50,9 @@ struct MenuPosterHStack<Model: MenuPosterHStackModel>: View {
|
||||||
.fixedSize()
|
.fixedSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var items: [PosterButtonType<Model.Item>] {
|
private var items: [Model.Item] {
|
||||||
guard let selection = manager.menuSelection,
|
guard let selection = manager.menuSelection,
|
||||||
let items = manager.menuSections[selection] else { return [.noResult] }
|
let items = manager.menuSections[selection] else { return [] }
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,15 +101,15 @@ extension MenuPosterHStack {
|
||||||
copy(modifying: \.itemScale, with: scale)
|
copy(modifying: \.itemScale, with: scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func content(@ViewBuilder _ content: @escaping (PosterButtonType<Model.Item>) -> any View) -> Self {
|
func content(@ViewBuilder _ content: @escaping (Model.Item) -> any View) -> Self {
|
||||||
copy(modifying: \.content, with: content)
|
copy(modifying: \.content, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageOverlay(@ViewBuilder _ content: @escaping (PosterButtonType<Model.Item>) -> any View) -> Self {
|
func imageOverlay(@ViewBuilder _ content: @escaping (Model.Item) -> any View) -> Self {
|
||||||
copy(modifying: \.imageOverlay, with: content)
|
copy(modifying: \.imageOverlay, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextMenu(@ViewBuilder _ content: @escaping (PosterButtonType<Model.Item>) -> any View) -> Self {
|
func contextMenu(@ViewBuilder _ content: @escaping (Model.Item) -> any View) -> Self {
|
||||||
copy(modifying: \.contextMenu, with: content)
|
copy(modifying: \.contextMenu, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ struct PagingLibraryView: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var libraryGridView: some View {
|
private var libraryGridView: some View {
|
||||||
CollectionView(items: viewModel.items.elements) { _, item, _ in
|
CollectionView(items: viewModel.items.elements) { _, item, _ in
|
||||||
PosterButton(state: .item(item), type: libraryGridPosterType)
|
PosterButton(item: item, type: libraryGridPosterType)
|
||||||
.scaleItem(libraryGridPosterType == .landscape && UIDevice.isPhone ? 0.85 : portraitPosterScale)
|
.scaleItem(libraryGridPosterType == .landscape && UIDevice.isPhone ? 0.85 : portraitPosterScale)
|
||||||
.onSelect {
|
.onSelect {
|
||||||
onSelect(item)
|
onSelect(item)
|
||||||
|
|
|
@ -10,17 +10,16 @@ import Defaults
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// TODO: Look at something better for accomadating loading/noResults/other types
|
// TODO: builder methods shouldn't take the item
|
||||||
|
|
||||||
struct PosterButton<Item: Poster>: View {
|
struct PosterButton<Item: Poster>: View {
|
||||||
|
|
||||||
private var state: PosterButtonType<Item>
|
private var item: Item
|
||||||
private var type: PosterType
|
private var type: PosterType
|
||||||
private var itemScale: CGFloat
|
private var itemScale: CGFloat
|
||||||
private var horizontalAlignment: HorizontalAlignment
|
private var content: (Item) -> any View
|
||||||
private var content: (PosterButtonType<Item>) -> any View
|
private var imageOverlay: (Item) -> any View
|
||||||
private var imageOverlay: (PosterButtonType<Item>) -> any View
|
private var contextMenu: (Item) -> any View
|
||||||
private var contextMenu: (PosterButtonType<Item>) -> any View
|
|
||||||
private var onSelect: () -> Void
|
private var onSelect: () -> Void
|
||||||
private var singleImage: Bool
|
private var singleImage: Bool
|
||||||
|
|
||||||
|
@ -28,66 +27,44 @@ struct PosterButton<Item: Poster>: View {
|
||||||
type.width * itemScale
|
type.width * itemScale
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var loadingPoster: some View {
|
|
||||||
Color.secondarySystemFill
|
|
||||||
.posterStyle(type: type, width: itemWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var noResultsPoster: some View {
|
|
||||||
Color.secondarySystemFill
|
|
||||||
.posterStyle(type: type, width: itemWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func poster(from item: any Poster) -> some View {
|
private func poster(from item: any Poster) -> some View {
|
||||||
Group {
|
switch type {
|
||||||
switch type {
|
case .portrait:
|
||||||
case .portrait:
|
ImageView(item.portraitPosterImageSource(maxWidth: itemWidth))
|
||||||
ImageView(item.portraitPosterImageSource(maxWidth: itemWidth))
|
.failure {
|
||||||
.failure {
|
InitialFailureView(item.displayTitle.initials)
|
||||||
InitialFailureView(item.displayTitle.initials)
|
}
|
||||||
}
|
case .landscape:
|
||||||
case .landscape:
|
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
||||||
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
.failure {
|
||||||
.failure {
|
InitialFailureView(item.displayTitle.initials)
|
||||||
InitialFailureView(item.displayTitle.initials)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: horizontalAlignment) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
onSelect()
|
onSelect()
|
||||||
} label: {
|
} label: {
|
||||||
Group {
|
poster(from: item)
|
||||||
switch state {
|
.overlay {
|
||||||
case .loading:
|
imageOverlay(item)
|
||||||
loadingPoster
|
.eraseToAnyView()
|
||||||
case .noResult:
|
.posterStyle(type)
|
||||||
noResultsPoster
|
|
||||||
case let .item(item):
|
|
||||||
poster(from: item)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.overlay {
|
|
||||||
imageOverlay(state)
|
|
||||||
.eraseToAnyView()
|
|
||||||
.posterStyle(type: type, width: itemWidth)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.contextMenu(menuItems: {
|
.contextMenu(menuItems: {
|
||||||
contextMenu(state)
|
contextMenu(item)
|
||||||
.eraseToAnyView()
|
.eraseToAnyView()
|
||||||
})
|
})
|
||||||
.posterStyle(type: type, width: itemWidth)
|
.posterStyle(type)
|
||||||
|
.frame(width: itemWidth)
|
||||||
.posterShadow()
|
.posterShadow()
|
||||||
|
|
||||||
content(state)
|
content(item)
|
||||||
.eraseToAnyView()
|
.eraseToAnyView()
|
||||||
}
|
}
|
||||||
.frame(width: itemWidth)
|
.frame(width: itemWidth)
|
||||||
|
@ -97,40 +74,35 @@ struct PosterButton<Item: Poster>: View {
|
||||||
extension PosterButton {
|
extension PosterButton {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
state: PosterButtonType<Item>,
|
item: Item,
|
||||||
type: PosterType,
|
type: PosterType,
|
||||||
singleImage: Bool = false
|
singleImage: Bool = false
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
state: state,
|
item: item,
|
||||||
type: type,
|
type: type,
|
||||||
itemScale: 1,
|
itemScale: 1,
|
||||||
horizontalAlignment: .leading,
|
content: { DefaultContentView(item: $0) },
|
||||||
content: { DefaultContentView(state: $0) },
|
imageOverlay: { DefaultOverlay(item: $0) },
|
||||||
imageOverlay: { DefaultOverlay(state: $0) },
|
|
||||||
contextMenu: { _ in EmptyView() },
|
contextMenu: { _ in EmptyView() },
|
||||||
onSelect: {},
|
onSelect: {},
|
||||||
singleImage: singleImage
|
singleImage: singleImage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func horizontalAlignment(_ alignment: HorizontalAlignment) -> Self {
|
|
||||||
copy(modifying: \.horizontalAlignment, with: alignment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleItem(_ scale: CGFloat) -> Self {
|
func scaleItem(_ scale: CGFloat) -> Self {
|
||||||
copy(modifying: \.itemScale, with: scale)
|
copy(modifying: \.itemScale, with: scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func content(@ViewBuilder _ content: @escaping (PosterButtonType<Item>) -> any View) -> Self {
|
func content(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self {
|
||||||
copy(modifying: \.content, with: content)
|
copy(modifying: \.content, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageOverlay(@ViewBuilder _ content: @escaping (PosterButtonType<Item>) -> any View) -> Self {
|
func imageOverlay(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self {
|
||||||
copy(modifying: \.imageOverlay, with: content)
|
copy(modifying: \.imageOverlay, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextMenu(@ViewBuilder _ content: @escaping (PosterButtonType<Item>) -> any View) -> Self {
|
func contextMenu(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self {
|
||||||
copy(modifying: \.contextMenu, with: content)
|
copy(modifying: \.contextMenu, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,50 +117,30 @@ extension PosterButton {
|
||||||
|
|
||||||
struct DefaultContentView: View {
|
struct DefaultContentView: View {
|
||||||
|
|
||||||
let state: PosterButtonType<Item>
|
let item: Item
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var title: some View {
|
private var title: some View {
|
||||||
Group {
|
if item.showTitle {
|
||||||
switch state {
|
Text(item.displayTitle)
|
||||||
case .loading:
|
.font(.footnote.weight(.regular))
|
||||||
String(repeating: "a", count: Int.random(in: 5 ..< 8)).text
|
.foregroundColor(.primary)
|
||||||
.redacted(reason: .placeholder)
|
.lineLimit(2)
|
||||||
case .noResult:
|
} else {
|
||||||
L10n.noResults.text
|
EmptyView()
|
||||||
case let .item(item):
|
|
||||||
if item.showTitle {
|
|
||||||
Text(item.displayTitle)
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.font(.footnote.weight(.regular))
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var subtitle: some View {
|
private var subtitle: some View {
|
||||||
Group {
|
if let subtitle = item.subtitle {
|
||||||
switch state {
|
Text(subtitle)
|
||||||
case .loading:
|
.font(.caption.weight(.medium))
|
||||||
String(repeating: "a", count: Int.random(in: 8 ..< 15)).text
|
.foregroundColor(.secondary)
|
||||||
.redacted(reason: .placeholder)
|
.lineLimit(2)
|
||||||
case .noResult:
|
} else {
|
||||||
L10n.noResults.text
|
EmptyView()
|
||||||
case let .item(item):
|
|
||||||
if let subtitle = item.subtitle {
|
|
||||||
Text(subtitle)
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.font(.caption.weight(.medium))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -215,30 +167,28 @@ extension PosterButton {
|
||||||
@Default(.Customization.Indicators.showPlayed)
|
@Default(.Customization.Indicators.showPlayed)
|
||||||
private var showPlayed
|
private var showPlayed
|
||||||
|
|
||||||
let state: PosterButtonType<Item>
|
let item: Item
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if case let PosterButtonType.item(item) = state {
|
ZStack {
|
||||||
ZStack {
|
if let item = item as? BaseItemDto {
|
||||||
if let item = item as? BaseItemDto {
|
if item.userData?.isPlayed ?? false {
|
||||||
if item.userData?.isPlayed ?? false {
|
WatchedIndicator(size: 25)
|
||||||
WatchedIndicator(size: 25)
|
.visible(showPlayed)
|
||||||
.visible(showPlayed)
|
} else {
|
||||||
|
if (item.userData?.playbackPositionTicks ?? 0) > 0 {
|
||||||
|
ProgressIndicator(progress: (item.userData?.playedPercentage ?? 0) / 100, height: 5)
|
||||||
|
.visible(showProgress)
|
||||||
} else {
|
} else {
|
||||||
if (item.userData?.playbackPositionTicks ?? 0) > 0 {
|
UnwatchedIndicator(size: 25)
|
||||||
ProgressIndicator(progress: (item.userData?.playedPercentage ?? 0) / 100, height: 5)
|
.foregroundColor(accentColor)
|
||||||
.visible(showProgress)
|
.visible(showUnplayed)
|
||||||
} else {
|
|
||||||
UnwatchedIndicator(size: 25)
|
|
||||||
.foregroundColor(accentColor)
|
|
||||||
.visible(showUnplayed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if item.userData?.isFavorite ?? false {
|
if item.userData?.isFavorite ?? false {
|
||||||
FavoriteIndicator(size: 25)
|
FavoriteIndicator(size: 25)
|
||||||
.visible(showFavorited)
|
.visible(showFavorited)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,12 @@ struct PosterHStack<Item: Poster>: View {
|
||||||
private var header: () -> any View
|
private var header: () -> any View
|
||||||
private var title: String?
|
private var title: String?
|
||||||
private var type: PosterType
|
private var type: PosterType
|
||||||
private var items: [PosterButtonType<Item>]
|
private var items: [Item]
|
||||||
private var singleImage: Bool
|
private var singleImage: Bool
|
||||||
private var itemScale: CGFloat
|
private var itemScale: CGFloat
|
||||||
private var content: (PosterButtonType<Item>) -> any View
|
private var content: (Item) -> any View
|
||||||
private var imageOverlay: (PosterButtonType<Item>) -> any View
|
private var imageOverlay: (Item) -> any View
|
||||||
private var contextMenu: (PosterButtonType<Item>) -> any View
|
private var contextMenu: (Item) -> any View
|
||||||
private var trailingContent: () -> any View
|
private var trailingContent: () -> any View
|
||||||
private var onSelect: (Item) -> Void
|
private var onSelect: (Item) -> Void
|
||||||
|
|
||||||
|
@ -43,9 +43,9 @@ struct PosterHStack<Item: Poster>: View {
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(alignment: .top, spacing: 15) {
|
HStack(alignment: .top, spacing: 15) {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.self) { item in
|
||||||
PosterButton(
|
PosterButton(
|
||||||
state: item,
|
item: item,
|
||||||
type: type,
|
type: type,
|
||||||
singleImage: singleImage
|
singleImage: singleImage
|
||||||
)
|
)
|
||||||
|
@ -53,12 +53,7 @@ struct PosterHStack<Item: Poster>: View {
|
||||||
.content { content($0).eraseToAnyView() }
|
.content { content($0).eraseToAnyView() }
|
||||||
.imageOverlay { imageOverlay($0).eraseToAnyView() }
|
.imageOverlay { imageOverlay($0).eraseToAnyView() }
|
||||||
.contextMenu { contextMenu($0).eraseToAnyView() }
|
.contextMenu { contextMenu($0).eraseToAnyView() }
|
||||||
.onSelect {
|
.onSelect { onSelect(item) }
|
||||||
if case let PosterButtonType.item(item) = item {
|
|
||||||
onSelect(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transition(.slide)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
@ -72,33 +67,11 @@ struct PosterHStack<Item: Poster>: View {
|
||||||
|
|
||||||
extension PosterHStack {
|
extension PosterHStack {
|
||||||
|
|
||||||
// TODO: Remove
|
|
||||||
init(
|
init(
|
||||||
title: String,
|
title: String,
|
||||||
type: PosterType,
|
type: PosterType,
|
||||||
items: [Item],
|
items: [Item],
|
||||||
singleImage: Bool = false
|
singleImage: Bool = false
|
||||||
) {
|
|
||||||
self.init(
|
|
||||||
header: { DefaultHeader(title: title) },
|
|
||||||
title: title,
|
|
||||||
type: type,
|
|
||||||
items: items.map { PosterButtonType.item($0) },
|
|
||||||
singleImage: singleImage,
|
|
||||||
itemScale: 1,
|
|
||||||
content: { PosterButton.DefaultContentView(state: $0) },
|
|
||||||
imageOverlay: { PosterButton.DefaultOverlay(state: $0) },
|
|
||||||
contextMenu: { _ in EmptyView() },
|
|
||||||
trailingContent: { EmptyView() },
|
|
||||||
onSelect: { _ in }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
|
||||||
title: String,
|
|
||||||
type: PosterType,
|
|
||||||
items: [PosterButtonType<Item>],
|
|
||||||
singleImage: Bool = false
|
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
header: { DefaultHeader(title: title) },
|
header: { DefaultHeader(title: title) },
|
||||||
|
@ -107,8 +80,8 @@ extension PosterHStack {
|
||||||
items: items,
|
items: items,
|
||||||
singleImage: singleImage,
|
singleImage: singleImage,
|
||||||
itemScale: 1,
|
itemScale: 1,
|
||||||
content: { PosterButton.DefaultContentView(state: $0) },
|
content: { PosterButton.DefaultContentView(item: $0) },
|
||||||
imageOverlay: { PosterButton.DefaultOverlay(state: $0) },
|
imageOverlay: { PosterButton.DefaultOverlay(item: $0) },
|
||||||
contextMenu: { _ in EmptyView() },
|
contextMenu: { _ in EmptyView() },
|
||||||
trailingContent: { EmptyView() },
|
trailingContent: { EmptyView() },
|
||||||
onSelect: { _ in }
|
onSelect: { _ in }
|
||||||
|
@ -117,7 +90,7 @@ extension PosterHStack {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
type: PosterType,
|
type: PosterType,
|
||||||
items: [PosterButtonType<Item>],
|
items: [Item],
|
||||||
singleImage: Bool = false
|
singleImage: Bool = false
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
|
@ -127,8 +100,8 @@ extension PosterHStack {
|
||||||
items: items,
|
items: items,
|
||||||
singleImage: singleImage,
|
singleImage: singleImage,
|
||||||
itemScale: 1,
|
itemScale: 1,
|
||||||
content: { PosterButton.DefaultContentView(state: $0) },
|
content: { PosterButton.DefaultContentView(item: $0) },
|
||||||
imageOverlay: { PosterButton.DefaultOverlay(state: $0) },
|
imageOverlay: { PosterButton.DefaultOverlay(item: $0) },
|
||||||
contextMenu: { _ in EmptyView() },
|
contextMenu: { _ in EmptyView() },
|
||||||
trailingContent: { EmptyView() },
|
trailingContent: { EmptyView() },
|
||||||
onSelect: { _ in }
|
onSelect: { _ in }
|
||||||
|
@ -143,15 +116,15 @@ extension PosterHStack {
|
||||||
copy(modifying: \.itemScale, with: scale)
|
copy(modifying: \.itemScale, with: scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func content(@ViewBuilder _ content: @escaping (PosterButtonType<Item>) -> any View) -> Self {
|
func content(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self {
|
||||||
copy(modifying: \.content, with: content)
|
copy(modifying: \.content, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageOverlay(@ViewBuilder _ content: @escaping (PosterButtonType<Item>) -> any View) -> Self {
|
func imageOverlay(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self {
|
||||||
copy(modifying: \.imageOverlay, with: content)
|
copy(modifying: \.imageOverlay, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextMenu(@ViewBuilder _ content: @escaping (PosterButtonType<Item>) -> any View) -> Self {
|
func contextMenu(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self {
|
||||||
copy(modifying: \.contextMenu, with: content)
|
copy(modifying: \.contextMenu, with: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,8 @@ extension CastAndCrewLibraryView {
|
||||||
} label: {
|
} label: {
|
||||||
HStack(alignment: .bottom) {
|
HStack(alignment: .bottom) {
|
||||||
ImageView(person.portraitPosterImageSource(maxWidth: 60))
|
ImageView(person.portraitPosterImageSource(maxWidth: 60))
|
||||||
.posterStyle(type: .portrait, width: 60)
|
.posterStyle(.portrait)
|
||||||
|
.frame(width: 60)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(person.displayTitle)
|
Text(person.displayTitle)
|
||||||
|
|
|
@ -46,7 +46,7 @@ struct CastAndCrewLibraryView: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var libraryGridView: some View {
|
private var libraryGridView: some View {
|
||||||
CollectionView(items: people) { _, person, _ in
|
CollectionView(items: people) { _, person, _ in
|
||||||
PosterButton(state: .item(person), type: .portrait)
|
PosterButton(item: person, type: .portrait)
|
||||||
.onSelect {
|
.onSelect {
|
||||||
router.route(to: \.library, .init(parent: person, type: .person, filters: .init()))
|
router.route(to: \.library, .init(parent: person, type: .person, filters: .init()))
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ struct CastAndCrewLibraryView: View {
|
||||||
.layout { _, layoutEnvironment in
|
.layout { _, layoutEnvironment in
|
||||||
.grid(
|
.grid(
|
||||||
layoutEnvironment: layoutEnvironment,
|
layoutEnvironment: layoutEnvironment,
|
||||||
layoutMode: .adaptive(withMinItemSize: PosterType.portrait.width + (UIDevice.isIPad ? 10 : 0)),
|
layoutMode: .adaptive(withMinItemSize: 150 + (UIDevice.isIPad ? 10 : 0)),
|
||||||
sectionInsets: .init(top: 0, leading: 10, bottom: 0, trailing: 10)
|
sectionInsets: .init(top: 0, leading: 10, bottom: 0, trailing: 10)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ extension DownloadListView {
|
||||||
Color.secondary
|
Color.secondary
|
||||||
.opacity(0.8)
|
.opacity(0.8)
|
||||||
}
|
}
|
||||||
.posterStyle(type: .portrait, width: 60)
|
// .posterStyle(type: .portrait, width: 60)
|
||||||
.posterShadow()
|
.posterShadow()
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -22,31 +22,27 @@ extension HomeView {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PosterHStack(
|
PosterHStack(
|
||||||
type: .landscape,
|
type: .landscape,
|
||||||
items: viewModel.resumeItems.map { .item($0) }
|
items: viewModel.resumeItems
|
||||||
)
|
)
|
||||||
.scaleItems(1.5)
|
.scaleItems(1.5)
|
||||||
.contextMenu { state in
|
.contextMenu { item in
|
||||||
if case let PosterButtonType.item(item) = state {
|
Button {
|
||||||
Button {
|
viewModel.markItemPlayed(item)
|
||||||
viewModel.markItemPlayed(item)
|
} label: {
|
||||||
} label: {
|
Label(L10n.played, systemImage: "checkmark.circle")
|
||||||
Label(L10n.played, systemImage: "checkmark.circle")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
viewModel.markItemUnplayed(item)
|
viewModel.markItemUnplayed(item)
|
||||||
} label: {
|
} label: {
|
||||||
Label(L10n.unplayed, systemImage: "minus.circle")
|
Label(L10n.unplayed, systemImage: "minus.circle")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.imageOverlay { state in
|
.imageOverlay { item in
|
||||||
if case let PosterButtonType.item(item) = state {
|
LandscapePosterProgressBar(
|
||||||
LandscapePosterProgressBar(
|
title: item.progressLabel ?? L10n.continue,
|
||||||
title: item.progressLabel ?? L10n.continue,
|
progress: (item.userData?.playedPercentage ?? 0) / 100
|
||||||
progress: (item.userData?.playedPercentage ?? 0) / 100
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
router.route(to: \.item, item)
|
router.route(to: \.item, item)
|
||||||
|
|
|
@ -23,8 +23,8 @@ extension HomeView {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: LibraryViewModel
|
var viewModel: LibraryViewModel
|
||||||
|
|
||||||
private var items: [PosterButtonType<BaseItemDto>] {
|
private var items: [BaseItemDto] {
|
||||||
viewModel.items.prefix(20).asArray.map { .item($0) }
|
viewModel.items.prefix(20).asArray
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
@ -23,8 +23,8 @@ extension HomeView {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: NextUpLibraryViewModel
|
var viewModel: NextUpLibraryViewModel
|
||||||
|
|
||||||
private var items: [PosterButtonType<BaseItemDto>] {
|
private var items: [BaseItemDto] {
|
||||||
viewModel.items.prefix(20).asArray.map { .item($0) }
|
viewModel.items.prefix(20).asArray
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -39,13 +39,11 @@ extension HomeView {
|
||||||
router.route(to: \.basicLibrary, .init(title: L10n.nextUp, viewModel: viewModel))
|
router.route(to: \.basicLibrary, .init(title: L10n.nextUp, viewModel: viewModel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contextMenu { state in
|
.contextMenu { item in
|
||||||
if case let PosterButtonType.item(item) = state {
|
Button {
|
||||||
Button {
|
viewModel.markPlayed(item: item)
|
||||||
viewModel.markPlayed(item: item)
|
} label: {
|
||||||
} label: {
|
Label(L10n.played, systemImage: "checkmark.circle")
|
||||||
Label(L10n.played, systemImage: "checkmark.circle")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
|
|
|
@ -23,8 +23,8 @@ extension HomeView {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: ItemTypeLibraryViewModel
|
var viewModel: ItemTypeLibraryViewModel
|
||||||
|
|
||||||
private var items: [PosterButtonType<BaseItemDto>] {
|
private var items: [BaseItemDto] {
|
||||||
viewModel.items.prefix(20).asArray.map { .item($0) }
|
viewModel.items.prefix(20).asArray
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
@ -40,7 +40,8 @@ extension ItemView {
|
||||||
viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel
|
viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel
|
||||||
.item.imageSource(.primary, maxWidth: 300)
|
.item.imageSource(.primary, maxWidth: 300)
|
||||||
)
|
)
|
||||||
.posterStyle(type: .portrait, width: 130)
|
.posterStyle(.portrait)
|
||||||
|
.frame(width: 130)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
|
||||||
OverviewCard(item: viewModel.item)
|
OverviewCard(item: viewModel.item)
|
||||||
|
|
|
@ -26,7 +26,6 @@ extension ItemView {
|
||||||
.filter(\.isDisplayed)
|
.filter(\.isDisplayed)
|
||||||
.prefix(20)
|
.prefix(20)
|
||||||
.asArray
|
.asArray
|
||||||
.map { .item($0) }
|
|
||||||
)
|
)
|
||||||
.trailing {
|
.trailing {
|
||||||
SeeAllButton()
|
SeeAllButton()
|
||||||
|
|
|
@ -26,10 +26,10 @@ struct SeriesEpisodeSelector: View {
|
||||||
)
|
)
|
||||||
.scaleItems(1.2)
|
.scaleItems(1.2)
|
||||||
.imageOverlay { type in
|
.imageOverlay { type in
|
||||||
EpisodeOverlay(type: type)
|
EpisodeOverlay(episode: type)
|
||||||
}
|
}
|
||||||
.content { type in
|
.content { type in
|
||||||
EpisodeContent(type: type)
|
EpisodeContent(episode: type)
|
||||||
}
|
}
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
guard let mediaSource = item.mediaSources?.first else { return }
|
guard let mediaSource = item.mediaSources?.first else { return }
|
||||||
|
@ -42,28 +42,24 @@ extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
struct EpisodeOverlay: View {
|
struct EpisodeOverlay: View {
|
||||||
|
|
||||||
let type: PosterButtonType<BaseItemDto>
|
let episode: BaseItemDto
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if case let PosterButtonType.item(episode) = type {
|
if let progressLabel = episode.progressLabel {
|
||||||
if let progressLabel = episode.progressLabel {
|
LandscapePosterProgressBar(
|
||||||
LandscapePosterProgressBar(
|
title: progressLabel,
|
||||||
title: progressLabel,
|
progress: (episode.userData?.playedPercentage ?? 0) / 100
|
||||||
progress: (episode.userData?.playedPercentage ?? 0) / 100
|
)
|
||||||
)
|
} else if episode.userData?.isPlayed ?? false {
|
||||||
} else if episode.userData?.isPlayed ?? false {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
Color.clear
|
||||||
Color.clear
|
|
||||||
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 30, height: 30, alignment: .bottomTrailing)
|
.frame(width: 30, height: 30, alignment: .bottomTrailing)
|
||||||
.accentSymbolRendering(accentColor: .white)
|
.accentSymbolRendering(accentColor: .white)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,71 +74,43 @@ extension SeriesEpisodeSelector {
|
||||||
@ScaledMetric
|
@ScaledMetric
|
||||||
private var staticOverviewHeight: CGFloat = 50
|
private var staticOverviewHeight: CGFloat = 50
|
||||||
|
|
||||||
let type: PosterButtonType<BaseItemDto>
|
let episode: BaseItemDto
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var subHeader: some View {
|
private var subHeader: some View {
|
||||||
Group {
|
Text(episode.episodeLocator ?? L10n.unknown)
|
||||||
switch type {
|
.font(.footnote)
|
||||||
case .loading:
|
.foregroundColor(.secondary)
|
||||||
String(repeating: "a", count: 5).text
|
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
case .noResult:
|
|
||||||
String.emptyDash.text
|
|
||||||
case let .item(episode):
|
|
||||||
Text(episode.episodeLocator ?? L10n.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
Group {
|
Text(episode.displayTitle)
|
||||||
switch type {
|
.font(.body)
|
||||||
case .loading:
|
.foregroundColor(.primary)
|
||||||
String(repeating: "a", count: Int.random(in: 8 ..< 18)).text
|
.padding(.bottom, 1)
|
||||||
.redacted(reason: .placeholder)
|
.lineLimit(2)
|
||||||
case .noResult:
|
.multilineTextAlignment(.leading)
|
||||||
L10n.noResults.text
|
|
||||||
case let .item(episode):
|
|
||||||
Text(episode.displayTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.padding(.bottom, 1)
|
|
||||||
.lineLimit(2)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
Group {
|
Group {
|
||||||
switch type {
|
ZStack(alignment: .topLeading) {
|
||||||
case .loading:
|
Color.clear
|
||||||
String(repeating: "a", count: Int.random(in: 50 ..< 100)).text
|
.frame(height: staticOverviewHeight)
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
case .noResult:
|
|
||||||
L10n.noOverviewAvailable.text
|
|
||||||
case let .item(episode):
|
|
||||||
ZStack(alignment: .topLeading) {
|
|
||||||
Color.clear
|
|
||||||
.frame(height: staticOverviewHeight)
|
|
||||||
|
|
||||||
if episode.isUnaired {
|
if episode.isUnaired {
|
||||||
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
|
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
|
||||||
} else {
|
} else {
|
||||||
Text(episode.overview ?? L10n.noOverviewAvailable)
|
Text(episode.overview ?? L10n.noOverviewAvailable)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
L10n.seeMore.text
|
|
||||||
.font(.footnote)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(accentColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
L10n.seeMore.text
|
||||||
|
.font(.footnote)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(accentColor)
|
||||||
}
|
}
|
||||||
.font(.caption.weight(.light))
|
.font(.caption.weight(.light))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
@ -152,9 +120,7 @@ extension SeriesEpisodeSelector {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
if case let PosterButtonType.item(item) = type {
|
router.route(to: \.item, episode)
|
||||||
router.route(to: \.item, item)
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
subHeader
|
subHeader
|
||||||
|
|
|
@ -26,7 +26,7 @@ extension ItemView {
|
||||||
PosterHStack(
|
PosterHStack(
|
||||||
title: L10n.recommended,
|
title: L10n.recommended,
|
||||||
type: similarPosterType,
|
type: similarPosterType,
|
||||||
items: items.map { .item($0) }
|
items: items
|
||||||
)
|
)
|
||||||
.trailing {
|
.trailing {
|
||||||
SeeAllButton()
|
SeeAllButton()
|
||||||
|
|
|
@ -22,7 +22,7 @@ extension ItemView {
|
||||||
PosterHStack(
|
PosterHStack(
|
||||||
title: L10n.specialFeatures,
|
title: L10n.specialFeatures,
|
||||||
type: .landscape,
|
type: .landscape,
|
||||||
items: items.map { .item($0) }
|
items: items
|
||||||
)
|
)
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
guard let mediaSource = item.mediaSources?.first else { return }
|
guard let mediaSource = item.mediaSources?.first else { return }
|
||||||
|
|
|
@ -19,14 +19,6 @@ extension CollectionItemView {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: CollectionItemViewModel
|
var viewModel: CollectionItemViewModel
|
||||||
|
|
||||||
private var items: [PosterButtonType<BaseItemDto>] {
|
|
||||||
if viewModel.isLoading {
|
|
||||||
return PosterButtonType.loading.random(in: 3 ..< 8)
|
|
||||||
} else {
|
|
||||||
return viewModel.collectionItems.map { .item($0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
|
||||||
|
@ -51,7 +43,7 @@ extension CollectionItemView {
|
||||||
PosterHStack(
|
PosterHStack(
|
||||||
title: L10n.items,
|
title: L10n.items,
|
||||||
type: .portrait,
|
type: .portrait,
|
||||||
items: items
|
items: viewModel.collectionItems
|
||||||
)
|
)
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
router.route(to: \.item, item)
|
router.route(to: \.item, item)
|
||||||
|
|
|
@ -173,7 +173,8 @@ extension ItemView.CompactPosterScrollView {
|
||||||
// MARK: Portrait Image
|
// MARK: Portrait Image
|
||||||
|
|
||||||
ImageView(viewModel.item.imageSource(.primary, maxWidth: 130))
|
ImageView(viewModel.item.imageSource(.primary, maxWidth: 130))
|
||||||
.posterStyle(type: .portrait, width: 130)
|
.posterStyle(.portrait)
|
||||||
|
.frame(width: 130)
|
||||||
.accessibilityIgnoresInvertColors()
|
.accessibilityIgnoresInvertColors()
|
||||||
|
|
||||||
rightShelfView
|
rightShelfView
|
||||||
|
|
|
@ -19,14 +19,6 @@ extension iPadOSCollectionItemView {
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var viewModel: CollectionItemViewModel
|
var viewModel: CollectionItemViewModel
|
||||||
|
|
||||||
private var items: [PosterButtonType<BaseItemDto>] {
|
|
||||||
if viewModel.isLoading {
|
|
||||||
return PosterButtonType.loading.random(in: 3 ..< 8)
|
|
||||||
} else {
|
|
||||||
return viewModel.collectionItems.map { .item($0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
|
||||||
|
@ -51,7 +43,7 @@ extension iPadOSCollectionItemView {
|
||||||
PosterHStack(
|
PosterHStack(
|
||||||
title: L10n.items,
|
title: L10n.items,
|
||||||
type: .portrait,
|
type: .portrait,
|
||||||
items: items
|
items: viewModel.collectionItems
|
||||||
)
|
)
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
router.route(to: \.item, item)
|
router.route(to: \.item, item)
|
||||||
|
|
|
@ -111,7 +111,8 @@ extension MediaView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.posterStyle(type: .landscape, width: itemWidth)
|
.posterStyle(.landscape)
|
||||||
|
.frame(width: itemWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ struct SearchView: View {
|
||||||
PosterHStack(
|
PosterHStack(
|
||||||
title: title,
|
title: title,
|
||||||
type: posterType,
|
type: posterType,
|
||||||
items: viewModel[keyPath: keyPath].map { .item($0) }
|
items: viewModel[keyPath: keyPath]
|
||||||
)
|
)
|
||||||
.onSelect { item in
|
.onSelect { item in
|
||||||
baseItemOnSelect(item)
|
baseItemOnSelect(item)
|
||||||
|
|
|
@ -74,38 +74,34 @@ extension VideoPlayer.Overlay {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(alignment: .top, spacing: 15) {
|
HStack(alignment: .top, spacing: 15) {
|
||||||
ForEach(viewModel.chapters, id: \.hashValue) { chapter in
|
ForEach(viewModel.chapters, id: \.self) { chapter in
|
||||||
PosterButton(
|
PosterButton(
|
||||||
state: .item(chapter),
|
item: chapter,
|
||||||
type: .landscape
|
type: .landscape
|
||||||
)
|
)
|
||||||
.imageOverlay { type in
|
.imageOverlay { info in
|
||||||
if case let PosterButtonType.item(info) = type,
|
if info.secondsRange.contains(currentProgressHandler.seconds) {
|
||||||
info.secondsRange.contains(currentProgressHandler.seconds)
|
|
||||||
{
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.stroke(accentColor, lineWidth: 8)
|
.stroke(accentColor, lineWidth: 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.content { type in
|
.content { info in
|
||||||
if case let PosterButtonType.item(info) = type {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
Text(info.chapterInfo.displayTitle)
|
||||||
Text(info.chapterInfo.displayTitle)
|
.font(.subheadline)
|
||||||
.font(.subheadline)
|
.fontWeight(.semibold)
|
||||||
.fontWeight(.semibold)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
.foregroundColor(.white)
|
||||||
.foregroundColor(.white)
|
|
||||||
|
|
||||||
Text(info.chapterInfo.timestampLabel)
|
Text(info.chapterInfo.timestampLabel)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(Color(UIColor.systemBlue))
|
.foregroundColor(Color(UIColor.systemBlue))
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 4)
|
||||||
.background {
|
.background {
|
||||||
Color(UIColor.darkGray).opacity(0.2).cornerRadius(4)
|
Color(UIColor.darkGray).opacity(0.2).cornerRadius(4)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onSelect {
|
.onSelect {
|
||||||
|
|
Loading…
Reference in New Issue