BusyMirror 1.2.1: fix calendar loading after grant, attendee status filter, main-actor; add Makefile

This commit is contained in:
2025-10-10 07:45:59 +02:00
parent 8f80a5f672
commit aac4de3fb3
3 changed files with 86 additions and 6 deletions

View File

@@ -410,7 +410,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusyMirror/Info.plist; INFOPLIST_FILE = BusyMirror/Info.plist;
@@ -421,7 +421,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -440,7 +440,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusyMirror/Info.plist; INFOPLIST_FILE = BusyMirror/Info.plist;
@@ -451,7 +451,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;

View File

@@ -132,6 +132,7 @@ struct ContentView: View {
@AppStorage("workHoursStart") private var workHoursStart: Int = 9 @AppStorage("workHoursStart") private var workHoursStart: Int = 9
@AppStorage("workHoursEnd") private var workHoursEnd: Int = 17 @AppStorage("workHoursEnd") private var workHoursEnd: Int = 17
@AppStorage("excludedTitleFilters") private var excludedTitleFiltersRaw: String = "" @AppStorage("excludedTitleFilters") private var excludedTitleFiltersRaw: String = ""
@AppStorage("mirrorAcceptedOnly") private var mirrorAcceptedOnly: Bool = false
var overlapMode: OverlapMode { var overlapMode: OverlapMode {
get { OverlapMode(rawValue: overlapModeRaw) ?? .allow } get { OverlapMode(rawValue: overlapModeRaw) ?? .allow }
nonmutating set { overlapModeRaw = newValue.rawValue } nonmutating set { overlapModeRaw = newValue.rawValue }
@@ -418,6 +419,8 @@ 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)
Toggle("Mirror accepted events only", isOn: $mirrorAcceptedOnly)
.disabled(isRunning)
Picker("Overlap mode", selection: $overlapModeRaw) { Picker("Overlap mode", selection: $overlapModeRaw) {
ForEach(OverlapMode.allCases) { mode in ForEach(OverlapMode.allCases) { mode in
Text(mode.rawValue).tag(mode.rawValue) Text(mode.rawValue).tag(mode.rawValue)
@@ -755,13 +758,18 @@ struct ContentView: View {
} }
// MARK: - Permissions & Calendars // MARK: - Permissions & Calendars
@MainActor
func requestAccess() { func requestAccess() {
log("Requesting calendar access…") log("Requesting calendar access…")
if #available(macOS 14.0, *) { if #available(macOS 14.0, *) {
store.requestFullAccessToEvents { granted, _ in store.requestFullAccessToEvents { granted, _ in
DispatchQueue.main.async { DispatchQueue.main.async {
hasAccess = granted hasAccess = granted
if granted { reloadCalendars() } if granted {
// Reinitialize the store after permission changes to ensure sources load
store = EKEventStore()
reloadCalendars()
}
log(granted ? "Access granted." : "Access denied.") log(granted ? "Access granted." : "Access denied.")
} }
} }
@@ -769,13 +777,18 @@ struct ContentView: View {
store.requestAccess(to: .event) { granted, _ in store.requestAccess(to: .event) { granted, _ in
DispatchQueue.main.async { DispatchQueue.main.async {
hasAccess = granted hasAccess = granted
if granted { reloadCalendars() } if granted {
// Reinitialize the store after permission changes to ensure sources load
store = EKEventStore()
reloadCalendars()
}
log(granted ? "Access granted." : "Access denied.") log(granted ? "Access granted." : "Access denied.")
} }
} }
} }
} }
@MainActor
func reloadCalendars() { func reloadCalendars() {
let fetched = store.calendars(for: .event) let fetched = store.calendars(for: .event)
calendars = sortedCalendars(fetched) calendars = sortedCalendars(fetched)
@@ -835,7 +848,22 @@ struct ContentView: View {
let allowedEndMinutes = workHoursEnd * 60 let allowedEndMinutes = workHoursEnd * 60
var skippedWorkHours = 0 var skippedWorkHours = 0
var skippedTitles = 0 var skippedTitles = 0
var skippedStatus = 0
for ev in srcEvents { for ev in srcEvents {
if mirrorAcceptedOnly, ev.hasAttendees {
// Only include events where the current user's attendee status is Accepted
let attendees = ev.attendees ?? []
if let me = attendees.first(where: { $0.isCurrentUser }) {
if me.participantStatus != .accepted {
skippedStatus += 1
continue
}
} else {
// If we cannot determine a self attendee, treat as not accepted
skippedStatus += 1
continue
}
}
if enforceWorkHours, !ev.isAllDay, let start = ev.startDate, if enforceWorkHours, !ev.isAllDay, let start = ev.startDate,
isOutsideWorkHours(start, calendar: cal, startMinutes: allowedStartMinutes, endMinutes: allowedEndMinutes) { isOutsideWorkHours(start, calendar: cal, startMinutes: allowedStartMinutes, endMinutes: allowedEndMinutes) {
skippedWorkHours += 1 skippedWorkHours += 1
@@ -866,6 +894,9 @@ struct ContentView: View {
if skippedTitles > 0 { if skippedTitles > 0 {
log("- SKIP title filter: \(skippedTitles) event(s)") log("- SKIP title filter: \(skippedTitles) event(s)")
} }
if skippedStatus > 0 {
log("- SKIP non-accepted status: \(skippedStatus) event(s)")
}
// Deduplicate source blocks to avoid duplicates from EventKit (recurrences / sync races) // Deduplicate source blocks to avoid duplicates from EventKit (recurrences / sync races)
srcBlocks = uniqueBlocks(srcBlocks, trackByID: mergeGapMin == 0) srcBlocks = uniqueBlocks(srcBlocks, trackByID: mergeGapMin == 0)
@@ -1128,6 +1159,7 @@ private struct SettingsPayload: Codable {
var workHoursStart: Int = 9 var workHoursStart: Int = 9
var workHoursEnd: Int = 17 var workHoursEnd: Int = 17
var excludedTitleFilters: [String] = [] var excludedTitleFilters: [String] = []
var mirrorAcceptedOnly: Bool = false
var overlapMode: String var overlapMode: String
var titlePrefix: String var titlePrefix: String
var placeholderTitle: String var placeholderTitle: String
@@ -1150,6 +1182,7 @@ private struct SettingsPayload: Codable {
workHoursStart: workHoursStart, workHoursStart: workHoursStart,
workHoursEnd: workHoursEnd, workHoursEnd: workHoursEnd,
excludedTitleFilters: excludedTitleFilterList, excludedTitleFilters: excludedTitleFilterList,
mirrorAcceptedOnly: mirrorAcceptedOnly,
overlapMode: overlapMode.rawValue, overlapMode: overlapMode.rawValue,
titlePrefix: titlePrefix, titlePrefix: titlePrefix,
placeholderTitle: placeholderTitle, placeholderTitle: placeholderTitle,
@@ -1172,6 +1205,7 @@ private struct SettingsPayload: Codable {
workHoursStart = s.workHoursStart workHoursStart = s.workHoursStart
workHoursEnd = s.workHoursEnd workHoursEnd = s.workHoursEnd
excludedTitleFiltersRaw = s.excludedTitleFilters.joined(separator: "\n") excludedTitleFiltersRaw = s.excludedTitleFilters.joined(separator: "\n")
mirrorAcceptedOnly = s.mirrorAcceptedOnly
overlapMode = OverlapMode(rawValue: s.overlapMode) ?? .allow overlapMode = OverlapMode(rawValue: s.overlapMode) ?? .allow
titlePrefix = s.titlePrefix titlePrefix = s.titlePrefix
placeholderTitle = s.placeholderTitle placeholderTitle = s.placeholderTitle

46
Makefile Normal file
View File

@@ -0,0 +1,46 @@
# Simple build and package helpers for BusyMirror
SCHEME ?= BusyMirror
PROJECT ?= BusyMirror.xcodeproj
DERIVED ?= build/DerivedData
DEST := platform=macOS
# Extract marketing version from project settings
VERSION := $(shell sed -n 's/.*MARKETING_VERSION = \([0-9.]*\);.*/\1/p' $(PROJECT)/project.pbxproj | head -n1)
.PHONY: all clean build-debug build-release open app package
all: build-release
clean:
@echo "Cleaning derived data…"
xcodebuild -scheme $(SCHEME) -project $(PROJECT) -derivedDataPath $(DERIVED) -destination '$(DEST)' CODE_SIGNING_ALLOWED=NO clean >/dev/null
@echo "Done."
build-debug:
@echo "Building Debug…"
xcodebuild -scheme $(SCHEME) -project $(PROJECT) -configuration Debug -destination '$(DEST)' -derivedDataPath $(DERIVED) CODE_SIGNING_ALLOWED=NO build
build-release:
@echo "Building Release…"
xcodebuild -scheme $(SCHEME) -project $(PROJECT) -configuration Release -destination '$(DEST)' -derivedDataPath $(DERIVED) CODE_SIGNING_ALLOWED=NO build
# Convenience to open the built app in Finder
open: app
@open "$<"
# Path to built app (Release)
APP_PATH := $(DERIVED)/Build/Products/Release/BusyMirror.app
app: build-release
@# Ensure the app exists
@test -d "$(APP_PATH)" && echo "Built: $(APP_PATH)" || (echo "App not found at $(APP_PATH)" && exit 1)
@echo "Version: $(VERSION)"
@echo "OK"
package: app
@echo "Packaging BusyMirror $(VERSION)"
@zip -qry "BusyMirror-$(VERSION)-macOS.zip" "$(APP_PATH)"
@shasum -a 256 "BusyMirror-$(VERSION)-macOS.zip" | awk '{print $$1}' > "BusyMirror-$(VERSION)-macOS.zip.sha256"
@echo "Created BusyMirror-$(VERSION)-macOS.zip and .sha256"