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 {
content.sound = UNNotificationSound.default
} 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

View File

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

View File

@ -84,8 +84,8 @@ class AlarmService {
// Use FocusModeService for better Focus mode compatibility
focusModeService.scheduleAlarmNotification(
identifier: alarm.id.uuidString,
title: "Wake Up!",
body: "Your alarm is ringing.",
title: alarm.label,
body: alarm.notificationMessage,
date: alarm.time,
soundName: alarm.soundName,
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()
content.title = title
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.userInfo = [
"alarmId": identifier,
@ -115,7 +118,10 @@ class FocusModeService {
let components = calendar.dateComponents([.hour, .minute], from: date)
trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
} 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

View File

@ -40,7 +40,7 @@ class AlarmViewModel {
await notificationService.scheduleAlarmNotification(
id: alarm.id.uuidString,
title: alarm.label,
body: "Time to wake up!",
body: alarm.notificationMessage,
soundName: alarm.soundName,
date: alarm.time
)
@ -55,7 +55,7 @@ class AlarmViewModel {
await notificationService.scheduleAlarmNotification(
id: alarm.id.uuidString,
title: alarm.label,
body: "Time to wake up!",
body: alarm.notificationMessage,
soundName: alarm.soundName,
date: alarm.time
)
@ -83,7 +83,7 @@ class AlarmViewModel {
await notificationService.scheduleAlarmNotification(
id: alarm.id.uuidString,
title: alarm.label,
body: "Time to wake up!",
body: alarm.notificationMessage,
soundName: alarm.soundName,
date: alarm.time
)
@ -100,6 +100,7 @@ class AlarmViewModel {
time: Date,
soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false,
@ -111,6 +112,7 @@ class AlarmViewModel {
isEnabled: true,
soundName: soundName,
label: label,
notificationMessage: notificationMessage,
snoozeDuration: snoozeDuration,
isVibrationEnabled: isVibrationEnabled,
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 selectedSoundName = "digital-alarm.mp3"
@State private var alarmLabel = "Alarm"
@State private var notificationMessage = "Your alarm is ringing"
@State private var snoozeDuration = 9 // minutes
@State private var isVibrationEnabled = true
@State private var isLightFlashEnabled = false
@ -24,53 +25,67 @@ struct AddAlarmView: View {
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 0) {
TimePickerSection(selectedTime: $newAlarmTime)
TimeUntilAlarmSection(alarmTime: newAlarmTime)
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)
}
}
// 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)
}
VStack(spacing: 0) {
// Time picker section at top
TimePickerSection(selectedTime: $newAlarmTime)
TimeUntilAlarmSection(alarmTime: newAlarmTime)
// 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)
}
.listStyle(.insetGrouped)
}
.navigationTitle("Alarm")
.navigationBarTitleDisplayMode(.inline)
@ -90,6 +105,7 @@ struct AddAlarmView: View {
time: newAlarmTime,
soundName: selectedSoundName,
label: alarmLabel,
notificationMessage: notificationMessage,
snoozeDuration: snoozeDuration,
isVibrationEnabled: isVibrationEnabled,
isLightFlashEnabled: isLightFlashEnabled,

View File

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