Files
Flaro/Flixy/Views/Detail/TechnicalDetails.swift
T
GuidoandClaude Opus 5 9a69df4635 Phase 1: Anmeldung, Bibliothek, Detailseiten, Hybrid-Player
Die Datenschicht:
- plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund
- Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor
  Vermittlung), mit Abgleich der Server-Kennung gegen veraltete
  plex.direct-Adressen
- PlexServer als einzige Stelle, an der die App Endpunkte kennt:
  Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten,
  Staffeln/Folgen, Suche

Die Oberfläche:
- Startseite mit Aufmacher und den Reihen vom Server
- Mediathek als Raster oder Liste, mit Filter- und Sortierblatt
- Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung,
  Fassungsauswahl und aufklappbaren technischen Angaben
- Suche mit gemerkten Anfragen
- Vollbildplayer mit eigenem Chrome

Der Player:
- AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll
- VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test`
  in FlixyKit nicht das 920-MB-Binärpaket mitzieht

Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf,
dass `strong=true` einen dreißigstelligen Code liefert, der sich bei
plex.tv/link nicht eintippen lässt — jetzt der kurze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:46:28 +02:00

129 lines
5.1 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 PlexKit
/// 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: PlexMedia
let part: PlexPart
@Binding var isExpanded: Bool
private var decision: PlaybackDecision {
PlaybackDecider().decide(media: media, part: 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: PlexStream) -> 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 .nativelySupported, .bitrateExceedsLimit, .transcodingUnavailable:
continue
}
}
return nil
}
}