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