mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #168 from Ebullioscopic/feature/timer
Update floating window controls
This commit is contained in:
commit
4f36b69abf
4 changed files with 132 additions and 61 deletions
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue