Files
Flaro/Packages/HaloKit/Sources/HaloDesign/MiniPlayerPlacement.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
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>
2026-10-03 11:12:32 +02:00

82 lines
3.4 KiB
Swift

import Foundation
import CoreGraphics
/// Wo die schwebende Videokachel sitzt.
///
/// Sie klebt an der linken oder rechten Seite und ist dort **frei in der
/// Höhe** — nicht auf vier Ecken festgelegt. Vier Ecken sind schnell gebaut,
/// aber im Weg: steht die Kachel unten rechts über einem Knopf, bleibt nur
/// die Wahl zwischen vier Orten, von denen drei ebenso stören.
///
/// Eigener Typ statt Rechnungen in der Ansicht, aus demselben Grund wie bei
/// `GuideLayout`: hier geht lautlos schief, was man nicht sieht. Eine Kachel,
/// die halb aus dem Bild ragt, fällt erst auf dem Gerät auf — und dann als
/// Gefühl, nicht als Fehler.
public struct MiniPlayerPlacement: Sendable, Equatable {
public enum Side: String, Sendable, CaseIterable, Codable {
case leading, trailing
}
/// Der sichtbare Bereich, in dem die Kachel sich bewegen darf.
public let bounds: CGSize
public let tile: CGSize
/// Abstand zum Rand. Eine Kachel, die den Rand berührt, sieht aus wie ein
/// Fehler statt wie eine Entscheidung.
public let inset: CGFloat
public init(bounds: CGSize, tile: CGSize, inset: CGFloat = HaloSpacing.regular) {
self.bounds = bounds
self.tile = tile
self.inset = inset
}
private var halfWidth: CGFloat { tile.width / 2 }
private var halfHeight: CGFloat { tile.height / 2 }
private var minimumX: CGFloat { inset + halfWidth }
private var maximumX: CGFloat { max(minimumX, bounds.width - inset - halfWidth) }
private var minimumY: CGFloat { inset + halfHeight }
private var maximumY: CGFloat { max(minimumY, bounds.height - inset - halfHeight) }
/// An welche Seite eine losgelassene Kachel gehört.
///
/// Entschieden wird nach dem **Mittelpunkt** der Kachel, nicht nach dem
/// Finger: sonst springt eine Kachel, die man am Rand angefasst hat, auf
/// die falsche Seite.
public func nearestSide(toCenter center: CGPoint) -> Side {
center.x < bounds.width / 2 ? .leading : .trailing
}
/// Wie weit unten die Kachel sitzt: 0 ganz oben, 1 ganz unten.
///
/// Als Anteil statt als Punktwert, damit die Kachel beim Drehen des Geräts
/// an derselben *relativen* Stelle bleibt, statt unten herauszufallen.
public func verticalFraction(forCenter center: CGPoint) -> Double {
let travel = maximumY - minimumY
guard travel > 0 else { return 0 }
return min(max(Double((center.y - minimumY) / travel), 0), 1)
}
public func center(side: Side, verticalFraction: Double) -> CGPoint {
let fraction = min(max(verticalFraction, 0), 1)
return CGPoint(
x: side == .leading ? minimumX : maximumX,
y: minimumY + (maximumY - minimumY) * CGFloat(fraction)
)
}
/// Hält einen Mittelpunkt im sichtbaren Bereich.
///
/// Während des Ziehens, damit die Kachel nicht hinter den Rand rutscht und
/// dort unerreichbar wird — der Finger wäre dann schneller als das Ziel.
public func clamped(center: CGPoint) -> CGPoint {
CGPoint(
// `max(min…)` in dieser Reihenfolge, weil bei einem Bereich, der
// schmaler ist als die Kachel, sonst der untere Anschlag verliert
// und die Kachel rechts hinausragt.
x: max(minimumX, min(maximumX, center.x)),
y: max(minimumY, min(maximumY, center.y))
)
}
}