Files
Flaro/Flixy/Views/Detail/TechnicalDetails.swift
T
ScarriffleandClaude Opus 5 fe2d730c00 Die Ansichten sprechen nicht mehr mit Plex
Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit
`MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für
einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre.

Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle`
wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und
`TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um
davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten.

`Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung
gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die
konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein
zweites Mal falsch — zu schreiben.

Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie
waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
Bildschirm eine andere Erklärung.

Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die
fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist
aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört
nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das
jetzt ausdrücklich, damit es niemand später „aufräumt".

Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit
Plex. Sie folgen als Nächstes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:46:52 +02:00

131 lines
5.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}