Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
b8428ca134
commit
3844e19b39
@ -95,7 +95,8 @@ public class SoundConfigurationService {
|
|||||||
|
|
||||||
/// Load sound configuration from multiple category-specific JSON files
|
/// Load sound configuration from multiple category-specific JSON files
|
||||||
public func loadConfigurationFromBundles(from bundle: Bundle = .main) -> SoundConfiguration {
|
public func loadConfigurationFromBundles(from bundle: Bundle = .main) -> SoundConfiguration {
|
||||||
let bundleNames = ["Colored", "Nature", "Mechanical", "Ambient"]
|
// Include AlarmSounds bundle for alarm sound preview functionality
|
||||||
|
let bundleNames = ["Colored", "Nature", "Mechanical", "Ambient", "AlarmSounds"]
|
||||||
var allSounds: [Sound] = []
|
var allSounds: [Sound] = []
|
||||||
|
|
||||||
for bundleName in bundleNames {
|
for bundleName in bundleNames {
|
||||||
|
|||||||
12
PRD.md
12
PRD.md
@ -452,17 +452,15 @@ TheNoiseClock/
|
|||||||
│ │ │ ├── State/
|
│ │ │ ├── State/
|
||||||
│ │ │ │ └── AlarmViewModel.swift
|
│ │ │ │ └── AlarmViewModel.swift
|
||||||
│ │ │ ├── Services/
|
│ │ │ ├── Services/
|
||||||
│ │ │ │ ├── AlarmService.swift
|
│ │ │ │ ├── AlarmService.swift # Alarm persistence
|
||||||
│ │ │ │ ├── AlarmSoundService.swift
|
│ │ │ │ ├── AlarmSoundService.swift # Alarm sound metadata
|
||||||
│ │ │ │ ├── AlarmKitService.swift # AlarmKit integration (iOS 26+)
|
│ │ │ │ └── AlarmKitService.swift # AlarmKit integration (iOS 26+)
|
||||||
│ │ │ │ ├── FocusModeService.swift
|
│ │ │ ├── Intents/
|
||||||
│ │ │ │ ├── NotificationService.swift
|
│ │ │ │ └── AlarmIntents.swift # App Intents for Stop/Snooze
|
||||||
│ │ │ │ └── NotificationDelegate.swift
|
|
||||||
│ │ │ └── Views/
|
│ │ │ └── Views/
|
||||||
│ │ │ ├── AlarmView.swift
|
│ │ │ ├── AlarmView.swift
|
||||||
│ │ │ ├── AddAlarmView.swift
|
│ │ │ ├── AddAlarmView.swift
|
||||||
│ │ │ ├── EditAlarmView.swift
|
│ │ │ ├── EditAlarmView.swift
|
||||||
│ │ │ ├── AlarmScreen.swift
|
|
||||||
│ │ │ └── Components/
|
│ │ │ └── Components/
|
||||||
│ │ │ ├── AlarmRowView.swift
|
│ │ │ ├── AlarmRowView.swift
|
||||||
│ │ │ ├── EmptyAlarmsView.swift
|
│ │ │ ├── EmptyAlarmsView.swift
|
||||||
|
|||||||
@ -88,14 +88,28 @@ final class AlarmKitService {
|
|||||||
textColor: .red,
|
textColor: .red,
|
||||||
systemImageName: "stop.fill"
|
systemImageName: "stop.fill"
|
||||||
)
|
)
|
||||||
Design.debugLog("[alarmkit] Created stop button")
|
|
||||||
|
|
||||||
// Create the alert presentation (sound is in AlarmConfiguration, not here)
|
// Create the snooze button (secondary button with countdown behavior)
|
||||||
let alert = AlarmPresentation.Alert(
|
let snoozeButton = AlarmButton(
|
||||||
title: LocalizedStringResource(stringLiteral: alarm.label),
|
text: "Snooze",
|
||||||
stopButton: stopButton
|
textColor: .white,
|
||||||
|
systemImageName: "moon.zzz"
|
||||||
)
|
)
|
||||||
Design.debugLog("[alarmkit] Created alert presentation")
|
Design.debugLog("[alarmkit] Created stop and snooze buttons")
|
||||||
|
|
||||||
|
// Create the alert presentation with snooze as secondary button
|
||||||
|
// secondaryButtonBehavior: .countdown enables snooze functionality
|
||||||
|
// Include both label and notification message in the title
|
||||||
|
let alertTitle = alarm.notificationMessage.isEmpty
|
||||||
|
? alarm.label
|
||||||
|
: "\(alarm.label) - \(alarm.notificationMessage)"
|
||||||
|
let alert = AlarmPresentation.Alert(
|
||||||
|
title: LocalizedStringResource(stringLiteral: alertTitle),
|
||||||
|
stopButton: stopButton,
|
||||||
|
secondaryButton: snoozeButton,
|
||||||
|
secondaryButtonBehavior: .countdown
|
||||||
|
)
|
||||||
|
Design.debugLog("[alarmkit] Created alert with title: \(alertTitle)")
|
||||||
|
|
||||||
// Create metadata for the alarm
|
// Create metadata for the alarm
|
||||||
let metadata = NoiseClockAlarmMetadata(
|
let metadata = NoiseClockAlarmMetadata(
|
||||||
@ -103,9 +117,10 @@ final class AlarmKitService {
|
|||||||
soundName: alarm.soundName,
|
soundName: alarm.soundName,
|
||||||
snoozeDuration: alarm.snoozeDuration,
|
snoozeDuration: alarm.snoozeDuration,
|
||||||
label: alarm.label,
|
label: alarm.label,
|
||||||
|
message: alarm.notificationMessage,
|
||||||
volume: alarm.volume
|
volume: alarm.volume
|
||||||
)
|
)
|
||||||
Design.debugLog("[alarmkit] Created metadata: alarmId=\(metadata.alarmId), sound=\(metadata.soundName)")
|
Design.debugLog("[alarmkit] Created metadata: alarmId=\(metadata.alarmId), sound=\(metadata.soundName), message=\(metadata.message)")
|
||||||
|
|
||||||
// Create alarm attributes
|
// Create alarm attributes
|
||||||
let attributes = AlarmAttributes<NoiseClockAlarmMetadata>(
|
let attributes = AlarmAttributes<NoiseClockAlarmMetadata>(
|
||||||
@ -119,12 +134,15 @@ final class AlarmKitService {
|
|||||||
let schedule = createSchedule(for: alarm)
|
let schedule = createSchedule(for: alarm)
|
||||||
Design.debugLog("[alarmkit] Created schedule")
|
Design.debugLog("[alarmkit] Created schedule")
|
||||||
|
|
||||||
// Create countdown duration (5 min before alarm, 1 min after)
|
// CountdownDuration for snooze support:
|
||||||
let countdownDuration = AlarmKit.Alarm.CountdownDuration(
|
// - preAlert: nil = no countdown before alarm (fires immediately at scheduled time)
|
||||||
preAlert: 300, // 5 minutes before
|
// - postAlert: snooze duration (how long until alarm fires again after snooze)
|
||||||
postAlert: 60 // 1 minute after
|
// If no snooze, set countdownDuration to nil
|
||||||
)
|
let snoozeDurationSeconds = TimeInterval(alarm.snoozeDuration * 60)
|
||||||
Design.debugLog("[alarmkit] Countdown duration: preAlert=300s, postAlert=60s")
|
let countdownDuration: AlarmKit.Alarm.CountdownDuration? = snoozeDurationSeconds > 0
|
||||||
|
? AlarmKit.Alarm.CountdownDuration(preAlert: nil, postAlert: snoozeDurationSeconds)
|
||||||
|
: nil
|
||||||
|
Design.debugLog("[alarmkit] Countdown duration: preAlert=nil (immediate), postAlert=\(snoozeDurationSeconds)s (snooze)")
|
||||||
|
|
||||||
// Create the sound
|
// Create the sound
|
||||||
let soundName = getSoundNameForAlarmKit(alarm.soundName)
|
let soundName = getSoundNameForAlarmKit(alarm.soundName)
|
||||||
@ -157,54 +175,138 @@ final class AlarmKitService {
|
|||||||
|
|
||||||
// MARK: - Sound Configuration
|
// MARK: - Sound Configuration
|
||||||
|
|
||||||
/// Get the sound name for AlarmKit (without extension)
|
/// Get the sound name for AlarmKit and ensure it's in Library/Sounds
|
||||||
|
/// AlarmKit can only play sounds from the main bundle root or Library/Sounds
|
||||||
private func getSoundNameForAlarmKit(_ soundName: String) -> String {
|
private func getSoundNameForAlarmKit(_ soundName: String) -> String {
|
||||||
// AlarmKit expects the sound name without extension
|
Design.debugLog("[alarmkit] Preparing sound for AlarmKit: \(soundName)")
|
||||||
|
|
||||||
|
// Copy sound to Library/Sounds so AlarmKit can access it
|
||||||
|
if copySoundToLibrarySounds(soundName) {
|
||||||
|
Design.debugLog("[alarmkit] ✅ Sound ready in Library/Sounds: \(soundName)")
|
||||||
|
} else {
|
||||||
|
Design.debugLog("[alarmkit] ⚠️ Failed to copy sound to Library/Sounds, alarm may use default sound")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlarmKit expects just the filename (with extension) when in Library/Sounds
|
||||||
|
return soundName
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy a sound file from AlarmSounds folder to Library/Sounds
|
||||||
|
/// Returns true if successful or file already exists
|
||||||
|
private func copySoundToLibrarySounds(_ soundName: String) -> Bool {
|
||||||
|
let fileManager = FileManager.default
|
||||||
let nameWithoutExtension = (soundName as NSString).deletingPathExtension
|
let nameWithoutExtension = (soundName as NSString).deletingPathExtension
|
||||||
let ext = (soundName as NSString).pathExtension
|
let ext = (soundName as NSString).pathExtension
|
||||||
|
|
||||||
Design.debugLog("[alarmkit] Sound name for AlarmKit: \(nameWithoutExtension) (from: \(soundName))")
|
// Try multiple locations for the source sound file
|
||||||
|
var sourceURL: URL?
|
||||||
|
|
||||||
// Verify the sound file exists in the bundle
|
// 1. Try AlarmSounds subfolder in main bundle (Resources/AlarmSounds/)
|
||||||
if let _ = Bundle.main.url(forResource: nameWithoutExtension, withExtension: ext) {
|
if let url = Bundle.main.url(forResource: nameWithoutExtension, withExtension: ext, subdirectory: "AlarmSounds") {
|
||||||
Design.debugLog("[alarmkit] ✅ Sound file found in main bundle: \(soundName)")
|
sourceURL = url
|
||||||
} else if let _ = Bundle.main.url(forResource: "AlarmSounds/\(nameWithoutExtension)", withExtension: ext) {
|
Design.debugLog("[alarmkit] Found sound in AlarmSounds subfolder: \(soundName)")
|
||||||
Design.debugLog("[alarmkit] ✅ Sound file found in AlarmSounds folder: \(soundName)")
|
}
|
||||||
} else {
|
// 2. Try AlarmSounds.bundle
|
||||||
Design.debugLog("[alarmkit] ⚠️ Sound file NOT found in bundle: \(soundName)")
|
else if let bundleURL = Bundle.main.url(forResource: "AlarmSounds", withExtension: "bundle"),
|
||||||
Design.debugLog("[alarmkit] ⚠️ AlarmKit may not be able to play this sound")
|
let alarmBundle = Bundle(url: bundleURL),
|
||||||
|
let url = alarmBundle.url(forResource: nameWithoutExtension, withExtension: ext) {
|
||||||
// Log bundle contents for debugging
|
sourceURL = url
|
||||||
logBundleSoundFiles()
|
Design.debugLog("[alarmkit] Found sound in AlarmSounds.bundle: \(soundName)")
|
||||||
|
}
|
||||||
|
// 3. Try main bundle root
|
||||||
|
else if let url = Bundle.main.url(forResource: nameWithoutExtension, withExtension: ext) {
|
||||||
|
sourceURL = url
|
||||||
|
Design.debugLog("[alarmkit] Found sound in main bundle root: \(soundName)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nameWithoutExtension
|
guard let sourceURL = sourceURL else {
|
||||||
|
Design.debugLog("[alarmkit] ❌ Sound file not found anywhere: \(soundName)")
|
||||||
|
logAvailableAlarmSounds()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log available sound files in the bundle for debugging
|
// Get destination URL in Library/Sounds
|
||||||
private func logBundleSoundFiles() {
|
guard let libraryURL = fileManager.urls(for: .libraryDirectory, in: .userDomainMask).first else {
|
||||||
Design.debugLog("[alarmkit] ========== BUNDLE SOUND FILES ==========")
|
Design.debugLog("[alarmkit] ❌ Could not get Library directory")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Check main bundle
|
let soundsDirectory = libraryURL.appendingPathComponent("Sounds")
|
||||||
if let resourcePath = Bundle.main.resourcePath {
|
let destinationURL = soundsDirectory.appendingPathComponent(soundName)
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
// Create Sounds directory if it doesn't exist
|
||||||
|
if !fileManager.fileExists(atPath: soundsDirectory.path) {
|
||||||
do {
|
do {
|
||||||
let files = try fileManager.contentsOfDirectory(atPath: resourcePath)
|
try fileManager.createDirectory(at: soundsDirectory, withIntermediateDirectories: true)
|
||||||
let soundFiles = files.filter { $0.hasSuffix(".mp3") || $0.hasSuffix(".caf") || $0.hasSuffix(".wav") }
|
Design.debugLog("[alarmkit] Created Library/Sounds directory")
|
||||||
if soundFiles.isEmpty {
|
} catch {
|
||||||
Design.debugLog("[alarmkit] No sound files in main bundle root")
|
Design.debugLog("[alarmkit] ❌ Failed to create Sounds directory: \(error)")
|
||||||
} else {
|
return false
|
||||||
Design.debugLog("[alarmkit] Sound files in main bundle: \(soundFiles)")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check AlarmSounds subdirectory
|
// Copy file if it doesn't exist or is different
|
||||||
let alarmSoundsPath = (resourcePath as NSString).appendingPathComponent("AlarmSounds")
|
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||||
if fileManager.fileExists(atPath: alarmSoundsPath) {
|
// Check if source is newer (simple check - could compare file sizes/hashes)
|
||||||
let alarmFiles = try fileManager.contentsOfDirectory(atPath: alarmSoundsPath)
|
Design.debugLog("[alarmkit] Sound already exists in Library/Sounds: \(soundName)")
|
||||||
Design.debugLog("[alarmkit] Sound files in AlarmSounds: \(alarmFiles)")
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try fileManager.copyItem(at: sourceURL, to: destinationURL)
|
||||||
|
Design.debugLog("[alarmkit] ✅ Copied sound to Library/Sounds: \(soundName)")
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
Design.debugLog("[alarmkit] Error listing bundle: \(error)")
|
Design.debugLog("[alarmkit] ❌ Failed to copy sound: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log available sound files in Library/Sounds for debugging
|
||||||
|
private func logLibrarySounds() {
|
||||||
|
guard let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let soundsDirectory = libraryURL.appendingPathComponent("Sounds")
|
||||||
|
Design.debugLog("[alarmkit] ========== LIBRARY/SOUNDS FILES ==========")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let files = try FileManager.default.contentsOfDirectory(atPath: soundsDirectory.path)
|
||||||
|
Design.debugLog("[alarmkit] Files in Library/Sounds: \(files)")
|
||||||
|
} catch {
|
||||||
|
Design.debugLog("[alarmkit] Library/Sounds directory doesn't exist or is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log available alarm sounds in the bundle for debugging
|
||||||
|
private func logAvailableAlarmSounds() {
|
||||||
|
Design.debugLog("[alarmkit] ========== AVAILABLE ALARM SOUNDS ==========")
|
||||||
|
|
||||||
|
// Check AlarmSounds subfolder
|
||||||
|
if let resourcePath = Bundle.main.resourcePath {
|
||||||
|
let alarmSoundsPath = (resourcePath as NSString).appendingPathComponent("AlarmSounds")
|
||||||
|
if FileManager.default.fileExists(atPath: alarmSoundsPath) {
|
||||||
|
do {
|
||||||
|
let files = try FileManager.default.contentsOfDirectory(atPath: alarmSoundsPath)
|
||||||
|
Design.debugLog("[alarmkit] Files in AlarmSounds folder: \(files)")
|
||||||
|
} catch {
|
||||||
|
Design.debugLog("[alarmkit] Error reading AlarmSounds folder: \(error)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Design.debugLog("[alarmkit] AlarmSounds folder doesn't exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check AlarmSounds.bundle
|
||||||
|
if let bundleURL = Bundle.main.url(forResource: "AlarmSounds", withExtension: "bundle"),
|
||||||
|
let alarmBundle = Bundle(url: bundleURL),
|
||||||
|
let bundlePath = alarmBundle.resourcePath {
|
||||||
|
do {
|
||||||
|
let files = try FileManager.default.contentsOfDirectory(atPath: bundlePath)
|
||||||
|
Design.debugLog("[alarmkit] Files in AlarmSounds.bundle: \(files)")
|
||||||
|
} catch {
|
||||||
|
Design.debugLog("[alarmkit] Error reading AlarmSounds.bundle: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
"fileName": "digital-alarm.mp3",
|
"fileName": "digital-alarm.mp3",
|
||||||
"description": "Classic digital alarm sound",
|
"description": "Classic digital alarm sound",
|
||||||
"category": "alarm",
|
"category": "alarm",
|
||||||
"bundleName": null,
|
"bundleName": "AlarmSounds",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -15,7 +15,7 @@
|
|||||||
"fileName": "buzzing-alarm.mp3",
|
"fileName": "buzzing-alarm.mp3",
|
||||||
"description": "Buzzing sound for gentle wake-up",
|
"description": "Buzzing sound for gentle wake-up",
|
||||||
"category": "alarm",
|
"category": "alarm",
|
||||||
"bundleName": null
|
"bundleName": "AlarmSounds"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "classic-alarm",
|
"id": "classic-alarm",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"fileName": "classic-alarm.mp3",
|
"fileName": "classic-alarm.mp3",
|
||||||
"description": "Traditional alarm sound",
|
"description": "Traditional alarm sound",
|
||||||
"category": "alarm",
|
"category": "alarm",
|
||||||
"bundleName": null
|
"bundleName": "AlarmSounds"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "beep-alarm",
|
"id": "beep-alarm",
|
||||||
@ -31,7 +31,7 @@
|
|||||||
"fileName": "beep-alarm.mp3",
|
"fileName": "beep-alarm.mp3",
|
||||||
"description": "Short beep alarm sound",
|
"description": "Short beep alarm sound",
|
||||||
"category": "alarm",
|
"category": "alarm",
|
||||||
"bundleName": null
|
"bundleName": "AlarmSounds"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "siren-alarm",
|
"id": "siren-alarm",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"fileName": "siren-alarm.mp3",
|
"fileName": "siren-alarm.mp3",
|
||||||
"description": "Emergency siren alarm for heavy sleepers",
|
"description": "Emergency siren alarm for heavy sleepers",
|
||||||
"category": "alarm",
|
"category": "alarm",
|
||||||
"bundleName": null
|
"bundleName": "AlarmSounds"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -23,6 +23,9 @@ nonisolated struct NoiseClockAlarmMetadata: AlarmMetadata {
|
|||||||
/// The alarm label to display
|
/// The alarm label to display
|
||||||
var label: String
|
var label: String
|
||||||
|
|
||||||
|
/// The custom notification message
|
||||||
|
var message: String
|
||||||
|
|
||||||
/// Volume level (0.0 to 1.0)
|
/// Volume level (0.0 to 1.0)
|
||||||
var volume: Float
|
var volume: Float
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,9 @@ nonisolated struct NoiseClockAlarmMetadata: AlarmMetadata {
|
|||||||
/// The alarm label to display
|
/// The alarm label to display
|
||||||
var label: String
|
var label: String
|
||||||
|
|
||||||
|
/// The custom notification message
|
||||||
|
var message: String
|
||||||
|
|
||||||
/// Volume level (0.0 to 1.0)
|
/// Volume level (0.0 to 1.0)
|
||||||
var volume: Float
|
var volume: Float
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user