Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-09-08 15:34:47 -05:00
parent 0baed76275
commit f4f365f4c7
9 changed files with 312 additions and 53 deletions

View File

@ -39,7 +39,10 @@ enum NotificationUtils {
if soundName == AppConstants.SystemSounds.defaultSound { if soundName == AppConstants.SystemSounds.defaultSound {
content.sound = UNNotificationSound.default content.sound = UNNotificationSound.default
} else { } else {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "\(soundName).caf")) // For alarm sounds, use the default notification sound since custom sounds need to be in the app bundle
// and properly configured. For now, use default to ensure notifications work.
content.sound = UNNotificationSound.default
print("🔔 Using default notification sound for alarm: \(soundName)")
} }
return content return content

View File

@ -14,6 +14,7 @@ struct Alarm: Identifiable, Codable, Equatable {
var isEnabled: Bool var isEnabled: Bool
var soundName: String var soundName: String
var label: String var label: String
var notificationMessage: String // Custom notification message
var snoozeDuration: Int // in minutes var snoozeDuration: Int // in minutes
var isVibrationEnabled: Bool var isVibrationEnabled: Bool
var isLightFlashEnabled: Bool var isLightFlashEnabled: Bool
@ -26,6 +27,7 @@ struct Alarm: Identifiable, Codable, Equatable {
isEnabled: Bool = true, isEnabled: Bool = true,
soundName: String = AppConstants.SystemSounds.defaultSound, soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm", label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
snoozeDuration: Int = 9, snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true, isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false, isLightFlashEnabled: Bool = false,
@ -36,6 +38,7 @@ struct Alarm: Identifiable, Codable, Equatable {
self.isEnabled = isEnabled self.isEnabled = isEnabled
self.soundName = soundName self.soundName = soundName
self.label = label self.label = label
self.notificationMessage = notificationMessage
self.snoozeDuration = snoozeDuration self.snoozeDuration = snoozeDuration
self.isVibrationEnabled = isVibrationEnabled self.isVibrationEnabled = isVibrationEnabled
self.isLightFlashEnabled = isLightFlashEnabled self.isLightFlashEnabled = isLightFlashEnabled

View File

@ -84,8 +84,8 @@ class AlarmService {
// 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,
title: "Wake Up!", title: alarm.label,
body: "Your alarm is ringing.", 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

View File

@ -100,7 +100,10 @@ class FocusModeService {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = title content.title = title
content.body = body content.body = body
content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName)) // Use default notification sound for now to ensure notifications work
// Custom sounds require proper bundle configuration
content.sound = UNNotificationSound.default
print("🔔 Using default notification sound for alarm: \(soundName)")
content.categoryIdentifier = "ALARM_CATEGORY" content.categoryIdentifier = "ALARM_CATEGORY"
content.userInfo = [ content.userInfo = [
"alarmId": identifier, "alarmId": identifier,
@ -115,7 +118,10 @@ class FocusModeService {
let components = calendar.dateComponents([.hour, .minute], from: date) let components = calendar.dateComponents([.hour, .minute], from: date)
trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
} else { } else {
trigger = UNTimeIntervalNotificationTrigger(timeInterval: date.timeIntervalSinceNow, repeats: false) // Use calendar trigger for one-time alarms to avoid time interval issues
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: date)
trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
} }
// Create request // Create request

View File

@ -40,7 +40,7 @@ class AlarmViewModel {
await notificationService.scheduleAlarmNotification( await notificationService.scheduleAlarmNotification(
id: alarm.id.uuidString, id: alarm.id.uuidString,
title: alarm.label, title: alarm.label,
body: "Time to wake up!", body: alarm.notificationMessage,
soundName: alarm.soundName, soundName: alarm.soundName,
date: alarm.time date: alarm.time
) )
@ -55,7 +55,7 @@ class AlarmViewModel {
await notificationService.scheduleAlarmNotification( await notificationService.scheduleAlarmNotification(
id: alarm.id.uuidString, id: alarm.id.uuidString,
title: alarm.label, title: alarm.label,
body: "Time to wake up!", body: alarm.notificationMessage,
soundName: alarm.soundName, soundName: alarm.soundName,
date: alarm.time date: alarm.time
) )
@ -83,7 +83,7 @@ class AlarmViewModel {
await notificationService.scheduleAlarmNotification( await notificationService.scheduleAlarmNotification(
id: alarm.id.uuidString, id: alarm.id.uuidString,
title: alarm.label, title: alarm.label,
body: "Time to wake up!", body: alarm.notificationMessage,
soundName: alarm.soundName, soundName: alarm.soundName,
date: alarm.time date: alarm.time
) )
@ -100,6 +100,7 @@ class AlarmViewModel {
time: Date, time: Date,
soundName: String = AppConstants.SystemSounds.defaultSound, soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm", label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
snoozeDuration: Int = 9, snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true, isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false, isLightFlashEnabled: Bool = false,
@ -111,6 +112,7 @@ class AlarmViewModel {
isEnabled: true, isEnabled: true,
soundName: soundName, soundName: soundName,
label: label, label: label,
notificationMessage: notificationMessage,
snoozeDuration: snoozeDuration, snoozeDuration: snoozeDuration,
isVibrationEnabled: isVibrationEnabled, isVibrationEnabled: isVibrationEnabled,
isLightFlashEnabled: isLightFlashEnabled, isLightFlashEnabled: isLightFlashEnabled,

View File

@ -17,6 +17,7 @@ struct AddAlarmView: View {
@State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date() @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 selectedSoundName = "digital-alarm.mp3"
@State private var alarmLabel = "Alarm" @State private var alarmLabel = "Alarm"
@State private var notificationMessage = "Your alarm is ringing"
@State private var snoozeDuration = 9 // minutes @State private var snoozeDuration = 9 // minutes
@State private var isVibrationEnabled = true @State private var isVibrationEnabled = true
@State private var isLightFlashEnabled = false @State private var isLightFlashEnabled = false
@ -24,53 +25,67 @@ struct AddAlarmView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
ScrollView { VStack(spacing: 0) {
VStack(spacing: 0) { // Time picker section at top
TimePickerSection(selectedTime: $newAlarmTime) TimePickerSection(selectedTime: $newAlarmTime)
TimeUntilAlarmSection(alarmTime: newAlarmTime) TimeUntilAlarmSection(alarmTime: newAlarmTime)
List { // List for settings below
// Label Section List {
NavigationLink(destination: LabelEditView(label: $alarmLabel)) { // Label Section
HStack { NavigationLink(destination: LabelEditView(label: $alarmLabel)) {
Image(systemName: "textformat") HStack {
.foregroundColor(UIConstants.Colors.accentColor) Image(systemName: "textformat")
.frame(width: 24) .foregroundColor(UIConstants.Colors.accentColor)
Text("Label") .frame(width: 24)
Spacer() Text("Label")
Text(alarmLabel) Spacer()
.foregroundColor(.secondary) Text(alarmLabel)
} .foregroundColor(.secondary)
} }
}
// Sound Section
NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { // Notification Message Section
HStack { NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) {
Image(systemName: "music.note") HStack {
.foregroundColor(UIConstants.Colors.accentColor) Image(systemName: "message")
.frame(width: 24) .foregroundColor(UIConstants.Colors.accentColor)
Text("Sound") .frame(width: 24)
Spacer() Text("Message")
Text(getSoundDisplayName(selectedSoundName)) Spacer()
.foregroundColor(.secondary) Text(notificationMessage)
} .foregroundColor(.secondary)
} .lineLimit(1)
}
// Snooze Section }
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
HStack { // Sound Section
Image(systemName: "clock.arrow.circlepath") NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) {
.foregroundColor(UIConstants.Colors.accentColor) HStack {
.frame(width: 24) Image(systemName: "music.note")
Text("Snooze") .foregroundColor(UIConstants.Colors.accentColor)
Spacer() .frame(width: 24)
Text("for \(snoozeDuration) min") Text("Sound")
.foregroundColor(.secondary) Spacer()
} Text(getSoundDisplayName(selectedSoundName))
.foregroundColor(.secondary)
}
}
// Snooze Section
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
HStack {
Image(systemName: "clock.arrow.circlepath")
.foregroundColor(UIConstants.Colors.accentColor)
.frame(width: 24)
Text("Snooze")
Spacer()
Text("for \(snoozeDuration) min")
.foregroundColor(.secondary)
} }
} }
.listStyle(.insetGrouped)
} }
.listStyle(.insetGrouped)
} }
.navigationTitle("Alarm") .navigationTitle("Alarm")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@ -90,6 +105,7 @@ struct AddAlarmView: View {
time: newAlarmTime, time: newAlarmTime,
soundName: selectedSoundName, soundName: selectedSoundName,
label: alarmLabel, label: alarmLabel,
notificationMessage: notificationMessage,
snoozeDuration: snoozeDuration, snoozeDuration: snoozeDuration,
isVibrationEnabled: isVibrationEnabled, isVibrationEnabled: isVibrationEnabled,
isLightFlashEnabled: isLightFlashEnabled, isLightFlashEnabled: isLightFlashEnabled,

View File

@ -13,6 +13,7 @@ struct AlarmView: View {
// MARK: - Properties // MARK: - Properties
@State private var viewModel = AlarmViewModel() @State private var viewModel = AlarmViewModel()
@State private var showAddAlarm = false @State private var showAddAlarm = false
@State private var selectedAlarmForEdit: Alarm?
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@ -35,7 +36,9 @@ struct AlarmView: View {
await viewModel.toggleAlarm(id: alarm.id) await viewModel.toggleAlarm(id: alarm.id)
} }
}, },
onEdit: { /* TODO: Implement edit functionality */ } onEdit: {
selectedAlarmForEdit = alarm
}
) )
} }
.onDelete(perform: deleteAlarm) .onDelete(perform: deleteAlarm)
@ -64,6 +67,12 @@ struct AlarmView: View {
isPresented: $showAddAlarm isPresented: $showAddAlarm
) )
} }
.sheet(item: $selectedAlarmForEdit) { alarm in
EditAlarmView(
viewModel: viewModel,
alarm: alarm
)
}
} }
// MARK: - Private Methods // MARK: - Private Methods

View File

@ -0,0 +1,55 @@
//
// NotificationMessageEditView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
/// View for editing alarm notification message
struct NotificationMessageEditView: View {
@Binding var message: String
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(spacing: UIConstants.Spacing.large) {
TextField("Notification message", text: $message)
.textFieldStyle(RoundedBorderTextFieldStyle())
.contentPadding(horizontal: UIConstants.Spacing.large)
// Preview section
VStack(alignment: .leading, spacing: UIConstants.Spacing.small) {
Text("Preview:")
.font(.headline)
.foregroundColor(.secondary)
VStack(alignment: .leading, spacing: 4) {
Text("Alarm")
.font(.headline)
.foregroundColor(.primary)
Text(message.isEmpty ? "Your alarm is ringing" : message)
.font(.body)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
.contentPadding(horizontal: UIConstants.Spacing.large)
Spacer()
}
.navigationTitle("Message")
.navigationBarTitleDisplayMode(.inline)
.contentPadding(vertical: UIConstants.Spacing.large)
}
}
// MARK: - Preview
#Preview {
NavigationStack {
NotificationMessageEditView(message: .constant("Your alarm is ringing"))
}
}

View File

@ -0,0 +1,165 @@
//
// EditAlarmView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
/// View for editing existing alarms
struct EditAlarmView: View {
// MARK: - Properties
let viewModel: AlarmViewModel
let alarm: Alarm
@Environment(\.dismiss) private var dismiss
@State private var alarmTime: Date
@State private var selectedSoundName: String
@State private var alarmLabel: String
@State private var notificationMessage: String
@State private var snoozeDuration: Int
@State private var isVibrationEnabled: Bool
@State private var isLightFlashEnabled: Bool
@State private var volume: Float
// MARK: - Initialization
init(viewModel: AlarmViewModel, alarm: Alarm) {
self.viewModel = viewModel
self.alarm = alarm
// Initialize state with current alarm values
self._alarmTime = State(initialValue: alarm.time)
self._selectedSoundName = State(initialValue: alarm.soundName)
self._alarmLabel = State(initialValue: alarm.label)
self._notificationMessage = State(initialValue: alarm.notificationMessage)
self._snoozeDuration = State(initialValue: alarm.snoozeDuration)
self._isVibrationEnabled = State(initialValue: alarm.isVibrationEnabled)
self._isLightFlashEnabled = State(initialValue: alarm.isLightFlashEnabled)
self._volume = State(initialValue: alarm.volume)
}
// MARK: - Body
var body: some View {
NavigationView {
VStack(spacing: 0) {
// Time picker section at top
TimePickerSection(selectedTime: $alarmTime)
TimeUntilAlarmSection(alarmTime: alarmTime)
// List for settings below
List {
// Label Section
NavigationLink(destination: LabelEditView(label: $alarmLabel)) {
HStack {
Image(systemName: "textformat")
.foregroundColor(UIConstants.Colors.accentColor)
.frame(width: 24)
Text("Label")
Spacer()
Text(alarmLabel)
.foregroundColor(.secondary)
}
}
// Notification Message Section
NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) {
HStack {
Image(systemName: "message")
.foregroundColor(UIConstants.Colors.accentColor)
.frame(width: 24)
Text("Message")
Spacer()
Text(notificationMessage)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
// Sound Section
NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) {
HStack {
Image(systemName: "music.note")
.foregroundColor(UIConstants.Colors.accentColor)
.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(UIConstants.Colors.accentColor)
.frame(width: 24)
Text("Snooze")
Spacer()
Text("for \(snoozeDuration) min")
.foregroundColor(.secondary)
}
}
}
.listStyle(.insetGrouped)
}
.navigationTitle("Edit Alarm")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
.foregroundColor(UIConstants.Colors.accentColor)
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
Task {
let updatedAlarm = Alarm(
id: alarm.id, // Keep the same ID
time: alarmTime,
isEnabled: alarm.isEnabled, // Keep the same enabled state
soundName: selectedSoundName,
label: alarmLabel,
notificationMessage: notificationMessage,
snoozeDuration: snoozeDuration,
isVibrationEnabled: isVibrationEnabled,
isLightFlashEnabled: isLightFlashEnabled,
volume: volume
)
await viewModel.updateAlarm(updatedAlarm)
dismiss()
}
}
.foregroundColor(UIConstants.Colors.accentColor)
.fontWeight(.semibold)
}
}
}
}
// 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 {
EditAlarmView(
viewModel: AlarmViewModel(),
alarm: Alarm(
time: Date(),
label: "Morning Alarm",
notificationMessage: "Time to wake up!"
)
)
}