Release 1.3.9
This commit is contained in:
@@ -410,7 +410,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = BusyMirror/Info.plist;
|
||||
@@ -421,7 +421,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.8;
|
||||
MARKETING_VERSION = 1.3.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -440,7 +440,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = BusyMirror/Info.plist;
|
||||
@@ -451,7 +451,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.8;
|
||||
MARKETING_VERSION = 1.3.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 source’s 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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
75
BusyMirror/MenuBarSupport.swift
Normal file
75
BusyMirror/MenuBarSupport.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to BusyMirror will be documented in this file.
|
||||
|
||||
## [1.3.9] - 2026-04-09
|
||||
- New: add a macOS menu bar extra with `Sync Now`, `Open BusyMirror`, and `Quit BusyMirror`.
|
||||
- UX: menu bar sync requests reuse the existing mirror flow and can open the main window automatically when needed.
|
||||
- UX: BusyMirror now runs as a menu bar-only app and no longer appears in the Dock.
|
||||
- Build: bump version to 1.3.9 (build 17).
|
||||
|
||||
## [1.3.8] - 2026-04-08
|
||||
- Fix: release ZIPs now package `BusyMirror.app` at the archive root instead of embedding the full build path.
|
||||
- Fix: release builds now apply an ad-hoc bundle signature before packaging so downloaded artifacts pass `codesign --verify --deep --strict`.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
BusyMirror mirrors meetings between your calendars so your availability stays consistent across accounts/devices.
|
||||
|
||||
On macOS, BusyMirror now runs as a menu bar app. Use the menu bar icon to sync manually or open the main window; it no longer appears in the Dock.
|
||||
|
||||
## What it does (current)
|
||||
- Route-driven mirroring (multi-source): define Source → Targets routes and run them in one go.
|
||||
- Manual selection mirroring: pick a source and targets in the UI and run.
|
||||
@@ -11,6 +13,7 @@ BusyMirror mirrors meetings between your calendars so your availability stays co
|
||||
- DRY-RUN mode: see what would be created/updated/deleted without writing.
|
||||
- Activity Log in the app plus persistent file logging on disk.
|
||||
- In-app scheduling: install or remove a `launchd` LaunchAgent from the `Scheduled runs` section.
|
||||
- Menu bar controls: trigger `Sync Now`, open the main window, or quit without keeping a Dock icon around.
|
||||
- Overlap modes: `allow`, `skipCovered`, `fillGaps`.
|
||||
- Merge adjacent events with a configurable gap.
|
||||
- Time window controls (days back/forward) and Work Hours filter.
|
||||
|
||||
9
ReleaseNotes-1.3.9.md
Normal file
9
ReleaseNotes-1.3.9.md
Normal file
@@ -0,0 +1,9 @@
|
||||
BusyMirror 1.3.9 - 2026-04-09
|
||||
|
||||
Changes
|
||||
- Add a menu bar extra with `Sync Now`, `Open BusyMirror`, and `Quit BusyMirror`.
|
||||
- Route menu bar sync requests through the same mirroring flow as the main window, opening the window automatically when needed.
|
||||
- Run BusyMirror as a menu bar-only app so it no longer appears in the Dock.
|
||||
|
||||
Build
|
||||
- Version bump to 1.3.9 (build 17).
|
||||
Reference in New Issue
Block a user