Phase 1 des Einstellungs-Abgleichs. Beide standen bisher nur im Web, obwohl sie nirgends hingehören, wo es die Sache nicht überall gibt. Die Kachelgröße ist ein Faktor und keine Punktzahl: die Breiten von Plakat und Querformat stehen in einem Verhältnis, das stimmen soll — zwei getrennte Zahlen ließen sich gegeneinander verstellen, bis eine Reihe zur anderen nicht mehr passt. Sie wirkt auf Startseite und Mediathek, sonst sähe das eine anders aus als das andere. Sie steht bei der Darstellung, nicht bei der Startseite: es geht ums Aussehen, und sie gilt für beide Seiten. Die Aufmacherhöhe steht bei der Startseite, direkt bei Quelle und Anzahl. Voreingestellt ist genau das bisherige feste Verhalten — wer nichts anfasst, merkt nichts. Gelesene Werte werden auf ihren Bereich begrenzt, damit eine ältere Ablage keine Kachel über den halben Bildschirm ergibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
4.9 KiB
Swift
135 lines
4.9 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 {
|
|
list
|
|
.background(Color.flixyCanvas)
|
|
.navigationTitle(Text("homeLayout.title"))
|
|
.flixyInlineTitle()
|
|
// Auf dem Mac gibt es den Knopf nicht — dort zieht man Zeilen ohne
|
|
// vorherigen Moduswechsel.
|
|
#if os(iOS)
|
|
.toolbar { EditButton() }
|
|
#endif
|
|
.task { await load() }
|
|
}
|
|
|
|
private var list: some View {
|
|
List {
|
|
@Bindable var layout = layout
|
|
|
|
Section {
|
|
Picker("home.heroSource", selection: $layout.heroSource) {
|
|
ForEach(HomeLayoutSettings.HeroSource.allCases) { source in
|
|
Text(source.label).tag(source)
|
|
}
|
|
}
|
|
Picker("home.heroCount", selection: $layout.heroCount) {
|
|
ForEach(HomeLayoutSettings.heroCountChoices, id: \.self) { count in
|
|
Text(count.formatted()).tag(count)
|
|
}
|
|
}
|
|
|
|
// Ein Regler und keine Auswahl aus festen Stufen: es gibt hier
|
|
// keinen richtigen Wert, nur den, bei dem die erste Reihe
|
|
// gerade so weit hereinlugt, wie man es mag.
|
|
LabeledContent {
|
|
Text(layout.heroHeight.formatted(.percent.precision(.fractionLength(0))))
|
|
.foregroundStyle(Color.flixyTextSecondary)
|
|
.monospacedDigit()
|
|
} label: {
|
|
Text("home.heroHeight")
|
|
}
|
|
Slider(
|
|
value: $layout.heroHeight,
|
|
in: HomeLayoutSettings.heroHeightRange,
|
|
step: 0.05
|
|
)
|
|
.accessibilityLabel(Text("home.heroHeight"))
|
|
} header: {
|
|
Text("home.hero")
|
|
} footer: {
|
|
Text("home.heroExplanation")
|
|
}
|
|
.listRowBackground(Color.flixySurface)
|
|
|
|
Section {
|
|
Toggle("home.combineContinue", isOn: $layout.combinesContinueRows)
|
|
Toggle("home.hideWatched", isOn: $layout.hidesWatchedInLatest)
|
|
}
|
|
.listRowBackground(Color.flixySurface)
|
|
|
|
if isLoading {
|
|
ProgressView()
|
|
.frame(maxWidth: .infinity)
|
|
.listRowBackground(Color.clear)
|
|
} else if rows.isEmpty {
|
|
Text("homeLayout.empty")
|
|
.foregroundStyle(Color.flixyTextSecondary)
|
|
.listRowBackground(Color.flixySurface)
|
|
}
|
|
|
|
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))
|
|
}
|
|
} header: {
|
|
Text("home.rowsHeader")
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
extension HomeLayoutSettings.HeroSource {
|
|
var label: LocalizedStringKey {
|
|
switch self {
|
|
case .continueFirst: "home.heroSource.continueFirst"
|
|
case .latestOnly: "home.heroSource.latestOnly"
|
|
}
|
|
}
|
|
}
|