Mac-utlity-app / MUA / Models / FileItem.swift Blame
135 lines
6dacfa4 Mitchel Jan 17, 2026
//
//  FileItem.swift
//  MUA
//
//  Created by Mitchel Volkering on 21/12/2025.
//
import Foundation
import SwiftUI
/// Represents a file or folder in the disk analysis
/// Using nonisolated(unsafe) to allow creation and mutation from any thread
/// This is safe because we only mutate during scanning (single-threaded per item)
/// and then only read from the main thread after scanning completes
final class FileItem: Identifiable, Hashable, @unchecked Sendable {
    let id = UUID()
    let url: URL
    let name: String
    let isDirectory: Bool
    nonisolated(unsafe) var size: Int64
    nonisolated(unsafe) var children: [FileItem]
    nonisolated(unsafe) weak var parent: FileItem?
    /// Color for visualization - assigned based on file type or folder depth
    var color: Color {
        if isDirectory {
            return folderColor
        }
        return fileTypeColor
    }
    private var folderColor: Color {
        let colors: [Color] = [
            .blue, .purple, .pink, .orange, .yellow, .green, .teal, .cyan, .indigo, .mint
        ]
        let hash = abs(name.hashValue)
        return colors[hash % colors.count]
    }
    private var fileTypeColor: Color {
        let ext = url.pathExtension.lowercased()
        switch ext {
        case "app", "dmg", "pkg":
            return .purple
        case "jpg", "jpeg", "png", "gif", "heic", "raw", "svg":
            return .pink
        case "mp4", "mov", "avi", "mkv", "m4v":
            return .red
        case "mp3", "wav", "aac", "flac", "m4a":
            return .orange
        case "zip", "tar", "gz", "rar", "7z":
            return .yellow
        case "pdf", "doc", "docx", "txt", "rtf":
            return .blue
        case "swift", "js", "py", "html", "css", "json":
            return .green
        case "xls", "xlsx", "csv", "numbers":
            return .teal
        default:
            return .gray
        }
    }
    /// Formatted size string
    var formattedSize: String {
        ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
    }
    /// Percentage of parent's size
    func percentageOfParent() -> Double {
        guard let parent = parent, parent.size > 0 else { return 100 }
        return (Double(size) / Double(parent.size)) * 100
    }
    /// System icon for file type
    var icon: String {
        if isDirectory {
            return "folder.fill"
        }
        let ext = url.pathExtension.lowercased()
        switch ext {
        case "app":
            return "app.fill"
        case "dmg", "pkg":
            return "shippingbox.fill"
        case "jpg", "jpeg", "png", "gif", "heic", "raw", "svg":
            return "photo.fill"
        case "mp4", "mov", "avi", "mkv", "m4v":
            return "film.fill"
        case "mp3", "wav", "aac", "flac", "m4a":
            return "music.note"
        case "zip", "tar", "gz", "rar", "7z":
            return "doc.zipper"
        case "pdf":
            return "doc.richtext.fill"
        case "doc", "docx", "txt", "rtf":
            return "doc.text.fill"
        case "swift", "js", "py", "html", "css", "json":
            return "chevron.left.forwardslash.chevron.right"
        case "xls", "xlsx", "csv", "numbers":
            return "tablecells.fill"
        default:
            return "doc.fill"
        }
    }
    nonisolated init(url: URL, isDirectory: Bool, size: Int64 = 0, children: [FileItem] = [], parent: FileItem? = nil) {
        self.url = url
        self.name = url.lastPathComponent
        self.isDirectory = isDirectory
        self.size = size
        self.children = children
        self.parent = parent
    }
    static func == (lhs: FileItem, rhs: FileItem) -> Bool {
        lhs.id == rhs.id
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    /// Get top N children by size
    func topChildren(count: Int = 10) -> [FileItem] {
        children.sorted { $0.size > $1.size }.prefix(count).map { $0 }
    }
    /// Calculate total count of all descendants
    var descendantCount: Int {
        if !isDirectory { return 0 }
        return children.reduce(0) { $0 + 1 + $1.descendantCount }
    }
}