Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
409 changes: 409 additions & 0 deletions SleepFocus.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion SleepFocus/Components/Home/HomeHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SwiftUI

struct HomeHeaderView: View {
@Binding var selectedDate: Date
let maxSelectableDate: Date
var isSyncing: Bool = false
@State private var isShowingDatePicker = false

Expand Down Expand Up @@ -45,7 +46,7 @@ struct HomeHeaderView: View {
get: { selectedDate },
set: { selectedDate = Calendar.current.startOfDay(for: $0) }
),
in: ...Date(),
in: ...maxSelectableDate,
displayedComponents: .date
)
.datePickerStyle(.graphical)
Expand Down
101 changes: 99 additions & 2 deletions SleepFocus/Components/Home/HomeMainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@ struct HomeMainView: View {
@EnvironmentObject var authManager: AuthenticationManager
@EnvironmentObject var sleepPreferences: SleepPreferencesStore
@EnvironmentObject var notificationSettings: NotificationSettingsStore
@Environment(\.scenePhase) private var scenePhase
@StateObject private var homeSummaryStore = HomeSummaryStore()
@State private var selectedDate = Calendar.current.startOfDay(for: Date())
@State private var hasDetectedSleepForToday = false
@State private var didUserSelectDate = false
@State private var cutoverReferenceDate = Date()

var body: some View {
ScrollView {
VStack(spacing: 12) {
HomeHeaderView(
selectedDate: $selectedDate,
selectedDate: Binding(
get: { selectedDate },
set: { newValue in
didUserSelectDate = true
selectedDate = Calendar.current.startOfDay(for: newValue)
}
),
maxSelectableDate: maxSelectableDate,
isSyncing: homeSummaryStore.isSyncing || homeSummaryStore.isWaitingForModel
)

Expand All @@ -28,7 +39,9 @@ struct HomeMainView: View {
SmartAlarmCard(
isEnabled: sleepPreferences.smartAlarmEnabled,
isSmartAlarmMode: sleepPreferences.smartAlarmMode == .bedtime,
targetTime: sleepPreferences.currentSmartAlarmTriggerDisplay,
targetTime: sleepPreferences.smartAlarmMode == .automatic
? sleepPreferences.automaticModePrimaryDisplay
: sleepPreferences.currentSmartAlarmTriggerDisplay,
overrideAlarmTime: sleepPreferences.date(for: sleepPreferences.smartAlarmWakeTimeMinutes),
onOverrideAlarmChange: { updatedDate in
sleepPreferences.updateSmartAlarmWakeTime(updatedDate)
Expand Down Expand Up @@ -59,9 +72,93 @@ struct HomeMainView: View {
.background(Color.appBackground)
.task(id: Calendar.current.startOfDay(for: selectedDate)) {
await homeSummaryStore.load(using: authManager, for: selectedDate)
refreshCutoverState()
await sleepPreferences.refreshSmartAlarmCycleGuidance(using: authManager)
await sleepPreferences.syncSmartAlarmRuntime(notificationSettings: notificationSettings)
}
.task {
await runCutoverLoop()
}
.onChange(of: scenePhase) { _, phase in
guard phase == .active else { return }
refreshCutoverState()
}
}

private var maxSelectableDate: Date {
let calendar = Calendar.current
let reference = cutoverReferenceDate
let todayStart = calendar.startOfDay(for: reference)
let noon = calendar.date(byAdding: .hour, value: 12, to: todayStart) ?? todayStart
if reference >= noon || hasDetectedSleepForToday {
return calendar.date(byAdding: .day, value: 1, to: todayStart) ?? todayStart
}

return reference
}

private func refreshCutoverState() {
cutoverReferenceDate = Date()
updateSleepDetectionSnapshotIfNeeded(reference: cutoverReferenceDate)
autoAdvanceToNextSessionDateIfNeeded(reference: cutoverReferenceDate)
}

private func updateSleepDetectionSnapshotIfNeeded(reference: Date) {
let calendar = Calendar.current
let todayStart = calendar.startOfDay(for: reference)

guard calendar.isDate(selectedDate, inSameDayAs: todayStart) else {
return
}

if homeSummaryStore.hasLocalSleepData {
hasDetectedSleepForToday = true
sleepPreferences.noteLocalSleepDetected(reference: reference, calendar: calendar)
}
}

private func autoAdvanceToNextSessionDateIfNeeded(reference: Date) {
guard !didUserSelectDate else {
return
}

let calendar = Calendar.current
let todayStart = calendar.startOfDay(for: reference)
let noon = calendar.date(byAdding: .hour, value: 12, to: todayStart) ?? todayStart

guard calendar.isDate(selectedDate, inSameDayAs: todayStart) else {
return
}

guard reference >= noon || hasDetectedSleepForToday else {
return
}

selectedDate = calendar.date(byAdding: .day, value: 1, to: todayStart) ?? todayStart
}

private func runCutoverLoop() async {
while !Task.isCancelled {
await MainActor.run {
refreshCutoverState()
}

let now = Date()
let calendar = Calendar.current
let todayStart = calendar.startOfDay(for: now)
let todayNoon = calendar.date(byAdding: .hour, value: 12, to: todayStart) ?? todayStart
let nextNoon = now < todayNoon
? todayNoon
: (calendar.date(byAdding: .day, value: 1, to: todayNoon) ?? todayNoon.addingTimeInterval(24 * 3600))

let waitSeconds = max(nextNoon.timeIntervalSince(now), 5)
let waitNanoseconds = UInt64(waitSeconds * 1_000_000_000)
do {
try await Task.sleep(nanoseconds: waitNanoseconds)
} catch {
return
}
}
}

}
43 changes: 43 additions & 0 deletions SleepFocus/Models/AutoSmartAlarmModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

enum AutoSmartAlarmStatus: String, Codable {
case scheduled
case noFit
case timeout
case unsupported
case permissionDenied
}

struct AutoSmartAlarmConfig: Codable, Equatable {
let enabled: Bool
let mode: String
let preferredBedtimeMinutes: Int
let earliestWakeMinutes: Int
let finalWakeMinutes: Int
let timezoneIdentifier: String
let fallbackWakeTimeMinutes: Int

var isAutomaticModeEnabled: Bool {
enabled && mode == "automatic"
}
}

struct AutoSmartAlarmDecision: Codable, Equatable {
let status: AutoSmartAlarmStatus
let onsetTimestamp: TimeInterval?
let wakeTimestamp: TimeInterval?
let sentAtTimestamp: TimeInterval

var onsetDate: Date? {
onsetTimestamp.map(Date.init(timeIntervalSince1970:))
}

var wakeDate: Date? {
wakeTimestamp.map(Date.init(timeIntervalSince1970:))
}
}

enum AutoSmartAlarmPayloadKey {
static let config = "autoSmartAlarm.config"
static let decision = "autoSmartAlarm.decision"
}
9 changes: 8 additions & 1 deletion SleepFocus/Models/SmartAlarmSharedModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ enum SmartAlarmSharedConfig {

enum SmartAlarmModeDisplay {
static func title(for mode: String) -> String {
mode == "bedtime" ? "Smart Alarm" : "Ideal Bedtime"
switch mode {
case "bedtime":
return "Smart Alarm"
case "automatic":
return "Auto Smart Alarm"
default:
return "Ideal Bedtime"
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions SleepFocus/Models/UserBehaviorProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ final class BehaviorProfileStore: ObservableObject {
defaults.set(encoded, forKey: profileKey)
}
}

typealias BehaviorStore = BehaviorProfileStore
Loading
Loading