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>
102 lines
3.9 KiB
Swift
102 lines
3.9 KiB
Swift
import Foundation
|
||
import Testing
|
||
@testable import HaloDesign
|
||
|
||
/// Wo die schwebende Videokachel landet.
|
||
@Suite("Kachel-Platzierung")
|
||
struct MiniPlayerPlacementTests {
|
||
|
||
/// Ein iPhone im Hochformat, Kachel 180 × 101.
|
||
private let placement = MiniPlayerPlacement(
|
||
bounds: CGSize(width: 390, height: 844),
|
||
tile: CGSize(width: 180, height: 101),
|
||
inset: 12
|
||
)
|
||
|
||
@Test("Die nähere Seite gewinnt")
|
||
func picksTheNearestSide() {
|
||
#expect(placement.nearestSide(toCenter: CGPoint(x: 50, y: 400)) == .leading)
|
||
#expect(placement.nearestSide(toCenter: CGPoint(x: 340, y: 400)) == .trailing)
|
||
}
|
||
|
||
@Test("Die Höhe bleibt frei, nicht auf Ecken festgelegt")
|
||
func keepsTheVerticalPosition() {
|
||
// Der Kern des Umbaus: vier Ecken sind schnell gebaut, aber im Weg.
|
||
let middle = placement.center(side: .trailing, verticalFraction: 0.5)
|
||
let top = placement.center(side: .trailing, verticalFraction: 0)
|
||
let bottom = placement.center(side: .trailing, verticalFraction: 1)
|
||
|
||
#expect(top.y < middle.y)
|
||
#expect(middle.y < bottom.y)
|
||
#expect(top.x == middle.x && middle.x == bottom.x)
|
||
}
|
||
|
||
@Test("Anteil und Mittelpunkt rechnen ineinander um")
|
||
func roundTripsTheFraction() {
|
||
for fraction in [0.0, 0.25, 0.5, 0.9, 1.0] {
|
||
let center = placement.center(side: .leading, verticalFraction: fraction)
|
||
let back = placement.verticalFraction(forCenter: center)
|
||
|
||
#expect(abs(back - fraction) < 0.0001)
|
||
}
|
||
}
|
||
|
||
@Test("Der Anteil bleibt zwischen null und eins")
|
||
func clampsTheFraction() {
|
||
// Sonst wandert die Kachel beim Drehen aus dem Bild: der Anteil wird
|
||
// gespeichert, und ein Wert über eins landete unterhalb des Randes.
|
||
#expect(placement.verticalFraction(forCenter: CGPoint(x: 0, y: -9_000)) == 0)
|
||
#expect(placement.verticalFraction(forCenter: CGPoint(x: 0, y: 9_000)) == 1)
|
||
#expect(placement.center(side: .leading, verticalFraction: 5).y <= 844 - 12)
|
||
}
|
||
|
||
@Test("An jeder Seite bleibt der Sicherheitsabstand")
|
||
func keepsTheInset() {
|
||
for side in MiniPlayerPlacement.Side.allCases {
|
||
for fraction in [0.0, 0.5, 1.0] {
|
||
let center = placement.center(side: side, verticalFraction: fraction)
|
||
|
||
#expect(center.x - 90 >= 12)
|
||
#expect(center.x + 90 <= 390 - 12)
|
||
#expect(center.y - 50.5 >= 12)
|
||
#expect(center.y + 50.5 <= 844 - 12)
|
||
}
|
||
}
|
||
}
|
||
|
||
@Test("Beim Ziehen rutscht nichts über den Rand")
|
||
func clampsWhileDragging() {
|
||
// Ohne diese Grenze zieht man die Kachel hinter den Bildschirmrand,
|
||
// und dort ist sie mit dem Finger nicht mehr einzuholen.
|
||
let farOut = placement.clamped(center: CGPoint(x: -500, y: -500))
|
||
#expect(farOut.x == 102.0)
|
||
#expect(farOut.y == 62.5)
|
||
|
||
let alsoFarOut = placement.clamped(center: CGPoint(x: 9_000, y: 9_000))
|
||
#expect(alsoFarOut.x == 288.0)
|
||
#expect(alsoFarOut.y == 781.5)
|
||
}
|
||
|
||
@Test("Ein Punkt in der Mitte bleibt, wo er ist")
|
||
func leavesValidPointsAlone() {
|
||
let inside = CGPoint(x: 195, y: 400)
|
||
|
||
#expect(placement.clamped(center: inside) == inside)
|
||
}
|
||
|
||
@Test("Ein zu enger Bereich schiebt die Kachel nicht hinaus")
|
||
func survivesTinyBounds() {
|
||
// Kommt beim Drehen und in der Übergangsphase vor: der Bereich ist
|
||
// kurzzeitig schmaler als die Kachel. Ein falsch herum geklammerter
|
||
// Wert schöbe sie dann dauerhaft aus dem Bild.
|
||
let narrow = MiniPlayerPlacement(
|
||
bounds: CGSize(width: 100, height: 80),
|
||
tile: CGSize(width: 180, height: 101),
|
||
inset: 12
|
||
)
|
||
|
||
#expect(narrow.clamped(center: CGPoint(x: 0, y: 0)) == CGPoint(x: 102, y: 62.5))
|
||
#expect(narrow.center(side: .trailing, verticalFraction: 1) == CGPoint(x: 102, y: 62.5))
|
||
}
|
||
}
|