mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +00:00
TreeMap updates
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
||||||
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.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 */; };
|
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 */; };
|
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
|
||||||
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
||||||
@@ -99,6 +100,7 @@
|
|||||||
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||||
@@ -180,6 +182,8 @@
|
|||||||
C77B901B2AF193A3009CC655 /* Styles.swift */,
|
C77B901B2AF193A3009CC655 /* Styles.swift */,
|
||||||
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
|
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
|
||||||
C74FDC622BCD833D00B8960F /* Conditions.swift */,
|
C74FDC622BCD833D00B8960F /* Conditions.swift */,
|
||||||
|
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
|
||||||
|
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */,
|
||||||
);
|
);
|
||||||
path = Logic;
|
path = Logic;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -270,8 +274,7 @@
|
|||||||
C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
|
C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
|
||||||
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
|
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
|
||||||
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */,
|
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */,
|
||||||
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
|
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */,
|
||||||
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */,
|
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -432,6 +435,7 @@
|
|||||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
||||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||||
|
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */,
|
||||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||||
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
|
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
|
||||||
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
|
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ class AppState: ObservableObject {
|
|||||||
id: UUID(),
|
id: UUID(),
|
||||||
fileSize: [:],
|
fileSize: [:],
|
||||||
fileSizeLogical: [:],
|
fileSizeLogical: [:],
|
||||||
fileIcon: [:]
|
fileIcon: [:],
|
||||||
|
isDirectory: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
updateExtensionStatus()
|
updateExtensionStatus()
|
||||||
@@ -133,6 +134,7 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
|
|||||||
var fileSize: [URL:Int64]
|
var fileSize: [URL:Int64]
|
||||||
var fileSizeLogical: [URL:Int64]
|
var fileSizeLogical: [URL:Int64]
|
||||||
var fileIcon: [URL:NSImage?]
|
var fileIcon: [URL:NSImage?]
|
||||||
|
var isDirectory: [URL:Bool]
|
||||||
var totalSize: Int64
|
var totalSize: Int64
|
||||||
{
|
{
|
||||||
return fileSize.values.reduce(0, +)
|
return fileSize.values.reduce(0, +)
|
||||||
@@ -143,10 +145,30 @@ 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum Arch {
|
enum Arch {
|
||||||
case arm
|
case arm
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class ReversePathsSearcher {
|
|||||||
private var fileSize: [URL: Int64] = [:]
|
private var fileSize: [URL: Int64] = [:]
|
||||||
private var fileSizeLogical: [URL: Int64] = [:]
|
private var fileSizeLogical: [URL: Int64] = [:]
|
||||||
private var fileIcon: [URL: NSImage?] = [:]
|
private var fileIcon: [URL: NSImage?] = [:]
|
||||||
|
private var isDirectory: [URL: Bool] = [:]
|
||||||
private let dispatchGroup = DispatchGroup()
|
private let dispatchGroup = DispatchGroup()
|
||||||
private let sortedApps: [AppInfo]
|
private let sortedApps: [AppInfo]
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ class ReversePathsSearcher {
|
|||||||
fileSize[path] = size.real
|
fileSize[path] = size.real
|
||||||
fileSizeLogical[path] = size.logical
|
fileSizeLogical[path] = size.logical
|
||||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||||
|
isDirectory[path] = path.hasDirectoryPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +123,7 @@ class ReversePathsSearcher {
|
|||||||
updatedZombieFile.fileSize = self.fileSize
|
updatedZombieFile.fileSize = self.fileSize
|
||||||
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
|
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
|
||||||
updatedZombieFile.fileIcon = self.fileIcon
|
updatedZombieFile.fileIcon = self.fileIcon
|
||||||
|
updatedZombieFile.isDirectory = self.isDirectory
|
||||||
self.appState.zombieFile = updatedZombieFile
|
self.appState.zombieFile = updatedZombieFile
|
||||||
self.appState.showProgress = false
|
self.appState.showProgress = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ struct ZombieView: View {
|
|||||||
@State private var totalRealSizeUninstallBtn: String = ""
|
@State private var totalRealSizeUninstallBtn: String = ""
|
||||||
@State private var totalLogicalSizeUninstallBtn: String = ""
|
@State private var totalLogicalSizeUninstallBtn: String = ""
|
||||||
@State private var totalFinderSizeUninstallBtn: String = ""
|
@State private var totalFinderSizeUninstallBtn: String = ""
|
||||||
|
@State private var isTreeMap: Bool = false
|
||||||
|
@State private var hoveredItem: Item?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
@@ -170,6 +172,18 @@ struct ZombieView: View {
|
|||||||
.padding(.top, 0)
|
.padding(.top, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle("Tree Map View", isOn: $isTreeMap)
|
||||||
|
|
||||||
|
|
||||||
|
if isTreeMap {
|
||||||
|
TreeMapChart(items: appState.zombieFile.toItems(), onItemSelected: { selectedItem in
|
||||||
|
// if selectedItem.isDirectory {
|
||||||
|
// drillDown(to: selectedItem)
|
||||||
|
// } else {
|
||||||
|
// NSWorkspace.shared.selectFile(selectedItem.url.path, inFileViewerRootedAtPath: selectedItem.url.deletingLastPathComponent().path)
|
||||||
|
// }
|
||||||
|
}, hoveredItem: $hoveredItem)
|
||||||
|
} else {
|
||||||
// Item selection and sorting toolbar
|
// Item selection and sorting toolbar
|
||||||
HStack {
|
HStack {
|
||||||
Toggle("", isOn: Binding(
|
Toggle("", isOn: Binding(
|
||||||
@@ -215,6 +229,7 @@ struct ZombieView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Button("") {
|
Button("") {
|
||||||
selectedSortAlpha.toggle()
|
selectedSortAlpha.toggle()
|
||||||
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||||
@@ -230,11 +245,11 @@ struct ZombieView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
|
||||||
ScrollView() {
|
ScrollView() {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(memoizedFiles, id: \.self) { file in
|
ForEach(memoizedFiles, id: \.self) { file in
|
||||||
if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file] {
|
if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file], let iconImage = fileIcon.map(Image.init(nsImage:)) {
|
||||||
let iconImage = fileIcon.map(Image.init(nsImage:))
|
|
||||||
VStack {
|
VStack {
|
||||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
@@ -245,6 +260,12 @@ struct ZombieView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -252,6 +273,8 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
HStack() {
|
HStack() {
|
||||||
|
|
||||||
|
Text(hoveredItem?.name ?? "")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
InfoButton(text: "Leftover file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.", color: .orange, warning: true, edge: .top)
|
InfoButton(text: "Leftover file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.", color: .orange, warning: true, edge: .top)
|
||||||
@@ -484,18 +507,6 @@ struct ZombieFileDetailsItem: View {
|
|||||||
Toggle("", isOn: $isSelected)
|
Toggle("", isOn: $isSelected)
|
||||||
.toggleStyle(SimpleCheckboxToggleStyle())
|
.toggleStyle(SimpleCheckboxToggleStyle())
|
||||||
|
|
||||||
// Toggle("", isOn: Binding(
|
|
||||||
// get: { self.selectedZombieItemsLocal.contains(self.path) },
|
|
||||||
// set: { isChecked in
|
|
||||||
// if isChecked {
|
|
||||||
// self.selectedZombieItemsLocal.insert(self.path)
|
|
||||||
// } else {
|
|
||||||
// self.selectedZombieItemsLocal.remove(self.path)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ))
|
|
||||||
// .toggleStyle(SimpleCheckboxToggleStyle())
|
|
||||||
|
|
||||||
if let appIcon = icon {
|
if let appIcon = icon {
|
||||||
appIcon
|
appIcon
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -528,6 +539,7 @@ struct ZombieFileDetailsItem: View {
|
|||||||
if let imageView = folderImages(for: path.path) {
|
if let imageView = folderImages(for: path.path) {
|
||||||
imageView
|
imageView
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(path.path)
|
Text(path.path)
|
||||||
|
|||||||
Reference in New Issue
Block a user