diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d708e7e..3bb54b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,10 +8,15 @@ on: jobs: build: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4 + - name: Select Xcode 16 (Swift 6.1 for PermissionFlow) + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16' + - name: Build Aware run: | xcodebuild -scheme Aware -configuration Debug -derivedDataPath build \ diff --git a/.github/workflows/publish-appcast.yml b/.github/workflows/publish-appcast.yml index ef7a190..44de1f1 100644 --- a/.github/workflows/publish-appcast.yml +++ b/.github/workflows/publish-appcast.yml @@ -15,10 +15,15 @@ permissions: jobs: build-appcast: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4 + - name: Select Xcode 16 (Swift 6.1 for PermissionFlow) + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16' + - name: Resolve Sparkle tools run: | xcodebuild -resolvePackageDependencies -project Aware.xcodeproj -scheme Aware -derivedDataPath build -quiet diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de0733b..083120c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,13 +15,13 @@ env: jobs: build: - runs-on: macos-14 + runs-on: macos-15 outputs: sparkle_enabled: ${{ steps.sparkle_settings.outputs.enabled }} steps: - uses: actions/checkout@v4 - - name: Select Xcode 16 (Swift 6 for DMGMaker) + - name: Select Xcode 16 (Swift 6 for DMGMaker, 6.1 for PermissionFlow) uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '16' diff --git a/AGENTS.md b/AGENTS.md index 495b22f..76d9c19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,7 @@ Repo-agnostic skills (`sharpen-the-tip`, `prototype-mode`, `housekeeping`, `proa ## Local Divergence - Build verification: `xcodebuild -scheme Aware -configuration Debug -derivedDataPath build build` +- PermissionFlow comes from the LPFchan/PermissionFlow fork, pinned to `2.11.2-swift6.1`: upstream v2.11.2 with `swift-tools-version` lowered to 6.1 so Xcode 16.3+ can build it. Move to a new fork tag when upstream updates. - Commit provenance setup: `scripts/install-hooks.sh` configures the tracked `commit-msg` hook locally. - Commit provenance checks: `scripts/check-commit-standards.sh ` and `scripts/check-commit-range.sh ` - There is no dedicated automated test suite in the repo today. For runtime changes, use the build plus focused manual validation. diff --git a/Aware.xcodeproj/project.pbxproj b/Aware.xcodeproj/project.pbxproj index a51d1bb..0ef6521 100644 --- a/Aware.xcodeproj/project.pbxproj +++ b/Aware.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ A1B2C3D40003 /* PresenceDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D10003 /* PresenceDetector.swift */; }; A1B2C3D40004 /* SleepAssertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D10004 /* SleepAssertion.swift */; }; E1F2A3B40000 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1F2A3B40002 /* Sparkle */; }; + F5A6B7C80004 /* PermissionFlow in Frameworks */ = {isa = PBXBuildFile; productRef = F5A6B7C80002 /* PermissionFlow */; }; + F5A6B7C80005 /* PermissionFlowCameraStatus in Frameworks */ = {isa = PBXBuildFile; productRef = F5A6B7C80003 /* PermissionFlowCameraStatus */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -41,6 +43,8 @@ buildActionMask = 2147483647; files = ( E1F2A3B40000 /* Sparkle in Frameworks */, + F5A6B7C80004 /* PermissionFlow in Frameworks */, + F5A6B7C80005 /* PermissionFlowCameraStatus in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -123,6 +127,8 @@ name = Aware; packageProductDependencies = ( E1F2A3B40002 /* Sparkle */, + F5A6B7C80002 /* PermissionFlow */, + F5A6B7C80003 /* PermissionFlowCameraStatus */, ); productName = Aware; productReference = A1B2C3D00001 /* Aware.app */; @@ -157,6 +163,7 @@ mainGroup = A1B2C3D30001; packageReferences = ( E1F2A3B40001 /* XCRemoteSwiftPackageReference "Sparkle" */, + F5A6B7C80001 /* XCRemoteSwiftPackageReference "PermissionFlow" */, ); productRefGroup = A1B2C3D30003 /* Products */; projectDirPath = ""; @@ -350,6 +357,14 @@ minimumVersion = 2.7.0; }; }; + F5A6B7C80001 /* XCRemoteSwiftPackageReference "PermissionFlow" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LPFchan/PermissionFlow"; + requirement = { + kind = exactVersion; + version = "2.11.2-swift6.1"; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -358,6 +373,16 @@ package = E1F2A3B40001 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + F5A6B7C80002 /* PermissionFlow */ = { + isa = XCSwiftPackageProductDependency; + package = F5A6B7C80001 /* XCRemoteSwiftPackageReference "PermissionFlow" */; + productName = PermissionFlow; + }; + F5A6B7C80003 /* PermissionFlowCameraStatus */ = { + isa = XCSwiftPackageProductDependency; + package = F5A6B7C80001 /* XCRemoteSwiftPackageReference "PermissionFlow" */; + productName = PermissionFlowCameraStatus; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = A1B2C3D20006 /* Project object */; diff --git a/Aware.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Aware.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6e450d9..5e0c3b4 100644 --- a/Aware.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Aware.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,22 @@ { - "originHash" : "e721da7f9826abdffcb6185e886155efa2514bd6234475f1afa893e29eb258d6", + "originHash" : "af57ec271e51d6743d8d5de17d0378c5c593331081bd3c110c5ca46764ecc95b", "pins" : [ + { + "identity" : "permissionflow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/LPFchan/PermissionFlow", + "state" : { + "revision" : "4ceff2a83937f4c039fc4cf1b31eb18587c38459", + "version" : "2.11.2-swift6.1" + } + }, { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "21d8df80440b1ca3b65fa82e40782f1e5a9e6ba2", - "version" : "2.9.0" + "revision" : "eef1a539a373c1f1a320624b1130fc5de7b2e100", + "version" : "2.10.0" } } ], diff --git a/Aware/MenuBarController.swift b/Aware/MenuBarController.swift index 0d1d8be..7b9b294 100644 --- a/Aware/MenuBarController.swift +++ b/Aware/MenuBarController.swift @@ -9,6 +9,8 @@ import AppKit import AVFoundation import CoreGraphics +import PermissionFlow +import PermissionFlowCameraStatus import ServiceManagement private enum MenuItemTag: Int { @@ -63,6 +65,9 @@ final class MenuBarController: NSObject { private let sleepAssertion = SleepAssertion() private let checkForUpdatesTarget: AnyObject? private let checkForUpdatesAction: Selector? + /// Opens the System Settings camera pane when access has been denied. + /// Created lazily on the main actor, where menu actions and alerts run. + private var permissionController: PermissionFlowController? private var pollingTimer: DispatchSourceTimer? private let timerQueue = DispatchQueue(label: "com.aware.timer", qos: .userInitiated) @@ -93,6 +98,9 @@ final class MenuBarController: NSObject { self.checkForUpdatesTarget = checkForUpdatesTarget self.checkForUpdatesAction = checkForUpdatesAction super.init() + MainActor.assumeIsolated { + PermissionFlowCameraStatus.register() + } #if DEBUG debugLog("MenuBarController init started") #endif @@ -349,6 +357,14 @@ final class MenuBarController: NSObject { alert.alertStyle = .warning alert.addButton(withTitle: String(localized: "alert.ok")) alert.runModal() + // Camera uses the system toggle list (no drag panel), so this just + // opens System Settings > Privacy & Security > Camera. + MainActor.assumeIsolated { + if permissionController == nil { + permissionController = PermissionFlow.makeController() + } + permissionController?.authorize(pane: .camera) + } } @objc private func handleDisplaySleep() { @@ -369,20 +385,18 @@ final class MenuBarController: NSObject { if isEnabled { isEnabled = false } else { - let status = AVCaptureDevice.authorizationStatus(for: .video) - switch status { - case .authorized: + let cameraPermission = CameraPermissionStatusProvider() + if cameraPermission.authorizationState() == .granted { isEnabled = true - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + } else if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined { + // System prompt via PermissionFlow's camera status provider. + cameraPermission.requestAuthorization { [weak self] state in DispatchQueue.main.async { - self?.isEnabled = granted + self?.isEnabled = (state == .granted) } } - case .denied, .restricted: + } else { showCameraDeniedAlert() - @unknown default: - break } } }