Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit, damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind. 712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25 Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit `HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`, `JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben. `docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben hiesse, die Vergangenheit zu glätten. **Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis: `com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container; Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen. **Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter `…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet. Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht statt umbenannt — sie steht ohnehin nicht im Repo. Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist ein Handgriff ausserhalb des Projekts. 368 Tests grün. iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
3.2 KiB
Swift
79 lines
3.2 KiB
Swift
import Foundation
|
|
import CoreGraphics
|
|
|
|
/// Die Geometrie eines Programmrasters: Zeit in Punkte.
|
|
///
|
|
/// Eigener Typ statt Rechnungen in der Ansicht, weil hier alles schiefgehen
|
|
/// kann, was man nicht sieht: eine Sendung, die vor dem Fenster begann, säße
|
|
/// bei negativem Versatz außerhalb; eine Kurzmeldung wäre zwei Punkte breit
|
|
/// und unlesbar; ein leeres Raster hätte die Breite null. Geprüft werden
|
|
/// diese Fälle nur, wenn sie außerhalb einer `View` stehen.
|
|
public struct GuideLayout: Sendable, Equatable {
|
|
/// Der Anfang des Fensters — die angebrochene halbe Stunde vor „jetzt".
|
|
///
|
|
/// Nicht „jetzt" selbst: dann trüge die Achse krumme Zeiten und die
|
|
/// laufende Sendung wäre immer angeschnitten.
|
|
public let start: Date
|
|
public let end: Date
|
|
public let pointsPerMinute: CGFloat
|
|
/// Schmaler wird kein Block, egal wie kurz die Sendung ist.
|
|
public let minimumBlockWidth: CGFloat
|
|
|
|
private static let halfHour: TimeInterval = 30 * 60
|
|
|
|
public init(
|
|
now: Date,
|
|
latestEnd: Date? = nil,
|
|
pointsPerMinute: CGFloat = 4.4,
|
|
minimumBlockWidth: CGFloat = 36,
|
|
minimumHours: Double = 3,
|
|
maximumHours: Double = 14
|
|
) {
|
|
let anchor = (now.timeIntervalSince1970 / Self.halfHour).rounded(.down) * Self.halfHour
|
|
let start = Date(timeIntervalSince1970: anchor)
|
|
self.start = start
|
|
// Unten begrenzt, damit ein Raster mit einer einzigen bekannten
|
|
// Sendung nicht zur Briefmarke wird; oben, damit es bei einem
|
|
// Anbieter mit zwei Wochen Vorschau nicht endlos wird.
|
|
let bounded = min(
|
|
max(latestEnd ?? start, start.addingTimeInterval(minimumHours * 3_600)),
|
|
start.addingTimeInterval(maximumHours * 3_600)
|
|
)
|
|
// Auch das Ende auf die halbe Stunde, nach oben. Sonst endet die
|
|
// Achse mit einem Stummel, dessen Beschriftung keine Spalte mehr hat.
|
|
self.end = Date(
|
|
timeIntervalSince1970: (bounded.timeIntervalSince1970 / Self.halfHour).rounded(.up) * Self.halfHour
|
|
)
|
|
self.pointsPerMinute = pointsPerMinute
|
|
self.minimumBlockWidth = minimumBlockWidth
|
|
}
|
|
|
|
public var totalWidth: CGFloat {
|
|
CGFloat(end.timeIntervalSince(start) / 60) * pointsPerMinute
|
|
}
|
|
|
|
/// Der waagerechte Versatz eines Zeitpunkts.
|
|
///
|
|
/// Nie negativ: was vor dem Fenster liegt, klebt an dessen linkem Rand.
|
|
public func x(for date: Date) -> CGFloat {
|
|
max(0, CGFloat(date.timeIntervalSince(start) / 60) * pointsPerMinute)
|
|
}
|
|
|
|
/// Die Breite einer Sendung, auf das Fenster beschnitten.
|
|
public func width(from begin: Date, to finish: Date) -> CGFloat {
|
|
let visibleStart = max(begin, start)
|
|
let visibleEnd = min(finish, end)
|
|
let minutes = max(0, visibleEnd.timeIntervalSince(visibleStart) / 60)
|
|
return max(CGFloat(minutes) * pointsPerMinute, minimumBlockWidth)
|
|
}
|
|
|
|
/// Die Beschriftungen der Zeitachse, halbstündlich.
|
|
public var ticks: [Date] {
|
|
stride(from: 0, to: end.timeIntervalSince(start), by: Self.halfHour)
|
|
.map { start.addingTimeInterval($0) }
|
|
}
|
|
|
|
/// Wie breit eine Achsenbeschriftung ist.
|
|
public var tickWidth: CGFloat { CGFloat(Self.halfHour / 60) * pointsPerMinute }
|
|
}
|