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
10 changes: 8 additions & 2 deletions MacOSCleaner/App/MacOSCleanerApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct MacOSCleanerApp: App {
private let cleanupViewModel: CleanupViewModel
private let appSettings = AppSettings()
private let permissionsManager = PermissionsManager()
private let updateChecker = UpdateChecker()
@State private var availableUpdate: String? = nil

init() {
Self.installCrashHandlers()
Expand Down Expand Up @@ -58,8 +60,12 @@ struct MacOSCleanerApp: App {
cleanupViewModel: cleanupViewModel,
journal: journal,
appSettings: appSettings,
permissionsManager: permissionsManager
permissionsManager: permissionsManager,
availableUpdate: $availableUpdate
)
.task {
availableUpdate = await updateChecker.checkForUpdate()
}
}
.commands {
CommandGroup(replacing: .appInfo) {
Expand All @@ -70,7 +76,7 @@ struct MacOSCleanerApp: App {
}

Window("about_title".localized, id: "about") {
AboutView()
AboutView(availableUpdate: availableUpdate)
}
.windowResizability(.contentSize)
.defaultPosition(.center)
Expand Down
7 changes: 5 additions & 2 deletions MacOSCleaner/App/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct RootView: View {
let journal: TransactionJournal
let appSettings: AppSettings
@Bindable var permissionsManager: PermissionsManager
@Binding var availableUpdate: String?

var body: some View {
NavigationSplitView {
Expand Down Expand Up @@ -60,7 +61,8 @@ struct RootView: View {
Task {
try? await journal.clear()
}
}
},
availableUpdate: $availableUpdate
)
}
}
Expand All @@ -78,6 +80,7 @@ struct RootView: View {
),
journal: journal,
appSettings: settings,
permissionsManager: PermissionsManager()
permissionsManager: PermissionsManager(),
availableUpdate: .constant(nil)
)
}
37 changes: 36 additions & 1 deletion MacOSCleaner/Features/About/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
struct AboutView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
var availableUpdate: String? = nil

var body: some View {
VStack(spacing: 0) {
Expand Down Expand Up @@ -46,6 +47,10 @@ struct AboutView: View {

private var content: some View {
VStack(spacing: 16) {
if let update = availableUpdate {
updateBanner(version: update)
}

developerCard
linksCard

Expand All @@ -60,6 +65,36 @@ struct AboutView: View {
.padding(20)
}

private func updateBanner(version: String) -> some View {
Button {
NSWorkspace.shared.open(UpdateChecker.releasesURL)
} label: {
HStack(spacing: 12) {
Image(systemName: "bell.badge.fill")
.font(.title2)
.foregroundStyle(.white)
.symbolRenderingMode(.multicolor)

VStack(alignment: .leading, spacing: 2) {
Text(String(format: "update.available".localized, version))
.font(.headline)
.foregroundStyle(.white)
Text("update.download".localized + " →")
.font(.subheadline)
.foregroundStyle(.white.opacity(0.8))
}
Spacer()
}
.padding(14)
.background(
LinearGradient(colors: [.blue, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing)
)
.clipShape(RoundedRectangle(cornerRadius: 10))
.shadow(color: .blue.opacity(0.3), radius: 5, y: 2)
}
.buttonStyle(.plain)
}

private var developerCard: some View {
HStack(spacing: 12) {
Image(systemName: "person.circle.fill")
Expand Down Expand Up @@ -97,5 +132,5 @@ struct AboutView: View {
}

#Preview {
AboutView()
AboutView(availableUpdate: "1.2.0")
}
30 changes: 29 additions & 1 deletion MacOSCleaner/Features/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ public enum RefreshInterval: String, CaseIterable, Identifiable {
}
}

public enum ScanMode: String, CaseIterable, Identifiable, Sendable {
case safe
case balanced

public var id: String { rawValue }

public var localizedName: String {
switch self {
case .safe: return "scan_mode.safe".localized
case .balanced: return "scan_mode.balanced".localized
}
}

public var localizedDescription: String {
switch self {
case .safe: return "scan_mode.safe.desc".localized
case .balanced: return "scan_mode.balanced.desc".localized
}
}
}

public enum ProcessSortOption: String, CaseIterable, Identifiable {
case cpu = "CPU"
case memory = "Memory"
Expand Down Expand Up @@ -105,6 +126,7 @@ public final class AppSettings {
static let emptyTrashImmediately = "settings_emptyTrashImmediately"
static let processRefreshInterval = "settings_processRefreshInterval"
static let processSortOption = "settings_processSortOption"
static let uninstallerScanMode = "settings_uninstallerScanMode"
}

// MARK: - General
Expand Down Expand Up @@ -152,6 +174,10 @@ public final class AppSettings {
didSet { UserDefaults.standard.set(bypassTrashOnUninstall, forKey: Keys.bypassTrashOnUninstall) }
}

public var uninstallerScanMode: ScanMode {
didSet { UserDefaults.standard.set(uninstallerScanMode.rawValue, forKey: Keys.uninstallerScanMode) }
}

// MARK: - Advanced

public var showRelatedFiles: Bool {
Expand Down Expand Up @@ -193,6 +219,7 @@ public final class AppSettings {
self.emptyTrashImmediately = defaults.bool(forKey: Keys.emptyTrashImmediately)
self.processRefreshInterval = RefreshInterval(rawValue: defaults.string(forKey: Keys.processRefreshInterval) ?? "") ?? .manual
self.processSortOption = ProcessSortOption(rawValue: defaults.string(forKey: Keys.processSortOption) ?? "") ?? .cpu
self.uninstallerScanMode = ScanMode(rawValue: defaults.string(forKey: Keys.uninstallerScanMode) ?? "") ?? .balanced

LanguageManager.shared.setLanguage(lang)

Expand All @@ -209,7 +236,7 @@ public final class AppSettings {
Keys.language, Keys.theme, Keys.showNotifications, Keys.showTooltips,
Keys.autoScanOnStartup, Keys.emptyTrashDuringCleanup, Keys.bypassTrashOnUninstall,
Keys.showRelatedFiles, Keys.emptyTrashImmediately,
Keys.processRefreshInterval, Keys.processSortOption
Keys.processRefreshInterval, Keys.processSortOption, Keys.uninstallerScanMode
]
for key in allKeys {
defaults.removeObject(forKey: key)
Expand All @@ -226,6 +253,7 @@ public final class AppSettings {
emptyTrashImmediately = false
processRefreshInterval = .manual
processSortOption = .cpu
uninstallerScanMode = .balanced

LanguageManager.shared.setLanguage(.english)
}
Expand Down
91 changes: 89 additions & 2 deletions MacOSCleaner/Features/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ struct SettingsView: View {
@Bindable var settings: AppSettings
let permissionsManager: PermissionsManager
let onForget: () -> Void
@Binding var availableUpdate: String?

@State private var showResetConfirmation = false
@State private var trashManager = TrashManager()
@State private var notificationStatus: UNAuthorizationStatus = .notDetermined
@State private var isCheckingForUpdates = false

var body: some View {
Form {
permissionsSection
generalSection
processesSection
uninstallerSection
startupSection
trashDeletionSection
advancedSection
Expand Down Expand Up @@ -112,6 +116,34 @@ struct SettingsView: View {

Toggle("settings_auto_scan".localized, isOn: $settings.autoScanOnStartup)
.tooltip("settings_tooltip_auto_scan".localized, enabled: settings.showTooltips)

HStack {
Text("update.check".localized)
Spacer()
if isCheckingForUpdates {
ProgressView().controlSize(.small)
Text("update.checking".localized).foregroundStyle(.secondary)
} else if let version = availableUpdate {
Button {
NSWorkspace.shared.open(UpdateChecker.releasesURL)
} label: {
Text(String(format: "update.available".localized, version))
}
.buttonStyle(.link)
} else {
Text("update.up_to_date".localized).foregroundStyle(.secondary)
Button {
Task {
isCheckingForUpdates = true
availableUpdate = await UpdateChecker().checkForUpdate()
isCheckingForUpdates = false
}
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.plain)
}
}
} header: {
Label("settings_general".localized, systemImage: "gearshape")
}
Expand Down Expand Up @@ -139,6 +171,56 @@ struct SettingsView: View {
}
}

// MARK: - Uninstaller

private var uninstallerSection: some View {
Section {
VStack(alignment: .leading, spacing: 10) {
Picker("scan_mode".localized, selection: $settings.uninstallerScanMode) {
ForEach(ScanMode.allCases) { mode in
Text(mode.localizedName).tag(mode)
}
}
.pickerStyle(.segmented)

ForEach(ScanMode.allCases) { mode in
HStack(alignment: .top, spacing: 8) {
Image(systemName: settings.uninstallerScanMode == mode
? "checkmark.circle.fill" : "circle")
.foregroundStyle(settings.uninstallerScanMode == mode
? (mode == .safe ? Color.blue : Color.green)
: Color.secondary)
.font(.caption)
.padding(.top, 2)

VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(mode.localizedName)
.font(.callout)
.fontWeight(.medium)
if mode == .balanced {
Text("scan_mode.balanced.default".localized)
.font(.caption2)
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(Color.green.opacity(0.15))
.foregroundStyle(.green)
.clipShape(Capsule())
}
}
Text(mode.localizedDescription)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
} header: {
Label("settings_uninstaller".localized, systemImage: "trash.slash")
}
}

// MARK: - Startup

private var startupSection: some View {
Expand Down Expand Up @@ -291,6 +373,11 @@ private extension View {
}

#Preview {
SettingsView(settings: AppSettings(), permissionsManager: PermissionsManager(), onForget: {})
.frame(width: 700, height: 700)
SettingsView(
settings: AppSettings(),
permissionsManager: PermissionsManager(),
onForget: {},
availableUpdate: .constant("1.5.0")
)
.frame(width: 700, height: 700)
}
Loading
Loading