// // LargeFilesView.swift // MUA // // Created by Mitchel Volkering on 21/12/2025. // import SwiftUI /// View for finding large files quickly struct LargeFilesView: View { @State private var files: [FileItem] = [] @State private var isScanning = false @State private var scanProgress: Double = 0 @State private var selectedFile: FileItem? @State private var minimumSize: Int64 = 100_000_000 // 100 MB @State private var searchPath: URL = FileManager.default.homeDirectoryForCurrentUser private let sizeOptions: [(String, Int64)] = [ ("10 MB", 10_000_000), ("50 MB", 50_000_000), ("100 MB", 100_000_000), ("500 MB", 500_000_000), ("1 GB", 1_000_000_000), ] var body: some View { VStack(spacing: 0) { // Toolbar toolbar // Content if isScanning { scanningView } else if files.isEmpty { emptyView } else { filesList } } .background(Color(nsColor: .windowBackgroundColor)) } private var toolbar: some View { HStack(spacing: 16) { // Size picker Picker("Minimum size", selection: $minimumSize) { ForEach(sizeOptions, id: \.1) { option in Text(option.0).tag(option.1) } } .pickerStyle(.segmented) .frame(maxWidth: 400) Spacer() // Scan button Button { Task { await scanForLargeFiles() } } label: { Label("Scan Home", systemImage: "magnifyingglass") } .buttonStyle(.borderedProminent) .disabled(isScanning) // Total size badge if !files.isEmpty { let totalSize = files.reduce(0) { $0 + $1.size } Text("Total: \(ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file))") .font(.subheadline) .foregroundStyle(.secondary) .padding(.horizontal, 12) .padding(.vertical, 6) .background(.ultraThinMaterial, in: Capsule()) } } .padding() .background(.ultraThinMaterial) } private var scanningView: some View { VStack(spacing: 20) { ProgressView(value: scanProgress) .progressViewStyle(.circular) .scaleEffect(1.5) Text("Searching for large files...") .font(.headline) Text("\(files.count) files found") .font(.subheadline) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } private var emptyView: some View { VStack(spacing: 16) { Image(systemName: "doc.badge.arrow.up") .font(.system(size: 56)) .foregroundStyle(.quaternary) Text("Find Large Files") .font(.title2) .fontWeight(.medium) Text("Scan to find files larger than \(ByteCountFormatter.string(fromByteCount: minimumSize, countStyle: .file))") .font(.subheadline) .foregroundStyle(.secondary) Button { Task { await scanForLargeFiles() } } label: { Label("Start Scan", systemImage: "magnifyingglass") } .buttonStyle(.borderedProminent) .controlSize(.large) } .frame(maxWidth: .infinity, maxHeight: .infinity) } private var filesList: some View { HSplitView { // File list List(selection: $selectedFile) { ForEach(files) { file in LargeFileRow(file: file, isSelected: selectedFile?.id == file.id) .tag(file) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) } } .listStyle(.plain) .frame(minWidth: 400) // Detail panel DetailPanelView(item: selectedFile) } } private func scanForLargeFiles() async { isScanning = true files = [] scanProgress = 0 let searchURL = searchPath let minSize = minimumSize // Perform file enumeration on background thread using a static helper let foundFiles = await Task.detached(priority: .userInitiated) { Self.scanForFilesSync(at: searchURL, minSize: minSize) }.value files = foundFiles isScanning = false scanProgress = 1.0 } /// Synchronous file scanning helper - runs on background thread private nonisolated static func scanForFilesSync(at url: URL, minSize: Int64) -> [FileItem] { let fileManager = FileManager.default var results: [FileItem] = [] guard let enumerator = fileManager.enumerator( at: url, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsPackageDescendants] ) else { return results } while let fileURL = enumerator.nextObject() as? URL { do { let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) guard resourceValues.isDirectory == false else { continue } let size = Int64(resourceValues.fileSize ?? 0) if size >= minSize { let item = FileItem(url: fileURL, isDirectory: false, size: size) results.append(item) } } catch { continue } } return results.sorted { $0.size > $1.size } } } struct LargeFileRow: View { let file: FileItem let isSelected: Bool @State private var isHovered = false var body: some View { HStack(spacing: 12) { // Icon with glass effect ZStack { RoundedRectangle(cornerRadius: 8) .fill( .linearGradient( colors: [ file.color.opacity(0.6), file.color.opacity(0.3) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 40, height: 40) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(.white.opacity(0.2), lineWidth: 1) ) Image(systemName: file.icon) .foregroundStyle(.white) } VStack(alignment: .leading, spacing: 2) { Text(file.name) .lineLimit(1) .truncationMode(.middle) Text(file.url.deletingLastPathComponent().path) .font(.caption) .foregroundStyle(.tertiary) .lineLimit(1) .truncationMode(.middle) } Spacer() Text(file.formattedSize) .font(.system(.body, design: .rounded, weight: .medium)) .foregroundStyle(.secondary) } .padding(8) .background( RoundedRectangle(cornerRadius: 10) .fill(isSelected ? Color.accentColor.opacity(0.1) : (isHovered ? Color.primary.opacity(0.03) : Color.clear)) ) .onHover { hovering in isHovered = hovering } .contextMenu { Button("Show in Finder") { NSWorkspace.shared.selectFile(file.url.path, inFileViewerRootedAtPath: file.url.deletingLastPathComponent().path) } Button("Move to Trash", role: .destructive) { do { try FileManager.default.trashItem(at: file.url, resultingItemURL: nil) } catch { print("Failed to trash: \(error)") } } } } } #Preview { LargeFilesView() .frame(width: 800, height: 600) }