Halo heisst ab hier Flaro — die Marke stand im Weg

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>
This commit is contained in:
Scarriffle
2026-10-03 19:38:27 +02:00
co-authored by Claude Opus 5
parent f98b995050
commit a7cf6c5def
390 changed files with 2078 additions and 2069 deletions
@@ -2,21 +2,21 @@
import PackageDescription
let package = Package(
name: "HaloKit",
name: "FlaroKit",
// Quellsprache ist Englisch, damit die Schlüssel im Code lesbar bleiben.
// Deutsch kommt als Übersetzung dazu; die Systemsprache entscheidet.
defaultLocalization: "en",
// macOS steht hier nur, damit `swift test` die Logik-Tests ohne
// Simulator laufen lassen kann — das spart bei jedem Durchlauf Minuten.
// Welche Plattformen die Apps bedienen, legt project.yml fest: iOS für
// Halo, tvOS für HaloTV.
// Flaro, tvOS für FlaroTV.
platforms: [.iOS(.v26), .macOS(.v26), .tvOS(.v26)],
products: [
.library(name: "MediaKit", targets: ["MediaKit"]),
.library(name: "JellyfinKit", targets: ["JellyfinKit"]),
.library(name: "HaloDesign", targets: ["HaloDesign"]),
.library(name: "HaloPlayback", targets: ["HaloPlayback"]),
.library(name: "HaloDownloads", targets: ["HaloDownloads"]),
.library(name: "FlaroDesign", targets: ["FlaroDesign"]),
.library(name: "FlaroPlayback", targets: ["FlaroPlayback"]),
.library(name: "FlaroDownloads", targets: ["FlaroDownloads"]),
],
targets: [
// Sprechen mit dem Server: Anmeldung, Endpunkte, Modelle. Kein SwiftUI,
@@ -42,21 +42,21 @@ let package = Package(
// Ein Werkzeug für die Fragen, die reines HTTP sind — ohne Oberfläche,
// ohne Gerät, ohne Upload. Eine Runde dauert damit Sekunden statt
// eines halben Tages, und das Upload-Kontingent bleibt für Builds.
.executableTarget(name: "HaloProbe", dependencies: ["MediaKit", "JellyfinKit"]),
.executableTarget(name: "FlaroProbe", dependencies: ["MediaKit", "JellyfinKit"]),
// Farben, Typografie, wiederkehrende Bausteine. Hängt bewusst von
// nichts ab: dadurch bleibt das Umstellen der Akzentfarbe ein
// Token-Wechsel und keine Suche quer durch die App.
.target(name: "HaloDesign", resources: [.process("Localizable.xcstrings")]),
.testTarget(name: "HaloDesignTests", dependencies: ["HaloDesign"]),
.target(name: "FlaroDesign", resources: [.process("Localizable.xcstrings")]),
.testTarget(name: "FlaroDesignTests", dependencies: ["FlaroDesign"]),
// Die Wiedergabe-Abstraktion samt AVPlayer-Engine und der Entscheidung
// direkt / umpacken / umrechnen.
.target(name: "HaloPlayback", dependencies: ["MediaKit"]),
.testTarget(name: "HaloPlaybackTests", dependencies: ["HaloPlayback"]),
.target(name: "FlaroPlayback", dependencies: ["MediaKit"]),
.testTarget(name: "FlaroPlaybackTests", dependencies: ["FlaroPlayback"]),
// Warteschlange, Hintergrundübertragung, Offline-Bestand.
.target(name: "HaloDownloads", dependencies: ["MediaKit"], resources: [.process("Localizable.xcstrings")]),
.testTarget(name: "HaloDownloadsTests", dependencies: ["HaloDownloads"]),
.target(name: "FlaroDownloads", dependencies: ["MediaKit"], resources: [.process("Localizable.xcstrings")]),
.testTarget(name: "FlaroDownloadsTests", dependencies: ["FlaroDownloads"]),
]
)
@@ -4,7 +4,7 @@ extension EnvironmentValues {
/// Die aktuelle Akzentfarbe, bereits kontrastkorrigiert.
///
/// Bewusst eine schlichte `Color` in der Umgebung statt einer Abhängigkeit
/// auf `HaloTheme`: Bausteine wie `WatchProgressBar` sollen sich in einer
/// auf `FlaroTheme`: Bausteine wie `WatchProgressBar` sollen sich in einer
/// Vorschau mit einer beliebigen Farbe zeigen lassen, ohne dass dafür ein
/// ganzes Theme-Objekt gebaut werden muss.
/// Der Vorgabewert liest die gespeicherte Auswahl, statt blind die
@@ -14,19 +14,19 @@ extension EnvironmentValues {
/// Formulare zeichnen Zeilen teilweise in einem eigenen Kontext. Vorher
/// leuchtete dort die Standardfarbe, während daneben schon die gewählte
/// stand; jetzt stimmen beide.
@Entry public var haloAccent: Color = AccentOption.storedAccentColor()
@Entry public var flaroAccent: Color = AccentOption.storedAccentColor()
}
extension View {
/// Setzt die Akzentfarbe für diesen Teilbaum.
///
/// Wird einmal an der Wurzel aus `HaloTheme` gesetzt; darunter greift
/// Wird einmal an der Wurzel aus `FlaroTheme` gesetzt; darunter greift
/// alles automatisch darauf zu.
public func haloAccent(_ color: Color) -> some View {
environment(\.haloAccent, color)
public func flaroAccent(_ color: Color) -> some View {
environment(\.flaroAccent, color)
}
public func haloAccent(_ theme: HaloTheme) -> some View {
environment(\.haloAccent, theme.accentColor)
public func flaroAccent(_ theme: FlaroTheme) -> some View {
environment(\.flaroAccent, theme.accentColor)
}
}
@@ -3,7 +3,7 @@ import SwiftUI
/// Eine wählbare Akzentfarbe.
///
/// Die kuratierten Werte sind so gewählt, dass sie auf `HaloColor.canvas`
/// Die kuratierten Werte sind so gewählt, dass sie auf `FlaroColor.canvas`
/// mindestens 4,5:1 erreichen — geprüft von `AccentPaletteTests`, damit beim
/// Nachlegen einer Farbe nicht versehentlich etwas Unlesbares hineinrutscht.
public struct AccentOption: Sendable, Identifiable {
@@ -66,13 +66,13 @@ extension AccentOption {
extension AccentOption {
/// Wo die Auswahl liegt.
///
/// Hier statt in `HaloTheme`, weil zwei Stellen sie brauchen: das Theme
/// Hier statt in `FlaroTheme`, weil zwei Stellen sie brauchen: das Theme
/// und der Vorgabewert der Umgebung. Zwei Kopien derselben Schlüssel
/// driften auseinander, und zwar unbemerkt — es fällt erst auf, wenn eine
/// Farbe irgendwo nicht mitkommt.
enum StorageKey {
static let identifier = "halo.accent.identifier"
static let customHex = "halo.accent.customHex"
static let identifier = "flaro.accent.identifier"
static let customHex = "flaro.accent.customHex"
}
/// Die gespeicherte Auswahl, oder die Voreinstellung.
@@ -102,6 +102,6 @@ extension AccentOption {
/// Die gespeicherte Farbe, bereits kontrastkorrigiert.
public static func storedAccentColor(in defaults: UserDefaults = .standard) -> Color {
Contrast.legible(stored(in: defaults).color, on: HaloColor.canvas).swiftUIColor
Contrast.legible(stored(in: defaults).color, on: FlaroColor.canvas).swiftUIColor
}
}
@@ -2,7 +2,7 @@ import Foundation
/// Kontrastrechnung nach WCAG 2.1.
///
/// Gebraucht wird sie für die eigene Akzentfarbe: Halo lässt jede Farbe zu,
/// Gebraucht wird sie für die eigene Akzentfarbe: Flaro lässt jede Farbe zu,
/// aber ein dunkles Marineblau auf fast schwarzem Grund ist als Schrift oder
/// Symbol unlesbar. `legible(_:on:)` hebt solche Farben so weit an, bis sie
/// den geforderten Kontrast erreichen — und lässt alles in Ruhe, was ihn
@@ -2,10 +2,10 @@ import SwiftUI
/// Die Flächen- und Schriftfarben der App.
///
/// Halo kennt nur den dunklen Modus — es gibt deshalb bewusst keine
/// Flaro kennt nur den dunklen Modus — es gibt deshalb bewusst keine
/// Hell-/Dunkel-Paare, sondern je einen festen Wert. Die einzige Farbe, die
/// sich zur Laufzeit ändert, ist der Akzent; er kommt aus `HaloTheme`.
public enum HaloColor {
/// sich zur Laufzeit ändert, ist der Akzent; er kommt aus `FlaroTheme`.
public enum FlaroColor {
// Flächen, von hinten nach vorn gestaffelt.
/// Der Seitenhintergrund.
public static let canvas = RGB(hex: 0x0B0B0D)
@@ -28,18 +28,18 @@ public enum HaloColor {
}
extension Color {
public static let haloCanvas = HaloColor.canvas.swiftUIColor
public static let haloSurface = HaloColor.surface.swiftUIColor
public static let haloElevated = HaloColor.elevated.swiftUIColor
public static let haloPlaceholder = HaloColor.placeholder.swiftUIColor
public static let haloTextPrimary = HaloColor.textPrimary.swiftUIColor
public static let haloTextSecondary = HaloColor.textSecondary.swiftUIColor
public static let haloTextMuted = HaloColor.textMuted.swiftUIColor
public static let haloDestructive = HaloColor.destructive.swiftUIColor
public static let flaroCanvas = FlaroColor.canvas.swiftUIColor
public static let flaroSurface = FlaroColor.surface.swiftUIColor
public static let flaroElevated = FlaroColor.elevated.swiftUIColor
public static let flaroPlaceholder = FlaroColor.placeholder.swiftUIColor
public static let flaroTextPrimary = FlaroColor.textPrimary.swiftUIColor
public static let flaroTextSecondary = FlaroColor.textSecondary.swiftUIColor
public static let flaroTextMuted = FlaroColor.textMuted.swiftUIColor
public static let flaroDestructive = FlaroColor.destructive.swiftUIColor
/// Trennlinien und Ränder. Als Weiß mit Deckkraft statt als fester Wert,
/// damit sie auf jeder Flächenstufe gleich zurückhaltend wirken.
public static let haloSeparator = Color.white.opacity(0.10)
public static let flaroSeparator = Color.white.opacity(0.10)
/// Die Abdunklung hinter der ausgefahrenen Seitenleiste.
public static let haloScrim = Color.black.opacity(0.55)
public static let flaroScrim = Color.black.opacity(0.55)
}
@@ -7,7 +7,7 @@ import SwiftUI
/// Startseite zeigt dutzende Bilder gleichzeitig, und `AsyncImage` lädt sie bei
/// jedem Erscheinen neu. Auf dem Mac wurde daraus ein sichtbarer Fehler: der
/// Aufmacher und die Hälfte der Plakate blieben grau.
public struct HaloArtwork: View {
public struct FlaroArtwork: View {
/// Die Adressen, der Reihe nach probiert.
///
/// **Warum mehrere.** Eine Folge hat selten ein eigenes Querformatbild,
@@ -90,9 +90,9 @@ public struct HaloArtwork: View {
@ViewBuilder
private var placeholder: some View {
if fillsFrame {
Rectangle().fill(Color.haloPlaceholder)
Rectangle().fill(Color.flaroPlaceholder)
} else {
Rectangle().fill(Color.haloPlaceholder).aspectRatio(aspectRatio, contentMode: .fit)
Rectangle().fill(Color.flaroPlaceholder).aspectRatio(aspectRatio, contentMode: .fit)
}
}
@@ -124,7 +124,7 @@ public struct HaloArtwork: View {
// mehr Lärm als Hilfe. Das Symbol sagt genug.
Image(systemName: fallbackSymbol)
.font(.title2)
.foregroundStyle(Color.haloTextMuted)
.foregroundStyle(Color.flaroTextMuted)
} else {
ShimmerPlaceholder()
}
@@ -81,7 +81,7 @@ public struct HeroBanner<Actions: View>: View {
// 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 = HaloAspectRatio.hero,
aspectRatio: CGFloat = FlaroAspectRatio.hero,
fillsAvailableSpace: Bool = false,
@ViewBuilder actions: () -> Actions
) {
@@ -114,16 +114,16 @@ public struct HeroBanner<Actions: View>: View {
/// Das Bild — und, solange gewechselt wird, das vorige darunter.
///
/// Ohne Wechsel ist es genau ein `HaloArtwork` wie zuvor: die zweite
/// Ohne Wechsel ist es genau ein `FlaroArtwork` wie zuvor: die zweite
/// Ebene entsteht nur, wenn jemand eine vorige Adresse mitgibt.
@ViewBuilder
private var bild: some View {
if outgoingArtworkURLs.isEmpty {
HaloArtwork(urls: artworkURLs, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace)
FlaroArtwork(urls: artworkURLs, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace)
} else {
ZStack {
HaloArtwork(urls: outgoingArtworkURLs, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace)
HaloArtwork(urls: artworkURLs, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace)
FlaroArtwork(urls: outgoingArtworkURLs, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace)
FlaroArtwork(urls: artworkURLs, aspectRatio: aspectRatio, fillsFrame: fillsAvailableSpace)
.opacity(crossfade)
.offset(x: incomingOffset)
}
@@ -166,8 +166,8 @@ public struct HeroBanner<Actions: View>: View {
LinearGradient(
stops: [
.init(color: .clear, location: 0),
.init(color: Color.haloCanvas.opacity(0.75), location: 0.45),
.init(color: Color.haloCanvas, location: 1),
.init(color: Color.flaroCanvas.opacity(0.75), location: 0.45),
.init(color: Color.flaroCanvas, location: 1),
],
startPoint: .top,
endPoint: .bottom
@@ -250,7 +250,7 @@ public struct HeroBanner<Actions: View>: View {
logo: PlatformImage?,
scale: CGFloat
) -> some View {
VStack(alignment: .leading, spacing: HaloSpacing.snug * scale) {
VStack(alignment: .leading, spacing: FlaroSpacing.snug * scale) {
if let logo {
Image(platform: logo)
.resizable()
@@ -266,24 +266,24 @@ public struct HeroBanner<Actions: View>: View {
// eine Systemschrift.
Text(title)
.font(.system(size: 34 * scale, weight: .bold))
.foregroundStyle(Color.haloTextPrimary)
.foregroundStyle(Color.flaroTextPrimary)
.lineLimit(2)
}
if let metadataContent {
metadataContent
.font(.system(size: 12 * scale))
.foregroundStyle(Color.haloTextSecondary)
.foregroundStyle(Color.flaroTextSecondary)
} else if let metadata {
Text(metadata)
.font(.system(size: 12 * scale))
.foregroundStyle(Color.haloTextSecondary)
.foregroundStyle(Color.flaroTextSecondary)
}
if let summary {
Text(summary)
.font(.system(size: 12 * scale))
.foregroundStyle(Color.haloTextSecondary)
.foregroundStyle(Color.flaroTextSecondary)
.lineLimit(2)
// Ein Satz über die ganze Breite eines großen Fensters ist
// unlesbar; das Auge verliert die Zeile. Achtzig Zeichen
@@ -291,12 +291,12 @@ public struct HeroBanner<Actions: View>: View {
.frame(maxWidth: 640 * scale, alignment: .leading)
}
HStack(spacing: HaloSpacing.regular) {
HStack(spacing: FlaroSpacing.regular) {
actions
}
.padding(.top, HaloSpacing.tight)
.padding(.top, FlaroSpacing.tight)
}
.padding(.horizontal, HaloSpacing.screenMargin * scale)
.padding(.bottom, HaloSpacing.loose * scale)
.padding(.horizontal, FlaroSpacing.screenMargin * scale)
.padding(.bottom, FlaroSpacing.loose * scale)
}
}
@@ -20,24 +20,24 @@ public struct LandscapeCard: View {
}
public var body: some View {
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
HaloArtwork(url: artworkURL, aspectRatio: Self.aspectRatio, fallbackSymbol: "play.rectangle")
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
FlaroArtwork(url: artworkURL, aspectRatio: Self.aspectRatio, fallbackSymbol: "play.rectangle")
.overlay(alignment: .bottom) {
if let progress {
WatchProgressBar(fraction: progress)
}
}
.clipShape(.rect(cornerRadius: HaloRadius.card))
.clipShape(.rect(cornerRadius: FlaroRadius.card))
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.haloCardTitle)
.foregroundStyle(Color.haloTextPrimary)
.font(.flaroCardTitle)
.foregroundStyle(Color.flaroTextPrimary)
.lineLimit(1)
if let subtitle {
Text(subtitle)
.font(.haloCaption)
.foregroundStyle(Color.haloTextMuted)
.font(.flaroCaption)
.foregroundStyle(Color.flaroTextMuted)
.lineLimit(1)
}
}
@@ -30,18 +30,18 @@ public struct PlaceholderCard: View {
}
public var body: some View {
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
RoundedRectangle(cornerRadius: HaloRadius.card)
.fill(Color.haloElevated)
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
RoundedRectangle(cornerRadius: FlaroRadius.card)
.fill(Color.flaroElevated)
.aspectRatio(PosterCard.aspectRatio, contentMode: .fit)
// Zwei Balken an der Stelle von Titel und Jahr. Ohne sie springt
// die Zeile, sobald der echte Titel kommt.
RoundedRectangle(cornerRadius: 3)
.fill(Color.haloElevated)
.fill(Color.flaroElevated)
.frame(height: 10)
RoundedRectangle(cornerRadius: 3)
.fill(Color.haloElevated)
.fill(Color.flaroElevated)
.frame(width: 32, height: 8)
}
.opacity(isBright ? 0.85 : 0.45)
@@ -19,25 +19,25 @@ public struct PosterCard: View {
}
public var body: some View {
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
HaloArtwork(url: artworkURL, aspectRatio: Self.aspectRatio)
.clipShape(.rect(cornerRadius: HaloRadius.card))
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
FlaroArtwork(url: artworkURL, aspectRatio: Self.aspectRatio)
.clipShape(.rect(cornerRadius: FlaroRadius.card))
.overlay(alignment: .bottom) {
if let progress {
WatchProgressBar(fraction: progress)
}
}
.clipShape(.rect(cornerRadius: HaloRadius.card))
.clipShape(.rect(cornerRadius: FlaroRadius.card))
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.haloCardTitle)
.foregroundStyle(Color.haloTextPrimary)
.font(.flaroCardTitle)
.foregroundStyle(Color.flaroTextPrimary)
.lineLimit(1)
if let subtitle {
Text(subtitle)
.font(.haloCaption)
.foregroundStyle(Color.haloTextMuted)
.font(.flaroCaption)
.foregroundStyle(Color.flaroTextMuted)
.lineLimit(1)
}
}
@@ -15,7 +15,7 @@ public struct ShimmerPlaceholder: View {
public var body: some View {
Rectangle()
.fill(Color.haloPlaceholder)
.fill(Color.flaroPlaceholder)
.overlay {
if !reduceMotion {
GeometryReader { proxy in
@@ -10,7 +10,7 @@ import SwiftUI
/// `scaleEffect` und ein Schatten — sieht auf den ersten Blick gleich aus und
/// auf den zweiten falsch: die Parallaxe ist der Teil, den man wiedererkennt.
///
/// Liegt in HaloDesign und nicht im Fernseh-Ziel, aus demselben Grund wie
/// Liegt in FlaroDesign und nicht im Fernseh-Ziel, aus demselben Grund wie
/// `PosterCard`: das Paket kennt weder `MediaItem` noch die Wiedergabe, und
/// das soll so bleiben. Herein kommen Zeichenketten und Adressen.
public struct TVPosterCard: View {
@@ -100,7 +100,7 @@ public struct TVLandscapeCard: View {
public var body: some View {
TVCard(
aspectRatio: HaloAspectRatio.landscape,
aspectRatio: FlaroAspectRatio.landscape,
title: title,
subtitle: subtitle,
artworkURL: artworkURL,
@@ -124,13 +124,13 @@ private struct TVCard: View {
let action: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
Button(action: action) {
HaloArtwork(url: artworkURL, aspectRatio: aspectRatio)
FlaroArtwork(url: artworkURL, aspectRatio: aspectRatio)
.overlay(alignment: .bottom) {
if let progress { WatchProgressBar(fraction: progress) }
}
.clipShape(.rect(cornerRadius: HaloRadius.card))
.clipShape(.rect(cornerRadius: FlaroRadius.card))
}
.buttonStyle(.card)
.focused($isFocused)
@@ -143,13 +143,13 @@ private struct TVCard: View {
// Metern eine Textwand.
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.haloCardTitle)
.foregroundStyle(Color.haloTextPrimary)
.font(.flaroCardTitle)
.foregroundStyle(Color.flaroTextPrimary)
.lineLimit(1)
if let subtitle {
Text(subtitle)
.font(.haloCaption)
.foregroundStyle(Color.haloTextMuted)
.font(.flaroCaption)
.foregroundStyle(Color.flaroTextMuted)
.lineLimit(1)
}
}
@@ -5,7 +5,7 @@ public struct WatchProgressBar: View {
/// 0…1. Werte außerhalb werden abgeschnitten, damit ein fehlerhafter
/// Serverwert den Balken nicht über die Kachel hinausschieben kann.
private let fraction: Double
@Environment(\.haloAccent) private var accent
@Environment(\.flaroAccent) private var accent
public init(fraction: Double) {
self.fraction = fraction.clamped()
@@ -24,17 +24,17 @@ import SwiftUI
extension Font {
#if os(macOS)
/// Der Titel im Aufmacher-Bild.
public static let haloHero = Font.system(size: 34, weight: .bold)
public static let flaroHero = Font.system(size: 34, weight: .bold)
/// Titel auf Detailseiten.
public static let haloTitle = Font.system(size: 22, weight: .semibold)
public static let flaroTitle = Font.system(size: 22, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let haloRowHeader = Font.system(size: 17, weight: .semibold)
public static let flaroRowHeader = Font.system(size: 17, weight: .semibold)
/// Titel unter einem Poster.
public static let haloCardTitle = Font.system(size: 15, weight: .medium)
public static let flaroCardTitle = Font.system(size: 15, weight: .medium)
/// Fließtext, Beschreibungen.
public static let haloBody = Font.system(size: 17)
public static let flaroBody = Font.system(size: 17)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let haloCaption = Font.system(size: 12)
public static let flaroCaption = Font.system(size: 12)
#elseif os(tvOS)
/// Der Titel im Aufmacher-Bild.
///
@@ -43,34 +43,34 @@ extension Font {
/// verfügbar markiert. Was SwiftUI daraus macht, steht nirgends, und eine
/// Schrift, deren Grösse niemand nennen kann, gehört nicht in die grösste
/// Zeile der App.
public static let haloHero = Font.system(.title, design: .default, weight: .bold)
public static let flaroHero = Font.system(.title, design: .default, weight: .bold)
/// Titel auf Detailseiten.
public static let haloTitle = Font.system(.title2, design: .default, weight: .semibold)
public static let flaroTitle = Font.system(.title2, design: .default, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let haloRowHeader = Font.system(.headline, design: .default, weight: .semibold)
public static let flaroRowHeader = Font.system(.headline, design: .default, weight: .semibold)
/// Titel unter einem Poster — auf tvOS nur bei Fokus sichtbar.
///
/// Eine Stufe tiefer als auf iOS, und das ist gewollt: auf tvOS ist
/// `caption` schon gross, und ein Raster aus sechs Spalten mit
/// `subheadline`-Titeln hätte mehr Schrift als Bild.
public static let haloCardTitle = Font.system(.caption, design: .default, weight: .medium)
public static let flaroCardTitle = Font.system(.caption, design: .default, weight: .medium)
/// Fließtext, Beschreibungen.
public static let haloBody = Font.system(.body)
public static let flaroBody = Font.system(.body)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let haloCaption = Font.system(.caption2)
public static let flaroCaption = Font.system(.caption2)
#else
/// Der Titel im Aufmacher-Bild.
public static let haloHero = Font.system(.largeTitle, design: .default, weight: .bold)
public static let flaroHero = Font.system(.largeTitle, design: .default, weight: .bold)
/// Titel auf Detailseiten.
public static let haloTitle = Font.system(.title2, design: .default, weight: .semibold)
public static let flaroTitle = Font.system(.title2, design: .default, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let haloRowHeader = Font.system(.headline, design: .default, weight: .semibold)
public static let flaroRowHeader = Font.system(.headline, design: .default, weight: .semibold)
/// Titel unter einem Poster.
public static let haloCardTitle = Font.system(.subheadline, design: .default, weight: .medium)
public static let flaroCardTitle = Font.system(.subheadline, design: .default, weight: .medium)
/// Fließtext, Beschreibungen.
public static let haloBody = Font.system(.body)
public static let flaroBody = Font.system(.body)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let haloCaption = Font.system(.caption)
public static let flaroCaption = Font.system(.caption)
#endif
}
@@ -78,7 +78,7 @@ extension Font {
///
/// Eine Skala statt frei gewählter Zahlen: dadurch stimmen die Ränder zwischen
/// Bildschirmen überein, ohne dass man sie einzeln nachmisst.
public enum HaloSpacing {
public enum FlaroSpacing {
#if os(tvOS)
// Durchweg doppelt: ein Fernseher ist 1920 Punkte breit und wird aus drei
// Metern gelesen. Acht Punkte Abstand sind dort keine Trennung mehr.
@@ -124,15 +124,15 @@ public enum HaloSpacing {
/// „neunzig" stimmt nicht; eine zweite Quelle nannte achtzig und hatte recht.
///
/// **Und man bekommt sie geschenkt.** SwiftUI legt sie von selbst als
/// Sicherheitsrand an — deshalb steht in `HaloSpacing.screenMargin` nur die
/// Sicherheitsrand an — deshalb steht in `FlaroSpacing.screenMargin` nur die
/// Rinne innerhalb davon. Die Zahl hier ist für die Gegenrichtung: ein
/// bildfüllender Aufmacher, der den Sicherheitsrand absichtlich übergeht und
/// dessen Schrift trotzdem drinbleiben muss.
public enum HaloSafeArea {
public enum FlaroSafeArea {
public static let tv = EdgeInsets(top: 60, leading: 80, bottom: 60, trailing: 80)
}
public enum HaloRadius {
public enum FlaroRadius {
/// Die Ecke einer Kachel.
///
/// Auf dem Fernseher grösser: eine Kachel, die sich beim Fokus anhebt,
@@ -155,7 +155,7 @@ public enum HaloRadius {
/// beschneidet: ein 16:9-Hintergrundbild in einem 3:4-Rahmen verliert links
/// und rechts je ein Viertel, und man sieht dem Ergebnis nicht an, dass etwas
/// fehlt.
public enum HaloAspectRatio {
public enum FlaroAspectRatio {
/// Kinoplakate im Hochformat.
public static let poster: CGFloat = 2.0 / 3.0
/// Szenenbilder und Hintergrundbilder.
@@ -7,7 +7,7 @@ import SwiftUI
/// darauf zu achten.
@MainActor
@Observable
public final class HaloTheme {
public final class FlaroTheme {
private let defaults: UserDefaults
/// Was in den Einstellungen ausgewählt ist — unkorrigiert, damit die
@@ -24,7 +24,7 @@ public final class HaloTheme {
/// Bei den kuratierten Werten ändert die Korrektur nichts — sie erfüllen
/// den Grenzwert bereits. Sie greift nur bei eigenen Farben.
public var accent: RGB {
Contrast.legible(selection.color, on: HaloColor.canvas)
Contrast.legible(selection.color, on: FlaroColor.canvas)
}
public var accentColor: Color { accent.swiftUIColor }
@@ -24,7 +24,7 @@ public struct MiniPlayerPlacement: Sendable, Equatable {
/// Fehler statt wie eine Entscheidung.
public let inset: CGFloat
public init(bounds: CGSize, tile: CGSize, inset: CGFloat = HaloSpacing.regular) {
public init(bounds: CGSize, tile: CGSize, inset: CGFloat = FlaroSpacing.regular) {
self.bounds = bounds
self.tile = tile
self.inset = inset
@@ -13,7 +13,7 @@ import AppKit
/// nirgends sonst. Sonst wandert er in zwanzig Dateien und wird beim
/// nächsten Bildschirm vergessen.
/// Auf was für einem Gerät läuft das hier gerade?
public enum HaloPlatform {
public enum FlaroPlatform {
/// Läuft die App auf einem Mac?
///
/// **Zwei Wege führen dorthin, und für den Übersetzer sehen sie
@@ -43,7 +43,7 @@ public enum HaloPlatform {
#elseif os(macOS)
true
#else
#error("HaloPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
#error("FlaroPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
#endif
}()
@@ -60,7 +60,7 @@ public enum HaloPlatform {
#elseif os(iOS) || os(macOS)
false
#else
#error("HaloPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
#error("FlaroPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
#endif
}()
}
@@ -68,7 +68,7 @@ public enum HaloPlatform {
extension View {
/// Eine schmale Titelzeile — auf dem Mac und auf dem Fernseher gibt es
/// sie nicht. Beide fallen in denselben `#else`-Zweig, und beide zu Recht.
public func haloInlineTitle() -> some View {
public func flaroInlineTitle() -> some View {
#if os(iOS)
navigationBarTitleDisplayMode(.inline)
#else
@@ -80,7 +80,7 @@ extension View {
///
/// Auf dem Mac ist ein Sheet ein Fenster: dort zählt keine Rastung,
/// sondern eine vernünftige Mindestgröße.
public func haloMediumSheet() -> some View {
public func flaroMediumSheet() -> some View {
#if os(tvOS)
// Auf dem Fernseher ist ein Sheet immer bildfüllend. `presentationDetents`
// übersetzt dort zwar — es gibt sie seit tvOS 16 —, bewirkt aber
@@ -92,18 +92,18 @@ extension View {
// macOS griff nie: auf dem Mac läuft der iOS-Bau, und damit bekam ein
// grosses Fenster eine Sheet-Kante in iPhone-Höhe — ein schmaler
// Streifen, der unten im Nichts klebt.
presentationDetents(HaloPlatform.isMac ? [.large] : [.medium])
presentationDetents(FlaroPlatform.isMac ? [.large] : [.medium])
#endif
}
/// Dasselbe, aber auf Wunsch bis zur vollen Höhe aufziehbar.
public func haloResizableSheet() -> some View {
public func flaroResizableSheet() -> some View {
#if os(tvOS)
self
#else
// Siehe oben. Auf dem Mac gibt es kein „halb aufgezogen": ein Fenster
// hat die Höhe, die es hat, und eine Liste soll sie nutzen.
presentationDetents(HaloPlatform.isMac ? [.large] : [.medium, .large])
presentationDetents(FlaroPlatform.isMac ? [.large] : [.medium, .large])
#endif
}
@@ -117,7 +117,7 @@ extension View {
/// Stil ist derselbe, den iOS ohnehin nimmt — und der Grund, warum die
/// Einstellungen auf dem Mac wie ein Systemdialog aus einem anderen
/// Jahrzehnt aussahen.
public func haloForm() -> some View {
public func flaroForm() -> some View {
#if os(macOS)
formStyle(.grouped)
#else
@@ -130,7 +130,7 @@ extension View {
///
/// Auf tvOS ebenfalls der `#else`-Zweig: die Bildschirmtastatur dort
/// kennt weder `keyboardType` noch Autokorrektur in dieser Form.
public func haloPlainEntry(isAddress: Bool = false) -> some View {
public func flaroPlainEntry(isAddress: Bool = false) -> some View {
#if os(iOS)
self
.keyboardType(isAddress ? .URL : .default)
@@ -149,7 +149,7 @@ extension View {
/// Leiste verschleierte die Hälfte davon. Auf dem Mac und auf dem
/// Fernseher gibt es die Leiste in dieser Form nicht — beide fallen in
/// den `#else`-Zweig.
public func haloClearToolbarBackground() -> some View {
public func flaroClearToolbarBackground() -> some View {
#if os(iOS)
self
.toolbarBackground(.hidden, for: .navigationBar)
@@ -164,7 +164,7 @@ extension View {
///
/// UIKit und AppKit heißen verschieden und meinen dasselbe; die Diagnose soll
/// deswegen nicht mit `#if` beginnen.
public enum HaloClipboard {
public enum FlaroClipboard {
public static func copy(_ text: String) {
#if os(tvOS)
// Es gibt keine. `UIPasteboard` ist auf tvOS als nicht verfügbar
@@ -186,7 +186,7 @@ public enum HaloClipboard {
extension ToolbarItemPlacement {
/// Rechts in der Titelzeile.
public static var haloTrailing: ToolbarItemPlacement {
public static var flaroTrailing: ToolbarItemPlacement {
#if os(iOS) || os(tvOS)
.topBarTrailing
#else
@@ -195,7 +195,7 @@ extension ToolbarItemPlacement {
}
/// Links in der Titelzeile.
public static var haloLeading: ToolbarItemPlacement {
public static var flaroLeading: ToolbarItemPlacement {
#if os(iOS) || os(tvOS)
.topBarLeading
#else
@@ -78,7 +78,7 @@ public enum DownloadRefusal: Error, Equatable, Sendable {
public final class DownloadCoordinator {
/// Die Kennung der Hintergrundsitzung. Muss über App-Starts hinweg gleich
/// bleiben, sonst findet iOS die laufenden Übertragungen nicht wieder.
public static let sessionIdentifier = "com.scarriffleservices.halo.downloads"
public static let sessionIdentifier = "com.scarriffleservices.flaro.downloads"
public private(set) var items: [DownloadItem] = []
/// Wie viel die Downloads insgesamt belegen.
@@ -134,11 +134,11 @@ public final class DownloadCoordinator {
}
private enum Key {
static let wifiOnly = "halo.downloads.wifiOnly"
static let storageLimit = "halo.downloads.storageLimit"
static let deleteAfter = "halo.downloads.deleteWatchedAfter"
static let keepUnwatched = "halo.downloads.keepUnwatched"
static let maximumConcurrent = "halo.downloads.maximumConcurrent"
static let wifiOnly = "flaro.downloads.wifiOnly"
static let storageLimit = "flaro.downloads.storageLimit"
static let deleteAfter = "flaro.downloads.deleteWatchedAfter"
static let keepUnwatched = "flaro.downloads.keepUnwatched"
static let maximumConcurrent = "flaro.downloads.maximumConcurrent"
}
/// Der Container, nicht nur der Kontext.
@@ -11,7 +11,7 @@ import ActivityKit
/// Was im Sperrbildschirm und in der Dynamic Island steht.
///
/// Liegt in HaloDownloads, weil App und Widget-Erweiterung denselben Typ
/// Liegt in FlaroDownloads, weil App und Widget-Erweiterung denselben Typ
/// brauchen — zwei getrennte Kopien wären die klassische Quelle für eine Live
/// Activity, die sich nicht mehr aktualisieren lässt: ActivityKit gleicht die
/// Typen über ihren vollen Namen ab.
@@ -29,7 +29,7 @@ public final class NetworkConditions {
isConstrained = path.isConstrained
}
}
monitor.start(queue: DispatchQueue(label: "halo.network"))
monitor.start(queue: DispatchQueue(label: "flaro.network"))
}
deinit {
@@ -70,7 +70,7 @@ public actor LocalStreamServer {
}
private let cache: StreamCache
private let logger = Logger(subsystem: "app.halo", category: "LocalStreamServer")
private let logger = Logger(subsystem: "app.flaro", category: "LocalStreamServer")
private var listener: NWListener?
private var port: UInt16?
private var playlist: String?
@@ -44,7 +44,7 @@ public actor StreamCache {
private let policy: Policy
private let session: URLSession
private let directory: URL
private let logger = Logger(subsystem: "app.halo", category: "StreamCache")
private let logger = Logger(subsystem: "app.flaro", category: "StreamCache")
/// Woher die Schnipsel wirklich kommen.
private var upstream: URL?
@@ -83,7 +83,7 @@ public actor StreamCache {
self.session = URLSession(configuration: configuration)
self.directory = FileManager.default.temporaryDirectory
.appending(path: "halo-stream-\(UUID().uuidString)", directoryHint: .isDirectory)
.appending(path: "flaro-stream-\(UUID().uuidString)", directoryHint: .isDirectory)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
}
@@ -172,9 +172,9 @@ func jellyfinLeiter(host: String) async {
}
let identity = JellyfinIdentity(
client: "Halo-Probe",
client: "Flaro-Probe",
device: "Mac",
deviceID: "halo-probe-" + UUID().uuidString,
deviceID: "flaro-probe-" + UUID().uuidString,
version: "1.0"
)
let client = JellyfinClient(baseURL: basis, identity: identity)
@@ -471,9 +471,9 @@ func zeigeSpuren(host: String, suche: String) async {
guard let passwort = String(validatingCString: getpass("Passwort: ")), !passwort.isEmpty else { return }
let identity = JellyfinIdentity(
client: "Halo-Probe",
client: "Flaro-Probe",
device: "Mac",
deviceID: "halo-probe-" + UUID().uuidString,
deviceID: "flaro-probe-" + UUID().uuidString,
version: "1.0"
)
let client = JellyfinClient(baseURL: basis, identity: identity)
@@ -46,7 +46,7 @@ if let index = CommandLine.arguments.firstIndex(of: "--jellyfin"),
// Kein Modus erkannt. Ohne diesen Hinweis endete das Werkzeug wortlos — und
// ein Tippfehler im Argument sähe aus wie ein Server, der nichts liefert.
print("""
HaloProbe — fragt Server über reines HTTP, ohne Oberfläche und ohne Gerät.
FlaroProbe — fragt Server über reines HTTP, ohne Oberfläche und ohne Gerät.
--playlist <url> Eine HLS-Leiter prüfen: Varianten, Tonspuren,
erster Schnipsel
@@ -54,7 +54,7 @@ public struct QuickConnectRequest: Sendable, Equatable, Decodable {
///
/// **Quick Connect** ist der erste: die App zeigt einen sechsstelligen Code,
/// den man in einem bereits angemeldeten Client bestätigt. Das Passwort
/// erreicht Halo dabei nie — dieselbe Zusage, die bei Plex gilt, wo die
/// erreicht Flaro dabei nie — dieselbe Zusage, die bei Plex gilt, wo die
/// Anmeldemaske von Plex selbst kommt.
///
/// **Benutzername und Passwort** sind der Rückfall, falls der Server Quick
@@ -453,7 +453,7 @@ public func isInLocalNetwork() async -> Bool {
private static func transcodeSession(itemID: String, deviceID: String) -> [URLQueryItem] {
[
URLQueryItem(name: "deviceId", value: deviceID),
URLQueryItem(name: "playSessionId", value: "halo-\(UUID().uuidString)"),
URLQueryItem(name: "playSessionId", value: "flaro-\(UUID().uuidString)"),
]
}
@@ -4,7 +4,7 @@ import UIKit
#endif
extension JellyfinIdentity {
private static let storageKey = "halo.jellyfin.deviceID"
private static let storageKey = "flaro.jellyfin.deviceID"
/// Baut die Kennung für das laufende Gerät und merkt sie sich.
///
@@ -47,7 +47,7 @@ public actor LibraryCache {
}
private let root: URL
private let logger = Logger(subsystem: "app.halo", category: "LibraryCache")
private let logger = Logger(subsystem: "app.flaro", category: "LibraryCache")
/// Wie viele Titel höchstens gespeichert werden.
///
@@ -1,10 +1,10 @@
import Foundation
import Testing
@testable import HaloDesign
@testable import FlaroDesign
@Suite("Kontrastkorrektur")
struct ContrastTests {
private let canvas = HaloColor.canvas
private let canvas = FlaroColor.canvas
@Test("Schwarz zu Weiß ergibt das Maximum von 21:1")
func extremesProduceMaximumRatio() {
@@ -73,7 +73,7 @@ struct AccentPaletteTests {
// hier durch, gehört sie nicht in die Palette — nicht in die
// Korrektur.
for option in AccentOption.palette {
let ratio = Contrast.ratio(option.color, HaloColor.canvas)
let ratio = Contrast.ratio(option.color, FlaroColor.canvas)
#expect(
ratio >= Contrast.minimumForText,
"\(option.id) erreicht nur \(String(format: "%.2f", ratio)):1"
@@ -146,7 +146,7 @@ struct DefaultAccentTests {
@Test("Die Voreinstellung braucht keine Kontrastkorrektur")
func defaultNeedsNoCorrection() {
let corrected = Contrast.legible(AccentOption.default.color, on: HaloColor.canvas)
let corrected = Contrast.legible(AccentOption.default.color, on: FlaroColor.canvas)
#expect(corrected == AccentOption.default.color)
}
}
@@ -154,7 +154,7 @@ struct DefaultAccentTests {
@Suite("Gespeicherte Akzentfarbe")
struct AccentStorageTests {
private func freshDefaults() -> UserDefaults {
UserDefaults(suiteName: "halo.accent.tests.\(UUID().uuidString)")!
UserDefaults(suiteName: "flaro.accent.tests.\(UUID().uuidString)")!
}
@Test("Ohne gespeicherte Auswahl gilt die Voreinstellung")
@@ -200,7 +200,7 @@ struct AccentStorageTests {
let defaults = freshDefaults()
AccentOption.custom(RGB(hex: 0x8B5CF6)).store(in: defaults)
let theme = HaloTheme(defaults: defaults)
let theme = FlaroTheme(defaults: defaults)
#expect(theme.accentColor == AccentOption.storedAccentColor(in: defaults))
}
}
@@ -1,8 +1,8 @@
import Testing
@testable import HaloDesign
@testable import FlaroDesign
@Suite("Auf welchem Gerät wir laufen")
struct HaloPlatformTests {
struct FlaroPlatformTests {
/// **Der Test, den es vorher nicht gab — und der gefehlt hat.**
///
@@ -17,6 +17,6 @@ struct HaloPlatformTests {
/// hält er wenigstens fest, dass die Frage gestellt gehört.
@Test("Ein Gerät ist nicht zugleich Mac und Fernseher")
func neverBoth() {
#expect(!(HaloPlatform.isMac && HaloPlatform.isTV))
#expect(!(FlaroPlatform.isMac && FlaroPlatform.isTV))
}
}
@@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import HaloDesign
@testable import FlaroDesign
/// Die Geometrie des Programmrasters.
@Suite("Raster-Geometrie")
@@ -1,6 +1,6 @@
import Foundation
import Testing
@testable import HaloDesign
@testable import FlaroDesign
/// Wo die schwebende Videokachel landet.
@Suite("Kachel-Platzierung")
@@ -1,7 +1,7 @@
import Foundation
import SwiftData
import Testing
@testable import HaloDownloads
@testable import FlaroDownloads
/// Baut einen Koordinator, der nichts überträgt.
///
@@ -16,7 +16,7 @@ private func makeCoordinator(
for: DownloadItem.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let defaults = UserDefaults(suiteName: "halo.tests.\(UUID().uuidString)")!
let defaults = UserDefaults(suiteName: "flaro.tests.\(UUID().uuidString)")!
let coordinator = DownloadCoordinator(
container: container,
@@ -48,7 +48,7 @@ private func plan(
}
private func temporaryRoot() -> URL {
FileManager.default.temporaryDirectory.appending(path: "halo-tests-\(UUID().uuidString)")
FileManager.default.temporaryDirectory.appending(path: "flaro-tests-\(UUID().uuidString)")
}
@MainActor
@@ -1,5 +1,5 @@
import Testing
@testable import HaloDownloads
@testable import FlaroDownloads
@Suite("Downloadrate und Restzeit")
struct DownloadRateTests {
@@ -1,7 +1,7 @@
import Foundation
import MediaKit
import Testing
@testable import HaloDownloads
@testable import FlaroDownloads
/// Baut einen Eintrag im gewünschten Zustand, ohne Datenbank.
@MainActor
@@ -1,5 +1,5 @@
import Testing
@testable import HaloDownloads
@testable import FlaroDownloads
@Suite("Download-Zustandsautomat")
struct DownloadStateTests {
@@ -1,5 +1,5 @@
import Testing
@testable import HaloDownloads
@testable import FlaroDownloads
/// Warum gerade nichts übertragen wird.
@Suite("Übertragungssperre")
@@ -1,7 +1,7 @@
import Testing
import Foundation
import MediaKit
@testable import HaloPlayback
@testable import FlaroPlayback
@Suite("Sprachkürzel")
struct LanguageCodeTests {
@@ -1,6 +1,6 @@
import Testing
import Foundation
@testable import HaloPlayback
@testable import FlaroPlayback
@Suite("Wann der teure Weg erlaubt ist")
struct DowngradeRuleTests {
@@ -1,7 +1,7 @@
import Foundation
import Network
import Testing
@testable import HaloPlayback
@testable import FlaroPlayback
/// Was der lokale Server dem Player wirklich schickt.
///
@@ -1,6 +1,6 @@
import Testing
import Foundation
@testable import HaloPlayback
@testable import FlaroPlayback
@Suite("Abspielliste lesen und umschreiben")
struct MediaPlaylistTests {

Some files were not shown because too many files have changed in this diff Show More