Phase 0: Projektgerüst, Design-Tokens und Navigation

Legt das Fundament für den Plex-Client an:

- XcodeGen-Projekt nach dem Muster von Onyx (com.scarriffleservices,
  strict concurrency, Projektdatei nicht eingecheckt)
- FlixyKit mit vier Modulen: PlexKit, FlixyDesign, FlixyPlayback,
  FlixyDownloads. macOS steht als Plattform mit drin, damit die
  Logik-Tests ohne Simulator laufen.
- FlixyDesign: dunkle Farb-Tokens, Schriftskala, Poster- und
  Querformat-Kacheln, Aufmacher-Bild. Die Akzentfarbe fließt über die
  Umgebung und wird per WCAG-Rechnung lesbar gehalten.
- PlexKit: Kopfzeilen-Identität, Transport-Protokoll, Fehlerarten und
  die Medienmodelle samt nachsichtigem Decoder.
- FlixyPlayback: VideoPlayerEngine-Protokoll und PlaybackDecider — die
  eine Funktion, an der Phase 2 das Transcoding andockt.
- Ausfahrbare Seitenleiste auf dem iPhone, Split-View auf dem iPad.
- 45 Tests, alle grün.

VLCKit 4.0.0-a24 ist vorab geprüft: baut über SPM unter Swift 6 mit
strict concurrency. Die iOS-Slice ist mit 239 MB allerdings gewichtig,
deshalb bekommt sie in Phase 1 ein eigenes Target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-11 16:04:40 +02:00
co-authored by Claude Opus 5
commit 1ed64bf166
54 changed files with 3584 additions and 0 deletions
@@ -0,0 +1,65 @@
import SwiftUI
/// Hält die gewählte Akzentfarbe und sorgt dafür, dass sie lesbar bleibt.
///
/// Nach außen gibt es nur `accent` — schon kontrastkorrigiert. Views sollen
/// sich nie fragen müssen, ob die eingestellte Farbe dunkel genug ist, um
/// darauf zu achten.
@MainActor
@Observable
public final class FlixyTheme {
private let defaults: UserDefaults
private enum Key {
static let selectedIdentifier = "flixy.accent.identifier"
static let customHex = "flixy.accent.customHex"
}
/// Was in den Einstellungen ausgewählt ist — unkorrigiert, damit die
/// Farbkachel genau das zeigt, was angetippt wurde.
public private(set) var selection: AccentOption
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.selection = Self.restore(from: defaults)
}
/// Die Farbe, die tatsächlich gezeichnet wird.
///
/// 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: FlixyColor.canvas)
}
public var accentColor: Color { accent.swiftUIColor }
/// Ob die Korrektur bei der aktuellen Auswahl eingegriffen hat. Der
/// Einstellungsdialog kann darauf hinweisen, statt den Nutzer rätseln zu
/// lassen, warum seine Farbe heller aussieht als gewählt.
public var accentWasAdjusted: Bool { accent != selection.color }
public func select(_ option: AccentOption) {
selection = option
defaults.set(option.id, forKey: Key.selectedIdentifier)
if option.id == AccentOption.customIdentifier {
defaults.set(option.color.hexString, forKey: Key.customHex)
}
}
private static func restore(from defaults: UserDefaults) -> AccentOption {
guard let identifier = defaults.string(forKey: Key.selectedIdentifier) else {
return .default
}
if identifier == AccentOption.customIdentifier {
// Fällt die gespeicherte Zeichenkette aus (etwa weil jemand die
// Voreinstellungen von Hand bearbeitet hat), ist der Standard
// besser als eine Farbe, die niemand gewählt hat.
guard let hex = defaults.string(forKey: Key.customHex),
let color = RGB(string: hex)
else { return .default }
return .custom(color)
}
return AccentOption.palette.first { $0.id == identifier } ?? .default
}
}