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,71 @@
import Foundation
/// Der schmale Spalt, durch den PlexKit ans Netz kommt.
///
/// Alles darüber kennt nur dieses Protokoll. Dadurch laufen die Tests gegen
/// aufgezeichnete Antworten statt gegen einen echten Server — und zwar ohne
/// `URLProtocol`-Verrenkungen, die bei paralleler Testausführung erfahrungs-
/// gemäß ins Rutschen kommen.
public protocol PlexTransport: Sendable {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
extension URLSession: PlexTransport {}
/// Eine Anfrage an Plex, unabhängig davon, an welchen Host sie geht.
///
/// Plex nimmt den Token wahlweise als Kopfzeile oder als Abfrageparameter.
/// Hier steht er in der Kopfzeile — sonst landet er in jedem Protokoll und in
/// jedem Absturzbericht, der URLs mitschreibt.
public struct PlexRequest: Sendable, Equatable {
public enum Method: String, Sendable { case get = "GET", post = "POST", put = "PUT", delete = "DELETE" }
public var method: Method
public var path: String
public var query: [URLQueryItem]
public var additionalHeaders: [String: String]
public init(
method: Method = .get,
path: String,
query: [URLQueryItem] = [],
additionalHeaders: [String: String] = [:]
) {
self.method = method
self.path = path
self.query = query
self.additionalHeaders = additionalHeaders
}
/// Setzt die Anfrage gegen einen Host zusammen.
public func urlRequest(
baseURL: URL,
identity: PlexClientIdentity,
token: String?
) throws(PlexError) -> URLRequest {
guard var components = URLComponents(
url: baseURL.appending(path: path),
resolvingAgainstBaseURL: false
) else {
throw .transport(description: "Ungültige Basisadresse: \(baseURL)")
}
components.queryItems = query.isEmpty ? nil : query
guard let url = components.url else {
throw .transport(description: "Anfrage ließ sich nicht zusammensetzen: \(path)")
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
for (field, value) in identity.headers {
request.setValue(value, forHTTPHeaderField: field)
}
for (field, value) in additionalHeaders {
request.setValue(value, forHTTPHeaderField: field)
}
if let token {
request.setValue(token, forHTTPHeaderField: "X-Plex-Token")
}
return request
}
}