Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
63b220d
feat: release 2.1.0 codebase (clean history)
Jul 30, 2026
a74f60e
chore: update paths for engine catalogs in .gitignore
Jul 30, 2026
bc77214
feat: implement private catalog support with validation and UI updates
Jul 30, 2026
1202ac5
refactor: replace URL initializations with NormalizedPath methods for…
Aug 1, 2026
38b500b
refactor: enhance path normalization across cleanup and uninstallatio…
Aug 1, 2026
8a6f044
refactor: update cleanup catalog handling and enhance path collection…
Aug 1, 2026
22af39f
refactor: implement expandable disclosure groups for confidence tiers…
Aug 1, 2026
7fc3a9a
refactor: implement per-version uninstallation flow with enhanced con…
Aug 2, 2026
9af2f1c
feat: add debug mode to advanced settings and restrict log visibility…
Aug 2, 2026
8373d38
docs: expand localization list, increment path definitions count, and…
Aug 2, 2026
ef47dc9
refactor: internationalize SettingsModel search items and update loca…
Aug 2, 2026
b91aac8
chore: update README screenshots to version 2.1 and remove obsolete a…
Aug 2, 2026
c0ef062
style: update screenshot display widths in README.md
Aug 2, 2026
f980246
Merge branch 'release' into dev/release-2.1.0
AlexTkDev Aug 2, 2026
9f2a53c
feat: add XcodeGen installation step to build workflow
Aug 2, 2026
b01c29b
ci: restrict build and test destinations to arm64 architecture in Git…
Aug 2, 2026
389af31
build: remove architecture restriction from xcodebuild test and build…
Aug 2, 2026
fde486f
refactor: update WeightABTests to use ScoreThresholds and cleanup evi…
Aug 2, 2026
c2b8533
refactor: improve path exclusion logic, update Ollama model paths, ma…
Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
43 changes: 40 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name: Build macOS App
on:
push:
branches:
- relese
- release

pull_request:
branches:
- relese
- release

jobs:
build:
Expand All @@ -19,10 +19,47 @@ jobs:
- name: Show Xcode version
run: xcodebuild -version

- name: Build
- name: Optional path catalog check
run: |
if [[ -f MacOSCleaner/Resources/engine_paths.json && -f MacOSCleaner/Resources/ui_metadata.json ]]; then
python3 scripts/validate_engine_paths.py
swift scripts/generate_cleanup_paths.swift --check
else
echo "Optional catalog SoT not present — skipping pack check"
fi

- name: Install XcodeGen
run: brew install xcodegen

- name: Check XcodeGen drift
working-directory: MacOSCleaner
run: |
xcodegen
git -C .. diff --exit-code -- MacOSCleaner/MacOSCleaner.xcodeproj

- name: Isolated test suite (pass 1)
run: |
xcodebuild \
-project MacOSCleaner/MacOSCleaner.xcodeproj \
-scheme MacOSCleaner \
-destination 'platform=macOS' \
-derivedDataPath "$RUNNER_TEMP/MacOSCleaner-Test-1" \
test

- name: Isolated test suite (pass 2)
run: |
xcodebuild \
-project MacOSCleaner/MacOSCleaner.xcodeproj \
-scheme MacOSCleaner \
-destination 'platform=macOS' \
-derivedDataPath "$RUNNER_TEMP/MacOSCleaner-Test-2" \
test

- name: Release build
run: |
xcodebuild \
-project MacOSCleaner/MacOSCleaner.xcodeproj \
-scheme MacOSCleaner \
-configuration Release \
-derivedDataPath "$RUNNER_TEMP/MacOSCleaner-Release" \
build
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,10 @@ xcuserdata/
opencode.json
.agents
problematic_apps/
implementation_plan.md

# Protected Engine Catalogs (Local only)
engine_paths.json
ui_metadata.json
MacOSCleaner/Resources/Assets.xcassets/PrivateCleanupCatalog.dataset/
MacOSCleaner/.require-private-catalog
11 changes: 11 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ downloads, or distribution for payment), a product or service whose value
derives, entirely or substantially, from the functionality of the Software or
a derivative work.

----------------------------------------------------------------------
TRADEMARK & BRANDING RESTRICTIONS
----------------------------------------------------------------------

This license does not grant permission to use the trade names, trademarks,
service marks, application icons, logo graphics, or product names of the
Licensor (including "MacOSCleaner", "MacOS Cleaner", and associated artwork),
except as strictly required for reasonable and customary use in describing
the origin of the work. Any redistribution, derivative work, or modified
version MUST remove or replace all official project branding, names, and icons.

----------------------------------------------------------------------
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Expand Down
10 changes: 7 additions & 3 deletions MacOSCleaner/App/MacOSCleanerApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ struct MacOSCleanerApp: App {
let engine = CleanupEngine(commandRunner: commandRunner)
self.cleanupViewModel = CleanupViewModel(engine: engine, journal: journal, settings: appSettings)

// Preload Launch Services cache
Task { await LSRegisterCache().warmup() }
// Preload Launch Services cache and register AppShortcuts
Task {
await LSRegisterCache().warmup()
MacOSCleanerShortcuts.updateAppShortcutParameters()
}
}

private static func installCrashHandlers() {
Expand Down Expand Up @@ -67,7 +70,7 @@ struct MacOSCleanerApp: App {
}

var body: some Scene {
WindowGroup {
WindowGroup("MacOS Cleaner") {
RootView(
cleanupViewModel: cleanupViewModel,
journal: journal,
Expand All @@ -79,6 +82,7 @@ struct MacOSCleanerApp: App {
availableUpdate = await updateChecker.checkForUpdate()
}
}
.windowResizability(.contentMinSize)
.commands {
CommandGroup(replacing: .appInfo) {
Button("about_title".localized) {
Expand Down
141 changes: 93 additions & 48 deletions MacOSCleaner/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import SwiftUI


struct RootView: View {
@State private var selectedItem: NavigationItem? = .dashboard
/// Keep the sidebar visible; hiding `.windowToolbar` on Dashboard used to
/// collapse NavigationSplitView to detail-only on macOS 26+.
@State private var columnVisibility: NavigationSplitViewVisibility = .all
@State private var selectedItem: NavigationItem = .dashboard
let cleanupViewModel: CleanupViewModel
let journal: TransactionJournal
let appSettings: AppSettings
Expand All @@ -17,41 +14,27 @@ struct RootView: View {

var body: some View {
ZStack {
NavigationSplitView(columnVisibility: $columnVisibility) {
List(selection: $selectedItem) {
ForEach(SidebarSection.all) { section in
Section {
ForEach(section.items) { item in
Label(item.localizedTitle, systemImage: item.systemImage)
.tag(item)
}
} header: {
if let titleKey = section.titleKey {
Text(titleKey.localized)
}
VStack(spacing: 0) {
topNavigationBar

contentView(for: selectedItem)
.navigationTitle("MacOS Cleaner")
.navigationSubtitle(selectedItem.localizedSubtitle ?? "")
.toolbar {
ToolbarItem(placement: .automatic) {
Spacer()
}
}
}
.listStyle(.sidebar)
.navigationTitle("app_title".localized)
.navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 280)
} detail: {
Group {
if let selectedItem {
contentView(for: selectedItem)
.modifier(ScreenNavigationTitleModifier(item: selectedItem))
} else {
Text("sidebar_select_item".localized)
.foregroundColor(.secondary)
}
}
.scrollEdgeEffectStyle(.hard, for: .top)
.frame(minWidth: 800, minHeight: 600)
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Recreate screen content so every `.localized` string / glass layer
// matches the selected language (prevents stale RU labels in EN/FR/…).
.id(appSettings.language)
}
.navigationSplitViewStyle(.balanced)

GlassOverlayView(manager: GlassOverlayManager.shared)
}
.frame(minWidth: 1024, minHeight: 680)
.environment(\.locale, appSettings.language.locale)
.sheet(isPresented: $permissionsManager.showGuidance) {
PermissionsView(permissionsManager: permissionsManager)
}
Expand All @@ -64,6 +47,81 @@ struct RootView: View {
}
}

// MARK: - Navigation Groups
private let navGroups: [[NavigationItem]] = [
[.dashboard],
[.cleanup, .diskSpace, .duplicates, .uninstaller],
[.processes, .startupServices],
[.settings]
]

private var topNavigationBar: some View {
HStack(spacing: 0) {
ForEach(navGroups.indices, id: \.self) { groupIndex in
let group = navGroups[groupIndex]

HStack(spacing: 2) {
ForEach(group, id: \.self) { item in
navButton(for: item)
}
}

if groupIndex < navGroups.count - 1 {
Divider()
.frame(height: 18)
.opacity(0.4)
.padding(.horizontal, 4)
}
}
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.glassEffect(Glass.regular, in: RoundedRectangle(cornerRadius: 12))
.id(appSettings.language)
.frame(maxWidth: .infinity)
.padding(.horizontal, 12)
.padding(.top, 4)
.padding(.bottom, 6)
}

@ViewBuilder
private func navButton(for item: NavigationItem) -> some View {
let isSelected = selectedItem == item
Button {
withAnimation(.spring(response: 0.28, dampingFraction: 0.8)) {
selectedItem = item
}
} label: {
HStack(spacing: 6) {
Image(systemName: item.systemImage)
.font(.system(size: 15, weight: .medium))
.frame(width: 22, height: 22)
// Compact on all locales: label only for the selected item.
if isSelected {
Text(item.localizedTitle)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
}
.padding(.horizontal, isSelected ? 10 : 9)
.padding(.vertical, 6)
.frame(minWidth: isSelected ? nil : 40, minHeight: 32)
.contentShape(Rectangle())
.foregroundStyle(isSelected ? Color.white : Color.primary.opacity(0.6))
.background {
if isSelected {
Capsule()
.fill(Color.accentColor)
.glassEffect(Glass.regular.tint(Color.accentColor).interactive(), in: Capsule())
}
}
}
.buttonStyle(.plain)
.help(item.localizedTitle)
}


@ViewBuilder
private func contentView(for item: NavigationItem) -> some View {
switch item {
Expand All @@ -73,6 +131,8 @@ struct RootView: View {
CleanupView(viewModel: cleanupViewModel)
case .diskSpace:
DiskAnalyzerView(settings: appSettings)
case .duplicates:
DuplicatesView()
case .processes:
ProcessesView(settings: appSettings)
case .startupServices:
Expand All @@ -94,22 +154,7 @@ struct RootView: View {
}
}

private struct ScreenNavigationTitleModifier: ViewModifier {
let item: NavigationItem

func body(content: Content) -> some View {
if item == .dashboard {
// Keep the window toolbar (sidebar toggle lives there). Only drop the
// detail title so Dashboard stays chrome-light without collapsing the split.
content
.navigationTitle("")
.toolbar(removing: .title)
} else {
content
.navigationTitle(item.localizedTitle)
}
}
}

#Preview {
let journal = TransactionJournal()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ extension CleanupCategory {
labels.insert("Orphaned files")
case .largeFiles:
labels.insert("Large files")
labels.insert("Large Files")
case .dynamicCacheDiscovery:
labels.formUnion(["Dynamic cache discovery", "Auto-cleanable", "Review manually"])
case .timeMachineSnapshots:
Expand Down Expand Up @@ -127,6 +128,10 @@ extension CleanupCategory {
labels.insert("Garmin / Fitbit")
case .oldBackups:
labels.insert("Old Backups")
case .aiModels:
labels.insert("AI Models")
case .installerPackages:
labels.insert("Installer Packages")
case .dnsFlush:
labels.insert("DNS Cache")
case .fontCache:
Expand Down
Loading
Loading