Internal tooling for Mac utility for storage management.
Swift
98.7%
JSON
1.1%
Markdown
0.2%
//
// 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)
}
About
Internal tooling for Mac utility for storage management.
0 stars
0 forks