Merge pull request #168 from Ebullioscopic/feature/timer

Update floating window controls
This commit is contained in:
Hariharan Mudaliar 2025-12-11 23:37:29 +05:30 • committed by GitHub
commit 4f36b69abf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 132 additions and 61 deletions

View file

@ -125,6 +125,14 @@ struct LockScreenTimerWidget: View {
timerManager.isPaused ? "Resume" : "Pause" timerManager.isPaused ? "Resume" : "Pause"
} }
private var secondaryIcon: String {
timerManager.isOvertime ? "stop.fill" : "xmark"
}
private var secondaryLabel: String {
timerManager.isOvertime ? "Stop" : "Cancel"
}
var body: some View { var body: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
HStack(alignment: .center, spacing: 8) { HStack(alignment: .center, spacing: 8) {
@ -156,22 +164,24 @@ struct LockScreenTimerWidget: View {
private var controlButtons: some View { private var controlButtons: some View {
HStack(spacing: 10) { HStack(spacing: 10) {
CircleButton( if !timerManager.isOvertime {
icon: pauseIcon, CircleButton(
foreground: Color.white.opacity(0.95), icon: pauseIcon,
background: accentColor.opacity(0.32), foreground: Color.white.opacity(0.95),
action: togglePause, background: accentColor.opacity(0.32),
isEnabled: timerManager.allowsManualInteraction, action: togglePause,
helpText: pauseLabel isEnabled: timerManager.allowsManualInteraction,
) helpText: pauseLabel
)
}
CircleButton( CircleButton(
icon: "xmark", icon: secondaryIcon,
foreground: Color.white.opacity(0.95), foreground: Color.white.opacity(0.95),
background: Color.black.opacity(0.35), background: Color.black.opacity(0.35),
action: stopTimer, action: stopTimer,
isEnabled: timerManager.allowsManualInteraction, isEnabled: timerManager.allowsManualInteraction,
helpText: "Stop" helpText: secondaryLabel
) )
} }
} }
@ -255,20 +265,28 @@ struct LockScreenTimerWidget: View {
let isEnabled: Bool let isEnabled: Bool
let helpText: String let helpText: String
@State private var isHovering = false
private var effectiveBackground: Color {
background.opacity(isHovering ? 0.9 : 0.7)
}
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 18, weight: .semibold)) .font(.system(size: 18, weight: .semibold))
.foregroundStyle(foreground) .foregroundStyle(foreground)
.frame(width: 48, height: 48) .frame(width: 48, height: 48)
.background(background.opacity(isEnabled ? 1 : 0.25)) .background(effectiveBackground.opacity(isEnabled ? 1 : 0.25))
.clipShape(Circle()) .clipShape(Circle())
.contentTransition(.symbolEffect(.replace)) .contentTransition(.symbolEffect(.replace))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.contentShape(Circle())
.help(helpText) .help(helpText)
.disabled(!isEnabled) .disabled(!isEnabled)
.opacity(isEnabled ? 1 : 0.4) .opacity(isEnabled ? 1 : 0.4)
.onHover { hovering in isHovering = hovering }
} }
} }
} }

View file

@ -159,21 +159,31 @@ struct NotchTimerView: View {
private var leadingControlSection: some View { private var leadingControlSection: some View {
if timerManager.allowsManualInteraction { if timerManager.allowsManualInteraction {
HStack(spacing: 10) { HStack(spacing: 10) {
TimerControlButton( if !timerManager.isOvertime {
icon: pauseIconName, TimerControlButton(
foreground: .white.opacity(0.95), icon: pauseIconName,
background: timerAccentColor.opacity(0.32), foreground: .white.opacity(0.95),
accessibilityLabel: pauseAccessibilityLabel, background: timerAccentColor.opacity(0.32),
action: togglePauseAction accessibilityLabel: pauseAccessibilityLabel,
) action: togglePauseAction
)
TimerControlButton( TimerControlButton(
icon: "xmark", icon: "xmark",
foreground: .white.opacity(0.95), foreground: .white.opacity(0.95),
background: Color.white.opacity(0.16), background: Color.white.opacity(0.16),
accessibilityLabel: "Stop", accessibilityLabel: "Cancel",
action: stopTimerAction action: stopTimerAction
) )
} else {
TimerControlButton(
icon: "stop.fill",
foreground: .white.opacity(0.95),
background: Color.white.opacity(0.16),
accessibilityLabel: "Stop",
action: stopTimerAction
)
}
} }
} else { } else {
externalTimerNotice externalTimerNotice
@ -445,13 +455,17 @@ struct NotchTimerView: View {
} label: { } label: {
Label("Start", systemImage: "play.fill") Label("Start", systemImage: "play.fill")
.font(.system(size: 15, weight: .semibold)) .font(.system(size: 15, weight: .semibold))
.foregroundStyle(Color.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 10) .padding(.vertical, 10)
.foregroundStyle(Color.white) .background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(startButtonColor.opacity(isStartDisabled ? 0.5 : 1))
)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.background(startButtonColor.opacity(isStartDisabled ? 0.5 : 1)) .frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .contentShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous) RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.15), lineWidth: 1) .stroke(Color.white.opacity(0.15), lineWidth: 1)
@ -464,13 +478,17 @@ struct NotchTimerView: View {
Button(action: resetCustomTimerInputs) { Button(action: resetCustomTimerInputs) {
Label("Reset", systemImage: "arrow.counterclockwise") Label("Reset", systemImage: "arrow.counterclockwise")
.font(.system(size: 15, weight: .semibold)) .font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white.opacity(0.9))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 10) .padding(.vertical, 10)
.foregroundStyle(.white.opacity(0.9)) .background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color.white.opacity(0.16))
)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.background(Color.white.opacity(0.16)) .frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .contentShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous) RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.white.opacity(0.08), lineWidth: 1) .stroke(Color.white.opacity(0.08), lineWidth: 1)
@ -512,18 +530,22 @@ private struct TimerControlButton: View {
let accessibilityLabel: String let accessibilityLabel: String
let action: () -> Void let action: () -> Void
@State private var isHovering = false
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 18, weight: .semibold)) .font(.system(size: 18, weight: .semibold))
.foregroundStyle(foreground) .foregroundStyle(foreground)
.frame(width: 46, height: 46) .frame(width: 46, height: 46)
.background(background) .background(background.opacity(isHovering ? 0.95 : 0.8))
.clipShape(Circle()) .clipShape(Circle())
.contentTransition(.symbolEffect(.replace)) .contentTransition(.symbolEffect(.replace))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.contentShape(Circle())
.help(accessibilityLabel) .help(accessibilityLabel)
.onHover { hovering in isHovering = hovering }
} }
} }

View file

@ -10,14 +10,20 @@ struct TimerControlOverlay: View {
timerManager.isPaused ? "play.fill" : "pause.fill" timerManager.isPaused ? "play.fill" : "pause.fill"
} }
private var pauseForeground: Color { private var pauseForeground: Color { .white }
.white
}
private var helpText: String { private var helpText: String {
timerManager.isPaused ? "Resume" : "Pause" timerManager.isPaused ? "Resume" : "Pause"
} }
private var secondaryIcon: String {
timerManager.isOvertime ? "stop.fill" : "xmark"
}
private var secondaryHelp: String {
timerManager.isOvertime ? "Stop" : "Cancel"
}
private var buttonSize: CGFloat { private var buttonSize: CGFloat {
max(notchHeight - 20, 22) max(notchHeight - 20, 22)
} }
@ -32,26 +38,24 @@ struct TimerControlOverlay: View {
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Button(action: togglePause) { if !timerManager.isOvertime {
Image(systemName: pauseIcon) ControlButton(
.font(.system(size: iconSize, weight: .semibold)) icon: pauseIcon,
.frame(width: buttonSize, height: buttonSize) foreground: pauseForeground,
.foregroundStyle(pauseForeground) background: Color.white.opacity(0.14),
help: helpText,
action: togglePause
)
.disabled(!timerManager.allowsManualInteraction)
} }
.buttonStyle(.plain)
.contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.help(helpText)
.disabled(!timerManager.allowsManualInteraction)
Button(action: stopTimer) { ControlButton(
Image(systemName: "stop.fill") icon: secondaryIcon,
.font(.system(size: iconSize, weight: .semibold)) foreground: timerManager.isOvertime ? Color.white : Color.white,
.frame(width: buttonSize, height: buttonSize) background: timerManager.isOvertime ? Color.red.opacity(0.24) : Color.white.opacity(0.14),
.foregroundStyle(Color.red) help: secondaryHelp,
} action: stopTimer
.buttonStyle(.plain) )
.contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.help("Stop")
.disabled(!timerManager.allowsManualInteraction) .disabled(!timerManager.allowsManualInteraction)
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
@ -61,7 +65,7 @@ struct TimerControlOverlay: View {
RoundedRectangle(cornerRadius: windowCornerRadius, style: .continuous) RoundedRectangle(cornerRadius: windowCornerRadius, style: .continuous)
.fill(Color.black.opacity(0.9)) .fill(Color.black.opacity(0.9))
} }
.compositingGroup() .compositingGroup()
.animation(.smooth(duration: 0.2), value: timerManager.isPaused) .animation(.smooth(duration: 0.2), value: timerManager.isPaused)
.animation(.smooth(duration: 0.2), value: timerManager.isFinished) .animation(.smooth(duration: 0.2), value: timerManager.isFinished)
.animation(.smooth(duration: 0.2), value: timerManager.isOvertime) .animation(.smooth(duration: 0.2), value: timerManager.isOvertime)
@ -88,6 +92,31 @@ struct TimerControlOverlay: View {
} }
} }
private struct ControlButton: View {
let icon: String
let foreground: Color
let background: Color
let help: String
let action: () -> Void
@State private var isHovering = false
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.system(size: 14, weight: .semibold))
.frame(width: 32, height: 32)
.foregroundStyle(foreground)
.background(background.opacity(isHovering ? 1 : 0.7))
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
.buttonStyle(.plain)
.help(help)
.onHover { hovering in isHovering = hovering }
}
}
#Preview { #Preview {
TimerControlOverlay(notchHeight: 34, cornerRadius: 14) TimerControlOverlay(notchHeight: 34, cornerRadius: 14)
.padding() .padding()

View file

@ -152,14 +152,16 @@ private struct ActiveTimerSection: View {
} }
HStack(spacing: 8) { HStack(spacing: 8) {
Button(action: togglePause) { if !timerManager.isOvertime {
Label(timerManager.isPaused ? "Resume" : "Pause", systemImage: timerManager.isPaused ? "play.fill" : "pause.fill") Button(action: togglePause) {
.font(.system(size: 12, weight: .medium)) Label(timerManager.isPaused ? "Resume" : "Pause", systemImage: timerManager.isPaused ? "play.fill" : "pause.fill")
.labelStyle(.titleAndIcon) .font(.system(size: 12, weight: .medium))
.labelStyle(.titleAndIcon)
}
.buttonStyle(.bordered)
.disabled(!timerManager.allowsManualInteraction)
} }
.buttonStyle(.bordered)
.disabled(!timerManager.allowsManualInteraction)
Button(role: .destructive, action: stopTimer) { Button(role: .destructive, action: stopTimer) {
Label("Stop", systemImage: "stop.fill") Label("Stop", systemImage: "stop.fill")
.font(.system(size: 12, weight: .medium)) .font(.system(size: 12, weight: .medium))