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>
171 lines
6.4 KiB
Swift
171 lines
6.4 KiB
Swift
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)
|
|
}
|
|
}
|
|
|