From 6ef0feecc1ca0283f1323c392a0440141e54ac93 Mon Sep 17 00:00:00 2001 From: Tomas Kracmar Date: Fri, 10 Oct 2025 08:34:07 +0200 Subject: [PATCH] BusyMirror 1.2.3: reliable settings autosave/restore; remember source/target; @MainActor fixes; reinit EKEventStore after grant; Makefile; changelog + release notes --- .gitignore | 1 + BusyMirror.xcodeproj/project.pbxproj | 8 ++++---- BusyMirror/ContentView.swift | 25 +++++++++++++++++++++++++ CHANGELOG.md | 18 ++++++++++++++++++ README.md | 8 ++++++++ ReleaseNotes-1.2.3.md | 24 ++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 ReleaseNotes-1.2.3.md diff --git a/.gitignore b/.gitignore index 06e179c..ae27b90 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ ExportOptions.plist *.swp *.zip *.sha256 +dist/ diff --git a/BusyMirror.xcodeproj/project.pbxproj b/BusyMirror.xcodeproj/project.pbxproj index 937f96c..0592ac4 100644 --- a/BusyMirror.xcodeproj/project.pbxproj +++ b/BusyMirror.xcodeproj/project.pbxproj @@ -410,7 +410,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BusyMirror/Info.plist; @@ -421,7 +421,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.3; 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 = 4; + CURRENT_PROJECT_VERSION = 5; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BusyMirror/Info.plist; @@ -451,7 +451,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; diff --git a/BusyMirror/ContentView.swift b/BusyMirror/ContentView.swift index 30f337e..630c7ad 100644 --- a/BusyMirror/ContentView.swift +++ b/BusyMirror/ContentView.swift @@ -656,18 +656,33 @@ struct ContentView: View { tryRunCLIIfPresent() enforceNoSourceInTargets() } + // Persist key settings whenever they change, to ensure restore between runs + .onChange(of: daysBack) { _ in saveSettingsToDefaults() } + .onChange(of: daysForward) { _ in saveSettingsToDefaults() } + .onChange(of: mergeGapHours) { _ in saveSettingsToDefaults() } + .onChange(of: hideDetails) { _ in saveSettingsToDefaults() } + .onChange(of: copyDescription) { _ in saveSettingsToDefaults() } + .onChange(of: mirrorAllDay) { _ in saveSettingsToDefaults() } + .onChange(of: mirrorAcceptedOnly) { _ in saveSettingsToDefaults() } + .onChange(of: overlapModeRaw) { _ in saveSettingsToDefaults() } + .onChange(of: titlePrefix) { _ in saveSettingsToDefaults() } + .onChange(of: placeholderTitle) { _ in saveSettingsToDefaults() } + .onChange(of: autoDeleteMissing) { _ in saveSettingsToDefaults() } .onChange(of: sourceIndex) { newValue in // Track selected source by persistent ID and ensure it is not a target if newValue < calendars.count { sourceID = calendars[newValue].calendarIdentifier } enforceNoSourceInTargets() + saveSettingsToDefaults() } .onChange(of: targetSelections) { _ in // If the new source is accidentally included, drop it enforceNoSourceInTargets() + saveSettingsToDefaults() } .onChange(of: targetIDs) { _ in // If IDs contain the source’s ID, drop it enforceNoSourceInTargets() + saveSettingsToDefaults() } .onChange(of: routes) { _ in saveSettingsToDefaults() @@ -1165,6 +1180,9 @@ private struct SettingsPayload: Codable { var placeholderTitle: String var autoDeleteMissing: Bool var routes: [Route] + // UI selections (optional for backward compatibility) + var selectedSourceID: String? = nil + var selectedTargetIDs: [String]? = nil // optional metadata var appVersion: String? var exportedAt: Date = Date() @@ -1188,6 +1206,8 @@ private struct SettingsPayload: Codable { placeholderTitle: placeholderTitle, autoDeleteMissing: autoDeleteMissing, routes: routes, + selectedSourceID: sourceID, + selectedTargetIDs: Array(targetIDs).sorted(), appVersion: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, exportedAt: Date() ) @@ -1211,7 +1231,12 @@ private struct SettingsPayload: Codable { placeholderTitle = s.placeholderTitle autoDeleteMissing = s.autoDeleteMissing routes = s.routes + // Restore UI selections if provided + if let selSrc = s.selectedSourceID { sourceID = selSrc } + if let selTgts = s.selectedTargetIDs { targetIDs = Set(selTgts) } clampWorkHours() + // Rebuild indices from IDs after restoring selections + rebuildSelectionsFromIDs() } private func exportSettings() { diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d9e0109 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to BusyMirror will be documented in this file. + +## [1.2.3] - 2025-10-10 +- Fix: reliably save and restore settings between runs via autosave of key options and restoration of source/target selections by persistent IDs. +- UX: persist Source and Target selections; rebuild indices on launch so UI matches saved IDs. +- Build: bump version to 1.2.3 (build 5). + +## [1.2.1] - 2025-10-10 +- Fix: reinitialize EKEventStore after permission grant to avoid “Loaded 0 calendars” right after approval. +- Fix: attendee status filter uses current user’s attendee `participantStatus == .accepted` instead of unavailable APIs. +- Concurrency: mark `requestAccess()` and `reloadCalendars()` as `@MainActor` to satisfy strict concurrency checks. +- Dev: add Makefile with `build-debug`, `build-release`, and `package` targets; produce versioned ZIP + SHA-256. + +## [1.2.0] - 2024-09-29 +- Feature: multi-route mirroring, overlap modes, merge gaps, work hours filter, CLI support, export/import settings. + diff --git a/README.md b/README.md index 46ccc67..1638de7 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,19 @@ BusyMirror mirrors meetings between your calendars so your availability stays co Use one calendar’s confirmed meetings to block time in other calendars (e.g., corporate iPad vs. personal devices). ## Build (macOS) +Option A — Xcode 1. Open `BusyMirror.xcodeproj` in Xcode. 2. Select the BusyMirror scheme → My Mac. 3. Product → Build. 4. Product → Archive → Distribute App → Copy App (no notarization) to export a `.app` (or ZIP it for sharing). +Option B — Makefile (reproducible) +- Build Release: `make build-release` +- Package ZIP: `make package` (creates `BusyMirror--macOS.zip` + `.sha256`) +- Built app: `build/DerivedData/Build/Products/Release/BusyMirror.app` + +See `CHANGELOG.md` for notable changes. + ## Roadmap See [ROADMAP.md](ROADMAP.md) diff --git a/ReleaseNotes-1.2.3.md b/ReleaseNotes-1.2.3.md new file mode 100644 index 0000000..41855c9 --- /dev/null +++ b/ReleaseNotes-1.2.3.md @@ -0,0 +1,24 @@ +# BusyMirror 1.2.3 — 2025-10-10 + +This release focuses on reliable settings persistence and quality-of-life fixes from the 1.2.1 hotfix. + +Highlights +- Settings persist between runs: autosave key options on change; restore on launch. +- Source/Target selection is remembered using calendar IDs and rehydrated into UI indices. + +Fixes and improvements +- Save on change for: days back/forward, default merge gap, privacy/copy notes, all-day, accepted-only, overlap mode, title/placeholder prefixes, auto-delete. +- Restore saved `selectedSourceID` and `selectedTargetIDs` and rebuild index selections. +- Keep backward compatibility with older saved payloads. +- Version bump to 1.2.3 (build 5). + +Included from 1.2.1 +- Reinitialize `EKEventStore` after permission grant to avoid “Loaded 0 calendars”. +- Use attendee `participantStatus == .accepted` for accepted-only filter. +- Mark `requestAccess()` and `reloadCalendars()` as `@MainActor`. +- Makefile for reproducible builds and packaging. + +Build +- `make build-release` +- `make package` → BusyMirror-1.2.3-macOS.zip and .sha256 +