`ImageView` Refactor (#517)
This commit is contained in:
parent
70b75df110
commit
6f937ceddd
|
@ -16,16 +16,18 @@ extension BaseItemDto {
|
|||
|
||||
func imageURL(
|
||||
_ type: ImageType,
|
||||
maxWidth: Int
|
||||
maxWidth: Int? = nil,
|
||||
maxHeight: Int? = nil
|
||||
) -> URL {
|
||||
_imageURL(type, maxWidth: maxWidth, itemID: id ?? "")
|
||||
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "")
|
||||
}
|
||||
|
||||
func imageURL(
|
||||
_ type: ImageType,
|
||||
maxWidth: CGFloat
|
||||
maxWidth: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil
|
||||
) -> URL {
|
||||
_imageURL(type, maxWidth: Int(maxWidth), itemID: id ?? "")
|
||||
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: id ?? "")
|
||||
}
|
||||
|
||||
func blurHash(_ type: ImageType) -> String? {
|
||||
|
@ -39,26 +41,28 @@ extension BaseItemDto {
|
|||
return nil
|
||||
}
|
||||
|
||||
func imageSource(_ type: ImageType, maxWidth: Int) -> ImageSource {
|
||||
_imageSource(type, maxWidth: maxWidth)
|
||||
func imageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource {
|
||||
_imageSource(type, maxWidth: maxWidth, maxHeight: maxHeight)
|
||||
}
|
||||
|
||||
func imageSource(_ type: ImageType, maxWidth: CGFloat) -> ImageSource {
|
||||
_imageSource(type, maxWidth: Int(maxWidth))
|
||||
func imageSource(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> ImageSource {
|
||||
_imageSource(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight))
|
||||
}
|
||||
|
||||
// MARK: Series Images
|
||||
|
||||
func seriesImageURL(_ type: ImageType, maxWidth: Int) -> URL {
|
||||
_imageURL(type, maxWidth: maxWidth, itemID: seriesId ?? "")
|
||||
func seriesImageURL(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> URL {
|
||||
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesId ?? "")
|
||||
}
|
||||
|
||||
func seriesImageURL(_ type: ImageType, maxWidth: CGFloat) -> URL {
|
||||
_imageURL(type, maxWidth: Int(maxWidth), itemID: seriesId ?? "")
|
||||
func seriesImageURL(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> URL {
|
||||
let maxWidth = maxWidth != nil ? Int(maxWidth!) : nil
|
||||
let maxHeight = maxHeight != nil ? Int(maxHeight!) : nil
|
||||
return _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesId ?? "")
|
||||
}
|
||||
|
||||
func seriesImageSource(_ type: ImageType, maxWidth: Int) -> ImageSource {
|
||||
let url = _imageURL(type, maxWidth: maxWidth, itemID: seriesId ?? "")
|
||||
func seriesImageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource {
|
||||
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesId ?? "")
|
||||
return ImageSource(url: url, blurHash: nil)
|
||||
}
|
||||
|
||||
|
@ -70,22 +74,35 @@ extension BaseItemDto {
|
|||
|
||||
fileprivate func _imageURL(
|
||||
_ type: ImageType,
|
||||
maxWidth: Int,
|
||||
maxWidth: Int?,
|
||||
maxHeight: Int?,
|
||||
itemID: String
|
||||
) -> URL {
|
||||
let scaleWidth = UIScreen.main.scale(maxWidth)
|
||||
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
|
||||
let scaleHeight = maxHeight == nil ? nil : UIScreen.main.scale(maxHeight!)
|
||||
let tag = imageTags?[type.rawValue]
|
||||
return ImageAPI.getItemImageWithRequestBuilder(
|
||||
itemId: itemID,
|
||||
imageType: type,
|
||||
maxWidth: scaleWidth,
|
||||
maxHeight: scaleHeight,
|
||||
tag: tag
|
||||
).url
|
||||
}
|
||||
|
||||
fileprivate func _imageSource(_ type: ImageType, maxWidth: Int) -> ImageSource {
|
||||
let url = _imageURL(type, maxWidth: maxWidth, itemID: id ?? "")
|
||||
fileprivate func _imageSource(_ type: ImageType, maxWidth: Int?, maxHeight: Int?) -> ImageSource {
|
||||
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: id ?? "")
|
||||
let blurHash = blurHash(type)
|
||||
return ImageSource(url: url, blurHash: blurHash)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Int {
|
||||
init?(_ source: CGFloat?) {
|
||||
if let source = source {
|
||||
self.init(source)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +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) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import BlurHashKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct BlurHashView: UIViewRepresentable {
|
||||
|
||||
private let blurHash: String
|
||||
private let size: CGSize
|
||||
|
||||
init(blurHash: String, size: CGSize = .Circle(radius: 12)) {
|
||||
self.blurHash = blurHash
|
||||
self.size = size
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIBlurHashView {
|
||||
UIBlurHashView(blurHash, size: size)
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIBlurHashView, context: Context) {}
|
||||
}
|
||||
|
||||
class UIBlurHashView: UIView {
|
||||
|
||||
private let imageView: UIImageView
|
||||
|
||||
init(_ blurHash: String, size: CGSize) {
|
||||
let imageView = UIImageView()
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.imageView = imageView
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
computeBlurHashImageAsync(blurHash: blurHash, size: size) { [weak self] blurImage in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.imageView.image = blurImage
|
||||
self.imageView.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
addSubview(imageView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
imageView.leftAnchor.constraint(equalTo: leftAnchor),
|
||||
imageView.rightAnchor.constraint(equalTo: rightAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func computeBlurHashImageAsync(blurHash: String, size: CGSize, _ completion: @escaping (UIImage?) -> Void) {
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let image = UIImage(blurHash: blurHash, size: size)
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,13 @@
|
|||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import BlurHashKit
|
||||
import Nuke
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ImageSource {
|
||||
struct ImageSource: Hashable {
|
||||
let url: URL?
|
||||
let blurHash: String?
|
||||
|
||||
|
@ -28,91 +29,145 @@ struct DefaultFailureView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct ImageView<FailureView: View>: View {
|
||||
struct ImageView<ImageType: View, PlaceholderView: View, FailureView: View>: View {
|
||||
|
||||
@State
|
||||
private var sources: [ImageSource]
|
||||
private var currentURL: URL? { sources.first?.url }
|
||||
private var currentBlurHash: String? { sources.first?.blurHash }
|
||||
private var failureView: () -> FailureView
|
||||
private var image: (NukeUI.Image) -> ImageType
|
||||
private var placeholder: (() -> PlaceholderView)?
|
||||
private var failure: () -> FailureView
|
||||
private var resizingMode: ImageResizingMode
|
||||
|
||||
init(
|
||||
_ source: URL?,
|
||||
blurHash: String? = nil,
|
||||
resizingMode: ImageResizingMode = .aspectFill,
|
||||
@ViewBuilder failureView: @escaping () -> FailureView
|
||||
) {
|
||||
let imageSource = ImageSource(url: source, blurHash: blurHash)
|
||||
self.init(imageSource, resizingMode: resizingMode, failureView: failureView)
|
||||
}
|
||||
|
||||
init(
|
||||
_ source: ImageSource,
|
||||
resizingMode: ImageResizingMode = .aspectFill,
|
||||
@ViewBuilder failureView: @escaping () -> FailureView
|
||||
) {
|
||||
self.init([source], resizingMode: resizingMode, failureView: failureView)
|
||||
}
|
||||
|
||||
init(
|
||||
private init(
|
||||
_ sources: [ImageSource],
|
||||
resizingMode: ImageResizingMode = .aspectFill,
|
||||
resizingMode: ImageResizingMode,
|
||||
@ViewBuilder image: @escaping (NukeUI.Image) -> ImageType,
|
||||
placeHolder: (() -> PlaceholderView)?,
|
||||
@ViewBuilder failureView: @escaping () -> FailureView
|
||||
) {
|
||||
_sources = State(initialValue: sources)
|
||||
self.resizingMode = resizingMode
|
||||
self.failureView = failureView
|
||||
self.image = image
|
||||
self.placeholder = placeHolder
|
||||
self.failure = failureView
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var placeholderView: some View {
|
||||
if let currentBlurHash = currentBlurHash {
|
||||
BlurHashView(blurHash: currentBlurHash)
|
||||
.id(currentBlurHash)
|
||||
private func _placeholder(_ currentSource: ImageSource) -> some View {
|
||||
if let placeholder = placeholder {
|
||||
placeholder()
|
||||
} else if let blurHash = currentSource.blurHash {
|
||||
BlurHashView(blurHash: blurHash, size: .Circle(radius: 16))
|
||||
} else {
|
||||
Color.clear
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let currentURL = currentURL {
|
||||
LazyImage(source: currentURL) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizingMode(resizingMode)
|
||||
} else if state.error != nil {
|
||||
placeholderView.onAppear {
|
||||
sources.removeFirst()
|
||||
}
|
||||
if let currentSource = sources.first {
|
||||
LazyImage(url: currentSource.url) { state in
|
||||
if state.isLoading {
|
||||
_placeholder(currentSource)
|
||||
} else if let _image = state.image {
|
||||
image(_image.resizingMode(resizingMode))
|
||||
} else {
|
||||
placeholderView
|
||||
failure()
|
||||
}
|
||||
}
|
||||
.pipeline(ImagePipeline(configuration: .withDataCache))
|
||||
.id(currentURL)
|
||||
.id(currentSource)
|
||||
} else {
|
||||
failureView()
|
||||
failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageView where FailureView == DefaultFailureView {
|
||||
init(_ source: URL?, blurHash: String? = nil, resizingMode: ImageResizingMode = .aspectFill) {
|
||||
let imageSource = ImageSource(url: source, blurHash: blurHash)
|
||||
self.init([imageSource], resizingMode: resizingMode, failureView: { DefaultFailureView() })
|
||||
extension ImageView where ImageType == NukeUI.Image, PlaceholderView == EmptyView, FailureView == DefaultFailureView {
|
||||
init(_ source: ImageSource) {
|
||||
self.init(
|
||||
[source],
|
||||
resizingMode: .aspectFill,
|
||||
image: { $0 },
|
||||
placeHolder: nil,
|
||||
failureView: { DefaultFailureView() }
|
||||
)
|
||||
}
|
||||
|
||||
init(_ source: ImageSource, resizingMode: ImageResizingMode = .aspectFill) {
|
||||
self.init([source], resizingMode: resizingMode, failureView: { DefaultFailureView() })
|
||||
init(_ sources: [ImageSource]) {
|
||||
self.init(
|
||||
sources,
|
||||
resizingMode: .aspectFill,
|
||||
image: { $0 },
|
||||
placeHolder: nil,
|
||||
failureView: { DefaultFailureView() }
|
||||
)
|
||||
}
|
||||
|
||||
init(_ sources: [ImageSource], resizingMode: ImageResizingMode = .aspectFill) {
|
||||
self.init(sources, resizingMode: resizingMode, failureView: { DefaultFailureView() })
|
||||
init(_ source: URL?) {
|
||||
self.init(
|
||||
[ImageSource(url: source, blurHash: nil)],
|
||||
resizingMode: .aspectFill,
|
||||
image: { $0 },
|
||||
placeHolder: nil,
|
||||
failureView: { DefaultFailureView() }
|
||||
)
|
||||
}
|
||||
|
||||
init(sources: [URL], resizingMode: ImageResizingMode = .aspectFill) {
|
||||
let imageSources = sources.compactMap { ImageSource(url: $0, blurHash: nil) }
|
||||
self.init(imageSources, resizingMode: resizingMode, failureView: { DefaultFailureView() })
|
||||
init(_ sources: [URL?]) {
|
||||
self.init(
|
||||
sources.map { ImageSource(url: $0, blurHash: nil) },
|
||||
resizingMode: .aspectFill,
|
||||
image: { $0 },
|
||||
placeHolder: nil,
|
||||
failureView: { DefaultFailureView() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Extensions
|
||||
|
||||
extension ImageView {
|
||||
@ViewBuilder
|
||||
func image<I: View>(@ViewBuilder _ content: @escaping (NukeUI.Image) -> I) -> ImageView<I, PlaceholderView, FailureView> {
|
||||
ImageView<I, PlaceholderView, FailureView>(
|
||||
sources,
|
||||
resizingMode: resizingMode,
|
||||
image: content,
|
||||
placeHolder: placeholder,
|
||||
failureView: failure
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func placeholder<P: View>(@ViewBuilder _ content: @escaping () -> P) -> ImageView<ImageType, P, FailureView> {
|
||||
ImageView<ImageType, P, FailureView>(
|
||||
sources,
|
||||
resizingMode: resizingMode,
|
||||
image: image,
|
||||
placeHolder: content,
|
||||
failureView: failure
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func failure<F: View>(@ViewBuilder _ content: @escaping () -> F) -> ImageView<ImageType, PlaceholderView, F> {
|
||||
ImageView<ImageType, PlaceholderView, F>(
|
||||
sources,
|
||||
resizingMode: resizingMode,
|
||||
image: image,
|
||||
placeHolder: placeholder,
|
||||
failureView: content
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func resizingMode(_ resizingMode: ImageResizingMode) -> ImageView<ImageType, PlaceholderView, FailureView> {
|
||||
ImageView<ImageType, PlaceholderView, FailureView>(
|
||||
sources,
|
||||
resizingMode: resizingMode,
|
||||
image: image,
|
||||
placeHolder: placeholder,
|
||||
failureView: failure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,11 @@ struct PortraitButton<Item: PortraitPoster>: View {
|
|||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
ImageView(
|
||||
item.portraitPosterImageSource(maxWidth: 300),
|
||||
failureView: {
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: 270))
|
||||
.failure {
|
||||
InitialFailureView(item.title.initials)
|
||||
}
|
||||
)
|
||||
.frame(width: 270, height: 405)
|
||||
.frame(width: 270, height: 405)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
|
||||
|
|
|
@ -39,10 +39,9 @@ extension CollectionItemView {
|
|||
.id("topContentDivider")
|
||||
|
||||
if showLogo {
|
||||
ImageView(
|
||||
viewModel.item.imageSource(.logo, maxWidth: 500),
|
||||
resizingMode: .aspectFit,
|
||||
failureView: {
|
||||
ImageView(viewModel.item.imageSource(.logo, maxWidth: 500, maxHeight: 150))
|
||||
.resizingMode(.aspectFit)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -50,9 +49,8 @@ extension CollectionItemView {
|
|||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
)
|
||||
.frame(width: 500, height: 150)
|
||||
.padding(.top, 5)
|
||||
.frame(width: 500, height: 150)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
|
||||
PortraitPosterHStack(
|
||||
|
|
|
@ -36,11 +36,11 @@ extension ItemView {
|
|||
HStack {
|
||||
ImageView(
|
||||
viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel.item
|
||||
.imageSource(.primary, maxWidth: 300),
|
||||
failureView: {
|
||||
InitialFailureView(viewModel.item.title.initials)
|
||||
}
|
||||
.imageSource(.primary, maxWidth: 300)
|
||||
)
|
||||
.failure {
|
||||
InitialFailureView(viewModel.item.title.initials)
|
||||
}
|
||||
.portraitPoster(width: 270)
|
||||
|
||||
AboutViewCard(
|
||||
|
|
|
@ -39,10 +39,9 @@ extension MovieItemView {
|
|||
.id("topContentDivider")
|
||||
|
||||
if showLogo {
|
||||
ImageView(
|
||||
viewModel.item.imageSource(.logo, maxWidth: 500),
|
||||
resizingMode: .aspectFit,
|
||||
failureView: {
|
||||
ImageView(viewModel.item.imageSource(.logo, maxWidth: 500, maxHeight: 150))
|
||||
.resizingMode(.aspectFit)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -50,9 +49,8 @@ extension MovieItemView {
|
|||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
)
|
||||
.frame(width: 500, height: 150)
|
||||
.padding(.top, 5)
|
||||
.frame(width: 500, height: 150)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
|
||||
PortraitPosterHStack(
|
||||
|
|
|
@ -66,19 +66,21 @@ extension ItemView {
|
|||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
ImageView(
|
||||
viewModel.item.imageSource(.logo, maxWidth: 500),
|
||||
resizingMode: .aspectFit,
|
||||
failureView: {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: 500, maxHeight: 200)
|
||||
ImageView(viewModel.item.imageSource(
|
||||
.logo,
|
||||
maxWidth: UIScreen.main.bounds.width * 0.4,
|
||||
maxHeight: 250
|
||||
))
|
||||
.resizingMode(.bottomLeft)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
Text(viewModel.item.overview ?? L10n.noOverviewAvailable)
|
||||
.font(.subheadline)
|
||||
|
|
|
@ -32,7 +32,8 @@ struct EpisodeCard: View {
|
|||
} label: {
|
||||
ImageView(
|
||||
episode.imageSource(.primary, maxWidth: 600)
|
||||
) {
|
||||
)
|
||||
.failure {
|
||||
InitialFailureView(episode.title.initials)
|
||||
}
|
||||
.frame(width: 550, height: 308)
|
||||
|
|
|
@ -41,10 +41,9 @@ extension SeriesItemView {
|
|||
.id("topContentDivider")
|
||||
|
||||
if showLogo {
|
||||
ImageView(
|
||||
viewModel.item.imageSource(.logo, maxWidth: 500),
|
||||
resizingMode: .aspectFit,
|
||||
failureView: {
|
||||
ImageView(viewModel.item.imageSource(.logo, maxWidth: 500, maxHeight: 150))
|
||||
.resizingMode(.aspectFit)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -52,9 +51,8 @@ extension SeriesItemView {
|
|||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
)
|
||||
.frame(width: 500, height: 150)
|
||||
.padding(.top, 5)
|
||||
.frame(width: 500, height: 150)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
|
||||
SeriesEpisodesView(viewModel: viewModel)
|
||||
|
|
|
@ -238,7 +238,6 @@
|
|||
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */; };
|
||||
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */; };
|
||||
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */; };
|
||||
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; };
|
||||
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; };
|
||||
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
|
||||
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
|
||||
|
@ -385,7 +384,6 @@
|
|||
E18E02212887492B0022598C /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
|
||||
E18E02222887492B0022598C /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
|
||||
E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
|
||||
E18E02242887492B0022598C /* BlurHashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */; };
|
||||
E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; };
|
||||
E18E023A288749540022598C /* UIScrollViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0239288749540022598C /* UIScrollViewExtensions.swift */; };
|
||||
E18E023C288749540022598C /* UIScrollViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0239288749540022598C /* UIScrollViewExtensions.swift */; };
|
||||
|
@ -738,7 +736,6 @@
|
|||
E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = "<group>"; };
|
||||
E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = "<group>"; };
|
||||
E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = "<group>"; };
|
||||
E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashView.swift; sourceTree = "<group>"; };
|
||||
E1047E2227E5880000CB0D4A /* InitialFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialFailureView.swift; sourceTree = "<group>"; };
|
||||
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
|
||||
E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesEpisodesView.swift; sourceTree = "<group>"; };
|
||||
|
@ -2003,7 +2000,6 @@
|
|||
E18E0200288749200022598C /* AppIcon.swift */,
|
||||
E18E0201288749200022598C /* AttributeFillView.swift */,
|
||||
E18E0202288749200022598C /* AttributeOutlineView.swift */,
|
||||
E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */,
|
||||
E18E0203288749200022598C /* BlurView.swift */,
|
||||
E18E01FF288749200022598C /* Divider.swift */,
|
||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||
|
@ -2410,7 +2406,6 @@
|
|||
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
||||
E18E02212887492B0022598C /* MultiSelectorView.swift in Sources */,
|
||||
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||
E18E02242887492B0022598C /* BlurHashView.swift in Sources */,
|
||||
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
||||
|
@ -2641,7 +2636,6 @@
|
|||
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */,
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
|
||||
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */,
|
||||
E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */,
|
||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */,
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/LePips/BlurHashKit",
|
||||
"state" : {
|
||||
"revision" : "ee9f34f4f8fc03f3d67622e2a6eeb65f5108f2a3",
|
||||
"version" : "1.0.0"
|
||||
"revision" : "3c23237f1f2b62741bce70bd2e4ef2aa7799ea85",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -38,14 +38,12 @@ struct PortraitPosterButton<Item: PortraitPoster>: View {
|
|||
selectedAction(item)
|
||||
} label: {
|
||||
VStack(alignment: horizontalAlignment) {
|
||||
ImageView(
|
||||
item.portraitPosterImageSource(maxWidth: maxWidth),
|
||||
failureView: {
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: maxWidth))
|
||||
.failure {
|
||||
InitialFailureView(item.title.initials)
|
||||
}
|
||||
)
|
||||
.portraitPoster(width: maxWidth)
|
||||
.accessibilityIgnoresInvertColors()
|
||||
.portraitPoster(width: maxWidth)
|
||||
.accessibilityIgnoresInvertColors()
|
||||
|
||||
if item.showTitle {
|
||||
Text(item.title)
|
||||
|
|
|
@ -116,18 +116,17 @@ extension ItemView.CinematicScrollView {
|
|||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
ImageView(
|
||||
viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width),
|
||||
resizingMode: .aspectFit
|
||||
) {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
ImageView(viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width))
|
||||
.resizingMode(.aspectFit)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
DotHStack {
|
||||
if let firstGenre = viewModel.item.genres?.first {
|
||||
|
|
|
@ -145,18 +145,17 @@ extension ItemView.CompactLogoScrollView {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
ImageView(
|
||||
viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width),
|
||||
resizingMode: .aspectFit
|
||||
) {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
ImageView(viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width, maxHeight: 100))
|
||||
.resizingMode(.aspectFit)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 100)
|
||||
|
||||
DotHStack {
|
||||
if let firstGenre = viewModel.item.genres?.first {
|
||||
|
|
|
@ -104,23 +104,33 @@ extension ItemView.iPadOSCinematicScrollView {
|
|||
var viewModel: ItemViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(
|
||||
viewModel.item.imageURL(.logo, maxWidth: 500),
|
||||
resizingMode: .aspectFit
|
||||
) {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(maxWidth: UIScreen.main.bounds.width * 0.4, maxHeight: 100)
|
||||
HStack(alignment: .bottom) {
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(viewModel.item.imageSource(
|
||||
.logo,
|
||||
maxWidth: UIScreen.main.bounds.width * 0.4,
|
||||
maxHeight: 150
|
||||
))
|
||||
.resizingMode(.bottomLeft)
|
||||
.failure {
|
||||
Text(viewModel.item.displayName)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
TruncatedTextView(text: viewModel.item.overview ?? L10n.noOverviewAvailable) {
|
||||
itemRouter.route(to: \.itemOverview, viewModel.item)
|
||||
}
|
||||
.lineLimit(3)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
|
||||
DotHStack {
|
||||
if let firstGenre = viewModel.item.genres?.first {
|
||||
|
@ -135,30 +145,22 @@ extension ItemView.iPadOSCinematicScrollView {
|
|||
Text(runtime)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color(UIColor.lightGray))
|
||||
|
||||
TruncatedTextView(text: viewModel.item.overview ?? L10n.noOverviewAvailable) {
|
||||
itemRouter.route(to: \.itemOverview, viewModel.item)
|
||||
}
|
||||
.lineLimit(3)
|
||||
.foregroundColor(.white)
|
||||
|
||||
ItemView.AttributesHStack(viewModel: viewModel)
|
||||
}
|
||||
.padding(.trailing, 200)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(height: 50)
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
}
|
||||
.frame(width: 250)
|
||||
}
|
||||
.padding(.trailing, 200)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(height: 50)
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
}
|
||||
.frame(width: 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,13 +31,13 @@ extension UserSignInView {
|
|||
}
|
||||
} label: {
|
||||
HStack {
|
||||
ImageView(publicUser.profileImageSource(maxWidth: 50, maxHeight: 50)) {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(Circle())
|
||||
ImageView(publicUser.profileImageSource(maxWidth: 50, maxHeight: 50))
|
||||
.failure {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(publicUser.name ?? "--")
|
||||
Spacer()
|
||||
|
|
Loading…
Reference in New Issue