Files
EzTimer/EzTimer/ContentView.swift
2026-01-19 22:10:34 -05:00

133 lines
4.1 KiB
Swift

//
// ContentView.swift
// EzTimer
//
// Created by Jared Evans on 12/16/25.
//
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = TimerViewModel()
// Navigation State
@State private var selectedTab = 0
// Renaming state (Keep here to share via binding or handle alert)
@State private var renamingIndex: Int?
@State private var renamingText = ""
@State private var isRenaming = false
// Alarm State
@State private var flashOpacity = false
var body: some View {
ZStack {
// 1. Background
MeshBackground()
// 2. Main Content
TimerView(viewModel: viewModel)
// 3. Visual Alarm Overlay (Absolute top priority)
if viewModel.isTimerFinished {
AlarmOverlay(viewModel: viewModel, flashOpacity: $flashOpacity)
.zIndex(100)
}
}
.preferredColorScheme(.dark) // Enforce dark mode for the 'neon/glass' aesthetic
.onAppear {
viewModel.requestPermissions()
UIApplication.shared.isIdleTimerDisabled = true
}
.alert("Rename Button", isPresented: $isRenaming) {
TextField("Button Label", text: $renamingText)
Button("Save") {
if let index = renamingIndex {
viewModel.updateCustomLabel(at: index, with: renamingText)
}
}
Button("Cancel", role: .cancel) { }
}
}
}
// MARK: - Extracted Alarm Overlay
struct AlarmOverlay: View {
@ObservedObject var viewModel: TimerViewModel
@Binding var flashOpacity: Bool
// Flash timing constant for rapid strobe mode
private let flashIntervalSeconds: Double = 0.08 // 80ms strobe speed
@State private var flashTimer: Timer?
@State private var softFlashOpacity: Double = 0.0
var body: some View {
ZStack {
Color.black
.ignoresSafeArea()
// Use different opacity source based on mode
Color(viewModel.selectedColor.color)
.ignoresSafeArea()
.opacity(viewModel.selectedFlashMode == .soft ? softFlashOpacity : (flashOpacity ? 1.0 : 0.0))
.onAppear {
startFlashing()
}
.onDisappear {
stopFlashing()
}
VStack(spacing: 20) {
Image(systemName: "alarm.fill")
.font(.system(size: 80))
.foregroundColor(.white)
.symbolEffect(.bounce.byLayer, options: .repeating)
Text(viewModel.notificationMessage.isEmpty ? "Timer Finished!" : viewModel.notificationMessage)
.font(.system(size: 60, weight: .heavy))
.minimumScaleFactor(0.5)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding()
.shadow(color: .black, radius: 2, x: 1, y: 1)
}
}
.onTapGesture {
stopFlashing()
viewModel.isTimerFinished = false
// Visual alarm dismissed
}
}
// MARK: - Flash Timer Methods
private func startFlashing() {
switch viewModel.selectedFlashMode {
case .soft:
// Soft mode: continuous smooth fading animation
withAnimation(.easeInOut(duration: 0.5).repeatForever(autoreverses: true)) {
softFlashOpacity = 1.0
}
case .rapid:
// Rapid strobe mode: toggle flash every 80ms
flashTimer = Timer.scheduledTimer(withTimeInterval: flashIntervalSeconds, repeats: true) { _ in
flashOpacity.toggle()
}
}
}
private func stopFlashing() {
flashTimer?.invalidate()
flashTimer = nil
flashOpacity = false
softFlashOpacity = 0.0
}
}
#Preview {
ContentView()
}