mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
TreeMap updates
This commit is contained in:
@@ -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 */; };
|
||||
@@ -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>"; };
|
||||
@@ -180,6 +182,8 @@
|
||||
C77B901B2AF193A3009CC655 /* Styles.swift */,
|
||||
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
|
||||
C74FDC622BCD833D00B8960F /* Conditions.swift */,
|
||||
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
|
||||
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */,
|
||||
);
|
||||
path = Logic;
|
||||
sourceTree = "<group>";
|
||||
@@ -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>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -65,7 +65,8 @@ class AppState: ObservableObject {
|
||||
id: UUID(),
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:]
|
||||
fileIcon: [:],
|
||||
isDirectory: [:]
|
||||
)
|
||||
|
||||
updateExtensionStatus()
|
||||
@@ -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, +)
|
||||
@@ -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 {
|
||||
case arm
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -112,6 +113,7 @@ class ReversePathsSearcher {
|
||||
fileSize[path] = size.real
|
||||
fileSizeLogical[path] = size.logical
|
||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||
isDirectory[path] = path.hasDirectoryPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 totalLogicalSizeUninstallBtn: String = ""
|
||||
@State private var totalFinderSizeUninstallBtn: String = ""
|
||||
@State private var isTreeMap: Bool = false
|
||||
@State private var hoveredItem: Item?
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -170,88 +172,109 @@ struct ZombieView: View {
|
||||
.padding(.top, 0)
|
||||
}
|
||||
|
||||
// Item selection and sorting toolbar
|
||||
HStack {
|
||||
Toggle("", isOn: Binding(
|
||||
get: {
|
||||
if searchZ.isEmpty {
|
||||
// All items are selected if no filter is applied and all items are selected
|
||||
return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count
|
||||
} else {
|
||||
// All currently filtered files are selected when a filter is applied
|
||||
return Set(memoizedFiles).isSubset(of: selectedZombieItemsLocal) && selectedZombieItemsLocal.count == memoizedFiles.count
|
||||
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
|
||||
HStack {
|
||||
Toggle("", isOn: Binding(
|
||||
get: {
|
||||
if searchZ.isEmpty {
|
||||
// All items are selected if no filter is applied and all items are selected
|
||||
return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count
|
||||
} else {
|
||||
// All currently filtered files are selected when a filter is applied
|
||||
return Set(memoizedFiles).isSubset(of: selectedZombieItemsLocal) && selectedZombieItemsLocal.count == memoizedFiles.count
|
||||
}
|
||||
},
|
||||
set: { newValue in
|
||||
if newValue {
|
||||
if searchZ.isEmpty {
|
||||
// Select all files if no filter is applied
|
||||
selectedZombieItemsLocal = Set(appState.zombieFile.fileSize.keys)
|
||||
} else {
|
||||
// Select only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.formUnion(memoizedFiles)
|
||||
}
|
||||
} else {
|
||||
if searchZ.isEmpty {
|
||||
// Deselect all files if no filter is applied
|
||||
selectedZombieItemsLocal.removeAll()
|
||||
} else {
|
||||
// Deselect only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.subtract(memoizedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
updateTotalSizes()
|
||||
}
|
||||
},
|
||||
set: { newValue in
|
||||
if newValue {
|
||||
if searchZ.isEmpty {
|
||||
// Select all files if no filter is applied
|
||||
selectedZombieItemsLocal = Set(appState.zombieFile.fileSize.keys)
|
||||
} else {
|
||||
// Select only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.formUnion(memoizedFiles)
|
||||
}
|
||||
} else {
|
||||
if searchZ.isEmpty {
|
||||
// Deselect all files if no filter is applied
|
||||
selectedZombieItemsLocal.removeAll()
|
||||
} else {
|
||||
// Deselect only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.subtract(memoizedFiles)
|
||||
}
|
||||
))
|
||||
.toggleStyle(SimpleCheckboxToggleStyle())
|
||||
.help("All checkboxes")
|
||||
|
||||
|
||||
SearchBar(search: $searchZ, darker: true, glass: glass, sidebar: false)
|
||||
.padding(.horizontal)
|
||||
.onChange(of: searchZ) { newValue in
|
||||
updateMemoizedFiles(for: newValue, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
|
||||
}
|
||||
|
||||
updateTotalSizes()
|
||||
|
||||
|
||||
Button("") {
|
||||
selectedSortAlpha.toggle()
|
||||
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||
}
|
||||
))
|
||||
.toggleStyle(SimpleCheckboxToggleStyle())
|
||||
.help("All checkboxes")
|
||||
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||
|
||||
|
||||
SearchBar(search: $searchZ, darker: true, glass: glass, sidebar: false)
|
||||
.padding(.horizontal)
|
||||
.onChange(of: searchZ) { newValue in
|
||||
updateMemoizedFiles(for: newValue, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
|
||||
}
|
||||
|
||||
|
||||
Button("") {
|
||||
selectedSortAlpha.toggle()
|
||||
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||
|
||||
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical)
|
||||
|
||||
|
||||
Divider()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical)
|
||||
|
||||
ScrollView() {
|
||||
LazyVStack {
|
||||
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] {
|
||||
let iconImage = fileIcon.map(Image.init(nsImage:))
|
||||
VStack {
|
||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
||||
.padding(.vertical, 5)
|
||||
|
||||
Divider()
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
ScrollView() {
|
||||
LazyVStack {
|
||||
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], let iconImage = fileIcon.map(Image.init(nsImage:)) {
|
||||
VStack {
|
||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack() {
|
||||
|
||||
Text(hoveredItem?.name ?? "")
|
||||
|
||||
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)
|
||||
@@ -484,18 +507,6 @@ struct ZombieFileDetailsItem: View {
|
||||
Toggle("", isOn: $isSelected)
|
||||
.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 {
|
||||
appIcon
|
||||
.resizable()
|
||||
@@ -528,6 +539,7 @@ struct ZombieFileDetailsItem: View {
|
||||
if let imageView = folderImages(for: path.path) {
|
||||
imageView
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text(path.path)
|
||||
|
||||
Reference in New Issue
Block a user