import SwiftUI import FlixyDesign import FlixyPlayback import MediaKit /// Die aufklappbaren technischen Angaben zu einer Datei. /// /// Zeigt nicht nur, was in der Datei steckt, sondern auch, wie Flixy sie /// abspielen wird und warum. Bei der Frage „warum ruckelt das" ist das die /// erste Stelle zum Nachsehen — und in Phase 1, wo es noch kein Transcoding /// gibt, erklärt es, warum bei manchen Dateien kein Bild-in-Bild angeboten wird. struct TechnicalDetails: View { let media: MediaVersion let part: MediaFile @Binding var isExpanded: Bool private var decision: PlaybackDecision { PlaybackDecider().decide(profile: media.profile(file: part), policy: .directPlayOnly) } var body: some View { DisclosureGroup(isExpanded: $isExpanded) { VStack(alignment: .leading, spacing: FlixySpacing.snug) { row("technical.playbackMethod", playbackMethodLabel) if let reason = rationaleLabel { row("technical.reason", reason) } Divider().overlay(Color.flixySeparator).padding(.vertical, FlixySpacing.tight) if let container = part.container { row("technical.container", container.uppercased()) } if let codec = media.videoCodec { row("technical.videoCodec", codec.uppercased()) } if let width = media.width, let height = media.height { row("technical.resolution", "\(width) × \(height)") } if let bitrate = media.bitrate { row("technical.bitrate", "\(bitrate / 1000) Mbit/s") } if let size = part.size { row("technical.size", size.formatted(.byteCount(style: .file))) } if !part.audioStreams.isEmpty { Divider().overlay(Color.flixySeparator).padding(.vertical, FlixySpacing.tight) ForEach(part.audioStreams) { stream in row("technical.audio", trackLabel(stream)) } } if !part.subtitleStreams.isEmpty { ForEach(part.subtitleStreams) { stream in row("technical.subtitle", trackLabel(stream)) } } if let file = part.file { Divider().overlay(Color.flixySeparator).padding(.vertical, FlixySpacing.tight) VStack(alignment: .leading, spacing: 2) { Text("technical.file") .font(.flixyCaption) .foregroundStyle(Color.flixyTextMuted) Text(file) .font(.system(.caption, design: .monospaced)) .foregroundStyle(Color.flixyTextSecondary) .textSelection(.enabled) } } } .padding(.top, FlixySpacing.snug) } label: { Text("technical.title") .font(.flixyRowHeader) .foregroundStyle(Color.flixyTextPrimary) } .tint(Color.flixyTextSecondary) } private func row(_ label: LocalizedStringKey, _ value: String) -> some View { HStack(alignment: .top) { Text(label) .font(.flixyCaption) .foregroundStyle(Color.flixyTextMuted) Spacer() Text(value) .font(.flixyCaption) .foregroundStyle(Color.flixyTextSecondary) .multilineTextAlignment(.trailing) } } private func trackLabel(_ stream: MediaStream) -> String { stream.displayTitle ?? [stream.language, stream.codec?.uppercased()] .compactMap(\.self) .joined(separator: " · ") } private var playbackMethodLabel: String { switch decision.method { case .directPlay(let engine): switch engine { case .avPlayer: String(localized: "technical.directPlayNative") case .vlc: String(localized: "technical.directPlayVLC") } case .transcode: String(localized: "technical.transcode") } } /// Der erste Grund, der gegen den Systemplayer sprach — mehrere /// aufzuzählen hilft niemandem, es reicht einer. private var rationaleLabel: String? { for reason in decision.rationale { switch reason { case .containerUnsupported(let container): return String(localized: "technical.reason.container \(container.uppercased())") case .videoCodecUnsupported(let codec): return String(localized: "technical.reason.videoCodec \(codec.uppercased())") case .audioCodecUnsupported(let codec): return String(localized: "technical.reason.audioCodec \(codec.uppercased())") case .streamsUnknown: return String(localized: "technical.reason.unanalyzed") case .qualityChosen: return String(localized: "technical.reason.chosen") case .nativelySupported, .bitrateExceedsLimit, .transcodingUnavailable: continue } } return nil } }