Files
busymirror/BusyMirror/ScheduleSectionView.swift
tomas.kracmarandClaude Sonnet 5 edddf65288 Release 1.8.0
- Real Settings (Cmd+,) window via PreferencesView, hosting the pure
  @AppStorage-backed defaults that used to live in the main window's
  "General Settings" panel. Both windows share the same live values since
  they're backed by the same UserDefaults keys.
- Menu bar icon reflects idle/syncing/error state; dropdown shows last-sync
  time/result and whether auto-sync is armed, sourced from the same
  lastRunAtISO/lastRunOK/lastRunSummary keys --status reads.
- ContentView.swift split from ~2000 lines into CalendarsSectionView,
  RoutesSectionView, ScheduleSectionView, LogSectionView + a small shared
  CalendarDisplay.swift. View-layer extraction only — state ownership and
  settings persistence deliberately left alone.
- Fixed a latent revert-on-relaunch bug caught while building the Settings
  window: moving preference controls out of ContentView meant they no longer
  re-triggered saveSettingsToDefaults(), so settings.v2 could go stale and
  the next launch would silently revert a just-changed preference via the
  shared applySnapshot path. Split launch-time restore (routes + selection
  only) from Import's full restore.

All 47 unit tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 21:26:07 +02:00

103 lines
4.0 KiB
Swift

import SwiftUI
/// Manual/fixed-time `launchd` scheduling — extracted from ContentView. Since
/// 1.7.0 this is a fallback/override; the primary reliability mechanism is
/// the event-driven auto-sync in `BusyMirrorAppController`, which arms itself
/// automatically once routes exist and needs no UI.
struct ScheduleSectionView: View {
@Binding var scheduleMode: ScheduleMode
@Binding var scheduleIntervalHours: Int
@Binding var scheduleHour: Int
@Binding var scheduleMinute: Int
let isRunning: Bool
let routesEmpty: Bool
let hasInstalledSchedule: Bool
let scheduleSummary: String
let onInstall: () -> Void
let onRemove: () -> Void
let onRevealLaunchAgent: () -> Void
let onScheduleTimeChanged: () -> Void
private static let intFormatter: NumberFormatter = {
let f = NumberFormatter()
f.minimum = 0
f.maximumFractionDigits = 0
return f
}()
private static let hourFormatter: NumberFormatter = {
let f = NumberFormatter()
f.minimum = 0
f.maximum = 24
f.maximumFractionDigits = 0
return f
}()
private static let smallIntFormatter: NumberFormatter = {
let f = NumberFormatter()
f.minimum = 1
f.maximumFractionDigits = 0
return f
}()
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Scheduled runs (manual override)")
.font(.subheadline.weight(.semibold))
HStack(spacing: 8) {
Picker("Mode", selection: $scheduleMode) {
ForEach(ScheduleMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
.pickerStyle(.segmented)
.disabled(isRunning)
Spacer(minLength: 0)
}
if scheduleMode == .hourly {
HStack(spacing: 8) {
Text("Every")
TextField("1", value: $scheduleIntervalHours, formatter: Self.smallIntFormatter)
.textFieldStyle(.roundedBorder)
.frame(width: 56)
.disabled(isRunning)
Text(scheduleIntervalHours == 1 ? "hour" : "hours")
Spacer(minLength: 0)
}
} else {
HStack(spacing: 8) {
Text("Time")
TextField("8", value: $scheduleHour, formatter: Self.hourFormatter)
.textFieldStyle(.roundedBorder)
.frame(width: 56)
.disabled(isRunning)
Text(":")
TextField("0", value: $scheduleMinute, formatter: Self.intFormatter)
.textFieldStyle(.roundedBorder)
.frame(width: 56)
.disabled(isRunning)
Spacer(minLength: 0)
}
}
Text("Creates a LaunchAgent that runs the installed app with saved routes in write mode.")
.foregroundStyle(.secondary)
.font(.footnote)
Text(hasInstalledSchedule ? "Installed: \(scheduleSummary)" : "Not installed")
.font(.footnote)
.foregroundStyle(.secondary)
HStack(spacing: 10) {
Button("Install Schedule", action: onInstall)
.disabled(isRunning || routesEmpty)
Button("Remove Schedule", action: onRemove)
.disabled(isRunning || !hasInstalledSchedule)
Button("Reveal LaunchAgent", action: onRevealLaunchAgent)
.disabled(!hasInstalledSchedule)
Spacer(minLength: 0)
}
}
.onChange(of: scheduleHour) { _ in onScheduleTimeChanged() }
.onChange(of: scheduleMinute) { _ in onScheduleTimeChanged() }
.onChange(of: scheduleIntervalHours) { _ in onScheduleTimeChanged() }
}
}