mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +00:00
Update Permission Requirement
This commit is contained in:
parent
cf210eb192
commit
4adaba746d
4 changed files with 181 additions and 12 deletions
|
|
@ -1824,10 +1824,6 @@
|
||||||
},
|
},
|
||||||
"Accessibility" : {
|
"Accessibility" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Accessibility permission required" : {
|
|
||||||
"comment" : "A label explaining that accessibility permissions are required.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"Actions" : {
|
"Actions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -14994,10 +14990,6 @@
|
||||||
},
|
},
|
||||||
"Open Model Settings" : {
|
"Open Model Settings" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Open Settings" : {
|
|
||||||
"comment" : "A button that opens system settings when pressed.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"Open System Settings" : {
|
"Open System Settings" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2402,6 +2402,7 @@ struct Shelf: View {
|
||||||
@Default(.copyOnDrag) var copyOnDrag
|
@Default(.copyOnDrag) var copyOnDrag
|
||||||
@Default(.autoRemoveShelfItems) var autoRemoveShelfItems
|
@Default(.autoRemoveShelfItems) var autoRemoveShelfItems
|
||||||
@StateObject private var quickShareService = QuickShareService.shared
|
@StateObject private var quickShareService = QuickShareService.shared
|
||||||
|
@ObservedObject private var fullDiskAccessPermission = FullDiskAccessPermissionStore.shared
|
||||||
|
|
||||||
private var selectedProvider: QuickShareProvider? {
|
private var selectedProvider: QuickShareProvider? {
|
||||||
quickShareService.availableProviders.first(where: { $0.id == quickShareProvider })
|
quickShareService.availableProviders.first(where: { $0.id == quickShareProvider })
|
||||||
|
|
@ -2417,8 +2418,26 @@ struct Shelf: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
if !fullDiskAccessPermission.isAuthorized {
|
||||||
|
Section {
|
||||||
|
SettingsPermissionCallout(
|
||||||
|
title: "Full Disk Access required",
|
||||||
|
message: "Grant Full Disk Access so the Shelf can index and move files outside the app sandbox.",
|
||||||
|
icon: "internaldrive.badge.exclamationmark",
|
||||||
|
iconColor: .purple,
|
||||||
|
requestButtonTitle: "Request Full Disk Access",
|
||||||
|
openSettingsButtonTitle: "Open Privacy & Security",
|
||||||
|
requestAction: { fullDiskAccessPermission.requestAccessPrompt() },
|
||||||
|
openSettingsAction: { fullDiskAccessPermission.openSystemSettings() }
|
||||||
|
)
|
||||||
|
} header: {
|
||||||
|
Text("Permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Enable shelf", key: .dynamicShelf)
|
Defaults.Toggle("Enable shelf", key: .dynamicShelf)
|
||||||
|
.disabled(!fullDiskAccessPermission.isAuthorized)
|
||||||
.settingsHighlight(id: highlightID("Enable shelf"))
|
.settingsHighlight(id: highlightID("Enable shelf"))
|
||||||
|
|
||||||
Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault)
|
Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault)
|
||||||
|
|
@ -2503,6 +2522,9 @@ struct Shelf: View {
|
||||||
}
|
}
|
||||||
.accentColor(.effectiveAccent)
|
.accentColor(.effectiveAccent)
|
||||||
.navigationTitle("Shelf")
|
.navigationTitle("Shelf")
|
||||||
|
.onAppear {
|
||||||
|
fullDiskAccessPermission.refreshStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2511,6 +2533,7 @@ struct LiveActivitiesSettings: View {
|
||||||
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
||||||
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
||||||
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
@ObservedObject var doNotDisturbManager = DoNotDisturbManager.shared
|
||||||
|
@ObservedObject private var fullDiskAccessPermission = FullDiskAccessPermissionStore.shared
|
||||||
|
|
||||||
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
||||||
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
|
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
|
||||||
|
|
@ -2555,6 +2578,19 @@ struct LiveActivitiesSettings: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
if !fullDiskAccessPermission.isAuthorized {
|
||||||
|
SettingsPermissionCallout(
|
||||||
|
title: "Custom Focus metadata",
|
||||||
|
message: "Full Disk Access unlocks custom Focus icons, colors, and labels. Standard Focus detection still works without it—grant access only if you need personalized indicators.",
|
||||||
|
icon: "internaldrive.badge.exclamationmark",
|
||||||
|
iconColor: .purple,
|
||||||
|
requestButtonTitle: "Request Full Disk Access",
|
||||||
|
openSettingsButtonTitle: "Open Privacy & Security",
|
||||||
|
requestAction: { fullDiskAccessPermission.requestAccessPrompt() },
|
||||||
|
openSettingsAction: { fullDiskAccessPermission.openSystemSettings() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Defaults.Toggle("Enable Focus Detection", key: .enableDoNotDisturbDetection)
|
Defaults.Toggle("Enable Focus Detection", key: .enableDoNotDisturbDetection)
|
||||||
.settingsHighlight(id: highlightID("Enable Focus Detection"))
|
.settingsHighlight(id: highlightID("Enable Focus Detection"))
|
||||||
|
|
||||||
|
|
@ -2672,6 +2708,9 @@ struct LiveActivitiesSettings: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Live Activities")
|
.navigationTitle("Live Activities")
|
||||||
|
.onAppear {
|
||||||
|
fullDiskAccessPermission.refreshStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5217,27 +5256,52 @@ struct CustomOSDSettings: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsPermissionCallout: View {
|
struct SettingsPermissionCallout: View {
|
||||||
|
let title: String
|
||||||
let message: String
|
let message: String
|
||||||
|
let icon: String
|
||||||
|
let iconColor: Color
|
||||||
|
let requestButtonTitle: String
|
||||||
|
let openSettingsButtonTitle: String
|
||||||
let requestAction: () -> Void
|
let requestAction: () -> Void
|
||||||
let openSettingsAction: () -> Void
|
let openSettingsAction: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String = "Accessibility permission required",
|
||||||
|
message: String,
|
||||||
|
icon: String = "exclamationmark.triangle.fill",
|
||||||
|
iconColor: Color = .orange,
|
||||||
|
requestButtonTitle: String = "Request Access",
|
||||||
|
openSettingsButtonTitle: String = "Open Settings",
|
||||||
|
requestAction: @escaping () -> Void,
|
||||||
|
openSettingsAction: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.message = message
|
||||||
|
self.icon = icon
|
||||||
|
self.iconColor = iconColor
|
||||||
|
self.requestButtonTitle = requestButtonTitle
|
||||||
|
self.openSettingsButtonTitle = openSettingsButtonTitle
|
||||||
|
self.requestAction = requestAction
|
||||||
|
self.openSettingsAction = openSettingsAction
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Label("Accessibility permission required", systemImage: "exclamationmark.triangle.fill")
|
Label(title, systemImage: icon)
|
||||||
.font(.subheadline.weight(.semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundStyle(.orange)
|
.foregroundStyle(iconColor)
|
||||||
|
|
||||||
Text(message)
|
Text(message)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Button("Request Access") {
|
Button(requestButtonTitle) {
|
||||||
requestAction()
|
requestAction()
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
|
||||||
Button("Open Settings") {
|
Button(openSettingsButtonTitle) {
|
||||||
openSettingsAction()
|
openSettingsAction()
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
|
|
||||||
112
DynamicIsland/helpers/FullDiskAccessPermissionStore.swift
Normal file
112
DynamicIsland/helpers/FullDiskAccessPermissionStore.swift
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import AppKit
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum FullDiskAccessAuthorization {
|
||||||
|
private static let probeURLs: [URL] = [
|
||||||
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent("Library/Application Support/com.apple.TCC/TCC.db"),
|
||||||
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent("Library/DoNotDisturb/DB/ModeConfigurations.json")
|
||||||
|
]
|
||||||
|
|
||||||
|
static func hasPermission() -> Bool {
|
||||||
|
for url in probeURLs {
|
||||||
|
if canReadProtectedResource(at: url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func canReadProtectedResource(at url: URL) -> Bool {
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory), !isDirectory.boolValue else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let handle = try FileHandle(forReadingFrom: url)
|
||||||
|
try handle.close()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class FullDiskAccessPermissionStore: ObservableObject {
|
||||||
|
static let shared = FullDiskAccessPermissionStore()
|
||||||
|
|
||||||
|
@Published private(set) var isAuthorized: Bool = FullDiskAccessAuthorization.hasPermission()
|
||||||
|
|
||||||
|
private var pollingTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
pollingTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshStatus() {
|
||||||
|
updateAuthorizationStatus(to: FullDiskAccessAuthorization.hasPermission())
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAccessPrompt() {
|
||||||
|
#if os(macOS)
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Full Disk Access Required"
|
||||||
|
alert.informativeText = "Dynamic Island needs Full Disk Access to detect custom Focus indicators and power the Shelf. Click Continue to open Full Disk Access settings, then press the + button and select Dynamic Island (we'll reveal it in Finder for you)."
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
alert.addButton(withTitle: "Continue")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
|
||||||
|
if alert.runModal() == .alertFirstButtonReturn {
|
||||||
|
openSystemSettings()
|
||||||
|
revealAppBundleInFinder()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
beginPollingForStatusChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openSystemSettings() {
|
||||||
|
#if os(macOS)
|
||||||
|
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func revealAppBundleInFinder() {
|
||||||
|
#if os(macOS)
|
||||||
|
let bundleURL = Bundle.main.bundleURL
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting([bundleURL])
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginPollingForStatusChanges() {
|
||||||
|
pollingTask?.cancel()
|
||||||
|
pollingTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
for _ in 0..<40 {
|
||||||
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
let status = FullDiskAccessAuthorization.hasPermission()
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.updateAuthorizationStatus(to: status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAuthorizationStatus(to newValue: Bool) {
|
||||||
|
guard newValue != isAuthorized else { return }
|
||||||
|
isAuthorized = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -990,6 +990,7 @@ private final class FocusMetadataReader {
|
||||||
static let shared = FocusMetadataReader()
|
static let shared = FocusMetadataReader()
|
||||||
|
|
||||||
private func getModeConfig(for focusName: String) -> DNDMode? {
|
private func getModeConfig(for focusName: String) -> DNDMode? {
|
||||||
|
guard FullDiskAccessAuthorization.hasPermission() else { return nil }
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: pathToDatabase)
|
let data = try Data(contentsOf: pathToDatabase)
|
||||||
let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data)
|
let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue