416 lines
14 KiB
Swift
416 lines
14 KiB
Swift
//
|
||
// 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("24‑Hour 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 }
|
||
)
|
||
}
|