- 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>
85 lines
3.5 KiB
Swift
85 lines
3.5 KiB
Swift
import SwiftUI
|
|
import EventKit
|
|
|
|
/// The manual source/target calendar picker — extracted from ContentView.
|
|
/// Bindings mutate ContentView's own @State directly.
|
|
struct CalendarsSectionView: View {
|
|
let calendars: [EKCalendar]
|
|
@Binding var sourceIndex: Int
|
|
@Binding var targetSelections: Set<Int>
|
|
@Binding var targetIDs: Set<String>
|
|
let isRunning: Bool
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Source calendar")
|
|
.font(.subheadline.weight(.semibold))
|
|
Picker("Source", selection: $sourceIndex) {
|
|
ForEach(Array(calendars.indices), id: \.self) { i in
|
|
Text("\(i + 1): \(calLabel(calendars[i]))").tag(i)
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
.labelsHidden()
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.disabled(isRunning || calendars.isEmpty)
|
|
|
|
Divider()
|
|
|
|
HStack {
|
|
Text("Target calendars")
|
|
.font(.subheadline.weight(.semibold))
|
|
Spacer()
|
|
Text("\(targetIDs.count) selected")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: 8) {
|
|
ForEach(Array(calendars.indices), id: \.self) { i in
|
|
let isSource = (i == sourceIndex)
|
|
let binding = Binding<Bool>(
|
|
get: { !isSource && targetSelections.contains(i) },
|
|
set: { newValue in
|
|
// Never allow selecting the source as a target
|
|
if isSource { return }
|
|
if newValue {
|
|
targetSelections.insert(i)
|
|
targetIDs.insert(calendars[i].calendarIdentifier)
|
|
} else {
|
|
targetSelections.remove(i)
|
|
targetIDs.remove(calendars[i].calendarIdentifier)
|
|
}
|
|
}
|
|
)
|
|
Toggle(isOn: binding) {
|
|
HStack(spacing: 8) {
|
|
Text("\(i + 1).")
|
|
.font(.caption.monospacedDigit())
|
|
.foregroundStyle(.secondary)
|
|
calChip(calendars[i])
|
|
}
|
|
.padding(.vertical, 3)
|
|
}
|
|
.toggleStyle(.switch)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(Color(nsColor: .controlBackgroundColor))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.stroke(Color.primary.opacity(0.18), lineWidth: 1)
|
|
)
|
|
.disabled(isRunning || isSource)
|
|
.opacity(isSource ? 0.5 : 1)
|
|
}
|
|
}
|
|
}
|
|
.frame(minHeight: 170, maxHeight: 260)
|
|
}
|
|
}
|
|
}
|