mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +00:00
Update Shelf to work with limited permissions
This commit is contained in:
parent
d172b88349
commit
eccc16237b
3 changed files with 166 additions and 14 deletions
|
|
@ -464,7 +464,7 @@
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode";
|
INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode";
|
||||||
INFOPLIST_KEY_NSDesktopFolderUsageDescription = "Atoll needs access to save screenshots to your desktop.";
|
INFOPLIST_KEY_NSDesktopFolderUsageDescription = "Atoll needs access to save screenshots to your desktop.";
|
||||||
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read AI assistant files.";
|
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read shelf dragged files.";
|
||||||
INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Atoll needs access to read files in the downloads folder.";
|
INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Atoll needs access to read files in the downloads folder.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
INFOPLIST_KEY_NSLocationUsageDescription = "Atoll uses your location to display accurate lock screen weather.";
|
INFOPLIST_KEY_NSLocationUsageDescription = "Atoll uses your location to display accurate lock screen weather.";
|
||||||
|
|
@ -536,7 +536,7 @@
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode";
|
||||||
INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode";
|
INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode";
|
||||||
INFOPLIST_KEY_NSDesktopFolderUsageDescription = "Atoll needs access to save screenshots to your desktop.";
|
INFOPLIST_KEY_NSDesktopFolderUsageDescription = "Atoll needs access to save screenshots to your desktop.";
|
||||||
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read AI assistant files.";
|
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "Atoll needs access to save and read shelf dragged files.";
|
||||||
INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Atoll needs access to read files in the downloads folder.";
|
INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Atoll needs access to read files in the downloads folder.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
INFOPLIST_KEY_NSLocationUsageDescription = "Atoll uses your location to display accurate lock screen weather.";
|
INFOPLIST_KEY_NSLocationUsageDescription = "Atoll uses your location to display accurate lock screen weather.";
|
||||||
|
|
|
||||||
|
|
@ -3183,6 +3183,15 @@ struct Shelf: View {
|
||||||
@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
|
@ObservedObject private var fullDiskAccessPermission = FullDiskAccessPermissionStore.shared
|
||||||
|
@ObservedObject private var shelfFolderAccessPermission = ShelfFolderAccessPermissionStore.shared
|
||||||
|
|
||||||
|
private var hasDocumentsAndDownloadsAccess: Bool {
|
||||||
|
shelfFolderAccessPermission.hasDocumentsAndDownloadsAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
private var canEnableShelf: Bool {
|
||||||
|
fullDiskAccessPermission.isAuthorized || hasDocumentsAndDownloadsAccess
|
||||||
|
}
|
||||||
|
|
||||||
private var selectedProvider: QuickShareProvider? {
|
private var selectedProvider: QuickShareProvider? {
|
||||||
quickShareService.availableProviders.first(where: { $0.id == quickShareProvider })
|
quickShareService.availableProviders.first(where: { $0.id == quickShareProvider })
|
||||||
|
|
@ -3198,11 +3207,25 @@ struct Shelf: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
if !fullDiskAccessPermission.isAuthorized {
|
if !canEnableShelf || !fullDiskAccessPermission.isAuthorized {
|
||||||
Section {
|
Section {
|
||||||
|
if !canEnableShelf {
|
||||||
SettingsPermissionCallout(
|
SettingsPermissionCallout(
|
||||||
title: "Full Disk Access required",
|
title: "Additional folder access required",
|
||||||
message: "Grant Full Disk Access so the Shelf can index and move files outside the app sandbox.",
|
message: "Enable Full Disk Access, or grant access to both Documents and Downloads folders to use Shelf.",
|
||||||
|
icon: "folder.badge.questionmark",
|
||||||
|
iconColor: .orange,
|
||||||
|
requestButtonTitle: "Request Folder Access",
|
||||||
|
openSettingsButtonTitle: "Open Privacy & Security",
|
||||||
|
requestAction: { shelfFolderAccessPermission.requestAccessPrompt() },
|
||||||
|
openSettingsAction: { shelfFolderAccessPermission.openSystemSettings() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fullDiskAccessPermission.isAuthorized {
|
||||||
|
SettingsPermissionCallout(
|
||||||
|
title: "Full Disk Access for global mode",
|
||||||
|
message: "Without Full Disk Access, Shelf can only read files from Documents and Downloads. Grant Full Disk Access to make Shelf work globally.",
|
||||||
icon: "externaldrive.fill",
|
icon: "externaldrive.fill",
|
||||||
iconColor: .purple,
|
iconColor: .purple,
|
||||||
requestButtonTitle: "Request Full Disk Access",
|
requestButtonTitle: "Request Full Disk Access",
|
||||||
|
|
@ -3210,6 +3233,7 @@ struct Shelf: View {
|
||||||
requestAction: { fullDiskAccessPermission.requestAccessPrompt() },
|
requestAction: { fullDiskAccessPermission.requestAccessPrompt() },
|
||||||
openSettingsAction: { fullDiskAccessPermission.openSystemSettings() }
|
openSettingsAction: { fullDiskAccessPermission.openSystemSettings() }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Permissions")
|
Text("Permissions")
|
||||||
}
|
}
|
||||||
|
|
@ -3217,7 +3241,7 @@ struct Shelf: View {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Enable shelf", key: .dynamicShelf)
|
Defaults.Toggle("Enable shelf", key: .dynamicShelf)
|
||||||
.disabled(!fullDiskAccessPermission.isAuthorized)
|
.disabled(!canEnableShelf)
|
||||||
.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)
|
||||||
|
|
@ -3304,6 +3328,7 @@ struct Shelf: View {
|
||||||
.navigationTitle("Shelf")
|
.navigationTitle("Shelf")
|
||||||
.onAppear {
|
.onAppear {
|
||||||
fullDiskAccessPermission.refreshStatus()
|
fullDiskAccessPermission.refreshStatus()
|
||||||
|
shelfFolderAccessPermission.refreshStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,53 @@ enum FullDiskAccessAuthorization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ShelfFolderAccessAuthorization {
|
||||||
|
private static var documentsDirectoryURL: URL? {
|
||||||
|
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var downloadsDirectoryURL: URL? {
|
||||||
|
FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasDocumentsAccess() -> Bool {
|
||||||
|
canReadDirectory(at: documentsDirectoryURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasDownloadsAccess() -> Bool {
|
||||||
|
canReadDirectory(at: downloadsDirectoryURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasDocumentsAndDownloadsAccess() -> Bool {
|
||||||
|
hasDocumentsAccess() && hasDownloadsAccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func requestAccessProbe() {
|
||||||
|
if let documentsDirectoryURL {
|
||||||
|
_ = try? FileManager.default.contentsOfDirectory(at: documentsDirectoryURL, includingPropertiesForKeys: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let downloadsDirectoryURL {
|
||||||
|
_ = try? FileManager.default.contentsOfDirectory(at: downloadsDirectoryURL, includingPropertiesForKeys: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func canReadDirectory(at url: URL?) -> Bool {
|
||||||
|
guard let url else { return false }
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try FileManager.default.contentsOfDirectory(
|
||||||
|
at: url,
|
||||||
|
includingPropertiesForKeys: nil,
|
||||||
|
options: [.skipsHiddenFiles]
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class FullDiskAccessPermissionStore: ObservableObject {
|
final class FullDiskAccessPermissionStore: ObservableObject {
|
||||||
static let shared = FullDiskAccessPermissionStore()
|
static let shared = FullDiskAccessPermissionStore()
|
||||||
|
|
@ -128,3 +175,83 @@ final class FullDiskAccessPermissionStore: ObservableObject {
|
||||||
isAuthorized = newValue
|
isAuthorized = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class ShelfFolderAccessPermissionStore: ObservableObject {
|
||||||
|
static let shared = ShelfFolderAccessPermissionStore()
|
||||||
|
|
||||||
|
@Published private(set) var hasDocumentsAccess: Bool = ShelfFolderAccessAuthorization.hasDocumentsAccess()
|
||||||
|
@Published private(set) var hasDownloadsAccess: Bool = ShelfFolderAccessAuthorization.hasDownloadsAccess()
|
||||||
|
|
||||||
|
var hasDocumentsAndDownloadsAccess: Bool {
|
||||||
|
hasDocumentsAccess && hasDownloadsAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pollingTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
pollingTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshStatus() {
|
||||||
|
updateStatus(
|
||||||
|
documents: ShelfFolderAccessAuthorization.hasDocumentsAccess(),
|
||||||
|
downloads: ShelfFolderAccessAuthorization.hasDownloadsAccess()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAccessPrompt() {
|
||||||
|
ShelfFolderAccessAuthorization.requestAccessProbe()
|
||||||
|
beginPollingForStatusChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openSystemSettings() {
|
||||||
|
#if os(macOS)
|
||||||
|
let urls = [
|
||||||
|
"x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders",
|
||||||
|
"x-apple.systempreferences:com.apple.preference.security"
|
||||||
|
]
|
||||||
|
|
||||||
|
for candidate in urls {
|
||||||
|
guard let url = URL(string: candidate) else { continue }
|
||||||
|
if NSWorkspace.shared.open(url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#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 docs = ShelfFolderAccessAuthorization.hasDocumentsAccess()
|
||||||
|
let downloads = ShelfFolderAccessAuthorization.hasDownloadsAccess()
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.updateStatus(documents: docs, downloads: downloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
if docs && downloads {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateStatus(documents: Bool, downloads: Bool) {
|
||||||
|
if hasDocumentsAccess != documents {
|
||||||
|
hasDocumentsAccess = documents
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasDownloadsAccess != downloads {
|
||||||
|
hasDownloadsAccess = downloads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue