Der Aufmacher bleibt stehen, und Folgen bekommen ein Bild
Drei Sachen am Karussell. Die Vorschlaege wechselten bei jedem Start, obwohl es dafuer laengst ein Zeitfenster von sechs Stunden gab. Der Grund: das Los hing an der Position in der Liste, nicht am Titel. Kam einer dazu, verschwand einer, oder aenderte sich ein Fortschritt, bekamen alle dahinter ein anderes Los — und „Weiterschauen\" waechst und schrumpft staendig. Jetzt haengt das Los an der Kennung; wer dazukommt, stoert niemanden. Als eigener Typ mit elf Tests, weil „es wechselt zu oft\" kein Befund ist, den man debuggen kann. Folgen zeigten kein Bild. Die Kette Folge → Serie gab es zwar, aber sie hoerte beim ersten Glied auf, das sich **zusammensetzen** liess — und ob es das Bild dahinter gibt, stellt sich erst beim Abruf heraus. Die Reihenfolge wandert deshalb ins Laden: Folge, Staffel, Serie, der Reihe nach, bis eine Adresse antwortet. Und auf dem Mac gibt es jetzt Pfeile links und rechts, sobald der Zeiger ueber dem Aufmacher liegt, sowie Punkte, die unter dem Zeiger wachsen und auf die man springen kann. Nur dort: auf dem iPad und dem iPhone wischt man ohnehin, und zwei Knoepfe staenden nur im Bild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4b084bc926
commit
2a2afa8573
@@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
|
||||
/// Welche Titel oben gross gezeigt werden — und in welcher Reihenfolge.
|
||||
///
|
||||
/// **Warum es das als eigenen Typ gibt.** Die Auswahl soll sich zufällig
|
||||
/// anfühlen und trotzdem stabil sein: dreimal am Abend dieselbe, am nächsten
|
||||
/// Morgen eine andere. Das ist genau die Art Regel, die man nicht im
|
||||
/// laufenden Betrieb beurteilen kann — „es wechselt zu oft" ist kein Befund,
|
||||
/// den man debuggen könnte. Mit einer Liste hinein und einer heraus schon.
|
||||
///
|
||||
/// **Der Fehler, der es hierher gebracht hat.** Vorher wurde ein Zufallszahl-
|
||||
/// erzeuger einmal je Zeitfenster aufgesetzt und dann Titel für Titel
|
||||
/// abgefragt. Das Los hing damit an der **Position** in der Liste: kam ein
|
||||
/// Titel dazu, verschwand einer, oder änderte sich der Fortschritt, bekamen
|
||||
/// alle dahinter ein anderes Los — und der Aufmacher war nach jedem Neustart
|
||||
/// ein anderer, obwohl das Zeitfenster dasselbe war.
|
||||
///
|
||||
/// Jetzt hängt das Los an der **Kennung** des Titels. Wer dazukommt, stört
|
||||
/// niemanden; wer geht, hinterlässt eine Lücke und sonst nichts.
|
||||
public enum HeroSelection {
|
||||
/// Wie lange dieselbe Auswahl gilt.
|
||||
public static let windowHours: Double = 6
|
||||
|
||||
/// Ordnet die Titel, ohne die Reihenfolge fest vorzugeben.
|
||||
///
|
||||
/// **Ungleiche Lose.** Ohne Hintergrundbild ist der Aufmacher eine graue
|
||||
/// Fläche mit Text, und ein Titel ohne eines gehört dort eigentlich nicht
|
||||
/// hin. Ihn auszuschliessen wäre aber zu hart: bei einer Mediathek, in
|
||||
/// der wenige Titel eines tragen, blieben immer dieselben drei übrig.
|
||||
/// Also bekommt er ein schlechteres Los statt gar keines.
|
||||
public static func ordered(
|
||||
_ items: [MediaItem],
|
||||
at time: Date = .now,
|
||||
windowHours: Double = windowHours
|
||||
) -> [MediaItem] {
|
||||
let fenster = UInt64(max(0, time.timeIntervalSince1970 / (windowHours * 3_600)))
|
||||
|
||||
var bewertet: [(item: MediaItem, los: Double)] = []
|
||||
bewertet.reserveCapacity(items.count)
|
||||
for item in items {
|
||||
bewertet.append((item, lot(for: item, window: fenster)))
|
||||
}
|
||||
|
||||
// Bei gleichem Los die Kennung: zweimal dieselbe Eingabe soll zweimal
|
||||
// dasselbe ergeben, sonst ist ein Fehler nicht wiederholbar.
|
||||
bewertet.sort { links, rechts in
|
||||
links.los == rechts.los ? links.item.id < rechts.item.id : links.los > rechts.los
|
||||
}
|
||||
return bewertet.map(\.item)
|
||||
}
|
||||
|
||||
/// Das Los eines Titels in diesem Zeitfenster — zwischen 0 und 1.
|
||||
static func lot(for item: MediaItem, window: UInt64) -> Double {
|
||||
let hatBild = item.art != nil || item.seriesArt != nil
|
||||
let gewicht = hatBild ? 1.0 : 0.2
|
||||
return unitRandom(seed: stableHash(item.id) ^ window) * gewicht
|
||||
}
|
||||
|
||||
/// Ein Streuwert, der über Programmläufe hinweg gleich bleibt.
|
||||
///
|
||||
/// **Nicht `hashValue`.** Swift streut Hashes je Programmlauf zufällig;
|
||||
/// damit wäre die Auswahl nach jedem Start eine andere — also genau der
|
||||
/// Fehler, der hier behoben werden soll, nur an einer anderen Stelle.
|
||||
static func stableHash(_ text: String) -> UInt64 {
|
||||
// FNV-1a: vier Zeilen, keine Abhängigkeit, und für das Mischen von
|
||||
// ein paar Kacheln mehr als gut genug.
|
||||
var hash: UInt64 = 0xCBF2_9CE4_8422_2325
|
||||
for byte in text.utf8 {
|
||||
hash ^= UInt64(byte)
|
||||
hash = hash &* 0x0000_0100_0000_01B3
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
/// SplitMix64, auf 0..<1 gebracht.
|
||||
static func unitRandom(seed: UInt64) -> Double {
|
||||
var z = seed &+ 0x9E37_79B9_7F4A_7C15
|
||||
z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
|
||||
z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
|
||||
z = z ^ (z >> 31)
|
||||
// Die oberen 53 Bit: genau die, die ein `Double` verlustfrei trägt.
|
||||
return Double(z >> 11) / Double(1 << 53)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user