SwiftUI app against master API: link, profiles, browse, live TV, favorites sync, and TestFlight-ready assets. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
5.6 KiB
Swift
158 lines
5.6 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|