Phase 5: Nachladen, Startseiten-Layout

Nachladen
- DownloadRefiller wählt aus, WELCHE Folgen fehlen; wie viele, rechnet
  die Regel-Engine. Getrennt, weil das Rechnen ohne Server auskommt und
  damit prüfbar ist.
- In Ausstrahlungsreihenfolge, Specials in Staffel 0 bleiben außen vor.
- Läuft nach dem Ansehen und beim Start, nicht auf einem Zeitgeber: eine
  App, die im Hintergrund unaufgefordert Gigabytes lädt, wäre eine
  unangenehme Überraschung auf der Mobilfunkrechnung.

Startseiten-Layout
- Reihen ein-/ausblenden und per Bearbeiten-Modus umsortieren.
- Die Anordnungslogik liegt als HubArrangement in PlexKit statt im
  App-Target — dort ist sie ohne Simulator testbar, und dort sitzen die
  unangenehmen Fälle: neue Reihen vom Server landen am Ende statt zu
  verschwinden, verschwundene aus der gespeicherten Liste stören nicht,
  ein doppelter Eintrag stürzt nicht ab. 11 Tests.

126 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-11 17:43:20 +02:00
co-authored by Claude Opus 5
parent af23372997
commit b7fc18823a
10 changed files with 455 additions and 2 deletions
+3 -1
View File
@@ -5,6 +5,7 @@ import PlexKit
/// Die Startseite: Aufmacher oben, darunter die Reihen vom Server.
struct HomeView: View {
@Environment(FlixySession.self) private var session
@Environment(HomeLayoutSettings.self) private var layout
@State private var state: LoadState<[PlexHub]> = .loading
var body: some View {
@@ -22,7 +23,8 @@ struct HomeView: View {
Button("action.retry") { Task { await load() } }
}
case .loaded(let hubs):
case .loaded(let allHubs):
let hubs = layout.apply(to: allHubs)
if hubs.isEmpty {
ContentUnavailableView {
Label("home.empty.title", systemImage: "film.stack")
+6 -1
View File
@@ -381,8 +381,13 @@ final class PlayerModel {
case .reachedEnd:
isPlaying = false
hasReachedEnd = true
// Grundlage fürs automatische Löschen nach dem Ansehen.
// Grundlage fürs automatische Löschen nach dem Ansehen —
// und der Auslöser fürs Nachladen: der Vorrat ist jetzt
// um eine Folge kleiner.
downloads.markWatched(ratingKey: request.ratingKey)
Task { [server, downloads] in
await DownloadRefiller(server: server, coordinator: downloads).refill()
}
case .tracksResolved(let audio, let subtitle):
audioTracks = audio
subtitleTracks = subtitle
+70
View File
@@ -0,0 +1,70 @@
import SwiftUI
import FlixyDesign
import PlexKit
/// Reihen der Startseite ein- und ausblenden und umsortieren.
struct HomeLayoutView: View {
@Environment(FlixySession.self) private var session
@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")
}
}
.scrollContentBackground(.hidden)
}
private func load() async {
defer { isLoading = false }
guard let server = session.server 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 server.homeHubs()) ?? []
rows = layout.rows(from: hubs)
}
}
+6
View File
@@ -62,6 +62,12 @@ struct SettingsView: View {
} label: {
Label("appearance.title", systemImage: "paintpalette")
}
NavigationLink {
HomeLayoutView()
} label: {
Label("homeLayout.title", systemImage: "rectangle.stack")
}
}
downloadSection