Skip to content

Commit d4521aa

Browse files
committed
Add scheduling functionality with high-precision timing
- Implement SchedulingManager to manage scheduled automation tasks. - Create TimeZoneHelper for handling time zone conversions between PST/PDT and GMT/UTC. - Develop InlineSchedulingControls for user interface to schedule tasks. - Add SchedulingDiagnosticView to display scheduling timing information. - Enhance ClickItViewModel to support scheduling modes and task execution. - Integrate scheduling controls into QuickStartTab. - Add unit tests for HighPrecisionScheduler to ensure scheduling accuracy and cancellation.
1 parent 22e51be commit d4521aa

File tree

13 files changed

+1988
-45
lines changed

13 files changed

+1988
-45
lines changed

ClickIt/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundlePackageType</key>
2020
<string>APPL</string>
2121
<key>CFBundleShortVersionString</key>
22-
<string>1.5.2</string>
22+
<string>1.5.3</string>
2323
<key>CFBundleVersion</key>
2424
<string>$(CURRENT_PROJECT_VERSION)</string>
2525
<key>LSMinimumSystemVersion</key>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import SwiftUI
2+
3+
struct SchedulingCard: View {
4+
@ObservedObject var viewModel: ClickItViewModel
5+
@StateObject private var schedulingManager = SchedulingManager.shared
6+
7+
var body: some View {
8+
VStack(spacing: 16) {
9+
// Header
10+
HStack {
11+
Image(systemName: "clock")
12+
.foregroundColor(.purple)
13+
.font(.system(size: 16))
14+
15+
Text("Scheduling")
16+
.font(.headline)
17+
.fontWeight(.medium)
18+
19+
Spacer()
20+
21+
// Quick indicator if scheduling is active
22+
if schedulingManager.hasScheduledTask {
23+
Image(systemName: "clock.fill")
24+
.foregroundColor(.purple)
25+
.font(.system(size: 12))
26+
}
27+
}
28+
29+
// Scheduling Mode Selection
30+
VStack(spacing: 12) {
31+
HStack {
32+
Text("Execution Mode:")
33+
.font(.subheadline)
34+
.fontWeight(.medium)
35+
36+
Spacer()
37+
}
38+
39+
Picker("Scheduling Mode", selection: $viewModel.clickSettings.schedulingMode) {
40+
ForEach(SchedulingMode.allCases, id: \.self) { mode in
41+
Text(mode.displayName)
42+
.tag(mode)
43+
}
44+
}
45+
.pickerStyle(.segmented)
46+
}
47+
48+
// Scheduled Time Section (only show if scheduled mode is selected)
49+
if viewModel.clickSettings.schedulingMode == .scheduled {
50+
VStack(spacing: 12) {
51+
HStack {
52+
Text("Scheduled Time:")
53+
.font(.subheadline)
54+
.fontWeight(.medium)
55+
56+
Spacer()
57+
}
58+
59+
DatePicker(
60+
"Schedule Date & Time",
61+
selection: $viewModel.clickSettings.scheduledDateTime,
62+
in: Date()..., // Only allow future dates
63+
displayedComponents: [.date, .hourAndMinute]
64+
)
65+
.datePickerStyle(.compact)
66+
.labelsHidden()
67+
}
68+
69+
// Validation Message for Scheduled Time
70+
if !viewModel.clickSettings.isScheduledTimeValid {
71+
HStack(spacing: 8) {
72+
Image(systemName: "exclamationmark.triangle")
73+
.foregroundColor(.orange)
74+
.font(.system(size: 12))
75+
Text("Scheduled time must be in the future")
76+
.font(.caption)
77+
.foregroundColor(.orange)
78+
Spacer()
79+
}
80+
.padding(8)
81+
.background(Color.orange.opacity(0.1))
82+
.cornerRadius(6)
83+
}
84+
}
85+
86+
// Scheduling Status Display
87+
if schedulingManager.hasScheduledTask {
88+
VStack(spacing: 8) {
89+
HStack {
90+
Text("Status:")
91+
.font(.subheadline)
92+
.fontWeight(.medium)
93+
94+
Spacer()
95+
96+
Text("Scheduled")
97+
.font(.subheadline)
98+
.fontWeight(.semibold)
99+
.foregroundColor(.purple)
100+
}
101+
102+
HStack {
103+
Text("Starts:")
104+
.font(.subheadline)
105+
.fontWeight(.medium)
106+
107+
Spacer()
108+
109+
Text(schedulingManager.countdownString)
110+
.font(.system(.subheadline, design: .monospaced))
111+
.foregroundColor(.purple)
112+
.fontWeight(.semibold)
113+
}
114+
115+
// Scheduled time display
116+
HStack {
117+
Text("At:")
118+
.font(.caption)
119+
.foregroundColor(.secondary)
120+
121+
Spacer()
122+
123+
Text(formatScheduledTime(schedulingManager.scheduledDateTime))
124+
.font(.caption)
125+
.foregroundColor(.secondary)
126+
}
127+
}
128+
.padding(12)
129+
.background(Color.purple.opacity(0.1))
130+
.cornerRadius(8)
131+
.overlay(
132+
RoundedRectangle(cornerRadius: 8)
133+
.stroke(Color.purple.opacity(0.3), lineWidth: 1)
134+
)
135+
}
136+
137+
// Information about scheduling mode
138+
if viewModel.clickSettings.schedulingMode == .scheduled {
139+
HStack(spacing: 8) {
140+
Image(systemName: "info.circle")
141+
.foregroundColor(.blue)
142+
.font(.system(size: 12))
143+
Text("Automation will begin at the scheduled time")
144+
.font(.caption)
145+
.foregroundColor(.secondary)
146+
Spacer()
147+
}
148+
}
149+
}
150+
.padding(16)
151+
.background(Color(NSColor.controlBackgroundColor))
152+
.cornerRadius(12)
153+
}
154+
155+
private func formatScheduledTime(_ date: Date?) -> String {
156+
guard let date = date else { return "Not set" }
157+
158+
let formatter = DateFormatter()
159+
formatter.dateStyle = .short
160+
formatter.timeStyle = .short
161+
162+
return formatter.string(from: date)
163+
}
164+
}
165+
166+
#Preview {
167+
SchedulingCard(viewModel: {
168+
let vm = ClickItViewModel()
169+
vm.clickSettings.schedulingMode = .scheduled
170+
vm.clickSettings.scheduledDateTime = Date().addingTimeInterval(3600) // 1 hour from now
171+
return vm
172+
}())
173+
.frame(width: 400)
174+
.padding()
175+
}

ClickIt/UI/ViewModels/ClickItViewModel.swift

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class ClickItViewModel: ObservableObject {
1616
@Published var targetPoint: CGPoint?
1717
@Published var isRunning = false
1818
@Published var appStatus: AppStatus = .ready
19+
20+
// Settings
21+
@StateObject var clickSettings = ClickSettings()
1922

2023
// Configuration Properties
2124
@Published var intervalHours = 0
@@ -57,7 +60,11 @@ class ClickItViewModel: ObservableObject {
5760
}
5861

5962
var canStartAutomation: Bool {
60-
targetPoint != nil && totalMilliseconds > 0 && !isRunning && !timerIsActive
63+
targetPoint != nil &&
64+
totalMilliseconds > 0 &&
65+
!isRunning &&
66+
!timerIsActive &&
67+
clickSettings.isValid
6168
}
6269

6370
var totalTimerSeconds: Int {
@@ -71,6 +78,7 @@ class ClickItViewModel: ObservableObject {
7178

7279
// MARK: - Dependencies
7380
private let clickCoordinator = ClickCoordinator.shared
81+
private let schedulingManager = SchedulingManager.shared
7482

7583
// MARK: - Initialization
7684
init() {
@@ -88,10 +96,51 @@ class ClickItViewModel: ObservableObject {
8896
startTimerMode(durationMinutes: timerDurationMinutes, durationSeconds: timerDurationSeconds)
8997
return
9098
}
91-
92-
guard let point = targetPoint, canStartAutomation else { return }
93-
94-
let config = AutomationConfiguration(
99+
100+
guard let point = targetPoint, canStartAutomation else {
101+
print("ClickItViewModel: Cannot start automation - missing prerequisites")
102+
return
103+
}
104+
105+
// Handle scheduling modes
106+
switch clickSettings.schedulingMode {
107+
case .immediate:
108+
// Start automation immediately
109+
executeAutomation(at: point)
110+
111+
case .scheduled:
112+
// Schedule automation for later
113+
scheduleAutomation(at: point)
114+
}
115+
}
116+
117+
private func executeAutomation(at point: CGPoint) {
118+
print("ClickItViewModel: Executing automation immediately")
119+
120+
let config = createAutomationConfiguration(at: point)
121+
clickCoordinator.startAutomation(with: config)
122+
isRunning = true
123+
appStatus = .running
124+
}
125+
126+
private func scheduleAutomation(at point: CGPoint) {
127+
print("ClickItViewModel: Scheduling automation for \(clickSettings.scheduledDateTime)")
128+
129+
let success = schedulingManager.scheduleTask(for: clickSettings.scheduledDateTime) { [weak self] in
130+
guard let self = self else { return }
131+
print("ClickItViewModel: Executing scheduled automation")
132+
self.executeAutomation(at: point)
133+
}
134+
135+
if success {
136+
appStatus = .scheduled(clickSettings.scheduledDateTime)
137+
} else {
138+
appStatus = .error("Invalid scheduled time")
139+
}
140+
}
141+
142+
private func createAutomationConfiguration(at point: CGPoint) -> AutomationConfiguration {
143+
return AutomationConfiguration(
95144
location: point,
96145
clickType: clickType,
97146
clickInterval: Double(totalMilliseconds) / 1000.0,
@@ -104,10 +153,6 @@ class ClickItViewModel: ObservableObject {
104153
showVisualFeedback: showVisualFeedback,
105154
useDynamicMouseTracking: false // Normal automation uses fixed position
106155
)
107-
108-
clickCoordinator.startAutomation(with: config)
109-
isRunning = true
110-
appStatus = .running
111156
}
112157

113158
private func startDynamicAutomation() {
@@ -150,6 +195,7 @@ class ClickItViewModel: ObservableObject {
150195
func stopAutomation() {
151196
clickCoordinator.stopAutomation()
152197
cancelTimer() // Also cancel any active timer
198+
schedulingManager.cancelScheduledTask() // Cancel any scheduled tasks
153199
isRunning = false
154200
appStatus = .ready
155201
}
@@ -259,6 +305,17 @@ class ClickItViewModel: ObservableObject {
259305
countdownTimer = nil
260306
resetTimerState()
261307
}
308+
309+
func cancelScheduledTask() {
310+
schedulingManager.cancelScheduledTask()
311+
if case .scheduled = appStatus {
312+
appStatus = .ready
313+
}
314+
}
315+
316+
func isScheduledTimeValid() -> Bool {
317+
return clickSettings.isScheduledTimeValid
318+
}
262319

263320
private func onTimerExpired() {
264321
defer { resetTimerState() }
@@ -298,8 +355,12 @@ class ClickItViewModel: ObservableObject {
298355
timerIsActive = false
299356
remainingTime = 0
300357
timerMode = .off
301-
if appStatus.displayText.contains("timer") || appStatus.displayText.contains("countdown") {
358+
// Reset app status if it's related to timing or scheduling
359+
switch appStatus {
360+
case .scheduled, .error:
302361
appStatus = .ready
362+
case .ready, .running:
363+
break // Keep current state
303364
}
304365
}
305366

@@ -349,25 +410,32 @@ enum TimerMode {
349410
enum AppStatus {
350411
case ready
351412
case running
413+
case scheduled(Date)
352414
case error(String)
353-
415+
354416
var displayText: String {
355417
switch self {
356418
case .ready:
357419
return "Ready"
358420
case .running:
359421
return "Running"
422+
case .scheduled(let date):
423+
let formatter = DateFormatter()
424+
formatter.timeStyle = .short
425+
return "Scheduled for \(formatter.string(from: date))"
360426
case .error(let message):
361427
return "Error: \(message)"
362428
}
363429
}
364-
430+
365431
var color: Color {
366432
switch self {
367433
case .ready:
368434
return .green
369435
case .running:
370436
return .blue
437+
case .scheduled:
438+
return .purple
371439
case .error:
372440
return .red
373441
}

ClickIt/UI/Views/ContentView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ struct ContentView: View {
3535

3636
// Configuration Panel Card
3737
ConfigurationPanelCard(viewModel: viewModel)
38-
38+
39+
// Scheduling Card
40+
SchedulingCard(viewModel: viewModel)
41+
3942
// Advanced Settings Button
4043
AdvancedSettingsButton(viewModel: viewModel)
4144

0 commit comments

Comments
 (0)