Fixed building

This commit is contained in:
2025-09-29 15:18:42 +02:00
parent 53f21492da
commit b931f3ba2c

View File

@@ -68,8 +68,13 @@ private func parseMirrorURL(_ url: URL?) -> (srcEventID: String?, occ: Date?, st
var sDate: Date? = nil var sDate: Date? = nil
var eDate: Date? = nil var eDate: Date? = nil
if parts.count >= 3 { srcID = String(parts[2]) } if parts.count >= 3 { srcID = String(parts[2]) }
if parts.count >= 4, let ts = TimeInterval(parts[3]) { occDate = Date(timeIntervalSince1970: ts) } if parts.count >= 4,
if parts.count >= 6, let sTS = TimeInterval(parts[4]), let eTS = TimeInterval(parts[5]) { let ts = TimeInterval(String(parts[3])) {
occDate = Date(timeIntervalSince1970: ts)
}
if parts.count >= 6,
let sTS = TimeInterval(String(parts[4])),
let eTS = TimeInterval(String(parts[5])) {
sDate = Date(timeIntervalSince1970: sTS) sDate = Date(timeIntervalSince1970: sTS)
eDate = Date(timeIntervalSince1970: eTS) eDate = Date(timeIntervalSince1970: eTS)
} }
@@ -125,7 +130,7 @@ struct ContentView: View {
@AppStorage("overlapMode") private var overlapModeRaw: String = OverlapMode.allow.rawValue @AppStorage("overlapMode") private var overlapModeRaw: String = OverlapMode.allow.rawValue
var overlapMode: OverlapMode { var overlapMode: OverlapMode {
get { OverlapMode(rawValue: overlapModeRaw) ?? .allow } get { OverlapMode(rawValue: overlapModeRaw) ?? .allow }
set { overlapModeRaw = newValue.rawValue } nonmutating set { overlapModeRaw = newValue.rawValue }
} }
@State private var writeEnabled = false // dry-run unless checked @State private var writeEnabled = false // dry-run unless checked
@State private var logText = "Ready." @State private var logText = "Ready."
@@ -245,30 +250,70 @@ struct ContentView: View {
overlap: overlapMode, overlap: overlapMode,
allDay: mirrorAllDay) allDay: mirrorAllDay)
routes.append(r) routes.append(r)
}.disabled(isRunning || calendars.isEmpty || targetIDs.isEmpty) }
Button("Clear") { routes.removeAll() }.disabled(isRunning || routes.isEmpty) .disabled(isRunning || calendars.isEmpty || targetIDs.isEmpty)
Button("Clear") { routes.removeAll() }
.disabled(isRunning || routes.isEmpty)
} }
if routes.isEmpty { if routes.isEmpty {
Text("No routes yet. Pick a Source and Targets above, then click Add from current selection.") Text("No routes yet. Pick a Source and Targets above, then click Add from current selection.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
ForEach($routes, id: \.id) { $route in ForEach($routes, id: \.id) { routeBinding in
let routeVal = route.wrappedValue routeCard(for: routeBinding)
}
}
}
}
@ViewBuilder
private func routeCard(for routeBinding: Binding<Route>) -> some View {
let route = routeBinding.wrappedValue
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) { HStack(alignment: .firstTextBaseline, spacing: 8) {
sourceSummaryView(for: route)
Text("→ Targets:")
targetSummaryView(for: route)
Spacer()
Toggle("Private", isOn: routeBinding.privacy)
.help("If ON, mirror as \(titlePrefix)\(placeholderTitle) with no notes. If OFF, mirror source title (and optionally notes).")
separatorDot()
mergeGapField(for: routeBinding)
Toggle("Copy desc", isOn: routeBinding.copyNotes)
.disabled(isRunning || route.privacy)
.help("If ON and Private is OFF, copy the source events notes/description into the placeholder.")
overlapPicker(for: routeBinding)
.disabled(isRunning)
Toggle("All-day", isOn: routeBinding.allDay)
.disabled(isRunning)
.help("Mirror all-day events for this source.")
separatorDot()
Button(role: .destructive) { removeRoute(id: route.id) } label: { Text("Remove") }
}
}
.padding(6)
.background(.quaternary.opacity(0.1))
.cornerRadius(6)
}
@ViewBuilder
private func sourceSummaryView(for route: Route) -> some View {
Text("Source:") Text("Source:")
if let sCal = calendars.first(where: { $0.calendarIdentifier == routeVal.sourceID }) { if let sCal = calendars.first(where: { $0.calendarIdentifier == route.sourceID }) {
HStack(spacing: 6) { HStack(spacing: 6) {
Circle().fill(calColor(sCal)).frame(width: 10, height: 10) Circle().fill(calColor(sCal)).frame(width: 10, height: 10)
Text(calLabel(sCal)).bold() Text(calLabel(sCal)).bold()
} }
} else { } else {
Text(labelForCalendar(id: routeVal.sourceID)).bold() Text(labelForCalendar(id: route.sourceID)).bold()
} }
Text("→ Targets:") }
@ViewBuilder
private func targetSummaryView(for route: Route) -> some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) { HStack(spacing: 10) {
ForEach(routeVal.targetIDs.sorted(by: <), id: \.self) { tid in ForEach(route.targetIDs.sorted(by: <), id: \.self) { tid in
if let tCal = calendars.first(where: { $0.calendarIdentifier == tid }) { if let tCal = calendars.first(where: { $0.calendarIdentifier == tid }) {
HStack(spacing: 6) { HStack(spacing: 6) {
Circle().fill(calColor(tCal)).frame(width: 10, height: 10) Circle().fill(calColor(tCal)).frame(width: 10, height: 10)
@@ -281,53 +326,41 @@ struct ContentView: View {
} }
} }
.frame(maxWidth: 420) .frame(maxWidth: 420)
Spacer() }
Toggle("Private", isOn: $route.privacy)
.help("If ON, mirror as \(titlePrefix)\(placeholderTitle) with no notes. If OFF, mirror source title (and optionally notes).")
Text("·").foregroundStyle(.secondary)
@ViewBuilder
private func mergeGapField(for routeBinding: Binding<Route>) -> some View {
HStack(spacing: 6) { HStack(spacing: 6) {
Text("Merge gap:") Text("Merge gap:")
TextField("0", value: $route.mergeGapHours, formatter: Self.intFormatter) TextField("0", value: routeBinding.mergeGapHours, formatter: Self.intFormatter)
.frame(width: 48) .frame(width: 48)
.disabled(isRunning) .disabled(isRunning)
.help("Merge adjacent source events separated by ≤ this many hours (e.g., flight legs). 0 = no merge.") .help("Merge adjacent source events separated by ≤ this many hours (e.g., flight legs). 0 = no merge.")
Text("h").foregroundStyle(.secondary) Text("h").foregroundStyle(.secondary)
} }
}
Toggle("Copy desc", isOn: $route.copyNotes) @ViewBuilder
.disabled(isRunning || route.wrappedValue.privacy) private func overlapPicker(for routeBinding: Binding<Route>) -> some View {
.help("If ON and Private is OFF, copy the source events notes/description into the placeholder.")
HStack(spacing: 6) { HStack(spacing: 6) {
Text("Overlap:") Text("Overlap:")
Picker("Overlap", selection: $route.overlap) { Picker("Overlap", selection: routeBinding.overlap) {
ForEach(OverlapMode.allCases) { m in Text(m.rawValue).tag(m) } ForEach(OverlapMode.allCases) { mode in
Text(mode.rawValue).tag(mode)
}
} }
.frame(width: 160) .frame(width: 160)
.help("allow = always place; skipCovered = skip if target already has a block covering the time; fillGaps = only fill uncovered gaps within the source block.") .help("allow = always place; skipCovered = skip if target already has a block covering the time; fillGaps = only fill uncovered gaps within the source block.")
} }
.disabled(isRunning) }
Toggle("Allday", isOn: $route.allDay)
.disabled(isRunning)
.help("Mirror allday events for this source.")
@ViewBuilder
private func separatorDot() -> some View {
Text("·").foregroundStyle(.secondary) Text("·").foregroundStyle(.secondary)
}
Button(role: .destructive) { private func removeRoute(id: UUID) {
routes.removeAll { $0.id == routeVal.id } routes.removeAll { $0.id == id }
} label: { Text("Remove") }
}
}
.padding(6)
.background(.quaternary.opacity(0.1))
.cornerRadius(6)
}
}
}
} }
@ViewBuilder @ViewBuilder
@@ -362,9 +395,9 @@ struct ContentView: View {
.disabled(isRunning || hideDetails) .disabled(isRunning || hideDetails)
Toggle("Mirror all-day events", isOn: $mirrorAllDay) Toggle("Mirror all-day events", isOn: $mirrorAllDay)
.disabled(isRunning) .disabled(isRunning)
Picker("Overlap mode", selection: Binding(get: { overlapMode }, set: { overlapMode = $0 })) { Picker("Overlap mode", selection: $overlapModeRaw) {
ForEach(OverlapMode.allCases) { mode in ForEach(OverlapMode.allCases) { mode in
Text(mode.rawValue).tag(mode) Text(mode.rawValue).tag(mode.rawValue)
} }
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
@@ -426,7 +459,7 @@ struct ContentView: View {
copyDescription = r.copyNotes copyDescription = r.copyNotes
mergeGapHours = max(0, r.mergeGapHours) mergeGapHours = max(0, r.mergeGapHours)
mergeGapMin = mergeGapHours * 60 mergeGapMin = mergeGapHours * 60
overlapMode = r.overlap overlapModeRaw = r.overlap.rawValue
mirrorAllDay = r.allDay mirrorAllDay = r.allDay
} }
await runMirror() await runMirror()
@@ -436,7 +469,7 @@ struct ContentView: View {
copyDescription = prevCopy copyDescription = prevCopy
mergeGapHours = prevGapH mergeGapHours = prevGapH
mergeGapMin = prevGapM mergeGapMin = prevGapM
overlapMode = prevOverlap overlapModeRaw = prevOverlap.rawValue
mirrorAllDay = prevAllDay mirrorAllDay = prevAllDay
} }
} }
@@ -601,9 +634,9 @@ struct ContentView: View {
mergeGapMin = max(0, mergeGapHours * 60) mergeGapMin = max(0, mergeGapHours * 60)
if let modeStr = strArg("--mode")?.lowercased() { if let modeStr = strArg("--mode")?.lowercased() {
switch modeStr { switch modeStr {
case "allow": overlapMode = .allow case "allow": overlapModeRaw = OverlapMode.allow.rawValue
case "skipcovered", "skip": overlapMode = .skipCovered case "skipcovered", "skip": overlapModeRaw = OverlapMode.skipCovered.rawValue
case "fillgaps", "gaps": overlapMode = .fillGaps case "fillgaps", "gaps": overlapModeRaw = OverlapMode.fillGaps.rawValue
default: break default: break
} }
} }
@@ -1188,4 +1221,3 @@ private struct SettingsPayload: Codable {
} }
} }
} }