Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
CODE_SIGN_IDENTITY=""

release-artifact:
name: Build unsigned release zip
name: Build unsigned release package
runs-on: macos-26
timeout-minutes: 30
needs: test
Expand All @@ -73,6 +73,7 @@ jobs:
with:
name: CleanMac-unsigned-${{ github.sha }}
path: |
dist/*.dmg
dist/*.zip
dist/*.sha256
if-no-files-found: error
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ jobs:
if gh release view "$tag_name" >/dev/null 2>&1; then
echo "Release $tag_name already exists; replacing matching assets."
gh release upload "$tag_name" \
dist/*.dmg \
dist/*.zip \
dist/*.sha256 \
--clobber
else
gh release create "$tag_name" \
dist/*.dmg \
dist/*.zip \
dist/*.sha256 \
--title "CleanMac $tag_name" \
--notes "Unsigned CleanMac build. Download the zip and verify it with the attached sha256 file."
--notes "Unsigned CleanMac build. Open the DMG and drag CleanMac to Applications, or use the ZIP archive. Verify either download with its attached SHA-256 file."
fi
8 changes: 4 additions & 4 deletions CleanMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@
CODE_SIGN_ENTITLEMENTS = CleanMac/CleanMac.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand All @@ -275,7 +275,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.4.0;
MARKETING_VERSION = 0.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.codex.cleanmac;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -296,7 +296,7 @@
CODE_SIGN_ENTITLEMENTS = CleanMac/CleanMac.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand All @@ -310,7 +310,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.4.0;
MARKETING_VERSION = 0.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.codex.cleanmac;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down
19 changes: 17 additions & 2 deletions CleanMac/Models/CleanMacModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum CleanMacSection: String, CaseIterable, Identifiable {
case scan
case results
case diskAnalysis
case systemMaintenance
case duplicates
case shredder
case applications
Expand All @@ -19,6 +20,7 @@ enum CleanMacSection: String, CaseIterable, Identifiable {
case .scan: L.t("section.scan")
case .results: L.t("section.results")
case .diskAnalysis: L.t("section.diskAnalysis")
case .systemMaintenance: L.t("section.system")
case .duplicates: L.t("section.duplicates")
case .shredder: L.t("section.shredder")
case .applications: L.t("section.applications")
Expand All @@ -32,9 +34,10 @@ enum CleanMacSection: String, CaseIterable, Identifiable {
case .scan: "magnifyingglass"
case .results: "checklist"
case .diskAnalysis: "chart.pie.fill"
case .systemMaintenance: "memorychip"
case .duplicates: "square.on.square"
case .shredder: "terminal.fill"
case .applications: "app.badge.checkmark"
case .shredder: "scissors"
case .applications: "square.stack.3d.up.fill"
case .settings: "gearshape"
}
}
Expand Down Expand Up @@ -121,6 +124,7 @@ extension CleanupScanReason {
case .developerPackageCache: L.t("results.reason.developerPackageCache.title")
case .developerIDECache: L.t("results.reason.developerIDECache.title")
case .developerAITemporaryFile: L.t("results.reason.developerAITemporaryFile.title")
case .staleCodexRuntimeInstaller: L.t("results.reason.staleCodexRuntimeInstaller.title")
case .staleLog: L.t("results.reason.staleLog.title")
case .rotatedLog: L.t("results.reason.rotatedLog.title")
case .staleTemporary: L.t("results.reason.staleTemporary.title")
Expand All @@ -145,6 +149,7 @@ extension CleanupScanReason {
case .developerPackageCache: L.t("results.reason.developerPackageCache.detail")
case .developerIDECache: L.t("results.reason.developerIDECache.detail")
case .developerAITemporaryFile: L.t("results.reason.developerAITemporaryFile.detail")
case .staleCodexRuntimeInstaller: L.t("results.reason.staleCodexRuntimeInstaller.detail")
case .staleLog: L.t("results.reason.staleLog.detail")
case .rotatedLog: L.t("results.reason.rotatedLog.detail")
case .staleTemporary: L.t("results.reason.staleTemporary.detail")
Expand All @@ -169,6 +174,7 @@ extension CleanupScanReason {
case .developerPackageCache: "shippingbox.and.arrow.backward"
case .developerIDECache: "curlybraces.square"
case .developerAITemporaryFile: "sparkles.rectangle.stack"
case .staleCodexRuntimeInstaller: "shippingbox.and.arrow.backward.fill"
case .staleLog: "doc.text.magnifyingglass"
case .rotatedLog: "arrow.triangle.2.circlepath"
case .staleTemporary: "clock.arrow.circlepath"
Expand Down Expand Up @@ -365,6 +371,15 @@ enum CleanMacCatalog {
risk: .safe,
isDefaultSelected: true
),
CleanupArea(
category: .staleCodexRuntimeInstallers,
title: L.t("area.staleCodexRuntimes.title"),
detail: L.t("area.staleCodexRuntimes.detail"),
pathHint: "~/.cache/codex-runtimes/codex-runtime-install-*",
systemImage: "shippingbox.and.arrow.backward.fill",
risk: .review,
isDefaultSelected: true
),
CleanupArea(
category: .logs,
title: L.t("area.logs.title"),
Expand Down
12 changes: 11 additions & 1 deletion CleanMac/Support/CleanMacAutoScanScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ final class CleanMacAutoScanScheduler {
private let defaults: UserDefaults
private var timer: Timer?
private var scanTask: Task<Void, Never>?
private var didResetProcessState = false

init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}

func start() {
defaults.set(false, forKey: CleanMacPreferenceKeys.scanInProgress)
if !didResetProcessState {
// These flags coordinate windows only for the lifetime of this process.
// Reset stale values once at launch, never when restarting the scheduler.
defaults.set(false, forKey: CleanMacPreferenceKeys.scanInProgress)
defaults.set(false, forKey: CleanMacPreferenceKeys.fileOperationInProgress)
didResetProcessState = true
}
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
self?.evaluateSchedule()
Expand All @@ -37,6 +44,9 @@ final class CleanMacAutoScanScheduler {
guard !defaults.bool(forKey: CleanMacPreferenceKeys.scanInProgress) else {
return
}
guard !defaults.bool(forKey: CleanMacPreferenceKeys.fileOperationInProgress) else {
return
}

guard let dueRun = CleanMacScanSchedule.dueRun(defaults: defaults, now: now, calendar: calendar) else {
return
Expand Down
20 changes: 10 additions & 10 deletions CleanMac/Support/CleanMacFormatters.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import Foundation

enum CleanMacFormatters {
private static let byteFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]
formatter.countStyle = .file
formatter.includesActualByteCount = false
return formatter
}()

private static let relativeFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .short
Expand All @@ -26,17 +18,25 @@ enum CleanMacFormatters {
guard value > 0 else {
return L.t("size.zero")
}
return byteFormatter.string(fromByteCount: value)
return ByteCountFormatStyle(
style: .file,
allowedUnits: [.kb, .mb, .gb, .tb],
spellsOutZero: false,
includesActualByteCount: false,
locale: CleanMacLanguage.current.locale
).format(value)
}

static func relativeDate(_ date: Date?) -> String {
guard let date else {
return L.t("date.unknown")
}
relativeFormatter.locale = CleanMacLanguage.current.locale
return relativeFormatter.localizedString(for: date, relativeTo: Date())
}

static func time(_ date: Date) -> String {
timeFormatter.string(from: date)
timeFormatter.locale = CleanMacLanguage.current.locale
return timeFormatter.string(from: date)
}
}
65 changes: 65 additions & 0 deletions CleanMac/Support/CleanMacMotion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import SwiftUI

enum CleanMacMotion {
static func feedback(reduceMotion: Bool) -> Animation? {
reduceMotion ? nil : .easeOut(duration: 0.16)
}

static func progress(reduceMotion: Bool) -> Animation? {
reduceMotion ? nil : .easeOut(duration: 0.24)
}
}

struct CleanMacMotionPhase {
let isEnabled: Bool
let isAnimating: Bool
}

/// Owns decorative loops so an inactive scene or Reduce Motion actually removes
/// their animated render tree. Progress values remain owned by the caller.
struct CleanMacContinuousMotion<Content: View>: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.scenePhase) private var scenePhase

@ViewBuilder let content: (CleanMacMotionPhase) -> Content

private var isEnabled: Bool {
!reduceMotion && scenePhase == .active
}

var body: some View {
CleanMacContinuousMotionContent(isEnabled: isEnabled, content: content)
.id(isEnabled)
}
}

private struct CleanMacContinuousMotionContent<Content: View>: View {
let isEnabled: Bool
let content: (CleanMacMotionPhase) -> Content

@State private var isAnimating = false

var body: some View {
content(CleanMacMotionPhase(isEnabled: isEnabled, isAnimating: isAnimating))
.transaction { transaction in
if !isEnabled {
transaction.animation = nil
transaction.disablesAnimations = true
}
}
.task {
guard isEnabled else { return }
// Give the new subtree its initial pose before starting a loop.
await Task.yield()
guard !Task.isCancelled else { return }
isAnimating = true
}
.onDisappear {
var transaction = Transaction(animation: nil)
transaction.disablesAnimations = true
withTransaction(transaction) {
isAnimating = false
}
}
}
}
15 changes: 14 additions & 1 deletion CleanMac/Support/CleanMacPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Foundation
enum CleanMacPreferenceKeys {
static let onboardingCompleted = "CleanMac.onboardingCompleted"
static let selectedAreaIDs = "CleanMac.selectedAreaIDs"
static let selectedAreaSchemaVersion = "CleanMac.selectedAreaSchemaVersion"
static let lastScanItemCount = "CleanMac.lastScanItemCount"
static let lastScanBytes = "CleanMac.lastScanBytes"
static let lastScanTimestamp = "CleanMac.lastScanTimestamp"
Expand All @@ -17,6 +18,7 @@ enum CleanMacPreferenceKeys {
static let lowDiskSpaceLastNotificationTimestamp = "CleanMac.lowDiskSpaceLastNotificationTimestamp"
static let requestedSection = "CleanMac.requestedSection"
static let scanInProgress = "CleanMac.scanInProgress"
static let fileOperationInProgress = "CleanMac.fileOperationInProgress"
}

enum CleanMacScanSource: String {
Expand Down Expand Up @@ -57,6 +59,8 @@ enum CleanMacAutoScanFrequency: String, CaseIterable, Identifiable {
}

enum CleanMacScanPreferences {
private static let currentAreaSchemaVersion = 1

static var defaultSelectedAreaIDs: Set<String> {
Set(CleanMacCatalog.cleanupAreas.filter(\.isDefaultSelected).map(\.id))
}
Expand All @@ -68,16 +72,24 @@ enum CleanMacScanPreferences {
static func selectedAreaIDs(defaults: UserDefaults = .standard) -> Set<String> {
let rawValue = defaults.string(forKey: CleanMacPreferenceKeys.selectedAreaIDs)
guard let rawValue else {
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
return defaultSelectedAreaIDs
}
guard !rawValue.isEmpty else {
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
return []
}

let validIDs = Set(CleanMacCatalog.cleanupAreas.map(\.id))
let decodedIDs = Set(rawValue.split(separator: ",").map(String.init))
var decodedIDs = Set(rawValue.split(separator: ",").map(String.init))
.intersection(validIDs)

if defaults.integer(forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion) < currentAreaSchemaVersion {
decodedIDs.insert(CleanupCategory.staleCodexRuntimeInstallers.rawValue)
defaults.set(encodeAreaIDs(decodedIDs), forKey: CleanMacPreferenceKeys.selectedAreaIDs)
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
}

return decodedIDs.isEmpty ? defaultSelectedAreaIDs : decodedIDs
}

Expand All @@ -90,6 +102,7 @@ enum CleanMacScanPreferences {

static func storeSelectedAreaIDs(_ areaIDs: Set<String>, defaults: UserDefaults = .standard) {
defaults.set(encodeAreaIDs(areaIDs), forKey: CleanMacPreferenceKeys.selectedAreaIDs)
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
}

static func storeLastScan(_ report: CleanupScanReport, source: CleanMacScanSource, defaults: UserDefaults = .standard) {
Expand Down
Loading
Loading