Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
9ae5aef89b
commit
0909f93368
11
PRD.md
11
PRD.md
@ -397,7 +397,16 @@ TheNoiseClock/
|
|||||||
│ │ │ ├── DotCircle.swift # Individual dot component for colons
|
│ │ │ ├── DotCircle.swift # Individual dot component for colons
|
||||||
│ │ │ ├── BatteryOverlayView.swift # Battery level overlay
|
│ │ │ ├── BatteryOverlayView.swift # Battery level overlay
|
||||||
│ │ │ ├── DateOverlayView.swift # Date display overlay
|
│ │ │ ├── DateOverlayView.swift # Date display overlay
|
||||||
│ │ │ └── TopOverlayView.swift # Combined overlay container
|
│ │ │ ├── TopOverlayView.swift # Combined overlay container
|
||||||
|
│ │ │ └── Settings/
|
||||||
|
│ │ │ ├── BasicAppearanceSection.swift
|
||||||
|
│ │ │ ├── BasicDisplaySection.swift
|
||||||
|
│ │ │ ├── AdvancedAppearanceSection.swift
|
||||||
|
│ │ │ ├── AdvancedDisplaySection.swift
|
||||||
|
│ │ │ ├── FontSection.swift
|
||||||
|
│ │ │ ├── NightModeSection.swift
|
||||||
|
│ │ │ ├── OverlaySection.swift
|
||||||
|
│ │ │ └── TimePickerView.swift
|
||||||
│ │ ├── Alarms/
|
│ │ ├── Alarms/
|
||||||
│ │ │ ├── AlarmView.swift # Main alarm management view
|
│ │ │ ├── AlarmView.swift # Main alarm management view
|
||||||
│ │ │ ├── AddAlarmView.swift # Alarm creation interface
|
│ │ │ ├── AddAlarmView.swift # Alarm creation interface
|
||||||
|
|||||||
@ -175,6 +175,7 @@ private struct TestContentView: View {
|
|||||||
TimeDisplayView(date: newDate,
|
TimeDisplayView(date: newDate,
|
||||||
use24Hour: true,
|
use24Hour: true,
|
||||||
showSeconds: false,
|
showSeconds: false,
|
||||||
|
showAmPm: true,
|
||||||
digitColor: .primary,
|
digitColor: .primary,
|
||||||
glowIntensity: 0.5,
|
glowIntensity: 0.5,
|
||||||
manualScale: 1.0,
|
manualScale: 1.0,
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
// MARK: - Time Format Settings
|
// MARK: - Time Format Settings
|
||||||
var use24Hour: Bool = true
|
var use24Hour: Bool = true
|
||||||
var showSeconds: Bool = false
|
var showSeconds: Bool = false
|
||||||
|
var showAmPm: Bool = true
|
||||||
var forceHorizontalMode: Bool = false // Force horizontal layout even in portrait
|
var forceHorizontalMode: Bool = false // Force horizontal layout even in portrait
|
||||||
|
|
||||||
// MARK: - Visual Settings
|
// MARK: - Visual Settings
|
||||||
@ -26,7 +27,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
var backgroundHex: String = AppConstants.Defaults.backgroundColorHex
|
var backgroundHex: String = AppConstants.Defaults.backgroundColorHex
|
||||||
|
|
||||||
// MARK: - Color Theme Settings
|
// MARK: - Color Theme Settings
|
||||||
var selectedColorTheme: String = "Custom" // Custom, Red, Orange, Yellow, Green, Blue, Purple, Pink, White
|
var selectedColorTheme: String = "Custom" // Custom, Night, Day, Red, Orange, Yellow, Green, Blue, Purple, Pink, White
|
||||||
|
|
||||||
// MARK: - Night Mode Settings
|
// MARK: - Night Mode Settings
|
||||||
var nightModeEnabled: Bool = false
|
var nightModeEnabled: Bool = false
|
||||||
@ -61,6 +62,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case use24Hour
|
case use24Hour
|
||||||
case showSeconds
|
case showSeconds
|
||||||
|
case showAmPm
|
||||||
case forceHorizontalMode
|
case forceHorizontalMode
|
||||||
case digitColorHex
|
case digitColorHex
|
||||||
case randomizeColor
|
case randomizeColor
|
||||||
@ -99,6 +101,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
|
|
||||||
self.use24Hour = try container.decodeIfPresent(Bool.self, forKey: .use24Hour) ?? self.use24Hour
|
self.use24Hour = try container.decodeIfPresent(Bool.self, forKey: .use24Hour) ?? self.use24Hour
|
||||||
self.showSeconds = try container.decodeIfPresent(Bool.self, forKey: .showSeconds) ?? self.showSeconds
|
self.showSeconds = try container.decodeIfPresent(Bool.self, forKey: .showSeconds) ?? self.showSeconds
|
||||||
|
self.showAmPm = try container.decodeIfPresent(Bool.self, forKey: .showAmPm) ?? self.showAmPm
|
||||||
self.forceHorizontalMode = try container.decodeIfPresent(Bool.self, forKey: .forceHorizontalMode) ?? self.forceHorizontalMode
|
self.forceHorizontalMode = try container.decodeIfPresent(Bool.self, forKey: .forceHorizontalMode) ?? self.forceHorizontalMode
|
||||||
self.digitColorHex = try container.decodeIfPresent(String.self, forKey: .digitColorHex) ?? self.digitColorHex
|
self.digitColorHex = try container.decodeIfPresent(String.self, forKey: .digitColorHex) ?? self.digitColorHex
|
||||||
self.randomizeColor = try container.decodeIfPresent(Bool.self, forKey: .randomizeColor) ?? self.randomizeColor
|
self.randomizeColor = try container.decodeIfPresent(Bool.self, forKey: .randomizeColor) ?? self.randomizeColor
|
||||||
@ -142,6 +145,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(use24Hour, forKey: .use24Hour)
|
try container.encode(use24Hour, forKey: .use24Hour)
|
||||||
try container.encode(showSeconds, forKey: .showSeconds)
|
try container.encode(showSeconds, forKey: .showSeconds)
|
||||||
|
try container.encode(showAmPm, forKey: .showAmPm)
|
||||||
try container.encode(forceHorizontalMode, forKey: .forceHorizontalMode)
|
try container.encode(forceHorizontalMode, forKey: .forceHorizontalMode)
|
||||||
try container.encode(digitColorHex, forKey: .digitColorHex)
|
try container.encode(digitColorHex, forKey: .digitColorHex)
|
||||||
try container.encode(randomizeColor, forKey: .randomizeColor)
|
try container.encode(randomizeColor, forKey: .randomizeColor)
|
||||||
@ -199,6 +203,12 @@ class ClockStyle: Codable, Equatable {
|
|||||||
selectedColorTheme = theme
|
selectedColorTheme = theme
|
||||||
|
|
||||||
switch theme {
|
switch theme {
|
||||||
|
case "Night":
|
||||||
|
digitColorHex = "#FFFFFF"
|
||||||
|
backgroundHex = "#000000"
|
||||||
|
case "Day":
|
||||||
|
digitColorHex = "#000000"
|
||||||
|
backgroundHex = "#FFFFFF"
|
||||||
case "Red":
|
case "Red":
|
||||||
digitColorHex = "#FF3B30"
|
digitColorHex = "#FF3B30"
|
||||||
backgroundHex = "#000000"
|
backgroundHex = "#000000"
|
||||||
@ -235,6 +245,8 @@ class ClockStyle: Codable, Equatable {
|
|||||||
static func availableColorThemes() -> [(String, String)] {
|
static func availableColorThemes() -> [(String, String)] {
|
||||||
return [
|
return [
|
||||||
("Custom", "Custom"),
|
("Custom", "Custom"),
|
||||||
|
("Night", "Night"),
|
||||||
|
("Day", "Day"),
|
||||||
("Red", "Red"),
|
("Red", "Red"),
|
||||||
("Orange", "Orange"),
|
("Orange", "Orange"),
|
||||||
("Yellow", "Yellow"),
|
("Yellow", "Yellow"),
|
||||||
@ -357,6 +369,10 @@ class ClockStyle: Codable, Equatable {
|
|||||||
/// Get base brightness recommendation for current color theme
|
/// Get base brightness recommendation for current color theme
|
||||||
private func getBaseBrightnessForColor() -> Double {
|
private func getBaseBrightnessForColor() -> Double {
|
||||||
switch selectedColorTheme {
|
switch selectedColorTheme {
|
||||||
|
case "Night":
|
||||||
|
return 0.8
|
||||||
|
case "Day":
|
||||||
|
return 0.5
|
||||||
case "Red", "Orange":
|
case "Red", "Orange":
|
||||||
return 0.6 // Warmer colors work well at lower brightness
|
return 0.6 // Warmer colors work well at lower brightness
|
||||||
case "Yellow", "White":
|
case "Yellow", "White":
|
||||||
@ -423,6 +439,7 @@ class ClockStyle: Codable, Equatable {
|
|||||||
static func == (lhs: ClockStyle, rhs: ClockStyle) -> Bool {
|
static func == (lhs: ClockStyle, rhs: ClockStyle) -> Bool {
|
||||||
lhs.use24Hour == rhs.use24Hour &&
|
lhs.use24Hour == rhs.use24Hour &&
|
||||||
lhs.showSeconds == rhs.showSeconds &&
|
lhs.showSeconds == rhs.showSeconds &&
|
||||||
|
lhs.showAmPm == rhs.showAmPm &&
|
||||||
lhs.forceHorizontalMode == rhs.forceHorizontalMode &&
|
lhs.forceHorizontalMode == rhs.forceHorizontalMode &&
|
||||||
lhs.digitColorHex == rhs.digitColorHex &&
|
lhs.digitColorHex == rhs.digitColorHex &&
|
||||||
lhs.randomizeColor == rhs.randomizeColor &&
|
lhs.randomizeColor == rhs.randomizeColor &&
|
||||||
|
|||||||
@ -81,6 +81,7 @@ class AlarmService {
|
|||||||
// Schedule new notification if enabled
|
// Schedule new notification if enabled
|
||||||
if alarm.isEnabled {
|
if alarm.isEnabled {
|
||||||
Task {
|
Task {
|
||||||
|
let respectFocusModes = currentRespectFocusModes()
|
||||||
// Use FocusModeService for better Focus mode compatibility
|
// Use FocusModeService for better Focus mode compatibility
|
||||||
focusModeService.scheduleAlarmNotification(
|
focusModeService.scheduleAlarmNotification(
|
||||||
identifier: alarm.id.uuidString,
|
identifier: alarm.id.uuidString,
|
||||||
@ -88,12 +89,21 @@ class AlarmService {
|
|||||||
body: alarm.notificationMessage,
|
body: alarm.notificationMessage,
|
||||||
date: alarm.time,
|
date: alarm.time,
|
||||||
soundName: alarm.soundName,
|
soundName: alarm.soundName,
|
||||||
repeats: false // For now, set to false since Alarm model doesn't have repeatDays
|
repeats: false, // For now, set to false since Alarm model doesn't have repeatDays
|
||||||
|
respectFocusModes: respectFocusModes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func currentRespectFocusModes() -> Bool {
|
||||||
|
guard let data = UserDefaults.standard.data(forKey: ClockStyle.appStorageKey),
|
||||||
|
let style = try? JSONDecoder().decode(ClockStyle.self, from: data) else {
|
||||||
|
return ClockStyle().respectFocusModes
|
||||||
|
}
|
||||||
|
return style.respectFocusModes
|
||||||
|
}
|
||||||
|
|
||||||
private func saveAlarms() {
|
private func saveAlarms() {
|
||||||
persistenceWorkItem?.cancel()
|
persistenceWorkItem?.cancel()
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UserNotifications
|
|
||||||
import Observation
|
import Observation
|
||||||
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
/// Service to handle Focus mode interactions and ensure app functionality
|
/// Service to align notifications with Focus mode behavior
|
||||||
@Observable
|
@Observable
|
||||||
class FocusModeService {
|
class FocusModeService {
|
||||||
|
|
||||||
@ -17,9 +18,10 @@ class FocusModeService {
|
|||||||
static let shared = FocusModeService()
|
static let shared = FocusModeService()
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
private(set) var isFocusModeActive = false
|
private(set) var notificationAuthorizationStatus: UNAuthorizationStatus = .notDetermined
|
||||||
private(set) var currentFocusMode: String?
|
private(set) var timeSensitiveSetting: UNNotificationSetting = .notSupported
|
||||||
private var focusModeObserver: NSObjectProtocol?
|
private(set) var scheduledDeliverySetting: UNNotificationSetting = .notSupported
|
||||||
|
private var notificationSettingsObserver: NSObjectProtocol?
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
private init() {
|
private init() {
|
||||||
@ -33,13 +35,8 @@ class FocusModeService {
|
|||||||
// MARK: - Public Interface
|
// MARK: - Public Interface
|
||||||
|
|
||||||
/// Check if Focus mode is currently active
|
/// Check if Focus mode is currently active
|
||||||
var isActive: Bool {
|
var isAuthorized: Bool {
|
||||||
return isFocusModeActive
|
notificationAuthorizationStatus == .authorized
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current Focus mode name if available
|
|
||||||
var activeFocusMode: String? {
|
|
||||||
return currentFocusMode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request notification permissions that work with Focus modes
|
/// Request notification permissions that work with Focus modes
|
||||||
@ -54,6 +51,8 @@ class FocusModeService {
|
|||||||
await configureNotificationSettings()
|
await configureNotificationSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await refreshNotificationSettings()
|
||||||
|
|
||||||
return granted
|
return granted
|
||||||
} catch {
|
} catch {
|
||||||
DebugLogger.log("Error requesting notification permissions: \(error)", category: .general)
|
DebugLogger.log("Error requesting notification permissions: \(error)", category: .general)
|
||||||
@ -95,7 +94,8 @@ class FocusModeService {
|
|||||||
body: String,
|
body: String,
|
||||||
date: Date,
|
date: Date,
|
||||||
soundName: String,
|
soundName: String,
|
||||||
repeats: Bool = false
|
repeats: Bool = false,
|
||||||
|
respectFocusModes: Bool = true
|
||||||
) {
|
) {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = title
|
content.title = title
|
||||||
@ -110,6 +110,10 @@ class FocusModeService {
|
|||||||
DebugLogger.log("Sound file should be in main bundle: \(soundName)", category: .settings)
|
DebugLogger.log("Sound file should be in main bundle: \(soundName)", category: .settings)
|
||||||
}
|
}
|
||||||
content.categoryIdentifier = "ALARM_CATEGORY"
|
content.categoryIdentifier = "ALARM_CATEGORY"
|
||||||
|
|
||||||
|
if !respectFocusModes, timeSensitiveSetting == .enabled {
|
||||||
|
content.interruptionLevel = .timeSensitive
|
||||||
|
}
|
||||||
content.userInfo = [
|
content.userInfo = [
|
||||||
"alarmId": identifier,
|
"alarmId": identifier,
|
||||||
"soundName": soundName,
|
"soundName": soundName,
|
||||||
@ -162,45 +166,37 @@ class FocusModeService {
|
|||||||
|
|
||||||
/// Set up monitoring for Focus mode changes
|
/// Set up monitoring for Focus mode changes
|
||||||
private func setupFocusModeMonitoring() {
|
private func setupFocusModeMonitoring() {
|
||||||
// Monitor notification center for Focus mode changes
|
notificationSettingsObserver = NotificationCenter.default.addObserver(
|
||||||
focusModeObserver = NotificationCenter.default.addObserver(
|
forName: UIApplication.willEnterForegroundNotification,
|
||||||
forName: .NSSystemTimeZoneDidChange,
|
|
||||||
object: nil,
|
object: nil,
|
||||||
queue: .main
|
queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
self?.updateFocusModeStatus()
|
Task { await self?.refreshNotificationSettings() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial status check
|
Task { await refreshNotificationSettings() }
|
||||||
updateFocusModeStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove Focus mode observer
|
/// Remove Focus mode observer
|
||||||
private func removeFocusModeObserver() {
|
private func removeFocusModeObserver() {
|
||||||
if let observer = focusModeObserver {
|
if let observer = notificationSettingsObserver {
|
||||||
NotificationCenter.default.removeObserver(observer)
|
NotificationCenter.default.removeObserver(observer)
|
||||||
focusModeObserver = nil
|
notificationSettingsObserver = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update Focus mode status
|
/// Refresh notification settings to align with Focus mode behavior.
|
||||||
private func updateFocusModeStatus() {
|
@MainActor
|
||||||
// Check if Focus mode is active by examining notification settings
|
func refreshNotificationSettings() async {
|
||||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||||
DispatchQueue.main.async {
|
notificationAuthorizationStatus = settings.authorizationStatus
|
||||||
// This is a simplified check - in a real implementation,
|
timeSensitiveSetting = settings.timeSensitiveSetting
|
||||||
// you might need to use private APIs or other methods
|
scheduledDeliverySetting = settings.scheduledDeliverySetting
|
||||||
// to detect Focus mode status
|
|
||||||
self.isFocusModeActive = settings.authorizationStatus == .authorized
|
|
||||||
self.currentFocusMode = self.isFocusModeActive ? "Active" : nil
|
|
||||||
|
|
||||||
if self.isFocusModeActive {
|
DebugLogger.log(
|
||||||
DebugLogger.log("Focus mode is active", category: .settings)
|
"Notification settings updated: auth=\(settings.authorizationStatus), timeSensitive=\(settings.timeSensitiveSetting), scheduledDelivery=\(settings.scheduledDeliverySetting)",
|
||||||
} else {
|
category: .settings
|
||||||
DebugLogger.log("Focus mode is not active", category: .settings)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get notification authorization status
|
/// Get notification authorization status
|
||||||
|
|||||||
@ -74,6 +74,7 @@ class ClockViewModel {
|
|||||||
// This preserves the @Observable chain
|
// This preserves the @Observable chain
|
||||||
style.use24Hour = newStyle.use24Hour
|
style.use24Hour = newStyle.use24Hour
|
||||||
style.showSeconds = newStyle.showSeconds
|
style.showSeconds = newStyle.showSeconds
|
||||||
|
style.showAmPm = newStyle.showAmPm
|
||||||
style.forceHorizontalMode = newStyle.forceHorizontalMode
|
style.forceHorizontalMode = newStyle.forceHorizontalMode
|
||||||
style.digitColorHex = newStyle.digitColorHex
|
style.digitColorHex = newStyle.digitColorHex
|
||||||
style.glowIntensity = newStyle.glowIntensity
|
style.glowIntensity = newStyle.glowIntensity
|
||||||
|
|||||||
@ -74,362 +74,6 @@ struct ClockSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// Use the enum allCases for font options
|
|
||||||
|
|
||||||
// Computed property for available weights based on selected font
|
|
||||||
private var availableWeights: [Font.Weight] {
|
|
||||||
if style.fontFamily == .system {
|
|
||||||
return Font.Weight.allCases
|
|
||||||
} else {
|
|
||||||
return style.fontFamily.fontWeights
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed property for sorted font families (System first, then alphabetical)
|
|
||||||
private var sortedFontFamilies: [FontFamily] {
|
|
||||||
let allFamilies = FontFamily.allCases
|
|
||||||
let systemFamily = allFamilies.filter { $0 == .system }
|
|
||||||
let otherFamilies = allFamilies.filter { $0 != .system }.sorted { $0.rawValue < $1.rawValue }
|
|
||||||
return systemFamily + otherFamilies
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Section(header: Text("Font")) {
|
|
||||||
// Font Family
|
|
||||||
Picker("Family", selection: $style.fontFamily) {
|
|
||||||
ForEach(sortedFontFamilies, id: \.self) { family in
|
|
||||||
Text(family.rawValue).tag(family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
.onChange(of: style.fontFamily) { _, newFamily in
|
|
||||||
// Auto-set design to default for non-system fonts
|
|
||||||
if newFamily != .system {
|
|
||||||
style.fontDesign = .default
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-set weight to first available weight if current weight is not available
|
|
||||||
let availableWeights = newFamily == .system ? Font.Weight.allCases : newFamily.fontWeights
|
|
||||||
if !availableWeights.contains(style.fontWeight) {
|
|
||||||
style.fontWeight = availableWeights.first ?? .regular
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font Weight - show available weights for selected font
|
|
||||||
Picker("Weight", selection: $style.fontWeight) {
|
|
||||||
ForEach(availableWeights, id: \.self) { weight in
|
|
||||||
Text(weight.rawValue).tag(weight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
|
|
||||||
// Font Design - only show for system font
|
|
||||||
if style.fontFamily == .system {
|
|
||||||
Picker("Design", selection: $style.fontDesign) {
|
|
||||||
ForEach(Font.Design.allCases, id: \.self) { design in
|
|
||||||
Text(design.rawValue).tag(design)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font Preview
|
|
||||||
HStack {
|
|
||||||
Text("Preview:")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Spacer()
|
|
||||||
Text("12:34")
|
|
||||||
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
|
|
||||||
.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
|
// MARK: - Preview
|
||||||
#Preview {
|
#Preview {
|
||||||
ClockSettingsView(
|
ClockSettingsView(
|
||||||
|
|||||||
@ -34,6 +34,7 @@ struct ClockDisplayContainer: View {
|
|||||||
date: currentTime,
|
date: currentTime,
|
||||||
use24Hour: style.use24Hour,
|
use24Hour: style.use24Hour,
|
||||||
showSeconds: style.showSeconds,
|
showSeconds: style.showSeconds,
|
||||||
|
showAmPm: style.showAmPm,
|
||||||
digitColor: style.effectiveDigitColor,
|
digitColor: style.effectiveDigitColor,
|
||||||
glowIntensity: style.glowIntensity,
|
glowIntensity: style.glowIntensity,
|
||||||
manualScale: style.digitScale,
|
manualScale: style.digitScale,
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// AdvancedAppearanceSection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// AdvancedDisplaySection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// BasicAppearanceSection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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 "Night":
|
||||||
|
return .white
|
||||||
|
case "Day":
|
||||||
|
return .black
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// BasicDisplaySection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if !style.use24Hour {
|
||||||
|
Toggle("Show AM/PM", isOn: $style.showAmPm)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// FontSection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FontSection: View {
|
||||||
|
@Binding var style: ClockStyle
|
||||||
|
|
||||||
|
// Computed property for available weights based on selected font
|
||||||
|
private var availableWeights: [Font.Weight] {
|
||||||
|
if style.fontFamily == .system {
|
||||||
|
return Font.Weight.allCases
|
||||||
|
} else {
|
||||||
|
return style.fontFamily.fontWeights
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed property for sorted font families (System first, then alphabetical)
|
||||||
|
private var sortedFontFamilies: [FontFamily] {
|
||||||
|
let allFamilies = FontFamily.allCases
|
||||||
|
let systemFamily = allFamilies.filter { $0 == .system }
|
||||||
|
let otherFamilies = allFamilies.filter { $0 != .system }.sorted { $0.rawValue < $1.rawValue }
|
||||||
|
return systemFamily + otherFamilies
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section(header: Text("Font")) {
|
||||||
|
// Font Family
|
||||||
|
Picker("Family", selection: $style.fontFamily) {
|
||||||
|
ForEach(sortedFontFamilies, id: \.self) { family in
|
||||||
|
Text(family.rawValue).tag(family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.onChange(of: style.fontFamily) { _, newFamily in
|
||||||
|
// Auto-set design to default for non-system fonts
|
||||||
|
if newFamily != .system {
|
||||||
|
style.fontDesign = .default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-set weight to first available weight if current weight is not available
|
||||||
|
let weights = newFamily == .system ? Font.Weight.allCases : newFamily.fontWeights
|
||||||
|
if !weights.contains(style.fontWeight) {
|
||||||
|
style.fontWeight = weights.first ?? .regular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font Weight - show available weights for selected font
|
||||||
|
Picker("Weight", selection: $style.fontWeight) {
|
||||||
|
ForEach(availableWeights, id: \.self) { weight in
|
||||||
|
Text(weight.rawValue).tag(weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
|
||||||
|
// Font Design - only show for system font
|
||||||
|
if style.fontFamily == .system {
|
||||||
|
Picker("Design", selection: $style.fontDesign) {
|
||||||
|
ForEach(Font.Design.allCases, id: \.self) { design in
|
||||||
|
Text(design.rawValue).tag(design)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font Preview
|
||||||
|
HStack {
|
||||||
|
Text("Preview:")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text("12:34")
|
||||||
|
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// NightModeSection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// OverlaySection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// TimePickerView.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 9/7/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ struct TimeDisplayView: View {
|
|||||||
let date: Date
|
let date: Date
|
||||||
let use24Hour: Bool
|
let use24Hour: Bool
|
||||||
let showSeconds: Bool
|
let showSeconds: Bool
|
||||||
|
let showAmPm: Bool
|
||||||
let digitColor: Color
|
let digitColor: Color
|
||||||
let glowIntensity: Double
|
let glowIntensity: Double
|
||||||
let manualScale: Double
|
let manualScale: Double
|
||||||
@ -48,6 +49,13 @@ struct TimeDisplayView: View {
|
|||||||
return df
|
return df
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private static let amPmDF: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
df.dateFormat = "a"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
private static let secondDF: DateFormatter = {
|
private static let secondDF: DateFormatter = {
|
||||||
let df = DateFormatter()
|
let df = DateFormatter()
|
||||||
df.locale = Locale(identifier: "en_US_POSIX")
|
df.locale = Locale(identifier: "en_US_POSIX")
|
||||||
@ -68,6 +76,10 @@ struct TimeDisplayView: View {
|
|||||||
let secondsText = Self.secondDF.string(from: date)
|
let secondsText = Self.secondDF.string(from: date)
|
||||||
|
|
||||||
|
|
||||||
|
// AM/PM badge
|
||||||
|
let shouldShowAmPm = showAmPm && !use24Hour
|
||||||
|
let amPmText = Self.amPmDF.string(from: date).uppercased()
|
||||||
|
|
||||||
// Separators - reasonable spacing with extra padding in landscape
|
// Separators - reasonable spacing with extra padding in landscape
|
||||||
let dotDiameter = fontSize * 0.75
|
let dotDiameter = fontSize * 0.75
|
||||||
let dotSpacing = portrait ? fontSize * 0.18 : fontSize * 0.25 // More spacing in landscape
|
let dotSpacing = portrait ? fontSize * 0.18 : fontSize * 0.25 // More spacing in landscape
|
||||||
@ -101,6 +113,23 @@ struct TimeDisplayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
.overlay(alignment: .bottomTrailing) {
|
||||||
|
if shouldShowAmPm {
|
||||||
|
Text(amPmText)
|
||||||
|
.font(
|
||||||
|
FontUtils.createFont(
|
||||||
|
name: fontFamily,
|
||||||
|
weight: fontWeight,
|
||||||
|
design: fontDesign,
|
||||||
|
size: max(12, fontSize * 0.18)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.foregroundColor(digitColor)
|
||||||
|
.opacity(clockOpacity)
|
||||||
|
.padding(.horizontal, max(6, fontSize * 0.05))
|
||||||
|
.padding(.vertical, max(3, fontSize * 0.03))
|
||||||
|
}
|
||||||
|
}
|
||||||
.offset(y: portraitMode && forceHorizontalMode ? -containerSize.height * 0.10 : 0) // Push up in horizontal mode
|
.offset(y: portraitMode && forceHorizontalMode ? -containerSize.height * 0.10 : 0) // Push up in horizontal mode
|
||||||
.scaleEffect(finalScale, anchor: .center)
|
.scaleEffect(finalScale, anchor: .center)
|
||||||
.animation(UIConstants.AnimationCurves.smooth, value: finalScale)
|
.animation(UIConstants.AnimationCurves.smooth, value: finalScale)
|
||||||
@ -123,6 +152,7 @@ struct TimeDisplayView: View {
|
|||||||
date: Date(),
|
date: Date(),
|
||||||
use24Hour: true,
|
use24Hour: true,
|
||||||
showSeconds: false,
|
showSeconds: false,
|
||||||
|
showAmPm: true,
|
||||||
digitColor: .white,
|
digitColor: .white,
|
||||||
glowIntensity: 0.2,
|
glowIntensity: 0.2,
|
||||||
manualScale: 1.0,
|
manualScale: 1.0,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user