Internal tooling for Mac utility for storage management.

Swift 98.7% JSON 1.1% Markdown 0.2%
FullDiskAccessView.swift 214 lines (7 KB)
//
//  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: {}
    )
}

About

Internal tooling for Mac utility for storage management.

0 stars
0 forks