diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift index da8c7bd7..0c75677b 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift @@ -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 + } + } +} diff --git a/Shared/Views/BlurHashView.swift b/Shared/Views/BlurHashView.swift deleted file mode 100644 index d013a1b7..00000000 --- a/Shared/Views/BlurHashView.swift +++ /dev/null @@ -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) - } - } -} diff --git a/Shared/Views/ImageView.swift b/Shared/Views/ImageView.swift index 7e45c574..013cacf0 100644 --- a/Shared/Views/ImageView.swift +++ b/Shared/Views/ImageView.swift @@ -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: View { +struct ImageView: 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(@ViewBuilder _ content: @escaping (NukeUI.Image) -> I) -> ImageView { + ImageView( + sources, + resizingMode: resizingMode, + image: content, + placeHolder: placeholder, + failureView: failure + ) + } + + @ViewBuilder + func placeholder(@ViewBuilder _ content: @escaping () -> P) -> ImageView { + ImageView( + sources, + resizingMode: resizingMode, + image: image, + placeHolder: content, + failureView: failure + ) + } + + @ViewBuilder + func failure(@ViewBuilder _ content: @escaping () -> F) -> ImageView { + ImageView( + sources, + resizingMode: resizingMode, + image: image, + placeHolder: placeholder, + failureView: content + ) + } + + @ViewBuilder + func resizingMode(_ resizingMode: ImageResizingMode) -> ImageView { + ImageView( + sources, + resizingMode: resizingMode, + image: image, + placeHolder: placeholder, + failureView: failure + ) } } diff --git a/Swiftfin tvOS/Components/PortraitButton.swift b/Swiftfin tvOS/Components/PortraitButton.swift index 8f001216..9be925f2 100644 --- a/Swiftfin tvOS/Components/PortraitButton.swift +++ b/Swiftfin tvOS/Components/PortraitButton.swift @@ -19,13 +19,11 @@ struct PortraitButton: 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()) diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift index 01d6debe..d44a4382 100644 --- a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift @@ -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( diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift index d527f26c..5634eeaa 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift @@ -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( diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift index c9bd0d60..e33b8135 100644 --- a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift @@ -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( diff --git a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift index 5d5473dc..c64ca03e 100644 --- a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift @@ -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) diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift index 64c1b7ac..cb5e7d3a 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/EpisodeCard.swift @@ -32,7 +32,8 @@ struct EpisodeCard: View { } label: { ImageView( episode.imageSource(.primary, maxWidth: 600) - ) { + ) + .failure { InitialFailureView(episode.title.initials) } .frame(width: 550, height: 308) diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift index a49c1ad6..99d2f854 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift @@ -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) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index c41e2f70..d3bff267 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -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 = ""; }; E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = ""; }; E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = ""; }; - E1047E1F27E584AF00CB0D4A /* BlurHashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashView.swift; sourceTree = ""; }; E1047E2227E5880000CB0D4A /* InitialFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialFailureView.swift; sourceTree = ""; }; E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = ""; }; E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesEpisodesView.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f928dca4..3596aba6 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" } }, { diff --git a/Swiftfin/Components/PortraitPosterButton.swift b/Swiftfin/Components/PortraitPosterButton.swift index e56215d2..85b422e6 100644 --- a/Swiftfin/Components/PortraitPosterButton.swift +++ b/Swiftfin/Components/PortraitPosterButton.swift @@ -38,14 +38,12 @@ struct PortraitPosterButton: 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) diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift index 7298b88f..88c8050c 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift @@ -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 { diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift index a6c60f48..ac3600a9 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift @@ -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 { diff --git a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift index 56411729..7e536b2c 100644 --- a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift @@ -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) } } } diff --git a/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift b/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift index 20fe5681..9014bfab 100644 --- a/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift @@ -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()