From 6d1561c6bffaf99b85583ee4ba8edfca82e304d5 Mon Sep 17 00:00:00 2001 From: ysicing Date: Mon, 28 Sep 2026 13:42:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(cleaner):=20=E5=B0=86=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E8=AE=BE=E4=B8=BA=E5=8F=AF=E9=80=89=E5=B9=B6?= =?UTF-8?q?=E6=A0=A1=E6=AD=A3=E7=A1=AE=E8=AE=A4=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Cleaner/CleanEngine.swift | 3 +- .../Sources/Cleaner/CleanModels.swift | 9 +++++ .../Sources/Localization/Resources/ar.tsv | 4 +- .../Sources/Localization/Resources/de.tsv | 4 +- .../Sources/Localization/Resources/es.tsv | 4 +- .../Sources/Localization/Resources/fr.tsv | 4 +- .../Sources/Localization/Resources/ja.tsv | 4 +- .../Sources/Localization/Resources/ko.tsv | 4 +- .../Localization/Resources/zh-Hant.tsv | 4 +- .../Sources/Localization/Translations.swift | 4 +- .../XStatsUI/AppKit/AppController.swift | 28 +++++++++++-- .../AppKit/ProjectPurgeWindowController.swift | 2 + .../Sources/XStatsUI/Panel/CleanerPage.swift | 18 +++++++-- .../XStatsUI/Panel/MainWindowView.swift | 8 +++- .../XStatsUI/Settings/SettingsPages.swift | 7 ++++ .../XStatsUI/Snapshot/SnapshotRenderer.swift | 1 + .../XStatsUI/State/AlertController.swift | 6 ++- .../Sources/XStatsUI/State/AppSettings.swift | 11 ++++++ .../XStatsUI/State/CleanerController.swift | 6 +-- .../XStatsUI/State/SettingsDocument.swift | 3 ++ .../Tests/CleanerTests/CleanerTests.swift | 13 +++++++ .../Tests/XStatsUITests/XStatsUITests.swift | 39 +++++++++++++++++++ 22 files changed, 163 insertions(+), 23 deletions(-) diff --git a/Packages/XStatsKit/Sources/Cleaner/CleanEngine.swift b/Packages/XStatsKit/Sources/Cleaner/CleanEngine.swift index a6ee93b..52768ac 100644 --- a/Packages/XStatsKit/Sources/Cleaner/CleanEngine.swift +++ b/Packages/XStatsKit/Sources/Cleaner/CleanEngine.swift @@ -185,8 +185,7 @@ public enum CleanEngine { await onProgress?(currentProgress()) continue } - // 废纸篓规则本身必须永久删除;其他可再生内容按用户偏好决定 - let useTrash = scan.rule.id != RuleCatalog.trash.id && (scan.rule.policy == .trash || preferTrash) + let useTrash = !scan.rule.deletesPermanently(preferTrash: preferTrash) for item in scan.items { if Task.isCancelled { report.wasCancelled = true; return report } diff --git a/Packages/XStatsKit/Sources/Cleaner/CleanModels.swift b/Packages/XStatsKit/Sources/Cleaner/CleanModels.swift index 03718c8..69cd10a 100644 --- a/Packages/XStatsKit/Sources/Cleaner/CleanModels.swift +++ b/Packages/XStatsKit/Sources/Cleaner/CleanModels.swift @@ -111,6 +111,15 @@ public struct CleanRule: Sendable, Identifiable { } public var usesToolCleaner: Bool { cleanWithTool != nil } + + /// 确认提示与清理执行共用的删除方式;工具清理和清空废纸篓始终不可恢复。 + public func deletesPermanently(preferTrash: Bool) -> Bool { + if usesToolCleaner || id == RuleCatalog.trash.id { return true } + switch policy { + case .delete: return !preferTrash + case .trash: return false + } + } } public struct CleanItem: Sendable, Identifiable, Hashable { diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index 25e66e1..4ff2731 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -1026,7 +1026,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم P 继续运行 مواصلة التشغيل 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) أخضر: طبيعي؛ برتقالي: مرتفع (فوق 60%)؛ أحمر: مرتفع جدًا (فوق 85%) 缓存 الذاكرات المؤقتة -缓存与日志直接删除,下载内容移到废纸篓。 تُحذف الذاكرات المؤقتة والسجلات؛ وتُنقل التنزيلات إلى سلة المهملات. +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 ستُحذف الذاكرات المؤقتة والسجلات نهائيًا ولا يمكن استعادتها؛ وتُنقل التنزيلات إلى سلة المهملات. 缓存也先移到废纸篓 نقل الذاكرات المؤقتة إلى السلة أيضًا 编辑 تعديل 网站 الموقع @@ -1568,3 +1568,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 توقف التنظيف. لا يمكن استعادة العناصر التي نُظفت بالفعل. 扫描大小约 {} الحجم المفحوص: نحو {} 正在取消… جارٍ الإلغاء… +按需扫描缓存、日志与项目产物,清理前逐项确认。 افحص الذاكرات المؤقتة والسجلات ومخرجات المشاريع عند الحاجة، وراجع العناصر قبل تنظيفها. +“{}”只剩 {} 可用。请检查不再需要的文件。 لم يتبقَّ على «{}» سوى {}. راجع الملفات التي لم تعد بحاجة إليها. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index cb16f32..16ed74e 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -1026,7 +1026,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Py 继续运行 Weiterlaufen lassen 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) Grün: normal, Orange: hoch (über 60 %), Rot: sehr hoch (über 85 %) 缓存 Caches -缓存与日志直接删除,下载内容移到废纸篓。 Caches und Protokolle werden direkt gelöscht, Downloads in den Papierkorb verschoben. +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 Caches und Protokolle werden endgültig gelöscht und können nicht wiederhergestellt werden; Downloads werden in den Papierkorb verschoben. 缓存也先移到废纸篓 Auch Caches in den Papierkorb verschieben 编辑 Bearbeiten 网站 Website @@ -1568,3 +1568,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 Gestoppt. Bereits bereinigte Inhalte können nicht wiederhergestellt werden. 扫描大小约 {} Erfasste Größe: ca. {} 正在取消… Wird abgebrochen… +按需扫描缓存、日志与项目产物,清理前逐项确认。 Caches, Protokolle und Projektartefakte bei Bedarf scannen. Elemente vor dem Bereinigen einzeln prüfen. +“{}”只剩 {} 可用。请检查不再需要的文件。 Auf „{}“ sind nur noch {} verfügbar. Prüfe Dateien, die du nicht mehr benötigst. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index 7260303..aa85b2a 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -1026,7 +1026,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes 继续运行 Seguir funcionando 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) Verde: normal; naranja: alto (más del 60%); rojo: muy alto (más del 85%) 缓存 Cachés -缓存与日志直接删除,下载内容移到废纸篓。 Las cachés y registros se eliminan; las descargas van a la Papelera. +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 Las cachés y registros se eliminarán de forma permanente y no se podrán recuperar; las descargas van a la Papelera. 缓存也先移到废纸篓 Mover también las cachés a la Papelera 编辑 Editar 网站 Sitio @@ -1568,3 +1568,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 Detenido. Los elementos ya eliminados no se pueden restaurar. 扫描大小约 {} Tamaño analizado: aprox. {} 正在取消… Cancelando… +按需扫描缓存、日志与项目产物,清理前逐项确认。 Analiza cachés, registros y artefactos de proyectos cuando lo necesites. Revisa cada elemento antes de limpiar. +“{}”只剩 {} 可用。请检查不再需要的文件。 En «{}» solo quedan {} disponibles. Revisa los archivos que ya no necesitas. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index 5f4ea67..9f4b207 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -1026,7 +1026,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Caches de paquets P 继续运行 Continuer l’exécution 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) Vert : normal, orange : élevé (au-dessus de 60 %), rouge : très élevé (au-dessus de 85 %) 缓存 Caches -缓存与日志直接删除,下载内容移到废纸篓。 Les caches et journaux sont supprimés ; les téléchargements sont déplacés vers la Corbeille. +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 Les caches et journaux seront supprimés définitivement, sans possibilité de récupération ; les téléchargements sont déplacés vers la Corbeille. 缓存也先移到废纸篓 Déplacer aussi les caches vers la Corbeille 编辑 Édition 网站 Site @@ -1568,3 +1568,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 Arrêté. Les éléments déjà supprimés ne peuvent pas être restaurés. 扫描大小约 {} Taille analysée : environ {} 正在取消… Annulation en cours… +按需扫描缓存、日志与项目产物,清理前逐项确认。 Analysez les caches, journaux et artefacts de projet à la demande. Vérifiez les éléments avant le nettoyage. +“{}”只剩 {} 可用。请检查不再需要的文件。 « {} » : il ne reste que {} disponibles. Vérifiez les fichiers dont vous n’avez plus besoin. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index 5e61ff0..eeeaab2 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -1026,7 +1026,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッ 继续运行 実行を続ける 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) 緑は正常、オレンジは高め(60%超)、赤は非常に高い(85%超) 缓存 キャッシュ -缓存与日志直接删除,下载内容移到废纸篓。 キャッシュとログは直接削除し、ダウンロードはゴミ箱に移動します。 +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 キャッシュとログは直接削除され、復元できません。ダウンロードはゴミ箱に移動します。 缓存也先移到废纸篓 キャッシュもゴミ箱に移動 编辑 編集 网站 サイト @@ -1568,3 +1568,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 停止しました。すでに削除した内容は復元できません 扫描大小约 {} スキャン時のサイズ:約{} 正在取消… キャンセル中… +按需扫描缓存、日志与项目产物,清理前逐项确认。 キャッシュ、ログ、プロジェクトの生成物を必要なときにスキャンし、削除前に項目を確認します。 +“{}”只剩 {} 可用。请检查不再需要的文件。 「{}」の空き容量は残り {} です。不要なファイルを確認してください。 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index 9354303..4b35d9e 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -1026,7 +1026,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키 继续运行 계속 실행 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) 초록색은 정상, 주황색은 높음(60% 초과), 빨간색은 매우 높음(85% 초과) 缓存 캐시 -缓存与日志直接删除,下载内容移到废纸篓。 캐시와 로그는 바로 삭제하고 다운로드는 휴지통으로 이동합니다. +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 캐시와 로그는 영구 삭제되며 복구할 수 없습니다. 다운로드는 휴지통으로 이동합니다. 缓存也先移到废纸篓 캐시도 휴지통으로 이동 编辑 편집 网站 사이트 @@ -1568,3 +1568,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 중지됨. 이미 정리된 항목은 복원할 수 없습니다. 扫描大小约 {} 스캔 크기 약 {} 正在取消… 취소하는 중… +按需扫描缓存、日志与项目产物,清理前逐项确认。 필요할 때 캐시, 로그 및 프로젝트 빌드 산출물을 검사하고 정리 전에 항목을 확인합니다. +“{}”只剩 {} 可用。请检查不再需要的文件。 “{}”의 남은 공간은 {}입니다. 더 이상 필요하지 않은 파일을 확인하세요. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index 18402b7..7df20e7 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -1027,7 +1027,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python 继续运行 繼續運行 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) 綠色正常、橙色偏高(60% 以上)、紅色很高(85% 以上) 缓存 緩存 -缓存与日志直接删除,下载内容移到废纸篓。 緩存與日誌直接刪除,下載內容移到廢紙簍。 +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 緩存與日誌將直接刪除且無法復原,下載內容移到廢紙簍。 缓存也先移到废纸篓 緩存也先移到廢紙簍 编辑 編輯 网站 網站 @@ -1569,3 +1569,5 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依 已停止,已清理部分不会恢复 已停止,已清理部分無法復原 扫描大小约 {} 掃描大小約 {} 正在取消… 正在取消… +按需扫描缓存、日志与项目产物,清理前逐项确认。 按需掃描快取、日誌與專案產物,清理前逐項確認。 +“{}”只剩 {} 可用。请检查不再需要的文件。 「{}」只剩 {} 可用。請檢查不再需要的檔案。 diff --git a/Packages/XStatsKit/Sources/Localization/Translations.swift b/Packages/XStatsKit/Sources/Localization/Translations.swift index d624c05..ef5be7e 100644 --- a/Packages/XStatsKit/Sources/Localization/Translations.swift +++ b/Packages/XStatsKit/Sources/Localization/Translations.swift @@ -1149,7 +1149,7 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Python package and 继续运行 Keep running 绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上) Green is normal, orange is high (above 60%), red is very high (above 85%) 缓存 Caches -缓存与日志直接删除,下载内容移到废纸篓。 Caches and logs are deleted; downloads go to the Trash. +缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。 Caches and logs will be permanently deleted and cannot be recovered; downloads go to the Trash. 缓存也先移到废纸篓 Move caches to Trash too 编辑 Edit 网站 Site @@ -1419,5 +1419,7 @@ Agent 工作树 Agent worktree Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依赖。 Homebrew will clean old versions and caches based on their state at execution time, without automatically removing dependencies. 尚未扫描 Not scanned yet 点击重新扫描以计算可清理空间 Click Rescan to calculate reclaimable space +按需扫描缓存、日志与项目产物,清理前逐项确认。 Scan caches, logs, and project artifacts on demand. Review items before cleaning. +“{}”只剩 {} 可用。请检查不再需要的文件。 “{}” has only {} available. Check files you no longer need. """# } diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift index 00c829f..03ffc8d 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift @@ -101,9 +101,10 @@ public final class AppController: NSObject, NSApplicationDelegate { self?.speedTestWindow.show() } model.openProjectPurgeWindow = { [weak self] in - self?.menuBar.dismissPopovers() - self?.calendarMenuBar.dismiss() - self?.projectPurgeWindow.show() + guard let self, self.model.settings.cleanerEnabled else { return } + self.menuBar.dismissPopovers() + self.calendarMenuBar.dismiss() + self.projectPurgeWindow.show() } model.quit = { NSApp.terminate(nil) } model.helper.refreshStatus() @@ -119,6 +120,7 @@ public final class AppController: NSObject, NSApplicationDelegate { model.screenTime.start() observeScreenTimeHistorySetting() observeRestSettings() + observeCleanerSetting() observeRestMode() observeRestSound() rest.sync() @@ -632,6 +634,26 @@ public final class AppController: NSObject, NSApplicationDelegate { } } + /// 关闭清理模块时停止尚未结束的扫描或清理,并关闭其独立窗口。 + private func observeCleanerSetting() { + withObservationTracking { + _ = model.settings.cleanerEnabled + } onChange: { [weak self] in + Task { @MainActor [weak self] in + guard let self else { return } + if !self.model.settings.cleanerEnabled { + self.model.cleaner.cancelClean() + self.model.cleaner.cancelOperation() + self.model.projectPurge.isConfirming = false + self.model.projectPurge.cancelScan() + self.model.projectPurge.cancelClean() + self.projectPurgeWindow.close() + } + self.observeCleanerSetting() + } + } + } + private func observeRestMode() { withObservationTracking { _ = model.settings.restMode diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/ProjectPurgeWindowController.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/ProjectPurgeWindowController.swift index cf9f6a2..eccddf9 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/ProjectPurgeWindowController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/ProjectPurgeWindowController.swift @@ -20,6 +20,8 @@ final class ProjectPurgeWindowController: NSObject, NSWindowDelegate { func refreshLanguage() { window?.title = tr("项目产物") } + func close() { window?.close() } + func show() { let window = self.window ?? makeWindow() self.window = window diff --git a/Packages/XStatsKit/Sources/XStatsUI/Panel/CleanerPage.swift b/Packages/XStatsKit/Sources/XStatsUI/Panel/CleanerPage.swift index c9d1c56..71763b8 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Panel/CleanerPage.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Panel/CleanerPage.swift @@ -81,12 +81,22 @@ private struct SummaryCard: View { } if cleaner.isConfirming { - InfoBanner(icon: "exclamationmark.triangle.fill", text: confirmationText, tone: .warning) { + let deletesPermanently = cleaner.selectedScans.contains { + $0.rule.deletesPermanently(preferTrash: model.settings.cleanPrefersTrash) + } + InfoBanner(icon: "exclamationmark.triangle.fill", text: confirmationText, + tone: deletesPermanently ? .error : .warning) { HStack(spacing: DS.Space.s2) { Button(tr("取消")) { cleaner.cancelClean() } .buttonStyle(DSButtonStyle(kind: .secondary)) - Button(tr("确认清理")) { cleaner.confirmClean() } - .buttonStyle(DSButtonStyle(kind: .primary)) + if deletesPermanently { + Button(tr("确认清理"), role: .destructive) { cleaner.confirmClean() } + .buttonStyle(.borderedProminent) + .tint(DS.Palette.error) + } else { + Button(tr("确认清理")) { cleaner.confirmClean() } + .buttonStyle(DSButtonStyle(kind: .primary)) + } } } } else { @@ -152,7 +162,7 @@ private struct SummaryCard: View { let count = cleaner.selectedScans.reduce(0) { $0 + $1.items.count } let trashNote = model.settings.cleanPrefersTrash ? tr("支持的内容先移到废纸篓;工具缓存由对应命令直接清理。") - : tr("缓存与日志直接删除,下载内容移到废纸篓。") + : tr("缓存与日志将直接删除且无法恢复,下载内容移到废纸篓。") var text = tr("将清理 \(count) 个项目,共 \(Format.bytes(cleaner.selectedBytes, base: .decimal))。\(trashNote)") if cleaner.selectedScans.contains(where: { $0.id == "developer.homebrew" }) { text += " " + tr("Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依赖。") diff --git a/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift b/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift index 6a1b5c3..3dfad45 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift @@ -196,7 +196,13 @@ private struct MainSidebar: View { private var navigation: some View { VStack(alignment: .leading, spacing: DS.Space.s1) { group(tr("监控"), PanelTab.monitors.filter { $0 != .aiUsage || model.settings.aiUsageEnabled }) - group(tr("工具"), PanelTab.tools.filter { $0 != .rest || model.settings.restEnabled }) + group(tr("工具"), PanelTab.tools.filter { tab in + switch tab { + case .rest: model.settings.restEnabled + case .cleaner: model.settings.cleanerEnabled + default: true + } + }) group(tr("设置"), PanelTab.settings) } .sidebarGlider() diff --git a/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift b/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift index 7283afc..7c18314 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Settings/SettingsPages.swift @@ -122,6 +122,13 @@ struct GeneralSettings: View { DSToggle(isOn: $settings.aiUsageEnabled, label: tr("AI 用量与额度")) } } + GroupRow(showsDivider: false) { + SettingRow(title: tr("清理"), + subtitle: tr("按需扫描缓存、日志与项目产物,清理前逐项确认。"), + icon: "eraser") { + DSToggle(isOn: $settings.cleanerEnabled, label: tr("清理")) + } + } GroupRow { SettingRow(title: tr("菜单栏日历"), subtitle: tr("独立显示日期,点击打开月历;不受指标合并布局影响"), diff --git a/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift b/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift index 214107d..b973289 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift @@ -28,6 +28,7 @@ enum SnapshotRenderer { settings.language = L10n.language settings.menuBarItems = [.cpu, .gpu, .memory, .network, .disk, .temperature, .battery, .aiUsage] settings.aiUsageEnabled = true + settings.cleanerEnabled = true // 历史页用示例数据:最近 24 小时每分钟一条,中间留一段“睡眠”空档 let historyURL = FileManager.default.temporaryDirectory.appendingPathComponent("xstats-snapshot-history.sqlite") try? FileManager.default.removeItem(at: historyURL) diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/AlertController.swift b/Packages/XStatsKit/Sources/XStatsUI/State/AlertController.swift index 6d0dbe0..f3aac2f 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/AlertController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/AlertController.swift @@ -199,8 +199,10 @@ public final class AlertController: NSObject { let enabled = settings.enabledAlerts if enabled.contains(.diskSpace), let disk = DiskSampler.sample() { let low = disk.total > 0 && (Double(disk.available) / Double(disk.total) < 0.1 || disk.available < 10_000_000_000) - fireIfNeeded(.diskSpace, isActive: low, now: now, - body: tr("“\(disk.volumeName)”只剩 \(Format.bytes(disk.available, base: .decimal)) 可用。可以用 XStats 的清理功能释放缓存。")) + let body = settings.cleanerEnabled + ? tr("“\(disk.volumeName)”只剩 \(Format.bytes(disk.available, base: .decimal)) 可用。可以用 XStats 的清理功能释放缓存。") + : tr("“\(disk.volumeName)”只剩 \(Format.bytes(disk.available, base: .decimal)) 可用。请检查不再需要的文件。") + fireIfNeeded(.diskSpace, isActive: low, now: now, body: body) } if enabled.contains(.batteryHealth), let health = BatterySampler.sample()?.health { // 冷却时间跨越重启,记在偏好设置里 diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift b/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift index b24f482..d0c8fca 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift @@ -514,6 +514,13 @@ public final class AppSettings { if !aiUsageEnabled && panelTab == .aiUsage { panelTab = .settingsGeneral } } } + /// 清理工具默认关闭;扫描只在打开页面后按需执行。 + public var cleanerEnabled: Bool { + didSet { + defaults.set(cleanerEnabled, forKey: Keys.cleanerEnabled) + if !cleanerEnabled && panelTab == .cleaner { panelTab = .settingsGeneral } + } + } /// 来源开关只保存在本机;关闭后不扫描,也不显示其历史缓存。 public var aiUsageSources: Set { didSet { defaults.set(aiUsageSources.map(\.rawValue).sorted(), forKey: Keys.aiUsageSources) } @@ -708,6 +715,8 @@ public final class AppSettings { ? defaults.integer(forKey: Keys.refreshSeconds) : 2 let isAIUsageEnabled = defaults.bool(forKey: Keys.aiUsageEnabled) aiUsageEnabled = isAIUsageEnabled + let isCleanerEnabled = defaults.bool(forKey: Keys.cleanerEnabled) + cleanerEnabled = isCleanerEnabled aiUsageSources = defaults.stringArray(forKey: Keys.aiUsageSources) .map { Set($0.compactMap(AIProviderID.init(rawValue:))) } ?? Set(AIProviderID.allCases) aiUsageShowsLocalUsage = defaults.object(forKey: Keys.aiUsageShowsLocalUsage) as? Bool ?? true @@ -726,6 +735,7 @@ public final class AppSettings { panelTab = savedPanelTabValue == "settingsAI" || savedPanelTab == .settingsAccount || (savedPanelTab == .aiUsage && !isAIUsageEnabled) || (savedPanelTab == .rest && !isRestEnabled) + || (savedPanelTab == .cleaner && !isCleanerEnabled) ? .settingsGeneral : savedPanelTab appearance = defaults.string(forKey: Keys.appearance).flatMap(AppearanceMode.init(rawValue:)) ?? .system showDockIcon = defaults.bool(forKey: Keys.showDockIcon) @@ -817,6 +827,7 @@ public final class AppSettings { static let styleOverrides = "styleOverrides" static let refreshSeconds = "refreshSeconds" static let aiUsageEnabled = "aiUsageEnabled" + static let cleanerEnabled = "cleanerEnabled" static let aiUsageSources = "aiUsageSources" static let aiUsageShowsLocalUsage = "aiUsageShowsLocalUsage" static let aiUsageRefreshMinutes = "aiUsageRefreshMinutes" diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift b/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift index 42b24c3..40e0c4d 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift @@ -36,7 +36,7 @@ public final class CleanerController { var isBusy: Bool { phase == .scanning || phase == .cleaning } func scan(ifNeeded: Bool = false) { - guard !isBusy, !(ifNeeded && lastScan != nil) else { return } + guard settings.cleanerEnabled, !isBusy, !(ifNeeded && lastScan != nil) else { return } phase = .scanning isConfirming = false let environment = Self.environment() @@ -51,7 +51,7 @@ public final class CleanerController { } func requestClean() { - guard phase == .ready || phase == .finished, !selectedScans.isEmpty else { return } + guard settings.cleanerEnabled, (phase == .ready || phase == .finished), !selectedScans.isEmpty else { return } isConfirming = true } @@ -66,7 +66,7 @@ public final class CleanerController { } func confirmClean() { - guard isConfirming else { return } + guard settings.cleanerEnabled, isConfirming else { return } isConfirming = false report = nil cleanProgress = nil diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/SettingsDocument.swift b/Packages/XStatsKit/Sources/XStatsUI/State/SettingsDocument.swift index aadfb89..e5f28b7 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/SettingsDocument.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/SettingsDocument.swift @@ -55,6 +55,7 @@ public struct SettingsDocument: Codable, Equatable, Sendable { public var historyEnabled: Bool? public var autoCheckUpdates: Bool? public var updateCheckSchedule: String? + public var cleanerEnabled: Bool? public var cleanPrefersTrash: Bool? public init() {} @@ -103,6 +104,7 @@ extension AppSettings { doc.historyEnabled = historyEnabled doc.autoCheckUpdates = autoCheckUpdates doc.updateCheckSchedule = updateCheckSchedule.rawValue + doc.cleanerEnabled = cleanerEnabled doc.cleanPrefersTrash = cleanPrefersTrash return doc } @@ -172,6 +174,7 @@ extension AppSettings { } else { assign(\.autoCheckUpdates, doc.autoCheckUpdates) } + assign(\.cleanerEnabled, doc.cleanerEnabled) assign(\.cleanPrefersTrash, doc.cleanPrefersTrash) } diff --git a/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift b/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift index 8fd3d0e..724cf37 100644 --- a/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift +++ b/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift @@ -49,6 +49,19 @@ private struct FakeHome { func remove() { try? FileManager.default.removeItem(at: root) } } +@Suite("清理方式") +struct CleanDeletionPolicyTests { + @Test func confirmationMatchesActualDeletionPolicy() { + for preferTrash in [false, true] { + #expect(RuleCatalog.trash.deletesPermanently(preferTrash: preferTrash)) + #expect(RuleCatalog.uvCache.deletesPermanently(preferTrash: preferTrash)) + #expect(!RuleCatalog.incompleteDownloads.deletesPermanently(preferTrash: preferTrash)) + #expect(!RuleCatalog.xcodeArchives.deletesPermanently(preferTrash: preferTrash)) + #expect(RuleCatalog.userCaches.deletesPermanently(preferTrash: preferTrash) == !preferTrash) + } + } +} + @Suite("安全守卫") struct SafetyGuardTests { let guardian = SafetyGuard(home: "/Users/tester") diff --git a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift index d7b8282..d20f4f4 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift @@ -271,6 +271,45 @@ private func isolatedDefaults() -> UserDefaults { } } +@MainActor +@Suite struct CleanerFeatureTests { + @Test func defaultsOffAndHidesSavedPageWhenDisabled() { + let defaults = isolatedDefaults() + defaults.set("cleaner", forKey: "panelTab") + let settings = AppSettings(defaults: defaults) + #expect(!settings.cleanerEnabled) + #expect(settings.panelTab == .settingsGeneral) + + let cleaner = CleanerController(settings: settings) + cleaner.scan() + #expect(cleaner.phase == .idle) + + settings.cleanerEnabled = true + settings.panelTab = .cleaner + #expect(AppSettings(defaults: defaults).panelTab == .cleaner) + + settings.cleanerEnabled = false + #expect(settings.panelTab == .settingsGeneral) + #expect(!AppSettings(defaults: defaults).cleanerEnabled) + defaults.set("cleaner", forKey: "panelTab") + #expect(AppSettings(defaults: defaults).panelTab == .settingsGeneral) + } + + @Test func backupRestoresModulePreferenceAndOldBackupKeepsCurrentValue() throws { + let source = AppSettings(defaults: isolatedDefaults()) + source.cleanerEnabled = true + let data = try JSONEncoder().encode(source.exportDocument()) + let document = try JSONDecoder().decode(SettingsDocument.self, from: data) + #expect(document.cleanerEnabled == true) + + let target = AppSettings(defaults: isolatedDefaults()) + target.apply(document) + #expect(target.cleanerEnabled) + target.apply(SettingsDocument()) + #expect(target.cleanerEnabled) + } +} + @MainActor @Suite struct CleanerSelectionTests { private let toolRuleIDs: Set = [