Files
yattee/Yattee/Views/Settings/AppearanceSettingsView.swift
2026-02-08 18:33:56 +01:00

260 lines
7.9 KiB
Swift

//
// AppearanceSettingsView.swift
// Yattee
//
// Appearance settings with theme and accent color selection.
//
import SwiftUI
struct AppearanceSettingsView: View {
@Environment(\.appEnvironment) private var appEnvironment
var body: some View {
Form {
if let settings = appEnvironment?.settingsManager {
// Theme section
ThemeSection(settings: settings)
// App icon section (iOS only)
#if os(iOS)
AppIconSection(settings: settings)
#endif
// Accent color section
AccentColorSection(settings: settings)
// List style section
ListStyleSection(settings: settings)
// Thumbnail section
ThumbnailSection(settings: settings)
}
}
.navigationTitle(String(localized: "settings.appearance.title"))
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
// MARK: - Theme Section
private struct ThemeSection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section(String(localized: "settings.appearance.theme.header")) {
Picker(
String(localized: "settings.appearance.theme"),
selection: $settings.theme
) {
ForEach(AppTheme.allCases, id: \.self) { theme in
Text(theme.displayName).tag(theme)
}
}
.pickerStyle(.segmented)
}
}
}
// MARK: - App Icon Section (iOS only)
#if os(iOS)
private struct AppIconSection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section(String(localized: "settings.appearance.appIcon.header")) {
NavigationLink {
AppIconPickerView(settings: settings)
} label: {
HStack {
Image(settings.appIcon.previewImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 44, height: 44)
.clipShape(RoundedRectangle(cornerRadius: 10))
Text(settings.appIcon.displayName)
}
}
}
}
}
private struct AppIconPickerView: View {
@Bindable var settings: SettingsManager
var body: some View {
List {
ForEach(AppIcon.allCases, id: \.self) { appIcon in
Button {
settings.appIcon = appIcon
} label: {
HStack {
Image(appIcon.previewImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 13.5))
VStack(alignment: .leading, spacing: 2) {
Text(appIcon.displayName)
.foregroundStyle(.primary)
if let author = appIcon.author {
Text(author)
.font(.caption)
.foregroundStyle(.secondary)
}
}
Spacer()
if settings.appIcon == appIcon {
Image(systemName: "checkmark")
.foregroundStyle(.blue)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
.navigationTitle(String(localized: "settings.appearance.appIcon.header"))
.navigationBarTitleDisplayMode(.inline)
}
}
#endif
// MARK: - Accent Color Section
private struct AccentColorSection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section(String(localized: "settings.appearance.accentColor.header")) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 50))], spacing: 16) {
ForEach(AccentColor.allCases, id: \.self) { accentColor in
AccentColorButton(
accentColor: accentColor,
isSelected: settings.accentColor == accentColor,
onSelect: { settings.accentColor = accentColor }
)
}
}
.padding(.vertical, 8)
}
}
}
// MARK: - List Style Section
private struct ListStyleSection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section {
Picker(selection: $settings.listStyle) {
ForEach(VideoListStyle.allCases, id: \.self) { style in
Text(style.displayName).tag(style)
}
} label: {
Label(String(localized: "settings.appearance.listStyle"), systemImage: "list.bullet")
}
} header: {
Text(String(localized: "settings.appearance.listStyle.header"))
}
}
}
// MARK: - Thumbnail Section
private struct ThumbnailSection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section {
Toggle(isOn: $settings.showWatchedCheckmark) {
Label(String(localized: "settings.appearance.showWatchedCheckmark"),
systemImage: "checkmark.circle.fill")
}
} header: {
Text(String(localized: "settings.appearance.thumbnails.header"))
}
}
}
// MARK: - Accent Color Button
private struct AccentColorButton: View {
let accentColor: AccentColor
let isSelected: Bool
let onSelect: () -> Void
var body: some View {
Button(action: onSelect) {
ZStack {
Circle()
.fill(accentColor.color)
.frame(width: 40, height: 40)
if isSelected {
Circle()
.strokeBorder(.white, lineWidth: 3)
.frame(width: 40, height: 40)
Image(systemName: "checkmark")
.font(.caption.bold())
.foregroundStyle(.white)
}
}
}
.buttonStyle(.plain)
.accessibilityLabel(accentColor.displayName)
}
}
// MARK: - Theme Display Names
extension AppTheme {
var displayName: String {
switch self {
case .system: return String(localized: "settings.appearance.theme.system")
case .light: return String(localized: "settings.appearance.theme.light")
case .dark: return String(localized: "settings.appearance.theme.dark")
}
}
}
// MARK: - Ambient Glow Section
// MARK: - Accent Color Display Names
extension AccentColor {
var displayName: String {
switch self {
case .default: return String(localized: "settings.appearance.accentColor.default")
case .red: return String(localized: "settings.appearance.accentColor.red")
case .pink: return String(localized: "settings.appearance.accentColor.pink")
case .orange: return String(localized: "settings.appearance.accentColor.orange")
case .yellow: return String(localized: "settings.appearance.accentColor.yellow")
case .green: return String(localized: "settings.appearance.accentColor.green")
case .teal: return String(localized: "settings.appearance.accentColor.teal")
case .blue: return String(localized: "settings.appearance.accentColor.blue")
case .purple: return String(localized: "settings.appearance.accentColor.purple")
case .indigo: return String(localized: "settings.appearance.accentColor.indigo")
}
}
}
// MARK: - Preview
#Preview {
NavigationStack {
AppearanceSettingsView()
}
.appEnvironment(.preview)
}