TheNoiseClock/TheNoiseClock/Views/Clock/ClockSettingsView.swift

416 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ClockSettingsView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
/// Settings interface for clock customization
struct ClockSettingsView: View {
// MARK: - Properties
@State private var style: ClockStyle
let onCommit: (ClockStyle) -> Void
@State private var digitColor: Color = .white
@State private var backgroundColor: Color = .black
@State private var showAdvancedSettings = false
// MARK: - Init
init(style: ClockStyle, onCommit: @escaping (ClockStyle) -> Void) {
self._style = State(initialValue: style)
self.onCommit = onCommit
}
// MARK: - Body
var body: some View {
NavigationView {
Form {
// BASIC SETTINGS - Most commonly used
BasicAppearanceSection(
style: $style,
digitColor: $digitColor,
backgroundColor: $backgroundColor,
onCommit: onCommit
)
BasicDisplaySection(style: $style)
// ADVANCED SETTINGS - Toggle to show/hide
if showAdvancedSettings {
AdvancedAppearanceSection(
style: $style,
digitColor: $digitColor,
backgroundColor: $backgroundColor,
onCommit: onCommit
)
FontSection(style: $style)
NightModeSection(style: $style)
OverlaySection(style: $style)
AdvancedDisplaySection(style: $style)
}
// TOGGLE FOR ADVANCED SETTINGS
Section {
Toggle("Show Advanced Settings", isOn: $showAdvancedSettings)
}
}
.navigationTitle("Clock Settings")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
digitColor = Color(hex: style.digitColorHex) ?? .white
backgroundColor = Color(hex: style.backgroundHex) ?? .black
}
.onDisappear {
onCommit(style)
}
}
}
}
// MARK: - Supporting Views
// MARK: - Basic Settings Sections
private struct BasicAppearanceSection: View {
@Binding var style: ClockStyle
@Binding var digitColor: Color
@Binding var backgroundColor: Color
let onCommit: (ClockStyle) -> Void
var body: some View {
Section(header: Text("Colors"), footer: Text("Choose your favorite color theme or create a custom look.")) {
// Color Theme Picker
Picker("Color Theme", selection: $style.selectedColorTheme) {
ForEach(ClockStyle.availableColorThemes(), id: \.0) { theme in
HStack {
Circle()
.fill(themeColor(for: theme.0))
.frame(width: 20, height: 20)
Text(theme.1)
}
.tag(theme.0)
}
}
.pickerStyle(.menu)
.onChange(of: style.selectedColorTheme) { _, newTheme in
if newTheme != "Custom" {
style.applyColorTheme(newTheme)
digitColor = Color(hex: style.digitColorHex) ?? .white
backgroundColor = Color(hex: style.backgroundHex) ?? .black
}
}
// Custom color pickers (only show if Custom is selected)
if style.selectedColorTheme == "Custom" {
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true)
}
}
.onChange(of: backgroundColor) { _, newValue in
style.backgroundHex = newValue.toHex() ?? AppConstants.Defaults.backgroundColorHex
style.selectedColorTheme = "Custom"
style.clearColorCache()
}
.onChange(of: digitColor) { _, newValue in
style.digitColorHex = newValue.toHex() ?? AppConstants.Defaults.digitColorHex
style.selectedColorTheme = "Custom"
style.clearColorCache()
}
}
/// Get the color for a theme
private func themeColor(for theme: String) -> Color {
switch theme {
case "Custom":
return .gray
case "Red":
return .red
case "Orange":
return .orange
case "Yellow":
return .yellow
case "Green":
return .green
case "Blue":
return .blue
case "Purple":
return .purple
case "Pink":
return .pink
case "White":
return .white
default:
return .gray
}
}
}
private struct BasicDisplaySection: View {
@Binding var style: ClockStyle
var body: some View {
Section(header: Text("Display"), footer: Text("Basic display settings for your clock.")) {
Toggle("24Hour Format", isOn: $style.use24Hour)
Toggle("Show Seconds", isOn: $style.showSeconds)
Toggle("Auto Brightness", isOn: $style.autoBrightness)
// Only show horizontal mode option in portrait orientation
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
Toggle("Horizontal Mode", isOn: $style.forceHorizontalMode)
}
}
}
}
// MARK: - Advanced Settings Sections
private struct AdvancedAppearanceSection: View {
@Binding var style: ClockStyle
@Binding var digitColor: Color
@Binding var backgroundColor: Color
let onCommit: (ClockStyle) -> Void
var body: some View {
Section(header: Text("Advanced Appearance"), footer: Text("Fine-tune the visual appearance of your clock.")) {
Toggle("Randomize Color (every minute)", isOn: $style.randomizeColor)
Toggle("Stretched (auto-fit)", isOn: $style.stretched)
if !style.stretched {
HStack {
Text("Size")
Slider(value: $style.digitScale, in: 0.0...1.0)
Text("\(Int((min(max(style.digitScale, 0.0), 1.0)) * 100))%")
.frame(width: 50, alignment: .trailing)
}
}
HStack {
Text("Glow")
Slider(value: $style.glowIntensity, in: 0...1)
Text("\(Int(style.glowIntensity * 100))%")
.frame(width: 50, alignment: .trailing)
}
HStack {
Text("Clock Opacity")
Slider(value: $style.clockOpacity, in: 0.0...1.0)
Text("\(Int(style.clockOpacity * 100))%")
.frame(width: 50, alignment: .trailing)
}
}
}
}
private struct AdvancedDisplaySection: View {
@Binding var style: ClockStyle
var body: some View {
Section(header: Text("Advanced Display"), footer: Text("Advanced display and system integration settings.")) {
Toggle("Keep Awake in Display Mode", isOn: $style.keepAwake)
if style.autoBrightness {
HStack {
Text("Current Brightness")
Spacer()
Text("\(Int(style.effectiveBrightness * 100))%")
.foregroundColor(.secondary)
}
}
}
Section(header: Text("Focus Modes"), footer: Text("Control how the app behaves when Focus modes (Do Not Disturb) are active.")) {
Toggle("Respect Focus Modes", isOn: $style.respectFocusModes)
}
}
}
private struct FontSection: View {
@Binding var style: ClockStyle
private let fontFamilies = ["System", "Arial", "Courier", "Georgia", "Helvetica", "Monaco", "Times New Roman", "Verdana"]
private let fontWeights = ["Ultra Light", "Thin", "Light", "Regular", "Medium", "Semibold", "Bold", "Heavy", "Black"]
private let fontDesigns = ["Default", "Serif", "Rounded", "Monospaced"]
var body: some View {
Section(header: Text("Font")) {
// Font Family
Picker("Family", selection: $style.fontFamily) {
ForEach(fontFamilies, id: \.self) { family in
Text(family).tag(family)
}
}
.pickerStyle(.menu)
// Font Weight
Picker("Weight", selection: $style.fontWeight) {
ForEach(fontWeights, id: \.self) { weight in
Text(weight).tag(weight)
}
}
.pickerStyle(.menu)
// Font Design
Picker("Design", selection: $style.fontDesign) {
ForEach(fontDesigns, id: \.self) { design in
Text(design).tag(design)
}
}
.pickerStyle(.menu)
// Font Preview
HStack {
Text("Preview:")
.foregroundColor(.secondary)
Spacer()
Text("12:34")
.font(FontUtils.customFont(
size: 24,
family: style.fontFamily,
weight: style.fontWeight,
design: style.fontDesign
))
.foregroundColor(.primary)
}
}
}
}
private struct NightModeSection: View {
@Binding var style: ClockStyle
var body: some View {
Section(header: Text("Night Mode"), footer: Text("Night mode displays the clock in red to reduce eye strain in low light environments.")) {
Toggle("Enable Night Mode", isOn: $style.nightModeEnabled)
Toggle("Auto Night Mode", isOn: $style.autoNightMode)
if style.autoNightMode {
HStack {
Text("Light Threshold")
Spacer()
Slider(value: $style.ambientLightThreshold, in: 0.1...0.8)
Text("\(Int(style.ambientLightThreshold * 100))%")
.frame(width: 50, alignment: .trailing)
}
}
Toggle("Scheduled Night Mode", isOn: $style.scheduledNightMode)
if style.scheduledNightMode {
HStack {
Text("Start Time")
Spacer()
TimePickerView(timeString: $style.nightModeStartTime)
}
HStack {
Text("End Time")
Spacer()
TimePickerView(timeString: $style.nightModeEndTime)
}
}
if style.isNightModeActive {
HStack {
Image(systemName: "moon.fill")
.foregroundColor(.red)
Text("Night Mode Active")
.foregroundColor(.red)
Spacer()
}
}
}
}
}
private struct OverlaySection: View {
@Binding var style: ClockStyle
private let dateFormats = Date.availableDateFormats()
var body: some View {
Section(header: Text("Overlays")) {
HStack {
Text("Overlay Opacity")
Slider(value: $style.overlayOpacity, in: 0.0...1.0)
Text("\(Int(style.overlayOpacity * 100))%")
.frame(width: 50, alignment: .trailing)
}
Toggle("Battery Level", isOn: $style.showBattery)
Toggle("Date", isOn: $style.showDate)
if style.showDate {
Picker("Date Format", selection: $style.dateFormat) {
ForEach(dateFormats, id: \.1) { format in
Text(format.0).tag(format.1)
}
}
.pickerStyle(.menu)
}
}
}
}
private struct TimePickerView: View {
@Binding var timeString: String
@State private var selectedTime = Date()
var body: some View {
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
.labelsHidden()
.onAppear {
updateSelectedTimeFromString()
}
.onChange(of: selectedTime) { _, newTime in
updateStringFromTime(newTime)
}
.onChange(of: timeString) { _, _ in
updateSelectedTimeFromString()
}
}
private func updateSelectedTimeFromString() {
let components = timeString.split(separator: ":")
guard components.count == 2,
let hour = Int(components[0]),
let minute = Int(components[1]) else {
return
}
let calendar = Calendar.current
let now = Date()
let dateComponents = calendar.dateComponents([.year, .month, .day], from: now)
var newComponents = dateComponents
newComponents.hour = hour
newComponents.minute = minute
if let newDate = calendar.date(from: newComponents) {
selectedTime = newDate
}
}
private func updateStringFromTime(_ time: Date) {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
timeString = formatter.string(from: time)
}
}
// MARK: - Preview
#Preview {
ClockSettingsView(
style: ClockStyle(),
onCommit: { _ in }
)
}