Files
Flaro/Packages/FlixyKit/Sources/FlixyDesign/Components/HeroBanner.swift
T
GuidoandClaude Opus 5 ed84b5bf22 Live-TV-Sender gefunden, Film-Schriftzüge ergänzt
Senderliste
  Plex liefert die Sender verschachtelt in der Antwort auf /livetv/dvrs
  mit — jeder Empfänger bringt seine eigenen unter `Device` → `Channel`.
  Mein getrennter Endpunkt /livetv/dvrs/{id}/channels war eine Vermutung
  und lieferte nichts. Bei einem Server mit über dreißig eingerichteten
  Sendern stand deshalb „Keine Sender".

  Die verschachtelte Liste hat jetzt Vorrang, der Endpunkt bleibt als
  Rückfalllinie. `ChannelMapping` wird als ältere Schreibweise ebenfalls
  gelesen, und der Empfänger-Schlüssel kommt je nach Serverfassung als
  Zahl oder als Zeichenkette.

  Bleibt die Liste leer, sagt die Ansicht jetzt welcher Weg was ergeben
  hat. In einem Bereich ohne offizielle Beschreibung ist das die Angabe,
  mit der sich der nächste Fehler finden lässt.

Schriftzüge
  Plex liefert bei vielen Filmen ein `clearLogo` — den Schriftzug in
  seiner eigenen Type, freigestellt. Der tritt jetzt im Aufmacher an die
  Stelle des gesetzten Titels.

  Dabei ein Fallstrick: /photo/:/transcode liefert JPEG, und JPEG kennt
  keine Transparenz. Über den Bild-Transcoder geladen wäre aus dem
  freigestellten Logo ein schwarzer Kasten geworden. Es wird deshalb
  unverändert geladen. Fehlt der Schriftzug oder scheitert das Laden,
  bleibt es beim Titel.

161 Tests.

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

125 lines
4.7 KiB
Swift

import SwiftUI
/// Das Aufmacher-Bild oben auf der Startseite.
///
/// Die Aktionsknöpfe kommen von außen, weil ein Film „Abspielen" und
/// „Herunterladen" braucht, eine angefangene Folge dagegen „Weiterschauen".
/// Der Baustein legt nur Bild, Verlauf und Textsatz fest.
public struct HeroBanner<Actions: View>: View {
private let title: String
private let metadata: String?
private let summary: String?
private let artworkURL: URL?
/// Der freigestellte Schriftzug des Films. Steht er zur Verfügung, tritt
/// er an die Stelle des gesetzten Titels — „Harry Potter" in seiner
/// eigenen Schrift ist wiedererkennbar, eine Systemschrift nicht.
private let logoURL: URL?
private let aspectRatio: CGFloat
private let actions: Actions
public init(
title: String,
metadata: String? = nil,
summary: String? = nil,
artworkURL: URL?,
logoURL: URL? = nil,
// 16:9, weil Plex genau das als `art` liefert. Vorher stand hier
// 3:4 — mit `scaledToFill` wurde das Bild dadurch links und rechts
// kräftig beschnitten, und vom Aufmacher war nur ein Ausschnitt zu
// sehen.
aspectRatio: CGFloat = FlixyAspectRatio.hero,
@ViewBuilder actions: () -> Actions
) {
self.title = title
self.metadata = metadata
self.summary = summary
self.artworkURL = artworkURL
self.logoURL = logoURL
self.aspectRatio = aspectRatio
self.actions = actions()
}
/// Schriftzug, wenn vorhanden — sonst gesetzter Titel.
///
/// Der Schriftzug wird ohne Bild-Transcoder geladen; er ist ein
/// freigestelltes PNG, und der Transcoder liefert JPEG. Aus dem
/// freigestellten Logo würde sonst ein schwarzer Kasten.
@ViewBuilder
private var titleView: some View {
if let logoURL {
AsyncImage(url: logoURL) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFit()
// Nach oben begrenzt: manche Schriftzüge sind fast
// quadratisch, andere sehr breit. Ohne Deckel schöbe
// ein hohes Logo die Knöpfe aus dem Bild.
.frame(maxWidth: 260, maxHeight: 76, alignment: .leading)
default:
// Solange geladen wird — und wenn es scheitert — bleibt es
// beim gesetzten Titel. Ein leerer Platz wäre schlimmer
// als eine Systemschrift.
plainTitle
}
}
.accessibilityLabel(Text(title))
} else {
plainTitle
}
}
private var plainTitle: some View {
Text(title)
.font(.flixyHero)
.foregroundStyle(Color.flixyTextPrimary)
.lineLimit(2)
}
public var body: some View {
FlixyArtwork(url: artworkURL, aspectRatio: aspectRatio)
.overlay(alignment: .bottom) {
// Der Verlauf endet in der Hintergrundfarbe der Seite, nicht in
// Schwarz: sonst entsteht genau dort, wo der Aufmacher in die
// erste Reihe übergeht, eine sichtbare Kante.
LinearGradient(
stops: [
.init(color: .clear, location: 0),
.init(color: Color.flixyCanvas.opacity(0.75), location: 0.45),
.init(color: Color.flixyCanvas, location: 1),
],
startPoint: .top,
endPoint: .bottom
)
.frame(maxHeight: .infinity, alignment: .bottom)
.allowsHitTesting(false)
}
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading, spacing: FlixySpacing.snug) {
titleView
if let metadata {
Text(metadata)
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
}
if let summary {
Text(summary)
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
.lineLimit(2)
}
HStack(spacing: FlixySpacing.regular) {
actions
}
.padding(.top, FlixySpacing.tight)
}
.padding(.horizontal, FlixySpacing.screenMargin)
.padding(.bottom, FlixySpacing.loose)
}
}
}