Working Timer, need to enable fulscreen mode

This commit is contained in:
Hariharan Mudaliar 2025-07-14 08:50:50 +05:30
commit 19b1977fa8
13 changed files with 937 additions and 7 deletions

View file

@ -46,6 +46,11 @@ struct InlineHUD: View {
.symbolVariant(value > 0 ? .none : .slash)
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
case .timer:
Image(systemName: "timer")
.symbolRenderingMode(.hierarchical)
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
default:
EmptyView()
}
@ -75,6 +80,14 @@ struct InlineHUD: View {
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .trailing)
.contentTransition(.interpolate)
} else if (type == .timer) {
Text(TimerManager.shared.formattedRemainingTime())
.foregroundStyle(TimerManager.shared.timerColor)
.lineLimit(1)
.allowsTightening(true)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .trailing)
.contentTransition(.interpolate)
} else {
HStack {
DraggableProgressBar(value: $value)

View file

@ -0,0 +1,250 @@
//
// 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
var body: some View {
HStack(alignment: .center, spacing: 32) {
// Timer Progress and Controls
timerProgressSection
// Timer Control Panel
controlsSection
}
.padding(.horizontal, 20)
.transition(.opacity.combined(with: .blurReplace))
}
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 ? 1.1 : 1.0)
.animation(.easeInOut(duration: 0.3).repeatForever(autoreverses: true), value: timerManager.isRunning)
}
// 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")
}
// Demo timer
Button(action: {
timerManager.startDemoTimer(duration: 10) // 10 second demo
}) {
HStack(spacing: 8) {
Image(systemName: "flask")
.font(.system(size: 14, weight: .medium))
Text("Demo (10s)")
.font(.system(size: 16, weight: .medium))
}
.foregroundStyle(.white)
.frame(height: 40)
.frame(minWidth: 120)
.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 func quickTimerButton(minutes: Int, title: String) -> some View {
Button(action: {
timerManager.startDemoTimer(duration: TimeInterval(minutes * 60))
}) {
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)
}

View file

@ -490,8 +490,31 @@ struct Media: View {
Section {
Toggle(
"Enable music live activity",
isOn: $coordinator.musicLiveActivityEnabled.animation()
isOn: $coordinator.musicLiveActivityEnabled
)
.animation(.easeInOut, value: coordinator.musicLiveActivityEnabled)
Toggle(
"Enable timer live activity",
isOn: $coordinator.timerLiveActivityEnabled
)
.animation(.easeInOut, value: coordinator.timerLiveActivityEnabled)
// Demo timer button
HStack {
Button("Start 5-min Demo Timer") {
TimerManager.shared.startDemoTimer(duration: 300)
}
.buttonStyle(.borderedProminent)
Spacer()
Button("Stop Timer") {
TimerManager.shared.stopTimer()
}
.buttonStyle(.bordered)
}
Toggle("Enable sneak peek", isOn: $enableSneakPeek)
Picker("Sneak Peek Style", selection: $sneakPeekStyles){
ForEach(SneakPeekStyle.allCases) { style in
@ -1105,6 +1128,16 @@ struct Shortcuts: View {
Section {
KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen)
}
Section {
KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer)
} header: {
Text("Timer")
} footer: {
Text("Starts a 5-minute demo timer to test the timer live activity feature.")
.multilineTextAlignment(.trailing)
.foregroundStyle(.secondary)
.font(.caption)
}
}
.navigationTitle("Shortcuts")
}

View file

@ -16,7 +16,8 @@ struct TabModel: Identifiable {
let tabs = [
TabModel(label: "Home", icon: "house.fill", view: .home),
TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)
TabModel(label: "Shelf", icon: "tray.fill", view: .shelf),
TabModel(label: "Timer", icon: "timer", view: .timer)
]
struct TabSelectionView: View {

View file

@ -0,0 +1,119 @@
//
// TimerIconAnimation.swift
// DynamicIsland
//
// Created by GitHub Copilot on 2025-01-13.
//
import SwiftUI
import Combine
struct TimerIconAnimation: View {
@ObservedObject var timerManager = TimerManager.shared
@State private var pulseScale: CGFloat = 1.0
@State private var rotationAngle: Double = 0
var body: some View {
ZStack {
// Background circle with timer color
Circle()
.fill(timerManager.timerColor.gradient)
.frame(width: 24, height: 24)
.scaleEffect(pulseScale)
.animation(
timerManager.isTimerActive && !timerManager.isPaused
? .easeInOut(duration: 1.0).repeatForever(autoreverses: true)
: .easeInOut(duration: 0.3),
value: pulseScale
)
// Timer icon
Image(systemName: "timer")
.font(.system(size: 12, weight: .medium))
.foregroundColor(.white)
.rotationEffect(.degrees(rotationAngle))
.animation(
timerManager.isTimerActive && !timerManager.isPaused
? .linear(duration: 2.0).repeatForever(autoreverses: false)
: .easeInOut(duration: 0.3),
value: rotationAngle
)
// Progress ring
Circle()
.trim(from: 0, to: timerManager.progress)
.stroke(
Color.white.opacity(0.8),
style: StrokeStyle(lineWidth: 2, lineCap: .round)
)
.frame(width: 26, height: 26)
.rotationEffect(.degrees(-90)) // Start from top
.animation(.easeInOut(duration: 0.5), value: timerManager.progress)
}
.onAppear {
startAnimations()
}
.onChange(of: timerManager.isTimerActive) { _, isActive in
if isActive {
startAnimations()
} else {
stopAnimations()
}
}
.onChange(of: timerManager.isPaused) { _, isPaused in
if isPaused {
stopAnimations()
} else if timerManager.isTimerActive {
startAnimations()
}
}
}
private func startAnimations() {
guard timerManager.isTimerActive && !timerManager.isPaused else { return }
// Start pulsing
pulseScale = 1.1
// Start rotation
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: false)) {
rotationAngle = 360
}
}
private func stopAnimations() {
// Stop pulsing
pulseScale = 1.0
// Stop rotation
withAnimation(.easeInOut(duration: 0.3)) {
rotationAngle = 0
}
}
}
struct TimerIconView: NSViewRepresentable {
@ObservedObject var timerManager = TimerManager.shared
func makeNSView(context: Context) -> NSView {
let hostingView = NSHostingView(rootView: TimerIconAnimation())
hostingView.translatesAutoresizingMaskIntoConstraints = false
return hostingView
}
func updateNSView(_ nsView: NSView, context: Context) {
if let hostingView = nsView as? NSHostingView<TimerIconAnimation> {
hostingView.rootView = TimerIconAnimation()
}
}
}
#Preview {
TimerIconAnimation()
.frame(width: 50, height: 50)
.background(.black)
.onAppear {
// Start a demo timer for preview
TimerManager.shared.startDemoTimer(duration: 300)
}
}

View file

@ -0,0 +1,106 @@
//
// TimerLiveActivity.swift
// DynamicIsland
//
// Created by GitHub Copilot on 2025-01-13.
//
import SwiftUI
import Defaults
struct TimerLiveActivity: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject var timerManager = TimerManager.shared
@State private var isHovering: Bool = false
@State private var gestureProgress: CGFloat = 0
var body: some View {
HStack {
// Left side - Timer icon animation
timerIconSection
// Center - Expandable timer info (similar to music banner)
timerInfoSection
// Right side - Timer countdown text
timerCountdownSection
}
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
}
private var timerIconSection: some View {
HStack {
// Simple timer icon with color, no background circle or animations
Image(systemName: "timer")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(timerManager.timerColor)
.frame(width: 24, height: 24)
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2),
height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center)
}
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2),
height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)))
}
private var timerInfoSection: some View {
Rectangle()
.fill(.black)
.overlay(
HStack(alignment: .center) {
if coordinator.expandingView.show && coordinator.expandingView.type == .timer {
// Timer name
MarqueeText(
.constant(timerManager.timerName),
textColor: timerManager.timerColor,
minDuration: 0.4,
frameWidth: 100
)
.opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
Spacer(minLength: vm.closedNotchSize.width)
// Timer status
Text(timerManager.isPaused ? "Paused" : "Active")
.lineLimit(1)
.truncationMode(.tail)
.foregroundStyle(timerManager.isOvertime ? .red : timerManager.timerColor)
.opacity((coordinator.expandingView.show && coordinator.expandingView.type == .timer && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
}
}
)
.frame(width: (coordinator.expandingView.show && coordinator.expandingView.type == .timer && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 380 : vm.closedNotchSize.width + (isHovering ? 8 : 0))
}
private var timerCountdownSection: some View {
HStack {
VStack(spacing: 2) {
// Remaining time
Text(timerManager.formattedRemainingTime())
.font(.system(size: 11, weight: .medium, design: .monospaced))
.foregroundColor(timerManager.isOvertime ? .red : .white)
// Progress indicator
if timerManager.totalDuration > 0 {
ProgressView(value: timerManager.progress)
.progressViewStyle(LinearProgressViewStyle(tint: timerManager.timerColor))
.frame(width: 30, height: 2)
}
}
.frame(maxWidth: .infinity)
}
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2),
height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center)
}
}
#Preview {
TimerLiveActivity()
.environmentObject(DynamicIslandViewModel())
.frame(width: 300, height: 32)
.background(.black)
.onAppear {
// Start a demo timer for preview
TimerManager.shared.startDemoTimer(duration: 300)
}
}