import SwiftUI import FlixyDesign /// Die Farbeinstellungen. /// /// Kuratierte Kacheln plus eine freie Farbe. Die kuratierten sind so gewählt, /// dass sie auf dem dunklen Grund ohne Nachhilfe lesbar sind; bei einer freien /// Farbe greift die Kontrastkorrektur — und sagt es auch, statt die Farbe /// stillschweigend zu verändern. struct AppearanceView: View { @Environment(FlixyTheme.self) private var theme @Environment(HomeLayoutSettings.self) private var layout @State private var customHex: String = "" @State private var showsCustomField = false private let columns = [GridItem(.adaptive(minimum: 64), spacing: FlixySpacing.regular)] var body: some View { Form { Section { LazyVGrid(columns: columns, spacing: FlixySpacing.regular) { ForEach(AccentOption.palette) { option in swatch(option) } } .padding(.vertical, FlixySpacing.snug) } header: { Text("appearance.accent") } footer: { Text("appearance.accentExplanation") } // Die Kachelgröße steht bei der Darstellung und nicht bei der // Startseite: sie gilt für Reihen **und** Mediathek, und es geht // ums Aussehen, nicht um den Aufbau einer bestimmten Seite. Section { @Bindable var layout = layout LabeledContent { Text(layout.cardScale.formatted(.percent.precision(.fractionLength(0)))) .foregroundStyle(Color.flixyTextSecondary) .monospacedDigit() } label: { Text("appearance.cardScale") } Slider( value: $layout.cardScale, in: HomeLayoutSettings.cardScaleRange, step: 0.1 ) .accessibilityLabel(Text("appearance.cardScale")) } footer: { Text("appearance.cardScaleExplanation") } .listRowBackground(Color.flixySurface) Section { Toggle("appearance.customColor", isOn: $showsCustomField.animation(.snappy)) if showsCustomField { HStack { TextField("appearance.hexPlaceholder", text: $customHex) .flixyPlainEntry() .onSubmit(applyCustomColor) Button("action.apply", action: applyCustomColor) .disabled(RGB(string: customHex) == nil) } if !customHex.isEmpty, RGB(string: customHex) == nil { Text("appearance.invalidHex") .font(.flixyCaption) .foregroundStyle(Color.flixyDestructive) } } if theme.accentWasAdjusted { // Ohne diesen Hinweis wirkt die Korrektur wie ein Fehler: // die gewählte Farbe sieht auf dem Bildschirm heller aus // als im Farbwähler, und niemand wüsste warum. Label { Text("appearance.adjusted \(theme.accent.hexString)") .font(.flixyCaption) } icon: { Image(systemName: "info.circle") } .foregroundStyle(Color.flixyTextSecondary) } } Section("appearance.preview") { PreviewRow() } } .flixyForm() .scrollContentBackground(.hidden) .background(Color.flixyCanvas) .navigationTitle(Text("appearance.title")) .flixyInlineTitle() .onAppear { if theme.selection.id == AccentOption.customIdentifier { customHex = theme.selection.color.hexString showsCustomField = true } } } private func swatch(_ option: AccentOption) -> some View { Button { theme.select(option) } label: { Circle() .fill(option.color.swiftUIColor) .frame(height: 44) .overlay { if theme.selection == option { Image(systemName: "checkmark") .font(.headline) // Dunkel auf der Farbe, nicht weiß: die Palette // enthält helle Töne wie Amber, auf denen Weiß // verschwinden würde. .foregroundStyle(Color.flixyCanvas) } } .overlay { Circle() .strokeBorder( theme.selection == option ? Color.flixyTextPrimary : .clear, lineWidth: 2 ) .padding(-4) } } .buttonStyle(.plain) .accessibilityLabel(Text(option.name)) .accessibilityAddTraits(theme.selection == option ? [.isButton, .isSelected] : .isButton) } private func applyCustomColor() { guard let color = RGB(string: customHex) else { return } theme.select(.custom(color)) } } /// Zeigt die gewählte Farbe an den Stellen, an denen sie später auftaucht. private struct PreviewRow: View { @Environment(\.flixyAccent) private var accent var body: some View { VStack(alignment: .leading, spacing: FlixySpacing.regular) { HStack(spacing: FlixySpacing.regular) { Label("hero.play", systemImage: "play.fill") .font(.flixyCardTitle) .padding(.horizontal, FlixySpacing.loose) .padding(.vertical, FlixySpacing.snug) .background(accent, in: .rect(cornerRadius: FlixyRadius.control)) .foregroundStyle(Color.flixyCanvas) Label("sidebar.downloads", systemImage: "arrow.down.circle") .font(.flixyCardTitle) .foregroundStyle(accent) } WatchProgressBar(fraction: 0.45) } .padding(.vertical, FlixySpacing.snug) .listRowBackground(Color.flixySurface) } }