mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
295 lines
11 KiB
Swift
295 lines
11 KiB
Swift
//
|
|
// NotchTimerView.swift
|
|
// DynamicIsland
|
|
//
|
|
// Timer tab interface for the Dynamic Island
|
|
//
|
|
|
|
import SwiftUI
|
|
import Defaults
|
|
|
|
struct NotchTimerView: View {
|
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
|
@ObservedObject var timerManager = TimerManager.shared
|
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
|
@Default(.enableTimerFeature) var enableTimerFeature
|
|
|
|
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default
|
|
|
|
var body: some View {
|
|
if enableTimerFeature {
|
|
HStack(alignment: .center, spacing: 32) {
|
|
// Timer Progress and Controls
|
|
timerProgressSection
|
|
|
|
// Timer Control Panel
|
|
controlsSection
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.transition(.opacity.combined(with: .blurReplace))
|
|
} else {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "timer.slash")
|
|
.font(.system(size: 48, weight: .light))
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text("Timer Disabled")
|
|
.font(.title2)
|
|
.fontWeight(.medium)
|
|
.foregroundStyle(.primary)
|
|
|
|
Text("Enable timer feature in Settings to use this tab")
|
|
.font(.body)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
|
|
private var timerProgressSection: some View {
|
|
VStack(spacing: 16) {
|
|
// Circular progress display
|
|
ZStack {
|
|
// Background ring
|
|
Circle()
|
|
.stroke(.white.opacity(0.1), lineWidth: 8)
|
|
.frame(width: 100, height: 100)
|
|
|
|
// Progress ring
|
|
Circle()
|
|
.trim(from: 0, to: timerManager.progress)
|
|
.stroke(
|
|
timerManager.currentColor,
|
|
style: StrokeStyle(lineWidth: 8, lineCap: .round)
|
|
)
|
|
.frame(width: 100, height: 100)
|
|
.rotationEffect(.degrees(-90))
|
|
.animation(.easeInOut(duration: 0.5), value: timerManager.progress)
|
|
|
|
// Timer icon in center
|
|
Image(systemName: "timer")
|
|
.font(.system(size: 32, weight: .medium))
|
|
.foregroundStyle(timerManager.currentColor)
|
|
.scaleEffect(timerManager.isRunning && timerManager.isTimerActive ? 1.1 : 1.0)
|
|
.animation(
|
|
timerManager.isRunning && timerManager.isTimerActive ?
|
|
.easeInOut(duration: 0.3).repeatForever(autoreverses: true) :
|
|
.easeInOut(duration: 0.3),
|
|
value: timerManager.isRunning && timerManager.isTimerActive
|
|
)
|
|
}
|
|
|
|
// Time display
|
|
VStack(spacing: 4) {
|
|
Text(timerManager.formattedTimeRemaining)
|
|
.font(.system(size: 24, weight: .semibold, design: .monospaced))
|
|
.foregroundStyle(timerManager.isOvertime ? .red : .white)
|
|
|
|
if timerManager.isTimerActive && timerManager.isPaused {
|
|
Text("Paused")
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var controlsSection: some View {
|
|
VStack(spacing: 20) {
|
|
if timerManager.isTimerActive {
|
|
// Active timer controls
|
|
activeTimerControls
|
|
} else {
|
|
// Quick timer setup
|
|
quickTimerControls
|
|
}
|
|
}
|
|
}
|
|
|
|
private var activeTimerControls: some View {
|
|
VStack(spacing: 16) {
|
|
if timerManager.isOvertime {
|
|
// Overtime controls - only show stop button
|
|
Button(action: {
|
|
timerManager.forceStopTimer()
|
|
}) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "stop.fill")
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(.red)
|
|
Text("Stop")
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(.red)
|
|
}
|
|
.frame(height: 40)
|
|
.frame(minWidth: 100)
|
|
.background(.white.opacity(0.15))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovering in
|
|
if isHovering {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
} else {
|
|
// Normal timer controls
|
|
HStack(spacing: 16) {
|
|
// Pause/Resume button
|
|
Button(action: {
|
|
if timerManager.isPaused {
|
|
timerManager.resumeTimer()
|
|
} else {
|
|
timerManager.pauseTimer()
|
|
}
|
|
}) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: timerManager.isPaused ? "play.fill" : "pause.fill")
|
|
.font(.system(size: 16, weight: .medium))
|
|
Text(timerManager.isPaused ? "Resume" : "Pause")
|
|
.font(.system(size: 16, weight: .medium))
|
|
}
|
|
.foregroundStyle(.white)
|
|
.frame(height: 40)
|
|
.frame(minWidth: 100)
|
|
.background(.white.opacity(0.1))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovering in
|
|
if isHovering {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
|
|
// Stop button
|
|
Button(action: {
|
|
timerManager.stopTimer()
|
|
}) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "stop.fill")
|
|
.font(.system(size: 16, weight: .medium))
|
|
Text("Stop")
|
|
.font(.system(size: 16, weight: .medium))
|
|
}
|
|
.foregroundStyle(.white)
|
|
.frame(height: 40)
|
|
.frame(minWidth: 100)
|
|
.background(.white.opacity(0.15))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovering in
|
|
if isHovering {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var quickTimerControls: some View {
|
|
VStack(spacing: 16) {
|
|
// Quick timer grid
|
|
VStack(spacing: 12) {
|
|
HStack(spacing: 12) {
|
|
quickTimerButton(minutes: 1, title: "1 min")
|
|
quickTimerButton(minutes: 5, title: "5 min")
|
|
quickTimerButton(minutes: 15, title: "15 min")
|
|
}
|
|
|
|
HStack(spacing: 12) {
|
|
// Custom timer button
|
|
Button(action: {
|
|
timerManager.startTimer(duration: customTimerDuration, name: "Custom Timer")
|
|
}) {
|
|
VStack(spacing: 4) {
|
|
Text(customTimerDisplayText)
|
|
.font(.system(size: 16, weight: .medium))
|
|
Text("Custom")
|
|
.font(.system(size: 12, weight: .regular))
|
|
.opacity(0.7)
|
|
}
|
|
.foregroundStyle(.white)
|
|
.frame(height: 60)
|
|
.frame(minWidth: 80)
|
|
.background(.white.opacity(0.08))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(.white.opacity(0.1), lineWidth: 1)
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovering in
|
|
if isHovering {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var customTimerDisplayText: String {
|
|
let totalMinutes = Int(customTimerDuration) / 60
|
|
let seconds = Int(customTimerDuration) % 60
|
|
let hours = totalMinutes / 60
|
|
let minutes = totalMinutes % 60
|
|
|
|
if hours > 0 {
|
|
return "\(hours):\(String(format: "%02d", minutes))"
|
|
} else if minutes > 0 {
|
|
return "\(minutes) min"
|
|
} else {
|
|
return "\(seconds)s"
|
|
}
|
|
}
|
|
|
|
private func quickTimerButton(minutes: Int, title: String) -> some View {
|
|
Button(action: {
|
|
timerManager.startTimer(duration: TimeInterval(minutes * 60), name: "\(minutes) Min Timer")
|
|
}) {
|
|
VStack(spacing: 4) {
|
|
Text("\(minutes)")
|
|
.font(.system(size: 22, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
Text("min")
|
|
.font(.system(size: 12, weight: .medium))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(width: 80, height: 60)
|
|
.background(.white.opacity(0.1))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(.white.opacity(0.1), lineWidth: 1)
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovering in
|
|
if isHovering {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NotchTimerView()
|
|
.environmentObject(DynamicIslandViewModel())
|
|
.frame(width: 500, height: 200)
|
|
.background(.black)
|
|
}
|