[iOS & tvOS] Video Range Types (#1449)
* `VideoRangeType` "Extension" for now it's an enum until 10.10. Otherwise, done. * Limit lines to 1 and variable width as needed. * CodeFactor issue resolution * AttributeViewModifier -> AttributeBadge * change API * `WrappingHStack` --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
8f21860e5e
commit
890bf1fa31
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AttributeBadge<Content: View>: View {
|
||||
|
||||
@Environment(\.font)
|
||||
private var font
|
||||
|
||||
enum AttributeStyle {
|
||||
case fill
|
||||
case outline
|
||||
}
|
||||
|
||||
private let style: AttributeStyle
|
||||
private let content: () -> Content
|
||||
|
||||
private var usedFont: Font {
|
||||
font ?? .caption.weight(.semibold)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if style == .fill {
|
||||
content()
|
||||
.padding(.init(vertical: 1, horizontal: 4))
|
||||
.hidden()
|
||||
.background {
|
||||
Color(UIColor.lightGray)
|
||||
.cornerRadius(2)
|
||||
.inverseMask {
|
||||
content()
|
||||
.padding(.init(vertical: 1, horizontal: 4))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
.foregroundStyle(Color(UIColor.lightGray))
|
||||
.padding(.init(vertical: 1, horizontal: 4))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color(UIColor.lightGray), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.labelStyle(AttributeBadgeLabelStyle())
|
||||
.font(usedFont)
|
||||
}
|
||||
}
|
||||
|
||||
extension AttributeBadge where Content == Text {
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: @autoclosure @escaping () -> Text
|
||||
) {
|
||||
self.init(style: style) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: String
|
||||
) {
|
||||
self.init(style: style) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AttributeBadge where Content == Label<Text, Image> {
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: String,
|
||||
image: Image
|
||||
) {
|
||||
self.style = style
|
||||
self.content = {
|
||||
Label { Text(title) } icon: { image }
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: String,
|
||||
image: @escaping () -> Image
|
||||
) {
|
||||
self.style = style
|
||||
self.content = {
|
||||
Label { Text(title) } icon: { image() }
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: String,
|
||||
systemName: String
|
||||
) {
|
||||
self.style = style
|
||||
self.content = {
|
||||
Label { Text(title) } icon: { Image(systemName: systemName) }
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: Text,
|
||||
image: Image
|
||||
) {
|
||||
self.style = style
|
||||
self.content = {
|
||||
Label { title } icon: { image }
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: Text,
|
||||
image: @escaping () -> Image
|
||||
) {
|
||||
self.style = style
|
||||
self.content = {
|
||||
Label { title } icon: { image() }
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
style: AttributeStyle,
|
||||
title: Text,
|
||||
systemName: String
|
||||
) {
|
||||
self.style = style
|
||||
self.content = {
|
||||
Label { title } icon: { Image(systemName: systemName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AttributeBadgeLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(spacing: 2) {
|
||||
configuration.icon
|
||||
|
||||
configuration.title
|
||||
}
|
||||
}
|
||||
}
|
|
@ -265,6 +265,14 @@ extension [MediaStream] {
|
|||
contains { $0.isHDVideo }
|
||||
}
|
||||
|
||||
var hasHDRVideo: Bool {
|
||||
contains { VideoRangeType(from: $0.videoRangeType).isHDR }
|
||||
}
|
||||
|
||||
var hasDolbyVision: Bool {
|
||||
contains { VideoRangeType(from: $0.videoRangeType).isDolbyVision }
|
||||
}
|
||||
|
||||
var hasSubtitles: Bool {
|
||||
contains { $0.type == .subtitle }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// TODO: 10.10+ Replace with extension of https://github.com/jellyfin/jellyfin-sdk-swift/blob/main/Sources/Entities/VideoRangeType.swift
|
||||
enum VideoRangeType: String, Displayable {
|
||||
/// Unknown video range type.
|
||||
case unknown = "Unknown"
|
||||
/// SDR video range type (8bit).
|
||||
case sdr = "SDR"
|
||||
/// HDR10 video range type (10bit).
|
||||
case hdr10 = "HDR10"
|
||||
/// HLG video range type (10bit).
|
||||
case hlg = "HLG"
|
||||
/// Dolby Vision video range type (10bit encoded / 12bit remapped).
|
||||
case dovi = "DOVI"
|
||||
/// Dolby Vision with HDR10 video range fallback (10bit).
|
||||
case doviWithHDR10 = "DOVIWithHDR10"
|
||||
/// Dolby Vision with HLG video range fallback (10bit).
|
||||
case doviWithHLG = "DOVIWithHLG"
|
||||
/// Dolby Vision with SDR video range fallback (8bit / 10bit).
|
||||
case doviWithSDR = "DOVIWithSDR"
|
||||
/// HDR10+ video range type (10bit to 16bit).
|
||||
case hdr10Plus = "HDR10Plus"
|
||||
|
||||
/// Initializes from an optional string, defaulting to `.unknown` if nil or invalid.
|
||||
init(from rawValue: String?) {
|
||||
self = VideoRangeType(rawValue: rawValue ?? "") ?? .unknown
|
||||
}
|
||||
|
||||
/// Returns a human-readable display title for each video range type.
|
||||
/// Dolby Vision is a proper noun so it is not localized
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return L10n.unknown
|
||||
case .dovi:
|
||||
return "Dolby Vision"
|
||||
case .doviWithHDR10:
|
||||
return "Dolby Vision / HDR10"
|
||||
case .doviWithHLG:
|
||||
return "Dolby Vision / HLG"
|
||||
case .doviWithSDR:
|
||||
return "Dolby Vision / SDR"
|
||||
case .hdr10Plus:
|
||||
return "HDR10+"
|
||||
default:
|
||||
return self.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the video format is HDR (including Dolby Vision).
|
||||
var isHDR: Bool {
|
||||
switch self {
|
||||
case .unknown, .sdr:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the video format is Dolby Vision.
|
||||
var isDolbyVision: Bool {
|
||||
switch self {
|
||||
case .dovi, .doviWithHDR10, .doviWithHLG, .doviWithSDR:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +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) 2025 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: remove as a `ViewModifier` and instead a wrapper view
|
||||
|
||||
struct AttributeViewModifier: ViewModifier {
|
||||
|
||||
enum Style {
|
||||
case fill
|
||||
case outline
|
||||
}
|
||||
|
||||
let style: Style
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if style == .fill {
|
||||
content
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.hidden()
|
||||
.background {
|
||||
Color(UIColor.lightGray)
|
||||
.cornerRadius(2)
|
||||
.inverseMask {
|
||||
content
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(Color(UIColor.lightGray))
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color(UIColor.lightGray), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -266,10 +266,6 @@ extension View {
|
|||
}
|
||||
}
|
||||
|
||||
func asAttributeStyle(_ style: AttributeViewModifier.Style) -> some View {
|
||||
modifier(AttributeViewModifier(style: style))
|
||||
}
|
||||
|
||||
// TODO: rename `blurredFullScreenCover`
|
||||
func blurFullScreenCover(
|
||||
isPresented: Binding<Bool>,
|
||||
|
|
|
@ -17,12 +17,16 @@ extension ItemView {
|
|||
private var itemViewAttributes
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 25) {
|
||||
ForEach(itemViewAttributes, id: \.self) { attribute in
|
||||
getAttribute(attribute)
|
||||
if itemViewAttributes.isNotEmpty {
|
||||
HStack(spacing: 25) {
|
||||
ForEach(itemViewAttributes, id: \.self) { attribute in
|
||||
getAttribute(attribute)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
}
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(Color(UIColor.darkGray))
|
||||
}
|
||||
.foregroundStyle(Color(UIColor.darkGray))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -30,58 +34,52 @@ extension ItemView {
|
|||
switch attribute {
|
||||
case .ratingCritics:
|
||||
if let criticRating = viewModel.item.criticRating {
|
||||
HStack(spacing: 2) {
|
||||
Group {
|
||||
if criticRating >= 60 {
|
||||
Image(.tomatoFresh)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Image(.tomatoRotten)
|
||||
}
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(criticRating, specifier: "%.0f")")
|
||||
) {
|
||||
if criticRating >= 60 {
|
||||
Image(.tomatoFresh)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Image(.tomatoRotten)
|
||||
}
|
||||
.font(.caption2)
|
||||
|
||||
Text("\(criticRating, specifier: "%.0f")")
|
||||
}
|
||||
.asAttributeStyle(.outline)
|
||||
}
|
||||
case .ratingCommunity:
|
||||
if let communityRating = viewModel.item.communityRating {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption2)
|
||||
|
||||
Text("\(communityRating, specifier: "%.1f")")
|
||||
}
|
||||
.asAttributeStyle(.outline)
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(communityRating, specifier: "%.01f")"),
|
||||
systemName: "star.fill"
|
||||
)
|
||||
}
|
||||
case .ratingOfficial:
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
Text(officialRating)
|
||||
.asAttributeStyle(.outline)
|
||||
AttributeBadge(style: .outline, title: officialRating)
|
||||
}
|
||||
case .videoQuality:
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.hasHDVideo == true {
|
||||
Text("HD")
|
||||
.asAttributeStyle(.fill)
|
||||
}
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.has4KVideo == true {
|
||||
Text("4K")
|
||||
.asAttributeStyle(.fill)
|
||||
AttributeBadge(style: .fill, title: "4K")
|
||||
} else if viewModel.selectedMediaSource?.mediaStreams?.hasHDVideo == true {
|
||||
AttributeBadge(style: .fill, title: "HD")
|
||||
}
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.hasDolbyVision == true {
|
||||
AttributeBadge(style: .fill, title: "DV")
|
||||
}
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.hasHDRVideo == true {
|
||||
AttributeBadge(style: .fill, title: "HDR")
|
||||
}
|
||||
case .audioChannels:
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.has51AudioChannelLayout == true {
|
||||
Text("5.1")
|
||||
.asAttributeStyle(.fill)
|
||||
AttributeBadge(style: .fill, title: "5.1")
|
||||
}
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.has71AudioChannelLayout == true {
|
||||
Text("7.1")
|
||||
.asAttributeStyle(.fill)
|
||||
AttributeBadge(style: .fill, title: "7.1")
|
||||
}
|
||||
case .subtitles:
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.hasSubtitles == true {
|
||||
Text("CC")
|
||||
.asAttributeStyle(.outline)
|
||||
AttributeBadge(style: .outline, title: "CC")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
|
||||
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
|
||||
4E00DCB42D7F7D0900DC3CBB /* VideoRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E00DCB32D7F7D0300DC3CBB /* VideoRangeType.swift */; };
|
||||
4E00DCB52D7F7D0900DC3CBB /* VideoRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E00DCB32D7F7D0300DC3CBB /* VideoRangeType.swift */; };
|
||||
4E00DCB72D7F93F000DC3CBB /* AttributeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E00DCB62D7F93ED00DC3CBB /* AttributeBadge.swift */; };
|
||||
4E00DCB82D7F94CA00DC3CBB /* AttributeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E00DCB62D7F93ED00DC3CBB /* AttributeBadge.swift */; };
|
||||
4E01446C2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
|
||||
4E01446D2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
|
||||
4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0195E32CE04678007844F4 /* ItemSection.swift */; };
|
||||
|
@ -178,6 +182,7 @@
|
|||
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
|
||||
4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */; };
|
||||
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */; };
|
||||
4E9DDBEF2D81EB9E0001C562 /* WrappingHStack in Frameworks */ = {isa = PBXBuildFile; productRef = 4E9DDBEE2D81EB9E0001C562 /* WrappingHStack */; };
|
||||
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */; };
|
||||
4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */; };
|
||||
4EA397472CD31CC000904C25 /* AddServerUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */; };
|
||||
|
@ -950,10 +955,8 @@
|
|||
E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D9288747230022598C /* ActionButtonHStack.swift */; };
|
||||
E18E01FA288747580022598C /* AboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01F3288747580022598C /* AboutAppView.swift */; };
|
||||
E18E0204288749200022598C /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* RowDivider.swift */; };
|
||||
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0202288749200022598C /* AttributeStyleModifier.swift */; };
|
||||
E18E0208288749200022598C /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0203288749200022598C /* BlurView.swift */; };
|
||||
E18E021C2887492B0022598C /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0203288749200022598C /* BlurView.swift */; };
|
||||
E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0202288749200022598C /* AttributeStyleModifier.swift */; };
|
||||
E18E021E2887492B0022598C /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* RowDivider.swift */; };
|
||||
E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
||||
E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
|
||||
|
@ -1275,6 +1278,8 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
|
||||
4E00DCB32D7F7D0300DC3CBB /* VideoRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRangeType.swift; sourceTree = "<group>"; };
|
||||
4E00DCB62D7F93ED00DC3CBB /* AttributeBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeBadge.swift; sourceTree = "<group>"; };
|
||||
4E01446B2D0292E000193038 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
||||
4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; };
|
||||
4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1930,7 +1935,6 @@
|
|||
E18E01D9288747230022598C /* ActionButtonHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButtonHStack.swift; sourceTree = "<group>"; };
|
||||
E18E01F3288747580022598C /* AboutAppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = "<group>"; };
|
||||
E18E01FF288749200022598C /* RowDivider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowDivider.swift; sourceTree = "<group>"; };
|
||||
E18E0202288749200022598C /* AttributeStyleModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeStyleModifier.swift; sourceTree = "<group>"; };
|
||||
E18E0203288749200022598C /* BlurView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = "<group>"; };
|
||||
E19070482C84F2BB0004600E /* ButtonStyle-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ButtonStyle-iOS.swift"; sourceTree = "<group>"; };
|
||||
E190704B2C858CEB0004600E /* VideoPlayerType+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoPlayerType+Shared.swift"; sourceTree = "<group>"; };
|
||||
|
@ -2194,6 +2198,7 @@
|
|||
E15210582946DF1B00375CC2 /* PulseUI in Frameworks */,
|
||||
E1153DA42BBA614F00424D36 /* CollectionVGrid in Frameworks */,
|
||||
62666DF727E5012C00EC0ECD /* MobileVLCKit.xcframework in Frameworks */,
|
||||
4E9DDBEF2D81EB9E0001C562 /* WrappingHStack in Frameworks */,
|
||||
62666E0327E5017100EC0ECD /* CoreMedia.framework in Frameworks */,
|
||||
E10706122942F57D00646DAF /* PulseLogHandler in Frameworks */,
|
||||
62666E0627E5017A00EC0ECD /* CoreVideo.framework in Frameworks */,
|
||||
|
@ -3926,6 +3931,7 @@
|
|||
E17AC9692954D00E003D2BC2 /* URLResponse.swift */,
|
||||
E19D41AF2BF2B7540082B8B2 /* URLSessionConfiguration.swift */,
|
||||
E1A1528128FD126C00600579 /* VerticalAlignment.swift */,
|
||||
4E00DCB32D7F7D0300DC3CBB /* VideoRangeType.swift */,
|
||||
E11895A22893409D0042947B /* ViewExtensions */,
|
||||
);
|
||||
path = Extensions;
|
||||
|
@ -4654,7 +4660,6 @@
|
|||
E170D101294CE4C10017224C /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E18E0202288749200022598C /* AttributeStyleModifier.swift */,
|
||||
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */,
|
||||
E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */,
|
||||
4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */,
|
||||
|
@ -5057,6 +5062,7 @@
|
|||
children = (
|
||||
E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */,
|
||||
E104DC952B9E7E29008F506D /* AssertionFailureView.swift */,
|
||||
4E00DCB62D7F93ED00DC3CBB /* AttributeBadge.swift */,
|
||||
E18E0203288749200022598C /* BlurView.swift */,
|
||||
E145EB212BDCCA43003BF6F3 /* BulletedList.swift */,
|
||||
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */,
|
||||
|
@ -5533,6 +5539,7 @@
|
|||
E176EBE82D050925009F4CF1 /* CollectionVGrid */,
|
||||
E1A09F712D05933D00835265 /* CollectionVGrid */,
|
||||
E1A09F742D05935100835265 /* CollectionHStack */,
|
||||
4E9DDBEE2D81EB9E0001C562 /* WrappingHStack */,
|
||||
);
|
||||
productName = JellyfinPlayer;
|
||||
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
|
||||
|
@ -5636,6 +5643,7 @@
|
|||
E176EBDC2D050067009F4CF1 /* XCRemoteSwiftPackageReference "swift-identified-collections" */,
|
||||
E1A09F702D05933D00835265 /* XCRemoteSwiftPackageReference "CollectionVGrid" */,
|
||||
E1A09F732D05935100835265 /* XCRemoteSwiftPackageReference "CollectionHStack" */,
|
||||
4E9DDBED2D81E9810001C562 /* XCRemoteSwiftPackageReference "WrappingHStack" */,
|
||||
);
|
||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -6065,6 +6073,7 @@
|
|||
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */,
|
||||
E1575E9C293E7B1E001665B1 /* Collection.swift in Sources */,
|
||||
E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */,
|
||||
4E00DCB72D7F93F000DC3CBB /* AttributeBadge.swift in Sources */,
|
||||
E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */,
|
||||
E1CAF65E2BA345830087D991 /* MediaType.swift in Sources */,
|
||||
E1F5CF062CB09EA000607465 /* CurrentDate.swift in Sources */,
|
||||
|
@ -6098,6 +6107,7 @@
|
|||
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
|
||||
E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||
4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */,
|
||||
4E00DCB52D7F7D0900DC3CBB /* VideoRangeType.swift in Sources */,
|
||||
E1DC9845296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
||||
E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */,
|
||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
|
@ -6286,7 +6296,6 @@
|
|||
E1E6C44929AECEE70064123F /* AutoPlayActionButton.swift in Sources */,
|
||||
E1C926152887565C002A7A66 /* EpisodeCard.swift in Sources */,
|
||||
E1A1528E28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */,
|
||||
E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */,
|
||||
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */,
|
||||
4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */,
|
||||
E10231492BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||
|
@ -6397,6 +6406,7 @@
|
|||
E1E1644128BB301900323B0A /* Array.swift in Sources */,
|
||||
E18CE0AF28A222240092E7F1 /* PublicUserRow.swift in Sources */,
|
||||
E129429828F4785200796AC6 /* CaseIterablePicker.swift in Sources */,
|
||||
4E00DCB82D7F94CA00DC3CBB /* AttributeBadge.swift in Sources */,
|
||||
E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */,
|
||||
E154965E296CA2EF00C4EF88 /* DownloadTask.swift in Sources */,
|
||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||
|
@ -6606,7 +6616,6 @@
|
|||
E19D41A72BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
|
||||
E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */,
|
||||
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */,
|
||||
E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */,
|
||||
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
|
||||
4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */,
|
||||
4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */,
|
||||
|
@ -6835,6 +6844,7 @@
|
|||
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
|
||||
4E2AC4C22C6C491200DD600D /* AudoCodec.swift in Sources */,
|
||||
4E00DCB42D7F7D0900DC3CBB /* VideoRangeType.swift in Sources */,
|
||||
4E49DED52CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */,
|
||||
E10B1EC72BD9AF6100A92EAF /* V2ServerModel.swift in Sources */,
|
||||
E13DD3C827164B1E009D4DAF /* UIDevice.swift in Sources */,
|
||||
|
@ -7606,6 +7616,14 @@
|
|||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
4E9DDBED2D81E9810001C562 /* XCRemoteSwiftPackageReference "WrappingHStack" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/dkk/WrappingHStack";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.2.11;
|
||||
};
|
||||
};
|
||||
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect";
|
||||
|
@ -7793,6 +7811,11 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
4E9DDBEE2D81EB9E0001C562 /* WrappingHStack */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4E9DDBED2D81E9810001C562 /* XCRemoteSwiftPackageReference "WrappingHStack" */;
|
||||
productName = WrappingHStack;
|
||||
};
|
||||
6220D0C826D63F3700B8E046 /* Stinsen */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "b7189175c8066640649da818750e83deee8ef2f766db25c34025f23d451b301d",
|
||||
"originHash" : "06a5dcccf9b916d25a75602bcb345218714cc642e54ef793a56ac6adc5439df8",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "blurhashkit",
|
||||
|
@ -252,6 +252,15 @@
|
|||
"branch" : "main",
|
||||
"revision" : "50d4f6ec05a2d8333952def0d8e45019a4207132"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "wrappinghstack",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/dkk/WrappingHStack",
|
||||
"state" : {
|
||||
"revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b",
|
||||
"version" : "2.2.11"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import WrappingHStack
|
||||
|
||||
extension ItemView {
|
||||
struct AttributesHStack: View {
|
||||
|
@ -16,74 +17,146 @@ extension ItemView {
|
|||
@StoredValue(.User.itemViewAttributes)
|
||||
private var itemViewAttributes
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ForEach(itemViewAttributes, id: \.self) { attribute in
|
||||
getAttribute(attribute)
|
||||
let badges = computeBadges()
|
||||
if !badges.isEmpty {
|
||||
WrappingHStack(badges, id: \.self, alignment: .center, spacing: .constant(8), lineSpacing: 8) { badgeItem in
|
||||
badgeItem
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
.foregroundStyle(Color(UIColor.darkGray))
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
.foregroundStyle(Color(UIColor.darkGray))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func getAttribute(_ attribute: ItemViewAttribute) -> some View {
|
||||
switch attribute {
|
||||
case .ratingCritics:
|
||||
if let criticRating = viewModel.item.criticRating {
|
||||
HStack(spacing: 2) {
|
||||
Group {
|
||||
if criticRating >= 60 {
|
||||
Image(.tomatoFresh)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Image(.tomatoRotten)
|
||||
// MARK: - Compute Badges
|
||||
|
||||
private func computeBadges() -> [AnyView] {
|
||||
var badges: [AnyView] = []
|
||||
var processedGroups = Set<ItemViewAttribute>()
|
||||
|
||||
for attribute in itemViewAttributes {
|
||||
|
||||
if processedGroups.contains(attribute) { continue }
|
||||
processedGroups.insert(attribute)
|
||||
|
||||
switch attribute {
|
||||
case .ratingCritics:
|
||||
if let criticRating = viewModel.item.criticRating {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(criticRating, specifier: "%.0f")")
|
||||
) {
|
||||
if criticRating >= 60 {
|
||||
Image(.tomatoFresh)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Image(.tomatoRotten)
|
||||
}
|
||||
}
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
case .ratingCommunity:
|
||||
if let communityRating = viewModel.item.communityRating {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: Text("\(communityRating, specifier: "%.01f")"),
|
||||
systemName: "star.fill"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
case .ratingOfficial:
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: officialRating
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
case .videoQuality:
|
||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
||||
// Resolution badge (if available). Only one of 4K or HD is shown.
|
||||
if mediaStreams.has4KVideo {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "4K"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
} else if mediaStreams.hasHDVideo {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "HD"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
if mediaStreams.hasDolbyVision {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "DV"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
if mediaStreams.hasHDRVideo {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "HDR"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
.font(.caption2)
|
||||
|
||||
Text("\(criticRating, specifier: "%.0f")")
|
||||
}
|
||||
.asAttributeStyle(.outline)
|
||||
}
|
||||
case .ratingCommunity:
|
||||
if let communityRating = viewModel.item.communityRating {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption2)
|
||||
|
||||
Text("\(communityRating, specifier: "%.1f")")
|
||||
case .audioChannels:
|
||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams {
|
||||
if mediaStreams.has51AudioChannelLayout {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "5.1"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
if mediaStreams.has71AudioChannelLayout {
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .fill,
|
||||
title: "7.1"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
}
|
||||
case .subtitles:
|
||||
if let mediaStreams = viewModel.selectedMediaSource?.mediaStreams,
|
||||
mediaStreams.hasSubtitles
|
||||
{
|
||||
let badge = AnyView(
|
||||
AttributeBadge(
|
||||
style: .outline,
|
||||
title: "CC"
|
||||
)
|
||||
)
|
||||
badges.append(badge)
|
||||
}
|
||||
.asAttributeStyle(.outline)
|
||||
}
|
||||
case .ratingOfficial:
|
||||
if let officialRating = viewModel.item.officialRating {
|
||||
Text(officialRating)
|
||||
.asAttributeStyle(.outline)
|
||||
}
|
||||
case .videoQuality:
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.hasHDVideo == true {
|
||||
Text("HD")
|
||||
.asAttributeStyle(.fill)
|
||||
}
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.has4KVideo == true {
|
||||
Text("4K")
|
||||
.asAttributeStyle(.fill)
|
||||
}
|
||||
case .audioChannels:
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.has51AudioChannelLayout == true {
|
||||
Text("5.1")
|
||||
.asAttributeStyle(.fill)
|
||||
}
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.has71AudioChannelLayout == true {
|
||||
Text("7.1")
|
||||
.asAttributeStyle(.fill)
|
||||
}
|
||||
case .subtitles:
|
||||
if viewModel.selectedMediaSource?.mediaStreams?.hasSubtitles == true {
|
||||
Text("CC")
|
||||
.asAttributeStyle(.outline)
|
||||
}
|
||||
}
|
||||
return badges
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue