mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
perf: Rendering performance optimizations
- UI rendering - App load rendering - Scroll rendering
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>AdobeDownloaderHelperTool.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
@@ -25,7 +25,7 @@
|
||||
<key>3CCC3ADF2CC67B8F006E22B4</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true />
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 相关条目")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
//}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
190
Adobe Downloader/Views/CleanConfigView.swift
Normal file
190
Adobe Downloader/Views/CleanConfigView.swift
Normal file
@@ -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: "正在清理其他文件...")
|
||||
}
|
||||
}
|
||||
}
|
||||
645
Adobe Downloader/Views/CleanupView.swift
Normal file
645
Adobe Downloader/Views/CleanupView.swift
Normal file
@@ -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<CleanupOption>()
|
||||
@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<CleanupOption>()
|
||||
@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<CleanupOption>
|
||||
@Binding var expandedOptions: Set<CleanupOption>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("确定要删除所有已完成和失败的下载任务吗?此操作将同时删除本地文件。")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
127
Adobe Downloader/Views/LogEntryView.swift
Normal file
127
Adobe Downloader/Views/LogEntryView.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ struct ProductGridView: View {
|
||||
let products: [UniqueProduct]
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVGrid(
|
||||
columns: [
|
||||
@@ -87,6 +88,7 @@ struct ProductGridView: View {
|
||||
}
|
||||
.padding()
|
||||
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Capsule()
|
||||
.fill(Color.green)
|
||||
@@ -97,7 +99,7 @@ struct ProductGridView: View {
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.background(Color(.clear))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
59
Adobe Downloader/Views/QAView.swift
Normal file
59
Adobe Downloader/Views/QAView.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Adobe Downloader/Views/Styles/BeautifulGroupBox.swift
Normal file
38
Adobe Downloader/Views/Styles/BeautifulGroupBox.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// BeautifulGroupBox.swift
|
||||
// Adobe Downloader
|
||||
//
|
||||
// Created by X1a0He on 3/28/25.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct BeautifulGroupBox<Label: View, Content: View>: 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,12 +115,15 @@ private struct VersionListView: View {
|
||||
@Binding var expandedVersions: Set<String>
|
||||
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 {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
LazyVStack(spacing: VersionPickerConstants.verticalSpacing) {
|
||||
ForEach(filteredVersions, id: \.key) { version, info in
|
||||
ForEach(getFilteredVersions(), id: \.key) { version, info in
|
||||
VersionRow(
|
||||
productId: productId,
|
||||
version: version,
|
||||
@@ -129,6 +132,8 @@ private struct VersionListView: View {
|
||||
onSelect: handleVersionSelect,
|
||||
onToggle: handleVersionToggle
|
||||
)
|
||||
.id(version)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -137,7 +142,7 @@ private struct VersionListView: View {
|
||||
Capsule()
|
||||
.fill(Color.green)
|
||||
.frame(width: 6, height: 6)
|
||||
Text("获取到 \(filteredVersions.count) 个版本")
|
||||
Text("获取到 \(getFilteredVersions().count) 个版本")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -145,9 +150,29 @@ private struct VersionListView: View {
|
||||
}
|
||||
}
|
||||
.background(Color(.clear))
|
||||
.onChange(of: expandedVersions) { newValue in
|
||||
if let lastExpanded = newValue.sorted().last {
|
||||
withAnimation {
|
||||
proxy.scrollTo(lastExpanded, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if cachedVersions.isEmpty {
|
||||
cachedVersions = loadFilteredVersions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +473,24 @@ private struct DependenciesList: View {
|
||||
let dependencies: [Product.Platform.LanguageSet.Dependency]
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(alignment: .leading, spacing: 2) {
|
||||
ForEach(dependencies, id: \.sapCode) { dependency in
|
||||
DependencyRow(dependency: dependency)
|
||||
.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)
|
||||
@@ -526,8 +582,6 @@ private struct DependenciesList: View {
|
||||
.padding(.leading, 22)
|
||||
#endif
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private func getPlatformIcon(for platform: String) -> Image {
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user