Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.0 KiB
Swift
100 lines
4.0 KiB
Swift
import Foundation
|
|
import MediaKit
|
|
|
|
/// Was in einem Spurenmenü steht — und was ein Umschalten kostet.
|
|
///
|
|
/// **Warum es diesen Typ gibt.** Auf dem Fernseher sitzt die Spurwahl in
|
|
/// Apples Transportleiste, und die nimmt `UIMenu`-Einträge entgegen. Die
|
|
/// Abbildung von Spuren auf Einträge hat mit UIKit aber nichts zu tun: sie
|
|
/// beantwortet, welche Spuren zur Wahl stehen, welche gerade gilt, und — das
|
|
/// ist der Teil, der sonst nirgends steht — **ob das Umschalten Sekunden mit
|
|
/// schwarzem Bild kostet**.
|
|
///
|
|
/// Diese Regel lag bisher nur mitten im Vollzug von
|
|
/// `PlayerModel.selectAudioStream` und war damit weder ablesbar noch prüfbar:
|
|
/// auslösen lässt sie sich nur an einem echten Server mit einer Datei, die
|
|
/// mehrere Tonspuren hat. Hier steht sie als Auskunft — und die Oberfläche
|
|
/// kann sie **vorher** anzeigen, statt den Nutzer hinterher vor ein schwarzes
|
|
/// Bild zu setzen.
|
|
///
|
|
/// Liegt neben `AudioTrackChooser` und `DowngradeRule`, weil es dasselbe ist:
|
|
/// eine Regel über die Wiedergabe, ohne Oberfläche und ohne Netz.
|
|
public enum TrackMenu {
|
|
public struct Entry: Equatable, Sendable, Identifiable {
|
|
/// Der Index der Spur beim Server. `nil` heisst „Aus".
|
|
public let id: Int?
|
|
public let title: String
|
|
public let isSelected: Bool
|
|
/// Ob das Umschalten den Stream neu aufbaut.
|
|
public let rebuilds: Bool
|
|
|
|
public init(id: Int?, title: String, isSelected: Bool, rebuilds: Bool) {
|
|
self.id = id
|
|
self.title = title
|
|
self.isSelected = isSelected
|
|
self.rebuilds = rebuilds
|
|
}
|
|
}
|
|
|
|
/// Die Tonspuren **der Datei**, nicht die des Players.
|
|
///
|
|
/// Rechnet der Server um, kennt der Player genau eine Spur — die, für die
|
|
/// er sich entschieden hat. Zur Wahl steht, was in der Datei liegt.
|
|
///
|
|
/// - Parameter playerTrackCount: Wie viele Tonspuren der Player meldet.
|
|
/// Weicht die Zahl ab, stimmt die Zuordnung über die Reihenfolge nicht,
|
|
/// und es muss neu aufgebaut werden — lieber eine Unterbrechung als die
|
|
/// falsche Sprache.
|
|
public static func audio(
|
|
from streams: [MediaStream],
|
|
selected: MediaStream?,
|
|
isDirectPlay: Bool,
|
|
playerTrackCount: Int
|
|
) -> [Entry] {
|
|
let billig = isDirectPlay && playerTrackCount == streams.count
|
|
return streams.enumerated().map { stelle, stream in
|
|
Entry(
|
|
id: stream.index ?? stelle,
|
|
title: stream.name,
|
|
isSelected: stream == selected,
|
|
rebuilds: !billig
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Die Untertitelspuren, mit „Aus" an erster Stelle.
|
|
///
|
|
/// - Parameter offLabel: Wie „Aus" in der Sprache des Nutzers heisst.
|
|
/// Als Argument, weil dieses Paket keinen Textkatalog der App kennt.
|
|
/// - Parameter playerTrackLanguages: Die Sprachkürzel der Spuren, die der
|
|
/// Player wirklich hat. Nur was sich darin abbilden lässt, schaltet ohne
|
|
/// Neuaufbau um — sonst stünde der falsche Untertitel im Bild, und das
|
|
/// ist schlimmer als eine kurze Unterbrechung.
|
|
public static func subtitles(
|
|
from streams: [MediaStream],
|
|
selected: MediaStream?,
|
|
isDirectPlay: Bool,
|
|
playerTrackLanguages: [String?] = [],
|
|
offLabel: String
|
|
) -> [Entry] {
|
|
// „Aus" kostet nie einen Neuaufbau: abschalten kann der Player immer,
|
|
// auch eine Spur, die er nie hatte.
|
|
var einträge = [
|
|
Entry(id: nil, title: offLabel, isSelected: selected == nil, rebuilds: false)
|
|
]
|
|
|
|
einträge += streams.enumerated().map { stelle, stream in
|
|
let abbildbar = isDirectPlay && playerTrackLanguages.contains {
|
|
LanguageCode.matches(stream.languageCode, $0)
|
|
}
|
|
return Entry(
|
|
id: stream.index ?? stelle,
|
|
title: stream.name,
|
|
isSelected: stream == selected,
|
|
rebuilds: !abbildbar
|
|
)
|
|
}
|
|
return einträge
|
|
}
|
|
}
|