Files
wehub-resource-sync 869b84f27c
Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:29:30 +08:00

368 lines
14 KiB
Swift

import AppKit
import SwiftUI
import ServiceManagement
struct SettingsView: View {
var body: some View {
TabView {
GeneralSettingsView()
.tabItem { Label("General", systemImage: "gear") }
CleaningSettingsView()
.tabItem { Label("Cleaning", systemImage: "trash") }
ScheduleSettingsView()
.tabItem { Label("Schedule", systemImage: "clock") }
AboutSettingsView()
.tabItem { Label("About", systemImage: "info.circle") }
}
.frame(width: 480, height: 430)
}
}
// MARK: - General
enum SearchSensitivity: String, CaseIterable, Identifiable, Codable {
case strict = "Strict"
case enhanced = "Enhanced"
case deep = "Deep"
var id: String { rawValue }
var description: String {
switch self {
case .strict: return "Exact bundle ID and name matches only. Safest option."
case .enhanced: return "Includes partial name matching and bundle ID components."
case .deep: return "Includes company name, entitlements, and team identifier matching."
}
}
}
struct GeneralSettingsView: View {
@AppStorage("settings.general.launchAtLogin") private var launchAtLogin = false
@AppStorage("settings.general.searchSensitivity") private var sensitivity: SearchSensitivity = .enhanced
@AppStorage("settings.general.confirmBeforeDelete") private var confirmBeforeDelete = true
@AppStorage("settings.general.menuBarMonitor") private var menuBarMonitor = false
@AppStorage(Haptics.soundEffectsKey) private var soundEffects = true
@AppStorage(AppLanguage.preferenceKey) private var appLanguageRaw = AppLanguage.current.rawValue
@State private var languageNeedsRelaunch = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
Form {
Section("Startup") {
Toggle("Launch PureMac at login", isOn: launchAtLoginBinding)
}
Section("App Scanning") {
Picker("Search sensitivity", selection: $sensitivity) {
ForEach(SearchSensitivity.allCases) { level in
VStack(alignment: .leading) {
Text(LocalizedStringKey(level.rawValue))
Text(LocalizedStringKey(level.description))
.font(.caption)
.foregroundStyle(.secondary)
}
.tag(level)
}
}
.pickerStyle(.radioGroup)
}
Section("Language") {
Picker("Language", selection: appLanguageBinding) {
ForEach(AppLanguage.allCases) { language in
Text(LocalizedStringKey(language.displayName)).tag(language)
}
}
if languageNeedsRelaunch {
Text("Restart PureMac to apply the selected language.")
.font(.caption)
.foregroundStyle(.secondary)
.transition(.opacity.combined(with: .move(edge: .top)))
Button("Relaunch Now") {
relaunchApp()
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
Section("System Monitor") {
Toggle("Show system monitor in menu bar", isOn: menuBarMonitorBinding)
Text("Live CPU, memory, and disk meters in the menu bar. PureMac keeps running in the background while this is on.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Sound") {
Toggle("Play sound effects", isOn: $soundEffects)
}
Section("Safety") {
Toggle("Confirm before deleting files", isOn: $confirmBeforeDelete)
}
}
.formStyle(.grouped)
.animation(reduceMotion ? nil : .spring(response: 0.35, dampingFraction: 0.85),
value: languageNeedsRelaunch)
}
private var menuBarMonitorBinding: Binding<Bool> {
Binding(
get: { menuBarMonitor },
set: { newValue in
menuBarMonitor = newValue
// Tell AppDelegate to add/remove the status item without relaunch.
NotificationCenter.default.post(name: .pureMacMenuBarMonitorChanged, object: nil)
}
)
}
private var launchAtLoginBinding: Binding<Bool> {
Binding(
get: { launchAtLogin },
set: { newValue in
launchAtLogin = newValue
toggleLaunchAtLogin(newValue)
}
)
}
private var appLanguageBinding: Binding<AppLanguage> {
Binding(
get: { AppLanguage(rawValue: appLanguageRaw) ?? .system },
set: { newValue in
appLanguageRaw = newValue.rawValue
applyLanguage(newValue)
}
)
}
private func toggleLaunchAtLogin(_ enabled: Bool) {
do {
if enabled {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
Logger.shared.log("Failed to \(enabled ? "enable" : "disable") launch at login: \(error.localizedDescription)", level: .error)
launchAtLogin = !enabled
}
}
private func applyLanguage(_ language: AppLanguage) {
AppLanguagePreferences.apply(language)
languageNeedsRelaunch = true
}
private func relaunchApp() {
guard Bundle.main.bundleURL.pathExtension == "app" else {
NSApp.terminate(nil)
return
}
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
task.arguments = ["-n", Bundle.main.bundleURL.path]
do {
try task.run()
NSApp.terminate(nil)
} catch {
Logger.shared.log("Failed to relaunch PureMac: \(error.localizedDescription)", level: .error)
}
}
}
// MARK: - Cleaning
struct CleaningSettingsView: View {
@EnvironmentObject var appState: AppState
@AppStorage("settings.cleaning.skipHiddenFiles") private var skipHiddenFiles = true
@AppStorage("settings.cleaning.largeFileThreshold") private var largeFileThresholdMB: Int = 100
@AppStorage("settings.cleaning.oldFileMonths") private var oldFileMonths: Int = 12
private static let excludedFoldersKey = "settings.cleaning.largeFileExcludedFolders"
@State private var excludedFolders: [String] = []
var body: some View {
Form {
Section("File Discovery") {
Toggle("Skip hidden files during scan", isOn: $skipHiddenFiles)
}
Section("Large Files") {
Stepper(
String(format: String(localized: "Minimum size: %lld MB"), Int64(largeFileThresholdMB)),
value: $largeFileThresholdMB,
in: 10...1000,
step: 10
)
Stepper(
String(format: String(localized: "Files older than: %lld months"), Int64(oldFileMonths)),
value: $oldFileMonths,
in: 1...60
)
}
Section("Excluded Folders") {
if excludedFolders.isEmpty {
Text("Files inside these folders are skipped from the Large & Old Files scan (Downloads, Documents, Desktop).")
.font(.caption)
.foregroundStyle(.secondary)
} else {
ForEach(excludedFolders, id: \.self) { folder in
HStack(spacing: 8) {
Image(systemName: "folder")
.foregroundStyle(.secondary)
Text((folder as NSString).abbreviatingWithTildeInPath)
.lineLimit(1)
.truncationMode(.middle)
.help(folder)
Spacer()
Button {
removeExcludedFolder(folder)
} label: {
Image(systemName: "minus.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.borderless)
.help(String(localized: "Remove from exclusions"))
}
}
}
Button("Add Folder…") { addExcludedFolder() }
}
Section("Orphan Finder") {
HStack {
// Read the live count directly (it's a UserDefaults-backed
// computed property on AppState) so it stays correct when
// orphans are ignored from the Orphans view while this tab
// is open. AppState fires objectWillChange on both ignore
// (via @Published orphanedFiles) and clear, re-rendering this.
Text(String(format: String(localized: "Ignored orphans: %lld"), Int64(appState.ignoredOrphanCount)))
.foregroundStyle(.secondary)
Spacer()
Button("Forget Ignored") {
appState.clearIgnoredOrphans()
}
.disabled(appState.ignoredOrphanCount == 0)
}
Text("Ignored files won't appear in future orphan scans.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.onAppear {
excludedFolders = UserDefaults.standard.stringArray(forKey: Self.excludedFoldersKey) ?? []
}
}
private func persistExcludedFolders() {
UserDefaults.standard.set(excludedFolders, forKey: Self.excludedFoldersKey)
}
private func addExcludedFolder() {
let panel = NSOpenPanel()
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.allowsMultipleSelection = true
panel.prompt = String(localized: "Add Folder…")
guard panel.runModal() == .OK else { return }
for url in panel.urls where !excludedFolders.contains(url.path) {
excludedFolders.append(url.path)
}
persistExcludedFolders()
}
private func removeExcludedFolder(_ folder: String) {
excludedFolders.removeAll { $0 == folder }
persistExcludedFolders()
}
}
// MARK: - Schedule
struct ScheduleSettingsView: View {
@EnvironmentObject var appState: AppState
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
Form {
Section("Automatic Scanning") {
Toggle("Enable scheduled scanning", isOn: $appState.scheduler.config.isEnabled)
if appState.scheduler.config.isEnabled {
Group {
Picker("Scan interval", selection: $appState.scheduler.config.interval) {
ForEach(ScheduleInterval.allCases) { interval in
Text(LocalizedStringKey(interval.rawValue)).tag(interval)
}
}
Toggle("Auto-clean after scan", isOn: $appState.scheduler.config.autoClean)
Toggle("Notify on completion", isOn: $appState.scheduler.config.notifyOnCompletion)
HStack {
Text("Last run")
Spacer()
Text(appState.scheduler.config.formattedLastRun)
.foregroundStyle(.secondary)
}
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
}
.formStyle(.grouped)
.animation(reduceMotion ? nil : .spring(response: 0.35, dampingFraction: 0.85),
value: appState.scheduler.config.isEnabled)
}
}
// MARK: - About
struct AboutSettingsView: View {
var body: some View {
Form {
Section {
HStack {
if let appIcon = NSImage(named: "AppIcon") {
Image(nsImage: appIcon)
.resizable()
.frame(width: 64, height: 64)
}
VStack(alignment: .leading, spacing: 4) {
Text("PureMac")
.font(.title2.bold())
Text(
String(
format: String(localized: "Version %@"),
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
)
)
.foregroundStyle(.secondary)
Text("Free, open-source macOS app manager.")
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
Section {
Link("GitHub Repository", destination: URL(string: "https://github.com/momenbasel/PureMac")!)
Link("Report an Issue", destination: URL(string: "https://github.com/momenbasel/PureMac/issues")!)
}
Section {
Text("MIT License")
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
}
}