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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
@@ -0,0 +1,81 @@
|
||||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user