// // FullDiskAccessView.swift // MUA // // Created by Mitchel Volkering on 21/12/2025. // import SwiftUI /// A view that explains and guides users to grant Full Disk Access /// Follows Apple's guidelines: inform user why access is needed, then open System Settings struct FullDiskAccessView: View { @ObservedObject var accessManager = FullDiskAccessManager.shared @Environment(\.dismiss) private var dismiss let onContinueWithoutAccess: () -> Void let onAccessGranted: () -> Void @State private var isCheckingAccess = false var body: some View { VStack(spacing: 0) { // Header illustration headerSection // Content ScrollView { VStack(spacing: 24) { explanationSection stepsSection noteSection } .padding(.horizontal, 40) .padding(.vertical, 24) } Divider() // Actions actionButtons } .frame(width: 520, height: 580) .background(Color(nsColor: .windowBackgroundColor)) } private var headerSection: some View { ZStack { // Background gradient LinearGradient( colors: [.blue.opacity(0.15), .purple.opacity(0.1)], startPoint: .topLeading, endPoint: .bottomTrailing ) VStack(spacing: 16) { // Icon with glass effect ZStack { Circle() .fill( .linearGradient( colors: [.blue.opacity(0.7), .purple.opacity(0.5)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 80, height: 80) .overlay( Circle() .fill( .linearGradient( colors: [.white.opacity(0.4), .clear], startPoint: .topLeading, endPoint: .center ) ) ) .overlay( Circle() .strokeBorder(.white.opacity(0.3), lineWidth: 1.5) ) .shadow(color: .blue.opacity(0.3), radius: 15) Image(systemName: "lock.shield.fill") .font(.system(size: 36)) .foregroundStyle(.white) } Text("Full Disk Access Required") .font(.title2) .fontWeight(.semibold) } .padding(.vertical, 32) } .frame(height: 180) } private var explanationSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Why does MUA need this?") .font(.headline) Text("To accurately analyze disk usage across your entire system, MUA needs permission to read all files and folders. Without Full Disk Access, some system folders and application data will be hidden from the analysis.") .font(.subheadline) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) // What we can access without FDA VStack(alignment: .leading, spacing: 8) { accessItem(granted: true, text: "Your Documents, Downloads, Desktop") accessItem(granted: true, text: "Applications folder") accessItem(granted: false, text: "Mail, Messages, Safari data") accessItem(granted: false, text: "Other apps' private data") accessItem(granted: false, text: "System caches and logs") } .padding(.top, 8) } } private func accessItem(granted: Bool, text: String) -> some View { HStack(spacing: 10) { Image(systemName: granted ? "checkmark.circle.fill" : "xmark.circle.fill") .foregroundStyle(granted ? .green : .orange) Text(text) .font(.subheadline) .foregroundStyle(granted ? .primary : .secondary) } } private var stepsSection: some View { VStack(alignment: .leading, spacing: 16) { Text("How to enable") .font(.headline) VStack(alignment: .leading, spacing: 12) { stepRow(number: 1, text: "Click \"Open System Settings\" below") stepRow(number: 2, text: "Find MUA in the list") stepRow(number: 3, text: "Toggle the switch to enable access") stepRow(number: 4, text: "Return to MUA (you may need to relaunch)") } } } private func stepRow(number: Int, text: String) -> some View { HStack(alignment: .top, spacing: 12) { Text("\(number)") .font(.caption) .fontWeight(.bold) .foregroundStyle(.white) .frame(width: 22, height: 22) .background(Color.accentColor, in: Circle()) Text(text) .font(.subheadline) } } private var noteSection: some View { HStack(spacing: 12) { Image(systemName: "info.circle.fill") .foregroundStyle(.blue) Text("MUA never sends your data anywhere. All analysis happens locally on your Mac.") .font(.caption) .foregroundStyle(.secondary) } .padding() .background(.blue.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) } private var actionButtons: some View { HStack(spacing: 12) { Button("Continue Without Full Access") { onContinueWithoutAccess() } .buttonStyle(.bordered) Spacer() if isCheckingAccess { ProgressView() .scaleEffect(0.8) .padding(.horizontal, 8) } Button("I've Granted Access") { isCheckingAccess = true accessManager.refreshAccessStatus() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { isCheckingAccess = false if accessManager.hasFullDiskAccess { onAccessGranted() } } } .buttonStyle(.bordered) Button("Open System Settings") { accessManager.openFullDiskAccessSettings() } .buttonStyle(.borderedProminent) } .padding() } } #Preview { FullDiskAccessView( onContinueWithoutAccess: {}, onAccessGranted: {} ) }