Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
c3dad57700
commit
263b2fffcc
@ -12,6 +12,8 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
let id: UUID
|
let id: UUID
|
||||||
var time: Date
|
var time: Date
|
||||||
var isEnabled: Bool
|
var isEnabled: Bool
|
||||||
|
/// Calendar weekday values (1=Sunday...7=Saturday). Empty means one-time alarm.
|
||||||
|
var repeatWeekdays: [Int]
|
||||||
var soundName: String
|
var soundName: String
|
||||||
var label: String
|
var label: String
|
||||||
var notificationMessage: String // Custom notification message
|
var notificationMessage: String // Custom notification message
|
||||||
@ -20,11 +22,26 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
var isLightFlashEnabled: Bool
|
var isLightFlashEnabled: Bool
|
||||||
var volume: Float
|
var volume: Float
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case time
|
||||||
|
case isEnabled
|
||||||
|
case repeatWeekdays
|
||||||
|
case soundName
|
||||||
|
case label
|
||||||
|
case notificationMessage
|
||||||
|
case snoozeDuration
|
||||||
|
case isVibrationEnabled
|
||||||
|
case isLightFlashEnabled
|
||||||
|
case volume
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init(
|
init(
|
||||||
id: UUID = UUID(),
|
id: UUID = UUID(),
|
||||||
time: Date,
|
time: Date,
|
||||||
isEnabled: Bool = true,
|
isEnabled: Bool = true,
|
||||||
|
repeatWeekdays: [Int] = [],
|
||||||
soundName: String = AppConstants.SystemSounds.defaultSound,
|
soundName: String = AppConstants.SystemSounds.defaultSound,
|
||||||
label: String = "Alarm",
|
label: String = "Alarm",
|
||||||
notificationMessage: String = "Your alarm is ringing",
|
notificationMessage: String = "Your alarm is ringing",
|
||||||
@ -36,6 +53,7 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
self.id = id
|
self.id = id
|
||||||
self.time = time
|
self.time = time
|
||||||
self.isEnabled = isEnabled
|
self.isEnabled = isEnabled
|
||||||
|
self.repeatWeekdays = Self.sanitizedWeekdays(repeatWeekdays)
|
||||||
self.soundName = soundName
|
self.soundName = soundName
|
||||||
self.label = label
|
self.label = label
|
||||||
self.notificationMessage = notificationMessage
|
self.notificationMessage = notificationMessage
|
||||||
@ -45,6 +63,23 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
self.volume = volume
|
self.volume = volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.id = try container.decode(UUID.self, forKey: .id)
|
||||||
|
self.time = try container.decode(Date.self, forKey: .time)
|
||||||
|
self.isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
|
||||||
|
self.repeatWeekdays = Self.sanitizedWeekdays(
|
||||||
|
try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? []
|
||||||
|
)
|
||||||
|
self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound
|
||||||
|
self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? "Alarm"
|
||||||
|
self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? "Your alarm is ringing"
|
||||||
|
self.snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 9
|
||||||
|
self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true
|
||||||
|
self.isLightFlashEnabled = try container.decodeIfPresent(Bool.self, forKey: .isLightFlashEnabled) ?? false
|
||||||
|
self.volume = try container.decodeIfPresent(Float.self, forKey: .volume) ?? 1.0
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Equatable
|
// MARK: - Equatable
|
||||||
static func ==(lhs: Alarm, rhs: Alarm) -> Bool {
|
static func ==(lhs: Alarm, rhs: Alarm) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
@ -52,10 +87,70 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
func nextTriggerTime() -> Date {
|
func nextTriggerTime() -> Date {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let now = Date()
|
||||||
|
let timeComponents = calendar.dateComponents([.hour, .minute], from: time)
|
||||||
|
|
||||||
|
guard let hour = timeComponents.hour, let minute = timeComponents.minute else {
|
||||||
|
return time.nextOccurrence()
|
||||||
|
}
|
||||||
|
|
||||||
|
let weekdays = Self.sanitizedWeekdays(repeatWeekdays)
|
||||||
|
guard !weekdays.isEmpty else {
|
||||||
|
return time.nextOccurrence()
|
||||||
|
}
|
||||||
|
|
||||||
|
let today = calendar.startOfDay(for: now)
|
||||||
|
for offset in 0...7 {
|
||||||
|
guard let candidateDay = calendar.date(byAdding: .day, value: offset, to: today),
|
||||||
|
let candidateDate = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: candidateDay)
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let weekday = calendar.component(.weekday, from: candidateDay)
|
||||||
|
if weekdays.contains(weekday), candidateDate > now {
|
||||||
|
return candidateDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return time.nextOccurrence()
|
return time.nextOccurrence()
|
||||||
}
|
}
|
||||||
|
|
||||||
func formattedTime() -> String {
|
func formattedTime() -> String {
|
||||||
time.formatted(date: .omitted, time: .shortened)
|
time.formatted(date: .omitted, time: .shortened)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var repeatSummary: String {
|
||||||
|
Self.repeatSummary(for: repeatWeekdays)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func repeatSummary(for weekdays: [Int]) -> String {
|
||||||
|
let normalized = sanitizedWeekdays(weekdays)
|
||||||
|
guard !normalized.isEmpty else { return "Once" }
|
||||||
|
|
||||||
|
let weekdaySet = Set(normalized)
|
||||||
|
if weekdaySet.count == 7 {
|
||||||
|
return "Every day"
|
||||||
|
}
|
||||||
|
|
||||||
|
if weekdaySet == Set([2, 3, 4, 5, 6]) {
|
||||||
|
return "Weekdays"
|
||||||
|
}
|
||||||
|
|
||||||
|
if weekdaySet == Set([1, 7]) {
|
||||||
|
return "Weekends"
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbols = Calendar.current.shortWeekdaySymbols
|
||||||
|
let labels = normalized.compactMap { weekday -> String? in
|
||||||
|
guard (1...7).contains(weekday) else { return nil }
|
||||||
|
return symbols[weekday - 1]
|
||||||
|
}
|
||||||
|
return labels.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sanitizedWeekdays(_ weekdays: [Int]) -> [Int] {
|
||||||
|
Array(Set(weekdays.filter { (1...7).contains($0) })).sorted()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -359,6 +359,24 @@ final class AlarmKitService {
|
|||||||
let debugFormat = Date.FormatStyle.dateTime.year().month().day().hour().minute().second().timeZone()
|
let debugFormat = Date.FormatStyle.dateTime.year().month().day().hour().minute().second().timeZone()
|
||||||
Design.debugLog("[alarmkit] Raw alarm.time: \(alarm.time.formatted(debugFormat))")
|
Design.debugLog("[alarmkit] Raw alarm.time: \(alarm.time.formatted(debugFormat))")
|
||||||
|
|
||||||
|
let normalizedWeekdays = Alarm.sanitizedWeekdays(alarm.repeatWeekdays)
|
||||||
|
if !normalizedWeekdays.isEmpty {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let components = calendar.dateComponents([.hour, .minute], from: alarm.time)
|
||||||
|
let hour = components.hour ?? 0
|
||||||
|
let minute = components.minute ?? 0
|
||||||
|
let localeWeekdays = normalizedWeekdays.compactMap(localeWeekday(from:))
|
||||||
|
|
||||||
|
Design.debugLog("[alarmkit] Repeat weekdays: \(normalizedWeekdays)")
|
||||||
|
let relative = AlarmKit.Alarm.Schedule.Relative(
|
||||||
|
time: .init(hour: hour, minute: minute),
|
||||||
|
repeats: .weekly(localeWeekdays)
|
||||||
|
)
|
||||||
|
let schedule = AlarmKit.Alarm.Schedule.relative(relative)
|
||||||
|
Design.debugLog("[alarmkit] Schedule created: relative weekly at \(hour):\(String(format: "%02d", minute))")
|
||||||
|
return schedule
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the next trigger time
|
// Calculate the next trigger time
|
||||||
let triggerDate = alarm.nextTriggerTime()
|
let triggerDate = alarm.nextTriggerTime()
|
||||||
|
|
||||||
@ -380,6 +398,19 @@ final class AlarmKitService {
|
|||||||
Design.debugLog("[alarmkit] Schedule created: fixed at \(triggerDate.formatted(debugFormat))")
|
Design.debugLog("[alarmkit] Schedule created: fixed at \(triggerDate.formatted(debugFormat))")
|
||||||
return schedule
|
return schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func localeWeekday(from calendarWeekday: Int) -> Locale.Weekday? {
|
||||||
|
switch calendarWeekday {
|
||||||
|
case 1: return .sunday
|
||||||
|
case 2: return .monday
|
||||||
|
case 3: return .tuesday
|
||||||
|
case 4: return .wednesday
|
||||||
|
case 5: return .thursday
|
||||||
|
case 6: return .friday
|
||||||
|
case 7: return .saturday
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Errors
|
// MARK: - Errors
|
||||||
|
|||||||
@ -132,6 +132,7 @@ final class AlarmViewModel {
|
|||||||
|
|
||||||
func createNewAlarm(
|
func createNewAlarm(
|
||||||
time: Date,
|
time: Date,
|
||||||
|
repeatWeekdays: [Int] = [],
|
||||||
soundName: String = AppConstants.SystemSounds.defaultSound,
|
soundName: String = AppConstants.SystemSounds.defaultSound,
|
||||||
label: String = "Alarm",
|
label: String = "Alarm",
|
||||||
notificationMessage: String = "Your alarm is ringing",
|
notificationMessage: String = "Your alarm is ringing",
|
||||||
@ -144,6 +145,7 @@ final class AlarmViewModel {
|
|||||||
id: UUID(),
|
id: UUID(),
|
||||||
time: time,
|
time: time,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
repeatWeekdays: repeatWeekdays,
|
||||||
soundName: soundName,
|
soundName: soundName,
|
||||||
label: label,
|
label: label,
|
||||||
notificationMessage: notificationMessage,
|
notificationMessage: notificationMessage,
|
||||||
|
|||||||
@ -17,6 +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 repeatWeekdays: [Int] = []
|
||||||
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
|
@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"
|
||||||
@ -30,7 +31,7 @@ struct AddAlarmView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Time picker section at top
|
// Time picker section at top
|
||||||
TimePickerSection(selectedTime: $newAlarmTime)
|
TimePickerSection(selectedTime: $newAlarmTime)
|
||||||
TimeUntilAlarmSection(alarmTime: newAlarmTime)
|
TimeUntilAlarmSection(alarmTime: newAlarmTime, repeatWeekdays: repeatWeekdays)
|
||||||
|
|
||||||
// List for settings below
|
// List for settings below
|
||||||
List {
|
List {
|
||||||
@ -74,6 +75,20 @@ struct AddAlarmView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Repeat Section
|
||||||
|
NavigationLink(destination: RepeatSelectionView(repeatWeekdays: $repeatWeekdays)) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "repeat")
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
.frame(width: 24)
|
||||||
|
Text("Repeat")
|
||||||
|
Spacer()
|
||||||
|
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Snooze Section
|
// Snooze Section
|
||||||
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
|
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
|
||||||
HStack {
|
HStack {
|
||||||
@ -106,6 +121,7 @@ struct AddAlarmView: View {
|
|||||||
Task {
|
Task {
|
||||||
let newAlarm = viewModel.createNewAlarm(
|
let newAlarm = viewModel.createNewAlarm(
|
||||||
time: newAlarmTime,
|
time: newAlarmTime,
|
||||||
|
repeatWeekdays: repeatWeekdays,
|
||||||
soundName: selectedSoundName,
|
soundName: selectedSoundName,
|
||||||
label: alarmLabel,
|
label: alarmLabel,
|
||||||
notificationMessage: notificationMessage,
|
notificationMessage: notificationMessage,
|
||||||
|
|||||||
@ -32,6 +32,10 @@ struct AlarmRowView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
|
||||||
|
Text(alarm.repeatSummary)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
|
|
||||||
Text("• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
|
Text("• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
|||||||
@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// RepeatSelectionView.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 2/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
/// View for selecting repeat days for an alarm.
|
||||||
|
struct RepeatSelectionView: View {
|
||||||
|
@Binding var repeatWeekdays: [Int]
|
||||||
|
|
||||||
|
private let orderedWeekdays = [1, 2, 3, 4, 5, 6, 7]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section("Quick Picks") {
|
||||||
|
quickPickRow(title: "Once", weekdays: [])
|
||||||
|
quickPickRow(title: "Every Day", weekdays: orderedWeekdays)
|
||||||
|
quickPickRow(title: "Weekdays", weekdays: [2, 3, 4, 5, 6])
|
||||||
|
quickPickRow(title: "Weekends", weekdays: [1, 7])
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Repeat On") {
|
||||||
|
ForEach(orderedWeekdays, id: \.self) { weekday in
|
||||||
|
HStack {
|
||||||
|
Text(dayName(for: weekday))
|
||||||
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
Spacer()
|
||||||
|
if normalizedWeekdays.contains(weekday) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
.onTapGesture {
|
||||||
|
toggleDay(weekday)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
|
.navigationTitle("Repeat")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var normalizedWeekdays: [Int] {
|
||||||
|
Alarm.sanitizedWeekdays(repeatWeekdays)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func quickPickRow(title: String, weekdays: [Int]) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
Spacer()
|
||||||
|
if Set(normalizedWeekdays) == Set(Alarm.sanitizedWeekdays(weekdays)) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
.onTapGesture {
|
||||||
|
repeatWeekdays = Alarm.sanitizedWeekdays(weekdays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dayName(for weekday: Int) -> String {
|
||||||
|
let symbols = Calendar.current.weekdaySymbols
|
||||||
|
guard (1...7).contains(weekday) else { return "" }
|
||||||
|
return symbols[weekday - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleDay(_ weekday: Int) {
|
||||||
|
var updated = Set(normalizedWeekdays)
|
||||||
|
if updated.contains(weekday) {
|
||||||
|
updated.remove(weekday)
|
||||||
|
} else {
|
||||||
|
updated.insert(weekday)
|
||||||
|
}
|
||||||
|
repeatWeekdays = Alarm.sanitizedWeekdays(Array(updated))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -10,6 +10,12 @@ import SwiftUI
|
|||||||
/// Component showing time until alarm and day information
|
/// Component showing time until alarm and day information
|
||||||
struct TimeUntilAlarmSection: View {
|
struct TimeUntilAlarmSection: View {
|
||||||
let alarmTime: Date
|
let alarmTime: Date
|
||||||
|
let repeatWeekdays: [Int]
|
||||||
|
|
||||||
|
init(alarmTime: Date, repeatWeekdays: [Int] = []) {
|
||||||
|
self.alarmTime = alarmTime
|
||||||
|
self.repeatWeekdays = repeatWeekdays
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
@ -30,21 +36,13 @@ struct TimeUntilAlarmSection: View {
|
|||||||
.background(AppSurface.primary)
|
.background(AppSurface.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var nextAlarmTime: Date {
|
||||||
|
Alarm(time: alarmTime, repeatWeekdays: repeatWeekdays).nextTriggerTime()
|
||||||
|
}
|
||||||
|
|
||||||
private var timeUntilAlarm: String {
|
private var timeUntilAlarm: String {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
|
|
||||||
// Calculate the next occurrence of the alarm time
|
|
||||||
let nextAlarmTime: Date
|
|
||||||
if calendar.isDateInToday(alarmTime) && alarmTime < now {
|
|
||||||
// If alarm time has passed today, calculate for tomorrow
|
|
||||||
nextAlarmTime = calendar.date(byAdding: .day, value: 1, to: alarmTime) ?? alarmTime
|
|
||||||
} else {
|
|
||||||
// Use the alarm time as-is (either future today or already tomorrow+)
|
|
||||||
nextAlarmTime = alarmTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate time difference from now to next alarm
|
|
||||||
let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime)
|
let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime)
|
||||||
if let hours = components.hour, let minutes = components.minute {
|
if let hours = components.hour, let minutes = components.minute {
|
||||||
if hours > 0 {
|
if hours > 0 {
|
||||||
@ -61,17 +59,12 @@ struct TimeUntilAlarmSection: View {
|
|||||||
|
|
||||||
private var dayText: String {
|
private var dayText: String {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let now = Date()
|
if calendar.isDateInToday(nextAlarmTime) {
|
||||||
|
|
||||||
// If alarm time is in the past today, show tomorrow
|
|
||||||
if calendar.isDateInToday(alarmTime) && alarmTime < now {
|
|
||||||
return "Tomorrow"
|
|
||||||
} else if calendar.isDateInToday(alarmTime) {
|
|
||||||
return "Today"
|
return "Today"
|
||||||
} else if calendar.isDateInTomorrow(alarmTime) {
|
} else if calendar.isDateInTomorrow(nextAlarmTime) {
|
||||||
return "Tomorrow"
|
return "Tomorrow"
|
||||||
} else {
|
} else {
|
||||||
return alarmTime.formatted(.dateTime.weekday(.wide))
|
return nextAlarmTime.formatted(.dateTime.weekday(.wide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ struct EditAlarmView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var alarmTime: Date
|
@State private var alarmTime: Date
|
||||||
|
@State private var repeatWeekdays: [Int]
|
||||||
@State private var selectedSoundName: String
|
@State private var selectedSoundName: String
|
||||||
@State private var alarmLabel: String
|
@State private var alarmLabel: String
|
||||||
@State private var notificationMessage: String
|
@State private var notificationMessage: String
|
||||||
@ -34,6 +35,7 @@ struct EditAlarmView: View {
|
|||||||
|
|
||||||
// Initialize state with current alarm values
|
// Initialize state with current alarm values
|
||||||
self._alarmTime = State(initialValue: alarm.time)
|
self._alarmTime = State(initialValue: alarm.time)
|
||||||
|
self._repeatWeekdays = State(initialValue: alarm.repeatWeekdays)
|
||||||
self._selectedSoundName = State(initialValue: alarm.soundName)
|
self._selectedSoundName = State(initialValue: alarm.soundName)
|
||||||
self._alarmLabel = State(initialValue: alarm.label)
|
self._alarmLabel = State(initialValue: alarm.label)
|
||||||
self._notificationMessage = State(initialValue: alarm.notificationMessage)
|
self._notificationMessage = State(initialValue: alarm.notificationMessage)
|
||||||
@ -49,7 +51,7 @@ struct EditAlarmView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Time picker section at top
|
// Time picker section at top
|
||||||
TimePickerSection(selectedTime: $alarmTime)
|
TimePickerSection(selectedTime: $alarmTime)
|
||||||
TimeUntilAlarmSection(alarmTime: alarmTime)
|
TimeUntilAlarmSection(alarmTime: alarmTime, repeatWeekdays: repeatWeekdays)
|
||||||
|
|
||||||
// List for settings below
|
// List for settings below
|
||||||
List {
|
List {
|
||||||
@ -99,6 +101,22 @@ struct EditAlarmView: View {
|
|||||||
}
|
}
|
||||||
.listRowBackground(AppSurface.card)
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
|
// Repeat Section
|
||||||
|
NavigationLink(destination: RepeatSelectionView(repeatWeekdays: $repeatWeekdays)) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "repeat")
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
.frame(width: 24)
|
||||||
|
Text("Repeat")
|
||||||
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
Spacer()
|
||||||
|
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
||||||
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
// Snooze Section
|
// Snooze Section
|
||||||
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
|
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
|
||||||
HStack {
|
HStack {
|
||||||
@ -137,6 +155,7 @@ struct EditAlarmView: View {
|
|||||||
id: alarm.id, // Keep the same ID
|
id: alarm.id, // Keep the same ID
|
||||||
time: alarmTime,
|
time: alarmTime,
|
||||||
isEnabled: alarm.isEnabled, // Keep the same enabled state
|
isEnabled: alarm.isEnabled, // Keep the same enabled state
|
||||||
|
repeatWeekdays: repeatWeekdays,
|
||||||
soundName: selectedSoundName,
|
soundName: selectedSoundName,
|
||||||
label: alarmLabel,
|
label: alarmLabel,
|
||||||
notificationMessage: notificationMessage,
|
notificationMessage: notificationMessage,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -44,5 +44,31 @@ struct TheNoiseClockTests {
|
|||||||
#expect(decoded.fontFamily == .avenir)
|
#expect(decoded.fontFamily == .avenir)
|
||||||
#expect(decoded.digitAnimationStyle == .glitch)
|
#expect(decoded.digitAnimationStyle == .glitch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func repeatingAlarmComputesNextMatchingWeekday() {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let now = Date()
|
||||||
|
let targetDate = calendar.date(byAdding: .day, value: 2, to: now) ?? now
|
||||||
|
let targetWeekday = calendar.component(.weekday, from: targetDate)
|
||||||
|
|
||||||
|
let templateTime = calendar.date(bySettingHour: 9, minute: 30, second: 0, of: now) ?? now
|
||||||
|
let alarm = Alarm(time: templateTime, repeatWeekdays: [targetWeekday])
|
||||||
|
let next = alarm.nextTriggerTime()
|
||||||
|
|
||||||
|
let components = calendar.dateComponents([.hour, .minute, .weekday], from: next)
|
||||||
|
#expect(next > now)
|
||||||
|
#expect(components.weekday == targetWeekday)
|
||||||
|
#expect(components.hour == 9)
|
||||||
|
#expect(components.minute == 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func repeatSummaryRecognizesCommonPatterns() {
|
||||||
|
#expect(Alarm.repeatSummary(for: []) == "Once")
|
||||||
|
#expect(Alarm.repeatSummary(for: [1, 2, 3, 4, 5, 6, 7]) == "Every day")
|
||||||
|
#expect(Alarm.repeatSummary(for: [2, 3, 4, 5, 6]) == "Weekdays")
|
||||||
|
#expect(Alarm.repeatSummary(for: [1, 7]) == "Weekends")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user