From 8a73709fb12c389d771e1984dd7bc7b3d03a29fc Mon Sep 17 00:00:00 2001 From: X1a0He Date: Sat, 29 Mar 2025 17:41:20 +0800 Subject: [PATCH] perf: Rendering performance optimizations - UI rendering - App load rendering - Scroll rendering --- Adobe Downloader.xcodeproj/project.pbxproj | 8 +- .../xcschemes/xcschememanagement.plist | 4 +- Adobe Downloader/AppDelegate.swift | 16 - Adobe Downloader/Commons/Enums.swift | 232 ++++ .../Services/NetworkService.swift | 91 -- Adobe Downloader/Views/AboutView.swift | 1200 +---------------- Adobe Downloader/Views/AppCardView.swift | 3 + Adobe Downloader/Views/CleanConfigView.swift | 190 +++ Adobe Downloader/Views/CleanupView.swift | 645 +++++++++ .../Views/DownloadManagerView.swift | 58 +- .../Views/DownloadProgressView.swift | 4 +- Adobe Downloader/Views/LogEntryView.swift | 127 ++ Adobe Downloader/Views/MainContentView.swift | 48 +- Adobe Downloader/Views/QAView.swift | 59 + .../Views/Styles/BeautifulGroupBox.swift | 38 + .../Views/VersionPickerView.swift | 298 ++-- Localizables/Localizable.xcstrings | 47 +- 17 files changed, 1606 insertions(+), 1462 deletions(-) delete mode 100644 Adobe Downloader/Services/NetworkService.swift create mode 100644 Adobe Downloader/Views/CleanConfigView.swift create mode 100644 Adobe Downloader/Views/CleanupView.swift create mode 100644 Adobe Downloader/Views/LogEntryView.swift create mode 100644 Adobe Downloader/Views/QAView.swift create mode 100644 Adobe Downloader/Views/Styles/BeautifulGroupBox.swift diff --git a/Adobe Downloader.xcodeproj/project.pbxproj b/Adobe Downloader.xcodeproj/project.pbxproj index bb644fa..bf0ab7f 100644 --- a/Adobe Downloader.xcodeproj/project.pbxproj +++ b/Adobe Downloader.xcodeproj/project.pbxproj @@ -416,7 +416,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 200; + CURRENT_PROJECT_VERSION = 201; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\""; DEVELOPMENT_TEAM = TG862GVKHK; @@ -432,7 +432,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.0.1; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -450,7 +450,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 200; + CURRENT_PROJECT_VERSION = 201; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\""; DEVELOPMENT_TEAM = TG862GVKHK; @@ -466,7 +466,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.0.1; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcschemes/xcschememanagement.plist b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcschemes/xcschememanagement.plist index a1dde20..80b3b17 100644 --- a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcschemes/xcschememanagement.plist @@ -17,7 +17,7 @@ AdobeDownloaderHelperTool.xcscheme_^#shared#^_ orderHint - 2 + 1 SuppressBuildableAutocreation @@ -25,7 +25,7 @@ 3CCC3ADF2CC67B8F006E22B4 primary - + diff --git a/Adobe Downloader/AppDelegate.swift b/Adobe Downloader/AppDelegate.swift index af323a1..aa5595e 100644 --- a/Adobe Downloader/AppDelegate.swift +++ b/Adobe Downloader/AppDelegate.swift @@ -19,22 +19,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var eventMonitor: Any? func applicationDidFinishLaunching(_ notification: Notification) { - NSApp.mainMenu = nil - - for window in NSApplication.shared.windows { - window.titlebarAppearsTransparent = true - window.backgroundColor = NSColor(white: 1, alpha: 0) - - if let titlebarView = window.standardWindowButton(.closeButton)?.superview { - let blurView = NSVisualEffectView(frame: titlebarView.bounds) - blurView.blendingMode = .behindWindow - blurView.material = .hudWindow - blurView.state = .active - blurView.autoresizingMask = [.width, .height] - titlebarView.addSubview(blurView, positioned: .below, relativeTo: nil) - } - } - if let window = NSApp.windows.first { window.minSize = NSSize(width: 800, height: 765) } diff --git a/Adobe Downloader/Commons/Enums.swift b/Adobe Downloader/Commons/Enums.swift index f253683..40af1ea 100644 --- a/Adobe Downloader/Commons/Enums.swift +++ b/Adobe Downloader/Commons/Enums.swift @@ -476,3 +476,235 @@ private extension TimeInterval { return String(format: NSLocalizedString("%02d:%02d:%02d", comment: ""), hours, minutes, seconds) } } + +enum CleanupOption: String, CaseIterable, Identifiable { + case adobeApps = "Adobe 应用程序" + case adobeCreativeCloud = "Adobe Creative Cloud" + case adobePreferences = "Adobe 偏好设置" + case adobeCaches = "Adobe 缓存文件" + case adobeLicenses = "Adobe 许可文件" + case adobeLogs = "Adobe 日志文件" + case adobeServices = "Adobe 服务" + case adobeKeychain = "Adobe 钥匙串" + case adobeGenuineService = "Adobe 正版验证服务" + case adobeHosts = "Adobe Hosts" + + var id: String { self.rawValue } + + var localizedName: String { + switch self { + case .adobeApps: + return String(localized: "Adobe 应用程序") + case .adobeCreativeCloud: + return String(localized: "Adobe Creative Cloud") + case .adobePreferences: + return String(localized: "Adobe 偏好设置") + case .adobeCaches: + return String(localized: "Adobe 缓存文件") + case .adobeLicenses: + return String(localized: "Adobe 许可文件") + case .adobeLogs: + return String(localized: "Adobe 日志文件") + case .adobeServices: + return String(localized: "Adobe 服务") + case .adobeKeychain: + return String(localized: "Adobe 钥匙串") + case .adobeGenuineService: + return String(localized: "Adobe 正版验证服务") + case .adobeHosts: + return String(localized: "Adobe Hosts") + } + } + + var commands: [String] { + switch self { + case .adobeApps: + return [ + "sudo find /Applications -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo find /Applications/Utilities -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo rm -rf /Applications/Adobe Creative Cloud", + "sudo rm -rf /Applications/Utilities/Adobe Creative Cloud", + "sudo rm -rf /Applications/Utilities/Adobe Creative Cloud Experience", + "sudo rm -rf /Applications/Utilities/Adobe Installers/Uninstall Adobe Creative Cloud", + "sudo rm -rf /Applications/Utilities/Adobe Sync", + "sudo rm -rf /Applications/Utilities/Adobe Genuine Service" + ] + case .adobeCreativeCloud: + return [ + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/ADBox", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/ADS", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/AppsPanel", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/CEF", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/Core", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/CoreExt", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/DEBox", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/ElevationManager", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/FilesPanel", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/FontsPanel", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/HEX", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/LCC", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/NHEX", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/Notifications", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/pim.db", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/RemoteComponents", + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/TCC", + "sudo rm -rf /Library/Application Support/Adobe/ARMNext", + "sudo rm -rf /Library/Application Support/Adobe/ARMDC/Application", + "sudo rm -rf /Library/Application Support/Adobe/PII/com.adobe.pii.prefs", + "sudo rm -rf /Library/Application Support/Adobe/ACPLocal*", + "sudo rm -rf /Library/Application Support/regid.1986-12.com.adobe", + "sudo rm -rf /Library/Internet Plug-Ins/AdobeAAMDetect.plugin", + "sudo rm -rf /Library/Internet Plug-Ins/AdobePDF*", + "sudo rm -rf /Library/PDF Services/Save as Adobe PDF*", + "sudo rm -rf /Library/ScriptingAdditions/Adobe Unit Types.osax", + "sudo rm -rf /Library/Automator/Save as Adobe PDF.action", + "sudo rm -rf ~/.adobe", + "sudo rm -rf ~/Creative Cloud Files*", + "sudo find ~/Library/Application\\ Scripts -name '*com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", + "sudo find ~/Library/Group\\ Containers -name '*com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", + "sudo rm -rf ~/Library/Application\\ Scripts/Adobe-Hub-App || true", + "sudo rm -rf ~/Library/Group\\ Containers/Adobe-Hub-App || true", + "sudo rm -rf ~/Library/Application\\ Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.adobe* || true", + "sudo find ~/Library/Application\\ Support -name 'Acrobat*' ! -path '*/Adobe Downloader/*' -print0 | xargs -0 sudo rm -rf || true", + "sudo find ~/Library/Application\\ Support -name 'Adobe*' ! -name '*Adobe Downloader*' ! -path '*/Adobe Downloader/*' -print0 | xargs -0 sudo rm -rf || true", + "sudo find ~/Library/Application\\ Support -name 'com.adobe*' ! -name '*Adobe Downloader*' ! -path '*/Adobe Downloader/*' -print0 | xargs -0 sudo rm -rf || true", + "sudo rm -rf ~/Library/Application Support/io.branch", + "sudo rm -rf ~/Library/PhotoshopCrashes", + "sudo rm -rf ~/Library/WebKit/com.adobe*" + ] + case .adobePreferences: + return [ + "sudo find /Library/Preferences -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo find ~/Library/Preferences -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo find ~/Library/Preferences -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo find ~/Library/Preferences/ByHost -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo rm -rf ~/Library/Preferences/adobe.com*", + "sudo rm -rf ~/Library/Preferences/AIRobin*", + "sudo rm -rf ~/Library/Preferences/Macromedia*", + "sudo rm -rf ~/Library/Saved Application State/com.adobe*" + ] + case .adobeCaches: + return [ + "sudo find ~/Library/Caches -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", + "sudo find ~/Library/Caches -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", + "sudo rm -rf ~/Library/Caches/Acrobat* || true", + "sudo rm -rf ~/Library/Caches/CSXS || true", + "sudo rm -rf ~/Library/Caches/com.crashlytics.data/com.adobe* || true", + "sudo rm -rf ~/Library/Containers/com.adobe* || true", + "sudo rm -rf ~/Library/Cookies/com.adobe* || true", + "sudo find ~/Library/HTTPStorages -name '*Adobe*' ! -name '*Adobe Downloader*' ! -name '*com.x1a0he.macOS.Adobe-Downloader*' -print0 | xargs -0 sudo rm -rf || true", + "sudo find ~/Library/HTTPStorages -name 'com.adobe*' ! -name '*Adobe Downloader*' ! -name '*com.x1a0he.macOS.Adobe-Downloader*' -print0 | xargs -0 sudo rm -rf || true", + "sudo rm -rf ~/Library/HTTPStorages/Creative\\ Cloud\\ Content\\ Manager.node || true" + ] + case .adobeLicenses: + return [ + "sudo rm -rf /Library/Application Support/Adobe/Adobe PCD", + "sudo rm -rf /Library/Application Support/Adobe/AdobeGCClient", + "sudo rm -rf /Library/Application Support/regid.1986-12.com.adobe", + "sudo rm -rf /private/var/db/receipts/com.adobe*", + "sudo rm -rf /private/var/db/receipts/*Photoshop*", + "sudo rm -rf /private/var/db/receipts/*CreativeCloud*", + "sudo rm -rf /private/var/db/receipts/*CCXP*", + "sudo rm -rf /private/var/db/receipts/*mygreatcompany*", + "sudo rm -rf /private/var/db/receipts/*AntiCC*", + "sudo rm -rf /private/var/db/receipts/*.RiD.*", + "sudo rm -rf /private/var/db/receipts/*.CCRuntime.*" + ] + case .adobeLogs: + return [ + "sudo find ~/Library/Logs -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo find ~/Library/Logs -name 'adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", + "sudo rm -rf ~/Library/Logs/Adobe Creative Cloud Cleaner Tool.log", + "sudo rm -rf ~/Library/Logs/CreativeCloud", + "sudo rm -rf /Library/Logs/CreativeCloud", + "sudo rm -rf ~/Library/Logs/CSXS", + "sudo rm -rf ~/Library/Logs/amt3.log", + "sudo rm -rf ~/Library/Logs/CoreSyncInstall.log", + "sudo rm -rf ~/Library/Logs/CrashReporter/*Adobe*", + "sudo rm -rf ~/Library/Logs/acroLicLog.log", + "sudo rm -rf ~/Library/Logs/acroNGLLog.txt", + "sudo rm -rf ~/Library/Logs/DiagnosticReports/*Adobe*", + "sudo rm -rf ~/Library/Logs/distNGLLog.txt", + "sudo rm -rf ~/Library/Logs/NGL*", + "sudo rm -rf ~/Library/Logs/oobelib.log", + "sudo rm -rf ~/Library/Logs/PDApp*", + "sudo rm -rf /Library/Logs/adobe*", + "sudo rm -rf /Library/Logs/Adobe*", + "sudo rm -rf ~/Library/Logs/Adobe*", + "sudo rm -rf ~/Library/Logs/adobe*", + "sudo rm -rf /Library/Logs/DiagnosticReports/*Adobe*", + "sudo rm -rf /Library/Application Support/CrashReporter/*Adobe*", + "sudo rm -rf ~/Library/Application Support/CrashReporter/*Adobe*" + ] + case .adobeServices: + return [ + "sudo launchctl bootout gui/$(id -u) /Library/LaunchAgents/com.adobe.* || true", + "sudo launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.adobe.* || true", + "sudo launchctl unload /Library/LaunchDaemons/com.adobe.* || true", + "sudo launchctl remove com.adobe.AdobeCreativeCloud || true", + "sudo launchctl remove com.adobe.AdobeGenuineService.plist || true", + "sudo ps aux | grep -i 'Adobe' | grep -v 'Adobe Downloader' | grep -v 'Adobe-Downloader.helper' | grep -v grep | awk '{print $2}' | { pids=$(cat); [ ! -z \"$pids\" ] && echo \"$pids\" | xargs sudo kill -9; } || true", + "sudo rm -rf /Library/LaunchAgents/com.adobe.*", + "sudo rm -rf /Library/LaunchDaemons/com.adobe.*", + "sudo rm -rf /Library/LaunchAgents/com.adobe.ARMDCHelper*", + "sudo rm -rf /Library/LaunchAgents/com.adobe.AdobeCreativeCloud.plist", + "sudo rm -rf /Library/LaunchAgents/com.adobe.ccxprocess.plist" + ] + case .adobeKeychain: + return [ + "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'acrobat.com' | grep -i 'srvr' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-internet-password -s \"$line\" /Library/Keychains/System.keychain; done || true", + "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'acrobat.com' | grep -i 'srvr' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-internet-password -s \"$line\" ~/Library/Keychains/login.keychain-db; done || true", + "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'Adobe.APS' | grep -v 'Adobe Downloader' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-generic-password -l \"$line\" /Library/Keychains/System.keychain; done || true", + "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'Adobe.APS' | grep -v 'Adobe Downloader' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-generic-password -l \"$line\" ~/Library/Keychains/login.keychain-db; done || true", + "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'Adobe App Info\\|Adobe App Prefetched Info\\|Adobe User\\|com.adobe\\|Adobe Lightroom' | grep -v 'Adobe Downloader' | grep -i 'svce' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-generic-password -s \"$line\" /Library/Keychains/System.keychain; done || true", + "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'Adobe App Info\\|Adobe App Prefetched Info\\|Adobe User\\|com.adobe\\|Adobe Lightroom' | grep -v 'Adobe Downloader' | grep -i 'svce' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-generic-password -s \"$line\" ~/Library/Keychains/login.keychain-db; done || true", + "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'Adobe Content \\|Adobe Intermediate' | grep -v 'Adobe Downloader' | grep -i 'alis' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-certificate -c \"$line\" /Library/Keychains/System.keychain; done || true", + "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'Adobe Content \\|Adobe Intermediate' | grep -v 'Adobe Downloader' | grep -i 'alis' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-certificate -c \"$line\" ~/Library/Keychains/login.keychain-db; done || true" + ] + case .adobeGenuineService: + return [ + "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/AdobeGenuineClient", + "sudo rm -rf /Library/Application Support/Adobe/AdobeGCClient", + "sudo rm -rf /Library/Preferences/com.adobe.AdobeGenuineService.plist", + "sudo rm -rf /Applications/Utilities/Adobe Creative Cloud/Utils/AdobeGenuineValidator", + "sudo rm -rf /Applications/Utilities/Adobe Genuine Service", + "sudo rm -rf /Library/PrivilegedHelperTools/com.adobe.acc*", + "sudo find /private/tmp -type d -iname '*adobe*' ! -iname '*Adobe Downloader*' -o -type f -iname '*adobe*' ! -iname '*Adobe Downloader*' | xargs rm -rf {} \\+", + "sudo find /private/tmp -type d -iname '*CCLBS*' ! -iname '*Adobe Downloader*' -o -type f -iname '*adobe*' ! -iname '*Adobe Downloader*' | xargs rm -rf {} \\+", + "sudo find /private/var/folders/ -type d -iname '*adobe*' ! -iname '*Adobe Downloader*' -o -type f -iname '*adobe*' ! -iname '*Adobe Downloader*' | xargs rm -rf {} \\+", + "sudo rm -rf /private/tmp/com.adobe*", + "sudo rm -rf /private/tmp/Adobe*", + "sudo rm -rf /private/tmp/.adobe*" + ] + case .adobeHosts: + return [ + "sudo sh -c 'grep -v \"adobe\" /etc/hosts > /etc/hosts.temp && mv /etc/hosts.temp /etc/hosts'" + ] + } + } + + var description: String { + switch self { + case .adobeApps: + return String(localized: "删除所有已安装的 Adobe 应用程序(不包括 Adobe Downloader)") + case .adobeCreativeCloud: + return String(localized: "删除 Adobe Creative Cloud 应用程序及其组件") + case .adobePreferences: + return String(localized: "删除 Adobe 应用程序的偏好设置文件(不包括 Adobe Downloader)") + case .adobeCaches: + return String(localized: "删除 Adobe 应用程序的缓存文件(不包括 Adobe Downloader)") + case .adobeLicenses: + return String(localized: "删除 Adobe 许可和激活相关文件") + case .adobeLogs: + return String(localized: "删除 Adobe 应用程序的日志文件(不包括 Adobe Downloader)") + case .adobeServices: + return String(localized: "停止并删除 Adobe 相关服务") + case .adobeKeychain: + return String(localized: "删除钥匙串中的 Adobe 相关条目") + case .adobeGenuineService: + return String(localized: "删除 Adobe 正版验证服务及其组件") + case .adobeHosts: + return String(localized: "清理 hosts 文件中的 Adobe 相关条目") + } + } +} diff --git a/Adobe Downloader/Services/NetworkService.swift b/Adobe Downloader/Services/NetworkService.swift deleted file mode 100644 index 610e9b4..0000000 --- a/Adobe Downloader/Services/NetworkService.swift +++ /dev/null @@ -1,91 +0,0 @@ -//import Foundation -// -//class NetworkService { -// typealias ProductsData = (products: [String: Sap], sapCodes: [SapCodes]) -// -// private func makeProductsURL() throws -> URL { -// var components = URLComponents(string: NetworkConstants.productsJSONURL) -// components?.queryItems = [ -// URLQueryItem(name: "channel", value: "ccm"), -// URLQueryItem(name: "channel", value: "sti"), -// URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"), -// URLQueryItem(name: "_type", value: "json"), -// URLQueryItem(name: "productType", value: "Desktop") -// ] -// -// guard let url = components?.url else { -// throw NetworkError.invalidURL(NetworkConstants.productsJSONURL) -// } -// return url -// } -// -// private func configureRequest(_ request: inout URLRequest, headers: [String: String]) { -// headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } -// } -// -// func fetchProductsData() async throws -> ProductsData { -// let url = try makeProductsURL() -// var request = URLRequest(url: url) -// request.httpMethod = "GET" -// configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders) -// -// let (data, response) = try await URLSession.shared.data(for: request) -// -// guard let httpResponse = response as? HTTPURLResponse else { -// throw NetworkError.invalidResponse -// } -// -// guard (200...299).contains(httpResponse.statusCode) else { -// throw NetworkError.httpError(httpResponse.statusCode, nil) -// } -// -// guard let jsonString = String(data: data, encoding: .utf8) else { -// throw NetworkError.invalidData("无法解码JSON数据") -// } -// -// let result: ProductsData = try await Task.detached(priority: .userInitiated) { -// let parseResult = try JSONParser.parse(jsonString: jsonString) -// // 测试新API -// try NewJSONParser.parse(jsonString: jsonString) -// let products = parseResult.products, cdn = parseResult.cdn -// -// let sapCodes = products.values -// .filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) } -// .map { SapCodes(sapCode: $0.sapCode, displayName: $0.displayName) } -// -// return (products, sapCodes) -// }.value -// -// return result -// } -// -// func getApplicationInfo(buildGuid: String) async throws -> String { -// guard let url = URL(string: NetworkConstants.applicationJsonURL) else { -// throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL) -// } -// -// var request = URLRequest(url: url) -// request.httpMethod = "GET" -// -// var headers = NetworkConstants.adobeRequestHeaders -// headers["x-adobe-build-guid"] = buildGuid -// -// headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } -// -// let (data, response) = try await URLSession.shared.data(for: request) -// -// guard let httpResponse = response as? HTTPURLResponse else { -// throw NetworkError.invalidResponse -// } -// -// guard (200...299).contains(httpResponse.statusCode) else { -// throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8)) -// } -// -// guard let jsonString = String(data: data, encoding: .utf8) else { -// throw NetworkError.invalidData("无法将响应数据转换为json符串") -// } -// -// return jsonString -// } -//} diff --git a/Adobe Downloader/Views/AboutView.swift b/Adobe Downloader/Views/AboutView.swift index a73364b..964d501 100644 --- a/Adobe Downloader/Views/AboutView.swift +++ b/Adobe Downloader/Views/AboutView.swift @@ -128,21 +128,34 @@ struct AboutAppView: View { struct PulsingCircle: View { let color: Color - @State private var scale: CGFloat = 1.0 - + @State private var pulsing = false + var body: some View { - Circle() - .fill(color) - .frame(width: 8, height: 8) - .scaleEffect(scale) - .animation( - Animation.easeInOut(duration: 1.0) - .repeatForever(autoreverses: true), - value: scale - ) - .onAppear { - scale = 1.5 + ZStack { + ZStack(alignment: .center) { + Circle() + .fill(color) + .frame(width: 6, height: 6) + + Circle() + .stroke(color, lineWidth: 1) + .frame(width: pulsing ? 14 : 8, height: pulsing ? 14 : 8) + .opacity(pulsing ? 0 : 0.8) + + Circle() + .stroke(color, lineWidth: 1) + .frame(width: pulsing ? 12 : 6, height: pulsing ? 12 : 6) + .opacity(pulsing ? 0.2 : 0.6) } + .frame(width: 16, height: 16) + } + .frame(width: 16, height: 16, alignment: .center) + .clipped() + .onAppear { + withAnimation(Animation.easeOut(duration: 1.5).repeatForever(autoreverses: false)) { + pulsing = true + } + } } } @@ -423,37 +436,6 @@ private struct GeneralSettingsAlerts: ViewModifier { } } -struct BeautifulGroupBox: View { - let label: Label - let content: Content - - init(label: @escaping () -> Label, @ViewBuilder content: () -> Content) { - self.label = label() - self.content = content() - } - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - label - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.primary.opacity(0.85)) - - VStack(alignment: .leading, spacing: 0) { - content - } - .padding(8) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.secondary.opacity(0.15), lineWidth: 1) - ) - ) - } - } -} - struct DownloadSettingsView: View { @ObservedObject var viewModel: GeneralSettingsViewModel @@ -576,138 +558,6 @@ struct UpdateSettingsView: View { } } -struct CleanConfigView: View { - @State private var showConfirmation = false - @State private var showAlert = false - @State private var alertMessage = "" - @State private var chipInfo: String = "" - - private func getChipInfo() -> String { - var size = 0 - sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0) - var machine = [CChar](repeating: 0, count: size) - sysctlbyname("machdep.cpu.brand_string", &machine, &size, nil, 0) - let chipName = String(cString: machine) - - if chipName.contains("Apple") { - return chipName - } else { - return chipName.components(separatedBy: "@")[0].trimmingCharacters(in: .whitespaces) - } - } - - var body: some View { - HStack(spacing: 16) { - BeautifulGroupBox(label: { - Text("重置程序") - }) { - VStack(alignment: .leading, spacing: 8) { - HStack { - Button("重置程序") { - showConfirmation = true - } - .buttonStyle(BeautifulButtonStyle(baseColor: .red.opacity(0.8))) - .foregroundColor(.white) - } - .fixedSize(horizontal: false, vertical: true) - } - } - - BeautifulGroupBox(label: { - Text("系统信息") - }) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - Image(systemName: "desktopcomputer") - .foregroundColor(.blue) - .imageScale(.medium) - .frame(width: 22, height: 22) - .background(Circle().fill(Color.blue.opacity(0.1)).frame(width: 28, height: 28)) - - VStack(alignment: .leading, spacing: 1) { - Text("macOS \(ProcessInfo.processInfo.operatingSystemVersionString)") - .fontWeight(.medium) - - Text("\(chipInfo.isEmpty ? "加载中..." : chipInfo)") - .foregroundColor(.secondary) - .font(.system(size: 12)) - } - Spacer() - } - } - } - } - .alert("确认重置程序", isPresented: $showConfirmation) { - Button("取消", role: .cancel) { } - Button("确定", role: .destructive) { - cleanConfig() - } - } message: { - Text("这将清空所有配置并结束应用程序,确定要继续吗?") - } - .alert("操作结果", isPresented: $showAlert) { - Button("确定") { } - } message: { - Text(alertMessage) - } - .onAppear { - chipInfo = getChipInfo() - } - } - - private func cleanConfig() { - do { - let downloadsURL = try FileManager.default.url(for: .downloadsDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: false) - let scriptURL = downloadsURL.appendingPathComponent("clean-config.sh") - - guard let scriptPath = Bundle.main.path(forResource: "clean-config", ofType: "sh"), - let scriptContent = try? String(contentsOfFile: scriptPath, encoding: .utf8) else { - throw NSError(domain: "ScriptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法读取脚本文件"]) - } - - try scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8) - - try FileManager.default.setAttributes([.posixPermissions: 0o755], - ofItemAtPath: scriptURL.path) - - if PrivilegedHelperManager.getHelperStatus { - PrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in - if output.starts(with: "Error") { - alertMessage = "清空配置失败: \(output)" - showAlert = true - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - exit(0) - } - } - } - } else { - let terminalURL = URL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app") - NSWorkspace.shared.open([scriptURL], - withApplicationAt: terminalURL, - configuration: NSWorkspace.OpenConfiguration()) { _, error in - if let error = error { - alertMessage = "打开终端失败: \(error.localizedDescription)" - showAlert = true - return - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - exit(0) - } - } - } - - } catch { - alertMessage = "清空配置失败: \(error.localizedDescription)" - showAlert = true - } - } -} - private class PreviewUpdater: SPUUpdater { init() { let hostBundle = Bundle.main @@ -1034,13 +884,15 @@ struct HelperStatusRow: View { Text("连接状态: ") .font(.system(size: 14, weight: .medium)) - HStack(spacing: 5) { + HStack(alignment: .center, spacing: 5) { PulsingCircle(color: helperStatusColor) - .frame(width: 12, height: 12) + .layoutPriority(1) + Text(helperStatusText) .font(.system(size: 14)) .foregroundColor(helperStatusColor) } + .fixedSize(horizontal: false, vertical: true) .padding(.vertical, 4) .padding(.horizontal, 8) .background(helperStatusBackgroundColor) @@ -1413,995 +1265,3 @@ struct AutoDownloadRow: View { } } } - -struct QAView: View { - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - Group { - QAItem( - question: String(localized: "为什么需要安装 Helper?"), - answer: String(localized: "Helper 是一个具有管理员权限的辅助工具,用于执行需要管理员权限的操作,如修改系统文件等。没有 Helper 将无法正常使用软件的某些功能。") - ) - - QAItem( - question: String(localized: "为什么需要下载 Setup 组件?"), - answer: String(localized: "Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。") - ) - - QAItem( - question: String(localized: "为什么有时候下载会失败?"), - answer: String(localized: "下载失败可能有多种原因:\n1. 网络连接不稳定\n2. Adobe 服务器响应超时\n3. 本地磁盘空间不足\n建议您检查网络连接并重试,如果问题持续存在,可以尝试使用代理或 VPN。") - ) - - QAItem( - question: String(localized: "如何修复安装失败的问题?"), - answer: String(localized: "如果安装失败,您可以尝试以下步骤:\n1. 确保已正确安装并连接 Helper\n2. 确保已下载并处理 Setup 组件\n3. 检查磁盘剩余空间是否充足\n4. 尝试重新下载并安装\n如果问题仍然存在,可以尝试重新安装 Helper 和重新处理 Setup 组件。") - ) - } - } - .padding() - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -struct QAItem: View { - let question: String - let answer: String - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(question) - .font(.headline) - .foregroundColor(.primary) - - Text(answer) - .font(.body) - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) - - Divider() - } - } -} - -struct CleanupLog: Identifiable { - let id = UUID() - let timestamp: Date - let command: String - let status: LogStatus - let message: String - - enum LogStatus { - case running - case success - case error - case cancelled - } - - static func getCleanupDescription(for command: String) -> String { - if command.contains("Library/Logs") || command.contains("DiagnosticReports") { - if command.contains("Adobe Creative Cloud") { - return String(localized: "正在清理 Creative Cloud 日志文件...") - } else if command.contains("CrashReporter") { - return String(localized: "正在清理崩溃报告日志...") - } else { - return String(localized: "正在清理应用程序日志文件...") - } - } else if command.contains("Library/Caches") { - return String(localized: "正在清理缓存文件...") - } else if command.contains("Library/Preferences") { - return String(localized: "正在清理偏好设置文件...") - } else if command.contains("Applications") { - if command.contains("Creative Cloud") { - return String(localized: "正在清理 Creative Cloud 应用...") - } else { - return String(localized: "正在清理 Adobe 应用程序...") - } - } else if command.contains("LaunchAgents") || command.contains("LaunchDaemons") { - return String(localized: "正在清理启动项服务...") - } else if command.contains("security") { - return String(localized: "正在清理钥匙串数据...") - } else if command.contains("AdobeGenuineClient") || command.contains("AdobeGCClient") { - return String(localized: "正在清理正版验证服务...") - } else if command.contains("hosts") { - return String(localized: "正在清理 hosts 文件...") - } else if command.contains("kill") { - return String(localized: "正在停止 Adobe 相关进程...") - } else if command.contains("receipts") { - return String(localized: "正在清理安装记录...") - } else { - return String(localized: "正在清理其他文件...") - } - } -} - -struct CleanupView: View { - @State private var showConfirmation = false - @State private var showAlert = false - @State private var alertMessage = "" - @State private var selectedOptions = Set() - @State private var isProcessing = false - @State private var cleanupLogs: [CleanupLog] = [] - @State private var currentCommandIndex = 0 - @State private var totalCommands = 0 - @State private var expandedOptions = Set() - @State private var isCancelled = false - @State private var isLogExpanded = false - - enum CleanupOption: String, CaseIterable, Identifiable { - case adobeApps = "Adobe 应用程序" - case adobeCreativeCloud = "Adobe Creative Cloud" - case adobePreferences = "Adobe 偏好设置" - case adobeCaches = "Adobe 缓存文件" - case adobeLicenses = "Adobe 许可文件" - case adobeLogs = "Adobe 日志文件" - case adobeServices = "Adobe 服务" - case adobeKeychain = "Adobe 钥匙串" - case adobeGenuineService = "Adobe 正版验证服务" - case adobeHosts = "Adobe Hosts" - - var id: String { self.rawValue } - - var localizedName: String { - switch self { - case .adobeApps: - return String(localized: "Adobe 应用程序") - case .adobeCreativeCloud: - return String(localized: "Adobe Creative Cloud") - case .adobePreferences: - return String(localized: "Adobe 偏好设置") - case .adobeCaches: - return String(localized: "Adobe 缓存文件") - case .adobeLicenses: - return String(localized: "Adobe 许可文件") - case .adobeLogs: - return String(localized: "Adobe 日志文件") - case .adobeServices: - return String(localized: "Adobe 服务") - case .adobeKeychain: - return String(localized: "Adobe 钥匙串") - case .adobeGenuineService: - return String(localized: "Adobe 正版验证服务") - case .adobeHosts: - return String(localized: "Adobe Hosts") - } - } - - var commands: [String] { - switch self { - case .adobeApps: - return [ - "sudo find /Applications -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo find /Applications/Utilities -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo rm -rf /Applications/Adobe Creative Cloud", - "sudo rm -rf /Applications/Utilities/Adobe Creative Cloud", - "sudo rm -rf /Applications/Utilities/Adobe Creative Cloud Experience", - "sudo rm -rf /Applications/Utilities/Adobe Installers/Uninstall Adobe Creative Cloud", - "sudo rm -rf /Applications/Utilities/Adobe Sync", - "sudo rm -rf /Applications/Utilities/Adobe Genuine Service" - ] - case .adobeCreativeCloud: - return [ - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/ADBox", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/ADS", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/AppsPanel", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/CEF", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/Core", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/CoreExt", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/DEBox", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/ElevationManager", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/FilesPanel", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/FontsPanel", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/HEX", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/LCC", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/NHEX", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/Notifications", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/pim.db", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/RemoteComponents", - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/TCC", - "sudo rm -rf /Library/Application Support/Adobe/ARMNext", - "sudo rm -rf /Library/Application Support/Adobe/ARMDC/Application", - "sudo rm -rf /Library/Application Support/Adobe/PII/com.adobe.pii.prefs", - "sudo rm -rf /Library/Application Support/Adobe/ACPLocal*", - "sudo rm -rf /Library/Application Support/regid.1986-12.com.adobe", - "sudo rm -rf /Library/Internet Plug-Ins/AdobeAAMDetect.plugin", - "sudo rm -rf /Library/Internet Plug-Ins/AdobePDF*", - "sudo rm -rf /Library/PDF Services/Save as Adobe PDF*", - "sudo rm -rf /Library/ScriptingAdditions/Adobe Unit Types.osax", - "sudo rm -rf /Library/Automator/Save as Adobe PDF.action", - "sudo rm -rf ~/.adobe", - "sudo rm -rf ~/Creative Cloud Files*", - "sudo find ~/Library/Application\\ Scripts -name '*com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", - "sudo find ~/Library/Group\\ Containers -name '*com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", - "sudo rm -rf ~/Library/Application\\ Scripts/Adobe-Hub-App || true", - "sudo rm -rf ~/Library/Group\\ Containers/Adobe-Hub-App || true", - "sudo rm -rf ~/Library/Application\\ Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.adobe* || true", - "sudo find ~/Library/Application\\ Support -name 'Acrobat*' ! -path '*/Adobe Downloader/*' -print0 | xargs -0 sudo rm -rf || true", - "sudo find ~/Library/Application\\ Support -name 'Adobe*' ! -name '*Adobe Downloader*' ! -path '*/Adobe Downloader/*' -print0 | xargs -0 sudo rm -rf || true", - "sudo find ~/Library/Application\\ Support -name 'com.adobe*' ! -name '*Adobe Downloader*' ! -path '*/Adobe Downloader/*' -print0 | xargs -0 sudo rm -rf || true", - "sudo rm -rf ~/Library/Application Support/io.branch", - "sudo rm -rf ~/Library/PhotoshopCrashes", - "sudo rm -rf ~/Library/WebKit/com.adobe*" - ] - case .adobePreferences: - return [ - "sudo find /Library/Preferences -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo find ~/Library/Preferences -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo find ~/Library/Preferences -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo find ~/Library/Preferences/ByHost -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo rm -rf ~/Library/Preferences/adobe.com*", - "sudo rm -rf ~/Library/Preferences/AIRobin*", - "sudo rm -rf ~/Library/Preferences/Macromedia*", - "sudo rm -rf ~/Library/Saved Application State/com.adobe*" - ] - case .adobeCaches: - return [ - "sudo find ~/Library/Caches -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", - "sudo find ~/Library/Caches -name 'com.adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf || true", - "sudo rm -rf ~/Library/Caches/Acrobat* || true", - "sudo rm -rf ~/Library/Caches/CSXS || true", - "sudo rm -rf ~/Library/Caches/com.crashlytics.data/com.adobe* || true", - "sudo rm -rf ~/Library/Containers/com.adobe* || true", - "sudo rm -rf ~/Library/Cookies/com.adobe* || true", - "sudo find ~/Library/HTTPStorages -name '*Adobe*' ! -name '*Adobe Downloader*' ! -name '*com.x1a0he.macOS.Adobe-Downloader*' -print0 | xargs -0 sudo rm -rf || true", - "sudo find ~/Library/HTTPStorages -name 'com.adobe*' ! -name '*Adobe Downloader*' ! -name '*com.x1a0he.macOS.Adobe-Downloader*' -print0 | xargs -0 sudo rm -rf || true", - "sudo rm -rf ~/Library/HTTPStorages/Creative\\ Cloud\\ Content\\ Manager.node || true" - ] - case .adobeLicenses: - return [ - "sudo rm -rf /Library/Application Support/Adobe/Adobe PCD", - "sudo rm -rf /Library/Application Support/Adobe/AdobeGCClient", - "sudo rm -rf /Library/Application Support/regid.1986-12.com.adobe", - "sudo rm -rf /private/var/db/receipts/com.adobe*", - "sudo rm -rf /private/var/db/receipts/*Photoshop*", - "sudo rm -rf /private/var/db/receipts/*CreativeCloud*", - "sudo rm -rf /private/var/db/receipts/*CCXP*", - "sudo rm -rf /private/var/db/receipts/*mygreatcompany*", - "sudo rm -rf /private/var/db/receipts/*AntiCC*", - "sudo rm -rf /private/var/db/receipts/*.RiD.*", - "sudo rm -rf /private/var/db/receipts/*.CCRuntime.*" - ] - case .adobeLogs: - return [ - "sudo find ~/Library/Logs -name 'Adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo find ~/Library/Logs -name 'adobe*' ! -name '*Adobe Downloader*' -print0 | xargs -0 sudo rm -rf", - "sudo rm -rf ~/Library/Logs/Adobe Creative Cloud Cleaner Tool.log", - "sudo rm -rf ~/Library/Logs/CreativeCloud", - "sudo rm -rf /Library/Logs/CreativeCloud", - "sudo rm -rf ~/Library/Logs/CSXS", - "sudo rm -rf ~/Library/Logs/amt3.log", - "sudo rm -rf ~/Library/Logs/CoreSyncInstall.log", - "sudo rm -rf ~/Library/Logs/CrashReporter/*Adobe*", - "sudo rm -rf ~/Library/Logs/acroLicLog.log", - "sudo rm -rf ~/Library/Logs/acroNGLLog.txt", - "sudo rm -rf ~/Library/Logs/DiagnosticReports/*Adobe*", - "sudo rm -rf ~/Library/Logs/distNGLLog.txt", - "sudo rm -rf ~/Library/Logs/NGL*", - "sudo rm -rf ~/Library/Logs/oobelib.log", - "sudo rm -rf ~/Library/Logs/PDApp*", - "sudo rm -rf /Library/Logs/adobe*", - "sudo rm -rf /Library/Logs/Adobe*", - "sudo rm -rf ~/Library/Logs/Adobe*", - "sudo rm -rf ~/Library/Logs/adobe*", - "sudo rm -rf /Library/Logs/DiagnosticReports/*Adobe*", - "sudo rm -rf /Library/Application Support/CrashReporter/*Adobe*", - "sudo rm -rf ~/Library/Application Support/CrashReporter/*Adobe*" - ] - case .adobeServices: - return [ - "sudo launchctl bootout gui/$(id -u) /Library/LaunchAgents/com.adobe.* || true", - "sudo launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.adobe.* || true", - "sudo launchctl unload /Library/LaunchDaemons/com.adobe.* || true", - "sudo launchctl remove com.adobe.AdobeCreativeCloud || true", - "sudo launchctl remove com.adobe.AdobeGenuineService.plist || true", - "sudo ps aux | grep -i 'Adobe' | grep -v 'Adobe Downloader' | grep -v 'Adobe-Downloader.helper' | grep -v grep | awk '{print $2}' | { pids=$(cat); [ ! -z \"$pids\" ] && echo \"$pids\" | xargs sudo kill -9; } || true", - "sudo rm -rf /Library/LaunchAgents/com.adobe.*", - "sudo rm -rf /Library/LaunchDaemons/com.adobe.*", - "sudo rm -rf /Library/LaunchAgents/com.adobe.ARMDCHelper*", - "sudo rm -rf /Library/LaunchAgents/com.adobe.AdobeCreativeCloud.plist", - "sudo rm -rf /Library/LaunchAgents/com.adobe.ccxprocess.plist" - ] - case .adobeKeychain: - return [ - "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'acrobat.com' | grep -i 'srvr' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-internet-password -s \"$line\" /Library/Keychains/System.keychain; done || true", - "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'acrobat.com' | grep -i 'srvr' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-internet-password -s \"$line\" ~/Library/Keychains/login.keychain-db; done || true", - "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'Adobe.APS' | grep -v 'Adobe Downloader' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-generic-password -l \"$line\" /Library/Keychains/System.keychain; done || true", - "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'Adobe.APS' | grep -v 'Adobe Downloader' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-generic-password -l \"$line\" ~/Library/Keychains/login.keychain-db; done || true", - "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'Adobe App Info\\|Adobe App Prefetched Info\\|Adobe User\\|com.adobe\\|Adobe Lightroom' | grep -v 'Adobe Downloader' | grep -i 'svce' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-generic-password -s \"$line\" /Library/Keychains/System.keychain; done || true", - "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'Adobe App Info\\|Adobe App Prefetched Info\\|Adobe User\\|com.adobe\\|Adobe Lightroom' | grep -v 'Adobe Downloader' | grep -i 'svce' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-generic-password -s \"$line\" ~/Library/Keychains/login.keychain-db; done || true", - "sudo security dump-keychain /Library/Keychains/System.keychain | grep -i 'Adobe Content \\|Adobe Intermediate' | grep -v 'Adobe Downloader' | grep -i 'alis' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do sudo security delete-certificate -c \"$line\" /Library/Keychains/System.keychain; done || true", - "sudo security dump-keychain ~/Library/Keychains/login.keychain-db | grep -i 'Adobe Content \\|Adobe Intermediate' | grep -v 'Adobe Downloader' | grep -i 'alis' | awk -F '=' '{print $2}' | cut -d '\"' -f2 | while read -r line; do security delete-certificate -c \"$line\" ~/Library/Keychains/login.keychain-db; done || true" - ] - case .adobeGenuineService: - return [ - "sudo rm -rf /Library/Application Support/Adobe/Adobe Desktop Common/AdobeGenuineClient", - "sudo rm -rf /Library/Application Support/Adobe/AdobeGCClient", - "sudo rm -rf /Library/Preferences/com.adobe.AdobeGenuineService.plist", - "sudo rm -rf /Applications/Utilities/Adobe Creative Cloud/Utils/AdobeGenuineValidator", - "sudo rm -rf /Applications/Utilities/Adobe Genuine Service", - "sudo rm -rf /Library/PrivilegedHelperTools/com.adobe.acc*", - "sudo find /private/tmp -type d -iname '*adobe*' ! -iname '*Adobe Downloader*' -o -type f -iname '*adobe*' ! -iname '*Adobe Downloader*' | xargs rm -rf {} \\+", - "sudo find /private/tmp -type d -iname '*CCLBS*' ! -iname '*Adobe Downloader*' -o -type f -iname '*adobe*' ! -iname '*Adobe Downloader*' | xargs rm -rf {} \\+", - "sudo find /private/var/folders/ -type d -iname '*adobe*' ! -iname '*Adobe Downloader*' -o -type f -iname '*adobe*' ! -iname '*Adobe Downloader*' | xargs rm -rf {} \\+", - "sudo rm -rf /private/tmp/com.adobe*", - "sudo rm -rf /private/tmp/Adobe*", - "sudo rm -rf /private/tmp/.adobe*" - ] - case .adobeHosts: - return [ - "sudo sh -c 'grep -v \"adobe\" /etc/hosts > /etc/hosts.temp && mv /etc/hosts.temp /etc/hosts'" - ] - } - } - - var description: String { - switch self { - case .adobeApps: - return String(localized: "删除所有已安装的 Adobe 应用程序(不包括 Adobe Downloader)") - case .adobeCreativeCloud: - return String(localized: "删除 Adobe Creative Cloud 应用程序及其组件") - case .adobePreferences: - return String(localized: "删除 Adobe 应用程序的偏好设置文件(不包括 Adobe Downloader)") - case .adobeCaches: - return String(localized: "删除 Adobe 应用程序的缓存文件(不包括 Adobe Downloader)") - case .adobeLicenses: - return String(localized: "删除 Adobe 许可和激活相关文件") - case .adobeLogs: - return String(localized: "删除 Adobe 应用程序的日志文件(不包括 Adobe Downloader)") - case .adobeServices: - return String(localized: "停止并删除 Adobe 相关服务") - case .adobeKeychain: - return String(localized: "删除钥匙串中的 Adobe 相关条目") - case .adobeGenuineService: - return String(localized: "删除 Adobe 正版验证服务及其组件") - case .adobeHosts: - return String(localized: "清理 hosts 文件中的 Adobe 相关条目") - } - } - } - - var body: some View { - VStack(alignment: .leading) { - Text("选择要清理的内容") - .font(.headline) - .padding(.bottom, 4) - - Text("注意:清理过程不会影响 Adobe Downloader 的文件和下载数据") - .font(.subheadline) - .foregroundColor(.secondary) - - ScrollView(showsIndicators: false) { - VStack(alignment: .leading) { - ForEach(CleanupOption.allCases) { option in - VStack(spacing: 0) { - #if DEBUG - Button(action: { - withAnimation { - if expandedOptions.contains(option) { - expandedOptions.remove(option) - } else { - expandedOptions.insert(option) - } - } - }) { - HStack(spacing: 12) { - Toggle(isOn: Binding( - get: { selectedOptions.contains(option) }, - set: { isSelected in - if isSelected { - selectedOptions.insert(option) - } else { - selectedOptions.remove(option) - } - } - )) { - EmptyView() - } - .toggleStyle(SwitchToggleStyle(tint: .green)) - .disabled(isProcessing) - .labelsHidden() - .scaleEffect(0.85) - - VStack(alignment: .leading, spacing: 4) { - Text(option.localizedName) - .font(.system(size: 15, weight: .semibold)) - Text(option.description) - .font(.system(size: 12)) - .foregroundColor(.secondary) - .lineLimit(2) - } - - Spacer() - - Image(systemName: expandedOptions.contains(option) ? "chevron.down" : "chevron.right") - .foregroundColor(.secondary) - .font(.system(size: 14, weight: .medium)) - .frame(width: 20, height: 20) - .animation(.easeInOut, value: expandedOptions.contains(option)) - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .disabled(isProcessing) - - if expandedOptions.contains(option) { - VStack(alignment: .leading, spacing: 8) { - Text("将执行的命令:") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.secondary) - .padding(.top, 2) - .padding(.horizontal, 12) - - VStack(spacing: 6) { - ForEach(option.commands, id: \.self) { command in - Text(command) - .font(.system(size: 11, design: .monospaced)) - .foregroundColor(Color(.white)) - .padding(10) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color.black.opacity(0.85)) - .cornerRadius(6) - } - } - .padding(.horizontal, 12) - } - .padding(.bottom, 12) - .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) - } - #else - HStack(spacing: 12) { - Toggle(isOn: Binding( - get: { selectedOptions.contains(option) }, - set: { isSelected in - if isSelected { - selectedOptions.insert(option) - } else { - selectedOptions.remove(option) - } - } - )) { - EmptyView() - } - .toggleStyle(SwitchToggleStyle(tint: .green)) - .disabled(isProcessing) - .labelsHidden() - .scaleEffect(0.85) - - VStack(alignment: .leading, spacing: 4) { - Text(option.localizedName) - .font(.system(size: 15, weight: .semibold)) - Text(option.description) - .font(.system(size: 12)) - .foregroundColor(.secondary) - .lineLimit(2) - } - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - #endif - } - .background(Color(NSColor.controlBackgroundColor)) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.gray.opacity(0.2), lineWidth: 1) - ) - .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) - } - } - } - - Divider() - .padding(.vertical, 8) - - VStack(alignment: .leading, spacing: 12) { - if isProcessing { - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("清理进度:\(currentCommandIndex)/\(totalCommands)") - .font(.system(size: 12, weight: .medium)) - - Spacer() - - let percentage = totalCommands > 0 ? Int((Double(currentCommandIndex) / Double(totalCommands)) * 100) : 0 - Text("\(percentage)%") - .font(.system(size: 12, weight: .bold)) - .foregroundColor(.blue) - } - .padding(.horizontal, 2) - - GeometryReader { geometry in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 10) - .fill(Color.secondary.opacity(0.2)) - .frame(height: 12) - - let progressWidth = totalCommands > 0 ? - CGFloat(Double(currentCommandIndex) / Double(totalCommands)) * geometry.size.width : 0 - - RoundedRectangle(cornerRadius: 10) - .fill( - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.blue]), - startPoint: .leading, - endPoint: .trailing - ) - ) - .frame(width: progressWidth, height: 12) - } - } - .frame(height: 12) - .animation(.linear(duration: 0.3), value: currentCommandIndex) - - Button(action: { - isCancelled = true - }) { - Text("取消清理") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white) - } - .buttonStyle(BeautifulButtonStyle(baseColor: Color.red.opacity(0.8))) - .disabled(isCancelled) - .opacity(isCancelled ? 0.5 : 1) - } - .padding(10) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(NSColor.controlBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.blue.opacity(0.2), lineWidth: 1) - ) - - if let lastLog = cleanupLogs.last { - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 8) { - Image(systemName: "arrow.triangle.turn.up.right.circle.fill") - .foregroundColor(.blue.opacity(0.8)) - .font(.system(size: 14)) - - Text("当前执行:") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.secondary) - } - - #if DEBUG - Text(lastLog.command) - .font(.system(size: 11)) - .foregroundColor(.secondary) - .lineLimit(2) - .truncationMode(.middle) - .frame(maxWidth: .infinity, alignment: .leading) - #else - Text(CleanupLog.getCleanupDescription(for: lastLog.command)) - .font(.system(size: 11)) - .foregroundColor(.secondary) - .lineLimit(2) - .truncationMode(.middle) - .frame(maxWidth: .infinity, alignment: .leading) - #endif - } - .frame(height: 70) - .padding(.vertical, 6) - .padding(.horizontal, 8) - .frame(maxWidth: .infinity) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(Color(NSColor.textBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(Color.secondary.opacity(0.2), lineWidth: 1) - ) - } - } - - VStack(alignment: .leading, spacing: 0) { - Button(action: { - withAnimation { - isLogExpanded.toggle() - } - }) { - HStack { - Image(systemName: "terminal.fill") - .font(.system(size: 12)) - .foregroundColor(.blue.opacity(0.8)) - - Text("最近日志:") - .font(.system(size: 12, weight: .medium)) - - if isProcessing { - HStack(spacing: 4) { - Circle() - .fill(Color.green) - .frame(width: 6, height: 6) - - Text("正在执行...") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(.horizontal, 4) - .padding(.vertical, 2) - .background(Color.secondary.opacity(0.1)) - .cornerRadius(4) - } - - Spacer() - - Text(isLogExpanded ? "收起" : "展开") - .font(.system(size: 11)) - .foregroundColor(.blue) - .padding(.trailing, 4) - - Image(systemName: isLogExpanded ? "chevron.down" : "chevron.right") - .font(.system(size: 10)) - .foregroundColor(.blue) - } - .padding(.vertical, 8) - .padding(.horizontal, 10) - .background(Color(NSColor.controlBackgroundColor)) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - - ScrollView { - VStack(alignment: .leading, spacing: 8) { - if cleanupLogs.isEmpty { - HStack { - Spacer() - VStack(spacing: 6) { - Image(systemName: "doc.text.magnifyingglass") - .font(.system(size: 20)) - .foregroundColor(.secondary.opacity(0.6)) - - Text("暂无清理记录") - .font(.system(size: 11)) - .foregroundColor(.secondary) - } - .padding(.vertical, 16) - Spacer() - } - } else { - if isLogExpanded { - ForEach(cleanupLogs.reversed()) { log in - LogEntryView(log: log) - } - } else if let lastLog = cleanupLogs.last { - LogEntryView(log: lastLog) - } - } - } - .padding(.vertical, 6) - .padding(.horizontal, 2) - } - .frame(height: cleanupLogs.isEmpty ? 80 : (isLogExpanded ? 220 : 54)) - .animation(.easeInOut(duration: 0.3), value: isLogExpanded) - .background(Color(NSColor.textBackgroundColor).opacity(0.6)) - .cornerRadius(6) - .padding(.bottom, 1) - } - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(NSColor.textBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.secondary.opacity(0.2), lineWidth: 1) - ) - } - - HStack(spacing: 10) { - Group { - Button(action: { - selectedOptions = Set(CleanupOption.allCases) - }) { - Text("全选") - .frame(minWidth: 50) - } - .buttonStyle(BeautifulButtonStyle(baseColor: Color.blue.opacity(0.7))) - .foregroundColor(.white) - - Button(action: { - selectedOptions.removeAll() - }) { - Text("取消全选") - .frame(minWidth: 65) - } - .buttonStyle(BeautifulButtonStyle(baseColor: Color.gray.opacity(0.7))) - .foregroundColor(.white) - - #if DEBUG - Button(action: { - if expandedOptions.count == CleanupOption.allCases.count { - expandedOptions.removeAll() - } else { - expandedOptions = Set(CleanupOption.allCases) - } - }) { - Text(expandedOptions.count == CleanupOption.allCases.count ? "折叠全部" : "展开全部") - .frame(minWidth: 65) - } - .buttonStyle(BeautifulButtonStyle(baseColor: Color.purple.opacity(0.7))) - .foregroundColor(.white) - #endif - } - .disabled(isProcessing) - - Spacer() - - Button(action: { - if !selectedOptions.isEmpty { - showConfirmation = true - } - }) { - HStack(spacing: 6) { - Image(systemName: "trash") - Text("开始清理") - } - .frame(minWidth: 100) - } - .buttonStyle(BeautifulButtonStyle(baseColor: Color.red.opacity(0.8))) - .foregroundColor(.white) - .opacity(selectedOptions.isEmpty || isProcessing ? 0.5 : 1) - .saturation(selectedOptions.isEmpty || isProcessing ? 0.3 : 1) - .disabled(selectedOptions.isEmpty || isProcessing) - } - .padding(.top, 6) - } - .padding() - .alert("确认清理", isPresented: $showConfirmation) { - Button("取消", role: .cancel) { } - Button("确定", role: .destructive) { - cleanupSelectedItems() - } - } message: { - Text("这将删除所选的 Adobe 相关文件,该操作不可撤销。清理过程不会影响 Adobe Downloader 的文件和下载数据。是否继续?") - } - .alert(isPresented: $showAlert) { - Alert( - title: Text("清理结果"), - message: Text(alertMessage), - dismissButton: .default(Text("确定")) - ) - } - } - - private func cleanupSelectedItems() { - isProcessing = true - cleanupLogs.removeAll() - currentCommandIndex = 0 - isCancelled = false - - let userHome = NSHomeDirectory() - - var commands: [String] = [] - for option in selectedOptions { - let userCommands = option.commands.map { command in - command.replacingOccurrences(of: "~/", with: "\(userHome)/") - } - commands.append(contentsOf: userCommands) - } - - totalCommands = commands.count - - executeNextCommand(commands: commands) - } - - private func executeNextCommand(commands: [String]) { - guard currentCommandIndex < commands.count else { - DispatchQueue.main.async { - isProcessing = false - alertMessage = isCancelled ? String(localized: "清理已取消") : String(localized: "清理完成") - showAlert = true - selectedOptions.removeAll() - } - return - } - - if isCancelled { - DispatchQueue.main.async { - isProcessing = false - alertMessage = String(localized: "清理已取消") - showAlert = true - selectedOptions.removeAll() - } - return - } - - let command = commands[currentCommandIndex] - cleanupLogs.append(CleanupLog( - timestamp: Date(), - command: command, - status: .running, - message: String(localized: "正在执行...") - )) - - let timeoutTimer = DispatchSource.makeTimerSource(queue: .global()) - timeoutTimer.schedule(deadline: .now() + 30) - timeoutTimer.setEventHandler { [self] in - if let index = cleanupLogs.lastIndex(where: { $0.command == command }) { - DispatchQueue.main.async { - cleanupLogs[index] = CleanupLog( - timestamp: Date(), - command: command, - status: .error, - message: String(localized: "执行结果:执行超时\n执行命令:\(command)") - ) - currentCommandIndex += 1 - executeNextCommand(commands: commands) - } - } - } - timeoutTimer.resume() - - PrivilegedHelperManager.shared.executeCommand(command) { [self] output in - timeoutTimer.cancel() - DispatchQueue.main.async { - if let index = cleanupLogs.lastIndex(where: { $0.command == command }) { - if isCancelled { - cleanupLogs[index] = CleanupLog( - timestamp: Date(), - command: command, - status: .cancelled, - message: String(localized: "已取消") - ) - } else { - let isSuccess = output.isEmpty || output.lowercased() == "success" - let message = if isSuccess { - String(localized: "执行成功") - } else { - String(localized: "执行结果:\(output)\n执行命令:\(command)") - } - cleanupLogs[index] = CleanupLog( - timestamp: Date(), - command: command, - status: isSuccess ? .success : .error, - message: message - ) - } - } - currentCommandIndex += 1 - executeNextCommand(commands: commands) - } - } - } - - private func statusIcon(for status: CleanupLog.LogStatus) -> String { - switch status { - case .running: - return "arrow.triangle.2.circlepath" - case .success: - return "checkmark.circle.fill" - case .error: - return "exclamationmark.circle.fill" - case .cancelled: - return "xmark.circle.fill" - } - } - - private func statusColor(for status: CleanupLog.LogStatus) -> Color { - switch status { - case .running: - return .blue - case .success: - return .green - case .error: - return .red - case .cancelled: - return .orange - } - } - - private func timeString(from date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss" - return formatter.string(from: date) - } -} - -struct LogEntryView: View { - let log: CleanupLog - @State private var showCopyButton = false - - var body: some View { - HStack { - Image(systemName: statusIcon(for: log.status)) - .foregroundColor(statusColor(for: log.status)) - - Text(timeString(from: log.timestamp)) - .font(.system(size: 11)) - .foregroundColor(.secondary) - - #if DEBUG - Text(log.command) - .font(.system(size: 11, design: .monospaced)) - .lineLimit(1) - .truncationMode(.middle) - #else - Text(CleanupLog.getCleanupDescription(for: log.command)) - .font(.system(size: 11)) - .lineLimit(1) - .truncationMode(.middle) - #endif - - Spacer() - - if log.status == .error && !log.message.isEmpty { - HStack(spacing: 4) { - Text(truncatedErrorMessage(log.message)) - .font(.system(size: 11)) - .foregroundColor(.secondary) - - Button(action: { - copyToClipboard(log.message) - }) { - Image(systemName: "doc.on.doc") - .font(.system(size: 11)) - } - .buttonStyle(.plain) - .help("复制完整错误信息") - } - } else { - Text(log.message) - .font(.system(size: 11)) - .foregroundColor(.secondary) - } - } - .padding(.vertical, 4) - .padding(.horizontal, 8) - } - - private func truncatedErrorMessage(_ message: String) -> String { - if message.hasPrefix("执行失败:") { - let errorMessage = String(message.dropFirst(5)) - if errorMessage.count > 30 { - return "执行失败:" + errorMessage.prefix(30) + "..." - } - } - return message - } - - private func copyToClipboard(_ message: String) { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(message, forType: .string) - } - - private func statusIcon(for status: CleanupLog.LogStatus) -> String { - switch status { - case .running: - return "arrow.triangle.2.circlepath" - case .success: - return "checkmark.circle.fill" - case .error: - return "exclamationmark.circle.fill" - case .cancelled: - return "xmark.circle.fill" - } - } - - private func statusColor(for status: CleanupLog.LogStatus) -> Color { - switch status { - case .running: - return .blue - case .success: - return .green - case .error: - return .red - case .cancelled: - return .orange - } - } - - private func timeString(from date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss" - return formatter.string(from: date) - } -} diff --git a/Adobe Downloader/Views/AppCardView.swift b/Adobe Downloader/Views/AppCardView.swift index d1146b9..99a2283 100644 --- a/Adobe Downloader/Views/AppCardView.swift +++ b/Adobe Downloader/Views/AppCardView.swift @@ -164,6 +164,8 @@ final class AppCardViewModel: ObservableObject { } func loadIcon() { + if iconImage != nil { return } + if let bestIcon = globalProducts.first(where: { $0.id == uniqueProduct.id })?.getBestIcon(), let iconURL = URL(string: bestIcon.value) { if let cachedImage = IconCache.shared.getIcon(for: bestIcon.value) { @@ -308,6 +310,7 @@ struct AppCardView: View { Spacer() DownloadButtonView(viewModel: viewModel) } + .drawingGroup() } .background( RoundedRectangle(cornerRadius: 12) diff --git a/Adobe Downloader/Views/CleanConfigView.swift b/Adobe Downloader/Views/CleanConfigView.swift new file mode 100644 index 0000000..e4bddf1 --- /dev/null +++ b/Adobe Downloader/Views/CleanConfigView.swift @@ -0,0 +1,190 @@ +// +// CleanConfigView.swift +// Adobe Downloader +// +// Created by X1a0He on 3/28/25. +// +import SwiftUI + +struct CleanConfigView: View { + @State private var showConfirmation = false + @State private var showAlert = false + @State private var alertMessage = "" + @State private var chipInfo: String = "" + + private func getChipInfo() -> String { + var size = 0 + sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0) + var machine = [CChar](repeating: 0, count: size) + sysctlbyname("machdep.cpu.brand_string", &machine, &size, nil, 0) + let chipName = String(cString: machine) + + if chipName.contains("Apple") { + return chipName + } else { + return chipName.components(separatedBy: "@")[0].trimmingCharacters(in: .whitespaces) + } + } + + var body: some View { + HStack(spacing: 16) { + BeautifulGroupBox(label: { + Text("重置程序") + }) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Button("重置程序") { + showConfirmation = true + } + .buttonStyle(BeautifulButtonStyle(baseColor: .red.opacity(0.8))) + .foregroundColor(.white) + } + .fixedSize(horizontal: false, vertical: true) + } + } + + BeautifulGroupBox(label: { + Text("系统信息") + }) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Image(systemName: "desktopcomputer") + .foregroundColor(.blue) + .imageScale(.medium) + .frame(width: 22, height: 22) + .background(Circle().fill(Color.blue.opacity(0.1)).frame(width: 28, height: 28)) + + VStack(alignment: .leading, spacing: 1) { + Text("macOS \(ProcessInfo.processInfo.operatingSystemVersionString)") + .fontWeight(.medium) + + Text("\(chipInfo.isEmpty ? "加载中..." : chipInfo)") + .foregroundColor(.secondary) + .font(.system(size: 12)) + } + Spacer() + } + } + } + } + .alert("确认重置程序", isPresented: $showConfirmation) { + Button("取消", role: .cancel) { } + Button("确定", role: .destructive) { + cleanConfig() + } + } message: { + Text("这将清空所有配置并结束应用程序,确定要继续吗?") + } + .alert("操作结果", isPresented: $showAlert) { + Button("确定") { } + } message: { + Text(alertMessage) + } + .onAppear { + chipInfo = getChipInfo() + } + } + + private func cleanConfig() { + do { + let downloadsURL = try FileManager.default.url(for: .downloadsDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false) + let scriptURL = downloadsURL.appendingPathComponent("clean-config.sh") + + guard let scriptPath = Bundle.main.path(forResource: "clean-config", ofType: "sh"), + let scriptContent = try? String(contentsOfFile: scriptPath, encoding: .utf8) else { + throw NSError(domain: "ScriptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法读取脚本文件"]) + } + + try scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8) + + try FileManager.default.setAttributes([.posixPermissions: 0o755], + ofItemAtPath: scriptURL.path) + + if PrivilegedHelperManager.getHelperStatus { + PrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in + if output.starts(with: "Error") { + alertMessage = "清空配置失败: \(output)" + showAlert = true + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + exit(0) + } + } + } + } else { + let terminalURL = URL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app") + NSWorkspace.shared.open([scriptURL], + withApplicationAt: terminalURL, + configuration: NSWorkspace.OpenConfiguration()) { _, error in + if let error = error { + alertMessage = "打开终端失败: \(error.localizedDescription)" + showAlert = true + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + exit(0) + } + } + } + + } catch { + alertMessage = "清空配置失败: \(error.localizedDescription)" + showAlert = true + } + } +} + +struct CleanupLog: Identifiable { + let id = UUID() + let timestamp: Date + let command: String + let status: LogStatus + let message: String + + enum LogStatus { + case running + case success + case error + case cancelled + } + + static func getCleanupDescription(for command: String) -> String { + if command.contains("Library/Logs") || command.contains("DiagnosticReports") { + if command.contains("Adobe Creative Cloud") { + return String(localized: "正在清理 Creative Cloud 日志文件...") + } else if command.contains("CrashReporter") { + return String(localized: "正在清理崩溃报告日志...") + } else { + return String(localized: "正在清理应用程序日志文件...") + } + } else if command.contains("Library/Caches") { + return String(localized: "正在清理缓存文件...") + } else if command.contains("Library/Preferences") { + return String(localized: "正在清理偏好设置文件...") + } else if command.contains("Applications") { + if command.contains("Creative Cloud") { + return String(localized: "正在清理 Creative Cloud 应用...") + } else { + return String(localized: "正在清理 Adobe 应用程序...") + } + } else if command.contains("LaunchAgents") || command.contains("LaunchDaemons") { + return String(localized: "正在清理启动项服务...") + } else if command.contains("security") { + return String(localized: "正在清理钥匙串数据...") + } else if command.contains("AdobeGenuineClient") || command.contains("AdobeGCClient") { + return String(localized: "正在清理正版验证服务...") + } else if command.contains("hosts") { + return String(localized: "正在清理 hosts 文件...") + } else if command.contains("kill") { + return String(localized: "正在停止 Adobe 相关进程...") + } else if command.contains("receipts") { + return String(localized: "正在清理安装记录...") + } else { + return String(localized: "正在清理其他文件...") + } + } +} diff --git a/Adobe Downloader/Views/CleanupView.swift b/Adobe Downloader/Views/CleanupView.swift new file mode 100644 index 0000000..fc72ef1 --- /dev/null +++ b/Adobe Downloader/Views/CleanupView.swift @@ -0,0 +1,645 @@ +// +// CleanupView.swift +// Adobe Downloader +// +// Created by X1a0He on 3/28/25. +// +import SwiftUI + +struct CleanupView: View { + @State private var showConfirmation = false + @State private var showAlert = false + @State private var alertMessage = "" + @State private var selectedOptions = Set() + @State private var isProcessing = false + @State private var cleanupLogs: [CleanupLog] = [] + @State private var currentCommandIndex = 0 + @State private var totalCommands = 0 + @State private var expandedOptions = Set() + @State private var isCancelled = false + @State private var isLogExpanded = false + + private var percentage: Int { + totalCommands > 0 ? Int((Double(currentCommandIndex) / Double(totalCommands)) * 100) : 0 + } + + private func calculateProgressWidth(_ width: CGFloat) -> CGFloat { + if totalCommands <= 0 { + return 0 + } + let progress = Double(currentCommandIndex) / Double(totalCommands) + let clampedProgress = min(1.0, max(0.0, progress)) + return width * CGFloat(clampedProgress) + } + + var body: some View { + VStack(alignment: .leading) { + Text("选择要清理的内容") + .font(.headline) + .padding(.bottom, 4) + + Text("注意:清理过程不会影响 Adobe Downloader 的文件和下载数据") + .font(.subheadline) + .foregroundColor(.secondary) + + ScrollView(showsIndicators: false) { + LazyVStack(alignment: .leading) { + ForEach(CleanupOption.allCases) { option in + CleanupOptionView( + option: option, + isProcessing: isProcessing, + selectedOptions: $selectedOptions, + expandedOptions: $expandedOptions + ) + .id(option.id) + } + } + } + + Divider() + .padding(.vertical, 8) + + VStack(alignment: .leading, spacing: 12) { + if isProcessing { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("清理进度:\(currentCommandIndex)/\(totalCommands)") + .font(.system(size: 12, weight: .medium)) + + Spacer() + + Text("\(percentage)%") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(.blue) + } + .padding(.horizontal, 2) + + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 10) + .fill(Color.secondary.opacity(0.2)) + .frame(height: 12) + + RoundedRectangle(cornerRadius: 10) + .fill( + LinearGradient( + gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.blue]), + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: calculateProgressWidth(geometry.size.width), height: 12) + } + } + .frame(height: 12) + .animation(.linear(duration: 0.3), value: currentCommandIndex) + + Button(action: { + isCancelled = true + }) { + Text("取消清理") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.white) + } + .buttonStyle(BeautifulButtonStyle(baseColor: Color.red.opacity(0.8))) + .disabled(isCancelled) + .opacity(isCancelled ? 0.5 : 1) + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(NSColor.controlBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.blue.opacity(0.2), lineWidth: 1) + ) + + if let lastLog = cleanupLogs.last { + CurrentLogView(lastLog: lastLog) + } + } + + VStack(alignment: .leading, spacing: 0) { + Button(action: { + withAnimation { + isLogExpanded.toggle() + } + }) { + HStack { + Image(systemName: "terminal.fill") + .font(.system(size: 12)) + .foregroundColor(.blue.opacity(0.8)) + + Text("最近日志:") + .font(.system(size: 12, weight: .medium)) + + if isProcessing { + HStack(spacing: 4) { + Circle() + .fill(Color.green) + .frame(width: 6, height: 6) + + Text("正在执行...") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background(Color.secondary.opacity(0.1)) + .cornerRadius(4) + } + + Spacer() + + Text(isLogExpanded ? "收起" : "展开") + .font(.system(size: 11)) + .foregroundColor(.blue) + .padding(.trailing, 4) + + Image(systemName: isLogExpanded ? "chevron.down" : "chevron.right") + .font(.system(size: 10)) + .foregroundColor(.blue) + } + .padding(.vertical, 8) + .padding(.horizontal, 10) + .background(Color(NSColor.controlBackgroundColor)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + + ScrollView { + if cleanupLogs.isEmpty { + EmptyLogView() + } else { + LogContentView( + logs: cleanupLogs, + isExpanded: isLogExpanded + ) + } + } + .frame(height: cleanupLogs.isEmpty ? 80 : (isLogExpanded ? 220 : 54)) + .animation(.easeInOut(duration: 0.3), value: isLogExpanded) + .background(Color(NSColor.textBackgroundColor).opacity(0.6)) + .cornerRadius(6) + .padding(.bottom, 1) + } + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(NSColor.textBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.2), lineWidth: 1) + ) + } + + HStack(spacing: 10) { + Group { + Button(action: { + selectedOptions = Set(CleanupOption.allCases) + }) { + Text("全选") + .frame(minWidth: 50) + } + .buttonStyle(BeautifulButtonStyle(baseColor: Color.blue.opacity(0.7))) + .foregroundColor(.white) + + Button(action: { + selectedOptions.removeAll() + }) { + Text("取消全选") + .frame(minWidth: 65) + } + .buttonStyle(BeautifulButtonStyle(baseColor: Color.gray.opacity(0.7))) + .foregroundColor(.white) + + #if DEBUG + Button(action: { + if expandedOptions.count == CleanupOption.allCases.count { + expandedOptions.removeAll() + } else { + expandedOptions = Set(CleanupOption.allCases) + } + }) { + Text(expandedOptions.count == CleanupOption.allCases.count ? "折叠全部" : "展开全部") + .frame(minWidth: 65) + } + .buttonStyle(BeautifulButtonStyle(baseColor: Color.purple.opacity(0.7))) + .foregroundColor(.white) + #endif + } + .disabled(isProcessing) + + Spacer() + + Button(action: { + if !selectedOptions.isEmpty { + showConfirmation = true + } + }) { + HStack(spacing: 6) { + Image(systemName: "trash") + Text("开始清理") + } + .frame(minWidth: 100) + } + .buttonStyle(BeautifulButtonStyle(baseColor: Color.red.opacity(0.8))) + .foregroundColor(.white) + .opacity(selectedOptions.isEmpty || isProcessing ? 0.5 : 1) + .saturation(selectedOptions.isEmpty || isProcessing ? 0.3 : 1) + .disabled(selectedOptions.isEmpty || isProcessing) + } + .padding(.top, 6) + } + .padding() + .alert("确认清理", isPresented: $showConfirmation) { + Button("取消", role: .cancel) { } + Button("确定", role: .destructive) { + cleanupSelectedItems() + } + } message: { + Text("这将删除所选的 Adobe 相关文件,该操作不可撤销。清理过程不会影响 Adobe Downloader 的文件和下载数据。是否继续?") + } + .alert(isPresented: $showAlert) { + Alert( + title: Text("清理结果"), + message: Text(alertMessage), + dismissButton: .default(Text("确定")) + ) + } + } + + private func cleanupSelectedItems() { + isProcessing = true + cleanupLogs.removeAll() + currentCommandIndex = 0 + isCancelled = false + + let userHome = NSHomeDirectory() + + var commands: [String] = [] + for option in selectedOptions { + let userCommands = option.commands.map { command in + command.replacingOccurrences(of: "~/", with: "\(userHome)/") + } + commands.append(contentsOf: userCommands) + } + + totalCommands = commands.count + + executeNextCommand(commands: commands) + } + + private func executeNextCommand(commands: [String]) { + guard currentCommandIndex < commands.count else { + DispatchQueue.main.async { + isProcessing = false + alertMessage = isCancelled ? String(localized: "清理已取消") : String(localized: "清理完成") + showAlert = true + selectedOptions.removeAll() + } + return + } + + if isCancelled { + DispatchQueue.main.async { + isProcessing = false + alertMessage = String(localized: "清理已取消") + showAlert = true + selectedOptions.removeAll() + } + return + } + + let command = commands[currentCommandIndex] + cleanupLogs.append(CleanupLog( + timestamp: Date(), + command: command, + status: .running, + message: String(localized: "正在执行...") + )) + + let timeoutTimer = DispatchSource.makeTimerSource(queue: .global()) + timeoutTimer.schedule(deadline: .now() + 30) + timeoutTimer.setEventHandler { [self] in + if let index = cleanupLogs.lastIndex(where: { $0.command == command }) { + DispatchQueue.main.async { + cleanupLogs[index] = CleanupLog( + timestamp: Date(), + command: command, + status: .error, + message: String(localized: "执行结果:执行超时\n执行命令:\(command)") + ) + currentCommandIndex += 1 + executeNextCommand(commands: commands) + } + } + } + timeoutTimer.resume() + + PrivilegedHelperManager.shared.executeCommand(command) { [self] output in + timeoutTimer.cancel() + DispatchQueue.main.async { + if let index = cleanupLogs.lastIndex(where: { $0.command == command }) { + if isCancelled { + cleanupLogs[index] = CleanupLog( + timestamp: Date(), + command: command, + status: .cancelled, + message: String(localized: "已取消") + ) + } else { + let isSuccess = output.isEmpty || output.lowercased() == "success" + let message = if isSuccess { + String(localized: "执行成功") + } else { + String(localized: "执行结果:\(output)\n执行命令:\(command)") + } + cleanupLogs[index] = CleanupLog( + timestamp: Date(), + command: command, + status: isSuccess ? .success : .error, + message: message + ) + } + } + currentCommandIndex += 1 + executeNextCommand(commands: commands) + } + } + } + + private func statusIcon(for status: CleanupLog.LogStatus) -> String { + switch status { + case .running: + return "arrow.triangle.2.circlepath" + case .success: + return "checkmark.circle.fill" + case .error: + return "exclamationmark.circle.fill" + case .cancelled: + return "xmark.circle.fill" + } + } + + private func statusColor(for status: CleanupLog.LogStatus) -> Color { + switch status { + case .running: + return .blue + case .success: + return .green + case .error: + return .red + case .cancelled: + return .orange + } + } + + private func timeString(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } +} + +struct CleanupOptionView: View { + let option: CleanupOption + let isProcessing: Bool + @Binding var selectedOptions: Set + @Binding var expandedOptions: Set + + private var isExpanded: Bool { + expandedOptions.contains(option) + } + + private var isSelected: Bool { + selectedOptions.contains(option) + } + + var body: some View { + VStack(spacing: 0) { + #if DEBUG + Button(action: { + let animation = Animation.easeInOut(duration: 0.2) + withAnimation(animation) { + if expandedOptions.contains(option) { + expandedOptions.remove(option) + } else { + expandedOptions.insert(option) + } + } + }) { + HStack(spacing: 12) { + Toggle(isOn: Binding( + get: { isSelected }, + set: { isSelected in + if isSelected { + selectedOptions.insert(option) + } else { + selectedOptions.remove(option) + } + } + )) { + EmptyView() + } + .toggleStyle(SwitchToggleStyle(tint: .green)) + .disabled(isProcessing) + .labelsHidden() + .scaleEffect(0.85) + + VStack(alignment: .leading, spacing: 4) { + Text(option.localizedName) + .font(.system(size: 15, weight: .semibold)) + Text(option.description) + .font(.system(size: 12)) + .foregroundColor(.secondary) + .lineLimit(2) + } + + Spacer() + + Image(systemName: isExpanded ? "chevron.down" : "chevron.right") + .foregroundColor(.secondary) + .font(.system(size: 14, weight: .medium)) + .frame(width: 20, height: 20) + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .disabled(isProcessing) + + if isExpanded { + CommandListView(option: option) + } + #else + HStack(spacing: 12) { + Toggle(isOn: Binding( + get: { selectedOptions.contains(option) }, + set: { isSelected in + if isSelected { + selectedOptions.insert(option) + } else { + selectedOptions.remove(option) + } + } + )) { + EmptyView() + } + .toggleStyle(SwitchToggleStyle(tint: .green)) + .disabled(isProcessing) + .labelsHidden() + .scaleEffect(0.85) + + VStack(alignment: .leading, spacing: 4) { + Text(option.localizedName) + .font(.system(size: 15, weight: .semibold)) + Text(option.description) + .font(.system(size: 12)) + .foregroundColor(.secondary) + .lineLimit(2) + } + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + #endif + } + .background(Color(NSColor.controlBackgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray.opacity(0.2), lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) + } +} + +struct CommandListView: View { + let option: CleanupOption + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("将执行的命令:") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.secondary) + .padding(.top, 2) + .padding(.horizontal, 12) + + LazyVStack(spacing: 6) { + ForEach(option.commands, id: \.self) { command in + Text(command) + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(Color(.white)) + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.black.opacity(0.85)) + .cornerRadius(6) + } + } + .padding(.horizontal, 12) + } + .padding(.bottom, 12) + .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) + } +} + +struct CurrentLogView: View { + let lastLog: CleanupLog + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Image(systemName: "arrow.triangle.turn.up.right.circle.fill") + .foregroundColor(.blue.opacity(0.8)) + .font(.system(size: 14)) + + Text("当前执行:") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.secondary) + } + + #if DEBUG + Text(lastLog.command) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .lineLimit(2) + .truncationMode(.middle) + .frame(maxWidth: .infinity, alignment: .leading) + #else + Text(CleanupLog.getCleanupDescription(for: lastLog.command)) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .lineLimit(2) + .truncationMode(.middle) + .frame(maxWidth: .infinity, alignment: .leading) + #endif + } + .frame(height: 70) + .padding(.vertical, 6) + .padding(.horizontal, 8) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color(NSColor.textBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color.secondary.opacity(0.2), lineWidth: 1) + ) + } +} + +struct EmptyLogView: View { + var body: some View { + HStack { + Spacer() + VStack(spacing: 6) { + Image(systemName: "doc.text.magnifyingglass") + .font(.system(size: 20)) + .foregroundColor(.secondary.opacity(0.6)) + + Text("暂无清理记录") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + .padding(.vertical, 16) + Spacer() + } + } +} + +struct LogContentView: View { + let logs: [CleanupLog] + let isExpanded: Bool + + var body: some View { + ScrollViewReader { scrollProxy in + VStack(alignment: .leading, spacing: 8) { + if isExpanded { + LazyVStack(alignment: .leading, spacing: 8) { + ForEach(logs.reversed()) { log in + LogEntryView(log: log) + .id(log.id) + } + } + } else if let lastLog = logs.last { + LogEntryView(log: lastLog) + .id(lastLog.id) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 2) + .onChange(of: logs.count) { newCount in + if let lastLog = logs.last { + withAnimation { + scrollProxy.scrollTo(lastLog.id, anchor: .bottom) + } + } + } + } + } +} \ No newline at end of file diff --git a/Adobe Downloader/Views/DownloadManagerView.swift b/Adobe Downloader/Views/DownloadManagerView.swift index bc29060..3aad672 100644 --- a/Adobe Downloader/Views/DownloadManagerView.swift +++ b/Adobe Downloader/Views/DownloadManagerView.swift @@ -94,6 +94,7 @@ private struct DownloadManagerToolbar: View { private struct ToolbarButtons: View { let dismiss: DismissAction + @State private var showClearCompletedConfirmation = false var body: some View { HStack(spacing: 12) { @@ -109,8 +110,8 @@ private struct ToolbarButtons: View { } } }) { - Label("全部暂停", systemImage: "pause.circle.fill") - .font(.system(size: 13, weight: .medium)) + Image(systemName: "pause.circle.fill") + .font(.system(size: 18)) .foregroundColor(.white) } .buttonStyle(BeautifulButtonStyle(baseColor: .orange)) @@ -124,38 +125,49 @@ private struct ToolbarButtons: View { } } }) { - Label("全部继续", systemImage: "play.circle.fill") - .font(.system(size: 13, weight: .medium)) + Image(systemName: "play.circle.fill") + .font(.system(size: 18)) .foregroundColor(.white) } .buttonStyle(BeautifulButtonStyle(baseColor: .blue)) Button(action: { - globalNetworkManager.downloadTasks.removeAll { task in - if case .completed = task.status { - return true - } - if case .failed = task.status { - return true - } - return false - } - globalNetworkManager.updateDockBadge() + showClearCompletedConfirmation = true }) { - Label("清理已完成", systemImage: "trash.circle.fill") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(.white) - } - .buttonStyle(BeautifulButtonStyle(baseColor: .green)) - - Button(action: { dismiss() }) { - Label("关闭", systemImage: "xmark.circle.fill") - .font(.system(size: 13, weight: .medium)) + Image(systemName: "trash.circle.fill") + .font(.system(size: 18)) .foregroundColor(.white) } .buttonStyle(BeautifulButtonStyle(baseColor: .red)) + + Button(action: { dismiss() }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 18)) + .foregroundColor(.white) + } + .buttonStyle(BeautifulButtonStyle(baseColor: .gray)) } .background(Color(NSColor.clear)) + .alert("确认删除", isPresented: $showClearCompletedConfirmation) { + Button("取消", role: .cancel) { } + Button("确认", role: .destructive) { + Task { + let tasksToRemove = globalNetworkManager.downloadTasks.filter { task in + if case .completed = task.status { return true } + if case .failed = task.status { return true } + return false + } + + for task in tasksToRemove { + globalNetworkManager.removeTask(taskId: task.id, removeFiles: true) + } + + globalNetworkManager.updateDockBadge() + } + } + } message: { + Text("确定要删除所有已完成和失败的下载任务吗?此操作将同时删除本地文件。") + } } } diff --git a/Adobe Downloader/Views/DownloadProgressView.swift b/Adobe Downloader/Views/DownloadProgressView.swift index 366ad64..9e1bf2b 100644 --- a/Adobe Downloader/Views/DownloadProgressView.swift +++ b/Adobe Downloader/Views/DownloadProgressView.swift @@ -138,7 +138,7 @@ struct DownloadProgressView: View { showSetupProcessAlert = true } }) { - Label("安装", systemImage: "square.and.arrow.down.on.square") + Label("安装", systemImage: "tray.and.arrow.down") .font(.system(size: 13, weight: .medium)) .foregroundColor(.white) } @@ -168,7 +168,7 @@ struct DownloadProgressView: View { showSetupProcessAlert = true } }) { - Label("安装", systemImage: "square.and.arrow.down.on.square") + Label("安装", systemImage: "tray.and.arrow.down") .font(.system(size: 13, weight: .medium)) .foregroundColor(.white) } diff --git a/Adobe Downloader/Views/LogEntryView.swift b/Adobe Downloader/Views/LogEntryView.swift new file mode 100644 index 0000000..7c467f2 --- /dev/null +++ b/Adobe Downloader/Views/LogEntryView.swift @@ -0,0 +1,127 @@ +// +// LogEntryView.swift +// Adobe Downloader +// +// Created by X1a0He on 3/28/25. +// +import SwiftUI + +struct LogEntryView: View { + let log: CleanupLog + @State private var showCopyButton = false + + private var statusIconName: String { + statusIcon(for: log.status) + } + + private var statusColorValue: Color { + statusColor(for: log.status) + } + + private var timeFormatted: String { + timeString(from: log.timestamp) + } + + private var displayText: String { + #if DEBUG + return log.command + #else + return CleanupLog.getCleanupDescription(for: log.command) + #endif + } + + private var errorDisplayText: String? { + if log.status == .error && !log.message.isEmpty { + return truncatedErrorMessage(log.message) + } + return nil + } + + var body: some View { + HStack { + Image(systemName: statusIconName) + .foregroundColor(statusColorValue) + + Text(timeFormatted) + .font(.system(size: 11)) + .foregroundColor(.secondary) + + Text(displayText) + .font(.system(size: 11, design: .monospaced)) + .lineLimit(1) + .truncationMode(.middle) + + Spacer() + + if let errorText = errorDisplayText { + HStack(spacing: 4) { + Text(errorText) + .font(.system(size: 11)) + .foregroundColor(.secondary) + + Button(action: { + copyToClipboard(log.message) + }) { + Image(systemName: "doc.on.doc") + .font(.system(size: 11)) + } + .buttonStyle(.plain) + .help("复制完整错误信息") + } + } else { + Text(log.message) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + .padding(.vertical, 4) + .padding(.horizontal, 8) + } + + private func truncatedErrorMessage(_ message: String) -> String { + if message.hasPrefix("执行失败:") { + let errorMessage = String(message.dropFirst(5)) + if errorMessage.count > 30 { + return "执行失败:" + errorMessage.prefix(30) + "..." + } + } + return message + } + + private func copyToClipboard(_ message: String) { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(message, forType: .string) + } + + private func statusIcon(for status: CleanupLog.LogStatus) -> String { + switch status { + case .running: + return "arrow.triangle.2.circlepath" + case .success: + return "checkmark.circle.fill" + case .error: + return "exclamationmark.circle.fill" + case .cancelled: + return "xmark.circle.fill" + } + } + + private func statusColor(for status: CleanupLog.LogStatus) -> Color { + switch status { + case .running: + return .blue + case .success: + return .green + case .error: + return .red + case .cancelled: + return .orange + } + } + + private func timeString(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } +} diff --git a/Adobe Downloader/Views/MainContentView.swift b/Adobe Downloader/Views/MainContentView.swift index 89efc48..df35042 100644 --- a/Adobe Downloader/Views/MainContentView.swift +++ b/Adobe Downloader/Views/MainContentView.swift @@ -72,32 +72,34 @@ struct ProductGridView: View { let products: [UniqueProduct] var body: some View { - ScrollView(showsIndicators: false) { - LazyVGrid( - columns: [ - GridItem(.adaptive(minimum: 240, maximum: 300), spacing: 20) - ], - spacing: 20 - ) { - ForEach(products, id: \.id) { uniqueProduct in - AppCardView(uniqueProduct: uniqueProduct) - .id(uniqueProduct.id) - .modifier(AppearAnimationModifier()) + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + LazyVGrid( + columns: [ + GridItem(.adaptive(minimum: 240, maximum: 300), spacing: 20) + ], + spacing: 20 + ) { + ForEach(products, id: \.id) { uniqueProduct in + AppCardView(uniqueProduct: uniqueProduct) + .id(uniqueProduct.id) + .modifier(AppearAnimationModifier()) + } } + .padding() + + + HStack(spacing: 8) { + Capsule() + .fill(Color.green) + .frame(width: 6, height: 6) + Text("获取到 \(products.count) 款产品") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .padding(.bottom, 16) } - .padding() - - HStack(spacing: 8) { - Capsule() - .fill(Color.green) - .frame(width: 6, height: 6) - Text("获取到 \(products.count) 款产品") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(.bottom, 16) } - .background(Color(.clear)) } } diff --git a/Adobe Downloader/Views/QAView.swift b/Adobe Downloader/Views/QAView.swift new file mode 100644 index 0000000..daad320 --- /dev/null +++ b/Adobe Downloader/Views/QAView.swift @@ -0,0 +1,59 @@ +// +// QAView.swift +// Adobe Downloader +// +// Created by X1a0He on 3/28/25. +// +import SwiftUI + +struct QAView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Group { + QAItem( + question: String(localized: "为什么需要安装 Helper?"), + answer: String(localized: "Helper 是一个具有管理员权限的辅助工具,用于执行需要管理员权限的操作,如修改系统文件等。没有 Helper 将无法正常使用软件的某些功能。") + ) + + QAItem( + question: String(localized: "为什么需要下载 Setup 组件?"), + answer: String(localized: "Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。") + ) + + QAItem( + question: String(localized: "为什么有时候下载会失败?"), + answer: String(localized: "下载失败可能有多种原因:\n1. 网络连接不稳定\n2. Adobe 服务器响应超时\n3. 本地磁盘空间不足\n建议您检查网络连接并重试,如果问题持续存在,可以尝试使用代理或 VPN。") + ) + + QAItem( + question: String(localized: "如何修复安装失败的问题?"), + answer: String(localized: "如果安装失败,您可以尝试以下步骤:\n1. 确保已正确安装并连接 Helper\n2. 确保已下载并处理 Setup 组件\n3. 检查磁盘剩余空间是否充足\n4. 尝试重新下载并安装\n如果问题仍然存在,可以尝试重新安装 Helper 和重新处理 Setup 组件。") + ) + } + } + .padding() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct QAItem: View { + let question: String + let answer: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(question) + .font(.headline) + .foregroundColor(.primary) + + Text(answer) + .font(.body) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + + Divider() + } + } +} diff --git a/Adobe Downloader/Views/Styles/BeautifulGroupBox.swift b/Adobe Downloader/Views/Styles/BeautifulGroupBox.swift new file mode 100644 index 0000000..4549121 --- /dev/null +++ b/Adobe Downloader/Views/Styles/BeautifulGroupBox.swift @@ -0,0 +1,38 @@ +// +// BeautifulGroupBox.swift +// Adobe Downloader +// +// Created by X1a0He on 3/28/25. +// +import SwiftUI + +struct BeautifulGroupBox: View { + let label: Label + let content: Content + + init(label: @escaping () -> Label, @ViewBuilder content: () -> Content) { + self.label = label() + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + label + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary.opacity(0.85)) + + VStack(alignment: .leading, spacing: 0) { + content + } + .padding(8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.15), lineWidth: 1) + ) + ) + } + } +} diff --git a/Adobe Downloader/Views/VersionPickerView.swift b/Adobe Downloader/Views/VersionPickerView.swift index e06f282..a2dd45f 100644 --- a/Adobe Downloader/Views/VersionPickerView.swift +++ b/Adobe Downloader/Views/VersionPickerView.swift @@ -115,39 +115,64 @@ private struct VersionListView: View { @Binding var expandedVersions: Set let onSelect: (String) -> Void let dismiss: DismissAction + @State private var scrollPosition: String? + @State private var cachedVersions: [(key: String, value: Product.Platform)] = [] var body: some View { - ScrollView(showsIndicators: false) { - VStack(spacing: 0) { - LazyVStack(spacing: VersionPickerConstants.verticalSpacing) { - ForEach(filteredVersions, id: \.key) { version, info in - VersionRow( - productId: productId, - version: version, - info: info, - isExpanded: expandedVersions.contains(version), - onSelect: handleVersionSelect, - onToggle: handleVersionToggle - ) + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + LazyVStack(spacing: VersionPickerConstants.verticalSpacing) { + ForEach(getFilteredVersions(), id: \.key) { version, info in + VersionRow( + productId: productId, + version: version, + info: info, + isExpanded: expandedVersions.contains(version), + onSelect: handleVersionSelect, + onToggle: handleVersionToggle + ) + .id(version) + .transition(.opacity) + } + } + .padding() + + HStack(spacing: 8) { + Capsule() + .fill(Color.green) + .frame(width: 6, height: 6) + Text("获取到 \(getFilteredVersions().count) 个版本") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .padding(.bottom, 16) + } + } + .background(Color(.clear)) + .onChange(of: expandedVersions) { newValue in + if let lastExpanded = newValue.sorted().last { + withAnimation { + proxy.scrollTo(lastExpanded, anchor: .top) } } - .padding() - - HStack(spacing: 8) { - Capsule() - .fill(Color.green) - .frame(width: 6, height: 6) - Text("获取到 \(filteredVersions.count) 个版本") - .font(.system(size: 12)) - .foregroundColor(.secondary) + } + .onAppear { + if cachedVersions.isEmpty { + cachedVersions = loadFilteredVersions() } - .padding(.bottom, 16) } } - .background(Color(.clear)) } - private var filteredVersions: [(key: String, value: Product.Platform)] { + private func getFilteredVersions() -> [(key: String, value: Product.Platform)] { + if !cachedVersions.isEmpty { + return cachedVersions + } + return loadFilteredVersions() + } + + private func loadFilteredVersions() -> [(key: String, value: Product.Platform)] { let products = findProducts(id: productId) if products.isEmpty { return [] @@ -187,7 +212,7 @@ private struct VersionListView: View { } } -private struct VersionRow: View { +private struct VersionRow: View, Equatable { @StorageValue(\.defaultLanguage) private var defaultLanguage let productId: String @@ -197,12 +222,16 @@ private struct VersionRow: View { let onSelect: (String) -> Void let onToggle: (String) -> Void + static func == (lhs: VersionRow, rhs: VersionRow) -> Bool { + lhs.productId == rhs.productId && + lhs.version == rhs.version && + lhs.isExpanded == rhs.isExpanded + } + + @State private var cachedExistingPath: URL? = nil + private var existingPath: URL? { - globalNetworkManager.isVersionDownloaded( - productId: productId, - version: version, - language: defaultLanguage - ) + cachedExistingPath } var body: some View { @@ -227,6 +256,16 @@ private struct VersionRow: View { .padding(.horizontal) .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(VersionPickerConstants.cornerRadius) + .animation(.easeInOut(duration: 0.2), value: isExpanded) + .onAppear { + if cachedExistingPath == nil { + cachedExistingPath = globalNetworkManager.isVersionDownloaded( + productId: productId, + version: version, + language: defaultLanguage + ) + } + } } } @@ -434,99 +473,114 @@ private struct DependenciesList: View { let dependencies: [Product.Platform.LanguageSet.Dependency] var body: some View { - ForEach(dependencies, id: \.sapCode) { dependency in - VStack(alignment: .leading, spacing: 5) { - HStack(spacing: 8) { - getPlatformIcon(for: dependency.selectedPlatform) - .foregroundColor(.blue.opacity(0.8)) - .font(.system(size: 12)) - .frame(width: 16) - - Text(dependency.sapCode) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.primary.opacity(0.8)) - - Text("\(dependency.productVersion)") - .font(.system(size: 11)) - .padding(.horizontal, 5) - .padding(.vertical, 1) - .background( - RoundedRectangle(cornerRadius: 3) - .fill(Color.blue.opacity(0.1)) - ) - .foregroundColor(.blue.opacity(0.8)) - - if dependency.baseVersion != dependency.productVersion { - HStack(spacing: 3) { - Text("base:") - .font(.system(size: 10)) - .foregroundColor(.secondary.opacity(0.7)) - Text(dependency.baseVersion) - .font(.system(size: 10, weight: .medium)) - .foregroundColor(.secondary.opacity(0.9)) - } - .padding(.horizontal, 5) - .padding(.vertical, 1) - .background( - RoundedRectangle(cornerRadius: 3) - .fill(Color.secondary.opacity(0.1)) - ) - } - } - .padding(.vertical, 2) - - HStack(spacing: 10) { - if !dependency.buildGuid.isEmpty { - HStack(spacing: 3) { - Text("buildGuid:") - .font(.system(size: 10)) - .foregroundColor(.secondary.opacity(0.7)) - Text(dependency.buildGuid) - .font(.system(size: 10, weight: .medium)) - .foregroundColor(.secondary.opacity(0.9)) - .textSelection(.enabled) - } - } - } - .padding(.top, 2) - .padding(.leading, 24) - - #if DEBUG - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 4) { - Text("Match:") - .font(.caption2) - .foregroundColor(.secondary) - Text(dependency.isMatchPlatform ? "✅" : "❌") - .font(.caption2) - - Text("•") - .font(.caption2) - .foregroundColor(.secondary) - - Text("Target:") - .font(.caption2) - .foregroundColor(.secondary) - Text(dependency.targetPlatform) - .font(.caption2) - .foregroundColor(.blue) - } - - if !dependency.selectedReason.isEmpty { - HStack(spacing: 4) { - Text("Reason:") - .font(.caption2) - .foregroundColor(.secondary) - Text(dependency.selectedReason) - .font(.caption2) - .foregroundColor(.orange) - } - } - } - .padding(.leading, 22) - #endif + LazyVStack(alignment: .leading, spacing: 2) { + ForEach(dependencies, id: \.sapCode) { dependency in + DependencyRow(dependency: dependency) + .padding(.vertical, 4) } - .padding(.vertical, 4) + } + } +} + +private struct DependencyRow: View, Equatable { + let dependency: Product.Platform.LanguageSet.Dependency + + static func == (lhs: DependencyRow, rhs: DependencyRow) -> Bool { + lhs.dependency.sapCode == rhs.dependency.sapCode && + lhs.dependency.productVersion == rhs.dependency.productVersion + } + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + HStack(spacing: 8) { + getPlatformIcon(for: dependency.selectedPlatform) + .foregroundColor(.blue.opacity(0.8)) + .font(.system(size: 12)) + .frame(width: 16) + + Text(dependency.sapCode) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.primary.opacity(0.8)) + + Text("\(dependency.productVersion)") + .font(.system(size: 11)) + .padding(.horizontal, 5) + .padding(.vertical, 1) + .background( + RoundedRectangle(cornerRadius: 3) + .fill(Color.blue.opacity(0.1)) + ) + .foregroundColor(.blue.opacity(0.8)) + + if dependency.baseVersion != dependency.productVersion { + HStack(spacing: 3) { + Text("base:") + .font(.system(size: 10)) + .foregroundColor(.secondary.opacity(0.7)) + Text(dependency.baseVersion) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.secondary.opacity(0.9)) + } + .padding(.horizontal, 5) + .padding(.vertical, 1) + .background( + RoundedRectangle(cornerRadius: 3) + .fill(Color.secondary.opacity(0.1)) + ) + } + } + .padding(.vertical, 2) + + HStack(spacing: 10) { + if !dependency.buildGuid.isEmpty { + HStack(spacing: 3) { + Text("buildGuid:") + .font(.system(size: 10)) + .foregroundColor(.secondary.opacity(0.7)) + Text(dependency.buildGuid) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.secondary.opacity(0.9)) + .textSelection(.enabled) + } + } + } + .padding(.top, 2) + .padding(.leading, 24) + + #if DEBUG + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 4) { + Text("Match:") + .font(.caption2) + .foregroundColor(.secondary) + Text(dependency.isMatchPlatform ? "✅" : "❌") + .font(.caption2) + + Text("•") + .font(.caption2) + .foregroundColor(.secondary) + + Text("Target:") + .font(.caption2) + .foregroundColor(.secondary) + Text(dependency.targetPlatform) + .font(.caption2) + .foregroundColor(.blue) + } + + if !dependency.selectedReason.isEmpty { + HStack(spacing: 4) { + Text("Reason:") + .font(.caption2) + .foregroundColor(.secondary) + Text(dependency.selectedReason) + .font(.caption2) + .foregroundColor(.orange) + } + } + } + .padding(.leading, 22) + #endif } } diff --git a/Localizables/Localizable.xcstrings b/Localizables/Localizable.xcstrings index dc17b19..6ae0330 100644 --- a/Localizables/Localizable.xcstrings +++ b/Localizables/Localizable.xcstrings @@ -433,13 +433,20 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Setup component not processed" + "value" : "Setup component unprocessed" } } } }, "Setup 组件未处理,无法安装" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setup component unprocessed, installation unavailable." + } + } + } }, "Setup未备份提示" : { "localizations" : { @@ -595,7 +602,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Download management" + "value" : "Download Management" } } } @@ -816,21 +823,23 @@ } }, "全部暂停" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Pause All" + "value" : "Pause" } } } }, "全部继续" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Continue All" + "value" : "Continue" } } } @@ -1081,7 +1090,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Processing status:" + "value" : "Process Status:" } } } @@ -1303,7 +1312,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Connection status:" + "value" : "Connection Status:" } } } @@ -1351,7 +1360,6 @@ } }, "将执行的命令:" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2509,6 +2517,7 @@ } }, "清理已完成" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2599,6 +2608,16 @@ } } }, + "确定要删除所有已完成和失败的下载任务吗?此操作将同时删除本地文件。" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm deletion of all completed/failed downloads? This will also remove local files." + } + } + } + }, "确定要取消%@的下载吗?" : { "localizations" : { "en" : { @@ -2649,6 +2668,16 @@ } } }, + "确认删除" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm Delete" + } + } + } + }, "确认取消" : { "localizations" : { "en" : { @@ -3032,7 +3061,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Connection status:" + "value" : "Connection Status:" } } }