import AppKit import CodexBarCore import 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? 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")) } }