Remove iOS `PosterButtonType` + cleanup (#883)

This commit is contained in:
Ethan Pippin 2023-10-31 23:52:06 -06:00 committed by GitHub
parent 5a407410fb
commit 9266d53ae0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 299 additions and 443 deletions

View File

@ -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)

View File

@ -0,0 +1,23 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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] {
[]

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -29,7 +29,8 @@ struct NonePosterButton: View {
.foregroundColor(.secondary)
}
}
.posterStyle(type: type, width: type.width)
.posterStyle(type)
.frame(width: type.width)
}
}
.buttonStyle(.card)

View File

@ -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)

View File

@ -29,7 +29,8 @@ struct SeeAllPosterButton: View {
.font(.title3)
}
}
.posterStyle(type: type, width: type.width)
.posterStyle(type)
.frame(width: type.width)
}
.buttonStyle(.card)
}

View File

@ -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 {

View File

@ -93,7 +93,8 @@ extension MediaView {
}
}
}
.posterStyle(type: .landscape, width: itemWidth)
.posterStyle(.landscape)
.frame(width: itemWidth)
}
.buttonStyle(.card)
}

View File

@ -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 */,

View File

@ -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) {

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
)
}

View File

@ -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) {

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -26,7 +26,6 @@ extension ItemView {
.filter(\.isDisplayed)
.prefix(20)
.asArray
.map { .item($0) }
)
.trailing {
SeeAllButton()

View File

@ -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

View File

@ -26,7 +26,7 @@ extension ItemView {
PosterHStack(
title: L10n.recommended,
type: similarPosterType,
items: items.map { .item($0) }
items: items
)
.trailing {
SeeAllButton()

View File

@ -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 }

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -111,7 +111,8 @@ extension MediaView {
}
}
}
.posterStyle(type: .landscape, width: itemWidth)
.posterStyle(.landscape)
.frame(width: itemWidth)
}
}
}

View File

@ -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)

View File

@ -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 {