Drei Dinge aus dem letzten Durchgang. **Die Startseite hatte zu wenig anzubieten.** Es gab eine Reihe „Zuletzt hinzugefügt" für alles zusammen. Jetzt gibt es eine je Mediathek — „Neu in Filme" und „Neu in Serien" sind zwei verschiedene Fragen, und wer nach einem Film sucht, blättert in einer gemeinsamen Reihe an Folgen vorbei. Dazu Favoriten, die es bei Jellyfin ohnehin gibt. Die drei Abfragen laufen nebeneinander; nur die Mediatheken werden streng geholt, weil sie die Probe darauf sind, ob der Server überhaupt antwortet. **Die Seitenleiste ist jetzt echtes Glas**, nicht das alte Material: im ausfahrbaren Blatt über `glassEffect`, in der geteilten Ansicht auf dem iPad vom System selbst — dort lag mein eigener Hintergrund darüber und machte ihn stumpf. Die zum Inhalt zeigende Kante ist gerundet, damit das Blatt sichtbar über der Seite liegt statt daneben zu stehen. **Die Navigation kann nach oben.** Eine Einstellung unter Erscheinungsbild, seitlich oder oben. Oben stehen alle Einträge gleichzeitig da, als Glaskapseln in einer waagerecht scrollenden Reihe — die Zahl der Mediatheken kommt vom Server, und zusammengequetschte Beschriftungen wären die schlechtere Antwort als eine Reihe, die weiterläuft. Angesehen auf iPhone und iPad: beide Formen, das Glas in beiden Leisten, die neuen Reihen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
169 lines
6.2 KiB
Swift
169 lines
6.2 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(NavigationSettings.self) private var navigation
|
|
@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 {
|
|
Picker("appearance.navigation", selection: Bindable(navigation).placement) {
|
|
ForEach(NavigationSettings.Placement.allCases) { placement in
|
|
Text(placement.label).tag(placement)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.padding(.vertical, FlixySpacing.tight)
|
|
} header: {
|
|
Text("appearance.navigation")
|
|
} footer: {
|
|
Text("appearance.navigationExplanation")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
Section {
|
|
Toggle("appearance.customColor", isOn: $showsCustomField.animation(.snappy))
|
|
|
|
if showsCustomField {
|
|
HStack {
|
|
TextField("appearance.hexPlaceholder", text: $customHex)
|
|
.textInputAutocapitalization(.characters)
|
|
.autocorrectionDisabled()
|
|
.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()
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(Color.flixyCanvas)
|
|
.navigationTitle(Text("appearance.title"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.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)
|
|
}
|
|
}
|
|
|
|
extension NavigationSettings.Placement {
|
|
var label: LocalizedStringKey {
|
|
switch self {
|
|
case .side: "appearance.navigationSide"
|
|
case .top: "appearance.navigationTop"
|
|
}
|
|
}
|
|
}
|