Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions PureMac/Models/Models.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,100 @@
import SwiftUI

// MARK: - Filter & Sort

struct FilterConfig {
var searchText: String = ""
var sizeFilter: SizeFilter = .all
var dateFilter: DateFilter = .all
var sortBy: SortBy = .size
var sortOrder: SortOrder = .descending

func matches(_ item: CleanableItem) -> Bool {
// Search text
if !searchText.isEmpty {
if !item.name.localizedCaseInsensitiveContains(searchText) &&
!item.path.localizedCaseInsensitiveContains(searchText) {
return false
}
}

// Size filter
if !sizeFilter.matches(item.size) {
return false
}

// Date filter
if let date = item.lastModified {
if !dateFilter.matches(date) {
return false
}
} else if dateFilter != .all {
return false
}

return true
}
}

enum SizeFilter: String, CaseIterable, Identifiable {
case all = "All Sizes"
case small = "Small (< 10 MB)"
case medium = "Medium (10 - 100 MB)"
case large = "Large (> 100 MB)"

var id: String { rawValue }

func matches(_ size: Int64) -> Bool {
switch self {
case .all: return true
case .small: return size < 10 * 1024 * 1024
case .medium: return size >= 10 * 1024 * 1024 && size <= 100 * 1024 * 1024
case .large: return size > 100 * 1024 * 1024
}
}
}

enum DateFilter: String, CaseIterable, Identifiable {
case all = "All Time"
case today = "Today"
case week = "Last 7 Days"
case month = "Last 30 Days"
case year = "Last Year"

var id: String { rawValue }

func matches(_ date: Date) -> Bool {
let now = Date()
let calendar = Calendar.current
switch self {
case .all: return true
case .today: return calendar.isDateInToday(date)
case .week:
let weekAgo = calendar.date(byAdding: .day, value: -7, to: now)!
return date >= weekAgo
case .month:
let monthAgo = calendar.date(byAdding: .month, value: -1, to: now)!
return date >= monthAgo
case .year:
let yearAgo = calendar.date(byAdding: .year, value: -1, to: now)!
return date >= yearAgo
}
}
}

enum SortBy: String, CaseIterable, Identifiable {
case size = "Size"
case date = "Date"
case name = "Name"

var id: String { rawValue }
}

enum SortOrder {
case ascending
case descending
}

// MARK: - Cleaning Category

enum CleaningCategory: String, CaseIterable, Identifiable, Codable {
Expand Down
4 changes: 2 additions & 2 deletions PureMac/Services/ScanEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ actor ScanEngine {
size: size,
category: .xcodeJunk,
isSelected: true,
lastModified: nil
lastModified: fileModDate(path: path)
))
}
}
Expand Down Expand Up @@ -346,7 +346,7 @@ actor ScanEngine {
size: size,
category: .brewCache,
isSelected: true,
lastModified: nil
lastModified: fileModDate(path: path)
))
}
}
Expand Down
12 changes: 12 additions & 0 deletions PureMac/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ class AppViewModel: ObservableObject {
}
}

func selectItems(_ items: [CleanableItem]) {
for item in items {
deselectedItems.remove(item.id)
}
}

func deselectItems(_ items: [CleanableItem]) {
for item in items {
deselectedItems.insert(item.id)
}
}

func selectedSizeInCategory(_ category: CleaningCategory) -> Int64 {
guard let result = categoryResults[category] else { return 0 }
return result.items.filter { isItemSelected($0) }.reduce(0) { $0 + $1.size }
Expand Down
139 changes: 119 additions & 20 deletions PureMac/Views/CategoryDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,32 @@ struct CategoryDetailView: View {
@EnvironmentObject var vm: AppViewModel
let category: CleaningCategory

@State private var sortDescending: Bool = true
@State private var filter = FilterConfig()

var result: CategoryResult? {
vm.categoryResults[category]
}

var filteredItems: [CleanableItem] {
guard let items = result?.items else { return [] }

let filtered = items.filter { filter.matches($0) }

return filtered.sorted { a, b in
let ascending = filter.sortOrder == .ascending
switch filter.sortBy {
case .size:
return ascending ? a.size < b.size : a.size > b.size
case .date:
let d1 = a.lastModified ?? Date.distantPast
let d2 = b.lastModified ?? Date.distantPast
return ascending ? d1 < d2 : d1 > d2
case .name:
return ascending ? a.name < b.name : a.name > b.name
}
}
}

var body: some View {
VStack(spacing: 0) {
// Category header
Expand All @@ -23,6 +43,11 @@ struct CategoryDetailView: View {
if result.items.isEmpty {
emptyState
} else {
// Search & Filter Bar
filterBar
.padding(.horizontal, 32)
.padding(.bottom, 8)

// File list
fileList(result)
}
Expand Down Expand Up @@ -80,14 +105,102 @@ struct CategoryDetailView: View {
}
}

// MARK: - Filter Bar

private var filterBar: some View {
HStack(spacing: 12) {
// Search field
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.pmTextMuted)
.font(.system(size: 12))
TextField("Search files...", text: $filter.searchText)
.textFieldStyle(.plain)
.font(.pmBody)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.pmCard)
.cornerRadius(8)
.frame(maxWidth: .infinity)

// Size Filter
filterMenu(title: filter.sizeFilter == .all ? "Size" : filter.sizeFilter.rawValue, icon: "line.3.horizontal.decrease.circle") {
Picker("Size", selection: $filter.sizeFilter) {
ForEach(SizeFilter.allCases) { f in
Text(f.rawValue).tag(f)
}
}
}

// Date Filter
filterMenu(title: filter.dateFilter == .all ? "Date" : filter.dateFilter.rawValue, icon: "calendar") {
Picker("Date", selection: $filter.dateFilter) {
ForEach(DateFilter.allCases) { f in
Text(f.rawValue).tag(f)
}
}
}

// Sort
Menu {
Picker("Sort By", selection: $filter.sortBy) {
ForEach(SortBy.allCases) { s in
Text(s.rawValue).tag(s)
}
}
Divider()
Button(action: { filter.sortOrder = .ascending }) {
HStack {
Text("Ascending")
if filter.sortOrder == .ascending { Image(systemName: "checkmark") }
}
}
Button(action: { filter.sortOrder = .descending }) {
HStack {
Text("Descending")
if filter.sortOrder == .descending { Image(systemName: "checkmark") }
}
}
} label: {
Image(systemName: "arrow.up.arrow.down")
.font(.system(size: 12))
.foregroundColor(.pmTextSecondary)
.padding(8)
.background(Color.pmCard)
.cornerRadius(8)
}
.menuStyle(.plain)
}
}

private func filterMenu<Content: View>(title: String, icon: String, @ViewBuilder content: () -> Content) -> some View {
Menu {
content()
} label: {
HStack(spacing: 4) {
Image(systemName: icon)
Text(title)
}
.font(.system(size: 11, weight: .medium))
.foregroundColor(.pmTextSecondary)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.pmCard)
.cornerRadius(8)
}
.menuStyle(.plain)
}

// MARK: - File List

private func fileList(_ result: CategoryResult) -> some View {
VStack(spacing: 0) {
// Select all / Deselect all bar
HStack(spacing: 16) {
let selectedCount = vm.selectedCountInCategory(category)
let totalCount = result.itemCount
let items = filteredItems
let selectedCount = items.filter { vm.isItemSelected($0) }.count
let totalCount = items.count

Text("\(selectedCount) of \(totalCount) selected")
.font(.system(size: 11, weight: .medium, design: .rounded))
Expand All @@ -96,7 +209,7 @@ struct CategoryDetailView: View {
Spacer()

Button("Select All") {
vm.selectAllInCategory(category)
vm.selectItems(items)
}
.font(.system(size: 11, weight: .medium))
.foregroundColor(.pmAccentLight)
Expand All @@ -107,19 +220,11 @@ struct CategoryDetailView: View {
.font(.system(size: 11))

Button("Deselect All") {
vm.deselectAllInCategory(category)
vm.deselectItems(items)
}
.font(.system(size: 11, weight: .medium))
.foregroundColor(.pmAccentLight)
.buttonStyle(.plain)

Button(action: { sortDescending.toggle() }) {
Image(systemName: "arrow.up.arrow.down")
.font(.system(size: 14))
.foregroundColor(.pmAccentLight)
}
.buttonStyle(.plain)
.help(sortDescending ? "Sorted: Largest First" : "Sorted: Smallest First")
}
.padding(.horizontal, 48)
.padding(.vertical, 8)
Expand All @@ -130,7 +235,7 @@ struct CategoryDetailView: View {

ScrollView {
LazyVStack(spacing: 4) {
ForEach(sortedItems(result.items)) { item in
ForEach(filteredItems) { item in
FileRow(item: item, color: category.color)
}
}
Expand Down Expand Up @@ -184,12 +289,6 @@ struct CategoryDetailView: View {
}
}

// MARK: - Sort

private func sortedItems(_ items: [CleanableItem]) -> [CleanableItem] {
items.sorted { sortDescending ? $0.size > $1.size : $0.size < $1.size }
}

// MARK: - Action Bar

private var categoryActionBar: some View {
Expand Down