Files
OpenStats/Packages/OpenStatsKit/Sources/Cleaner/CleanModels.swift
T
gentpan 1205483b5e 新增清理与系统维护,修复面板抖动,更换应用图标
- 清理:应用缓存、日志、浏览器缓存、Xcode/模拟器/npm 缓存、下载残留、安装包、废纸篓;
  白名单目录 + 安全守卫,运行中的应用自动跳过,执行前逐项复核,操作日志
- 系统维护:刷新 DNS 缓存、释放内存(辅助工具或管理员授权)
- 面板:去掉打开后的二次调整高度与窗口高度动画,内容放得下时不回弹,
  修复点击菜单栏图标关闭时被重新打开
- 更换应用图标,面板与关于页使用应用图标作为 Logo
- 清理未使用的组件、格式化方法、设计 token 与辅助工具接口
2026-09-13 03:03:08 +05:00

120 lines
4.0 KiB
Swift

import Foundation
public enum CleanCategory: String, Sendable, CaseIterable, Identifiable {
case system, browser, developer, downloads, trash
public var id: String { rawValue }
public var title: String {
switch self {
case .system: "系统"
case .browser: "浏览器"
case .developer: "开发者"
case .downloads: "下载"
case .trash: "废纸篓"
}
}
}
public enum DeletionPolicy: Sendable {
/// 可再生的缓存与日志:直接删除,空间立即释放
case delete
/// 用户文件(安装包、未完成下载):移到废纸篓,可恢复
case trash
}
public enum SkipReason: Sendable, Equatable, Hashable {
case appRunning(String)
case needsFullDiskAccess
case recentlyModified
case protected
public var title: String {
switch self {
case .appRunning(let name): "\(name) 正在运行"
case .needsFullDiskAccess: "需要完全磁盘访问权限"
case .recentlyModified: "刚刚被使用"
case .protected: "受保护"
}
}
}
/// 扫描所需的外部环境,便于在测试中替换
public struct CleanEnvironment: Sendable {
public var home: String
public var runningBundleIdentifiers: @Sendable () -> Set<String>
public var now: @Sendable () -> Date
public init(home: String = NSHomeDirectory(),
runningBundleIdentifiers: @escaping @Sendable () -> Set<String>,
now: @escaping @Sendable () -> Date = Date.init) {
self.home = home
self.runningBundleIdentifiers = runningBundleIdentifiers
self.now = now
}
}
public struct CleanRule: Sendable, Identifiable {
public let id: String
public let category: CleanCategory
public let title: String
public let detail: String
public let symbol: String
public let selectedByDefault: Bool
public let policy: DeletionPolicy
/// 这些应用运行时整条规则跳过(例如浏览器)
public let blockingApps: [(bundleID: String, name: String)]
/// 条目名为反向域名(如 com.foo.bar)时,对应应用运行中则跳过该条目
public let checksOwnerApp: Bool
/// 最近修改过的条目视为正在使用
public let minimumAge: TimeInterval
/// 列出候选条目(顶层文件或目录)
let locate: @Sendable (CleanEnvironment) throws -> [URL]
init(id: String, category: CleanCategory, title: String, detail: String, symbol: String,
selectedByDefault: Bool = true, policy: DeletionPolicy = .delete,
blockingApps: [(bundleID: String, name: String)] = [], checksOwnerApp: Bool = false,
minimumAge: TimeInterval = 120,
locate: @escaping @Sendable (CleanEnvironment) throws -> [URL]) {
self.id = id
self.category = category
self.title = title
self.detail = detail
self.symbol = symbol
self.selectedByDefault = selectedByDefault
self.policy = policy
self.blockingApps = blockingApps
self.checksOwnerApp = checksOwnerApp
self.minimumAge = minimumAge
self.locate = locate
}
}
public struct CleanItem: Sendable, Identifiable, Hashable {
public var id: String { url.path }
public let url: URL
public let size: UInt64
}
public struct RuleScan: Sendable, Identifiable {
public var id: String { rule.id }
public let rule: CleanRule
/// 按大小降序
public let items: [CleanItem]
public let skippedCount: Int
/// 整条规则被阻止的原因(浏览器运行中、缺少权限)
public let blocked: SkipReason?
public var totalSize: UInt64 { items.reduce(0) { $0 + $1.size } }
public var isCleanable: Bool { blocked == nil && !items.isEmpty }
}
public struct CleanReport: Sendable {
public var freedBytes: UInt64 = 0
public var removedCount = 0
public var skippedCount = 0
public var failures: [String] = []
/// 移到废纸篓的大小(不会立即释放)
public var trashedBytes: UInt64 = 0
}