213 lines · 7.5 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport SwiftUI struct StorageMenuCardSectionView: View {    let storageText: String    let topPadding: CGFloat    let bottomPadding: CGFloat    let width: CGFloat     var body: some View {        VStack(alignment: .leading, spacing: 6) {            Text(L("Storage"))                .font(.body)                .fontWeight(.medium)            Text(self.storageText)                .font(.caption)        }        .padding(.horizontal, UsageMenuCardLayout.horizontalPadding)        .padding(.top, self.topPadding)        .padding(.bottom, self.bottomPadding)        .frame(width: self.width, alignment: .leading)    }} struct StorageBreakdownMenuView: View {    let footprint: ProviderStorageFootprint    let width: CGFloat    let maxHeight: CGFloat     init(footprint: ProviderStorageFootprint, width: CGFloat, maxHeight: CGFloat = 560) {        self.footprint = footprint        self.width = width        self.maxHeight = maxHeight    }     var cleanupRecommendations: [ProviderStorageRecommendation] {        self.footprint.cleanupRecommendations    }     var copyablePaths: [String] {        let recommendationPaths = self.cleanupRecommendations.map(\.path)        return self.visibleComponents.map(\.path) + recommendationPaths    }     private var visibleComponents: [ProviderStorageFootprint.Component] {        Array(self.footprint.components.prefix(8))    }     private var maxBytes: Int64 {        max(self.visibleComponents.map(\.totalBytes).max() ?? 0, 1)    }     var body: some View {        ScrollView(.vertical) {            self.content        }        .scrollIndicators(.visible)        .frame(            minWidth: self.width,            idealWidth: self.width,            maxWidth: self.width,            maxHeight: self.maxHeight,            alignment: .topLeading)    }     private var content: some View {        VStack(alignment: .leading, spacing: 10) {            VStack(alignment: .leading, spacing: 3) {                Text(L("Storage"))                    .font(.body)                    .fontWeight(.medium)                Text(String(format: L("Total: %@"), UsageFormatter.byteCountString(self.footprint.totalBytes)))                    .font(.caption)                    .foregroundStyle(.secondary)            }             if self.visibleComponents.isEmpty {                Text(L("No local data found"))                    .font(.footnote)                    .foregroundStyle(.secondary)            } else {                VStack(alignment: .leading, spacing: 8) {                    ForEach(self.visibleComponents) { component in                        self.componentRow(component)                    }                }            }             if self.footprint.components.count > self.visibleComponents.count {                Text(String(format: L("%d more items"), self.footprint.components.count - self.visibleComponents.count))                    .font(.caption)                    .foregroundStyle(.secondary)            }            if !self.cleanupRecommendations.isEmpty {                Divider()                    .padding(.vertical, 2)                VStack(alignment: .leading, spacing: 8) {                    Text(L("Cleanup ideas"))                        .font(.body)                        .fontWeight(.medium)                    ForEach(self.cleanupRecommendations) { recommendation in                        self.recommendationRow(recommendation)                    }                }            }            if !self.footprint.unreadablePaths.isEmpty {                Text(String(format: L("%d unreadable item(s) skipped"), self.footprint.unreadablePaths.count))                    .font(.caption)                    .foregroundStyle(.secondary)            }        }        .padding(.horizontal, 16)        .padding(.vertical, 10)        .frame(width: self.width, alignment: .leading)    }     private func componentRow(_ component: ProviderStorageFootprint.Component) -> some View {        let fraction = CGFloat(max(0, min(1, Double(component.totalBytes) / Double(self.maxBytes))))        return VStack(alignment: .leading, spacing: 4) {            HStack(alignment: .firstTextBaseline) {                Text(component.path)                    .font(.caption)                    .lineLimit(1)                    .truncationMode(.middle)                    .help(component.path)                    .layoutPriority(1)                Spacer()                StoragePathCopyButton(path: component.path)                Text(UsageFormatter.byteCountString(component.totalBytes))                    .font(.caption)                    .foregroundStyle(.secondary)                    .lineLimit(1)            }            GeometryReader { proxy in                ZStack(alignment: .leading) {                    Capsule()                        .fill(Color(nsColor: .quaternaryLabelColor))                    Capsule()                        .fill(self.providerColor)                        .frame(width: max(2, proxy.size.width * fraction))                }            }            .frame(height: 5)        }    }     private func recommendationRow(_ recommendation: ProviderStorageRecommendation) -> some View {        VStack(alignment: .leading, spacing: 3) {            HStack(alignment: .firstTextBaseline) {                Text(L(recommendation.title))                    .font(.caption)                    .fontWeight(.medium)                    .lineLimit(1)                Spacer()                Text(UsageFormatter.byteCountString(recommendation.bytes))                    .font(.caption)                    .foregroundStyle(.secondary)                    .lineLimit(1)            }            HStack(spacing: 4) {                Text(recommendation.path)                    .font(.caption)                    .foregroundStyle(.secondary)                    .lineLimit(1)                    .truncationMode(.middle)                    .help(recommendation.path)                    .layoutPriority(1)                Spacer()                StoragePathCopyButton(path: recommendation.path)            }            Text(L(recommendation.consequence))                .font(.caption)                .foregroundStyle(.secondary)                .lineLimit(3)                .fixedSize(horizontal: false, vertical: true)        }    }     private var providerColor: Color {        let color = ProviderDescriptorRegistry.descriptor(for: self.footprint.provider).branding.color        return Color(red: color.red, green: color.green, blue: color.blue)    }} struct StoragePathCopyButton: View {    let path: String     @State private var didCopy = false    @State private var resetTask: Task<Void, Never>?     var body: some View {        Button {            self.resetTask?.cancel()            MenuPasteboardCopy.perform(self.path, completion: {                self.didCopy = true                self.resetTask = Task { @MainActor in                    try? await Task.sleep(for: .seconds(0.9))                    self.didCopy = false                }            })        } label: {            Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc")                .font(.caption2.weight(.semibold))                .foregroundStyle(.secondary)                .frame(width: 18, height: 18)                .contentShape(Rectangle())        }        .buttonStyle(.plain)        .help(self.didCopy ? L("Copied") : L("Copy path"))        .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy path"))    }}