Mac-utlity-app / MUA / Views / Disks / DisksView.swift Blame
200 lines
6dacfa4 Mitchel Jan 17, 2026
//
//  DisksView.swift
//  MUA
//
//  Created by Mitchel Volkering on 21/12/2025.
//
import SwiftUI
/// Shows all connected disks with detailed info
struct DisksView: View {
    @State private var disks: [DiskInfo] = []
    @State private var selectedDisk: DiskInfo?
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 16) {
                ForEach(disks) { disk in
                    DiskCardView(
                        disk: disk,
                        isSelected: selectedDisk?.id == disk.id
                    )
                    .onTapGesture {
                        withAnimation(.spring(response: 0.3)) {
                            selectedDisk = disk
                        }
                    }
                }
            }
            .padding()
        }
        .background(Color(nsColor: .windowBackgroundColor))
        .onAppear {
            refreshDisks()
        }
        .toolbar {
            ToolbarItem {
                Button {
                    refreshDisks()
                } label: {
                    Label("Refresh", systemImage: "arrow.clockwise")
                }
            }
        }
    }
    private func refreshDisks() {
        disks = DiskInfo.getAllDisks()
    }
}
struct DiskCardView: View {
    let disk: DiskInfo
    let isSelected: Bool
    @State private var isHovered = false
    var body: some View {
        HStack(spacing: 20) {
            // Disk icon with glass effect
            ZStack {
                RoundedRectangle(cornerRadius: 16)
                    .fill(
                        .linearGradient(
                            colors: [
                                iconColor.opacity(0.6),
                                iconColor.opacity(0.3)
                            ],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                    )
                    .frame(width: 72, height: 72)
                    .overlay(
                        RoundedRectangle(cornerRadius: 16)
                            .fill(
                                .linearGradient(
                                    colors: [.white.opacity(0.3), .clear],
                                    startPoint: .topLeading,
                                    endPoint: .center
                                )
                            )
                    )
                    .overlay(
                        RoundedRectangle(cornerRadius: 16)
                            .strokeBorder(.white.opacity(0.3), lineWidth: 1)
                    )
                    .shadow(color: iconColor.opacity(0.3), radius: isHovered ? 12 : 6)
                Image(systemName: disk.icon)
                    .font(.system(size: 32))
                    .foregroundStyle(.white)
            }
            VStack(alignment: .leading, spacing: 8) {
                HStack {
                    Text(disk.name)
                        .font(.title3)
                        .fontWeight(.semibold)
                    if disk.isRemovable {
                        Text("Removable")
                            .font(.caption2)
                            .padding(.horizontal, 6)
                            .padding(.vertical, 2)
                            .background(.quaternary, in: Capsule())
                    }
                }
                Text(disk.mountPoint.path)
                    .font(.caption)
                    .foregroundStyle(.tertiary)
                // Usage bar
                GeometryReader { geo in
                    ZStack(alignment: .leading) {
                        RoundedRectangle(cornerRadius: 4)
                            .fill(.quaternary)
                        RoundedRectangle(cornerRadius: 4)
                            .fill(usageGradient)
                            .frame(width: geo.size.width * CGFloat(disk.usedPercentage / 100))
                    }
                }
                .frame(height: 10)
                HStack {
                    Text("\(disk.formattedUsedSpace) used")
                        .foregroundStyle(.secondary)
                    Text("•")
                        .foregroundStyle(.quaternary)
                    Text("\(disk.formattedFreeSpace) free")
                        .foregroundStyle(.secondary)
                    Spacer()
                    Text(disk.formattedTotalSpace)
                        .foregroundStyle(.tertiary)
                }
                .font(.caption)
            }
            Spacer()
            // Usage percentage
            VStack {
                Text("\(Int(disk.usedPercentage))%")
                    .font(.system(size: 24, weight: .semibold, design: .rounded))
                    .foregroundStyle(usageColor)
                Text("used")
                    .font(.caption2)
                    .foregroundStyle(.tertiary)
            }
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 16)
                .fill(.regularMaterial)
                .overlay(
                    RoundedRectangle(cornerRadius: 16)
                        .strokeBorder(
                            isSelected ? Color.accentColor.opacity(0.5) : Color.clear,
                            lineWidth: 2
                        )
                )
        )
        .shadow(color: .black.opacity(0.05), radius: 8, y: 4)
        .scaleEffect(isHovered ? 1.01 : 1.0)
        .animation(.spring(response: 0.3), value: isHovered)
        .onHover { hovering in
            isHovered = hovering
        }
    }
    private var iconColor: Color {
        if disk.isRemovable { return .orange }
        if disk.isInternal { return .blue }
        return .purple
    }
    private var usageColor: Color {
        let percentage = disk.usedPercentage
        if percentage > 90 { return .red }
        if percentage > 75 { return .orange }
        return .green
    }
    private var usageGradient: LinearGradient {
        let color = usageColor
        return .linearGradient(
            colors: [color.opacity(0.8), color],
            startPoint: .leading,
            endPoint: .trailing
        )
    }
}
#Preview {
    DisksView()
        .frame(width: 700, height: 500)
}