Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit `MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre. Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle` wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und `TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten. `Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein zweites Mal falsch — zu schreiben. Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie waren bereits auseinandergelaufen: derselbe Fehler bekam je nach Bildschirm eine andere Erklärung. Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das jetzt ausdrücklich, damit es niemand später „aufräumt". Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit Plex. Sie folgen als Nächstes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
2.8 KiB
Swift
79 lines
2.8 KiB
Swift
import SwiftUI
|
|
import FlixyDesign
|
|
import MediaKit
|
|
|
|
/// Reihen der Startseite ein- und ausblenden und umsortieren.
|
|
struct HomeLayoutView: View {
|
|
@Environment(FlixySession.self) private var session
|
|
@Environment(\.flixyAccent) private var accent
|
|
@Environment(HomeLayoutSettings.self) private var layout
|
|
|
|
@State private var rows: [HomeRow] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if isLoading {
|
|
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else if rows.isEmpty {
|
|
ContentUnavailableView {
|
|
Label("home.empty.title", systemImage: "rectangle.stack")
|
|
} description: {
|
|
Text("homeLayout.empty")
|
|
}
|
|
} else {
|
|
list
|
|
}
|
|
}
|
|
.background(Color.flixyCanvas)
|
|
.navigationTitle(Text("homeLayout.title"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar { EditButton() }
|
|
.task { await load() }
|
|
}
|
|
|
|
private var list: some View {
|
|
List {
|
|
Section {
|
|
ForEach($rows) { $row in
|
|
Toggle(isOn: Binding(
|
|
get: { !row.isHidden },
|
|
set: {
|
|
row.isHidden = !$0
|
|
layout.setHidden(!$0, for: row.id)
|
|
}
|
|
)) {
|
|
Text(row.title)
|
|
.foregroundStyle(Color.flixyTextPrimary)
|
|
}
|
|
.listRowBackground(Color.flixySurface)
|
|
}
|
|
.onMove { indices, destination in
|
|
rows.move(fromOffsets: indices, toOffset: destination)
|
|
layout.setOrder(rows.map(\.id))
|
|
}
|
|
} footer: {
|
|
Text("homeLayout.explanation")
|
|
}
|
|
}
|
|
// Ausdrücklich, nicht nur geerbt.
|
|
//
|
|
// Listen und Formulare zeichnen ihre Zeilen teilweise in einem
|
|
// eigenen Darstellungskontext, in dem eigene Umgebungswerte verloren
|
|
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
|
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
|
.tint(accent)
|
|
.scrollContentBackground(.hidden)
|
|
}
|
|
|
|
private func load() async {
|
|
defer { isLoading = false }
|
|
guard let backend = session.backend else { return }
|
|
// Die Reihen kommen vom Server — es gibt keine feste Liste, die sich
|
|
// vorweg anzeigen ließe. Was hier steht, hängt davon ab, welche
|
|
// Mediatheken und Sammlungen angelegt sind.
|
|
let hubs = (try? await backend.homeRows()) ?? []
|
|
rows = layout.rows(from: hubs)
|
|
}
|
|
}
|