Ein Untertitelmenü statt zweier — und Staffel statt S
Zwei Sachen, die am laufenden Player auffielen. **Zwei Sprechblasen nebeneinander.** Eine war Apples eingebautes Untertitelmenü, eine unseres — und welche was tut, sah man erst nach dem Drücken. Schlimmer als die Doppelung war, was dahinterlag: Apples Menü verstellt die **Systemeinstellung** für Untertitel, während unsere Engine `AVTextStyleRule`s aus `SubtitleAppearance` setzt. Zwei Stilregler, von denen nur einer wirkt. Apples Knopf ist jetzt abgeschaltet — `allowedSubtitleOptionLanguages` auf eine leere Liste, der einzige dokumentierte Hebel dafür; die Auswahl von Hand bleibt unberührt. Unser Menü trägt die Spuren oben und darunter Schriftgröße und Darstellung als Untermenüs. Tiefer geht es nicht: die Transportleiste zeigt genau eine Ebene. **Und das Tonmenü verschwindet, wenn Apple es besser kann.** Wird die Datei unverändert gespielt und stimmt die Spurzahl, liegen alle Spuren im Stream — dann zeigt Apple sie selbst, und zwei Knöpfe wären dasselbe zweimal. Nur beim Umrechnen liegt im Stream genau eine, und dann führt allein unseres weiter. **„S7 E2" heisst jetzt „Staffel 7 · Episode 2"**, wo Platz ist: in der Titelzeile des Players und im Aufmacher am Fernseher. Auf einer Kachel bleibt die kurze Form — dort ist eine Zeile breit, und ausgeschrieben verdrängte sie den Titel. Mitgewandert in die geteilte Schicht sind die Namen der Stilstufen: sie standen bei der Einstellungsansicht und wurden damit nur vom Telefon übersetzt. Zwei Listen gleich zu halten ist genau die Art Fehler, die erst auffällt, wenn jemand „Gelb auf Schwarz" wählt und „Weiss auf Schwarz" bekommt. Der Stil wirkt sofort und ohne Neuaufbau, und die Haken im Menü wandern mit: dafür steht er in der Momentaufnahme, obwohl der Player selbst ihn nicht braucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
12bfc9e530
commit
e2714e058e
@@ -1551,6 +1551,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"episode.long %lld %lld": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Staffel %lld · Episode %lld"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Season %lld · Episode %lld"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"episode.number %lld": {
|
"episode.number %lld": {
|
||||||
"extractionState": "manual",
|
"extractionState": "manual",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
@@ -1569,7 +1586,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"episode.short %lld %lld": {
|
"episode.short %lld %lld": {
|
||||||
"comment": "Staffel- und Folgennummer",
|
"comment": "Nur auf Kacheln, wo eine Zeile breit ist. Wo Platz ist — Player, Aufmacher am Fernseher — steht episode.long.",
|
||||||
"extractionState": "stale",
|
"extractionState": "stale",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de": {
|
"de": {
|
||||||
@@ -6959,6 +6976,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"subtitleStyle.sizeHeader": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Schriftgröße"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Text size"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"subtitleStyle.title": {
|
"subtitleStyle.title": {
|
||||||
"extractionState": "manual",
|
"extractionState": "manual",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import FlixyPlayback
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Die Namen der Stufen stehen neben dem Modell, nicht darin — dasselbe
|
||||||
|
/// Muster wie bei `PlaybackSettings.Quality`: `SubtitleStyle` liegt im
|
||||||
|
/// prüfbaren Teil und soll keinen Textkatalog kennen.
|
||||||
|
///
|
||||||
|
/// **Hier und nicht bei der Einstellungsansicht.** Dort standen sie, und
|
||||||
|
/// damit übersetzte nur das Telefon sie: am Fernseher steht derselbe Stil im
|
||||||
|
/// Untertitelmenü des Players, und zwei Listen gleich zu halten ist genau die
|
||||||
|
/// Art Fehler, die erst auffällt, wenn jemand „Gelb auf Schwarz" wählt und
|
||||||
|
/// „Weiss auf Schwarz" bekommt.
|
||||||
|
///
|
||||||
|
/// Als `String` und nicht als `LocalizedStringKey`: ein `UIMenu` nimmt keinen
|
||||||
|
/// Schlüssel entgegen, und `Text` kommt mit beidem zurecht.
|
||||||
|
extension SubtitleStyle.Size {
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .small: String(localized: "subtitleStyle.size.small")
|
||||||
|
case .normal: String(localized: "subtitleStyle.size.normal")
|
||||||
|
case .large: String(localized: "subtitleStyle.size.large")
|
||||||
|
case .huge: String(localized: "subtitleStyle.size.huge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SubtitleStyle.Palette {
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .whiteOnClear: String(localized: "subtitleStyle.palette.whiteOnClear")
|
||||||
|
case .whiteOnBlack: String(localized: "subtitleStyle.palette.whiteOnBlack")
|
||||||
|
case .blackOnWhite: String(localized: "subtitleStyle.palette.blackOnWhite")
|
||||||
|
case .yellowOnClear: String(localized: "subtitleStyle.palette.yellowOnClear")
|
||||||
|
case .yellowOnBlack: String(localized: "subtitleStyle.palette.yellowOnBlack")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2049,7 +2049,10 @@ final class PlayerModel {
|
|||||||
private func playerSubtitle(for item: MediaItem) -> String? {
|
private func playerSubtitle(for item: MediaItem) -> String? {
|
||||||
guard item.kind == .episode else { return nil }
|
guard item.kind == .episode else { return nil }
|
||||||
if let season = item.parentIndex, let episode = item.index {
|
if let season = item.parentIndex, let episode = item.index {
|
||||||
return "\(String(localized: "episode.short \(season) \(episode)")) · \(item.title)"
|
// **Ausgeschrieben, nicht abgekürzt.** Diese Zeile steht in der
|
||||||
|
// Titelzeile des Players und hat die volle Breite; „S7 E2" spart
|
||||||
|
// dort nichts und liest sich wie eine Signatur.
|
||||||
|
return "\(String(localized: "episode.long \(season) \(episode)")) · \(item.title)"
|
||||||
}
|
}
|
||||||
return item.title
|
return item.title
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,28 +98,3 @@ extension SubtitleStyle.RGBA {
|
|||||||
Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
|
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ struct TVHeroSection: View {
|
|||||||
private var metadaten: String? {
|
private var metadaten: String? {
|
||||||
var teile: [String] = []
|
var teile: [String] = []
|
||||||
if item.kind == .episode, let staffel = item.parentIndex, let folge = item.index {
|
if item.kind == .episode, let staffel = item.parentIndex, let folge = item.index {
|
||||||
teile.append(String(localized: "episode.short \(staffel) \(folge)"))
|
// Ausgeschrieben: der Aufmacher am Fernseher ist 1760 Punkte
|
||||||
|
// breit, und gelesen wird er aus drei Metern.
|
||||||
|
teile.append(String(localized: "episode.long \(staffel) \(folge)"))
|
||||||
}
|
}
|
||||||
if let jahr = item.year { teile.append(String(jahr)) }
|
if let jahr = item.year { teile.append(String(jahr)) }
|
||||||
if let dauer = item.duration {
|
if let dauer = item.duration {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ final class TVPlayerCoordinator: NSObject, @MainActor AVPlayerViewControllerDele
|
|||||||
let controller = AVPlayerViewController()
|
let controller = AVPlayerViewController()
|
||||||
|
|
||||||
private let onDismiss: () -> Void
|
private let onDismiss: () -> Void
|
||||||
|
private let appearance: SubtitleAppearance
|
||||||
private var letzteAufnahme: TVPlayerSnapshot?
|
private var letzteAufnahme: TVPlayerSnapshot?
|
||||||
/// Wacht darüber, wann es etwas zu beschriften gibt.
|
/// Wacht darüber, wann es etwas zu beschriften gibt.
|
||||||
///
|
///
|
||||||
@@ -35,7 +36,8 @@ final class TVPlayerCoordinator: NSObject, @MainActor AVPlayerViewControllerDele
|
|||||||
private lazy var abdeckung = TVRebuildOverlay()
|
private lazy var abdeckung = TVRebuildOverlay()
|
||||||
private var abdeckungsAufgabe: Task<Void, Never>?
|
private var abdeckungsAufgabe: Task<Void, Never>?
|
||||||
|
|
||||||
init(onDismiss: @escaping () -> Void) {
|
init(appearance: SubtitleAppearance, onDismiss: @escaping () -> Void) {
|
||||||
|
self.appearance = appearance
|
||||||
self.onDismiss = onDismiss
|
self.onDismiss = onDismiss
|
||||||
super.init()
|
super.init()
|
||||||
controller.delegate = self
|
controller.delegate = self
|
||||||
@@ -44,6 +46,20 @@ final class TVPlayerCoordinator: NSObject, @MainActor AVPlayerViewControllerDele
|
|||||||
controller.allowsPictureInPicturePlayback = false
|
controller.allowsPictureInPicturePlayback = false
|
||||||
controller.transportBarIncludesTitleView = true
|
controller.transportBarIncludesTitleView = true
|
||||||
|
|
||||||
|
// **Apples eigenen Untertitelknopf abschalten.**
|
||||||
|
//
|
||||||
|
// Sonst stehen zwei Sprechblasen nebeneinander, und welche was tut,
|
||||||
|
// sieht man erst nach dem Drücken. Schlimmer noch: Apples Menü
|
||||||
|
// verstellt die **Systemeinstellung** für Untertitel, während unsere
|
||||||
|
// Engine `AVTextStyleRule`s aus `SubtitleAppearance` setzt — zwei
|
||||||
|
// Stilregler, von denen nur einer wirkt.
|
||||||
|
//
|
||||||
|
// `nil` heisst „alle Sprachen anbieten", eine leere Liste also keine.
|
||||||
|
// Das ist der einzige dokumentierte Hebel dafür; die Auswahl von Hand
|
||||||
|
// (`AVPlayerEngine`) bleibt davon unberührt, hier geht es nur um das,
|
||||||
|
// was die Leiste anbietet.
|
||||||
|
controller.allowedSubtitleOptionLanguages = []
|
||||||
|
|
||||||
// **Hinter den Bedienelementen und ohne Fokus.** `contentOverlayView`
|
// **Hinter den Bedienelementen und ohne Fokus.** `contentOverlayView`
|
||||||
// liegt über dem Bild, aber unter Apples Leiste, und nimmt keine
|
// liegt über dem Bild, aber unter Apples Leiste, und nimmt keine
|
||||||
// Eingaben an. `customOverlayViewController` täte beides nicht: der
|
// Eingaben an. `customOverlayViewController` täte beides nicht: der
|
||||||
@@ -79,6 +95,7 @@ final class TVPlayerCoordinator: NSObject, @MainActor AVPlayerViewControllerDele
|
|||||||
zeigeAbdeckung(aufnahme.isRebuilding)
|
zeigeAbdeckung(aufnahme.isRebuilding)
|
||||||
controller.transportBarCustomMenuItems = TVTrackMenuBuilder.items(
|
controller.transportBarCustomMenuItems = TVTrackMenuBuilder.items(
|
||||||
for: aufnahme,
|
for: aufnahme,
|
||||||
|
appearance: appearance,
|
||||||
onAudio: { [weak model] index in
|
onAudio: { [weak model] index in
|
||||||
guard let model, let stream = model.audioStreams.first(where: { $0.index == index })
|
guard let model, let stream = model.audioStreams.first(where: { $0.index == index })
|
||||||
else { return }
|
else { return }
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ struct TVPlayerHost: UIViewControllerRepresentable {
|
|||||||
/// Rumpf der umgebenden Ansicht gebildet — dort und nur dort entsteht die
|
/// Rumpf der umgebenden Ansicht gebildet — dort und nur dort entsteht die
|
||||||
/// Beobachtungsbeziehung zu `@Observable`.
|
/// Beobachtungsbeziehung zu `@Observable`.
|
||||||
let snapshot: TVPlayerSnapshot
|
let snapshot: TVPlayerSnapshot
|
||||||
|
/// Der Untertitelstil — er steht im selben Menü wie die Spurwahl.
|
||||||
|
let appearance: SubtitleAppearance
|
||||||
let onDismiss: () -> Void
|
let onDismiss: () -> Void
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||||
@@ -32,7 +34,7 @@ struct TVPlayerHost: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> TVPlayerCoordinator {
|
func makeCoordinator() -> TVPlayerCoordinator {
|
||||||
TVPlayerCoordinator(onDismiss: onDismiss)
|
TVPlayerCoordinator(appearance: appearance, onDismiss: onDismiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,11 +51,19 @@ struct TVPlayerSnapshot: Equatable {
|
|||||||
let subtitle: String?
|
let subtitle: String?
|
||||||
/// Der Zeitbereich einer Marke — für „Intro überspringen".
|
/// Der Zeitbereich einer Marke — für „Intro überspringen".
|
||||||
let markerLabel: String?
|
let markerLabel: String?
|
||||||
|
/// Der eingestellte Untertitelstil.
|
||||||
|
///
|
||||||
|
/// **Gehört in die Momentaufnahme, obwohl der Player ihn nicht braucht.**
|
||||||
|
/// Er steht im selben Menü wie die Spurwahl, und ohne ihn hier merkte
|
||||||
|
/// SwiftUI nicht, dass sich etwas geändert hat — die Haken blieben beim
|
||||||
|
/// alten Wert stehen, während die Untertitel längst anders aussähen.
|
||||||
|
let subtitleStyle: SubtitleStyle
|
||||||
|
|
||||||
/// `@MainActor`, weil das Modell es ist: gelesen wird hier zwölfmal aus
|
/// `@MainActor`, weil das Modell es ist: gelesen wird hier zwölfmal aus
|
||||||
/// ihm, und zwar im Rumpf einer View — also ohnehin auf dem Hauptakteur.
|
/// ihm, und zwar im Rumpf einer View — also ohnehin auf dem Hauptakteur.
|
||||||
@MainActor
|
@MainActor
|
||||||
init(_ model: PlayerModel, offLabel: String) {
|
init(_ model: PlayerModel, appearance: SubtitleAppearance, offLabel: String) {
|
||||||
|
subtitleStyle = appearance.style
|
||||||
playerIdentity = model.systemPlayer.map(ObjectIdentifier.init)
|
playerIdentity = model.systemPlayer.map(ObjectIdentifier.init)
|
||||||
let direkt: Bool = if case .directPlay = model.decision?.method { true } else { false }
|
let direkt: Bool = if case .directPlay = model.decision?.method { true } else { false }
|
||||||
audio = TrackMenu.audio(
|
audio = TrackMenu.audio(
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ struct TVPlayerScreen: View {
|
|||||||
// ein `updateUIViewController`.
|
// ein `updateUIViewController`.
|
||||||
TVPlayerHost(
|
TVPlayerHost(
|
||||||
model: model,
|
model: model,
|
||||||
snapshot: TVPlayerSnapshot(model, offLabel: String(localized: "player.subtitlesOff")),
|
snapshot: TVPlayerSnapshot(
|
||||||
|
model,
|
||||||
|
appearance: subtitleAppearance,
|
||||||
|
offLabel: String(localized: "player.subtitlesOff")
|
||||||
|
),
|
||||||
|
appearance: subtitleAppearance,
|
||||||
onDismiss: { playback.close() }
|
onDismiss: { playback.close() }
|
||||||
)
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
@@ -43,6 +48,19 @@ struct TVPlayerScreen: View {
|
|||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **Der Stil wirkt sofort, ohne Neuaufbau.**
|
||||||
|
//
|
||||||
|
// Auf `style` und nicht auf die Klasse: ein `@Observable` meldet jede
|
||||||
|
// Berührung, und dann liefe das hier auch, wenn sich nichts geändert
|
||||||
|
// hat. `SubtitleStyle` ist vergleichbar und feuert nur bei einer
|
||||||
|
// echten Änderung.
|
||||||
|
//
|
||||||
|
// Dass es hier steht und nicht im Coordinator, hat denselben Grund
|
||||||
|
// wie die Momentaufnahme: nur im Rumpf einer View entsteht die
|
||||||
|
// Beobachtungsbeziehung.
|
||||||
|
.onChange(of: subtitleAppearance.style) { _, neu in
|
||||||
|
model?.engine?.applySubtitleStyle(neu)
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
guard model == nil else { return }
|
guard model == nil else { return }
|
||||||
let frisch = PlayerModel(
|
let frisch = PlayerModel(
|
||||||
|
|||||||
@@ -8,67 +8,119 @@ import FlixyPlayback
|
|||||||
/// nur noch um UIKit.
|
/// nur noch um UIKit.
|
||||||
@MainActor
|
@MainActor
|
||||||
enum TVTrackMenuBuilder {
|
enum TVTrackMenuBuilder {
|
||||||
|
/// - Parameter appearance: Der Untertitelstil. Er steht **in** diesem
|
||||||
|
/// Menü und nicht daneben: vorher gab es zwei Knöpfe mit derselben
|
||||||
|
/// Sprechblase — einen für die Spur, einen für den Stil —, und welcher
|
||||||
|
/// was tat, sah man erst nach dem Drücken.
|
||||||
static func items(
|
static func items(
|
||||||
for aufnahme: TVPlayerSnapshot,
|
for aufnahme: TVPlayerSnapshot,
|
||||||
|
appearance: SubtitleAppearance,
|
||||||
onAudio: @escaping (Int?) -> Void,
|
onAudio: @escaping (Int?) -> Void,
|
||||||
onSubtitle: @escaping (Int?) -> Void
|
onSubtitle: @escaping (Int?) -> Void
|
||||||
) -> [UIMenuElement] {
|
) -> [UIMenuElement] {
|
||||||
var menüs: [UIMenuElement] = []
|
var menüs: [UIMenuElement] = []
|
||||||
if aufnahme.audio.count > 1 {
|
|
||||||
menüs.append(menü(
|
// **Unser Tonmenü nur, wenn Apples eigenes es nicht kann.**
|
||||||
titel: String(localized: "player.audioTracks"),
|
//
|
||||||
symbol: "waveform",
|
// Wird die Datei unverändert gespielt und stimmt die Spurzahl, liegen
|
||||||
einträge: aufnahme.audio,
|
// alle Spuren im Stream — dann zeigt Apple sie selbst, und zwei
|
||||||
gesperrt: aufnahme.isRebuilding,
|
// Knöpfe nebeneinander wären dasselbe zweimal. Beim Umrechnen liegt
|
||||||
aktion: onAudio
|
// im Stream dagegen genau eine, und nur unser Menü führt weiter.
|
||||||
|
if aufnahme.audio.count > 1, aufnahme.audio.contains(where: \.rebuilds) {
|
||||||
|
menüs.append(UIMenu(
|
||||||
|
title: String(localized: "player.audioTracks"),
|
||||||
|
image: UIImage(systemName: "waveform"),
|
||||||
|
options: [.singleSelection],
|
||||||
|
children: einträge(aufnahme.audio, gesperrt: aufnahme.isRebuilding, aktion: onAudio)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if aufnahme.subtitles.count > 1 {
|
|
||||||
menüs.append(menü(
|
// **Untertitel immer über uns**, auch wenn Apple es könnte: hier hängt
|
||||||
titel: String(localized: "player.subtitleTracks"),
|
// der Stil mit drin, und der ist unserer — `AVTextStyleRule`s aus
|
||||||
symbol: "captions.bubble",
|
// `SubtitleAppearance`, nicht die Systemeinstellung, die Apples Menü
|
||||||
einträge: aufnahme.subtitles,
|
// verstellt. Apples eigener Knopf ist deshalb abgeschaltet; siehe
|
||||||
|
// `TVPlayerCoordinator`.
|
||||||
|
menüs.append(UIMenu(
|
||||||
|
title: String(localized: "player.subtitleTracks"),
|
||||||
|
image: UIImage(systemName: "captions.bubble"),
|
||||||
|
children: [
|
||||||
|
// Inline: die Spuren stehen ohne Umweg da, der Stil darunter
|
||||||
|
// als eigene Untermenüs. Tiefer geht es nicht — die
|
||||||
|
// Transportleiste zeigt genau eine Ebene.
|
||||||
|
UIMenu(options: [.displayInline, .singleSelection],
|
||||||
|
children: einträge(aufnahme.subtitles,
|
||||||
gesperrt: aufnahme.isRebuilding,
|
gesperrt: aufnahme.isRebuilding,
|
||||||
aktion: onSubtitle
|
aktion: onSubtitle)),
|
||||||
|
größenMenü(appearance),
|
||||||
|
darstellungsMenü(appearance),
|
||||||
|
]
|
||||||
))
|
))
|
||||||
}
|
|
||||||
return menüs
|
return menüs
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func menü(
|
// MARK: - Die Spuren
|
||||||
titel: String,
|
|
||||||
symbol: String,
|
private static func einträge(
|
||||||
einträge: [TrackMenu.Entry],
|
_ liste: [TrackMenu.Entry],
|
||||||
gesperrt: Bool,
|
gesperrt: Bool,
|
||||||
aktion: @escaping (Int?) -> Void
|
aktion: @escaping (Int?) -> Void
|
||||||
) -> UIMenu {
|
) -> [UIMenuElement] {
|
||||||
UIMenu(
|
liste.map { eintrag in
|
||||||
title: titel,
|
|
||||||
image: UIImage(systemName: symbol),
|
|
||||||
options: [.singleSelection],
|
|
||||||
children: einträge.map { eintrag in
|
|
||||||
UIAction(
|
UIAction(
|
||||||
title: eintrag.title,
|
title: eintrag.title,
|
||||||
// **Vorher ansagen, nicht hinterher erklären.** Steht hier
|
// **Vorher ansagen, nicht hinterher erklären.** Steht hier
|
||||||
// nichts, ist die Sekundenpause danach eine Panne; steht
|
// nichts, ist die Sekundenpause danach eine Panne; steht es
|
||||||
// es hier, ist sie eine angekündigte Folge. Das ist der
|
// hier, ist sie eine angekündigte Folge. Das ist der billigste
|
||||||
// billigste Teil dieser ganzen Sache und der wirksamste.
|
// Teil dieser ganzen Sache und der wirksamste.
|
||||||
subtitle: eintrag.rebuilds
|
subtitle: eintrag.rebuilds ? String(localized: "player.rebuilds") : nil,
|
||||||
? String(localized: "player.rebuilds")
|
// **Keine zweite Bestellung, solange die erste läuft.** Sonst
|
||||||
: nil,
|
// räumt `restart()` eine Engine ab, die eine andere gerade baut.
|
||||||
// **Keine zweite Bestellung, solange die erste läuft.**
|
|
||||||
// Sonst räumt `restart()` eine Engine ab, die eine andere
|
|
||||||
// gerade baut.
|
|
||||||
attributes: gesperrt ? [.disabled] : [],
|
attributes: gesperrt ? [.disabled] : [],
|
||||||
state: eintrag.isSelected ? .on : .off
|
state: eintrag.isSelected ? .on : .off
|
||||||
) { auslöser in
|
) { auslöser in
|
||||||
// **Sofort quittieren.** Das Menü schliesst sich beim
|
// **Sofort quittieren.** Das Menü schliesst sich beim Drücken,
|
||||||
// Drücken, und darin ist kein Platz für einen Kreisel.
|
// und darin ist kein Platz für einen Kreisel. Der Haken
|
||||||
// Der Haken wandert, bevor sich am Bild etwas tut.
|
// wandert, bevor sich am Bild etwas tut.
|
||||||
auslöser.state = .on
|
auslöser.state = .on
|
||||||
aktion(eintrag.id)
|
aktion(eintrag.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Der Stil
|
||||||
|
|
||||||
|
private static func größenMenü(_ appearance: SubtitleAppearance) -> UIMenu {
|
||||||
|
UIMenu(
|
||||||
|
title: String(localized: "subtitleStyle.sizeHeader"),
|
||||||
|
image: UIImage(systemName: "textformat.size"),
|
||||||
|
options: [.singleSelection],
|
||||||
|
children: SubtitleStyle.Size.allCases.map { größe in
|
||||||
|
UIAction(
|
||||||
|
title: größe.label,
|
||||||
|
state: appearance.size == größe ? .on : .off
|
||||||
|
) { auslöser in
|
||||||
|
auslöser.state = .on
|
||||||
|
appearance.size = größe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func darstellungsMenü(_ appearance: SubtitleAppearance) -> UIMenu {
|
||||||
|
UIMenu(
|
||||||
|
title: String(localized: "subtitleStyle.paletteHeader"),
|
||||||
|
image: UIImage(systemName: "paintpalette"),
|
||||||
|
options: [.singleSelection],
|
||||||
|
children: SubtitleStyle.Palette.allCases.map { palette in
|
||||||
|
UIAction(
|
||||||
|
title: palette.label,
|
||||||
|
state: appearance.palette == palette ? .on : .off
|
||||||
|
) { auslöser in
|
||||||
|
auslöser.state = .on
|
||||||
|
appearance.palette = palette
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user