Einstellungen: sechs Bildschirme statt einer Liste — und was sie steuern
Deine Einstellungs-Bilder sind eine lange Liste. Ich habe sie durchgegangen
und getrennt: was etwas steuert, ist gebaut; was eine Einstellung für eine
Sache wäre, die es nicht gibt, ist nicht gebaut. Ein Schalter, der nichts
bewirkt, ist schlimmer als kein Schalter.
**Die Einstellungen sind jetzt eine Übersicht.** Vorher stand alles
untereinander auf einem Bildschirm — Serverangaben, Qualität, Wiedergabe, drei
Verweise, Downloads, Version, Abmelden. Das war beim Bauen bequem und beim
Suchen anstrengend. Jetzt führt jede Zeile in einen Bildschirm, der zu einer
Frage gehört: Server, Erscheinungsbild, Startseite, Wiedergabe, Ton &
Untertitel, Downloads, Diagnose.
**Neu, und jedes davon greift irgendwo:**
- Zurück- und Vorspulen getrennt einstellbar. Die Knöpfe im Player und die
Doppeltipp-Flächen tragen danach auch das passende Symbol.
- Startgeschwindigkeit — die Engine kann es längst, gefragt wurde nie.
- „Im Querformat sperren" abschaltbar. Für die allermeisten Filme richtig, für
den, der im Bett auf der Seite liegt, das Gegenteil.
- „Gewählte Qualität merken": die Stufe gilt dann auch für den nächsten Titel.
Aufbewahrt wird eine **Zahl**, keine Sprosse — welche Sprossen es gibt, hängt
von der Quelle ab, und eine 720p-Datei kennt keine Zwanzig-Megabit-Stufe.
Dann kommt die nächstgelegene, nicht „automatisch".
- „Bitrate im Qualitätsmenü zeigen".
- Gleichzeitige Downloads. Der Wert gab es schon in der Engine, nur ohne
Bedienung und ohne dass er einen Neustart überlebte.
- Aufmacher: wie viele Titel, und ob sie aus Angefangenem oder nur aus
Neuzugängen kommen.
- „Weiterschauen und Als Nächstes zusammenlegen" und „Gesehenes aus
Neuzugängen ausblenden" — beides in der Ansicht, nicht im Server: das sind
Fragen der Darstellung, und so gelten sie für Plex und Jellyfin gleich.
**Verkleinert herunterladen geht jetzt auch bei Jellyfin.** Da hatte ich zu
früh aufgegeben: ich hatte nur nach der Adresse zum *Abspielen* gesucht, und
die ist eine Abspielliste mit hunderten Schnipseln — als Datei unbrauchbar.
`/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als eine Datei heraus. Die
Größe bleibt eine Schätzung; eine Umrechnung, die erst beim Abruf beginnt,
kennt ihre eigene Länge nicht.
**Die Kontozeile ist jetzt eine Karte, die nach oben aufklappt**, kein
Systemmenü mehr. Ein Menü legt sich als eigene Ebene über alles und springt
dorthin, wo Platz ist; die Karte bleibt, wo sie ist, und lässt die Zeile mit
dem Namen stehen — man sieht also weiter, um wessen Konto es geht.
**Nicht gebaut, mit Grund:** Trailer im Karussell, Poster- und Sammlungsstil,
Untertitel-Aussehen, sekundäre Untertitel, lokale Intros, Modular Home,
App-Symbol. Für all das fehlt das, was die Einstellung steuern würde — es gibt
in Flixy weder Trailer noch Sammlungen, und der Player zeichnet Untertitel
nicht selbst.
263 Tests grün. **Nicht angesehen**: der Simulator-Tunnel ist weiterhin
abgestürzt und ließ sich nicht wieder aufbauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3f6c6d8816
commit
dace5d1543
@@ -24,7 +24,7 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
case .loaded(let allRows):
|
||||
let hubs = layout.apply(to: allRows)
|
||||
let hubs = layout.apply(to: prepared(allRows))
|
||||
if hubs.isEmpty {
|
||||
ContentUnavailableView {
|
||||
Label("home.empty.title", systemImage: "film.stack")
|
||||
@@ -72,26 +72,83 @@ struct HomeView: View {
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
|
||||
/// Wie viele Titel der Aufmacher durchwechselt.
|
||||
private static let heroCount = 5
|
||||
/// Bringt die Reihen vom Server in die Form, die eingestellt ist.
|
||||
///
|
||||
/// Hier und nicht im Backend: „Weiterschauen und Als Nächstes zusammen"
|
||||
/// und „Gesehenes ausblenden" sind Fragen der Darstellung, keine des
|
||||
/// Servers. Beides hier zu erledigen hält den Vertrag schmal und gilt für
|
||||
/// Plex und Jellyfin gleichermaßen.
|
||||
private func prepared(_ rows: [MediaRowGroup]) -> [MediaRowGroup] {
|
||||
var rows = rows
|
||||
|
||||
if layout.hidesWatchedInLatest {
|
||||
rows = rows.map { row in
|
||||
guard row.id.hasPrefix("latest") else { return row }
|
||||
return MediaRowGroup(
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
moreKey: row.moreKey,
|
||||
items: row.items.filter { !$0.isWatched }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if layout.combinesContinueRows {
|
||||
rows = combiningContinueRows(rows)
|
||||
}
|
||||
|
||||
return rows.filter { !$0.items.isEmpty }
|
||||
}
|
||||
|
||||
/// Legt „Weiterschauen" und „Als Nächstes" in eine Reihe.
|
||||
///
|
||||
/// Die zusammengelegte Reihe behält die Kennung der ersten — sonst wäre
|
||||
/// sie für die gespeicherte Reihenfolge eine neue und stünde plötzlich
|
||||
/// wieder ganz unten.
|
||||
private func combiningContinueRows(_ rows: [MediaRowGroup]) -> [MediaRowGroup] {
|
||||
let continuing = rows.filter(\.isContinueWatching)
|
||||
guard continuing.count > 1, let first = continuing.first else { return rows }
|
||||
|
||||
var seen = Set<String>()
|
||||
let merged = continuing.flatMap(\.items).filter { seen.insert($0.id).inserted }
|
||||
|
||||
var result: [MediaRowGroup] = []
|
||||
for row in rows {
|
||||
guard row.isContinueWatching else {
|
||||
result.append(row)
|
||||
continue
|
||||
}
|
||||
guard row.id == first.id else { continue }
|
||||
result.append(MediaRowGroup(id: row.id, title: row.title, items: merged))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Was oben groß gezeigt wird.
|
||||
///
|
||||
/// Angefangenes zuerst — wer die App öffnet, will meistens weiterschauen.
|
||||
/// Aufgefüllt wird aus den übrigen Reihen: bei einer einzigen angefangenen
|
||||
/// Folge gäbe es sonst nichts zu blättern.
|
||||
/// Folge gäbe es sonst nichts zu blättern. Wer den Aufmacher lieber als
|
||||
/// Schaufenster versteht, stellt auf Neuzugänge um.
|
||||
private func heroItems(in hubs: [MediaRowGroup]) -> [MediaItem] {
|
||||
let continuing = hubs.first(where: \.isContinueWatching)?.items ?? []
|
||||
let rest = hubs.filter { !$0.isContinueWatching }.flatMap(\.items)
|
||||
let candidates: [MediaItem]
|
||||
switch layout.heroSource {
|
||||
case .continueFirst:
|
||||
let continuing = hubs.first(where: \.isContinueWatching)?.items ?? []
|
||||
let rest = hubs.filter { !$0.isContinueWatching }.flatMap(\.items)
|
||||
candidates = continuing + rest
|
||||
case .latestOnly:
|
||||
candidates = hubs.filter { $0.id.hasPrefix("latest") }.flatMap(\.items)
|
||||
}
|
||||
|
||||
var seen = Set<String>()
|
||||
let candidates = (continuing + rest).filter { seen.insert($0.id).inserted }
|
||||
let unique = candidates.filter { seen.insert($0.id).inserted }
|
||||
|
||||
// Ohne Hintergrundbild ist der Aufmacher eine graue Fläche mit Text.
|
||||
// Lieber einen Titel überspringen als eine leere Bühne zeigen — es sei
|
||||
// denn, keiner hätte eines.
|
||||
let withArtwork = candidates.filter { $0.art != nil || $0.seriesArt != nil }
|
||||
return Array((withArtwork.isEmpty ? candidates : withArtwork).prefix(Self.heroCount))
|
||||
let withArtwork = unique.filter { $0.art != nil || $0.seriesArt != nil }
|
||||
return Array((withArtwork.isEmpty ? unique : withArtwork).prefix(layout.heroCount))
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
|
||||
Reference in New Issue
Block a user