Groesse in vier Stufen, Schrift und Hintergrund als fuenf feste Paare — Weiss, Weiss auf Schwarz, Schwarz auf Weiss, Gelb, Gelb auf Schwarz. Feste Paare statt zweier Regler, weil unabhaengige Achsen unlesbare Mischungen zulassen: Schwarz auf durchsichtig ueber einer Nachtszene ist eine gueltige Einstellung und eine wertlose. Die Uebersetzung nach AVFoundation ist eine Tabelle aus Schluesseln, bei der jeder Fehler lautlos ist — ein falscher Schluessel laesst den Initialisierer `nil` zurueckgeben, und dann gibt es gar keine Regel. Auf dem Bildschirm sieht „keine Regel\" genauso aus wie „Regel ohne Wirkung\". Zehn Tests decken das ab, darunter die drei Fallen: Alpha zuerst in der Farbe, Prozentwert statt Punktgroesse, und der Hintergrund auch fuer „durchsichtig\" ausdruecklich gesetzt statt weggelassen — weggelassen gilt sonst, was Jellyfins WebVTT selbst mitbringt, und das ist oft ein schwarzer Kasten. Als Protokollmethode, nicht als Feld an `PlaybackSource`: die wird einmal beim Oeffnen gelesen, und eine Einstellung, die erst beim naechsten Film wirkt, haelt jeder fuer kaputt. Angewandt wird an zwei Stellen — am laufenden Gegenstand und am frisch angelegten. Nur das erste zu tun hiesse, dass die naechste Folge wieder in der Vorgabe erscheint. VLC erbt die leere Vorgabe. Die Einstellungsseite sagt beides: dass es fuer den Standardplayer gilt, und dass eingebrannte Bilduntertitel von Blu-rays sich nicht gestalten lassen, weil es dort keinen Text gibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
4.3 KiB
Swift
126 lines
4.3 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import FlixyPlayback
|
|
|
|
/// Wie Untertitel aussehen sollen.
|
|
struct SubtitleAppearanceView: View {
|
|
@Environment(SubtitleAppearance.self) private var appearance
|
|
@Environment(\.flixyAccent) private var accent
|
|
|
|
var body: some View {
|
|
@Bindable var appearance = appearance
|
|
|
|
Form {
|
|
SettingsHeader(
|
|
symbol: "textformat.size",
|
|
colour: SettingsPalette.subtitles,
|
|
title: "subtitleStyle.title",
|
|
explanation: "subtitleStyle.headerExplanation"
|
|
)
|
|
|
|
Section {
|
|
preview
|
|
} header: {
|
|
Text("subtitleStyle.previewHeader")
|
|
}
|
|
|
|
Section {
|
|
Picker("subtitleStyle.size", selection: $appearance.size) {
|
|
ForEach(SubtitleStyle.Size.allCases) { size in
|
|
Text(size.label).tag(size)
|
|
}
|
|
}
|
|
} footer: {
|
|
Text("subtitleStyle.sizeExplanation")
|
|
}
|
|
|
|
Section {
|
|
Picker("subtitleStyle.palette", selection: $appearance.palette) {
|
|
ForEach(SubtitleStyle.Palette.allCases) { palette in
|
|
Text(palette.label).tag(palette)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("subtitleStyle.paletteHeader")
|
|
} footer: {
|
|
// Die Grenze gehört hierher, nicht nur in einen Kommentar:
|
|
// bei eingebrannten Untertiteln tut jede dieser Einstellungen
|
|
// sichtbar nichts, und ohne Erklärung sieht das nach einem
|
|
// Fehler aus.
|
|
Text("subtitleStyle.paletteExplanation")
|
|
}
|
|
}
|
|
.tint(accent)
|
|
.flixyForm()
|
|
.scrollContentBackground(.hidden)
|
|
.background(Color.flixyCanvas)
|
|
.navigationTitle(Text("subtitleStyle.title"))
|
|
.flixyInlineTitle()
|
|
}
|
|
|
|
/// Ein Beispielsatz über grauer Fläche.
|
|
///
|
|
/// **Eine Nachbildung, nicht AVFoundations Renderer.** Sie zeigt die
|
|
/// Wirkung der Einstellung, nicht das Bild auf den Punkt genau — wer
|
|
/// später zwei Pixel „repariert", repariert die falsche Sache.
|
|
private var preview: some View {
|
|
ZStack {
|
|
// Mittelgrau: auf Weiss wäre weisse Schrift unsichtbar, auf
|
|
// Schwarz schwarze. Ein Grauton zeigt beide ehrlich.
|
|
LinearGradient(
|
|
colors: [Color(white: 0.35), Color(white: 0.18)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
Text("subtitleStyle.previewText")
|
|
.font(.system(size: 15 * CGFloat(appearance.size.rawValue) / 100, weight: .medium))
|
|
.foregroundStyle(appearance.style.foreground.color)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 3)
|
|
.background(appearance.style.background.color)
|
|
.shadow(
|
|
color: appearance.style.wantsEdgeShadow ? .black.opacity(0.9) : .clear,
|
|
radius: 2,
|
|
y: 1
|
|
)
|
|
.padding(FlixySpacing.regular)
|
|
}
|
|
.frame(height: 96)
|
|
.clipShape(.rect(cornerRadius: FlixyRadius.card))
|
|
.listRowInsets(EdgeInsets())
|
|
.listRowBackground(Color.clear)
|
|
}
|
|
}
|
|
|
|
extension SubtitleStyle.RGBA {
|
|
var color: Color {
|
|
Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
|
|
}
|
|
}
|
|
|
|
/// Die Namen der Stufen stehen neben der Ansicht, nicht im Modell — dasselbe
|
|
/// Muster wie bei `PlaybackSettings.Quality`.
|
|
extension SubtitleStyle.Size {
|
|
var label: LocalizedStringKey {
|
|
switch self {
|
|
case .small: "subtitleStyle.size.small"
|
|
case .normal: "subtitleStyle.size.normal"
|
|
case .large: "subtitleStyle.size.large"
|
|
case .huge: "subtitleStyle.size.huge"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SubtitleStyle.Palette {
|
|
var label: LocalizedStringKey {
|
|
switch self {
|
|
case .whiteOnClear: "subtitleStyle.palette.whiteOnClear"
|
|
case .whiteOnBlack: "subtitleStyle.palette.whiteOnBlack"
|
|
case .blackOnWhite: "subtitleStyle.palette.blackOnWhite"
|
|
case .yellowOnClear: "subtitleStyle.palette.yellowOnClear"
|
|
case .yellowOnBlack: "subtitleStyle.palette.yellowOnBlack"
|
|
}
|
|
}
|
|
}
|