147 lines
3.7 KiB
Swift
147 lines
3.7 KiB
Swift
//
|
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
//
|
|
|
|
import BlurHashKit
|
|
import JellyfinAPI
|
|
import Nuke
|
|
import NukeUI
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
private let imagePipeline = ImagePipeline(configuration: .withDataCache)
|
|
|
|
// TODO: Binding inits?
|
|
// - instead of removing first source on failure, just safe index into sources
|
|
struct ImageView: View {
|
|
|
|
@State
|
|
private var sources: [ImageSource]
|
|
|
|
private var image: (Image) -> any View
|
|
private var placeholder: (() -> any View)?
|
|
private var failure: () -> any View
|
|
|
|
@ViewBuilder
|
|
private func _placeholder(_ currentSource: ImageSource) -> some View {
|
|
if let placeholder = placeholder {
|
|
placeholder()
|
|
.eraseToAnyView()
|
|
} else {
|
|
DefaultPlaceholderView(blurHash: currentSource.blurHash)
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
if let currentSource = sources.first {
|
|
LazyImage(url: currentSource.url, transaction: .init(animation: .linear)) { state in
|
|
if state.isLoading {
|
|
_placeholder(currentSource)
|
|
} else if let _image = state.image {
|
|
_image
|
|
.resizable()
|
|
} else if state.error != nil {
|
|
failure()
|
|
.eraseToAnyView()
|
|
.onAppear {
|
|
sources.removeFirstSafe()
|
|
}
|
|
}
|
|
}
|
|
.pipeline(imagePipeline)
|
|
} else {
|
|
failure()
|
|
.eraseToAnyView()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ImageView {
|
|
|
|
init(_ source: ImageSource) {
|
|
self.init(
|
|
sources: [source].compacted(using: \.url),
|
|
image: { $0 },
|
|
placeholder: nil,
|
|
failure: { DefaultFailureView() }
|
|
)
|
|
}
|
|
|
|
init(_ sources: [ImageSource]) {
|
|
self.init(
|
|
sources: sources.compacted(using: \.url),
|
|
image: { $0 },
|
|
placeholder: nil,
|
|
failure: { DefaultFailureView() }
|
|
)
|
|
}
|
|
|
|
init(_ source: URL?) {
|
|
self.init(
|
|
sources: [ImageSource(url: source)],
|
|
image: { $0 },
|
|
placeholder: nil,
|
|
failure: { DefaultFailureView() }
|
|
)
|
|
}
|
|
|
|
init(_ sources: [URL?]) {
|
|
let imageSources = sources
|
|
.compactMap { $0 }
|
|
.map { ImageSource(url: $0) }
|
|
|
|
self.init(
|
|
sources: imageSources,
|
|
image: { $0 },
|
|
placeholder: nil,
|
|
failure: { DefaultFailureView() }
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Extensions
|
|
|
|
extension ImageView {
|
|
|
|
func image(@ViewBuilder _ content: @escaping (Image) -> any View) -> Self {
|
|
copy(modifying: \.image, with: content)
|
|
}
|
|
|
|
func placeholder(@ViewBuilder _ content: @escaping () -> any View) -> Self {
|
|
copy(modifying: \.placeholder, with: content)
|
|
}
|
|
|
|
func failure(@ViewBuilder _ content: @escaping () -> any View) -> Self {
|
|
copy(modifying: \.failure, with: content)
|
|
}
|
|
}
|
|
|
|
// MARK: Defaults
|
|
|
|
extension ImageView {
|
|
|
|
struct DefaultFailureView: View {
|
|
|
|
var body: some View {
|
|
Color.secondarySystemFill
|
|
}
|
|
}
|
|
|
|
struct DefaultPlaceholderView: View {
|
|
|
|
let blurHash: String?
|
|
|
|
var body: some View {
|
|
if let blurHash {
|
|
BlurHashView(blurHash: blurHash, size: .Square(length: 8))
|
|
} else {
|
|
Color.secondarySystemFill
|
|
}
|
|
}
|
|
}
|
|
}
|