more refactoring by codex
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
86e4382cc2
commit
d2c6a09e47
37
PRD.md
37
PRD.md
@ -484,8 +484,7 @@ TheNoiseClock/
|
|||||||
│ │ └── OnboardingPageView.swift
|
│ │ └── OnboardingPageView.swift
|
||||||
│ └── Resources/
|
│ └── Resources/
|
||||||
│ ├── LaunchScreen.storyboard # Branded native launch screen
|
│ ├── LaunchScreen.storyboard # Branded native launch screen
|
||||||
│ ├── sounds.json # Ambient sound configuration and definitions
|
│ ├── SoundsSettings.json # Shared audio settings
|
||||||
│ ├── alarm-sounds.json # Alarm sound configuration and definitions
|
|
||||||
│ ├── Ambient.bundle/ # Ambient sound category
|
│ ├── Ambient.bundle/ # Ambient sound category
|
||||||
│ │ └── white-noise.mp3
|
│ │ └── white-noise.mp3
|
||||||
│ ├── Nature.bundle/ # Nature sound category
|
│ ├── Nature.bundle/ # Nature sound category
|
||||||
@ -493,11 +492,12 @@ TheNoiseClock/
|
|||||||
│ ├── Mechanical.bundle/ # Mechanical sound category
|
│ ├── Mechanical.bundle/ # Mechanical sound category
|
||||||
│ │ └── fan-white-noise-heater.mp3
|
│ │ └── fan-white-noise-heater.mp3
|
||||||
│ ├── AlarmSounds.bundle/ # Alarm sound category
|
│ ├── AlarmSounds.bundle/ # Alarm sound category
|
||||||
│ │ ├── digital-alarm.caf
|
│ │ ├── sounds.json # Alarm sound metadata
|
||||||
│ │ ├── classic-alarm.caf
|
│ │ ├── digital-alarm.mp3
|
||||||
│ │ ├── beep-alarm.caf
|
│ │ ├── classic-alarm.mp3
|
||||||
│ │ ├── siren-alarm.caf
|
│ │ ├── beep-alarm.mp3
|
||||||
│ │ └── buzzing-alarm.caf
|
│ │ ├── siren-alarm.mp3
|
||||||
|
│ │ └── buzzing-alarm.mp3
|
||||||
│ └── Assets.xcassets/
|
│ └── Assets.xcassets/
|
||||||
│ └── [Asset catalogs]
|
│ └── [Asset catalogs]
|
||||||
└── TheNoiseClock.xcodeproj/ # Xcode project with AudioPlaybackKit dependency
|
└── TheNoiseClock.xcodeproj/ # Xcode project with AudioPlaybackKit dependency
|
||||||
@ -727,8 +727,8 @@ The following terminal commands are used for building and testing the project. T
|
|||||||
# Navigate to project directory
|
# Navigate to project directory
|
||||||
cd /Users/mattbruce/Documents/Projects/TheNoiseClock
|
cd /Users/mattbruce/Documents/Projects/TheNoiseClock
|
||||||
|
|
||||||
# Build for iOS Simulator (iPad mini)
|
# Build for iOS Simulator (iPhone 17 Pro Max)
|
||||||
xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPad mini (A17 Pro),OS=18.1' build
|
xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.2' build
|
||||||
|
|
||||||
# Build for iOS Simulator (any device)
|
# Build for iOS Simulator (any device)
|
||||||
xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=Any iOS Simulator Device' build
|
xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=Any iOS Simulator Device' build
|
||||||
@ -740,7 +740,7 @@ xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock build
|
|||||||
#### Error Checking Commands
|
#### Error Checking Commands
|
||||||
```bash
|
```bash
|
||||||
# Check for build errors only (filtered output)
|
# Check for build errors only (filtered output)
|
||||||
xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPad mini (A17 Pro),OS=18.1' build 2>&1 | grep -E "(error:|warning:|failed)" | head -10
|
xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.2' build 2>&1 | grep -E "(error:|warning:|failed)" | head -10
|
||||||
|
|
||||||
# Quick syntax check for specific files
|
# Quick syntax check for specific files
|
||||||
swift -frontend -parse TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift
|
swift -frontend -parse TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift
|
||||||
@ -749,15 +749,7 @@ swift -frontend -parse TheNoiseClock/Views/Clock/Components/DigitView.swift
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Available Simulators
|
#### Available Simulators
|
||||||
The following simulators are available for testing:
|
Use **iPhone 17 Pro Max (iOS 26.2)** as the primary simulator for build and test validation.
|
||||||
- **iPad mini (A17 Pro)** - Primary testing device
|
|
||||||
- **iPad (10th generation)**
|
|
||||||
- **iPad Air 11-inch (M2)**
|
|
||||||
- **iPad Air 13-inch (M2)**
|
|
||||||
- **iPad Pro 11-inch (M4)**
|
|
||||||
- **iPad Pro 13-inch (M4)**
|
|
||||||
- **iPhone 16, 16 Plus, 16 Pro, 16 Pro Max**
|
|
||||||
- **iPhone SE (3rd generation)**
|
|
||||||
|
|
||||||
#### Build Troubleshooting
|
#### Build Troubleshooting
|
||||||
1. **Provisioning Profile Errors**: Use iOS Simulator builds instead of device builds
|
1. **Provisioning Profile Errors**: Use iOS Simulator builds instead of device builds
|
||||||
@ -772,6 +764,13 @@ The following simulators are available for testing:
|
|||||||
4. **Test on simulator** using Xcode or terminal build
|
4. **Test on simulator** using Xcode or terminal build
|
||||||
5. **Update PRD** if architectural changes are made
|
5. **Update PRD** if architectural changes are made
|
||||||
|
|
||||||
|
#### Recent Engineering Quality Updates
|
||||||
|
- Alarm creation now defaults to `digital-alarm.mp3` (matches bundled assets and AlarmKit expectations).
|
||||||
|
- `AlarmSoundService` now caches decoded configuration/settings and falls back gracefully if bundled JSON is missing or malformed.
|
||||||
|
- Alarm-change notifications are now separated from clock-style notifications using `Notification.Name.alarmsDidUpdate`.
|
||||||
|
- Date overlay formatting now uses a per-thread `DateFormatter` cache to reduce formatter churn.
|
||||||
|
- Shared `TheNoiseClock` scheme now includes both unit and UI test targets for consistent `xcodebuild test` execution.
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
### Project Information
|
### Project Information
|
||||||
|
|||||||
18
README.md
18
README.md
@ -90,7 +90,13 @@ Open `TheNoiseClock.xcodeproj` and run the `TheNoiseClock` scheme.
|
|||||||
### Terminal Build
|
### Terminal Build
|
||||||
```bash
|
```bash
|
||||||
cd /Users/mattbruce/Documents/Projects/iPhone/TheNoiseClock
|
cd /Users/mattbruce/Documents/Projects/iPhone/TheNoiseClock
|
||||||
xcodebuild -project TheNoiseClock/TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPad mini (A17 Pro),OS=18.1' build
|
xcodebuild -project TheNoiseClock/TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.2' build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal Test
|
||||||
|
```bash
|
||||||
|
cd /Users/mattbruce/Documents/Projects/iPhone/TheNoiseClock
|
||||||
|
xcodebuild -project TheNoiseClock/TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,OS=26.2' test
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -125,6 +131,16 @@ Swift access is provided via:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Recent Engineering Updates
|
||||||
|
|
||||||
|
- Alarm defaults now use `digital-alarm.mp3` for consistent AlarmKit scheduling.
|
||||||
|
- Alarm sound metadata loading now caches decoded data and falls back safely if configuration files are missing/corrupt.
|
||||||
|
- Alarm UI refresh uses a dedicated `alarmsDidUpdate` notification instead of reusing clock-style notifications.
|
||||||
|
- Date overlay formatting now uses cached formatters to reduce repeated allocation overhead.
|
||||||
|
- Shared scheme configuration includes both unit and UI tests under `xcodebuild ... -scheme TheNoiseClock test`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
TheNoiseClock follows a clean, modular structure:
|
TheNoiseClock follows a clean, modular structure:
|
||||||
|
|||||||
@ -551,7 +551,7 @@
|
|||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@ -585,7 +585,7 @@
|
|||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@ -607,9 +607,9 @@
|
|||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TheNoiseClock.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TheNoiseClock";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/The Noise Clock.app/The Noise Clock";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -630,9 +630,9 @@
|
|||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TheNoiseClock.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TheNoiseClock";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/The Noise Clock.app/The Noise Clock";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@ -651,7 +651,7 @@
|
|||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_TARGET_NAME = TheNoiseClock;
|
TEST_TARGET_NAME = TheNoiseClock;
|
||||||
};
|
};
|
||||||
@ -672,7 +672,7 @@
|
|||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_TARGET_NAME = TheNoiseClock;
|
TEST_TARGET_NAME = TheNoiseClock;
|
||||||
};
|
};
|
||||||
@ -692,7 +692,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@ -711,7 +711,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import Foundation
|
|||||||
/// Intent to stop an active alarm from the Live Activity or notification.
|
/// Intent to stop an active alarm from the Live Activity or notification.
|
||||||
struct StopAlarmIntent: LiveActivityIntent {
|
struct StopAlarmIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static var title: LocalizedStringResource = "Stop Alarm"
|
static let title: LocalizedStringResource = "Stop Alarm"
|
||||||
static var description = IntentDescription("Stops the currently ringing alarm")
|
static let description = IntentDescription("Stops the currently ringing alarm")
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: "Alarm ID")
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
@ -48,8 +48,8 @@ struct StopAlarmIntent: LiveActivityIntent {
|
|||||||
/// Intent to snooze an active alarm from the Live Activity or notification.
|
/// Intent to snooze an active alarm from the Live Activity or notification.
|
||||||
struct SnoozeAlarmIntent: LiveActivityIntent {
|
struct SnoozeAlarmIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static var title: LocalizedStringResource = "Snooze Alarm"
|
static let title: LocalizedStringResource = "Snooze Alarm"
|
||||||
static var description = IntentDescription("Snoozes the currently ringing alarm")
|
static let description = IntentDescription("Snoozes the currently ringing alarm")
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: "Alarm ID")
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
@ -80,9 +80,9 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
|
|||||||
/// Intent to open the app when the user taps the Live Activity.
|
/// Intent to open the app when the user taps the Live Activity.
|
||||||
struct OpenAlarmAppIntent: LiveActivityIntent {
|
struct OpenAlarmAppIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static var title: LocalizedStringResource = "Open TheNoiseClock"
|
static let title: LocalizedStringResource = "Open TheNoiseClock"
|
||||||
static var description = IntentDescription("Opens the app to the alarm screen")
|
static let description = IntentDescription("Opens the app to the alarm screen")
|
||||||
static var openAppWhenRun = true
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: "Alarm ID")
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
|
|||||||
@ -19,29 +19,28 @@ final class AlarmKitService {
|
|||||||
// MARK: - Singleton
|
// MARK: - Singleton
|
||||||
|
|
||||||
static let shared = AlarmKitService()
|
static let shared = AlarmKitService()
|
||||||
private let manager = AlarmManager.shared
|
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
Design.debugLog("[alarmkit] AlarmKitService initialized")
|
Design.debugLog("[alarmkit] AlarmKitService initialized")
|
||||||
Design.debugLog("[alarmkit] Authorization state: \(manager.authorizationState)")
|
Design.debugLog("[alarmkit] Authorization state: \(AlarmManager.shared.authorizationState)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Authorization
|
// MARK: - Authorization
|
||||||
|
|
||||||
/// The current authorization state for AlarmKit
|
/// The current authorization state for AlarmKit
|
||||||
var authorizationState: AlarmManager.AuthorizationState {
|
var authorizationState: AlarmManager.AuthorizationState {
|
||||||
manager.authorizationState
|
AlarmManager.shared.authorizationState
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request authorization to schedule alarms.
|
/// Request authorization to schedule alarms.
|
||||||
/// - Returns: `true` if authorized, `false` otherwise.
|
/// - Returns: `true` if authorized, `false` otherwise.
|
||||||
func requestAuthorization() async -> Bool {
|
func requestAuthorization() async -> Bool {
|
||||||
Design.debugLog("[alarmkit] Requesting authorization, current state: \(manager.authorizationState)")
|
Design.debugLog("[alarmkit] Requesting authorization, current state: \(AlarmManager.shared.authorizationState)")
|
||||||
|
|
||||||
switch manager.authorizationState {
|
switch AlarmManager.shared.authorizationState {
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
do {
|
do {
|
||||||
let state = try await manager.requestAuthorization()
|
let state = try await AlarmManager.shared.requestAuthorization()
|
||||||
Design.debugLog("[alarmkit] Authorization result: \(state)")
|
Design.debugLog("[alarmkit] Authorization result: \(state)")
|
||||||
return state == .authorized
|
return state == .authorized
|
||||||
} catch {
|
} catch {
|
||||||
@ -73,7 +72,7 @@ final class AlarmKitService {
|
|||||||
Design.debugLog("[alarmkit] ID: \(alarm.id)")
|
Design.debugLog("[alarmkit] ID: \(alarm.id)")
|
||||||
|
|
||||||
// Ensure we're authorized
|
// Ensure we're authorized
|
||||||
if manager.authorizationState != .authorized {
|
if AlarmManager.shared.authorizationState != .authorized {
|
||||||
Design.debugLog("[alarmkit] Not authorized, requesting...")
|
Design.debugLog("[alarmkit] Not authorized, requesting...")
|
||||||
let authorized = await requestAuthorization()
|
let authorized = await requestAuthorization()
|
||||||
guard authorized else {
|
guard authorized else {
|
||||||
@ -160,7 +159,7 @@ final class AlarmKitService {
|
|||||||
|
|
||||||
// Schedule the alarm
|
// Schedule the alarm
|
||||||
do {
|
do {
|
||||||
let scheduledAlarm = try await manager.schedule(
|
let scheduledAlarm = try await AlarmManager.shared.schedule(
|
||||||
id: alarm.id,
|
id: alarm.id,
|
||||||
configuration: configuration
|
configuration: configuration
|
||||||
)
|
)
|
||||||
@ -299,7 +298,7 @@ final class AlarmKitService {
|
|||||||
func cancelAlarm(id: UUID) {
|
func cancelAlarm(id: UUID) {
|
||||||
Design.debugLog("[alarmkit] Cancelling alarm: \(id)")
|
Design.debugLog("[alarmkit] Cancelling alarm: \(id)")
|
||||||
do {
|
do {
|
||||||
try manager.cancel(id: id)
|
try AlarmManager.shared.cancel(id: id)
|
||||||
Design.debugLog("[alarmkit] ✅ Alarm cancelled: \(id)")
|
Design.debugLog("[alarmkit] ✅ Alarm cancelled: \(id)")
|
||||||
} catch {
|
} catch {
|
||||||
Design.debugLog("[alarmkit] ❌ Cancel error: \(error)")
|
Design.debugLog("[alarmkit] ❌ Cancel error: \(error)")
|
||||||
@ -311,7 +310,7 @@ final class AlarmKitService {
|
|||||||
func stopAlarm(id: UUID) {
|
func stopAlarm(id: UUID) {
|
||||||
Design.debugLog("[alarmkit] Stopping alarm: \(id)")
|
Design.debugLog("[alarmkit] Stopping alarm: \(id)")
|
||||||
do {
|
do {
|
||||||
try manager.stop(id: id)
|
try AlarmManager.shared.stop(id: id)
|
||||||
Design.debugLog("[alarmkit] ✅ Alarm stopped: \(id)")
|
Design.debugLog("[alarmkit] ✅ Alarm stopped: \(id)")
|
||||||
} catch {
|
} catch {
|
||||||
Design.debugLog("[alarmkit] ❌ Stop error: \(error)")
|
Design.debugLog("[alarmkit] ❌ Stop error: \(error)")
|
||||||
@ -323,7 +322,7 @@ final class AlarmKitService {
|
|||||||
func snoozeAlarm(id: UUID) {
|
func snoozeAlarm(id: UUID) {
|
||||||
Design.debugLog("[alarmkit] Snoozing alarm: \(id)")
|
Design.debugLog("[alarmkit] Snoozing alarm: \(id)")
|
||||||
do {
|
do {
|
||||||
try manager.countdown(id: id)
|
try AlarmManager.shared.countdown(id: id)
|
||||||
Design.debugLog("[alarmkit] ✅ Alarm snoozed: \(id)")
|
Design.debugLog("[alarmkit] ✅ Alarm snoozed: \(id)")
|
||||||
} catch {
|
} catch {
|
||||||
Design.debugLog("[alarmkit] ❌ Snooze error: \(error)")
|
Design.debugLog("[alarmkit] ❌ Snooze error: \(error)")
|
||||||
@ -334,14 +333,14 @@ final class AlarmKitService {
|
|||||||
|
|
||||||
/// Async sequence that emits the current set of alarms whenever changes occur.
|
/// Async sequence that emits the current set of alarms whenever changes occur.
|
||||||
var alarmUpdates: some AsyncSequence<[AlarmKit.Alarm], Never> {
|
var alarmUpdates: some AsyncSequence<[AlarmKit.Alarm], Never> {
|
||||||
manager.alarmUpdates
|
AlarmManager.shared.alarmUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log current state of all scheduled alarms
|
/// Log current state of all scheduled alarms
|
||||||
func logCurrentAlarms() {
|
func logCurrentAlarms() {
|
||||||
Design.debugLog("[alarmkit] ========== CURRENT ALARMS ==========")
|
Design.debugLog("[alarmkit] ========== CURRENT ALARMS ==========")
|
||||||
Task {
|
Task {
|
||||||
for await alarms in manager.alarmUpdates {
|
for await alarms in AlarmManager.shared.alarmUpdates {
|
||||||
Design.debugLog("[alarmkit] Found \(alarms.count) alarm(s) in AlarmKit")
|
Design.debugLog("[alarmkit] Found \(alarms.count) alarm(s) in AlarmKit")
|
||||||
for alarm in alarms {
|
for alarm in alarms {
|
||||||
Design.debugLog("[alarmkit] - ID: \(alarm.id)")
|
Design.debugLog("[alarmkit] - ID: \(alarm.id)")
|
||||||
|
|||||||
@ -40,7 +40,7 @@ final class AlarmService {
|
|||||||
alarms.append(alarm)
|
alarms.append(alarm)
|
||||||
updateAlarmLookup()
|
updateAlarmLookup()
|
||||||
saveAlarms()
|
saveAlarms()
|
||||||
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
NotificationCenter.default.post(name: .alarmsDidUpdate, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update an alarm in storage. Does NOT reschedule - caller should use AlarmKitService.
|
/// Update an alarm in storage. Does NOT reschedule - caller should use AlarmKitService.
|
||||||
@ -53,7 +53,7 @@ final class AlarmService {
|
|||||||
alarms[index] = alarm
|
alarms[index] = alarm
|
||||||
updateAlarmLookup()
|
updateAlarmLookup()
|
||||||
saveAlarms()
|
saveAlarms()
|
||||||
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
NotificationCenter.default.post(name: .alarmsDidUpdate, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete an alarm from storage. Does NOT cancel - caller should use AlarmKitService.
|
/// Delete an alarm from storage. Does NOT cancel - caller should use AlarmKitService.
|
||||||
@ -62,7 +62,7 @@ final class AlarmService {
|
|||||||
alarms.removeAll { $0.id == id }
|
alarms.removeAll { $0.id == id }
|
||||||
updateAlarmLookup()
|
updateAlarmLookup()
|
||||||
saveAlarms()
|
saveAlarms()
|
||||||
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
NotificationCenter.default.post(name: .alarmsDidUpdate, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle an alarm's enabled state. Does NOT reschedule - caller should use AlarmKitService.
|
/// Toggle an alarm's enabled state. Does NOT reschedule - caller should use AlarmKitService.
|
||||||
@ -71,7 +71,7 @@ final class AlarmService {
|
|||||||
alarms[index].isEnabled.toggle()
|
alarms[index].isEnabled.toggle()
|
||||||
Design.debugLog("[alarms] AlarmService.toggleAlarm: \(id) now enabled=\(alarms[index].isEnabled)")
|
Design.debugLog("[alarms] AlarmService.toggleAlarm: \(id) now enabled=\(alarms[index].isEnabled)")
|
||||||
saveAlarms()
|
saveAlarms()
|
||||||
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
NotificationCenter.default.post(name: .alarmsDidUpdate, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlarm(id: UUID) -> Alarm? {
|
func getAlarm(id: UUID) -> Alarm? {
|
||||||
|
|||||||
@ -10,41 +10,85 @@ import AudioPlaybackKit
|
|||||||
import Bedrock
|
import Bedrock
|
||||||
|
|
||||||
/// Extension service for alarm-specific sound functionality
|
/// Extension service for alarm-specific sound functionality
|
||||||
class AlarmSoundService {
|
final class AlarmSoundService {
|
||||||
static let shared = AlarmSoundService()
|
static let shared = AlarmSoundService()
|
||||||
|
|
||||||
// MARK: - Constants
|
// MARK: - Constants
|
||||||
/// The category ID for alarm sounds as defined in alarm-sounds.json
|
/// The category ID for alarm sounds as defined in alarm-sounds.json
|
||||||
static let alarmCategoryId = "alarm"
|
static let alarmCategoryId = "alarm"
|
||||||
|
|
||||||
|
private let lock = NSLock()
|
||||||
|
private var cachedConfiguration: SoundConfiguration?
|
||||||
|
private var cachedSettings: AudioSettings?
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
/// Load alarm sound configuration from AlarmSounds.bundle
|
/// Load alarm sound configuration from AlarmSounds.bundle
|
||||||
private func loadAlarmConfiguration() -> SoundConfiguration {
|
private func loadAlarmConfiguration() -> SoundConfiguration {
|
||||||
guard let bundleURL = Bundle.main.url(forResource: "AlarmSounds", withExtension: "bundle"),
|
lock.lock()
|
||||||
let alarmBundle = Bundle(url: bundleURL),
|
if let cachedConfiguration {
|
||||||
let url = alarmBundle.url(forResource: "sounds", withExtension: "json") else {
|
lock.unlock()
|
||||||
fatalError("❌ sounds.json not found in AlarmSounds.bundle. Ensure the bundle and file exist.")
|
return cachedConfiguration
|
||||||
}
|
}
|
||||||
|
lock.unlock()
|
||||||
|
|
||||||
|
let configuration: SoundConfiguration
|
||||||
|
if let bundleURL = Bundle.main.url(forResource: "AlarmSounds", withExtension: "bundle"),
|
||||||
|
let alarmBundle = Bundle(url: bundleURL),
|
||||||
|
let url = alarmBundle.url(forResource: "sounds", withExtension: "json") {
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
let soundsOnly = try JSONDecoder().decode(SoundsOnly.self, from: data)
|
let soundsOnly = try JSONDecoder().decode(SoundsOnly.self, from: data)
|
||||||
|
configuration = SoundConfiguration(sounds: soundsOnly.sounds, settings: loadAudioSettings())
|
||||||
// Load settings from separate SoundsSettings.json file
|
|
||||||
let settings = loadAudioSettings()
|
|
||||||
|
|
||||||
return SoundConfiguration(sounds: soundsOnly.sounds, settings: settings)
|
|
||||||
} catch {
|
} catch {
|
||||||
fatalError("❌ Error loading alarm sound configuration: \(error)")
|
Design.debugLog("[audio] Warning: Failed to decode alarm sound config, using fallback sounds: \(error)")
|
||||||
|
configuration = SoundConfiguration(sounds: fallbackAlarmSounds(), settings: loadAudioSettings())
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Design.debugLog("[audio] Warning: AlarmSounds.bundle/sounds.json missing, using fallback sounds")
|
||||||
|
configuration = SoundConfiguration(sounds: fallbackAlarmSounds(), settings: loadAudioSettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.lock()
|
||||||
|
cachedConfiguration = configuration
|
||||||
|
lock.unlock()
|
||||||
|
return configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load audio settings from SoundsSettings.json
|
/// Load audio settings from SoundsSettings.json
|
||||||
private func loadAudioSettings() -> AudioSettings {
|
private func loadAudioSettings() -> AudioSettings {
|
||||||
|
lock.lock()
|
||||||
|
if let cachedSettings {
|
||||||
|
lock.unlock()
|
||||||
|
return cachedSettings
|
||||||
|
}
|
||||||
|
lock.unlock()
|
||||||
|
|
||||||
|
let settings: AudioSettings
|
||||||
guard let url = Bundle.main.url(forResource: "SoundsSettings", withExtension: "json") else {
|
guard let url = Bundle.main.url(forResource: "SoundsSettings", withExtension: "json") else {
|
||||||
Design.debugLog("[general] Warning: SoundsSettings.json not found, using default alarm settings")
|
Design.debugLog("[general] Warning: SoundsSettings.json not found, using default alarm settings")
|
||||||
return AudioSettings(
|
settings = AudioSettings(
|
||||||
|
defaultVolume: 1.0,
|
||||||
|
defaultLoopCount: -1,
|
||||||
|
preloadSounds: true,
|
||||||
|
preloadStrategy: "category",
|
||||||
|
audioSessionCategory: "playback",
|
||||||
|
audioSessionMode: "default",
|
||||||
|
audioSessionOptions: ["mixWithOthers"]
|
||||||
|
)
|
||||||
|
lock.lock()
|
||||||
|
cachedSettings = settings
|
||||||
|
lock.unlock()
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
settings = try JSONDecoder().decode(AudioSettings.self, from: data)
|
||||||
|
//Design.debugLog("[settings] Loaded audio settings for alarms from SoundsSettings.json")
|
||||||
|
} catch {
|
||||||
|
Design.debugLog("[general] Warning: Error loading audio settings for alarms, using defaults: \(error)")
|
||||||
|
settings = AudioSettings(
|
||||||
defaultVolume: 1.0,
|
defaultVolume: 1.0,
|
||||||
defaultLoopCount: -1,
|
defaultLoopCount: -1,
|
||||||
preloadSounds: true,
|
preloadSounds: true,
|
||||||
@ -55,23 +99,56 @@ class AlarmSoundService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
lock.lock()
|
||||||
let data = try Data(contentsOf: url)
|
cachedSettings = settings
|
||||||
let settings = try JSONDecoder().decode(AudioSettings.self, from: data)
|
lock.unlock()
|
||||||
//Design.debugLog("[settings] Loaded audio settings for alarms from SoundsSettings.json")
|
|
||||||
return settings
|
return settings
|
||||||
} catch {
|
|
||||||
Design.debugLog("[general] Warning: Error loading audio settings for alarms, using defaults: \(error)")
|
|
||||||
return AudioSettings(
|
|
||||||
defaultVolume: 1.0,
|
|
||||||
defaultLoopCount: -1,
|
|
||||||
preloadSounds: true,
|
|
||||||
preloadStrategy: "category",
|
|
||||||
audioSessionCategory: "playback",
|
|
||||||
audioSessionMode: "default",
|
|
||||||
audioSessionOptions: ["mixWithOthers"]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fallbackAlarmSounds() -> [Sound] {
|
||||||
|
[
|
||||||
|
Sound(
|
||||||
|
id: "digital-alarm",
|
||||||
|
name: "Digital Alarm",
|
||||||
|
fileName: "digital-alarm.mp3",
|
||||||
|
category: Self.alarmCategoryId,
|
||||||
|
description: "Classic digital alarm sound",
|
||||||
|
bundleName: "AlarmSounds",
|
||||||
|
isDefault: true
|
||||||
|
),
|
||||||
|
Sound(
|
||||||
|
id: "buzzing-alarm",
|
||||||
|
name: "Buzzing Alarm",
|
||||||
|
fileName: "buzzing-alarm.mp3",
|
||||||
|
category: Self.alarmCategoryId,
|
||||||
|
description: "Buzzing sound for gentle wake-up",
|
||||||
|
bundleName: "AlarmSounds"
|
||||||
|
),
|
||||||
|
Sound(
|
||||||
|
id: "classic-alarm",
|
||||||
|
name: "Classic Alarm",
|
||||||
|
fileName: "classic-alarm.mp3",
|
||||||
|
category: Self.alarmCategoryId,
|
||||||
|
description: "Traditional alarm sound",
|
||||||
|
bundleName: "AlarmSounds"
|
||||||
|
),
|
||||||
|
Sound(
|
||||||
|
id: "beep-alarm",
|
||||||
|
name: "Beep Alarm",
|
||||||
|
fileName: "beep-alarm.mp3",
|
||||||
|
category: Self.alarmCategoryId,
|
||||||
|
description: "Short beep alarm sound",
|
||||||
|
bundleName: "AlarmSounds"
|
||||||
|
),
|
||||||
|
Sound(
|
||||||
|
id: "siren-alarm",
|
||||||
|
name: "Siren Alarm",
|
||||||
|
fileName: "siren-alarm.mp3",
|
||||||
|
category: Self.alarmCategoryId,
|
||||||
|
description: "Emergency siren alarm for heavy sleepers",
|
||||||
|
bundleName: "AlarmSounds"
|
||||||
|
)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all available alarm sounds
|
/// Get all available alarm sounds
|
||||||
|
|||||||
@ -17,7 +17,7 @@ struct AddAlarmView: View {
|
|||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
|
|
||||||
@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.caf"
|
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
|
||||||
@State private var alarmLabel = "Alarm"
|
@State private var alarmLabel = "Alarm"
|
||||||
@State private var notificationMessage = "Your alarm is ringing"
|
@State private var notificationMessage = "Your alarm is ringing"
|
||||||
@State private var snoozeDuration = 9 // minutes
|
@State private var snoozeDuration = 9 // minutes
|
||||||
|
|||||||
@ -19,7 +19,8 @@ final class BatteryService {
|
|||||||
var batteryLevel: Int = 100
|
var batteryLevel: Int = 100
|
||||||
var isCharging: Bool = false
|
var isCharging: Bool = false
|
||||||
|
|
||||||
@ObservationIgnored private var monitoringTask: Task<Void, Never>?
|
@ObservationIgnored private var levelNotificationTask: Task<Void, Never>?
|
||||||
|
@ObservationIgnored private var stateNotificationTask: Task<Void, Never>?
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
private init() {
|
private init() {
|
||||||
@ -42,21 +43,20 @@ final class BatteryService {
|
|||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
private func startNotificationMonitoring() {
|
private func startNotificationMonitoring() {
|
||||||
monitoringTask = Task { [weak self] in
|
levelNotificationTask?.cancel()
|
||||||
await withTaskGroup(of: Void.self) { group in
|
stateNotificationTask?.cancel()
|
||||||
// Monitor battery level changes
|
|
||||||
group.addTask { @MainActor [weak self] in
|
levelNotificationTask = Task { [weak self] in
|
||||||
for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryLevelDidChangeNotification) {
|
for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryLevelDidChangeNotification) {
|
||||||
self?.updateBatteryInfo()
|
guard !Task.isCancelled else { break }
|
||||||
|
await self?.updateBatteryInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitor battery state changes
|
stateNotificationTask = Task { [weak self] in
|
||||||
group.addTask { @MainActor [weak self] in
|
|
||||||
for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryStateDidChangeNotification) {
|
for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryStateDidChangeNotification) {
|
||||||
self?.updateBatteryInfo()
|
guard !Task.isCancelled else { break }
|
||||||
}
|
await self?.updateBatteryInfo()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,9 @@ struct TopOverlayView: View {
|
|||||||
let _ = clockUpdateTrigger // Force re-render on style or alarm changes
|
let _ = clockUpdateTrigger // Force re-render on style or alarm changes
|
||||||
let _ = alarmService.alarms // Observe all alarms for changes
|
let _ = alarmService.alarms // Observe all alarms for changes
|
||||||
let _ = soundPlayer.isPlaying // Observe player state
|
let _ = soundPlayer.isPlaying // Observe player state
|
||||||
|
let nextEnabledAlarm = alarmService.enabledAlarms
|
||||||
let _ = print("TopOverlayView: Rendering. Alarms count: \(alarmService.alarms.count), Enabled: \(alarmService.enabledAlarms.count)")
|
.sorted(by: { $0.time.nextOccurrence() < $1.time.nextOccurrence() })
|
||||||
|
.first
|
||||||
|
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
// Row 1: Date and Battery (Standard Status Bar positions)
|
// Row 1: Date and Battery (Standard Status Bar positions)
|
||||||
@ -57,7 +58,7 @@ struct TopOverlayView: View {
|
|||||||
|
|
||||||
// Row 2: Alarms and Noise Controls (Below Dynamic Island)
|
// Row 2: Alarms and Noise Controls (Below Dynamic Island)
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if showNextAlarm, let nextAlarm = alarmService.enabledAlarms.sorted(by: { $0.time.nextOccurrence() < $1.time.nextOccurrence() }).first {
|
if showNextAlarm, let nextAlarm = nextEnabledAlarm {
|
||||||
NextAlarmOverlay(
|
NextAlarmOverlay(
|
||||||
alarmTime: nextAlarm.time.nextOccurrence(),
|
alarmTime: nextAlarm.time.nextOccurrence(),
|
||||||
color: color,
|
color: color,
|
||||||
@ -88,8 +89,8 @@ struct TopOverlayView: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.id(clockUpdateTrigger) // Force re-render on style or alarm changes
|
.id(clockUpdateTrigger) // Force re-render on alarm changes
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .clockStyleDidUpdate)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .alarmsDidUpdate)) { _ in
|
||||||
clockUpdateTrigger.toggle()
|
clockUpdateTrigger.toggle()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import Bedrock
|
|||||||
|
|
||||||
// MARK: - NoiseClock Surface Colors
|
// MARK: - NoiseClock Surface Colors
|
||||||
|
|
||||||
public enum NoiseClockSurfaceColors: SurfaceColorProvider {
|
public enum NoiseClockSurfaceColors: @MainActor SurfaceColorProvider {
|
||||||
public static let primary = Color(red: 0.06, green: 0.08, blue: 0.12)
|
public static let primary = Color(red: 0.06, green: 0.08, blue: 0.12)
|
||||||
public static let secondary = Color(red: 0.09, green: 0.11, blue: 0.18)
|
public static let secondary = Color(red: 0.09, green: 0.11, blue: 0.18)
|
||||||
public static let tertiary = Color(red: 0.12, green: 0.15, blue: 0.22)
|
public static let tertiary = Color(red: 0.12, green: 0.15, blue: 0.22)
|
||||||
@ -22,7 +22,7 @@ public enum NoiseClockSurfaceColors: SurfaceColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Text Colors
|
// MARK: - NoiseClock Text Colors
|
||||||
|
|
||||||
public enum NoiseClockTextColors: TextColorProvider {
|
public enum NoiseClockTextColors: @MainActor TextColorProvider {
|
||||||
public static let primary = Color.white
|
public static let primary = Color.white
|
||||||
public static let secondary = Color.white.opacity(Design.Opacity.accent)
|
public static let secondary = Color.white.opacity(Design.Opacity.accent)
|
||||||
public static let tertiary = Color.white.opacity(Design.Opacity.medium)
|
public static let tertiary = Color.white.opacity(Design.Opacity.medium)
|
||||||
@ -33,7 +33,7 @@ public enum NoiseClockTextColors: TextColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Accent Colors
|
// MARK: - NoiseClock Accent Colors
|
||||||
|
|
||||||
public enum NoiseClockAccentColors: AccentColorProvider {
|
public enum NoiseClockAccentColors: @MainActor AccentColorProvider {
|
||||||
public static let primary = Color(red: 0.45, green: 0.75, blue: 1.00)
|
public static let primary = Color(red: 0.45, green: 0.75, blue: 1.00)
|
||||||
public static let light = Color(red: 0.65, green: 0.85, blue: 1.00)
|
public static let light = Color(red: 0.65, green: 0.85, blue: 1.00)
|
||||||
public static let dark = Color(red: 0.25, green: 0.55, blue: 0.90)
|
public static let dark = Color(red: 0.25, green: 0.55, blue: 0.90)
|
||||||
@ -42,7 +42,7 @@ public enum NoiseClockAccentColors: AccentColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Button Colors
|
// MARK: - NoiseClock Button Colors
|
||||||
|
|
||||||
public enum NoiseClockButtonColors: ButtonColorProvider {
|
public enum NoiseClockButtonColors: @MainActor ButtonColorProvider {
|
||||||
public static let primaryLight = Color(red: 0.55, green: 0.80, blue: 1.00)
|
public static let primaryLight = Color(red: 0.55, green: 0.80, blue: 1.00)
|
||||||
public static let primaryDark = Color(red: 0.20, green: 0.50, blue: 0.85)
|
public static let primaryDark = Color(red: 0.20, green: 0.50, blue: 0.85)
|
||||||
public static let secondary = Color.white.opacity(Design.Opacity.subtle)
|
public static let secondary = Color.white.opacity(Design.Opacity.subtle)
|
||||||
@ -52,7 +52,7 @@ public enum NoiseClockButtonColors: ButtonColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Status Colors
|
// MARK: - NoiseClock Status Colors
|
||||||
|
|
||||||
public enum NoiseClockStatusColors: StatusColorProvider {
|
public enum NoiseClockStatusColors: @MainActor StatusColorProvider {
|
||||||
public static let success = Color(red: 0.20, green: 0.80, blue: 0.45)
|
public static let success = Color(red: 0.20, green: 0.80, blue: 0.45)
|
||||||
public static let warning = Color(red: 1.00, green: 0.75, blue: 0.20)
|
public static let warning = Color(red: 1.00, green: 0.75, blue: 0.20)
|
||||||
public static let error = Color(red: 0.90, green: 0.30, blue: 0.30)
|
public static let error = Color(red: 0.90, green: 0.30, blue: 0.30)
|
||||||
@ -61,7 +61,7 @@ public enum NoiseClockStatusColors: StatusColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Border Colors
|
// MARK: - NoiseClock Border Colors
|
||||||
|
|
||||||
public enum NoiseClockBorderColors: BorderColorProvider {
|
public enum NoiseClockBorderColors: @MainActor BorderColorProvider {
|
||||||
public static let subtle = Color.white.opacity(Design.Opacity.subtle)
|
public static let subtle = Color.white.opacity(Design.Opacity.subtle)
|
||||||
public static let standard = Color.white.opacity(Design.Opacity.hint)
|
public static let standard = Color.white.opacity(Design.Opacity.hint)
|
||||||
public static let emphasized = Color.white.opacity(Design.Opacity.light)
|
public static let emphasized = Color.white.opacity(Design.Opacity.light)
|
||||||
@ -70,7 +70,7 @@ public enum NoiseClockBorderColors: BorderColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Interactive Colors
|
// MARK: - NoiseClock Interactive Colors
|
||||||
|
|
||||||
public enum NoiseClockInteractiveColors: InteractiveColorProvider {
|
public enum NoiseClockInteractiveColors: @MainActor InteractiveColorProvider {
|
||||||
public static let selected = NoiseClockAccentColors.primary.opacity(Design.Opacity.selection)
|
public static let selected = NoiseClockAccentColors.primary.opacity(Design.Opacity.selection)
|
||||||
public static let hover = Color.white.opacity(Design.Opacity.subtle)
|
public static let hover = Color.white.opacity(Design.Opacity.subtle)
|
||||||
public static let pressed = Color.white.opacity(Design.Opacity.hint)
|
public static let pressed = Color.white.opacity(Design.Opacity.hint)
|
||||||
@ -79,7 +79,7 @@ public enum NoiseClockInteractiveColors: InteractiveColorProvider {
|
|||||||
|
|
||||||
// MARK: - NoiseClock Theme
|
// MARK: - NoiseClock Theme
|
||||||
|
|
||||||
public enum NoiseClockTheme: AppColorTheme {
|
public enum NoiseClockTheme: @MainActor AppColorTheme {
|
||||||
public typealias Surface = NoiseClockSurfaceColors
|
public typealias Surface = NoiseClockSurfaceColors
|
||||||
public typealias Text = NoiseClockTextColors
|
public typealias Text = NoiseClockTextColors
|
||||||
public typealias Accent = NoiseClockAccentColors
|
public typealias Accent = NoiseClockAccentColors
|
||||||
|
|||||||
@ -8,14 +8,29 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Date {
|
extension Date {
|
||||||
|
private static let formatterCacheKeyPrefix = "TheNoiseClock.DateFormatter."
|
||||||
|
|
||||||
/// Format date for display in overlay with custom format
|
/// Format date for display in overlay with custom format
|
||||||
/// - Parameter format: Date format string (e.g., "d MMM yyyy")
|
/// - Parameter format: Date format string (e.g., "d MMM yyyy")
|
||||||
/// - Returns: Formatted date string
|
/// - Returns: Formatted date string
|
||||||
func formattedForOverlay(format: String = "d MMM yyyy") -> String {
|
func formattedForOverlay(format: String = "d MMM yyyy") -> String {
|
||||||
|
let formatter = Self.cachedFormatter(for: format)
|
||||||
|
return formatter.string(from: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func cachedFormatter(for format: String) -> DateFormatter {
|
||||||
|
let cacheKey = formatterCacheKeyPrefix + format
|
||||||
|
let threadDictionary = Thread.current.threadDictionary
|
||||||
|
if let cached = threadDictionary[cacheKey] as? DateFormatter {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = format
|
formatter.dateFormat = format
|
||||||
return formatter.string(from: self)
|
formatter.locale = .autoupdatingCurrent
|
||||||
|
formatter.calendar = .autoupdatingCurrent
|
||||||
|
threadDictionary[cacheKey] = formatter
|
||||||
|
return formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get available date format options with their display names
|
/// Get available date format options with their display names
|
||||||
|
|||||||
@ -10,4 +10,5 @@ import Foundation
|
|||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let keepAwakePromptRequested = Notification.Name("keepAwakePromptRequested")
|
static let keepAwakePromptRequested = Notification.Name("keepAwakePromptRequested")
|
||||||
static let clockStyleDidUpdate = Notification.Name("clockStyleDidUpdate")
|
static let clockStyleDidUpdate = Notification.Name("clockStyleDidUpdate")
|
||||||
|
static let alarmsDidUpdate = Notification.Name("alarmsDidUpdate")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,43 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Testing
|
import Testing
|
||||||
@testable import TheNoiseClock
|
@testable import The_Noise_Clock
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct TheNoiseClockTests {
|
struct TheNoiseClockTests {
|
||||||
|
|
||||||
@Test func example() async throws {
|
@Test
|
||||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
func defaultAlarmSoundIsMP3() {
|
||||||
|
#expect(AppConstants.SystemSounds.defaultSound.hasSuffix(".mp3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func nextOccurrenceIsInFuture() {
|
||||||
|
let oneMinuteAgo = Calendar.current.date(byAdding: .minute, value: -1, to: Date()) ?? Date()
|
||||||
|
let next = oneMinuteAgo.nextOccurrence()
|
||||||
|
|
||||||
|
#expect(next > Date())
|
||||||
|
#expect(next.timeIntervalSinceNow < 26 * 60 * 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func clockStyleCodableRoundTripPreservesSettings() throws {
|
||||||
|
let style = ClockStyle()
|
||||||
|
style.showSeconds = true
|
||||||
|
style.showDate = false
|
||||||
|
style.keepAwake = true
|
||||||
|
style.fontFamily = .avenir
|
||||||
|
style.digitAnimationStyle = .glitch
|
||||||
|
|
||||||
|
let data = try JSONEncoder().encode(style)
|
||||||
|
let decoded = try JSONDecoder().decode(ClockStyle.self, from: data)
|
||||||
|
|
||||||
|
#expect(decoded.showSeconds)
|
||||||
|
#expect(decoded.showDate == false)
|
||||||
|
#expect(decoded.keepAwake)
|
||||||
|
#expect(decoded.fontFamily == .avenir)
|
||||||
|
#expect(decoded.digitAnimationStyle == .glitch)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import Foundation
|
|||||||
/// Intent to stop an active alarm from the Live Activity or notification.
|
/// Intent to stop an active alarm from the Live Activity or notification.
|
||||||
struct StopAlarmIntent: LiveActivityIntent {
|
struct StopAlarmIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static var title: LocalizedStringResource = "Stop Alarm"
|
static let title: LocalizedStringResource = "Stop Alarm"
|
||||||
static var description = IntentDescription("Stops the currently ringing alarm")
|
static let description = IntentDescription("Stops the currently ringing alarm")
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: "Alarm ID")
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
@ -49,8 +49,8 @@ struct StopAlarmIntent: LiveActivityIntent {
|
|||||||
/// Intent to snooze an active alarm from the Live Activity or notification.
|
/// Intent to snooze an active alarm from the Live Activity or notification.
|
||||||
struct SnoozeAlarmIntent: LiveActivityIntent {
|
struct SnoozeAlarmIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static var title: LocalizedStringResource = "Snooze Alarm"
|
static let title: LocalizedStringResource = "Snooze Alarm"
|
||||||
static var description = IntentDescription("Snoozes the currently ringing alarm")
|
static let description = IntentDescription("Snoozes the currently ringing alarm")
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: "Alarm ID")
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
@ -81,9 +81,9 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
|
|||||||
/// Intent to open the app when the user taps the Live Activity.
|
/// Intent to open the app when the user taps the Live Activity.
|
||||||
struct OpenAlarmAppIntent: LiveActivityIntent {
|
struct OpenAlarmAppIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static var title: LocalizedStringResource = "Open TheNoiseClock"
|
static let title: LocalizedStringResource = "Open TheNoiseClock"
|
||||||
static var description = IntentDescription("Opens the app to the alarm screen")
|
static let description = IntentDescription("Opens the app to the alarm screen")
|
||||||
static var openAppWhenRun = true
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: "Alarm ID")
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user