From 9266d53ae0c6603ef33a346614f07f50e29ec156 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 31 Oct 2023 23:52:06 -0600 Subject: [PATCH] Remove iOS `PosterButtonType` + cleanup (#883) --- Shared/Extensions/EnvironmentValue.swift | 1 + .../Modifiers/PaddingMultiplierModifier.swift | 23 +++ .../Modifiers/RatioCornerRadiusModifier.swift | 27 +++ .../ViewExtensions/ViewExtensions.swift | 67 +++---- Shared/Objects/MenuPosterHStackModel.swift | 2 +- Shared/Objects/Poster.swift | 6 +- Shared/Objects/PosterButtonType.swift | 36 ---- .../ItemViewModel/SeriesItemViewModel.swift | 21 +- .../ViewModels/SpecialFeaturesViewModel.swift | 4 +- .../Components/NonePosterButton.swift | 3 +- Swiftfin tvOS/Components/PosterButton.swift | 7 +- .../Components/SeeAllPosterButton.swift | 3 +- .../Components/SeriesEpisodeSelector.swift | 2 +- Swiftfin tvOS/Views/MediaView.swift | 3 +- Swiftfin.xcodeproj/project.pbxproj | 18 +- Swiftfin/Components/LibraryItemRow.swift | 3 +- Swiftfin/Components/MenuPosterHStack.swift | 16 +- Swiftfin/Components/PagingLibraryView.swift | 2 +- Swiftfin/Components/PosterButton.swift | 180 +++++++----------- Swiftfin/Components/PosterHStack.swift | 57 ++---- .../CastAndCrewItemRow.swift | 3 +- .../CastAndCrewLibraryView.swift | 4 +- Swiftfin/Views/DownloadListView.swift | 2 +- .../Components/ContinueWatchingView.swift | 36 ++-- .../Components/LatestInLibraryView.swift | 4 +- .../HomeView/Components/NextUpView.swift | 16 +- .../Components/RecentlyAddedView.swift | 4 +- .../Components/AboutView/AboutView.swift | 3 +- .../Components/CastAndCrewHStack.swift | 1 - .../Components/SeriesEpisodeSelector.swift | 116 ++++------- .../Components/SimilarItemsHStack.swift | 2 +- .../Components/SpecialFeatureHStack.swift | 2 +- .../CollectionItemContentView.swift | 10 +- .../CompactPortraitScrollView.swift | 3 +- .../iPadOSCollectionItemContentView.swift | 10 +- Swiftfin/Views/MediaView.swift | 3 +- Swiftfin/Views/SearchView.swift | 2 +- .../VideoPlayer/Overlays/ChapterOverlay.swift | 44 ++--- 38 files changed, 301 insertions(+), 445 deletions(-) create mode 100644 Shared/Extensions/ViewExtensions/Modifiers/PaddingMultiplierModifier.swift create mode 100644 Shared/Extensions/ViewExtensions/Modifiers/RatioCornerRadiusModifier.swift delete mode 100644 Shared/Objects/PosterButtonType.swift diff --git a/Shared/Extensions/EnvironmentValue.swift b/Shared/Extensions/EnvironmentValue.swift index 8757e509d..9edb9ad73 100644 --- a/Shared/Extensions/EnvironmentValue.swift +++ b/Shared/Extensions/EnvironmentValue.swift @@ -9,6 +9,7 @@ import SwiftUI // TODO: Look at name spacing +// TODO: Consistent naming: ...Key struct AudioOffset: EnvironmentKey { static let defaultValue: Binding = .constant(0) diff --git a/Shared/Extensions/ViewExtensions/Modifiers/PaddingMultiplierModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/PaddingMultiplierModifier.swift new file mode 100644 index 000000000..ee297f0a0 --- /dev/null +++ b/Shared/Extensions/ViewExtensions/Modifiers/PaddingMultiplierModifier.swift @@ -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) + } + } +} diff --git a/Shared/Extensions/ViewExtensions/Modifiers/RatioCornerRadiusModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/RatioCornerRadiusModifier.swift new file mode 100644 index 000000000..a7fe48188 --- /dev/null +++ b/Shared/Extensions/ViewExtensions/Modifiers/RatioCornerRadiusModifier.swift @@ -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 + + func body(content: Content) -> some View { + content + .cornerRadius(cornerRadius, corners: corners) + .onSizeChanged { newSize in + cornerRadius = newSize[keyPath: side] * ratio + } + } +} diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift index 2e79f53de..473b95ce9 100644 --- a/Shared/Extensions/ViewExtensions/ViewExtensions.swift +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -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) - } - } - } - - func posterStyle(type: PosterType, height: CGFloat) -> some View { - Group { - switch type { - case .portrait: - self.portraitPoster(height: height) - case .landscape: - self.landscapePoster(height: height) - } + // 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) } } - 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) -> 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, 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) diff --git a/Shared/Objects/MenuPosterHStackModel.swift b/Shared/Objects/MenuPosterHStackModel.swift index e40b83a7e..2d85ae408 100644 --- a/Shared/Objects/MenuPosterHStackModel.swift +++ b/Shared/Objects/MenuPosterHStackModel.swift @@ -15,7 +15,7 @@ protocol MenuPosterHStackModel: ObservableObject { associatedtype Item: Poster var menuSelection: Section? { get } - var menuSections: [Section: [PosterButtonType]] { get set } + var menuSections: [Section: [Item]] { get set } var menuSectionSort: (Section, Section) -> Bool { get } func select(section: Section) diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift index 44a665f86..8be5572c8 100644 --- a/Shared/Objects/Poster.swift +++ b/Shared/Objects/Poster.swift @@ -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] { [] diff --git a/Shared/Objects/PosterButtonType.swift b/Shared/Objects/PosterButtonType.swift deleted file mode 100644 index 571e86295..000000000 --- a/Shared/Objects/PosterButtonType.swift +++ /dev/null @@ -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: 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 - } - } -} diff --git a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift index 99c2800c8..8fc8abeee 100644 --- a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift @@ -17,7 +17,7 @@ final class SeriesItemViewModel: ItemViewModel, MenuPosterHStackModel { @Published var menuSelection: BaseItemDto? @Published - var menuSections: [BaseItemDto: [PosterButtonType]] + 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 } } } diff --git a/Shared/ViewModels/SpecialFeaturesViewModel.swift b/Shared/ViewModels/SpecialFeaturesViewModel.swift index aea798cdc..7ebb5fc6d 100644 --- a/Shared/ViewModels/SpecialFeaturesViewModel.swift +++ b/Shared/ViewModels/SpecialFeaturesViewModel.swift @@ -14,10 +14,10 @@ class SpecialFeaturesViewModel: ViewModel, MenuPosterHStackModel { @Published var menuSelection: SpecialFeatureType? @Published - var menuSections: [SpecialFeatureType: [PosterButtonType]] + var menuSections: [SpecialFeatureType: [BaseItemDto]] var menuSectionSort: (SpecialFeatureType, SpecialFeatureType) -> Bool - init(sections: [SpecialFeatureType: [PosterButtonType]]) { + 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 diff --git a/Swiftfin tvOS/Components/NonePosterButton.swift b/Swiftfin tvOS/Components/NonePosterButton.swift index e163bb028..294b46fd6 100644 --- a/Swiftfin tvOS/Components/NonePosterButton.swift +++ b/Swiftfin tvOS/Components/NonePosterButton.swift @@ -29,7 +29,8 @@ struct NonePosterButton: View { .foregroundColor(.secondary) } } - .posterStyle(type: type, width: type.width) + .posterStyle(type) + .frame(width: type.width) } } .buttonStyle(.card) diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index d8f4808c7..1762fe3f5 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -47,19 +47,20 @@ struct PosterButton: 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) diff --git a/Swiftfin tvOS/Components/SeeAllPosterButton.swift b/Swiftfin tvOS/Components/SeeAllPosterButton.swift index ef03a4ad0..db6b67df9 100644 --- a/Swiftfin tvOS/Components/SeeAllPosterButton.swift +++ b/Swiftfin tvOS/Components/SeeAllPosterButton.swift @@ -29,7 +29,8 @@ struct SeeAllPosterButton: View { .font(.title3) } } - .posterStyle(type: type, width: type.width) + .posterStyle(type) + .frame(width: type.width) } .buttonStyle(.card) } diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift index 8ca8b6f9b..f26235c0a 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift @@ -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 { diff --git a/Swiftfin tvOS/Views/MediaView.swift b/Swiftfin tvOS/Views/MediaView.swift index 8aac222b3..b5dc0d7e4 100644 --- a/Swiftfin tvOS/Views/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView.swift @@ -93,7 +93,8 @@ extension MediaView { } } } - .posterStyle(type: .landscape, width: itemWidth) + .posterStyle(.landscape) + .frame(width: itemWidth) } .buttonStyle(.card) } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index a280368c9..445034cf7 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -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 = ""; }; E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = ""; }; E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = ""; }; + E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatioCornerRadiusModifier.swift; sourceTree = ""; }; + E13317002ADE4B8D009BF865 /* PaddingMultiplierModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingMultiplierModifier.swift; sourceTree = ""; }; E133328729538D8D00EE76AB /* Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Files.swift; sourceTree = ""; }; E133328C2953AE4B00EE76AB /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; E133328E2953B71000EE76AB /* DownloadTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskView.swift; sourceTree = ""; }; @@ -1072,7 +1076,6 @@ E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = ""; }; E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatable.swift; sourceTree = ""; }; E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = ""; }; - E17665D828E80F0F00130507 /* PosterButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterButtonType.swift; sourceTree = ""; }; E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = ""; }; E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Swiftfin/Components/LibraryItemRow.swift b/Swiftfin/Components/LibraryItemRow.swift index 95e5a26fe..24198f9ef 100644 --- a/Swiftfin/Components/LibraryItemRow.swift +++ b/Swiftfin/Components/LibraryItemRow.swift @@ -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) { diff --git a/Swiftfin/Components/MenuPosterHStack.swift b/Swiftfin/Components/MenuPosterHStack.swift index 9290a2d19..b100dad45 100644 --- a/Swiftfin/Components/MenuPosterHStack.swift +++ b/Swiftfin/Components/MenuPosterHStack.swift @@ -17,9 +17,9 @@ struct MenuPosterHStack: View { private let type: PosterType private var itemScale: CGFloat private let singleImage: Bool - private var content: (PosterButtonType) -> any View - private var imageOverlay: (PosterButtonType) -> any View - private var contextMenu: (PosterButtonType) -> 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: View { .fixedSize() } - private var items: [PosterButtonType] { + 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) -> any View) -> Self { + func content(@ViewBuilder _ content: @escaping (Model.Item) -> any View) -> Self { copy(modifying: \.content, with: content) } - func imageOverlay(@ViewBuilder _ content: @escaping (PosterButtonType) -> any View) -> Self { + func imageOverlay(@ViewBuilder _ content: @escaping (Model.Item) -> any View) -> Self { copy(modifying: \.imageOverlay, with: content) } - func contextMenu(@ViewBuilder _ content: @escaping (PosterButtonType) -> any View) -> Self { + func contextMenu(@ViewBuilder _ content: @escaping (Model.Item) -> any View) -> Self { copy(modifying: \.contextMenu, with: content) } diff --git a/Swiftfin/Components/PagingLibraryView.swift b/Swiftfin/Components/PagingLibraryView.swift index e4d6a51d7..57ef52357 100644 --- a/Swiftfin/Components/PagingLibraryView.swift +++ b/Swiftfin/Components/PagingLibraryView.swift @@ -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) diff --git a/Swiftfin/Components/PosterButton.swift b/Swiftfin/Components/PosterButton.swift index 28da24c65..c01f36646 100644 --- a/Swiftfin/Components/PosterButton.swift +++ b/Swiftfin/Components/PosterButton.swift @@ -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: View { - private var state: PosterButtonType + private var item: Item private var type: PosterType private var itemScale: CGFloat - private var horizontalAlignment: HorizontalAlignment - private var content: (PosterButtonType) -> any View - private var imageOverlay: (PosterButtonType) -> any View - private var contextMenu: (PosterButtonType) -> 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: 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: View { extension PosterButton { init( - state: PosterButtonType, + 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) -> any View) -> Self { + func content(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { copy(modifying: \.content, with: content) } - func imageOverlay(@ViewBuilder _ content: @escaping (PosterButtonType) -> any View) -> Self { + func imageOverlay(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { copy(modifying: \.imageOverlay, with: content) } - func contextMenu(@ViewBuilder _ content: @escaping (PosterButtonType) -> 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 + 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 + 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) } } } diff --git a/Swiftfin/Components/PosterHStack.swift b/Swiftfin/Components/PosterHStack.swift index feedddc35..b587c4b25 100644 --- a/Swiftfin/Components/PosterHStack.swift +++ b/Swiftfin/Components/PosterHStack.swift @@ -15,12 +15,12 @@ struct PosterHStack: View { private var header: () -> any View private var title: String? private var type: PosterType - private var items: [PosterButtonType] + private var items: [Item] private var singleImage: Bool private var itemScale: CGFloat - private var content: (PosterButtonType) -> any View - private var imageOverlay: (PosterButtonType) -> any View - private var contextMenu: (PosterButtonType) -> 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: 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: 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: 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], - 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], + 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) -> any View) -> Self { + func content(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { copy(modifying: \.content, with: content) } - func imageOverlay(@ViewBuilder _ content: @escaping (PosterButtonType) -> any View) -> Self { + func imageOverlay(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { copy(modifying: \.imageOverlay, with: content) } - func contextMenu(@ViewBuilder _ content: @escaping (PosterButtonType) -> any View) -> Self { + func contextMenu(@ViewBuilder _ content: @escaping (Item) -> any View) -> Self { copy(modifying: \.contextMenu, with: content) } diff --git a/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift index ef98e228a..b8e4e2403 100644 --- a/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift +++ b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift @@ -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) diff --git a/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift index 8e1f74e30..c40cab019 100644 --- a/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift +++ b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift @@ -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) ) } diff --git a/Swiftfin/Views/DownloadListView.swift b/Swiftfin/Views/DownloadListView.swift index 026127878..f887a7f00 100644 --- a/Swiftfin/Views/DownloadListView.swift +++ b/Swiftfin/Views/DownloadListView.swift @@ -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) { diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift index 17aa6b717..f679680d1 100644 --- a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift @@ -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) diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index 35dc33ef0..c963ccedd 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -23,8 +23,8 @@ extension HomeView { @ObservedObject var viewModel: LibraryViewModel - private var items: [PosterButtonType] { - viewModel.items.prefix(20).asArray.map { .item($0) } + private var items: [BaseItemDto] { + viewModel.items.prefix(20).asArray } var body: some View { diff --git a/Swiftfin/Views/HomeView/Components/NextUpView.swift b/Swiftfin/Views/HomeView/Components/NextUpView.swift index a8eddb2eb..be29b718f 100644 --- a/Swiftfin/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin/Views/HomeView/Components/NextUpView.swift @@ -23,8 +23,8 @@ extension HomeView { @ObservedObject var viewModel: NextUpLibraryViewModel - private var items: [PosterButtonType] { - 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 diff --git a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift index 64758a887..5d85fb431 100644 --- a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift @@ -23,8 +23,8 @@ extension HomeView { @ObservedObject var viewModel: ItemTypeLibraryViewModel - private var items: [PosterButtonType] { - viewModel.items.prefix(20).asArray.map { .item($0) } + private var items: [BaseItemDto] { + viewModel.items.prefix(20).asArray } var body: some View { diff --git a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift index 131efd7e3..d38d62d3b 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift @@ -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) diff --git a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift index d867e8954..cc57af0ae 100644 --- a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift @@ -26,7 +26,6 @@ extension ItemView { .filter(\.isDisplayed) .prefix(20) .asArray - .map { .item($0) } ) .trailing { SeeAllButton() diff --git a/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift b/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift index 80a292e3c..774184cb7 100644 --- a/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift +++ b/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift @@ -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 + 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 - - Image(systemName: "checkmark.circle.fill") - .resizable() - .frame(width: 30, height: 30, alignment: .bottomTrailing) - .accentSymbolRendering(accentColor: .white) - .padding() - } + 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() } - } else { - EmptyView() } } } @@ -78,71 +74,43 @@ extension SeriesEpisodeSelector { @ScaledMetric private var staticOverviewHeight: CGFloat = 50 - let type: PosterButtonType + 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) - - if episode.isUnaired { - Text(episode.airDateLabel ?? L10n.noOverviewAvailable) - } else { - Text(episode.overview ?? L10n.noOverviewAvailable) - } + ZStack(alignment: .topLeading) { + Color.clear + .frame(height: staticOverviewHeight) + + 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 diff --git a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift index b59da6d8a..9d6db0c2f 100644 --- a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift @@ -26,7 +26,7 @@ extension ItemView { PosterHStack( title: L10n.recommended, type: similarPosterType, - items: items.map { .item($0) } + items: items ) .trailing { SeeAllButton() diff --git a/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift b/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift index 9b6312f7b..6258216e3 100644 --- a/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift @@ -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 } diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift index 21f0289a5..7197310aa 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift @@ -19,14 +19,6 @@ extension CollectionItemView { @ObservedObject var viewModel: CollectionItemViewModel - private var items: [PosterButtonType] { - 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) diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift index 3026bba01..ad636c3b4 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift @@ -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 diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift index 3aaf58760..9f5d47bc9 100644 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift @@ -19,14 +19,6 @@ extension iPadOSCollectionItemView { @ObservedObject var viewModel: CollectionItemViewModel - private var items: [PosterButtonType] { - 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) diff --git a/Swiftfin/Views/MediaView.swift b/Swiftfin/Views/MediaView.swift index b9faaccfb..d2323948e 100644 --- a/Swiftfin/Views/MediaView.swift +++ b/Swiftfin/Views/MediaView.swift @@ -111,7 +111,8 @@ extension MediaView { } } } - .posterStyle(type: .landscape, width: itemWidth) + .posterStyle(.landscape) + .frame(width: itemWidth) } } } diff --git a/Swiftfin/Views/SearchView.swift b/Swiftfin/Views/SearchView.swift index 9c95d482e..ec947500f 100644 --- a/Swiftfin/Views/SearchView.swift +++ b/Swiftfin/Views/SearchView.swift @@ -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) diff --git a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 6863ccf62..4e678ba85 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -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 {