Release 1.3.9

This commit is contained in:
2026-04-09 15:55:09 +02:00
parent cdf82b99cc
commit fe9e813583
8 changed files with 146 additions and 5 deletions

View File

@@ -2,10 +2,19 @@ import SwiftUI
@main
struct BusyMirrorApp: App {
@StateObject private var appController = BusyMirrorAppController()
var body: some Scene {
WindowGroup {
Window("BusyMirror", id: BusyMirrorSceneID.mainWindow) {
ContentView()
.environmentObject(appController)
.frame(minWidth: 720, minHeight: 520)
}
.defaultSize(width: 1120, height: 760)
MenuBarExtra("BusyMirror", systemImage: appController.isSyncing ? "arrow.triangle.2.circlepath.circle.fill" : "calendar.badge.clock") {
BusyMirrorMenuBarView()
.environmentObject(appController)
}
}
}

View File

@@ -284,6 +284,7 @@ struct Route: Identifiable, Hashable, Codable {
}
struct ContentView: View {
@EnvironmentObject private var appController: BusyMirrorAppController
@State private var store = EKEventStore()
@State private var hasAccess = false
@State private var calendars: [EKCalendar] = []
@@ -986,7 +987,14 @@ struct ContentView: View {
}
private func startMirrorNow() {
guard !appController.isSyncing else { return }
appController.setSyncing(true)
Task {
defer {
Task { @MainActor in
appController.setSyncing(false)
}
}
// New click -> reset the guard so we don't re-process
sessionGuard.removeAll()
if routes.isEmpty {
@@ -997,6 +1005,20 @@ struct ContentView: View {
}
}
private func handlePendingMenuBarSyncIfNeeded() {
guard appController.hasPendingSyncRequest else { return }
guard !isRunning else { return }
guard hasAccess else { return }
guard !calendars.isEmpty else { return }
guard canRunMirrorNow else {
appController.clearPendingSyncRequest()
log("Menu bar sync requested, but no valid manual targets or saved routes are available.")
return
}
appController.clearPendingSyncRequest()
startMirrorNow()
}
@ViewBuilder
private func optionsSection() -> some View {
VStack(alignment: .leading, spacing: 14) {
@@ -1449,6 +1471,7 @@ struct ContentView: View {
Text("This will remove events identified as mirrored (by URL prefix or title prefix \(titlePrefix)) within the current window (Days back/forward) from the selected target calendars.")
}
.onAppear {
appController.setMainWindowVisible(true)
AppLogStore.append("=== BusyMirror launch ===")
log("Log file: \(AppLogStore.logFileURL.path)")
requestAccess()
@@ -1456,8 +1479,15 @@ struct ContentView: View {
mergeGapMin = max(0, mergeGapHours * 60)
tryRunCLIIfPresent()
enforceNoSourceInTargets()
handlePendingMenuBarSyncIfNeeded()
}
.onDisappear {
appController.setMainWindowVisible(false)
}
// Persist key settings whenever they change, to ensure restore between runs
.onChange(of: appController.syncRequestToken) { _ in
handlePendingMenuBarSyncIfNeeded()
}
.onChange(of: daysBack) { _ in saveSettingsToDefaults() }
.onChange(of: daysForward) { _ in saveSettingsToDefaults() }
.onChange(of: mergeGapHours) { _ in saveSettingsToDefaults() }
@@ -1485,9 +1515,11 @@ struct ContentView: View {
// If IDs contain the sources ID, drop it
enforceNoSourceInTargets()
saveSettingsToDefaults()
handlePendingMenuBarSyncIfNeeded()
}
.onChange(of: routes) { _ in
saveSettingsToDefaults()
handlePendingMenuBarSyncIfNeeded()
}
}
@@ -1618,6 +1650,8 @@ struct ContentView: View {
// Reinitialize the store after permission changes to ensure sources load
store = EKEventStore()
reloadCalendars()
} else {
appController.clearPendingSyncRequest()
}
log(granted ? "Access granted." : "Access denied.")
}
@@ -1630,6 +1664,8 @@ struct ContentView: View {
// Reinitialize the store after permission changes to ensure sources load
store = EKEventStore()
reloadCalendars()
} else {
appController.clearPendingSyncRequest()
}
log(granted ? "Access granted." : "Access denied.")
}
@@ -1655,6 +1691,7 @@ struct ContentView: View {
saveSettingsToDefaults()
}
log("Loaded \(calendars.count) calendars.")
handlePendingMenuBarSyncIfNeeded()
}
// MARK: - Mirror engine (EventKit)

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSUIElement</key>
<true/>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>BusyMirror needs access to your calendars to create busy placeholders.</string>
<key>NSRemindersFullAccessUsageDescription</key>

View File

@@ -0,0 +1,75 @@
import SwiftUI
import AppKit
enum BusyMirrorSceneID {
static let mainWindow = "main-window"
}
@MainActor
final class BusyMirrorAppController: ObservableObject {
@Published private(set) var isSyncing = false
@Published private(set) var hasPendingSyncRequest = false
@Published private(set) var syncRequestToken = UUID()
@Published private(set) var isMainWindowVisible = false
func requestSync() {
hasPendingSyncRequest = true
syncRequestToken = UUID()
}
func clearPendingSyncRequest() {
hasPendingSyncRequest = false
}
func setSyncing(_ syncing: Bool) {
isSyncing = syncing
}
func setMainWindowVisible(_ visible: Bool) {
isMainWindowVisible = visible
}
func openMainWindow(using openWindow: OpenWindowAction) {
NSApp.activate(ignoringOtherApps: true)
openWindow(id: BusyMirrorSceneID.mainWindow)
}
}
struct BusyMirrorMenuBarView: View {
@Environment(\.openWindow) private var openWindow
@EnvironmentObject private var appController: BusyMirrorAppController
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("BusyMirror")
.font(.headline)
Text(appController.isSyncing ? "Sync in progress." : "Use your saved routes or current selection.")
.font(.subheadline)
.foregroundStyle(.secondary)
Divider()
Button(appController.isSyncing ? "Syncing…" : "Sync Now") {
let shouldOpenWindow = !appController.isMainWindowVisible
appController.requestSync()
if shouldOpenWindow {
appController.openMainWindow(using: openWindow)
}
}
.disabled(appController.isSyncing)
Button("Open BusyMirror") {
appController.openMainWindow(using: openWindow)
}
Divider()
Button("Quit BusyMirror") {
NSApp.terminate(nil)
}
}
.padding(12)
.frame(width: 240, alignment: .leading)
}
}