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