diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 8c353cb7..9161e3ff 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -92,6 +92,19 @@ public extension BaseItemDto { return URL(string: urlString)! } + func getThumbImage(maxWidth: Int) -> URL { + let imageType = ImageType.thumb + let imageItemId = id ?? "" + + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, + imageType: imageType, + maxWidth: Int(x), + quality: 96).URLString + return URL(string: urlString)! + } + func getEpisodeLocator() -> String? { if let seasonNo = parentIndexNumber, let episodeNo = indexNumber { return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo)) diff --git a/Shared/Views/ImageView.swift b/Shared/Views/ImageView.swift index 49a32be0..27ef4a24 100644 --- a/Shared/Views/ImageView.swift +++ b/Shared/Views/ImageView.swift @@ -10,26 +10,49 @@ import NukeUI import SwiftUI struct ImageView: View { - private let source: URL + + private let sources: [URL] private let blurhash: String private let failureInitials: String init(src: URL, bh: String = "001fC^", failureInitials: String = "") { - self.source = src + self.sources = [src] self.blurhash = bh self.failureInitials = failureInitials } - // TODO: fix placeholder hash image + init(sources: [URL], bh: String = "001fC^", failureInitials: String = "") { + assert(!sources.isEmpty, "Must supply at least one source") + + self.sources = sources + self.blurhash = bh + self.failureInitials = failureInitials + } + + // TODO: fix placeholder hash view @ViewBuilder - private var placeholderImage: some View { - Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 8, height: 8)) ?? - UIImage(blurHash: "001fC^", size: CGSize(width: 8, height: 8))!) - .resizable() + private func placeholderView() -> some View { +// Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 8, height: 8)) ?? +// UIImage(blurHash: "001fC^", size: CGSize(width: 8, height: 8))!) +// .resizable() + + #if os(tvOS) + ZStack { + Color.black.ignoresSafeArea() + + ProgressView() + } + #else + ZStack { + Color.gray.ignoresSafeArea() + + ProgressView() + } + #endif } @ViewBuilder - private var failureImage: some View { + private func failureImage() -> some View { ZStack { Rectangle() .foregroundColor(Color(UIColor.darkGray)) @@ -42,25 +65,66 @@ struct ImageView: View { } var body: some View { - LazyImage(source: source) { state in + ImageViewBackgroundA(index: 0, + sources: sources, + placeholderView: placeholderView, + failureView: failureImage) + } +} + +// Two image view are necessary to switch between one another to appease the type system +// as a recursive view with itself isn't valid + +fileprivate struct ImageViewBackgroundA: View { + + let index: Int + let sources: [URL] + let placeholderView: () -> PlaceholderView + let failureView: () -> FailureView + + var body: some View { + LazyImage(source: sources[index]) { state in if let image = state.image { image } else if state.error != nil { - failureImage + if index + 1 == sources.count { + failureView() + } else { + ImageViewBackgroundB(index: index + 1, + sources: sources, + placeholderView: placeholderView, + failureView: failureView) + } } else { - #if os(tvOS) - ZStack { - Color.black.ignoresSafeArea() - - ProgressView() - } - #else - ZStack { - Color.gray.ignoresSafeArea() - - ProgressView() - } - #endif + placeholderView() + } + } + .pipeline(ImagePipeline(configuration: .withDataCache)) + } +} + +fileprivate struct ImageViewBackgroundB: View { + + let index: Int + let sources: [URL] + let placeholderView: () -> PlaceholderView + let failureView: () -> FailureView + + var body: some View { + LazyImage(source: sources[index]) { state in + if let image = state.image { + image + } else if state.error != nil { + if index + 1 == sources.count { + failureView() + } else { + ImageViewBackgroundA(index: index + 1, + sources: sources, + placeholderView: placeholderView, + failureView: failureView) + } + } else { + placeholderView() } } .pipeline(ImagePipeline(configuration: .withDataCache)) diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift index 568ed84a..d596a05b 100644 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift @@ -27,7 +27,11 @@ struct CinematicNextUpCardView: View { ImageView(src: item.getSeriesBackdropImage(maxWidth: 350)) .frame(width: 350, height: 210) } else { - ImageView(src: item.getBackdropImage(maxWidth: 350)) + ImageView(sources: [ + item.getThumbImage(maxWidth: 320), + item.getBackdropImage(maxWidth: 320), + ], + bh: item.getBackdropImageBlurHash()) .frame(width: 350, height: 210) } diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift index 8586b5b4..0ef397a7 100644 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift +++ b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift @@ -28,7 +28,11 @@ struct CinematicResumeCardView: View { ImageView(src: item.getSeriesBackdropImage(maxWidth: 350)) .frame(width: 350, height: 210) } else { - ImageView(src: item.getBackdropImage(maxWidth: 350)) + ImageView(sources: [ + item.getThumbImage(maxWidth: 320), + item.getBackdropImage(maxWidth: 320), + ], + bh: item.getBackdropImageBlurHash()) .frame(width: 350, height: 210) } diff --git a/Swiftfin/Views/ContinueWatchingView.swift b/Swiftfin/Views/ContinueWatchingView.swift index cd32af82..47d2525a 100644 --- a/Swiftfin/Views/ContinueWatchingView.swift +++ b/Swiftfin/Views/ContinueWatchingView.swift @@ -27,7 +27,11 @@ struct ContinueWatchingView: View { VStack(alignment: .leading) { ZStack { - ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) + ImageView(sources: [ + item.getThumbImage(maxWidth: 320), + item.getBackdropImage(maxWidth: 320), + ], + bh: item.getBackdropImageBlurHash()) .frame(width: 320, height: 180) .accessibilityIgnoresInvertColors()