import AVKit import UIKit import FlaroPlayback /// Baut aus unseren Spurlisten die Menüs für Apples Transportleiste. /// /// Die reine Abbildung steht in `TrackMenu` und ist dort geprüft; hier geht es /// nur noch um UIKit. @MainActor 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( for aufnahme: TVPlayerSnapshot, appearance: SubtitleAppearance, onAudio: @escaping (Int?) -> Void, onSubtitle: @escaping (Int?) -> Void ) -> [UIMenuElement] { var menüs: [UIMenuElement] = [] // **Unser Tonmenü nur, wenn Apples eigenes es nicht 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 nebeneinander wären dasselbe zweimal. Beim Umrechnen liegt // 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) )) } // **Untertitel immer über uns**, auch wenn Apple es könnte: hier hängt // der Stil mit drin, und der ist unserer — `AVTextStyleRule`s aus // `SubtitleAppearance`, nicht die Systemeinstellung, die Apples Menü // 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, aktion: onSubtitle)), größenMenü(appearance), darstellungsMenü(appearance), ] )) return menüs } // MARK: - Die Spuren private static func einträge( _ liste: [TrackMenu.Entry], gesperrt: Bool, aktion: @escaping (Int?) -> Void ) -> [UIMenuElement] { liste.map { eintrag in UIAction( title: eintrag.title, // **Vorher ansagen, nicht hinterher erklären.** Steht hier // nichts, ist die Sekundenpause danach eine Panne; steht es // hier, ist sie eine angekündigte Folge. Das ist der billigste // Teil dieser ganzen Sache und der wirksamste. subtitle: eintrag.rebuilds ? String(localized: "player.rebuilds") : nil, // **Keine zweite Bestellung, solange die erste läuft.** Sonst // räumt `restart()` eine Engine ab, die eine andere gerade baut. attributes: gesperrt ? [.disabled] : [], state: eintrag.isSelected ? .on : .off ) { auslöser in // **Sofort quittieren.** Das Menü schliesst sich beim Drücken, // und darin ist kein Platz für einen Kreisel. Der Haken // wandert, bevor sich am Bild etwas tut. auslöser.state = .on 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 } } ) } /// „Intro überspringen" unten rechts, solange eine Marke läuft. /// /// Die tvOS-Entsprechung zu dem Knopf, den der iOS-Player selbst zeichnet. static func contextualActions( for aufnahme: TVPlayerSnapshot, onSkip: @escaping () -> Void ) -> [UIAction] { guard let beschriftung = aufnahme.markerLabel else { return [] } return [UIAction(title: beschriftung) { _ in onSkip() }] } }