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() .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) // **Mehrfachauswahl, und leer heisst alle.** Ein `Picker` könnte // nur eine Mediathek, und „keine" wäre dort ein eigener Eintrag, // den man versehentlich trifft. Hier sind Haken: keiner gesetzt // ist der Normalfall und bedeutet, dass alles infrage kommt. if !session.libraries.isEmpty { Section { ForEach(session.libraries) { library in Button { toggleHeroLibrary(library.id) } label: { LabeledContent { if layout.heroLibraries.contains(library.id) { Image(systemName: "checkmark") .font(.body.weight(.semibold)) .foregroundStyle(accent) } } label: { Text(library.title) .foregroundStyle(Color.flixyTextPrimary) } // Damit der ganze Streifen trifft und nicht nur // die Schrift — eine Zeile, bei der nur ein Wort // reagiert, liest sich als kaputt. .contentShape(.rect) } .buttonStyle(.plain) .accessibilityAddTraits( layout.heroLibraries.contains(library.id) ? .isSelected : [] ) } } header: { Text("home.heroLibraries") } footer: { Text("home.heroLibrariesExplanation") } .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: { // **Beim Abschnitt, auf den er wirkt.** In der Werkzeugleiste // stand er über allem — über dem Aufmacher, über den // Mediatheken — und tat dort nichts. Ein Knopf, der nur für // einen Teil des Bildschirms gilt, gehört an diesen Teil. // // Auf dem Mac gibt es ihn nicht: dort zieht man Zeilen ohne // vorherigen Moduswechsel. HStack { Text("home.rowsHeader") #if os(iOS) if !rows.isEmpty { Spacer() EditButton() .font(.flixyCaption) .textCase(nil) } #endif } } 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) } /// Kreuzt eine Mediathek für den Aufmacher an oder ab. private func toggleHeroLibrary(_ identifier: String) { if layout.heroLibraries.contains(identifier) { layout.heroLibraries.remove(identifier) } else { layout.heroLibraries.insert(identifier) } } 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" } } }