Skip to content

Commit

Permalink
TreeMap updates
Browse files Browse the repository at this point in the history
  • Loading branch information
alienator88 committed Aug 2, 2024
1 parent e0d41e7 commit e425219
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 69 deletions.
8 changes: 6 additions & 2 deletions Pearcleaner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; };
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
Expand Down Expand Up @@ -99,6 +100,7 @@
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; };
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -180,6 +182,8 @@
C77B901B2AF193A3009CC655 /* Styles.swift */,
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
C74FDC622BCD833D00B8960F /* Conditions.swift */,
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */,
);
path = Logic;
sourceTree = "<group>";
Expand Down Expand Up @@ -270,8 +274,7 @@
C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */,
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */,
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -432,6 +435,7 @@
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */,
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
Expand Down
26 changes: 24 additions & 2 deletions Pearcleaner/Logic/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ class AppState: ObservableObject {
id: UUID(),
fileSize: [:],
fileSizeLogical: [:],
fileIcon: [:]
fileIcon: [:],
isDirectory: [:]
)

updateExtensionStatus()
Expand Down Expand Up @@ -133,6 +134,7 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
var fileSize: [URL:Int64]
var fileSizeLogical: [URL:Int64]
var fileIcon: [URL:NSImage?]
var isDirectory: [URL:Bool]
var totalSize: Int64
{
return fileSize.values.reduce(0, +)
Expand All @@ -143,8 +145,28 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
}


static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:], isDirectory: [:])

}

extension ZombieFile {
/// Converts the data in ZombieFile into a list of Item instances.
func toItems() -> [Item] {
var items = [Item]()

// Iterate over each URL in fileSize to get corresponding properties
for (url, size) in fileSize {
let name = url.lastPathComponent
let isDir = isDirectory[url] ?? false
let parent = url.deletingLastPathComponent()

// Create an Item instance for each URL
let item = Item(url: url, name: name, size: size, isDirectory: isDir, parentURL: parent)
items.append(item)
}

return items//.sorted { $0.size > $1.size }
}
}


Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions Pearcleaner/Logic/ReversePathsFetch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ReversePathsSearcher {
private var fileSize: [URL: Int64] = [:]
private var fileSizeLogical: [URL: Int64] = [:]
private var fileIcon: [URL: NSImage?] = [:]
private var isDirectory: [URL: Bool] = [:]
private let dispatchGroup = DispatchGroup()
private let sortedApps: [AppInfo]

Expand Down Expand Up @@ -112,6 +113,7 @@ class ReversePathsSearcher {
fileSize[path] = size.real
fileSizeLogical[path] = size.logical
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
isDirectory[path] = path.hasDirectoryPath
}
}

Expand All @@ -121,6 +123,7 @@ class ReversePathsSearcher {
updatedZombieFile.fileSize = self.fileSize
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
updatedZombieFile.fileIcon = self.fileIcon
updatedZombieFile.isDirectory = self.isDirectory
self.appState.zombieFile = updatedZombieFile
self.appState.showProgress = false
}
Expand Down
File renamed without changes.
261 changes: 261 additions & 0 deletions Pearcleaner/Views/TreeMap.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
//
// TreeMap.swift
// Pearcleaner
//
// Created by Alin Lupascu on 8/1/24.
//

import Foundation
import SwiftUI
import AlinFoundation

final class Item: Identifiable {
let url: URL
let name: String
var size: Int64
let isDirectory: Bool
let parentURL: URL?
let timestamp: Date

init(url: URL, name: String, size: Int64, isDirectory: Bool, parentURL: URL? = nil) {
self.url = url
self.name = name
self.size = size
self.isDirectory = isDirectory
self.parentURL = parentURL
self.timestamp = Date()
}
}

struct TreeMapChart: View {
let items: [Item]
var onItemSelected: (Item) -> Void
@Binding var hoveredItem: Item?

var displayedItems: [Item] {
items.filter { $0.size >= 1_048_576 } // Filter to keep only items with size >= 1MB
.sorted(by: { $0.size > $1.size }) // Then sort the remaining items by size
}

var body: some View {

GeometryReader { proxy in

let ld = calculateLayout(
w: proxy.size.width,
h: proxy.size.height,
data: displayedItems.map { Double($0.size) },
from: 0
)

TreeMapView(ld: ld, items: displayedItems, onItemSelected: onItemSelected, hoveredItem: $hoveredItem)

}
.ignoresSafeArea()
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}

struct TreeMapView: View {
let ld: LayoutData
let items: [Item]
var onItemSelected: (Item) -> Void
@Binding var hoveredItem: Item?
@State private var hoveredIndex: Int?

var body: some View {
Group {
if ld.direction == .h {
HStack(spacing: 0) {
VStack(spacing: 0) {
content
}
if let child = ld.child {
TreeMapView(ld: child, items: items, onItemSelected: onItemSelected, hoveredItem: $hoveredItem)
}
}
} else {
VStack(spacing: 0) {
HStack(spacing: 0) {
content
}

if let child = ld.child {
TreeMapView(ld: child, items: items, onItemSelected: onItemSelected, hoveredItem: $hoveredItem)
}
}
}
}
}

private var content: some View {
ForEach(0..<ld.content.count, id: \.self) { i in
let file = items[ld.content[i].index]
let maxDimension = max(ld.content[i].w, ld.content[i].h) / 5

ZStack {
Rectangle()
// .foregroundStyle(randomColor())
// .foregroundStyle(colorForItem(file))
.fill(gradientForItem(file))
.brightness(hoveredIndex == i ? 0.1 : 0)
.frame(width: ld.content[i].w, height: ld.content[i].h)
.border(.black, width: 0.5)
.onTapGesture {
onItemSelected(file)
}
.overlay {

if hoveredIndex == i {
VStack {
Text(file.name)
.minimumScaleFactor(0.5)
.lineLimit(2)
Text("\(formatSize(file.size))")
.minimumScaleFactor(0.5)
.lineLimit(1)
}
.padding(5)
.padding(.horizontal, 2)
.background(.background.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.zIndex(1)

} else {
// Image(systemName: file.isDirectory ? "folder.fill" : "doc.fill")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: maxDimension, height: maxDimension)
// .foregroundStyle(.primary.opacity(0.5))
}
}
}
.onHover { isHovering in
withAnimation {
hoveredIndex = isHovering ? i : nil
hoveredItem = isHovering ? file : nil
}
}

}
}

private func colorForItem(_ item: Item) -> Color {
item.isDirectory ? .blue : .gray
}

private func gradientForItem(_ item: Item) -> LinearGradient {
item.isDirectory ? LinearGradient(
gradient: Gradient(colors: [Color.cyan, Color.blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
) : LinearGradient(
gradient: Gradient(colors: [Color.pink, Color.orange]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}

}


func formatSize(_ size: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useAll]
formatter.countStyle = .file
return formatter.string(fromByteCount: size)
}

func randomColor() -> Color {
Color(hue: Double.random(in: 0.0...1.0),
saturation: Double.random(in: 0.08...0.18),
brightness: Double.random(in: 0.90...1.0))
}

//MARK: ===================================================================================================================

// Layout
enum Direction {
case h
case v
}

class LayoutData {
var direction: Direction = .h
var content: [(index: Int, w: Double, h: Double)] = []
var child: LayoutData? = nil
}

func calculateLayout(w: Double, h: Double, data: [Double], from: Int) -> LayoutData {

let returnData = LayoutData()
if data.isEmpty {
print("TreeMap layout data is empty")
return returnData
}
let dataToArea = w * h / data[from...].reduce(0.0, +)
if w < h {
returnData.direction = .v
} else {
returnData.direction = .h
}
let mainLength = min(w, h)
var currentIndex = from
var area = data[currentIndex] * dataToArea
var crossLength = area / mainLength

var cellRatio = mainLength / crossLength
cellRatio = max(cellRatio, 1.0 / cellRatio)

while currentIndex + 1 < data.count {
let newIndex = currentIndex + 1
let newArea = area + data[newIndex] * dataToArea
let newCrossLength = newArea / mainLength
var newCellRatio = data[newIndex] * dataToArea / newCrossLength / newCrossLength
newCellRatio = max(newCellRatio, 1.0 / newCellRatio)

if newCellRatio < cellRatio {
currentIndex = newIndex
area = newArea
crossLength = newCrossLength
cellRatio = newCellRatio
} else {
break
}
}

switch returnData.direction {
case .h:
for i in from...currentIndex {
returnData.content.append((
index: i,
w: crossLength,
h: data[i] * dataToArea / crossLength))
}
case .v:
for i in from...currentIndex {
returnData.content.append((
index: i,
w: data[i] * dataToArea / crossLength,
h: crossLength))
}
}

if currentIndex != data.count - 1 {
switch returnData.direction {
case .h:
returnData.child = calculateLayout(
w: w - crossLength,
h: h,
data: data,
from: currentIndex + 1)
case .v:
returnData.child = calculateLayout(
w: w,
h: h - crossLength,
data: data,
from: currentIndex + 1)
}
}
return returnData
}
Loading

0 comments on commit e425219

Please sign in to comment.