diff --git a/TheNoiseClock/Models/Alarm.swift b/TheNoiseClock/Models/Alarm.swift index b7a5984..6dc54e8 100644 --- a/TheNoiseClock/Models/Alarm.swift +++ b/TheNoiseClock/Models/Alarm.swift @@ -13,13 +13,33 @@ struct Alarm: Identifiable, Codable, Equatable { var time: Date var isEnabled: Bool var soundName: String + var label: String + var snoozeDuration: Int // in minutes + var isVibrationEnabled: Bool + var isLightFlashEnabled: Bool + var volume: Float // MARK: - Initialization - init(id: UUID = UUID(), time: Date, isEnabled: Bool = true, soundName: String = AppConstants.SystemSounds.defaultSound) { + init( + id: UUID = UUID(), + time: Date, + isEnabled: Bool = true, + soundName: String = AppConstants.SystemSounds.defaultSound, + label: String = "Alarm", + snoozeDuration: Int = 9, + isVibrationEnabled: Bool = true, + isLightFlashEnabled: Bool = false, + volume: Float = 1.0 + ) { self.id = id self.time = time self.isEnabled = isEnabled self.soundName = soundName + self.label = label + self.snoozeDuration = snoozeDuration + self.isVibrationEnabled = isVibrationEnabled + self.isLightFlashEnabled = isLightFlashEnabled + self.volume = volume } // MARK: - Equatable diff --git a/TheNoiseClock/Models/SoundConfiguration.swift b/TheNoiseClock/Models/SoundConfiguration.swift index cedbf76..080db8a 100644 --- a/TheNoiseClock/Models/SoundConfiguration.swift +++ b/TheNoiseClock/Models/SoundConfiguration.swift @@ -115,6 +115,11 @@ class SoundConfigurationService { .map { $0.toSound() } } + /// Get alarm sounds specifically + func getAlarmSounds() -> [Sound] { + return getSoundsByCategory("alarm") + } + /// Get available categories func getAvailableCategories() -> [SoundCategory] { return getConfiguration()?.categories ?? [] @@ -128,9 +133,18 @@ class SoundConfigurationService { /// Fallback sounds if JSON loading fails private func getFallbackSounds() -> [Sound] { return [ + // White noise sounds Sound(name: "White Noise", fileName: "white-noise.mp3", bundleName: "Ambient"), Sound(name: "Heavy Rain White Noise", fileName: "heavy-rain-white-noise.mp3", bundleName: "Nature"), - Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater.mp3", bundleName: "Mechanical") + Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater.mp3", bundleName: "Mechanical"), + + // Alarm sounds + Sound(name: "Digital Alarm", fileName: "digital-alarm.mp3", bundleName: "AlarmSounds"), + Sound(name: "iPhone Alarm", fileName: "iphone-alarm.mp3", bundleName: "AlarmSounds"), + Sound(name: "Classic Alarm", fileName: "classic-alarm.mp3", bundleName: "AlarmSounds"), + Sound(name: "Beep Alarm", fileName: "beep-alarm.mp3", bundleName: "AlarmSounds"), + Sound(name: "Siren Alarm", fileName: "siren-alarm.mp3", bundleName: "AlarmSounds"), + Sound(name: "Voice Wake Up", fileName: "voice-wakeup.mp3", bundleName: "AlarmSounds") ] } } \ No newline at end of file diff --git a/TheNoiseClock/Resources/AlarmSounds.bundle/beep-alarm.mp3 b/TheNoiseClock/Resources/AlarmSounds.bundle/beep-alarm.mp3 new file mode 100644 index 0000000..4be0312 Binary files /dev/null and b/TheNoiseClock/Resources/AlarmSounds.bundle/beep-alarm.mp3 differ diff --git a/TheNoiseClock/Resources/AlarmSounds.bundle/buzzing-alarm.mp3 b/TheNoiseClock/Resources/AlarmSounds.bundle/buzzing-alarm.mp3 new file mode 100644 index 0000000..40c22a6 Binary files /dev/null and b/TheNoiseClock/Resources/AlarmSounds.bundle/buzzing-alarm.mp3 differ diff --git a/TheNoiseClock/Resources/AlarmSounds.bundle/classic-alarm.mp3 b/TheNoiseClock/Resources/AlarmSounds.bundle/classic-alarm.mp3 new file mode 100644 index 0000000..b9ca5cf Binary files /dev/null and b/TheNoiseClock/Resources/AlarmSounds.bundle/classic-alarm.mp3 differ diff --git a/TheNoiseClock/Resources/AlarmSounds.bundle/digital-alarm.mp3 b/TheNoiseClock/Resources/AlarmSounds.bundle/digital-alarm.mp3 new file mode 100644 index 0000000..a8f4cc0 Binary files /dev/null and b/TheNoiseClock/Resources/AlarmSounds.bundle/digital-alarm.mp3 differ diff --git a/TheNoiseClock/Resources/AlarmSounds.bundle/siren-alarm.mp3 b/TheNoiseClock/Resources/AlarmSounds.bundle/siren-alarm.mp3 new file mode 100644 index 0000000..9dba51d Binary files /dev/null and b/TheNoiseClock/Resources/AlarmSounds.bundle/siren-alarm.mp3 differ diff --git a/TheNoiseClock/Resources/sounds.json b/TheNoiseClock/Resources/sounds.json index e10dddd..099cf61 100644 --- a/TheNoiseClock/Resources/sounds.json +++ b/TheNoiseClock/Resources/sounds.json @@ -23,6 +23,46 @@ "category": "mechanical", "description": "Fan and heater sounds for consistent background noise", "bundleName": "Mechanical" + }, + { + "id": "digital-alarm", + "name": "Digital Alarm", + "fileName": "digital-alarm.mp3", + "category": "alarm", + "description": "Classic digital alarm sound", + "bundleName": "AlarmSounds" + }, + { + "id": "iphone-alarm", + "name": "Buzzing Alarm", + "fileName": "buzzing-alarm.mp3", + "category": "alarm", + "description": "Buzzing sound", + "bundleName": "AlarmSounds" + }, + { + "id": "classic-alarm", + "name": "Classic Alarm", + "fileName": "classic-alarm.mp3", + "category": "alarm", + "description": "Traditional alarm sound", + "bundleName": "AlarmSounds" + }, + { + "id": "beep-alarm", + "name": "Beep Alarm", + "fileName": "beep-alarm.mp3", + "category": "alarm", + "description": "Short beep alarm sound", + "bundleName": "AlarmSounds" + }, + { + "id": "siren-alarm", + "name": "Siren Alarm", + "fileName": "siren-alarm.mp3", + "category": "alarm", + "description": "Emergency siren alarm for heavy sleepers", + "bundleName": "AlarmSounds" } ], "categories": [ @@ -43,6 +83,12 @@ "name": "Mechanical", "description": "Mechanical and electronic sounds", "bundleName": "Mechanical" + }, + { + "id": "alarm", + "name": "Alarm Sounds", + "description": "Wake-up and notification alarm sounds", + "bundleName": "AlarmSounds" } ], "settings": { diff --git a/TheNoiseClock/Services/NoisePlayer.swift b/TheNoiseClock/Services/NoisePlayer.swift index 6835935..c819965 100644 --- a/TheNoiseClock/Services/NoisePlayer.swift +++ b/TheNoiseClock/Services/NoisePlayer.swift @@ -12,12 +12,15 @@ import Observation @Observable class NoisePlayer { + // MARK: - Singleton + static let shared = NoisePlayer() + // MARK: - Properties private var players: [String: AVAudioPlayer] = [:] private var currentPlayer: AVAudioPlayer? // MARK: - Initialization - init() { + private init() { setupAudioSession() preloadSounds() } diff --git a/TheNoiseClock/Services/NotificationService.swift b/TheNoiseClock/Services/NotificationService.swift index 547cf73..05d7cd9 100644 --- a/TheNoiseClock/Services/NotificationService.swift +++ b/TheNoiseClock/Services/NotificationService.swift @@ -44,6 +44,8 @@ class NotificationService { } } + /// Schedule a single alarm notification + @discardableResult func scheduleAlarmNotification( id: String, title: String, @@ -70,11 +72,15 @@ class NotificationService { ) } + + /// Cancel a single notification func cancelNotification(id: String) { NotificationUtils.removeNotification(identifier: id) } + /// Cancel all notifications func cancelAllNotifications() { NotificationUtils.removeAllNotifications() } + } diff --git a/TheNoiseClock/ViewModels/AlarmViewModel.swift b/TheNoiseClock/ViewModels/AlarmViewModel.swift index d20d01b..86c1eaa 100644 --- a/TheNoiseClock/ViewModels/AlarmViewModel.swift +++ b/TheNoiseClock/ViewModels/AlarmViewModel.swift @@ -32,32 +32,89 @@ class AlarmViewModel { } // MARK: - Public Interface - func addAlarm(_ alarm: Alarm) { + func addAlarm(_ alarm: Alarm) async { alarmService.addAlarm(alarm) + + // Schedule notification if alarm is enabled + if alarm.isEnabled { + await notificationService.scheduleAlarmNotification( + id: alarm.id.uuidString, + title: alarm.label, + body: "Time to wake up!", + soundName: alarm.soundName, + date: alarm.time + ) + } } - func updateAlarm(_ alarm: Alarm) { + func updateAlarm(_ alarm: Alarm) async { alarmService.updateAlarm(alarm) + + // Reschedule notification + if alarm.isEnabled { + await notificationService.scheduleAlarmNotification( + id: alarm.id.uuidString, + title: alarm.label, + body: "Time to wake up!", + soundName: alarm.soundName, + date: alarm.time + ) + } else { + notificationService.cancelNotification(id: alarm.id.uuidString) + } } - func deleteAlarm(id: UUID) { + func deleteAlarm(id: UUID) async { + // Cancel notification first + notificationService.cancelNotification(id: id.uuidString) + + // Then delete from storage alarmService.deleteAlarm(id: id) } - func toggleAlarm(id: UUID) { - alarmService.toggleAlarm(id: id) + func toggleAlarm(id: UUID) async { + guard var alarm = alarmService.getAlarm(id: id) else { return } + + alarm.isEnabled.toggle() + alarmService.updateAlarm(alarm) + + // Schedule or cancel notification based on new state + if alarm.isEnabled { + await notificationService.scheduleAlarmNotification( + id: alarm.id.uuidString, + title: alarm.label, + body: "Time to wake up!", + soundName: alarm.soundName, + date: alarm.time + ) + } else { + notificationService.cancelNotification(id: id.uuidString) + } } func getAlarm(id: UUID) -> Alarm? { return alarmService.getAlarm(id: id) } - func createNewAlarm(time: Date, soundName: String = AppConstants.SystemSounds.defaultSound) -> Alarm { + func createNewAlarm( + time: Date, + soundName: String = AppConstants.SystemSounds.defaultSound, + label: String = "Alarm", + snoozeDuration: Int = 9, + isVibrationEnabled: Bool = true, + isLightFlashEnabled: Bool = false, + volume: Float = 1.0 + ) -> Alarm { return Alarm( id: UUID(), time: time, isEnabled: true, - soundName: soundName + soundName: soundName, + label: label, + snoozeDuration: snoozeDuration, + isVibrationEnabled: isVibrationEnabled, + isLightFlashEnabled: isLightFlashEnabled, + volume: volume ) } diff --git a/TheNoiseClock/ViewModels/NoiseViewModel.swift b/TheNoiseClock/ViewModels/NoiseViewModel.swift index b14ee22..85553f8 100644 --- a/TheNoiseClock/ViewModels/NoiseViewModel.swift +++ b/TheNoiseClock/ViewModels/NoiseViewModel.swift @@ -24,7 +24,7 @@ class NoiseViewModel { } // MARK: - Initialization - init(noisePlayer: NoisePlayer = NoisePlayer()) { + init(noisePlayer: NoisePlayer = NoisePlayer.shared) { self.noisePlayer = noisePlayer } diff --git a/TheNoiseClock/Views/Alarms/AddAlarmView.swift b/TheNoiseClock/Views/Alarms/AddAlarmView.swift index 379801f..da0f854 100644 --- a/TheNoiseClock/Views/Alarms/AddAlarmView.swift +++ b/TheNoiseClock/Views/Alarms/AddAlarmView.swift @@ -7,82 +7,109 @@ import SwiftUI -/// View for creating new alarms +/// View for creating new alarms with iOS-native style interface struct AddAlarmView: View { // MARK: - Properties let viewModel: AlarmViewModel @Binding var isPresented: Bool - @State private var newAlarmTime = Date() - @State private var selectedSoundName = AppConstants.SystemSounds.defaultSound + @State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date() + @State private var selectedSoundName = "digital-alarm.mp3" + @State private var alarmLabel = "Alarm" + @State private var snoozeDuration = 9 // minutes + @State private var isVibrationEnabled = true + @State private var isLightFlashEnabled = false + @State private var volume: Float = 1.0 - // MARK: - Body var body: some View { NavigationView { - VStack(spacing: UIConstants.Spacing.extraLarge) { - TimePickerView( - selectedTime: $newAlarmTime - ) + VStack(spacing: 0) { + TimePickerSection(selectedTime: $newAlarmTime) + TimeUntilAlarmSection(alarmTime: newAlarmTime) - Picker("Sound", selection: $selectedSoundName) { - ForEach(viewModel.systemSounds, id: \.self) { sound in - Text(sound.capitalized).tag(sound) + List { + // Label Section + NavigationLink(destination: LabelEditView(label: $alarmLabel)) { + HStack { + Image(systemName: "textformat") + .foregroundColor(.orange) + .frame(width: 24) + Text("Label") + Spacer() + Text(alarmLabel) + .foregroundColor(.secondary) + } + } + + // Sound Section + NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { + HStack { + Image(systemName: "music.note") + .foregroundColor(.orange) + .frame(width: 24) + Text("Sound") + Spacer() + Text(getSoundDisplayName(selectedSoundName)) + .foregroundColor(.secondary) + } + } + + // Snooze Section + NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { + HStack { + Image(systemName: "clock.arrow.circlepath") + .foregroundColor(.orange) + .frame(width: 24) + Text("Snooze") + Spacer() + Text("for \(snoozeDuration) min") + .foregroundColor(.secondary) + } } } - .pickerStyle(.menu) - - HStack(spacing: UIConstants.Spacing.large) { + .listStyle(.insetGrouped) + } + .navigationTitle("Alarm") + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { isPresented = false } - .buttonStyle(isEnabled: true, color: UIConstants.Colors.secondaryText) - - Spacer() - - Button("Add Alarm") { - let newAlarm = viewModel.createNewAlarm( - time: newAlarmTime, - soundName: selectedSoundName - ) - viewModel.addAlarm(newAlarm) - isPresented = false + .foregroundColor(.orange) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + Task { + let newAlarm = viewModel.createNewAlarm( + time: newAlarmTime, + soundName: selectedSoundName, + label: alarmLabel, + snoozeDuration: snoozeDuration, + isVibrationEnabled: isVibrationEnabled, + isLightFlashEnabled: isLightFlashEnabled, + volume: volume + ) + await viewModel.addAlarm(newAlarm) + isPresented = false + } } - .buttonStyle(isEnabled: true, color: UIConstants.Colors.accentColor) + .foregroundColor(.orange) + .fontWeight(.semibold) } } - .padding(UIConstants.Spacing.large) - .navigationTitle("New Alarm") - .navigationBarTitleDisplayMode(.inline) } } -} - -// MARK: - Supporting Views -private struct TimePickerView: View { - @Binding var selectedTime: Date - var body: some View { - VStack(alignment: .leading, spacing: UIConstants.Spacing.small) { - Text("Time") - .font(.headline) - .foregroundColor(UIConstants.Colors.primaryText) - - DatePicker( - "Time", - selection: $selectedTime, - displayedComponents: .hourAndMinute - ) - .datePickerStyle(.wheel) - .labelsHidden() + // MARK: - Helper Methods + private func getSoundDisplayName(_ fileName: String) -> String { + let alarmSounds = SoundConfigurationService.shared.getAlarmSounds() + if let sound = alarmSounds.first(where: { $0.fileName == fileName }) { + return sound.name } + return fileName.replacingOccurrences(of: ".mp3", with: "").capitalized } -} - -// MARK: - Preview -#Preview { - AddAlarmView( - viewModel: AlarmViewModel(), - isPresented: .constant(true) - ) -} +} \ No newline at end of file diff --git a/TheNoiseClock/Views/Alarms/AlarmView.swift b/TheNoiseClock/Views/Alarms/AlarmView.swift index 0a031ea..b5e7e36 100644 --- a/TheNoiseClock/Views/Alarms/AlarmView.swift +++ b/TheNoiseClock/Views/Alarms/AlarmView.swift @@ -20,7 +20,11 @@ struct AlarmView: View { ForEach(viewModel.alarms) { alarm in AlarmRowView( alarm: alarm, - onToggle: { viewModel.toggleAlarm(id: alarm.id) }, + onToggle: { + Task { + await viewModel.toggleAlarm(id: alarm.id) + } + }, onEdit: { /* TODO: Implement edit functionality */ } ) } @@ -52,9 +56,11 @@ struct AlarmView: View { // MARK: - Private Methods private func deleteAlarm(at offsets: IndexSet) { - for index in offsets { - let alarm = viewModel.alarms[index] - viewModel.deleteAlarm(id: alarm.id) + Task { + for index in offsets { + let alarm = viewModel.alarms[index] + await viewModel.deleteAlarm(id: alarm.id) + } } } } diff --git a/TheNoiseClock/Views/Alarms/Components/AlarmRowView.swift b/TheNoiseClock/Views/Alarms/Components/AlarmRowView.swift index 3fd65cc..b38119a 100644 --- a/TheNoiseClock/Views/Alarms/Components/AlarmRowView.swift +++ b/TheNoiseClock/Views/Alarms/Components/AlarmRowView.swift @@ -23,11 +23,11 @@ struct AlarmRowView: View { .font(.headline) .foregroundColor(UIConstants.Colors.primaryText) - Text("Enabled: \(alarm.isEnabled ? "Yes" : "No")") + Text(alarm.label) .font(.subheadline) .foregroundColor(UIConstants.Colors.secondaryText) - Text("Sound: \(alarm.soundName)") + Text("• \(alarm.soundName)") .font(.caption) .foregroundColor(UIConstants.Colors.secondaryText) } @@ -45,6 +45,7 @@ struct AlarmRowView: View { onEdit() } } + } // MARK: - Preview diff --git a/TheNoiseClock/Views/Alarms/Components/LabelEditView.swift b/TheNoiseClock/Views/Alarms/Components/LabelEditView.swift new file mode 100644 index 0000000..1508c3f --- /dev/null +++ b/TheNoiseClock/Views/Alarms/Components/LabelEditView.swift @@ -0,0 +1,26 @@ +// +// LabelEditView.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import SwiftUI + +/// View for editing alarm label +struct LabelEditView: View { + @Binding var label: String + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 20) { + TextField("Alarm Label", text: $label) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + + Spacer() + } + .navigationTitle("Label") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/TheNoiseClock/Views/Alarms/Components/SnoozeSelectionView.swift b/TheNoiseClock/Views/Alarms/Components/SnoozeSelectionView.swift new file mode 100644 index 0000000..2f4f715 --- /dev/null +++ b/TheNoiseClock/Views/Alarms/Components/SnoozeSelectionView.swift @@ -0,0 +1,39 @@ +// +// SnoozeSelectionView.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import SwiftUI + +/// View for selecting snooze duration +struct SnoozeSelectionView: View { + @Binding var snoozeDuration: Int + @Environment(\.dismiss) private var dismiss + + private let snoozeOptions = [5, 9, 10, 15, 20, 30] + + var body: some View { + List { + Section("Snooze Duration") { + ForEach(snoozeOptions, id: \.self) { duration in + HStack { + Text("\(duration) minutes") + Spacer() + if snoozeDuration == duration { + Image(systemName: "checkmark") + .foregroundColor(.orange) + } + } + .contentShape(Rectangle()) + .onTapGesture { + snoozeDuration = duration + } + } + } + } + .navigationTitle("Snooze") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift b/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift new file mode 100644 index 0000000..65fc82c --- /dev/null +++ b/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift @@ -0,0 +1,85 @@ +// +// SoundSelectionView.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import SwiftUI + +/// View for selecting alarm sounds with preview functionality +struct SoundSelectionView: View { + @Binding var selectedSound: String + @Environment(\.dismiss) private var dismiss + + // Use shared player instance to avoid audio conflicts + private let noisePlayer = NoisePlayer.shared + private let alarmSounds = SoundConfigurationService.shared.getAlarmSounds().sorted { $0.name < $1.name } + + @State private var isPlaying = false + @State private var currentlyPlayingSound: String? = nil + + var body: some View { + List { + Section("Alarm Sounds") { + ForEach(alarmSounds, id: \.id) { sound in + HStack { + Text(sound.name) + .font(.body) + Spacer() + if selectedSound == sound.fileName { + Image(systemName: "checkmark") + .foregroundColor(.orange) + } + } + .contentShape(Rectangle()) + .onTapGesture { + // Stop any currently playing sound when selecting a new one + if isPlaying { + stopSound() + } + selectedSound = sound.fileName + } + } + } + } + .navigationTitle("Sound") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + if isPlaying && currentlyPlayingSound == selectedSound { + stopSound() + } else { + playSelectedSound() + } + }) { + Image(systemName: isPlaying && currentlyPlayingSound == selectedSound ? "stop.fill" : "play.fill") + .foregroundColor(.orange) + } + } + } + .onDisappear { + // Stop any playing sound when leaving the view + stopSound() + } + } + + private func playSelectedSound() { + guard let sound = alarmSounds.first(where: { $0.fileName == selectedSound }) else { return } + + // Stop any currently playing sound first + stopSound() + + // Start playing the new sound + noisePlayer.playSound(sound) + isPlaying = true + currentlyPlayingSound = selectedSound + } + + private func stopSound() { + noisePlayer.stopSound() + isPlaying = false + currentlyPlayingSound = nil + } +} diff --git a/TheNoiseClock/Views/Alarms/Components/TimePickerSection.swift b/TheNoiseClock/Views/Alarms/Components/TimePickerSection.swift new file mode 100644 index 0000000..cb34d76 --- /dev/null +++ b/TheNoiseClock/Views/Alarms/Components/TimePickerSection.swift @@ -0,0 +1,27 @@ +// +// TimePickerSection.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import SwiftUI + +/// Time picker component for alarm creation +struct TimePickerSection: View { + @Binding var selectedTime: Date + + var body: some View { + VStack(spacing: 0) { + DatePicker( + "Time", + selection: $selectedTime, + displayedComponents: .hourAndMinute + ) + .datePickerStyle(.wheel) + .labelsHidden() + .frame(height: 200) + } + .background(Color(.systemGroupedBackground)) + } +} diff --git a/TheNoiseClock/Views/Alarms/Components/TimeUntilAlarmSection.swift b/TheNoiseClock/Views/Alarms/Components/TimeUntilAlarmSection.swift new file mode 100644 index 0000000..1ba54d2 --- /dev/null +++ b/TheNoiseClock/Views/Alarms/Components/TimeUntilAlarmSection.swift @@ -0,0 +1,78 @@ +// +// TimeUntilAlarmSection.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import SwiftUI + +/// Component showing time until alarm and day information +struct TimeUntilAlarmSection: View { + let alarmTime: Date + + var body: some View { + VStack(spacing: 4) { + HStack { + Image(systemName: "calendar") + .foregroundColor(.orange) + Text(timeUntilAlarm) + .font(.subheadline) + .foregroundColor(.secondary) + } + + Text(dayText) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 12) + .background(Color(.systemGroupedBackground)) + } + + private var timeUntilAlarm: String { + let now = Date() + let calendar = Calendar.current + + // Calculate the next occurrence of the alarm time + let nextAlarmTime: Date + if calendar.isDateInToday(alarmTime) && alarmTime < now { + // If alarm time has passed today, calculate for tomorrow + nextAlarmTime = calendar.date(byAdding: .day, value: 1, to: alarmTime) ?? alarmTime + } else { + // Use the alarm time as-is (either future today or already tomorrow+) + nextAlarmTime = alarmTime + } + + // Calculate time difference from now to next alarm + let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime) + if let hours = components.hour, let minutes = components.minute { + if hours > 0 { + return "Will turn on in \(hours)h \(minutes)m" + } else if minutes > 0 { + return "Will turn on in \(minutes)m" + } else { + return "Will turn on now" + } + } + + return "Will turn on tomorrow" + } + + private var dayText: String { + let calendar = Calendar.current + let now = Date() + + // If alarm time is in the past today, show tomorrow + if calendar.isDateInToday(alarmTime) && alarmTime < now { + return "Tomorrow" + } else if calendar.isDateInToday(alarmTime) { + return "Today" + } else if calendar.isDateInTomorrow(alarmTime) { + return "Tomorrow" + } else { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE" + return formatter.string(from: alarmTime) + } + } +}