133 lines
4.1 KiB
Swift
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()
|
|
}
|