import SwiftUI enum ResolutionLogo: String { case uhd4k = "4K" case fhd1080 = "1080p" case hd720 = "720p" case sd = "SD" } enum FeatureLogo: String { case dolbyVision = "Dolby Vision" case hdr = "HDR" case atmos = "Atmos" case dtsX = "DTS:X" } struct MediaVersionBadges { let resolution: ResolutionLogo? let features: [FeatureLogo] static func parse(from file: MediaFileInfo) -> MediaVersionBadges { let hay = [file.resolution, file.hdrType, file.audioCodec, file.videoCodec, file.label] .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } .joined(separator: " ") return MediaVersionBadges( resolution: parseResolution(hay), features: parseFeatures(hay) ) } private static func parseResolution(_ hay: String) -> ResolutionLogo? { guard !hay.isEmpty else { return nil } if hay.range(of: #"(?:^|[^\d])(2160\s*p?|3840\s*[x×]\s*2160|4\s*k|uhd)(?:[^\d]|$)"#, options: [.regularExpression, .caseInsensitive]) != nil { return .uhd4k } if hay.range(of: #"(?:^|[^\d])(1080\s*p?|1920\s*[x×]\s*1080|fhd)(?:[^\d]|$)"#, options: [.regularExpression, .caseInsensitive]) != nil { return .fhd1080 } if hay.range(of: #"(?:^|[^\d])(720\s*p?|1280\s*[x×]\s*720)(?:[^\d]|$)"#, options: [.regularExpression, .caseInsensitive]) != nil { return .hd720 } if hay.range(of: #"(?:^|[^\d])(480\s*p?|576\s*p?|sd)(?:[^\d]|$)"#, options: [.regularExpression, .caseInsensitive]) != nil { return .sd } return nil } private static func parseFeatures(_ hay: String) -> [FeatureLogo] { guard !hay.isEmpty else { return [] } var out: [FeatureLogo] = [] if hay.range(of: #"dolby\s*vision|\bdovi\b|\bdv\b"#, options: [.regularExpression, .caseInsensitive]) != nil { out.append(.dolbyVision) } else if hay.range(of: #"hdr10\+?|\bhdr\b"#, options: [.regularExpression, .caseInsensitive]) != nil { out.append(.hdr) } if hay.range(of: #"atmos"#, options: [.regularExpression, .caseInsensitive]) != nil { out.append(.atmos) } else if hay.range(of: #"dts[\s.:_-]?x\b"#, options: [.regularExpression, .caseInsensitive]) != nil { out.append(.dtsX) } return out } } struct QualityBadgeRow: View { let file: MediaFileInfo var body: some View { let badges = MediaVersionBadges.parse(from: file) HStack(spacing: 8) { if let res = badges.resolution { ResolutionChip(logo: res) } else { Text("Versie") .font(.system(size: 13, weight: .semibold, design: .rounded)) .foregroundStyle(McColors.textDim) } ForEach(badges.features, id: \.rawValue) { feature in FeatureChip(logo: feature) } if file.nodeOnline == false { Text("offline") .font(.system(size: 11, weight: .bold, design: .rounded)) .foregroundStyle(McColors.danger) .padding(.horizontal, 8) .padding(.vertical, 4) .background(McColors.danger.opacity(0.15), in: RoundedRectangle(cornerRadius: 4, style: .continuous)) } Spacer(minLength: 0) } } } private struct ResolutionChip: View { let logo: ResolutionLogo var body: some View { switch logo { case .uhd4k: plate("4K", fg: .black, bg: Color(white: 0.95), weight: .black, tracking: 0.8) case .fhd1080: plate("1080p", fg: .white, bg: Color(white: 0.1), weight: .bold, border: Color.white.opacity(0.65)) case .hd720: plate("720p", fg: Color(white: 0.88), bg: .clear, weight: .semibold, border: Color.white.opacity(0.45)) case .sd: plate("SD", fg: Color(white: 0.74), bg: .clear, weight: .semibold, border: Color.white.opacity(0.35)) } } private func plate( _ text: String, fg: Color, bg: Color, weight: Font.Weight, tracking: CGFloat = 0.2, border: Color? = nil ) -> some View { Text(text) .font(.system(size: 12, weight: weight, design: .rounded)) .tracking(tracking) .foregroundStyle(fg) .padding(.horizontal, 9) .frame(minWidth: 36, minHeight: 26) .background(bg, in: RoundedRectangle(cornerRadius: 4, style: .continuous)) .overlay { if let border { RoundedRectangle(cornerRadius: 4, style: .continuous) .strokeBorder(border, lineWidth: 1) } } } } private struct FeatureChip: View { let logo: FeatureLogo var body: some View { let text: String = { switch logo { case .dolbyVision: return "VISION" case .hdr: return "HDR" case .atmos: return "ATMOS" case .dtsX: return "DTS:X" } }() Text(text) .font(.system(size: 10, weight: .bold, design: .rounded)) .tracking(0.6) .foregroundStyle(McColors.accent) .padding(.horizontal, 8) .frame(minHeight: 26) .overlay { RoundedRectangle(cornerRadius: 4, style: .continuous) .strokeBorder(McColors.accent.opacity(0.55), lineWidth: 1) } } }