Der Fehler steckte in der Abfrage, und er ist grob:
beginsAt> = jetzt − 1 Std.
endsAt< = jetzt + 6 Std.
Beide Grenzen schließen genau das aus, was man sehen will. Ein
Zweistünder, der um halb vier begann, war um Viertel nach fünf
unsichtbar — und angezeigt wurde stattdessen die nächste Sendung, die
durch den Filter passte. Deshalb wich die Anzeige von Plex' eigener ab.
Jetzt wird nur der **Anfang** eingegrenzt, mit sechs Stunden Vorlauf für
lange Sendungen, und keine Grenze auf das Ende. Zurück kommt, was den
Zeitraum berührt, statt was vollständig hineinpasst.
Gefiltert wird danach nur das Programm, nicht die Senderliste: ein Sender
ohne frische Daten soll „kein Programm" sagen, statt zu verschwinden.
Dazu zwei Wünsche aus dem Feldtest:
Die Kachel lässt sich in der Größe ziehen. Der Griff sitzt in der Ecke,
die zur Bildschirmmitte zeigt — am äußeren Rand läge er halb außerhalb
des Daumens. Gespeichert wird die begrenzte Breite, nicht die gezogene,
sonst bliebe eine einmal zu weit gezogene Kachel für immer zu groß.
Und eine Senderauswahl. Über zwanzig Sender zu zeichnen und ihr Programm
zu holen kostet Zeit, und die meisten sieht man nie an. Eine leere
Auswahl heißt weiterhin „alle" — sonst stünde man beim ersten Öffnen vor
einer leeren Liste.
Die Zeiten im Raster-Abzug liegen jetzt in der Zukunft. Mit den echten
Zeitstempeln von damals wäre er ab dem Tag nach der Aufnahme leer
gewesen, sobald auf Frische gefiltert wird.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.3 KiB
Swift
85 lines
3.3 KiB
Swift
import Foundation
|
|
import CoreGraphics
|
|
|
|
/// Wo die schwebende Videokachel sitzt.
|
|
///
|
|
/// 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, oder eine, die beim Loslassen in die falsche
|
|
/// Ecke springt, fällt erst auf dem Gerät auf — und dann als Gefühl, nicht
|
|
/// als Fehler.
|
|
public struct MiniPlayerPlacement: Sendable, Equatable {
|
|
public enum Corner: String, Sendable, CaseIterable, Codable {
|
|
case topLeading, topTrailing, bottomLeading, bottomTrailing
|
|
|
|
public var isTop: Bool { self == .topLeading || self == .topTrailing }
|
|
public var isLeading: Bool { self == .topLeading || self == .bottomLeading }
|
|
}
|
|
|
|
/// 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 = FlixySpacing.regular) {
|
|
self.bounds = bounds
|
|
self.tile = tile
|
|
self.inset = inset
|
|
}
|
|
|
|
/// Die Ecke, in die eine losgelassene Kachel gehört.
|
|
///
|
|
/// Entschieden wird nach dem **Mittelpunkt** der Kachel, nicht nach dem
|
|
/// Finger: sonst springt eine Kachel, die man an der oberen Kante
|
|
/// angefasst hat, in die falsche Hälfte.
|
|
public func nearestCorner(toCenter center: CGPoint) -> Corner {
|
|
let isLeading = center.x < bounds.width / 2
|
|
let isTop = center.y < bounds.height / 2
|
|
|
|
switch (isTop, isLeading) {
|
|
case (true, true): return .topLeading
|
|
case (true, false): return .topTrailing
|
|
case (false, true): return .bottomLeading
|
|
case (false, false): return .bottomTrailing
|
|
}
|
|
}
|
|
|
|
/// Die linke obere Ecke der Kachel für eine Ecke des Bildschirms.
|
|
public func origin(for corner: Corner) -> CGPoint {
|
|
CGPoint(
|
|
x: corner.isLeading ? inset : max(inset, bounds.width - tile.width - inset),
|
|
y: corner.isTop ? inset : max(inset, bounds.height - tile.height - inset)
|
|
)
|
|
}
|
|
|
|
/// Der Mittelpunkt der Kachel in einer Ecke — damit rechnet SwiftUI.
|
|
public func center(for corner: Corner) -> CGPoint {
|
|
let start = origin(for: corner)
|
|
return CGPoint(x: start.x + tile.width / 2, y: start.y + tile.height / 2)
|
|
}
|
|
|
|
/// 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 {
|
|
let halfWidth = tile.width / 2
|
|
let halfHeight = tile.height / 2
|
|
|
|
let minX = inset + halfWidth
|
|
let maxX = bounds.width - inset - halfWidth
|
|
let minY = inset + halfHeight
|
|
let maxY = bounds.height - inset - halfHeight
|
|
|
|
return 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(minX, min(maxX, center.x)),
|
|
y: max(minY, min(maxY, center.y))
|
|
)
|
|
}
|
|
}
|