From 28ab90a402ee7479bf31b8b0c16a7a5357e57a45 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 11 Dec 2025 23:29:02 +0530 Subject: [PATCH] Update floating window controls --- .../LockScreen/LockScreenTimerWidget.swift | 40 +++++++--- .../components/Notch/NotchTimerView.swift | 64 ++++++++++------ .../Timer/TimerControlOverlay.swift | 73 +++++++++++++------ .../components/Timer/TimerPopover.swift | 16 ++-- 4 files changed, 132 insertions(+), 61 deletions(-) diff --git a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift index 9e7594d..21a9be6 100644 --- a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift @@ -125,6 +125,14 @@ struct LockScreenTimerWidget: View { 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 { VStack(spacing: 16) { HStack(alignment: .center, spacing: 8) { @@ -156,22 +164,24 @@ struct LockScreenTimerWidget: View { private var controlButtons: some View { HStack(spacing: 10) { - CircleButton( - icon: pauseIcon, - foreground: Color.white.opacity(0.95), - background: accentColor.opacity(0.32), - action: togglePause, - isEnabled: timerManager.allowsManualInteraction, - helpText: pauseLabel - ) + if !timerManager.isOvertime { + CircleButton( + icon: pauseIcon, + foreground: Color.white.opacity(0.95), + background: accentColor.opacity(0.32), + action: togglePause, + isEnabled: timerManager.allowsManualInteraction, + helpText: pauseLabel + ) + } CircleButton( - icon: "xmark", + icon: secondaryIcon, foreground: Color.white.opacity(0.95), background: Color.black.opacity(0.35), action: stopTimer, isEnabled: timerManager.allowsManualInteraction, - helpText: "Stop" + helpText: secondaryLabel ) } } @@ -255,20 +265,28 @@ struct LockScreenTimerWidget: View { let isEnabled: Bool let helpText: String + @State private var isHovering = false + + private var effectiveBackground: Color { + background.opacity(isHovering ? 0.9 : 0.7) + } + var body: some View { Button(action: action) { Image(systemName: icon) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(foreground) .frame(width: 48, height: 48) - .background(background.opacity(isEnabled ? 1 : 0.25)) + .background(effectiveBackground.opacity(isEnabled ? 1 : 0.25)) .clipShape(Circle()) .contentTransition(.symbolEffect(.replace)) } .buttonStyle(.plain) + .contentShape(Circle()) .help(helpText) .disabled(!isEnabled) .opacity(isEnabled ? 1 : 0.4) + .onHover { hovering in isHovering = hovering } } } } diff --git a/DynamicIsland/components/Notch/NotchTimerView.swift b/DynamicIsland/components/Notch/NotchTimerView.swift index f010c17..23f2fab 100644 --- a/DynamicIsland/components/Notch/NotchTimerView.swift +++ b/DynamicIsland/components/Notch/NotchTimerView.swift @@ -159,21 +159,31 @@ struct NotchTimerView: View { private var leadingControlSection: some View { if timerManager.allowsManualInteraction { HStack(spacing: 10) { - TimerControlButton( - icon: pauseIconName, - foreground: .white.opacity(0.95), - background: timerAccentColor.opacity(0.32), - accessibilityLabel: pauseAccessibilityLabel, - action: togglePauseAction - ) + if !timerManager.isOvertime { + TimerControlButton( + icon: pauseIconName, + foreground: .white.opacity(0.95), + background: timerAccentColor.opacity(0.32), + accessibilityLabel: pauseAccessibilityLabel, + action: togglePauseAction + ) - TimerControlButton( - icon: "xmark", - foreground: .white.opacity(0.95), - background: Color.white.opacity(0.16), - accessibilityLabel: "Stop", - action: stopTimerAction - ) + TimerControlButton( + icon: "xmark", + foreground: .white.opacity(0.95), + background: Color.white.opacity(0.16), + accessibilityLabel: "Cancel", + action: stopTimerAction + ) + } else { + TimerControlButton( + icon: "stop.fill", + foreground: .white.opacity(0.95), + background: Color.white.opacity(0.16), + accessibilityLabel: "Stop", + action: stopTimerAction + ) + } } } else { externalTimerNotice @@ -445,13 +455,17 @@ struct NotchTimerView: View { } label: { Label("Start", systemImage: "play.fill") .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(Color.white) .frame(maxWidth: .infinity) .padding(.vertical, 10) - .foregroundStyle(Color.white) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(startButtonColor.opacity(isStartDisabled ? 0.5 : 1)) + ) } .buttonStyle(.plain) - .background(startButtonColor.opacity(isStartDisabled ? 0.5 : 1)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .frame(maxWidth: .infinity) + .contentShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.15), lineWidth: 1) @@ -464,13 +478,17 @@ struct NotchTimerView: View { Button(action: resetCustomTimerInputs) { Label("Reset", systemImage: "arrow.counterclockwise") .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(.white.opacity(0.9)) .frame(maxWidth: .infinity) .padding(.vertical, 10) - .foregroundStyle(.white.opacity(0.9)) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.white.opacity(0.16)) + ) } .buttonStyle(.plain) - .background(Color.white.opacity(0.16)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .frame(maxWidth: .infinity) + .contentShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.08), lineWidth: 1) @@ -512,18 +530,22 @@ private struct TimerControlButton: View { let accessibilityLabel: String let action: () -> Void + @State private var isHovering = false + var body: some View { Button(action: action) { Image(systemName: icon) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(foreground) .frame(width: 46, height: 46) - .background(background) + .background(background.opacity(isHovering ? 0.95 : 0.8)) .clipShape(Circle()) .contentTransition(.symbolEffect(.replace)) } .buttonStyle(.plain) + .contentShape(Circle()) .help(accessibilityLabel) + .onHover { hovering in isHovering = hovering } } } diff --git a/DynamicIsland/components/Timer/TimerControlOverlay.swift b/DynamicIsland/components/Timer/TimerControlOverlay.swift index 173648f..c65a4b8 100644 --- a/DynamicIsland/components/Timer/TimerControlOverlay.swift +++ b/DynamicIsland/components/Timer/TimerControlOverlay.swift @@ -10,14 +10,20 @@ struct TimerControlOverlay: View { timerManager.isPaused ? "play.fill" : "pause.fill" } - private var pauseForeground: Color { - .white - } + private var pauseForeground: Color { .white } private var helpText: String { 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 { max(notchHeight - 20, 22) } @@ -32,26 +38,24 @@ struct TimerControlOverlay: View { var body: some View { HStack(spacing: 12) { - Button(action: togglePause) { - Image(systemName: pauseIcon) - .font(.system(size: iconSize, weight: .semibold)) - .frame(width: buttonSize, height: buttonSize) - .foregroundStyle(pauseForeground) + if !timerManager.isOvertime { + ControlButton( + icon: pauseIcon, + foreground: 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) { - Image(systemName: "stop.fill") - .font(.system(size: iconSize, weight: .semibold)) - .frame(width: buttonSize, height: buttonSize) - .foregroundStyle(Color.red) - } - .buttonStyle(.plain) - .contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - .help("Stop") + ControlButton( + icon: secondaryIcon, + foreground: timerManager.isOvertime ? Color.white : Color.white, + background: timerManager.isOvertime ? Color.red.opacity(0.24) : Color.white.opacity(0.14), + help: secondaryHelp, + action: stopTimer + ) .disabled(!timerManager.allowsManualInteraction) } .padding(.horizontal, 12) @@ -61,7 +65,7 @@ struct TimerControlOverlay: View { RoundedRectangle(cornerRadius: windowCornerRadius, style: .continuous) .fill(Color.black.opacity(0.9)) } - .compositingGroup() + .compositingGroup() .animation(.smooth(duration: 0.2), value: timerManager.isPaused) .animation(.smooth(duration: 0.2), value: timerManager.isFinished) .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 { TimerControlOverlay(notchHeight: 34, cornerRadius: 14) .padding() diff --git a/DynamicIsland/components/Timer/TimerPopover.swift b/DynamicIsland/components/Timer/TimerPopover.swift index d6930ea..a3888e7 100644 --- a/DynamicIsland/components/Timer/TimerPopover.swift +++ b/DynamicIsland/components/Timer/TimerPopover.swift @@ -152,14 +152,16 @@ private struct ActiveTimerSection: View { } HStack(spacing: 8) { - Button(action: togglePause) { - Label(timerManager.isPaused ? "Resume" : "Pause", systemImage: timerManager.isPaused ? "play.fill" : "pause.fill") - .font(.system(size: 12, weight: .medium)) - .labelStyle(.titleAndIcon) + if !timerManager.isOvertime { + Button(action: togglePause) { + Label(timerManager.isPaused ? "Resume" : "Pause", systemImage: timerManager.isPaused ? "play.fill" : "pause.fill") + .font(.system(size: 12, weight: .medium)) + .labelStyle(.titleAndIcon) + } + .buttonStyle(.bordered) + .disabled(!timerManager.allowsManualInteraction) } - .buttonStyle(.bordered) - .disabled(!timerManager.allowsManualInteraction) - + Button(role: .destructive, action: stopTimer) { Label("Stop", systemImage: "stop.fill") .font(.system(size: 12, weight: .medium))