modern ios26
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
4bf7994e31
commit
2f59f2aaf8
@ -56,8 +56,6 @@ struct Alarm: Identifiable, Codable, Equatable {
|
||||
}
|
||||
|
||||
func formattedTime() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: time)
|
||||
time.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,11 +264,7 @@ final class AlarmKitService {
|
||||
|
||||
/// 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")
|
||||
let soundsDirectory = URL.libraryDirectory.appendingPathComponent("Sounds")
|
||||
Design.debugLog("[alarmkit] ========== LIBRARY/SOUNDS FILES ==========")
|
||||
|
||||
do {
|
||||
@ -374,15 +370,14 @@ final class AlarmKitService {
|
||||
/// Create an AlarmKit schedule from an Alarm model.
|
||||
private func createSchedule(for alarm: Alarm) -> AlarmKit.Alarm.Schedule {
|
||||
// Log the raw alarm time
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
|
||||
Design.debugLog("[alarmkit] Raw alarm.time: \(formatter.string(from: alarm.time))")
|
||||
let debugFormat = Date.FormatStyle.dateTime.year().month().day().hour().minute().second().timeZone()
|
||||
Design.debugLog("[alarmkit] Raw alarm.time: \(alarm.time.formatted(debugFormat))")
|
||||
|
||||
// Calculate the next trigger time
|
||||
let triggerDate = alarm.nextTriggerTime()
|
||||
|
||||
Design.debugLog("[alarmkit] Next trigger date: \(formatter.string(from: triggerDate))")
|
||||
Design.debugLog("[alarmkit] Current time: \(formatter.string(from: Date.now))")
|
||||
Design.debugLog("[alarmkit] Next trigger date: \(triggerDate.formatted(debugFormat))")
|
||||
Design.debugLog("[alarmkit] Current time: \(Date.now.formatted(debugFormat))")
|
||||
|
||||
let secondsUntil = triggerDate.timeIntervalSinceNow
|
||||
let minutesUntil = secondsUntil / 60
|
||||
@ -396,7 +391,7 @@ final class AlarmKitService {
|
||||
// Use fixed schedule for one-time alarms
|
||||
let schedule = AlarmKit.Alarm.Schedule.fixed(triggerDate)
|
||||
|
||||
Design.debugLog("[alarmkit] Schedule created: fixed at \(formatter.string(from: triggerDate))")
|
||||
Design.debugLog("[alarmkit] Schedule created: fixed at \(triggerDate.formatted(debugFormat))")
|
||||
return schedule
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import Bedrock
|
||||
/// Service for managing alarm persistence.
|
||||
/// Alarm scheduling is handled by AlarmKitService.
|
||||
@Observable
|
||||
class AlarmService {
|
||||
final class AlarmService {
|
||||
|
||||
// MARK: - Singleton
|
||||
static let shared = AlarmService()
|
||||
@ -24,7 +24,7 @@ class AlarmService {
|
||||
// MARK: - Properties
|
||||
private(set) var alarms: [Alarm] = []
|
||||
private var alarmLookup: [UUID: Int] = [:]
|
||||
private var persistenceWorkItem: DispatchWorkItem?
|
||||
private var persistenceTask: Task<Void, Never>?
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
@ -87,20 +87,16 @@ class AlarmService {
|
||||
}
|
||||
|
||||
private func saveAlarms() {
|
||||
persistenceWorkItem?.cancel()
|
||||
persistenceTask?.cancel()
|
||||
|
||||
let alarmsSnapshot = self.alarms
|
||||
let work = DispatchWorkItem {
|
||||
persistenceTask = Task { @MainActor in
|
||||
try? await Task.sleep(for: .seconds(AppConstants.PersistenceDelays.alarms))
|
||||
guard !Task.isCancelled else { return }
|
||||
if let encoded = try? JSONEncoder().encode(alarmsSnapshot) {
|
||||
UserDefaults.standard.set(encoded, forKey: AppConstants.StorageKeys.savedAlarms)
|
||||
}
|
||||
}
|
||||
persistenceWorkItem = work
|
||||
|
||||
DispatchQueue.main.asyncAfter(
|
||||
deadline: .now() + AppConstants.PersistenceDelays.alarms,
|
||||
execute: work
|
||||
)
|
||||
}
|
||||
|
||||
private func loadAlarms() {
|
||||
|
||||
@ -14,7 +14,7 @@ import Observation
|
||||
/// AlarmKit provides alarms that cut through Focus modes and silent mode,
|
||||
/// with built-in Live Activity countdown and system alarm UI.
|
||||
@Observable
|
||||
class AlarmViewModel {
|
||||
final class AlarmViewModel {
|
||||
|
||||
// MARK: - Properties
|
||||
private let alarmService: AlarmService
|
||||
|
||||
@ -92,6 +92,7 @@ struct AlarmView: View {
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.title2)
|
||||
.symbolEffect(.bounce, value: showAddAlarm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,13 +107,16 @@ struct AlarmView: View {
|
||||
viewModel: viewModel,
|
||||
isPresented: $showAddAlarm
|
||||
)
|
||||
.presentationCornerRadius(Design.CornerRadius.xxLarge)
|
||||
}
|
||||
.sheet(item: $selectedAlarmForEdit) { alarm in
|
||||
EditAlarmView(
|
||||
viewModel: viewModel,
|
||||
alarm: alarm
|
||||
)
|
||||
.presentationCornerRadius(Design.CornerRadius.xxLarge)
|
||||
}
|
||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
@ -41,6 +41,7 @@ struct AlarmRowView: View {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
Text("Foreground only for full alarm sound")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
|
||||
@ -47,6 +47,7 @@ struct EmptyAlarmsView: View {
|
||||
Button(action: onAddAlarm) {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.symbolEffect(.bounce, options: .nonRepeating)
|
||||
Text("Add Your First Alarm")
|
||||
}
|
||||
.typography(.bodyEmphasis)
|
||||
|
||||
@ -71,9 +71,7 @@ struct TimeUntilAlarmSection: View {
|
||||
} else if calendar.isDateInTomorrow(alarmTime) {
|
||||
return "Tomorrow"
|
||||
} else {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE"
|
||||
return formatter.string(from: alarmTime)
|
||||
return alarmTime.formatted(.dateTime.weekday(.wide))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,13 +67,13 @@ struct EditAlarmView: View {
|
||||
NavigationLink(destination: LabelEditView(label: $alarmLabel)) {
|
||||
HStack {
|
||||
Image(systemName: "textformat")
|
||||
.foregroundColor(AppAccent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Label")
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(alarmLabel)
|
||||
.foregroundColor(AppTextColors.secondary)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
}
|
||||
.listRowBackground(AppSurface.card)
|
||||
@ -82,13 +82,13 @@ struct EditAlarmView: View {
|
||||
NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) {
|
||||
HStack {
|
||||
Image(systemName: "message")
|
||||
.foregroundColor(AppAccent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Message")
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(notificationMessage)
|
||||
.foregroundColor(AppTextColors.secondary)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@ -98,13 +98,13 @@ struct EditAlarmView: View {
|
||||
NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) {
|
||||
HStack {
|
||||
Image(systemName: "music.note")
|
||||
.foregroundColor(AppAccent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Sound")
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(getSoundDisplayName(selectedSoundName))
|
||||
.foregroundColor(AppTextColors.secondary)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
}
|
||||
.listRowBackground(AppSurface.card)
|
||||
@ -113,13 +113,13 @@ struct EditAlarmView: View {
|
||||
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
|
||||
HStack {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.foregroundColor(AppAccent.primary)
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Snooze")
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text("for \(snoozeDuration) min")
|
||||
.foregroundColor(AppTextColors.secondary)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
}
|
||||
.listRowBackground(AppSurface.card)
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
/// Service for monitoring device battery level and state
|
||||
@Observable
|
||||
class BatteryService {
|
||||
@MainActor
|
||||
final class BatteryService {
|
||||
|
||||
// MARK: - Properties
|
||||
static let shared = BatteryService()
|
||||
@ -19,11 +19,11 @@ class BatteryService {
|
||||
var batteryLevel: Int = 100
|
||||
var isCharging: Bool = false
|
||||
|
||||
@ObservationIgnored private var cancellables = Set<AnyCancellable>()
|
||||
@ObservationIgnored private var monitoringTask: Task<Void, Never>?
|
||||
|
||||
// MARK: - Initialization
|
||||
private init() {
|
||||
setupBatteryMonitoring()
|
||||
startNotificationMonitoring()
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
@ -41,22 +41,24 @@ class BatteryService {
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func setupBatteryMonitoring() {
|
||||
#if canImport(UIKit)
|
||||
// Listen for battery level changes
|
||||
NotificationCenter.default.publisher(for: UIDevice.batteryLevelDidChangeNotification)
|
||||
.sink { [weak self] _ in
|
||||
private func startNotificationMonitoring() {
|
||||
monitoringTask = Task { [weak self] in
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
// Monitor battery level changes
|
||||
group.addTask { @MainActor [weak self] in
|
||||
for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryLevelDidChangeNotification) {
|
||||
self?.updateBatteryInfo()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// Listen for battery state changes
|
||||
NotificationCenter.default.publisher(for: UIDevice.batteryStateDidChangeNotification)
|
||||
.sink { [weak self] _ in
|
||||
// Monitor battery state changes
|
||||
group.addTask { @MainActor [weak self] in
|
||||
for await _ in NotificationCenter.default.notifications(named: UIDevice.batteryStateDidChangeNotification) {
|
||||
self?.updateBatteryInfo()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBatteryInfo() {
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Observation
|
||||
import AudioPlaybackKit
|
||||
import SwiftUI
|
||||
@ -14,6 +13,7 @@ import Bedrock
|
||||
|
||||
/// ViewModel for clock display and management
|
||||
@Observable
|
||||
@MainActor
|
||||
final class ClockViewModel {
|
||||
|
||||
// MARK: - Properties
|
||||
@ -27,15 +27,13 @@ final class ClockViewModel {
|
||||
// Ambient light service
|
||||
private let ambientLightService = AmbientLightService.shared
|
||||
|
||||
// Timer management
|
||||
private var secondTimer: Timer.TimerPublisher?
|
||||
private var minuteTimer: Timer.TimerPublisher?
|
||||
private var secondCancellable: AnyCancellable?
|
||||
private var minuteCancellable: AnyCancellable?
|
||||
private var styleObserver: NSObjectProtocol?
|
||||
// Async task management (replaces Combine publishers)
|
||||
@ObservationIgnored private var secondTimerTask: Task<Void, Never>?
|
||||
@ObservationIgnored private var minuteTimerTask: Task<Void, Never>?
|
||||
@ObservationIgnored private var styleObserverTask: Task<Void, Never>?
|
||||
|
||||
// Persistence
|
||||
private var persistenceWorkItem: DispatchWorkItem?
|
||||
@ObservationIgnored private var persistenceTask: Task<Void, Never>?
|
||||
private var styleJSON: Data {
|
||||
get {
|
||||
UserDefaults.standard.data(forKey: ClockStyle.appStorageKey) ?? {
|
||||
@ -57,11 +55,10 @@ final class ClockViewModel {
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopTimers()
|
||||
stopAmbientLightMonitoring()
|
||||
if let styleObserver {
|
||||
NotificationCenter.default.removeObserver(styleObserver)
|
||||
}
|
||||
secondTimerTask?.cancel()
|
||||
minuteTimerTask?.cancel()
|
||||
styleObserverTask?.cancel()
|
||||
persistenceTask?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Public Interface
|
||||
@ -153,82 +150,81 @@ final class ClockViewModel {
|
||||
}
|
||||
|
||||
private func observeStyleUpdates() {
|
||||
styleObserver = NotificationCenter.default.addObserver(
|
||||
forName: .clockStyleDidUpdate,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.loadStyle()
|
||||
self?.updateTimersIfNeeded()
|
||||
self?.updateWakeLockState()
|
||||
self?.updateBrightness()
|
||||
styleObserverTask = Task { [weak self] in
|
||||
for await _ in NotificationCenter.default.notifications(named: .clockStyleDidUpdate) {
|
||||
guard let self else { break }
|
||||
self.loadStyle()
|
||||
self.updateTimersIfNeeded()
|
||||
self.updateWakeLockState()
|
||||
self.updateBrightness()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveStyle() {
|
||||
persistenceWorkItem?.cancel()
|
||||
persistenceTask?.cancel()
|
||||
|
||||
let work = DispatchWorkItem {
|
||||
if let data = try? JSONEncoder().encode(self.style) {
|
||||
let style = self.style
|
||||
persistenceTask = Task {
|
||||
try? await Task.sleep(for: .seconds(AppConstants.PersistenceDelays.clockStyle))
|
||||
guard !Task.isCancelled else { return }
|
||||
if let data = try? JSONEncoder().encode(style) {
|
||||
self.styleJSON = data
|
||||
}
|
||||
}
|
||||
persistenceWorkItem = work
|
||||
|
||||
DispatchQueue.main.asyncAfter(
|
||||
deadline: .now() + AppConstants.PersistenceDelays.clockStyle,
|
||||
execute: work
|
||||
)
|
||||
}
|
||||
|
||||
private func setupTimers() {
|
||||
// Always need minute timer for color randomization
|
||||
if minuteTimer == nil {
|
||||
minuteTimer = Timer.publish(every: AppConstants.TimerIntervals.minute, on: .main, in: .common)
|
||||
minuteCancellable = minuteTimer?.autoconnect().sink { _ in
|
||||
// Always need minute timer for color randomization and night mode checks
|
||||
startMinuteTimer()
|
||||
|
||||
// Only create second timer if seconds are shown
|
||||
if style.showSeconds {
|
||||
startSecondTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private func startMinuteTimer() {
|
||||
minuteTimerTask?.cancel()
|
||||
minuteTimerTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.minute))
|
||||
guard !Task.isCancelled, let self else { break }
|
||||
|
||||
if self.style.randomizeColor {
|
||||
self.style.digitColorHex = Color.randomBrightColorHex()
|
||||
self.saveStyle()
|
||||
self.updateBrightness() // Update brightness when color changes
|
||||
self.updateBrightness()
|
||||
}
|
||||
|
||||
// Check for night mode state changes (scheduled night mode)
|
||||
// Force a UI update by updating currentTime slightly
|
||||
self.currentTime = Date()
|
||||
|
||||
// Update brightness if night mode is active
|
||||
self.updateBrightness()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only create second timer if seconds are shown
|
||||
if style.showSeconds && secondTimer == nil {
|
||||
secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common)
|
||||
secondCancellable = secondTimer?.autoconnect().sink { now in
|
||||
self.currentTime = now
|
||||
private func startSecondTimer() {
|
||||
secondTimerTask?.cancel()
|
||||
secondTimerTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.second))
|
||||
guard !Task.isCancelled, let self else { break }
|
||||
self.currentTime = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTimers() {
|
||||
secondCancellable?.cancel()
|
||||
minuteCancellable?.cancel()
|
||||
secondCancellable = nil
|
||||
minuteCancellable = nil
|
||||
secondTimer = nil
|
||||
minuteTimer = nil
|
||||
private func stopSecondTimer() {
|
||||
secondTimerTask?.cancel()
|
||||
secondTimerTask = nil
|
||||
}
|
||||
|
||||
private func updateTimersIfNeeded() {
|
||||
if style.showSeconds && secondTimer == nil {
|
||||
secondTimer = Timer.publish(every: AppConstants.TimerIntervals.second, on: .main, in: .common)
|
||||
secondCancellable = secondTimer?.autoconnect().sink { now in
|
||||
self.currentTime = now
|
||||
}
|
||||
} else if !style.showSeconds && secondTimer != nil {
|
||||
secondCancellable?.cancel()
|
||||
secondCancellable = nil
|
||||
secondTimer = nil
|
||||
if style.showSeconds && secondTimerTask == nil {
|
||||
startSecondTimer()
|
||||
} else if !style.showSeconds && secondTimerTask != nil {
|
||||
stopSecondTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,16 +249,10 @@ final class ClockViewModel {
|
||||
|
||||
// Set up callback to respond to brightness changes
|
||||
ambientLightService.onBrightnessChange = { [weak self] in
|
||||
//Design.debugLog("[brightness] ClockViewModel: Received brightness change notification")
|
||||
self?.updateBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop ambient light monitoring
|
||||
private func stopAmbientLightMonitoring() {
|
||||
ambientLightService.stopMonitoring()
|
||||
}
|
||||
|
||||
/// Update brightness based on color theme and night mode settings
|
||||
private func updateBrightness() {
|
||||
if style.autoBrightness {
|
||||
|
||||
@ -25,8 +25,12 @@ struct BatteryOverlayView: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: batteryIcon)
|
||||
.foregroundStyle(batteryColor)
|
||||
.symbolEffect(.pulse, options: .repeating, isActive: isCharging)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
Text("\(batteryLevel)%")
|
||||
.foregroundStyle(color)
|
||||
.contentTransition(.numericText())
|
||||
.animation(.snappy(duration: 0.3), value: batteryLevel)
|
||||
}
|
||||
.opacity(clamped)
|
||||
.font(.callout.weight(.semibold))
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// Component for displaying date overlay
|
||||
struct DateOverlayView: View {
|
||||
@ -17,8 +16,6 @@ struct DateOverlayView: View {
|
||||
let dateFormat: String
|
||||
|
||||
@State private var dateString: String = ""
|
||||
@State private var minuteTimer: Timer.TimerPublisher?
|
||||
@State private var minuteCancellable: AnyCancellable?
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
@ -30,31 +27,20 @@ struct DateOverlayView: View {
|
||||
.font(.callout.weight(.semibold))
|
||||
.onAppear {
|
||||
updateDate()
|
||||
startMinuteUpdates()
|
||||
}
|
||||
.onDisappear {
|
||||
stopMinuteUpdates()
|
||||
}
|
||||
.onChange(of: dateFormat) { _, _ in
|
||||
updateDate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func startMinuteUpdates() {
|
||||
let pub = Timer.publish(every: AppConstants.TimerIntervals.minute, on: .main, in: .common)
|
||||
minuteTimer = pub
|
||||
minuteCancellable = pub.autoconnect().sink { _ in
|
||||
.task {
|
||||
// Periodically update the date every minute
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(AppConstants.TimerIntervals.minute))
|
||||
updateDate()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopMinuteUpdates() {
|
||||
minuteCancellable?.cancel()
|
||||
minuteCancellable = nil
|
||||
minuteTimer = nil
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func updateDate() {
|
||||
dateString = Date().formattedForOverlay(format: dateFormat)
|
||||
}
|
||||
|
||||
@ -18,9 +18,7 @@ struct NextAlarmOverlay: View {
|
||||
|
||||
private var alarmString: String {
|
||||
guard let time = alarmTime else { return "" }
|
||||
let formatter = DateFormatter()
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: time)
|
||||
return time.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
@ -30,6 +28,7 @@ struct NextAlarmOverlay: View {
|
||||
HStack(spacing: Design.Spacing.xxSmall) {
|
||||
Image(systemName: "alarm.fill")
|
||||
.font(.caption)
|
||||
.symbolEffect(.bounce, value: alarmTime)
|
||||
|
||||
Text(alarmString)
|
||||
.typography(.calloutEmphasis)
|
||||
|
||||
@ -26,12 +26,14 @@ struct NoiseMiniPlayer: View {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Button(action: onToggle) {
|
||||
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(AppAccent.primary.opacity(0.8))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(isPlaying ? "Playing" : "Paused")
|
||||
|
||||
@ -54,6 +54,8 @@ struct AdvancedDisplaySection: View {
|
||||
Text("\(Int(style.effectiveBrightness * 100))%")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.contentTransition(.numericText())
|
||||
.animation(.snappy(duration: 0.3), value: style.effectiveBrightness)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
|
||||
@ -49,8 +49,9 @@ struct TimePickerView: View {
|
||||
}
|
||||
|
||||
private func updateStringFromTime(_ time: Date) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
timeString = formatter.string(from: time)
|
||||
let calendar = Calendar.current
|
||||
let hour = calendar.component(.hour, from: time)
|
||||
let minute = calendar.component(.minute, from: time)
|
||||
timeString = String(format: "%02d:%02d", hour, minute)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ struct SoundCategoryView: View {
|
||||
}
|
||||
|
||||
private var categoryTabs: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
ForEach(categories) { category in
|
||||
CategoryTab(
|
||||
@ -95,6 +95,7 @@ struct SoundCategoryView: View {
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
|
||||
private var soundGrid: some View {
|
||||
|
||||
@ -56,14 +56,8 @@ struct OnboardingWelcomePage: View {
|
||||
struct OnboardingClockText: View {
|
||||
let date: Date
|
||||
|
||||
private static let formatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "h:mm"
|
||||
return df
|
||||
}()
|
||||
|
||||
private var timeString: String {
|
||||
Self.formatter.string(from: date)
|
||||
date.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute(.twoDigits))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@ -17,6 +17,7 @@ struct KeepAwakePrompt: View {
|
||||
Image(systemName: "bolt.fill")
|
||||
.font(.system(size: 36, weight: .semibold))
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.symbolEffect(.bounce, options: .nonRepeating)
|
||||
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
Text("Keep Awake for Alarms")
|
||||
@ -48,6 +49,7 @@ struct KeepAwakePrompt: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(AppSurface.primary)
|
||||
.presentationDetents([.medium])
|
||||
.presentationCornerRadius(Design.CornerRadius.xxLarge)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import Foundation
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
class KeepAwakePromptState {
|
||||
final class KeepAwakePromptState {
|
||||
|
||||
var isPresented = false
|
||||
private var hasShownThisSession = false
|
||||
|
||||
Loading…
Reference in New Issue
Block a user