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>
82 lines
3.4 KiB
Swift
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))
|
|
)
|
|
}
|
|
}
|