import SwiftUI struct RemoteImage: View { let url: String? var contentMode: ContentMode = .fill /// Zet uit voor transparante logo's (geen donker vlak). var opaqueBackground: Bool = true var body: some View { Group { if let url, let u = URL(string: url) { AsyncImage(url: u) { phase in switch phase { case .success(let image): image.resizable().aspectRatio(contentMode: contentMode) case .failure: if opaqueBackground { placeholder } else { Color.clear } case .empty: if opaqueBackground { placeholder.overlay { ProgressView().tint(.white) } } else { Color.clear } @unknown default: if opaqueBackground { placeholder } else { Color.clear } } } } else if opaqueBackground { placeholder } else { Color.clear } } .clipped() .background(opaqueBackground ? McColors.surface : Color.clear) } private var placeholder: some View { Rectangle().fill(McColors.surface) } } /// Title-card / logo zonder achtergrondvlak. struct TitleLogo: View { let url: String? var maxWidth: CGFloat = 220 var maxHeight: CGFloat = 56 var body: some View { RemoteImage(url: url, contentMode: .fit, opaqueBackground: false) .frame(maxWidth: maxWidth, maxHeight: maxHeight, alignment: .leading) .shadow(color: .black.opacity(0.55), radius: 10, y: 4) } } struct PosterCard: View { let item: MediaItem var width: CGFloat = 112 /// Grid-bibliotheek: geen titel onder poster (zoals Android Browse). var showTitle: Bool = true var body: some View { VStack(alignment: .leading, spacing: showTitle ? 6 : 0) { RemoteImage(url: item.posterUrl) .frame(width: width, height: width * 1.5) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .overlay(alignment: .topTrailing) { if let badge = item.badge, !badge.isEmpty { Text(badge) .font(.system(size: 10, weight: .bold)) .padding(.horizontal, 6) .padding(.vertical, 3) .background(McColors.accent, in: Capsule()) .foregroundStyle(.white) .padding(6) } } .overlay(alignment: .bottom) { if let pos = item.positionMs, let dur = item.durationMs, dur > 0, pos > 0 { GeometryReader { geo in Capsule() .fill(McColors.accent) .frame(width: geo.size.width * min(CGFloat(pos) / CGFloat(dur), 1), height: 3) .frame(maxHeight: .infinity, alignment: .bottom) } .frame(height: 3) .padding(4) } } if showTitle { Text(item.title) .font(McFonts.caption) .foregroundStyle(McColors.text) .lineLimit(2) .multilineTextAlignment(.leading) .frame(width: width, height: 36, alignment: .topLeading) } } .frame(width: width, alignment: .leading) .accessibilityLabel(item.title) } } struct MetaChip: View { let text: String var body: some View { Text(text) .font(McFonts.caption) .foregroundStyle(McColors.textDim) .padding(.horizontal, 10) .padding(.vertical, 5) .background(McColors.surfaceSoft, in: Capsule()) } } struct BrandHeader: View { var subtitle: String? = nil var body: some View { HStack(spacing: 8) { ZStack { RoundedRectangle(cornerRadius: 7, style: .continuous) .fill( LinearGradient( colors: [McColors.accent, McColors.accent.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 26, height: 26) .shadow(color: McColors.accent.opacity(0.45), radius: 8, y: 2) Text("V") .font(.system(size: 14, weight: .black, design: .rounded)) .foregroundStyle(.white) } VStack(alignment: .leading, spacing: 0) { Text("VMC") .font(.system(size: 18, weight: .black, design: .rounded)) .foregroundStyle(McColors.text) if let subtitle { Text(subtitle) .font(.system(size: 10, weight: .semibold, design: .rounded)) .foregroundStyle(McColors.textDim) } } } } } struct MediaRouter: View { let item: MediaItem var body: some View { switch item.type.lowercased() { case "movie": MovieDetailScreen(movieId: item.id) case "series", "show": SeriesDetailScreen(seriesId: item.id) case "episode": if let mediaFileId = item.mediaFileId { PlayLauncher( mediaFileId: mediaFileId, title: item.title, mediaType: "episode", movieId: nil, episodeId: item.id, seriesId: item.seriesId, startMs: item.positionMs ?? 0 ) } else { ContentUnavailableView("Niet beschikbaar", systemImage: "play.slash") } default: MovieDetailScreen(movieId: item.id) } } }