mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working Timer, need to enable fulscreen mode
This commit is contained in:
parent
628d76b41c
commit
19b1977fa8
13 changed files with 937 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
250
DynamicIsland/components/Notch/NotchTimerView.swift
Normal file
250
DynamicIsland/components/Notch/NotchTimerView.swift
Normal 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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
119
DynamicIsland/components/Timer/TimerIconAnimation.swift
Normal file
119
DynamicIsland/components/Timer/TimerIconAnimation.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
106
DynamicIsland/components/Timer/TimerLiveActivity.swift
Normal file
106
DynamicIsland/components/Timer/TimerLiveActivity.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue