Files
busymirror/BusyMirror/CalendarsSectionView.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

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)
}
}
}