`ImageView` Refactor (#517)

This commit is contained in:
Ethan Pippin 2022-08-12 12:05:24 -06:00 committed by GitHub
parent 70b75df110
commit 6f937ceddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 256 additions and 267 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,8 @@ struct EpisodeCard: View {
} label: {
ImageView(
episode.imageSource(.primary, maxWidth: 600)
) {
)
.failure {
InitialFailureView(episode.title.initials)
}
.frame(width: 550, height: 308)

View File

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

View File

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

View File

@ -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"
}
},
{

View File

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

View File

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

View File

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

View File

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

View File

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