├── .gitignore ├── Adobe Downloader.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── Adobe Downloader.xcscheme └── xcuserdata │ └── hejinhui.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Adobe Downloader ├── Adobe Downloader.entitlements ├── Adobe DownloaderApp.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024pt-1.png │ │ ├── 128pt-1.png │ │ ├── 16pt-1.png │ │ ├── 256pt-1.png │ │ ├── 256pt-2.png │ │ ├── 32pt-1.png │ │ ├── 32pt-2.png │ │ ├── 512pt-1.png │ │ ├── 512pt-2.png │ │ ├── 64pt-1.png │ │ └── Contents.json │ └── Contents.json ├── Commons │ ├── Enums.swift │ ├── Globals.swift │ ├── NewEnums.swift │ ├── NewStructs.swift │ └── Statics.swift ├── ContentView.swift ├── HelperManager │ └── PrivilegedHelperManager.swift ├── Info.plist ├── Models │ ├── CheckForUpdatesViewModel.swift │ └── NewDownloadTask.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Scripts │ └── clean-config.sh ├── Services │ └── NewNetworkService.swift ├── Storages │ └── StorageData.swift ├── Utils │ ├── CancelTracker.swift │ ├── InstallManager.swift │ ├── ModifySetup.swift │ ├── NetworkManager.swift │ ├── NewDownloadUtils.swift │ ├── NewJSONParser.swift │ └── TaskPersistenceManager.swift └── Views │ ├── AboutView.swift │ ├── AppCardView.swift │ ├── BannerView.swift │ ├── CleanConfigView.swift │ ├── CleanupView.swift │ ├── CustomSettingsView.swift │ ├── DownloadManagerView.swift │ ├── DownloadProgressView.swift │ ├── ExistingFileAlertView.swift │ ├── InstallProgressView.swift │ ├── LanguagePickerView.swift │ ├── LogEntryView.swift │ ├── MainContentView.swift │ ├── QAView.swift │ ├── SetupBackupAlertView.swift │ ├── SetupBackupResultView.swift │ ├── ShouldExistsSetUpView.swift │ ├── Styles │ ├── BeautifulButtonStyle.swift │ └── BeautifulGroupBox.swift │ ├── TipsSheetView.swift │ ├── ToolbarView.swift │ └── VersionPickerView.swift ├── AdobeDownloaderHelperTool ├── AdobeDownloaderHelperTool.entitlements ├── Info.plist ├── Launchd.plist └── main.swift ├── LICENSE ├── Localizables └── Localizable.xcstrings ├── analyze └── channels.md ├── appcast.xml ├── change-log-generate.py ├── imgs ├── Adobe Downloader 2.0.0.png ├── download.png ├── language.png ├── preview-dark.png ├── preview-light.png └── version.png ├── readme-en.md ├── readme.md └── update-log.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | *.DS_Store 4 | .idea/ 5 | .fleet 6 | 7 | ## User settings 8 | xcuserdata/ 9 | *.xcuserstate 10 | 11 | ## Xcode Workspace 12 | *.xcworkspace 13 | *.xcodeproj/project.xcworkspace/ 14 | *.xcodeproj/xcuserdata/ 15 | 16 | ## Build generated 17 | build/ 18 | DerivedData/ 19 | 20 | ## Various settings 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | 30 | ## Other 31 | *.moved-aside 32 | *.xccheckout 33 | *.xcscmblueprint 34 | .xcuserstate 35 | 36 | ## Obj-C/Swift specific 37 | *.hmap 38 | *.ipa 39 | *.dSYM.zip 40 | *.dSYM 41 | 42 | ## Playgrounds 43 | timeline.xctimeline 44 | playground.xcworkspace 45 | 46 | # Swift Package Manager 47 | .build/ -------------------------------------------------------------------------------- /Adobe Downloader.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Adobe Downloader.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Adobe-Downloader.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | AdobeDownloaderHelperTool.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 1 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | 3CCC3ADF2CC67B8F006E22B4 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Adobe Downloader/Adobe Downloader.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Adobe Downloader/Adobe DownloaderApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Sparkle 3 | 4 | @main 5 | struct Adobe_DownloaderApp: App { 6 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 7 | @State private var showBackupAlert = false 8 | @State private var showTipsSheet = false 9 | @State private var showLanguagePicker = false 10 | @State private var showCreativeCloudAlert = false 11 | @State private var showBackupResultAlert = false 12 | @State private var showSettingsView = false 13 | 14 | @StateObject private var backupResult = BackupResult() 15 | 16 | private var storage: StorageData { StorageData.shared } 17 | private let updaterController: SPUStandardUpdaterController 18 | 19 | init() { 20 | globalNetworkService = NewNetworkService() 21 | globalNetworkManager = NetworkManager() 22 | globalNewDownloadUtils = NewDownloadUtils() 23 | updaterController = SPUStandardUpdaterController( 24 | startingUpdater: true, 25 | updaterDelegate: nil, 26 | userDriverDelegate: nil 27 | ) 28 | 29 | if storage.installedHelperBuild == "0" { 30 | storage.installedHelperBuild = "0" 31 | } 32 | 33 | if storage.isFirstLaunch { 34 | initializeFirstLaunch() 35 | } 36 | 37 | if storage.apiVersion == "6" { 38 | storage.apiVersion = "6" 39 | } 40 | } 41 | 42 | private func initializeFirstLaunch() { 43 | storage.downloadAppleSilicon = AppStatics.isAppleSilicon 44 | storage.confirmRedownload = true 45 | 46 | let systemLanguage = Locale.current.identifier 47 | let matchedLanguage = AppStatics.supportedLanguages.first { 48 | systemLanguage.hasPrefix($0.code.prefix(2)) 49 | }?.code ?? "ALL" 50 | storage.defaultLanguage = matchedLanguage 51 | storage.useDefaultLanguage = true 52 | 53 | if let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first { 54 | storage.defaultDirectory = downloadsURL.path 55 | storage.useDefaultDirectory = true 56 | } 57 | } 58 | 59 | var body: some Scene { 60 | WindowGroup { 61 | ZStack { 62 | BlurView() 63 | .ignoresSafeArea() 64 | 65 | ContentView(showSettingsView: $showSettingsView) 66 | .environmentObject(globalNetworkManager) 67 | .frame(minWidth: 792, minHeight: 600) 68 | .tint(.blue) 69 | .task { 70 | await setupApplication() 71 | } 72 | .sheet(isPresented: $showCreativeCloudAlert) { 73 | ShouldExistsSetUpView() 74 | .environmentObject(globalNetworkManager) 75 | } 76 | .sheet(isPresented: $showBackupAlert) { 77 | SetupBackupAlertView( 78 | onConfirm: { 79 | showBackupAlert = false 80 | handleBackup() 81 | }, 82 | onCancel: { 83 | showBackupAlert = false 84 | } 85 | ) 86 | } 87 | .sheet(isPresented: $showBackupResultAlert) { 88 | SetupBackupResultView( 89 | isSuccess: backupResult.success, 90 | message: backupResult.message, 91 | onDismiss: { 92 | showBackupResultAlert = false 93 | } 94 | ) 95 | } 96 | .sheet(isPresented: $showTipsSheet) { 97 | TipsSheetView( 98 | showTipsSheet: $showTipsSheet, 99 | showLanguagePicker: $showLanguagePicker 100 | ) 101 | .environmentObject(globalNetworkManager) 102 | .sheet(isPresented: $showLanguagePicker) { 103 | LanguagePickerView(languages: AppStatics.supportedLanguages) { language in 104 | storage.defaultLanguage = language 105 | showLanguagePicker = false 106 | } 107 | } 108 | } 109 | .sheet(isPresented: $showSettingsView) { 110 | CustomSettingsView(updater: updaterController.updater) 111 | .interactiveDismissDisabled(false) 112 | } 113 | } 114 | } 115 | .windowStyle(.hiddenTitleBar) 116 | .windowResizabilityContentSize() 117 | .commands { 118 | CommandGroup(after: .appInfo) { 119 | CheckForUpdatesView(updater: updaterController.updater) 120 | 121 | Divider() 122 | 123 | Button("设置...") { 124 | showSettingsView = true 125 | } 126 | .keyboardShortcut(",", modifiers: .command) 127 | } 128 | } 129 | } 130 | 131 | private func setupApplication() async { 132 | PrivilegedHelperManager.shared.checkInstall() 133 | 134 | await MainActor.run { 135 | globalNetworkManager.loadSavedTasks() 136 | } 137 | 138 | let needsBackup = !ModifySetup.isSetupBackup() 139 | let needsSetup = !ModifySetup.isSetupExists() 140 | 141 | await MainActor.run { 142 | if needsSetup { 143 | showCreativeCloudAlert = true 144 | } else if needsBackup { 145 | #if !DEBUG 146 | showBackupAlert = true 147 | #endif 148 | } 149 | 150 | if storage.isFirstLaunch { 151 | showTipsSheet = true 152 | storage.isFirstLaunch = false 153 | } 154 | } 155 | } 156 | 157 | private func handleBackup() { 158 | ModifySetup.backupAndModifySetupFile { success, message in 159 | DispatchQueue.main.async { 160 | self.backupResult.success = success 161 | self.backupResult.message = message 162 | self.showBackupResultAlert = true 163 | } 164 | } 165 | } 166 | } 167 | 168 | extension Scene { 169 | func windowResizabilityContentSize() -> some Scene { 170 | if #available(macOS 13.0, *) { 171 | return windowResizability(.contentSize) 172 | } else { 173 | return self 174 | } 175 | } 176 | } 177 | 178 | class BackupResult: ObservableObject { 179 | @Published var success: Bool = false 180 | @Published var message: String = "" 181 | } 182 | -------------------------------------------------------------------------------- /Adobe Downloader/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | 4 | struct BlurView: NSViewRepresentable { 5 | func makeNSView(context: Context) -> NSVisualEffectView { 6 | let effectView = NSVisualEffectView() 7 | effectView.blendingMode = .behindWindow 8 | effectView.material = .hudWindow 9 | effectView.state = .active 10 | effectView.isEmphasized = true 11 | return effectView 12 | } 13 | 14 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) { 15 | } 16 | } 17 | 18 | class AppDelegate: NSObject, NSApplicationDelegate { 19 | private var eventMonitor: Any? 20 | 21 | func applicationDidFinishLaunching(_ notification: Notification) { 22 | if let window = NSApp.windows.first { 23 | window.minSize = NSSize(width: 800, height: 765) 24 | } 25 | 26 | eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in 27 | if event.modifierFlags.contains(.command) && event.characters?.lowercased() == "q" { 28 | if let mainWindow = NSApp.mainWindow, 29 | mainWindow.sheets.isEmpty && !mainWindow.isSheet { 30 | _ = self?.applicationShouldTerminate(NSApp) 31 | return nil 32 | } 33 | } 34 | return event 35 | } 36 | 37 | PrivilegedHelperManager.shared.executeCommand("id -u") { _ in } 38 | } 39 | 40 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 41 | let hasActiveDownloads = globalNetworkManager.downloadTasks.contains { task in 42 | if case .downloading = task.totalStatus { return true } 43 | return false 44 | } 45 | 46 | if hasActiveDownloads { 47 | Task { 48 | for task in globalNetworkManager.downloadTasks { 49 | if case .downloading = task.totalStatus { 50 | await globalNewDownloadUtils.pauseDownloadTask( 51 | taskId: task.id, 52 | reason: .other(String(localized: "程序即将退出")) 53 | ) 54 | await globalNetworkManager.saveTask(task) 55 | } 56 | } 57 | 58 | await MainActor.run { 59 | let alert = NSAlert() 60 | alert.messageText = String(localized: "确认退出") 61 | alert.informativeText = String(localized:"有正在进行的下载任务,确定要退出吗?\n所有下载任务的进度已保存,下次启动可以继续下载") 62 | alert.alertStyle = .warning 63 | alert.addButton(withTitle: String(localized:"退出")) 64 | alert.addButton(withTitle: String(localized:"取消")) 65 | 66 | let response = alert.runModal() 67 | if response == .alertSecondButtonReturn { 68 | Task { 69 | for task in globalNetworkManager.downloadTasks { 70 | if case .paused = task.totalStatus { 71 | await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id) 72 | } 73 | } 74 | } 75 | } else { 76 | NSApplication.shared.terminate(0) 77 | } 78 | } 79 | } 80 | } else { 81 | NSApplication.shared.terminate(nil) 82 | } 83 | return .terminateCancel 84 | } 85 | 86 | deinit { 87 | if let monitor = eventMonitor { 88 | NSEvent.removeMonitor(monitor) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/1024pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/1024pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/128pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/128pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/16pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/16pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/256pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/256pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/256pt-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/256pt-2.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/32pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/32pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/32pt-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/32pt-2.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/512pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/512pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/512pt-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/512pt-2.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/64pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/Adobe Downloader/Assets.xcassets/AppIcon.appiconset/64pt-1.png -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16pt-1.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32pt-1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32pt-2.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64pt-1.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128pt-1.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256pt-1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256pt-2.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512pt-1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512pt-2.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "1024pt-1.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Adobe Downloader/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Adobe Downloader/Commons/Globals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Globals.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 2/26/25. 6 | // 7 | 8 | // 下面是所有全局变量的私有存储 9 | private var _globalStiResult: NewParseResult? 10 | private var _globalCcmResult: NewParseResult? 11 | private var _globalProducts: [Product]? 12 | private var _globalUniqueProducts: [UniqueProduct]? 13 | private var _globalCdn: String = "" 14 | private var _globalNetworkService: NewNetworkService? 15 | private var _globalNetworkManager: NetworkManager? 16 | private var _globalNewDownloadUtils: NewDownloadUtils? 17 | private var _globalCancelTracker: CancelTracker? 18 | 19 | struct DependencyCacheKey: Hashable { 20 | let sapCode: String 21 | let targetPlatform: String 22 | } 23 | 24 | private var _globalDependencyCache: [DependencyCacheKey: Product.Platform.LanguageSet.Dependency]? 25 | 26 | var globalDependencyCache: [DependencyCacheKey: Product.Platform.LanguageSet.Dependency] { 27 | get { 28 | if _globalDependencyCache == nil { 29 | _globalDependencyCache = [:] 30 | } 31 | return _globalDependencyCache! 32 | } 33 | set { 34 | _globalDependencyCache = newValue 35 | } 36 | } 37 | 38 | // 计算属性,确保总是返回有效实例 39 | var globalStiResult: NewParseResult { 40 | get { 41 | if _globalStiResult == nil { 42 | _globalStiResult = NewParseResult(products: [], cdn: "") 43 | } 44 | return _globalStiResult! 45 | } 46 | set { 47 | _globalStiResult = newValue 48 | } 49 | } 50 | 51 | var globalCcmResult: NewParseResult { 52 | get { 53 | if _globalCcmResult == nil { 54 | _globalCcmResult = NewParseResult(products: [], cdn: "") 55 | } 56 | return _globalCcmResult! 57 | } 58 | set { 59 | _globalCcmResult = newValue 60 | } 61 | } 62 | 63 | var globalCdn: String { 64 | get { 65 | return _globalCdn 66 | } 67 | set { 68 | _globalCdn = newValue 69 | } 70 | } 71 | 72 | var globalNetworkService: NewNetworkService { 73 | get { 74 | if _globalNetworkService == nil { 75 | fatalError("NewNetworkService 没有被初始化,请确保在应用启动时初始化") 76 | } 77 | return _globalNetworkService! 78 | } 79 | set { 80 | _globalNetworkService = newValue 81 | } 82 | } 83 | 84 | var globalNetworkManager: NetworkManager { 85 | get { 86 | if _globalNetworkManager == nil { 87 | fatalError("NetworkManager 没有被初始化,请确保在应用启动时初始化") 88 | } 89 | return _globalNetworkManager! 90 | } 91 | set { 92 | _globalNetworkManager = newValue 93 | } 94 | } 95 | 96 | var globalNewDownloadUtils: NewDownloadUtils { 97 | get { 98 | if _globalNewDownloadUtils == nil { 99 | fatalError("NewDownloadUtils 没有被初始化,请确保在应用启动时初始化") 100 | } 101 | return _globalNewDownloadUtils! 102 | } 103 | set { 104 | _globalNewDownloadUtils = newValue 105 | } 106 | } 107 | 108 | var globalCancelTracker: CancelTracker { 109 | get { 110 | if _globalCancelTracker == nil { 111 | _globalCancelTracker = CancelTracker() 112 | } 113 | return _globalCancelTracker! 114 | } 115 | set { 116 | _globalCancelTracker = newValue 117 | } 118 | } 119 | 120 | var globalProducts: [Product] { 121 | get { 122 | if _globalProducts == nil { 123 | _globalProducts = [] 124 | } 125 | return _globalProducts! 126 | } 127 | set { 128 | _globalProducts = newValue 129 | } 130 | } 131 | 132 | var globalUniqueProducts: [UniqueProduct] { 133 | get { 134 | if _globalUniqueProducts == nil { 135 | _globalUniqueProducts = [] 136 | } 137 | return _globalUniqueProducts! 138 | } 139 | set { 140 | _globalUniqueProducts = newValue 141 | } 142 | } 143 | 144 | func getAllProducts() -> [Product] { 145 | var allProducts = [Product]() 146 | let stiProducts = globalStiResult.products 147 | if !stiProducts.isEmpty { 148 | allProducts.append(contentsOf: stiProducts) 149 | } 150 | let ccmProducts = globalCcmResult.products 151 | if !ccmProducts.isEmpty { 152 | allProducts.append(contentsOf: ccmProducts) 153 | } 154 | return allProducts 155 | } 156 | 157 | /// 根据产品ID和版本号快速查找产品 158 | /// - Parameters: 159 | /// - id: 产品ID 160 | /// - version: 版本号(可选) 161 | /// - Returns: 如果提供版本号,返回指定版本的产品;否则返回最新版本的产品 162 | func findProduct(id: String, version: String? = nil) -> Product? { 163 | // 首先在全局产品列表中查找匹配ID的产品 164 | guard let product = globalProducts.first(where: { $0.id == id }) else { 165 | return nil 166 | } 167 | 168 | // 如果没有指定版本,直接返回找到的产品 169 | guard let version = version else { 170 | return product 171 | } 172 | 173 | // 检查产品的平台和语言集是否包含指定版本 174 | for platform in product.platforms { 175 | for languageSet in platform.languageSet { 176 | if languageSet.productVersion == version { 177 | return product 178 | } 179 | } 180 | } 181 | 182 | return nil 183 | } 184 | 185 | /// 获取产品的所有可用版本 186 | /// - Parameter id: 产品ID 187 | /// - Returns: 版本号数组,已去重并按版本号排序 188 | func getProductVersions(id: String) -> [String] { 189 | guard let product = globalProducts.first(where: { $0.id == id }) else { 190 | return [] 191 | } 192 | 193 | // 收集所有平台和语言集中的版本号 194 | var versions = Set() 195 | for platform in product.platforms { 196 | for languageSet in platform.languageSet { 197 | versions.insert(languageSet.productVersion) 198 | } 199 | } 200 | 201 | // 转换为数组并按版本号排序(降序) 202 | return Array(versions).sorted { version1, version2 in 203 | version1.compare(version2, options: .numeric) == .orderedDescending 204 | } 205 | } 206 | 207 | /// 获取产品在指定版本下的所有可用语言 208 | /// - Parameters: 209 | /// - id: 产品ID 210 | /// - version: 版本号 211 | /// - Returns: 语言代码数组 212 | func getProductLanguages(id: String, version: String) -> [String] { 213 | guard let product = globalProducts.first(where: { $0.id == id }) else { 214 | return [] 215 | } 216 | 217 | var languages = Set() 218 | for platform in product.platforms { 219 | for languageSet in platform.languageSet { 220 | if languageSet.productVersion == version { 221 | languages.insert(languageSet.name) 222 | } 223 | } 224 | } 225 | 226 | return Array(languages).sorted() 227 | } 228 | 229 | /// 查找所有匹配指定ID的产品 230 | /// - Parameter id: 产品ID 231 | /// - Returns: 匹配的产品数组 232 | func findProducts(id: String) -> [Product] { 233 | var matchedProducts = [Product]() 234 | 235 | // 从 globalProducts 中查找 236 | matchedProducts.append(contentsOf: globalProducts.filter { $0.id == id }) 237 | 238 | // 从 globalCcmResult 中查找 239 | matchedProducts.append(contentsOf: globalCcmResult.products.filter { $0.id == id }) 240 | 241 | // 从 globalStiResult 中查找 242 | matchedProducts.append(contentsOf: globalStiResult.products.filter { $0.id == id }) 243 | 244 | return matchedProducts 245 | } 246 | -------------------------------------------------------------------------------- /Adobe Downloader/Commons/NewEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewEnums.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 2/26/25. 6 | // 7 | 8 | enum ParserError: Error { 9 | case missingCDN 10 | case invalidJSON 11 | case missingRequired 12 | } -------------------------------------------------------------------------------- /Adobe Downloader/Commons/NewStructs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2/26/25. 5 | // 6 | 7 | import Foundation 8 | struct Product: Codable, Equatable { 9 | var type: String 10 | var displayName: String 11 | var family: String 12 | var appLineage: String 13 | var familyName: String 14 | var productIcons: [ProductIcon] 15 | var platforms: [Platform] 16 | var referencedProducts: [ReferencedProduct] 17 | var version: String 18 | var id: String 19 | var hidden: Bool 20 | 21 | func hasValidVersions(allowedPlatform: [String]) -> Bool { 22 | return platforms.contains { platform in 23 | allowedPlatform.contains(platform.id) && !platform.languageSet.isEmpty 24 | } 25 | } 26 | 27 | struct ProductIcon: Codable, Equatable { 28 | var value: String 29 | var size: String 30 | 31 | var dimension: Int { 32 | let components = size.split(separator: "x") 33 | if components.count == 2, 34 | let dimension = Int(components[0]) { 35 | return dimension 36 | } 37 | return 0 38 | } 39 | } 40 | 41 | struct Platform: Codable, Equatable { 42 | var languageSet: [LanguageSet] 43 | var modules: [Module] 44 | var range: [Range] 45 | var id: String 46 | 47 | struct LanguageSet: Codable, Equatable { 48 | var manifestURL: String 49 | var dependencies: [Dependency] 50 | var productCode: String 51 | var name: String 52 | var installSize: Int 53 | var buildGuid: String 54 | var baseVersion: String 55 | var productVersion: String 56 | 57 | struct Dependency: Codable, Equatable { 58 | var sapCode: String 59 | var baseVersion: String 60 | var productVersion: String 61 | var buildGuid: String 62 | var isMatchPlatform: Bool 63 | var targetPlatform: String 64 | var selectedPlatform: String 65 | var selectedReason: String 66 | 67 | init(sapCode: String, baseVersion: String, productVersion: String, buildGuid: String, isMatchPlatform: Bool = false, targetPlatform: String = "", selectedPlatform: String = "", selectedReason: String = "") { 68 | self.sapCode = sapCode 69 | self.baseVersion = baseVersion 70 | self.productVersion = productVersion 71 | self.buildGuid = buildGuid 72 | self.isMatchPlatform = isMatchPlatform 73 | self.targetPlatform = targetPlatform 74 | self.selectedPlatform = selectedPlatform 75 | self.selectedReason = selectedReason 76 | } 77 | } 78 | } 79 | 80 | struct Module: Codable, Equatable { 81 | var displayName: String 82 | var deploymentType: String 83 | var id: String 84 | } 85 | 86 | struct Range: Codable, Equatable { 87 | var min: String 88 | var max: String 89 | } 90 | } 91 | 92 | struct ReferencedProduct: Codable, Equatable { 93 | var sapCode: String 94 | var version: String 95 | } 96 | 97 | func getBestIcon() -> ProductIcon? { 98 | if let icon = productIcons.first(where: { $0.size == "192x192" }) { 99 | return icon 100 | } 101 | return productIcons.max(by: { $0.dimension < $1.dimension }) 102 | } 103 | } 104 | 105 | struct NewParseResult { 106 | var products: [Product] 107 | var cdn: String 108 | } 109 | 110 | /* ========== */ 111 | struct UniqueProduct: Equatable { 112 | var id: String 113 | var displayName: String 114 | 115 | static func == (lhs: UniqueProduct, rhs: UniqueProduct) -> Bool { 116 | return lhs.id == rhs.id && lhs.displayName == rhs.displayName 117 | } 118 | } 119 | 120 | /* ========== */ 121 | class DependenciesToDownload: ObservableObject, Codable { 122 | // 别人依赖就他吗叫sapCode,Adobe也是傻逼,一会id一会sapCode 123 | var sapCode: String 124 | var version: String 125 | var buildGuid: String 126 | var applicationJson: String? 127 | @Published var packages: [Package] = [] 128 | @Published var completedPackages: Int = 0 129 | 130 | var totalPackages: Int { 131 | packages.count 132 | } 133 | 134 | init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") { 135 | self.sapCode = sapCode 136 | self.version = version 137 | self.buildGuid = buildGuid 138 | self.applicationJson = applicationJson 139 | } 140 | 141 | func updateCompletedPackages() { 142 | Task { @MainActor in 143 | completedPackages = packages.filter { $0.downloaded }.count 144 | objectWillChange.send() 145 | } 146 | } 147 | 148 | enum CodingKeys: String, CodingKey { 149 | case sapCode, version, buildGuid, applicationJson, packages 150 | } 151 | 152 | func encode(to encoder: Encoder) throws { 153 | var container = encoder.container(keyedBy: CodingKeys.self) 154 | try container.encode(sapCode, forKey: .sapCode) 155 | try container.encode(version, forKey: .version) 156 | try container.encode(buildGuid, forKey: .buildGuid) 157 | try container.encodeIfPresent(applicationJson, forKey: .applicationJson) 158 | try container.encode(packages, forKey: .packages) 159 | } 160 | 161 | required init(from decoder: Decoder) throws { 162 | let container = try decoder.container(keyedBy: CodingKeys.self) 163 | sapCode = try container.decode(String.self, forKey: .sapCode) 164 | version = try container.decode(String.self, forKey: .version) 165 | buildGuid = try container.decode(String.self, forKey: .buildGuid) 166 | applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson) 167 | packages = try container.decode([Package].self, forKey: .packages) 168 | completedPackages = 0 169 | } 170 | } 171 | 172 | /* ========== */ 173 | class Package: Identifiable, ObservableObject, Codable { 174 | var id = UUID() 175 | var type: String 176 | var fullPackageName: String 177 | var downloadSize: Int64 178 | var downloadURL: String 179 | var packageVersion: String 180 | 181 | @Published var downloadedSize: Int64 = 0 { 182 | didSet { 183 | if downloadSize > 0 { 184 | progress = Double(downloadedSize) / Double(downloadSize) 185 | } 186 | } 187 | } 188 | @Published var progress: Double = 0 189 | @Published var speed: Double = 0 190 | @Published var status: PackageStatus = .waiting 191 | @Published var downloaded: Bool = false 192 | 193 | var lastUpdated: Date = Date() 194 | var lastRecordedSize: Int64 = 0 195 | var retryCount: Int = 0 196 | var lastError: Error? 197 | 198 | var canRetry: Bool { 199 | if case .failed = status { 200 | return retryCount < 3 201 | } 202 | return false 203 | } 204 | 205 | func markAsFailed(_ error: Error) { 206 | Task { @MainActor in 207 | self.lastError = error 208 | self.status = .failed(error.localizedDescription) 209 | objectWillChange.send() 210 | } 211 | } 212 | 213 | func prepareForRetry() { 214 | Task { @MainActor in 215 | self.retryCount += 1 216 | self.status = .waiting 217 | self.progress = 0 218 | self.speed = 0 219 | self.downloadedSize = 0 220 | objectWillChange.send() 221 | } 222 | } 223 | 224 | init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String) { 225 | self.type = type 226 | self.fullPackageName = fullPackageName 227 | self.downloadSize = downloadSize 228 | self.downloadURL = downloadURL 229 | self.packageVersion = packageVersion 230 | } 231 | 232 | func updateProgress(downloadedSize: Int64, speed: Double) { 233 | Task { @MainActor in 234 | self.downloadedSize = downloadedSize 235 | self.speed = speed 236 | self.status = .downloading 237 | objectWillChange.send() 238 | } 239 | } 240 | 241 | func markAsCompleted() { 242 | Task { @MainActor in 243 | self.downloaded = true 244 | self.progress = 1.0 245 | self.speed = 0 246 | self.status = .completed 247 | self.downloadedSize = downloadSize 248 | objectWillChange.send() 249 | } 250 | } 251 | 252 | var formattedSize: String { 253 | ByteCountFormatter.string(fromByteCount: downloadSize, countStyle: .file) 254 | } 255 | 256 | var formattedDownloadedSize: String { 257 | ByteCountFormatter.string(fromByteCount: downloadedSize, countStyle: .file) 258 | } 259 | 260 | var formattedSpeed: String { 261 | ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .file) + "/s" 262 | } 263 | 264 | var hasValidSize: Bool { 265 | downloadSize > 0 266 | } 267 | 268 | func updateStatus(_ status: PackageStatus) { 269 | Task { @MainActor in 270 | self.status = status 271 | objectWillChange.send() 272 | } 273 | } 274 | 275 | enum CodingKeys: String, CodingKey { 276 | case id, type, fullPackageName, downloadSize, downloadURL, packageVersion 277 | } 278 | 279 | func encode(to encoder: Encoder) throws { 280 | var container = encoder.container(keyedBy: CodingKeys.self) 281 | try container.encode(id, forKey: .id) 282 | try container.encode(type, forKey: .type) 283 | try container.encode(fullPackageName, forKey: .fullPackageName) 284 | try container.encode(downloadSize, forKey: .downloadSize) 285 | try container.encode(downloadURL, forKey: .downloadURL) 286 | } 287 | 288 | required init(from decoder: Decoder) throws { 289 | let container = try decoder.container(keyedBy: CodingKeys.self) 290 | id = try container.decode(UUID.self, forKey: .id) 291 | type = try container.decode(String.self, forKey: .type) 292 | fullPackageName = try container.decode(String.self, forKey: .fullPackageName) 293 | downloadSize = try container.decode(Int64.self, forKey: .downloadSize) 294 | downloadURL = try container.decode(String.self, forKey: .downloadURL) 295 | packageVersion = try container.decode(String.self, forKey: .packageVersion) 296 | } 297 | } 298 | 299 | struct NetworkConstants { 300 | static let downloadTimeout: TimeInterval = 300 301 | static let maxRetryAttempts = 3 302 | static let retryDelay: UInt64 = 3_000_000_000 303 | static let bufferSize = 1024 * 1024 304 | static let maxConcurrentDownloads = 3 305 | static let progressUpdateInterval: TimeInterval = 1 306 | 307 | static func generateCookie() -> String { 308 | let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 309 | let randomString = (0..<26).map { _ in chars.randomElement()! } 310 | return "fg=\(String(randomString))======" 311 | } 312 | 313 | static var productsJSONURL: String { 314 | "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")/products/all" 315 | } 316 | 317 | static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications" 318 | 319 | static var adobeRequestHeaders: [String: String] { 320 | [ 321 | "x-adobe-app-id": "accc-apps-panel-desktop", 322 | "x-api-key": "Creative Cloud_v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")_4", 323 | "User-Agent": "Creative Cloud/6.4.0.361/Mac-15.1", 324 | "Cookie": generateCookie() 325 | ] 326 | } 327 | 328 | static let downloadHeaders = [ 329 | "User-Agent": "Creative Cloud" 330 | ] 331 | } 332 | -------------------------------------------------------------------------------- /Adobe Downloader/Commons/Statics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2024/10/30. 5 | // 6 | import Foundation 7 | import SwiftUI 8 | import AppKit 9 | 10 | 11 | struct AppStatics { 12 | static let supportedLanguages: [(code: String, name: String)] = [ 13 | ("en_US", "English (US)"), 14 | ("fr_FR", "Français"), 15 | ("de_DE", "Deutsch"), 16 | ("ja_JP", "日本語"), 17 | ("fr_CA", "Français (Canada)"), 18 | ("en_GB", "English (UK)"), 19 | ("nl_NL", "Nederlands"), 20 | ("it_IT", "Italiano"), 21 | ("es_ES", "Español"), 22 | ("ex_MX", "Español (Mexico)"), 23 | ("pt_BR", "Português (Brasil)"), 24 | ("pt_PT", "Português"), 25 | ("sv_SE", "Svenska"), 26 | ("da_DK", "Dansk"), 27 | ("fi_FI", "Suomi"), 28 | ("nb_NO", "Norsk"), 29 | ("zh_CN", "简体中文"), 30 | ("zh_TW", "繁體中文"), 31 | ("kr_KR", "한국어"), 32 | ("cs_CZ", "Čeština"), 33 | ("ht_HU", "Magyar"), 34 | ("pl_PL", "Polski"), 35 | ("ru_RU", "Русский"), 36 | ("uk_UA", "Українська"), 37 | ("tr_TR", "Türkçe"), 38 | ("ro_RO", "Romaân"), 39 | ("fr_MA", "Français (Maroc)"), 40 | ("en_AE", "English (UAE)"), 41 | ("en_IL", "English (Israel)"), 42 | ("ALL", "ALL") 43 | ] 44 | 45 | static let cpuArchitecture: String = { 46 | #if arch(arm64) 47 | return "Apple Silicon" 48 | #elseif arch(x86_64) 49 | return "Intel" 50 | #else 51 | return "Unknown Architecture" 52 | #endif 53 | }() 54 | 55 | static let isAppleSilicon: Bool = { 56 | #if arch(arm64) 57 | return true 58 | #elseif arch(x86_64) 59 | return false 60 | #else 61 | return false 62 | #endif 63 | }() 64 | 65 | static let architectureSymbol: String = { 66 | #if arch(arm64) 67 | return "arm64" 68 | #elseif arch(x86_64) 69 | return "x64" 70 | #else 71 | return "Unknown Architecture" 72 | #endif 73 | }() 74 | 75 | /// 比较两个版本号 76 | /// - Parameters: 77 | /// - version1: 第一个版本号 78 | /// - version2: 第二个版本号 79 | /// - Returns: 负值表示version1version2 80 | static func compareVersions(_ version1: String, _ version2: String) -> Int { 81 | let components1 = version1.split(separator: ".").map { Int($0) ?? 0 } 82 | let components2 = version2.split(separator: ".").map { Int($0) ?? 0 } 83 | 84 | let maxLength = max(components1.count, components2.count) 85 | let paddedComponents1 = components1 + Array(repeating: 0, count: maxLength - components1.count) 86 | let paddedComponents2 = components2 + Array(repeating: 0, count: maxLength - components2.count) 87 | 88 | for i in 0.. 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | $(PRODUCT_BUNDLE_IDENTIFIER) 7 | NSAppTransportSecurity 8 | 9 | NSAllowsArbitraryLoads 10 | 11 | 12 | SMPrivilegedExecutables 13 | 14 | com.x1a0he.macOS.Adobe-Downloader.helper 15 | identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ 16 | 17 | SUFeedURL 18 | https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml 19 | SUPublicEDKey 20 | gYylad3ybfiyK5ZTS3xRrw+3c/8063mpXdQnPpMB86Q= 21 | com.apple.security.files.downloads.read-write 22 | 23 | com.apple.security.files.user-selected.read-write 24 | 25 | com.apple.security.network.client 26 | 27 | com.apple.security.network.server 28 | 29 | com.apple.security.temporary-exception.files.home-relative-path.read-write 30 | 31 | / 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Adobe Downloader/Models/CheckForUpdatesViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckForUpdatesViewModel.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 11/6/24. 6 | // 7 | 8 | import SwiftUI 9 | import Sparkle 10 | 11 | final class CheckForUpdatesViewModel: ObservableObject { 12 | @Published var canCheckForUpdates = false 13 | 14 | init(updater: SPUUpdater) { 15 | updater.publisher(for: \.canCheckForUpdates) 16 | .assign(to: &$canCheckForUpdates) 17 | } 18 | } 19 | 20 | struct CheckForUpdatesView: View { 21 | @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel 22 | private let updater: SPUUpdater 23 | 24 | init(updater: SPUUpdater) { 25 | self.updater = updater 26 | 27 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) 28 | } 29 | 30 | var body: some View { 31 | Button(action: updater.checkForUpdates) { 32 | HStack(spacing: 4) { 33 | Image(systemName: "arrow.clockwise") 34 | .font(.system(size: 12)) 35 | Text("检查更新") 36 | .font(.system(size: 13)) 37 | } 38 | } 39 | .buttonStyle(BeautifulButtonStyle(baseColor: Color.blue.opacity(0.8))) 40 | .foregroundColor(.white) 41 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 42 | .opacity(!checkForUpdatesViewModel.canCheckForUpdates ? 0.6 : 1.0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Adobe Downloader/Models/NewDownloadTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewDownloadTask.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 2/26/25. 6 | // 7 | import Foundation 8 | 9 | class NewDownloadTask: Identifiable, ObservableObject { 10 | let id = UUID() 11 | var productId: String 12 | let productVersion: String 13 | let language: String 14 | let displayName: String 15 | let directory: URL 16 | var dependenciesToDownload: [DependenciesToDownload] 17 | var retryCount: Int 18 | let createAt: Date 19 | var displayInstallButton: Bool 20 | 21 | var platform: String 22 | 23 | @Published var totalStatus: DownloadStatus? 24 | @Published var totalProgress: Double 25 | @Published var totalDownloadedSize: Int64 26 | @Published var totalSize: Int64 27 | @Published var totalSpeed: Double 28 | @Published var completedPackages: Int = 0 29 | @Published var totalPackages: Int = 0 30 | @Published var currentPackage: Package? { 31 | didSet { 32 | objectWillChange.send() 33 | } 34 | } 35 | 36 | var status: DownloadStatus { 37 | totalStatus ?? .waiting 38 | } 39 | 40 | var destinationURL: URL { directory } 41 | 42 | var formattedTotalSize: String { 43 | ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) 44 | } 45 | 46 | var formattedDownloadedSize: String { 47 | ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file) 48 | } 49 | 50 | var startTime: Date { 51 | switch totalStatus { 52 | case .downloading(let info): return info.startTime 53 | case .completed(let info): return info.timestamp - info.totalTime 54 | case .preparing(let info): return info.timestamp 55 | case .paused(let info): return info.timestamp 56 | case .failed(let info): return info.timestamp 57 | case .retrying(let info): return info.nextRetryDate - 60 58 | case .waiting, .none: return createAt 59 | } 60 | } 61 | 62 | func setStatus(_ newStatus: DownloadStatus) { 63 | DispatchQueue.main.async { 64 | self.totalStatus = newStatus 65 | self.objectWillChange.send() 66 | } 67 | } 68 | 69 | func updateProgress(downloaded: Int64, total: Int64, speed: Double) { 70 | DispatchQueue.main.async { 71 | self.totalDownloadedSize = downloaded 72 | self.totalSize = total 73 | self.totalSpeed = speed 74 | self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0 75 | self.objectWillChange.send() 76 | } 77 | } 78 | 79 | init(productId: String, productVersion: String, language: String, displayName: String, directory: URL, dependenciesToDownload: [DependenciesToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) { 80 | self.productId = productId 81 | self.productVersion = productVersion 82 | self.language = language 83 | self.displayName = displayName 84 | self.directory = directory 85 | self.dependenciesToDownload = dependenciesToDownload 86 | self.retryCount = retryCount 87 | self.createAt = createAt 88 | self.totalStatus = totalStatus 89 | self.totalProgress = totalProgress 90 | self.totalDownloadedSize = totalDownloadedSize 91 | self.totalSize = totalSize 92 | self.totalSpeed = totalSpeed 93 | self.currentPackage = currentPackage 94 | self.displayInstallButton = productId != "APRO" 95 | self.platform = platform 96 | } 97 | 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Adobe Downloader/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Adobe Downloader/Scripts/clean-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # clean-config.sh 4 | # AdobeDownloader 5 | # 6 | # Created by X1a0He on 2024/11/15. 7 | # Copyright © 2024 X1a0He. All rights reserved. 8 | sudo /usr/bin/killall -u root -9 Adobe\ Downloader 9 | sudo /bin/launchctl unload /Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist 10 | sudo /bin/rm /Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist 11 | sudo /bin/rm /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper 12 | sudo /bin/rm -rf ~/Library/Application\ Support/Adobe\ Downloader 13 | sudo /bin/rm ~/Library/Preferences/com.x1a0he.macOS.Adobe-Downloader.plist 14 | sudo /usr/bin/killall -u root -9 com.x1a0he.macOS.Adobe-Downloader.helper 15 | sudo rm "$0" -------------------------------------------------------------------------------- /Adobe Downloader/Services/NewNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewNetworkService.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 2/26/25. 6 | // 7 | 8 | import Foundation 9 | 10 | class NewNetworkService { 11 | typealias ProductsData = ([Product], [UniqueProduct]) 12 | 13 | private func makeProductsURL() throws -> URL { 14 | var components = URLComponents(string: NetworkConstants.productsJSONURL) 15 | components?.queryItems = [ 16 | URLQueryItem(name: "channel", value: "ccm"), 17 | URLQueryItem(name: "channel", value: "sti"), 18 | URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"), 19 | URLQueryItem(name: "_type", value: "json"), 20 | URLQueryItem(name: "productType", value: "Desktop") 21 | ] 22 | 23 | guard let url = components?.url else { 24 | throw NetworkError.invalidURL(NetworkConstants.productsJSONURL) 25 | } 26 | return url 27 | } 28 | 29 | private func configureRequest(_ request: inout URLRequest, headers: [String: String]) { 30 | headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } 31 | } 32 | 33 | func fetchProductsData() async throws -> ProductsData { 34 | let url = try makeProductsURL() 35 | var request = URLRequest(url: url) 36 | request.httpMethod = "GET" 37 | configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders) 38 | 39 | let (data, response) = try await URLSession.shared.data(for: request) 40 | 41 | guard let httpResponse = response as? HTTPURLResponse else { 42 | throw NetworkError.invalidResponse 43 | } 44 | 45 | guard (200...299).contains(httpResponse.statusCode) else { 46 | throw NetworkError.httpError(httpResponse.statusCode, nil) 47 | } 48 | 49 | guard let jsonString = String(data: data, encoding: .utf8) else { 50 | throw NetworkError.invalidData("无法解码JSON数据") 51 | } 52 | 53 | let result: ProductsData = try await Task.detached(priority: .userInitiated) { 54 | try NewJSONParser.parse(jsonString: jsonString) 55 | 56 | let products = globalCcmResult.products 57 | 58 | if products.isEmpty { return ([], []) } 59 | 60 | let validProducts = products.filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) } 61 | 62 | var uniqueProductsDict = [String: UniqueProduct]() 63 | for product in validProducts { 64 | uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName) 65 | } 66 | let uniqueProducts = Array(uniqueProductsDict.values) 67 | 68 | return (products, uniqueProducts) 69 | }.value 70 | 71 | return result 72 | } 73 | 74 | func getApplicationInfo(buildGuid: String) async throws -> String { 75 | guard let url = URL(string: NetworkConstants.applicationJsonURL) else { 76 | throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL) 77 | } 78 | 79 | var request = URLRequest(url: url) 80 | request.httpMethod = "GET" 81 | 82 | var headers = NetworkConstants.adobeRequestHeaders 83 | headers["x-adobe-build-guid"] = buildGuid 84 | 85 | headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } 86 | 87 | let (data, response) = try await URLSession.shared.data(for: request) 88 | 89 | guard let httpResponse = response as? HTTPURLResponse else { 90 | throw NetworkError.invalidResponse 91 | } 92 | 93 | guard (200...299).contains(httpResponse.statusCode) else { 94 | throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8)) 95 | } 96 | 97 | guard let jsonString = String(data: data, encoding: .utf8) else { 98 | throw NetworkError.invalidData("无法将响应数据转换为json符串") 99 | } 100 | 101 | return jsonString 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Adobe Downloader/Storages/StorageData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageData.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 11/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class StorageData: ObservableObject { 11 | static let shared = StorageData() 12 | 13 | @Published var installedHelperBuild: String { 14 | didSet { 15 | UserDefaults.standard.set(installedHelperBuild, forKey: "InstalledHelperBuild") 16 | objectWillChange.send() 17 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 18 | } 19 | } 20 | 21 | @Published var downloadAppleSilicon: Bool { 22 | didSet { 23 | UserDefaults.standard.set(downloadAppleSilicon, forKey: "downloadAppleSilicon") 24 | objectWillChange.send() 25 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 26 | } 27 | } 28 | 29 | @Published var useDefaultLanguage: Bool { 30 | didSet { 31 | UserDefaults.standard.set(useDefaultLanguage, forKey: "useDefaultLanguage") 32 | objectWillChange.send() 33 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 34 | } 35 | } 36 | 37 | @Published var defaultLanguage: String { 38 | didSet { 39 | UserDefaults.standard.set(defaultLanguage, forKey: "defaultLanguage") 40 | objectWillChange.send() 41 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 42 | } 43 | } 44 | 45 | @Published var useDefaultDirectory: Bool { 46 | didSet { 47 | UserDefaults.standard.set(useDefaultDirectory, forKey: "useDefaultDirectory") 48 | objectWillChange.send() 49 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 50 | } 51 | } 52 | 53 | @Published var defaultDirectory: String { 54 | didSet { 55 | UserDefaults.standard.set(defaultDirectory, forKey: "defaultDirectory") 56 | objectWillChange.send() 57 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 58 | } 59 | } 60 | 61 | @Published var confirmRedownload: Bool { 62 | didSet { 63 | UserDefaults.standard.set(confirmRedownload, forKey: "confirmRedownload") 64 | objectWillChange.send() 65 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 66 | } 67 | } 68 | 69 | @Published var apiVersion: String { 70 | didSet { 71 | UserDefaults.standard.set(apiVersion, forKey: "apiVersion") 72 | objectWillChange.send() 73 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 74 | } 75 | } 76 | 77 | @Published var isFirstLaunch: Bool { 78 | didSet { 79 | UserDefaults.standard.set(isFirstLaunch, forKey: "isFirstLaunch") 80 | objectWillChange.send() 81 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 82 | } 83 | } 84 | 85 | var allowedPlatform: [String] { 86 | if downloadAppleSilicon { 87 | return ["macuniversal", "macarm64"] 88 | } else { 89 | return ["macuniversal", "osx10-64", "osx10"] 90 | } 91 | } 92 | 93 | private init() { 94 | let isFirstLaunchKey = "isFirstLaunch" 95 | if !UserDefaults.standard.contains(key: isFirstLaunchKey) { 96 | self.isFirstLaunch = true 97 | UserDefaults.standard.set(true, forKey: isFirstLaunchKey) 98 | } else { 99 | self.isFirstLaunch = UserDefaults.standard.bool(forKey: isFirstLaunchKey) 100 | } 101 | 102 | self.installedHelperBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") ?? "0" 103 | self.downloadAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon") 104 | self.useDefaultLanguage = UserDefaults.standard.bool(forKey: "useDefaultLanguage") 105 | self.defaultLanguage = UserDefaults.standard.string(forKey: "defaultLanguage") ?? "ALL" 106 | self.useDefaultDirectory = UserDefaults.standard.bool(forKey: "useDefaultDirectory") 107 | self.defaultDirectory = UserDefaults.standard.string(forKey: "defaultDirectory") ?? "" 108 | self.confirmRedownload = UserDefaults.standard.bool(forKey: "confirmRedownload") 109 | self.apiVersion = UserDefaults.standard.string(forKey: "apiVersion") ?? "6" 110 | } 111 | } 112 | 113 | @propertyWrapper 114 | struct StorageValue: DynamicProperty { 115 | @ObservedObject private var storage = StorageData.shared 116 | private let keyPath: ReferenceWritableKeyPath 117 | 118 | var wrappedValue: T { 119 | get { storage[keyPath: keyPath] } 120 | nonmutating set { 121 | storage[keyPath: keyPath] = newValue 122 | } 123 | } 124 | 125 | var projectedValue: Binding { 126 | Binding( 127 | get: { storage[keyPath: keyPath] }, 128 | set: { storage[keyPath: keyPath] = $0 } 129 | ) 130 | } 131 | 132 | init(_ keyPath: ReferenceWritableKeyPath) { 133 | self.keyPath = keyPath 134 | } 135 | } 136 | 137 | extension Notification.Name { 138 | static let storageDidChange = Notification.Name("storageDidChange") 139 | } 140 | 141 | extension UserDefaults { 142 | func contains(key: String) -> Bool { 143 | return object(forKey: key) != nil 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /Adobe Downloader/Utils/CancelTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2024/10/30. 5 | // 6 | import Foundation 7 | 8 | actor CancelTracker { 9 | private var cancelledIds: Set = [] 10 | private var pausedIds: Set = [] 11 | var downloadTasks: [UUID: URLSessionDownloadTask] = [:] 12 | private var sessions: [UUID: URLSession] = [:] 13 | private var resumeData: [UUID: Data] = [:] 14 | 15 | func registerTask(_ id: UUID, task: URLSessionDownloadTask, session: URLSession) { 16 | downloadTasks[id] = task 17 | sessions[id] = session 18 | } 19 | 20 | func cancel(_ id: UUID) { 21 | cancelledIds.insert(id) 22 | pausedIds.remove(id) 23 | resumeData.removeValue(forKey: id) 24 | 25 | if let task = downloadTasks[id] { 26 | task.cancel() 27 | downloadTasks.removeValue(forKey: id) 28 | } 29 | 30 | if let session = sessions[id] { 31 | session.invalidateAndCancel() 32 | sessions.removeValue(forKey: id) 33 | } 34 | } 35 | 36 | func pause(_ id: UUID) async { 37 | if !cancelledIds.contains(id) { 38 | pausedIds.insert(id) 39 | if let task = downloadTasks[id] { 40 | let data = await withCheckedContinuation { continuation in 41 | task.cancel(byProducingResumeData: { data in 42 | continuation.resume(returning: data) 43 | }) 44 | } 45 | if let data = data { 46 | resumeData[id] = data 47 | } 48 | } 49 | } 50 | } 51 | 52 | func getResumeData(_ id: UUID) -> Data? { 53 | return resumeData[id] 54 | } 55 | 56 | func clearResumeData(_ id: UUID) { 57 | resumeData.removeValue(forKey: id) 58 | } 59 | 60 | func isCancelled(_ id: UUID) -> Bool { 61 | return cancelledIds.contains(id) 62 | } 63 | 64 | func isPaused(_ id: UUID) -> Bool { 65 | return pausedIds.contains(id) 66 | } 67 | 68 | func storeResumeData(_ id: UUID, data: Data) { 69 | resumeData[id] = data 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Adobe Downloader/Utils/InstallManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2024/10/30. 5 | // 6 | /* 7 | Adobe Exit Code 8 | 107: 架构不一致或安装文件被损坏 9 | 103: 权限问题 10 | 182: 表示创建的包不包含要安装的包 11 | 133: 磁盘空间不足 12 | */ 13 | import Foundation 14 | 15 | actor InstallManager { 16 | enum InstallError: Error, LocalizedError { 17 | case setupNotFound 18 | case installationFailed(String) 19 | case cancelled 20 | case permissionDenied 21 | case installationFailedWithDetails(String, String) 22 | 23 | var errorDescription: String? { 24 | switch self { 25 | case .setupNotFound: return String(localized: "找不到安装程序") 26 | case .installationFailed(let message): return message 27 | case .cancelled: return String(localized: "安装已取消") 28 | case .permissionDenied: return String(localized: "权限被拒绝") 29 | case .installationFailedWithDetails(let message, _): return message 30 | } 31 | } 32 | } 33 | 34 | private var installationProcess: Process? 35 | private var progressHandler: ((Double, String) -> Void)? 36 | private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" 37 | 38 | actor InstallationState { 39 | var isCompleted = false 40 | var error: Error? 41 | var hasExitCode0 = false 42 | var lastOutputTime = Date() 43 | 44 | func markCompleted() { 45 | isCompleted = true 46 | } 47 | 48 | func setError(_ error: Error) { 49 | if !isCompleted { 50 | self.error = error 51 | isCompleted = true 52 | } 53 | } 54 | 55 | func setExitCode0() { 56 | hasExitCode0 = true 57 | } 58 | 59 | func updateLastOutputTime() { 60 | lastOutputTime = Date() 61 | } 62 | 63 | func getTimeSinceLastOutput() -> TimeInterval { 64 | return Date().timeIntervalSince(lastOutputTime) 65 | } 66 | 67 | var shouldContinue: Bool { 68 | !isCompleted 69 | } 70 | 71 | var hasReceivedExitCode0: Bool { 72 | hasExitCode0 73 | } 74 | } 75 | 76 | private func getAdobeInstallLogDetails() async -> String? { 77 | let logPath = "/Library/Logs/Adobe/Installers/Install.log" 78 | guard FileManager.default.fileExists(atPath: logPath) else { 79 | return nil 80 | } 81 | 82 | do { 83 | let logContent = try String(contentsOfFile: logPath, encoding: .utf8) 84 | let lines = logContent.components(separatedBy: .newlines) 85 | 86 | let fatalLines = lines.filter { 87 | line in line.contains("FATAL:") 88 | } 89 | 90 | var uniqueLines: [String] = [] 91 | var seen = Set() 92 | 93 | for line in fatalLines { 94 | if !seen.contains(line) { 95 | seen.insert(line) 96 | uniqueLines.append(line) 97 | } 98 | } 99 | 100 | if uniqueLines.isEmpty, lines.count > 10 { 101 | uniqueLines = Array(lines.suffix(10)) 102 | } 103 | 104 | if !uniqueLines.isEmpty { 105 | return uniqueLines.joined(separator: "\n") 106 | } 107 | 108 | return nil 109 | } catch { 110 | print("读取安装日志失败: \(error.localizedDescription)") 111 | return nil 112 | } 113 | } 114 | 115 | private func executeInstallation( 116 | at appPath: URL, 117 | progressHandler: @escaping (Double, String) -> Void 118 | ) async throws { 119 | guard FileManager.default.fileExists(atPath: setupPath) else { 120 | throw InstallError.setupNotFound 121 | } 122 | 123 | let driverPath = appPath.appendingPathComponent("driver.xml").path 124 | guard FileManager.default.fileExists(atPath: driverPath) else { 125 | throw InstallError.installationFailed("找不到 driver.xml 文件") 126 | } 127 | 128 | let attributes = try? FileManager.default.attributesOfItem(atPath: driverPath) 129 | if let permissions = attributes?[.posixPermissions] as? NSNumber { 130 | if permissions.int16Value & 0o444 == 0 { 131 | throw InstallError.installationFailed("driver.xml 文件没有读取权限") 132 | } 133 | } 134 | 135 | await MainActor.run { 136 | progressHandler(0.0, String(localized: "正在清理安装环境...")) 137 | } 138 | 139 | let logFiles = [ 140 | "/Library/Logs/Adobe/Installers/Install.log", 141 | ] 142 | 143 | for logFile in logFiles { 144 | let removeCommand = "rm -f '\(logFile)'" 145 | let result = await withCheckedContinuation { continuation in 146 | PrivilegedHelperManager.shared.executeCommand(removeCommand) { result in 147 | continuation.resume(returning: result) 148 | } 149 | } 150 | 151 | if result.contains("Error") { 152 | print("清理安装日志失败: \(logFile) - \(result)") 153 | } 154 | } 155 | 156 | let installCommand = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\"" 157 | 158 | await MainActor.run { 159 | progressHandler(0.0, String(localized: "正在准备安装...")) 160 | } 161 | 162 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 163 | Task.detached { 164 | do { 165 | try await PrivilegedHelperManager.shared.executeInstallation(installCommand) { output in 166 | Task { @MainActor in 167 | if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression), 168 | let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces), 169 | let exitCode = Int(codeStr) { 170 | 171 | if exitCode == 0 { 172 | progressHandler(1.0, String(localized: "安装完成")) 173 | PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } 174 | continuation.resume() 175 | return 176 | } else { 177 | let errorMessage: String 178 | errorMessage = String(localized: "错误代码(\(exitCode)),请查看日志详情并向开发者汇报") 179 | if let logDetails = await self.getAdobeInstallLogDetails() { 180 | continuation.resume(throwing: InstallError.installationFailedWithDetails(errorMessage, logDetails)) 181 | } else { 182 | continuation.resume(throwing: InstallError.installationFailed(errorMessage)) 183 | } 184 | return 185 | } 186 | } 187 | 188 | if let progress = await self.parseProgress(from: output) { 189 | progressHandler(progress, String(localized: "正在安装...")) 190 | } 191 | } 192 | } 193 | } catch { 194 | continuation.resume(throwing: error) 195 | } 196 | } 197 | } 198 | } 199 | 200 | private func parseProgress(from output: String) -> Double? { 201 | if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression), 202 | let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces), 203 | let exitCode = Int(codeStr) { 204 | if exitCode == 0 { 205 | return 1.0 206 | } 207 | } 208 | 209 | if output.range(of: "Progress:\\s*[0-9]+/[0-9]+", options: .regularExpression) != nil { 210 | return nil 211 | } 212 | 213 | if let range = output.range(of: "Progress:\\s*([0-9]{1,3})%", options: .regularExpression), 214 | let progressStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces), 215 | let progressValue = Double(progressStr.replacingOccurrences(of: "%", with: "")) { 216 | return progressValue / 100.0 217 | } 218 | return nil 219 | } 220 | 221 | func install( 222 | at appPath: URL, 223 | progressHandler: @escaping (Double, String) -> Void 224 | ) async throws { 225 | try await executeInstallation( 226 | at: appPath, 227 | progressHandler: progressHandler 228 | ) 229 | } 230 | 231 | func cancel() { 232 | PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } 233 | } 234 | 235 | func getInstallCommand(for driverPath: String) -> String { 236 | return "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\"" 237 | } 238 | 239 | func retry( 240 | at appPath: URL, 241 | progressHandler: @escaping (Double, String) -> Void 242 | ) async throws { 243 | cancel() 244 | try await Task.sleep(nanoseconds: 1_000_000_000) 245 | 246 | try await executeInstallation( 247 | at: appPath, 248 | progressHandler: progressHandler 249 | ) 250 | } 251 | } 252 | 253 | -------------------------------------------------------------------------------- /Adobe Downloader/Utils/ModifySetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifySetup.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 11/5/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class ModifySetup { 12 | private static var cachedVersion: String? 13 | 14 | static func isSetupExists() -> Bool { 15 | return FileManager.default.fileExists(atPath: "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup") 16 | } 17 | 18 | static func isSetupBackup() -> Bool { 19 | return isSetupExists() && FileManager.default.fileExists(atPath: "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup.original") 20 | } 21 | 22 | static func checkComponentVersion() -> String { 23 | let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" 24 | 25 | guard FileManager.default.fileExists(atPath: setupPath) else { 26 | cachedVersion = nil 27 | return String(localized: "未找到 Setup 组件") 28 | } 29 | 30 | if let cachedVersion = cachedVersion { 31 | return cachedVersion 32 | } 33 | 34 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: setupPath)) else { 35 | return "Unknown" 36 | } 37 | 38 | let versionMarkers = ["Version ", "Adobe Setup Version: "] 39 | 40 | for marker in versionMarkers { 41 | if let markerData = marker.data(using: .utf8), 42 | let markerRange = data.range(of: markerData) { 43 | let versionStart = markerRange.upperBound 44 | let searchRange = versionStart..= 0x30 && byte <= 0x39) || byte == 0x2E || byte == 0x20 { 50 | versionBytes.append(byte) 51 | } else if byte == 0x28 { 52 | break 53 | } else if versionBytes.isEmpty { 54 | continue 55 | } else { break } 56 | } 57 | 58 | if let version = String(bytes: versionBytes, encoding: .utf8)?.trimmingCharacters(in: .whitespaces), 59 | !version.isEmpty { 60 | cachedVersion = version 61 | return version 62 | } 63 | } 64 | } 65 | 66 | let message = String(localized: "未知 Setup 组件版本号") 67 | cachedVersion = message 68 | return message 69 | } 70 | 71 | static func clearVersionCache() { 72 | cachedVersion = nil 73 | } 74 | 75 | static func backupSetupFile(completion: @escaping (Bool) -> Void) { 76 | let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" 77 | let backupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup.original" 78 | 79 | if !FileManager.default.fileExists(atPath: setupPath) { 80 | print("Setup 文件不存在: \(setupPath)") 81 | completion(false) 82 | return 83 | } 84 | 85 | if isSetupBackup() { 86 | print("检测到备份文件,尝试从备份恢复...") 87 | PrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in 88 | if result.starts(with: "Error:") { 89 | print("从备份恢复失败: \(result)") 90 | } 91 | completion(!result.starts(with: "Error:")) 92 | } 93 | } else { 94 | PrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(setupPath)' '\(backupPath)'") { result in 95 | if result.starts(with: "Error:") { 96 | print("创建备份失败: \(result)") 97 | completion(false) 98 | return 99 | } 100 | 101 | if !result.starts(with: "Error:") { 102 | if FileManager.default.fileExists(atPath: backupPath) { 103 | PrivilegedHelperManager.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in 104 | if chmodResult.starts(with: "Error:") { 105 | print("设置备份文件权限失败: \(chmodResult)") 106 | } 107 | completion(true) 108 | } 109 | } else { 110 | print("备份文件创建失败:文件不存在") 111 | completion(false) 112 | } 113 | } else { 114 | completion(false) 115 | } 116 | } 117 | } 118 | } 119 | 120 | static func modifySetupFile(completion: @escaping (Bool) -> Void) { 121 | let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" 122 | 123 | let commands = [ 124 | """ 125 | perl -0777pi -e 'BEGIN{$/=\\1e8} s|\\x55\\x48\\x89\\xE5\\x41\\x57\\x41\\x56\\x41\\x55\\x41\\x54\\x53\\x48\\x83\\xEC\\x48\\x48\\x89\\xFB\\x48\\x8B\\x05\\x8B\\x82\\x05\\x00\\x48\\x8B\\x00\\x48\\x89\\x45\\xD0\\x48\\x8D|\\x6A\\x01\\x58\\xC3\\x53\\x50\\x48\\x89\\xFB\\x48\\x8B\\x05\\x70\\xC7\\x03\\x00\\x48\\x8B\\x00\\x48\\x89\\x45\\xF0\\xE8\\x24\\xD7\\xFE\\xFF\\x48\\x83\\xC3\\x08\\x48\\x39\\xD8\\x0F|gs' '\(setupPath)' 126 | """, 127 | """ 128 | perl -0777pi -e 'BEGIN{$/=\\1e8} s|\\xFF\\x43\\x02\\xD1\\xFA\\x67\\x04\\xA9\\xF8\\x5F\\x05\\xA9\\xF6\\x57\\x06\\xA9\\xF4\\x4F\\x07\\xA9\\xFD\\x7B\\x08\\xA9\\xFD\\x03\\x02\\x91\\xF3\\x03\\x00\\xAA\\x1F\\x20\\x03\\xD5|\\x20\\x00\\x80\\xD2\\xC0\\x03\\x5F\\xD6\\xFD\\x7B\\x02\\xA9\\xFD\\x83\\x00\\x91\\xF3\\x03\\x00\\xAA\\x1F\\x20\\x03\\xD5\\x68\\xA1\\x1D\\x58\\x08\\x01\\x40\\xF9\\xE8\\x07\\x00\\xF9|gs' '\(setupPath)' 129 | """, 130 | "codesign --remove-signature '\(setupPath)'", 131 | "codesign -f -s - --timestamp=none --all-architectures --deep '\(setupPath)'", 132 | "xattr -cr '\(setupPath)'" 133 | ] 134 | 135 | func executeNextCommand(_ index: Int) { 136 | guard index < commands.count else { 137 | completion(true) 138 | return 139 | } 140 | 141 | PrivilegedHelperManager.shared.executeCommand(commands[index]) { result in 142 | if result.starts(with: "Error:") { 143 | print("命令执行失败: \(commands[index])") 144 | print("错误信息: \(result)") 145 | completion(false) 146 | return 147 | } 148 | executeNextCommand(index + 1) 149 | } 150 | } 151 | 152 | executeNextCommand(0) 153 | } 154 | 155 | static func backupAndModifySetupFile(completion: @escaping (Bool, String) -> Void) { 156 | DispatchQueue.global(qos: .userInitiated).async { 157 | if !isSetupExists() { 158 | DispatchQueue.main.async { 159 | completion(false, String(localized: "未找到 Setup 组件")) 160 | } 161 | return 162 | } 163 | 164 | backupSetupFile { backupSuccess in 165 | if !backupSuccess { 166 | DispatchQueue.main.async { 167 | completion(false, String(localized: "备份 Setup 组件失败")) 168 | } 169 | return 170 | } 171 | 172 | modifySetupFile { modifySuccess in 173 | DispatchQueue.main.async { 174 | if modifySuccess { 175 | completion(true, String(localized: "所有操作已成功完成")) 176 | } else { 177 | completion(false, String(localized: "修改 Setup 组件失败")) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | static func isSetupModified() -> Bool { 186 | let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup" 187 | 188 | guard FileManager.default.fileExists(atPath: setupPath) else { return false } 189 | 190 | let intelPattern = Data([0x55, 0x48, 0x89, 0xE5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xEC, 0x48, 0x48, 0x89, 0xFB, 0x48, 0x8B, 0x05, 0x8B, 0x82, 0x05, 0x00, 0x48, 0x8B, 0x00, 0x48, 0x89, 0x45, 0xD0, 0x48, 0x8D]) 191 | 192 | let armPattern = Data([0xFF, 0x43, 0x02, 0xD1, 0xFA, 0x67, 0x04, 0xA9, 0xF8, 0x5F, 0x05, 0xA9, 0xF6, 0x57, 0x06, 0xA9, 0xF4, 0x4F, 0x07, 0xA9, 0xFD, 0x7B, 0x08, 0xA9, 0xFD, 0x03, 0x02, 0x91, 0xF3, 0x03, 0x00, 0xAA, 0x1F, 0x20, 0x03, 0xD5]) 193 | 194 | do { 195 | let fileData = try Data(contentsOf: URL(fileURLWithPath: setupPath)) 196 | if fileData.range(of: intelPattern) != nil || fileData.range(of: armPattern) != nil { return false } 197 | return true 198 | 199 | } catch { 200 | print("Error reading Setup file: \(error)") 201 | return false 202 | } 203 | } 204 | } 205 | 206 | -------------------------------------------------------------------------------- /Adobe Downloader/Utils/TaskPersistenceManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class TaskPersistenceManager { 4 | static let shared = TaskPersistenceManager() 5 | 6 | private let fileManager = FileManager.default 7 | private var tasksDirectory: URL 8 | private weak var cancelTracker: CancelTracker? 9 | private var taskCache: [String: NewDownloadTask] = [:] 10 | 11 | private init() { 12 | let containerURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 13 | tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true) 14 | try? fileManager.createDirectory(at: tasksDirectory, withIntermediateDirectories: true) 15 | } 16 | 17 | func setCancelTracker(_ tracker: CancelTracker) { 18 | self.cancelTracker = tracker 19 | } 20 | 21 | private func getTaskFileName(productId: String, version: String, language: String, platform: String) -> String { 22 | return productId == "APRO" 23 | ? "Adobe Downloader \(productId)_\(version)_\(platform)-task.json" 24 | : "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)-task.json" 25 | } 26 | 27 | func saveTask(_ task: NewDownloadTask) async { 28 | let fileName = getTaskFileName( 29 | productId: task.productId, 30 | version: task.productVersion, 31 | language: task.language, 32 | platform: task.platform 33 | ) 34 | taskCache[fileName] = task 35 | let fileURL = tasksDirectory.appendingPathComponent(fileName) 36 | 37 | var resumeDataDict: [String: Data]? = nil 38 | 39 | if let currentPackage = task.currentPackage, 40 | let cancelTracker = self.cancelTracker, 41 | let resumeData = await cancelTracker.getResumeData(task.id) { 42 | resumeDataDict = [currentPackage.id.uuidString: resumeData] 43 | } 44 | 45 | let taskData = TaskData( 46 | sapCode: task.productId, 47 | version: task.productVersion, 48 | language: task.language, 49 | displayName: task.displayName, 50 | directory: task.directory, 51 | productsToDownload: task.dependenciesToDownload.map { product in 52 | ProductData( 53 | sapCode: product.sapCode, 54 | version: product.version, 55 | buildGuid: product.buildGuid, 56 | applicationJson: product.applicationJson, 57 | packages: product.packages.map { package in 58 | PackageData( 59 | type: package.type, 60 | fullPackageName: package.fullPackageName, 61 | downloadSize: package.downloadSize, 62 | downloadURL: package.downloadURL, 63 | downloadedSize: package.downloadedSize, 64 | progress: package.progress, 65 | speed: package.speed, 66 | status: package.status, 67 | downloaded: package.downloaded, 68 | packageVersion: package.packageVersion 69 | ) 70 | } 71 | ) 72 | }, 73 | retryCount: task.retryCount, 74 | createAt: task.createAt, 75 | totalStatus: task.totalStatus ?? .waiting, 76 | totalProgress: task.totalProgress, 77 | totalDownloadedSize: task.totalDownloadedSize, 78 | totalSize: task.totalSize, 79 | totalSpeed: task.totalSpeed, 80 | displayInstallButton: task.displayInstallButton, 81 | platform: task.platform, 82 | resumeData: resumeDataDict 83 | ) 84 | 85 | do { 86 | let encoder = JSONEncoder() 87 | encoder.outputFormatting = .prettyPrinted 88 | let data = try encoder.encode(taskData) 89 | // print("保存数据") 90 | try data.write(to: fileURL) 91 | } catch { 92 | print("Error saving task: \(error)") 93 | } 94 | } 95 | 96 | func loadTasks() async -> [NewDownloadTask] { 97 | var tasks: [NewDownloadTask] = [] 98 | 99 | do { 100 | let files = try fileManager.contentsOfDirectory(at: tasksDirectory, includingPropertiesForKeys: nil) 101 | for file in files where file.pathExtension == "json" { 102 | let fileName = file.lastPathComponent 103 | if let cachedTask = taskCache[fileName] { 104 | tasks.append(cachedTask) 105 | } else if let task = await loadTask(from: file) { 106 | taskCache[fileName] = task 107 | tasks.append(task) 108 | } 109 | } 110 | } catch { 111 | print("Error loading tasks: \(error)") 112 | } 113 | 114 | return tasks 115 | } 116 | 117 | private func loadTask(from url: URL) async -> NewDownloadTask? { 118 | do { 119 | let data = try Data(contentsOf: url) 120 | let decoder = JSONDecoder() 121 | let taskData = try decoder.decode(TaskData.self, from: data) 122 | 123 | let products = taskData.productsToDownload.map { productData -> DependenciesToDownload in 124 | let product = DependenciesToDownload( 125 | sapCode: productData.sapCode, 126 | version: productData.version, 127 | buildGuid: productData.buildGuid, 128 | applicationJson: productData.applicationJson ?? "" 129 | ) 130 | 131 | product.packages = productData.packages.map { packageData -> Package in 132 | let package = Package( 133 | type: packageData.type, 134 | fullPackageName: packageData.fullPackageName, 135 | downloadSize: packageData.downloadSize, 136 | downloadURL: packageData.downloadURL, 137 | packageVersion: packageData.packageVersion 138 | ) 139 | package.downloadedSize = packageData.downloadedSize 140 | package.progress = packageData.progress 141 | package.speed = packageData.speed 142 | package.status = packageData.status 143 | package.downloaded = packageData.downloaded 144 | return package 145 | } 146 | 147 | return product 148 | } 149 | 150 | for product in products { 151 | for package in product.packages { 152 | package.speed = 0 153 | } 154 | } 155 | 156 | let initialStatus: DownloadStatus 157 | switch taskData.totalStatus { 158 | case .completed(let info): 159 | initialStatus = .completed(info) 160 | case .failed(let info): 161 | initialStatus = .failed(info) 162 | case .downloading: 163 | initialStatus = .paused(DownloadStatus.PauseInfo( 164 | reason: .other(String(localized: "程序退出")), 165 | timestamp: Date(), 166 | resumable: true 167 | )) 168 | default: 169 | initialStatus = .paused(DownloadStatus.PauseInfo( 170 | reason: .other(String(localized: "程序重启后自动暂停")), 171 | timestamp: Date(), 172 | resumable: true 173 | )) 174 | } 175 | 176 | let task = NewDownloadTask( 177 | productId: taskData.sapCode, 178 | productVersion: taskData.version, 179 | language: taskData.language, 180 | displayName: taskData.displayName, 181 | directory: taskData.directory, 182 | dependenciesToDownload: products, 183 | retryCount: taskData.retryCount, 184 | createAt: taskData.createAt, 185 | totalStatus: initialStatus, 186 | totalProgress: taskData.totalProgress, 187 | totalDownloadedSize: taskData.totalDownloadedSize, 188 | totalSize: taskData.totalSize, 189 | totalSpeed: 0, 190 | currentPackage: products.first?.packages.first, 191 | platform: taskData.platform 192 | ) 193 | task.displayInstallButton = taskData.displayInstallButton 194 | 195 | if let resumeData = taskData.resumeData?.values.first { 196 | Task { 197 | if let cancelTracker = self.cancelTracker { 198 | await cancelTracker.storeResumeData(task.id, data: resumeData) 199 | } 200 | } 201 | } 202 | 203 | return task 204 | } catch { 205 | print("Error loading task from \(url): \(error)") 206 | return nil 207 | } 208 | } 209 | 210 | func removeTask(_ task: NewDownloadTask) { 211 | let fileName = getTaskFileName( 212 | productId: task.productId, 213 | version: task.productVersion, 214 | language: task.language, 215 | platform: task.platform 216 | ) 217 | let fileURL = tasksDirectory.appendingPathComponent(fileName) 218 | 219 | taskCache.removeValue(forKey: fileName) 220 | try? fileManager.removeItem(at: fileURL) 221 | } 222 | 223 | func createExistingProgramTask(productId: String, version: String, language: String, displayName: String, platform: String, directory: URL) async { 224 | let fileName = getTaskFileName( 225 | productId: productId, 226 | version: version, 227 | language: language, 228 | platform: platform 229 | ) 230 | 231 | let product = DependenciesToDownload( 232 | sapCode: productId, 233 | version: version, 234 | buildGuid: "", 235 | applicationJson: "" 236 | ) 237 | 238 | let package = Package( 239 | type: "", 240 | fullPackageName: "", 241 | downloadSize: 0, 242 | downloadURL: "", 243 | packageVersion: version 244 | ) 245 | package.downloaded = true 246 | package.progress = 1.0 247 | package.status = .completed 248 | 249 | product.packages = [package] 250 | 251 | let task = NewDownloadTask( 252 | productId: productId, 253 | productVersion: version, 254 | language: language, 255 | displayName: displayName, 256 | directory: directory, 257 | dependenciesToDownload: [product], 258 | retryCount: 0, 259 | createAt: Date(), 260 | totalStatus: .completed(DownloadStatus.CompletionInfo( 261 | timestamp: Date(), 262 | totalTime: 0, 263 | totalSize: 0 264 | )), 265 | totalProgress: 1.0, 266 | totalDownloadedSize: 0, 267 | totalSize: 0, 268 | totalSpeed: 0, 269 | currentPackage: package, 270 | platform: platform 271 | ) 272 | task.displayInstallButton = true 273 | 274 | taskCache[fileName] = task 275 | await saveTask(task) 276 | } 277 | } 278 | 279 | private struct TaskData: Codable { 280 | let sapCode: String 281 | let version: String 282 | let language: String 283 | let displayName: String 284 | let directory: URL 285 | let productsToDownload: [ProductData] 286 | let retryCount: Int 287 | let createAt: Date 288 | let totalStatus: DownloadStatus 289 | let totalProgress: Double 290 | let totalDownloadedSize: Int64 291 | let totalSize: Int64 292 | let totalSpeed: Double 293 | let displayInstallButton: Bool 294 | let platform: String 295 | let resumeData: [String: Data]? 296 | } 297 | 298 | private struct ProductData: Codable { 299 | let sapCode: String 300 | let version: String 301 | let buildGuid: String 302 | let applicationJson: String? 303 | let packages: [PackageData] 304 | } 305 | 306 | private struct PackageData: Codable { 307 | let type: String 308 | let fullPackageName: String 309 | let downloadSize: Int64 310 | let downloadURL: String 311 | let downloadedSize: Int64 312 | let progress: Double 313 | let speed: Double 314 | let status: PackageStatus 315 | let downloaded: Bool 316 | let packageVersion: String 317 | } 318 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/BannerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BannerView: View { 4 | var body: some View { 5 | HStack(spacing: 8) { 6 | Image(systemName: "exclamationmark.triangle.fill") 7 | .foregroundColor(.orange) 8 | Text("Adobe Downloader 完全开源免费: https://github.com/X1a0He/Adobe-Downloader") 9 | } 10 | .frame(maxWidth: .infinity, alignment: .center) 11 | .padding(.horizontal) 12 | .padding(.bottom, 5) 13 | .background(Color(.clear)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/CleanConfigView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanConfigView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 3/28/25. 6 | // 7 | import SwiftUI 8 | 9 | struct CleanConfigView: View { 10 | @State private var showConfirmation = false 11 | @State private var showAlert = false 12 | @State private var alertMessage = "" 13 | @State private var chipInfo: String = "" 14 | 15 | private func getChipInfo() -> String { 16 | var size = 0 17 | sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0) 18 | var machine = [CChar](repeating: 0, count: size) 19 | sysctlbyname("machdep.cpu.brand_string", &machine, &size, nil, 0) 20 | let chipName = String(cString: machine) 21 | 22 | if chipName.contains("Apple") { 23 | return chipName 24 | } else { 25 | return chipName.components(separatedBy: "@")[0].trimmingCharacters(in: .whitespaces) 26 | } 27 | } 28 | 29 | var body: some View { 30 | HStack(spacing: 16) { 31 | BeautifulGroupBox(label: { 32 | Text("重置程序") 33 | }) { 34 | VStack(alignment: .leading, spacing: 8) { 35 | HStack { 36 | Button("重置程序") { 37 | showConfirmation = true 38 | } 39 | .buttonStyle(BeautifulButtonStyle(baseColor: .red.opacity(0.8))) 40 | .foregroundColor(.white) 41 | } 42 | .fixedSize(horizontal: false, vertical: true) 43 | } 44 | } 45 | 46 | BeautifulGroupBox(label: { 47 | Text("系统信息") 48 | }) { 49 | VStack(alignment: .leading, spacing: 8) { 50 | HStack(spacing: 8) { 51 | Image(systemName: "desktopcomputer") 52 | .foregroundColor(.blue) 53 | .imageScale(.medium) 54 | .frame(width: 22, height: 22) 55 | .background(Circle().fill(Color.blue.opacity(0.1)).frame(width: 28, height: 28)) 56 | 57 | VStack(alignment: .leading, spacing: 1) { 58 | Text("macOS \(ProcessInfo.processInfo.operatingSystemVersionString)") 59 | .fontWeight(.medium) 60 | 61 | Text("\(chipInfo.isEmpty ? "加载中..." : chipInfo)") 62 | .foregroundColor(.secondary) 63 | .font(.system(size: 12)) 64 | } 65 | Spacer() 66 | } 67 | } 68 | } 69 | } 70 | .alert("确认重置程序", isPresented: $showConfirmation) { 71 | Button("取消", role: .cancel) { } 72 | Button("确定", role: .destructive) { 73 | cleanConfig() 74 | } 75 | } message: { 76 | Text("这将清空所有配置并结束应用程序,确定要继续吗?") 77 | } 78 | .alert("操作结果", isPresented: $showAlert) { 79 | Button("确定") { } 80 | } message: { 81 | Text(alertMessage) 82 | } 83 | .onAppear { 84 | chipInfo = getChipInfo() 85 | } 86 | } 87 | 88 | private func cleanConfig() { 89 | do { 90 | let downloadsURL = try FileManager.default.url(for: .downloadsDirectory, 91 | in: .userDomainMask, 92 | appropriateFor: nil, 93 | create: false) 94 | let scriptURL = downloadsURL.appendingPathComponent("clean-config.sh") 95 | 96 | guard let scriptPath = Bundle.main.path(forResource: "clean-config", ofType: "sh"), 97 | let scriptContent = try? String(contentsOfFile: scriptPath, encoding: .utf8) else { 98 | throw NSError(domain: "ScriptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法读取脚本文件"]) 99 | } 100 | 101 | try scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8) 102 | 103 | try FileManager.default.setAttributes([.posixPermissions: 0o755], 104 | ofItemAtPath: scriptURL.path) 105 | 106 | if PrivilegedHelperManager.getHelperStatus { 107 | PrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in 108 | if output.starts(with: "Error") { 109 | alertMessage = "清空配置失败: \(output)" 110 | showAlert = true 111 | } else { 112 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 113 | exit(0) 114 | } 115 | } 116 | } 117 | } else { 118 | let terminalURL = URL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app") 119 | NSWorkspace.shared.open([scriptURL], 120 | withApplicationAt: terminalURL, 121 | configuration: NSWorkspace.OpenConfiguration()) { _, error in 122 | if let error = error { 123 | alertMessage = "打开终端失败: \(error.localizedDescription)" 124 | showAlert = true 125 | return 126 | } 127 | 128 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 129 | exit(0) 130 | } 131 | } 132 | } 133 | 134 | } catch { 135 | alertMessage = "清空配置失败: \(error.localizedDescription)" 136 | showAlert = true 137 | } 138 | } 139 | } 140 | 141 | struct CleanupLog: Identifiable { 142 | let id = UUID() 143 | let timestamp: Date 144 | let command: String 145 | let status: LogStatus 146 | let message: String 147 | 148 | enum LogStatus { 149 | case running 150 | case success 151 | case error 152 | case cancelled 153 | } 154 | 155 | static func getCleanupDescription(for command: String) -> String { 156 | if command.contains("Library/Logs") || command.contains("DiagnosticReports") { 157 | if command.contains("Adobe Creative Cloud") { 158 | return String(localized: "正在清理 Creative Cloud 日志文件...") 159 | } else if command.contains("CrashReporter") { 160 | return String(localized: "正在清理崩溃报告日志...") 161 | } else { 162 | return String(localized: "正在清理应用程序日志文件...") 163 | } 164 | } else if command.contains("Library/Caches") { 165 | return String(localized: "正在清理缓存文件...") 166 | } else if command.contains("Library/Preferences") { 167 | return String(localized: "正在清理偏好设置文件...") 168 | } else if command.contains("Applications") { 169 | if command.contains("Creative Cloud") { 170 | return String(localized: "正在清理 Creative Cloud 应用...") 171 | } else { 172 | return String(localized: "正在清理 Adobe 应用程序...") 173 | } 174 | } else if command.contains("LaunchAgents") || command.contains("LaunchDaemons") { 175 | return String(localized: "正在清理启动项服务...") 176 | } else if command.contains("security") { 177 | return String(localized: "正在清理钥匙串数据...") 178 | } else if command.contains("AdobeGenuineClient") || command.contains("AdobeGCClient") { 179 | return String(localized: "正在清理正版验证服务...") 180 | } else if command.contains("hosts") { 181 | return String(localized: "正在清理 hosts 文件...") 182 | } else if command.contains("kill") { 183 | return String(localized: "正在停止 Adobe 相关进程...") 184 | } else if command.contains("receipts") { 185 | return String(localized: "正在清理安装记录...") 186 | } else { 187 | return String(localized: "正在清理其他文件...") 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/CustomSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanupView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 4/6/25. 6 | // 7 | import SwiftUI 8 | import Sparkle 9 | 10 | struct CustomSettingsView: View { 11 | @State private var selectedTab = "general_settings" 12 | @Environment(\.presentationMode) var presentationMode 13 | @Environment(\.colorScheme) var colorScheme 14 | 15 | private let updater: SPUUpdater 16 | 17 | init(updater: SPUUpdater) { 18 | self.updater = updater 19 | } 20 | 21 | var body: some View { 22 | ZStack { 23 | BlurView() 24 | .ignoresSafeArea() 25 | 26 | ZStack(alignment: .topTrailing) { 27 | VStack(spacing: 0) { 28 | HStack { 29 | HStack(spacing: 0) { 30 | SquareTabButton( 31 | imageName: "gear", 32 | title: String(localized: "通用"), 33 | isSelected: selectedTab == "general_settings" 34 | ) { 35 | withAnimation(.easeInOut(duration: 0.15)) { 36 | selectedTab = "general_settings" 37 | } 38 | } 39 | 40 | SquareTabButton( 41 | imageName: "trash", 42 | title: String(localized: "清理工具"), 43 | isSelected: selectedTab == "cleanup_view" 44 | ) { 45 | withAnimation(.easeInOut(duration: 0.15)) { 46 | selectedTab = "cleanup_view" 47 | } 48 | } 49 | .accessibilityLabel("清理工具") 50 | 51 | SquareTabButton( 52 | imageName: "questionmark.circle", 53 | title: String(localized: "常见问题"), 54 | isSelected: selectedTab == "qa_view" 55 | ) { 56 | withAnimation(.easeInOut(duration: 0.15)) { 57 | selectedTab = "qa_view" 58 | } 59 | } 60 | .accessibilityLabel("常见问题") 61 | 62 | SquareTabButton( 63 | imageName: "info.circle", 64 | title: String(localized: "关于"), 65 | isSelected: selectedTab == "about_app" 66 | ) { 67 | withAnimation(.easeInOut(duration: 0.15)) { 68 | selectedTab = "about_app" 69 | } 70 | } 71 | .accessibilityLabel("关于") 72 | } 73 | .padding(.leading, 8) 74 | 75 | Spacer() 76 | } 77 | .padding(.top, 10) 78 | .padding(.bottom, 6) 79 | 80 | Divider() 81 | .opacity(0.6) 82 | 83 | ScrollView { 84 | ZStack { 85 | if selectedTab == "general_settings" { 86 | GeneralSettingsView(updater: updater) 87 | .transition(contentTransition) 88 | .id("general_settings") 89 | } else if selectedTab == "cleanup_view" { 90 | CleanupView() 91 | .transition(contentTransition) 92 | .id("cleanup_view") 93 | } else if selectedTab == "qa_view" { 94 | QAView() 95 | .transition(contentTransition) 96 | .id("qa_view") 97 | } else if selectedTab == "about_app" { 98 | AboutAppView() 99 | .transition(contentTransition) 100 | .id("about_app") 101 | } 102 | } 103 | .frame(maxWidth: .infinity) 104 | } 105 | .background(Color.clear) 106 | } 107 | 108 | Button(action: { 109 | withAnimation { 110 | presentationMode.wrappedValue.dismiss() 111 | } 112 | }) { 113 | Image(systemName: "xmark") 114 | .font(.system(size: 11, weight: .bold)) 115 | .foregroundColor(.gray) 116 | .frame(width: 20, height: 20) 117 | .background( 118 | Circle() 119 | .fill(colorScheme == .dark ? 120 | Color.gray.opacity(0.3) : 121 | Color.gray.opacity(0.15)) 122 | ) 123 | .help("关闭") 124 | } 125 | .buttonStyle(PlainButtonStyle()) 126 | .keyboardShortcut(.escape, modifiers: []) 127 | .padding(.top, 10) 128 | .padding(.trailing, 10) 129 | } 130 | } 131 | .frame(width: 700, height: 650) 132 | .onAppear { 133 | selectedTab = "general_settings" 134 | } 135 | } 136 | 137 | private var contentTransition: AnyTransition { 138 | .asymmetric( 139 | insertion: .opacity.combined(with: .scale(scale: 0.98)).animation(.easeInOut(duration: 0.2)), 140 | removal: .opacity.animation(.easeInOut(duration: 0.1)) 141 | ) 142 | } 143 | } 144 | 145 | struct SquareTabButton: View { 146 | let imageName: String 147 | let title: String 148 | let isSelected: Bool 149 | let action: () -> Void 150 | @Environment(\.colorScheme) var colorScheme 151 | 152 | var body: some View { 153 | Button(action: action) { 154 | VStack(spacing: 4) { 155 | ZStack { 156 | if isSelected { 157 | RoundedRectangle(cornerRadius: 6) 158 | .fill(Color.blue.opacity(0.2)) 159 | .frame(width: 40, height: 40) 160 | } 161 | 162 | Image(systemName: imageName) 163 | .font(.system(size: isSelected ? 18 : 17)) 164 | .foregroundColor(isSelected ? .blue : colorScheme == .dark ? .white : .black) 165 | } 166 | 167 | Text(title) 168 | .font(.system(size: 12, weight: isSelected ? .medium : .regular)) 169 | .foregroundColor(isSelected ? .blue : colorScheme == .dark ? .white : .primary) 170 | } 171 | .frame(width: 70) 172 | .contentShape(Rectangle()) 173 | } 174 | .buttonStyle(PlainButtonStyle()) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/DownloadManagerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2024/10/30. 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct DownloadManagerView: View { 10 | @Environment(\.dismiss) private var dismiss 11 | 12 | @ObservedObject private var networkManager = globalNetworkManager 13 | @State private var sortOrder: SortOrder = .addTime 14 | 15 | enum SortOrder { 16 | case addTime 17 | case name 18 | case status 19 | 20 | var description: String { 21 | switch self { 22 | case .addTime: return String(localized: "按添加时间") 23 | case .name: return String(localized: "按名称") 24 | case .status: return String(localized: "按状态") 25 | } 26 | } 27 | } 28 | 29 | private func removeTask(_ task: NewDownloadTask) { 30 | Task { @MainActor in 31 | globalNetworkManager.removeTask(taskId: task.id) 32 | } 33 | } 34 | 35 | private func sortTasks(_ tasks: [NewDownloadTask]) -> [NewDownloadTask] { 36 | switch sortOrder { 37 | case .addTime: 38 | return tasks 39 | case .name: 40 | return tasks.sorted { task1, task2 in 41 | task1.displayName < task2.displayName 42 | } 43 | case .status: 44 | return tasks.sorted { task1, task2 in 45 | task1.status.sortOrder < task2.status.sortOrder 46 | } 47 | } 48 | } 49 | 50 | var body: some View { 51 | VStack(spacing: 0) { 52 | DownloadManagerToolbar( 53 | sortOrder: $sortOrder, 54 | dismiss: dismiss 55 | ) 56 | DownloadTaskList( 57 | tasks: sortTasks(networkManager.downloadTasks), 58 | removeTask: removeTask 59 | ) 60 | } 61 | .background(Color(.clear)) 62 | .frame(width:750, height: 600) 63 | } 64 | } 65 | 66 | private struct DownloadManagerToolbar: View { 67 | @Binding var sortOrder: DownloadManagerView.SortOrder 68 | let dismiss: DismissAction 69 | 70 | var body: some View { 71 | VStack(spacing: 0) { 72 | HStack { 73 | Text("下载管理") 74 | .font(.system(size: 20, weight: .bold)) 75 | .foregroundColor(.primary) 76 | 77 | Spacer() 78 | 79 | SortMenuView(sortOrder: $sortOrder) 80 | .frame(minWidth: 120) 81 | .fixedSize() 82 | 83 | ToolbarButtons(dismiss: dismiss) 84 | } 85 | .padding(.horizontal) 86 | .padding(.vertical, 14) 87 | 88 | Divider() 89 | } 90 | .background(Color(NSColor.clear)) 91 | .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) 92 | } 93 | } 94 | 95 | private struct ToolbarButtons: View { 96 | let dismiss: DismissAction 97 | @State private var showClearCompletedConfirmation = false 98 | 99 | var body: some View { 100 | HStack(spacing: 12) { 101 | Button(action: { 102 | Task { 103 | for task in globalNetworkManager.downloadTasks { 104 | if case .downloading = task.status { 105 | await globalNewDownloadUtils.pauseDownloadTask( 106 | taskId: task.id, 107 | reason: .userRequested 108 | ) 109 | } 110 | } 111 | } 112 | }) { 113 | Image(systemName: "pause.circle.fill") 114 | .font(.system(size: 18)) 115 | .foregroundColor(.white) 116 | } 117 | .buttonStyle(BeautifulButtonStyle(baseColor: .orange)) 118 | 119 | Button(action: { 120 | Task { 121 | for task in globalNetworkManager.downloadTasks { 122 | if case .paused = task.status { 123 | await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id) 124 | } 125 | } 126 | } 127 | }) { 128 | Image(systemName: "play.circle.fill") 129 | .font(.system(size: 18)) 130 | .foregroundColor(.white) 131 | } 132 | .buttonStyle(BeautifulButtonStyle(baseColor: .blue)) 133 | 134 | Button(action: { 135 | showClearCompletedConfirmation = true 136 | }) { 137 | Image(systemName: "trash.circle.fill") 138 | .font(.system(size: 18)) 139 | .foregroundColor(.white) 140 | } 141 | .buttonStyle(BeautifulButtonStyle(baseColor: .red)) 142 | 143 | Button(action: { dismiss() }) { 144 | Image(systemName: "xmark.circle.fill") 145 | .font(.system(size: 18)) 146 | .foregroundColor(.white) 147 | } 148 | .buttonStyle(BeautifulButtonStyle(baseColor: .gray)) 149 | } 150 | .background(Color(NSColor.clear)) 151 | .alert("确认删除", isPresented: $showClearCompletedConfirmation) { 152 | Button("取消", role: .cancel) { } 153 | Button("确认", role: .destructive) { 154 | Task { 155 | let tasksToRemove = globalNetworkManager.downloadTasks.filter { task in 156 | if case .completed = task.status { return true } 157 | if case .failed = task.status { return true } 158 | return false 159 | } 160 | 161 | for task in tasksToRemove { 162 | globalNetworkManager.removeTask(taskId: task.id, removeFiles: true) 163 | } 164 | 165 | globalNetworkManager.updateDockBadge() 166 | } 167 | } 168 | } message: { 169 | Text("确定要删除所有已完成和失败的下载任务吗?此操作将同时删除本地文件。") 170 | } 171 | } 172 | } 173 | 174 | private struct DownloadTaskList: View { 175 | let tasks: [NewDownloadTask] 176 | let removeTask: (NewDownloadTask) -> Void 177 | 178 | var body: some View { 179 | ScrollView(showsIndicators: false) { 180 | LazyVStack(spacing: 8) { 181 | ForEach(tasks) { task in 182 | DownloadProgressView( 183 | task: task, 184 | onCancel: { TaskOperations.cancelTask(task) }, 185 | onPause: { TaskOperations.pauseTask(task) }, 186 | onResume: { TaskOperations.resumeTask(task) }, 187 | onRetry: { TaskOperations.resumeTask(task) }, 188 | onRemove: { removeTask(task) } 189 | ) 190 | } 191 | } 192 | .padding(.horizontal) 193 | .padding(.vertical, 8) 194 | } 195 | .background(Color(NSColor.clear)) 196 | } 197 | } 198 | 199 | private enum TaskOperations { 200 | static func cancelTask(_ task: NewDownloadTask) { 201 | Task { 202 | await globalNewDownloadUtils.cancelDownloadTask(taskId: task.id) 203 | } 204 | } 205 | 206 | static func pauseTask(_ task: NewDownloadTask) { 207 | Task { 208 | await globalNewDownloadUtils.pauseDownloadTask( 209 | taskId: task.id, 210 | reason: .userRequested 211 | ) 212 | } 213 | } 214 | 215 | static func resumeTask(_ task: NewDownloadTask) { 216 | Task { 217 | await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id) 218 | } 219 | } 220 | } 221 | 222 | extension DownloadManagerView.SortOrder: Hashable {} 223 | 224 | struct SortMenuView: View { 225 | @Binding var sortOrder: DownloadManagerView.SortOrder 226 | 227 | var body: some View { 228 | Menu { 229 | ForEach([DownloadManagerView.SortOrder.addTime, .name, .status], id: \.self) { order in 230 | Button(action: { 231 | sortOrder = order 232 | }) { 233 | HStack { 234 | Text(order.description) 235 | .font(.system(size: 14, weight: .medium)) 236 | Spacer() 237 | if sortOrder == order { 238 | Image(systemName: "checkmark") 239 | .foregroundColor(.blue) 240 | .font(.system(size: 12, weight: .bold)) 241 | } 242 | } 243 | .frame(minWidth: 120) 244 | .padding(.vertical, 8) 245 | .padding(.horizontal, 12) 246 | .background( 247 | LinearGradient( 248 | gradient: Gradient(colors: [ 249 | sortOrder == order ? Color.blue.opacity(0.05) : Color.clear, 250 | sortOrder == order ? Color.blue.opacity(0.1) : Color.clear 251 | ]), 252 | startPoint: .top, 253 | endPoint: .bottom 254 | ) 255 | ) 256 | .contentShape(Rectangle()) 257 | } 258 | .buttonStyle(PlainButtonStyle()) 259 | } 260 | } label: { 261 | HStack(spacing: 6) { 262 | Image(systemName: "arrow.up.arrow.down") 263 | .font(.system(size: 12)) 264 | Text(sortOrder.description) 265 | .font(.system(size: 13, weight: .medium)) 266 | } 267 | .foregroundColor(.white) 268 | .padding(.vertical, 6) 269 | .padding(.horizontal, 10) 270 | .background( 271 | LinearGradient( 272 | gradient: Gradient(colors: [ 273 | Color.blue.opacity(0.7), 274 | Color.blue.opacity(0.8) 275 | ]), 276 | startPoint: .top, 277 | endPoint: .bottom 278 | ) 279 | ) 280 | .clipShape(RoundedRectangle(cornerRadius: 8)) 281 | .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) 282 | } 283 | .menuStyle(BorderlessButtonMenuStyle()) 284 | .menuIndicator(.hidden) 285 | .fixedSize() 286 | } 287 | } 288 | 289 | #Preview { 290 | DownloadManagerView() 291 | } 292 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/ExistingFileAlertView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He. 5 | // 6 | 7 | import SwiftUI 8 | 9 | private enum AlertConstants { 10 | static let iconSize: CGFloat = 64 11 | static let warningIconSize: CGFloat = 24 12 | static let warningIconOffset: CGFloat = 10 13 | static let verticalSpacing: CGFloat = 20 14 | static let buttonHeight: CGFloat = 24 15 | static let buttonWidth: CGFloat = 260 16 | static let buttonFontSize: CGFloat = 14 17 | static let cornerRadius: CGFloat = 12 18 | static let shadowRadius: CGFloat = 10 19 | } 20 | 21 | struct ExistingFileAlertView: View { 22 | let path: URL 23 | let onUseExisting: () -> Void 24 | let onRedownload: () -> Void 25 | let onCancel: () -> Void 26 | let iconImage: NSImage? 27 | 28 | var body: some View { 29 | VStack(spacing: AlertConstants.verticalSpacing) { 30 | IconSection(iconImage: iconImage) 31 | 32 | Text("安装程序已存在") 33 | .font(.headline) 34 | 35 | PathSection(path: path) 36 | ButtonSection( 37 | onUseExisting: onUseExisting, 38 | onRedownload: onRedownload, 39 | onCancel: onCancel 40 | ) 41 | } 42 | .padding() 43 | .background(BackgroundView()) 44 | } 45 | } 46 | 47 | private struct IconSection: View { 48 | let iconImage: NSImage? 49 | 50 | var body: some View { 51 | ZStack(alignment: .bottomTrailing) { 52 | AppIcon(iconImage: iconImage) 53 | WarningIcon() 54 | } 55 | .padding(.bottom, 5) 56 | } 57 | } 58 | 59 | private struct AppIcon: View { 60 | let iconImage: NSImage? 61 | 62 | var body: some View { 63 | Group { 64 | if let iconImage = iconImage { 65 | Image(nsImage: iconImage) 66 | .resizable() 67 | .interpolation(.high) 68 | .scaledToFit() 69 | } else { 70 | Image(systemName: "app.fill") 71 | .resizable() 72 | .scaledToFit() 73 | .foregroundColor(.secondary) 74 | } 75 | } 76 | .frame(width: AlertConstants.iconSize, height: AlertConstants.iconSize) 77 | .background( 78 | RoundedRectangle(cornerRadius: 8) 79 | .fill(Color.secondary.opacity(0.05)) 80 | .padding(-4) 81 | ) 82 | .overlay( 83 | RoundedRectangle(cornerRadius: 8) 84 | .stroke(Color.secondary.opacity(0.1), lineWidth: 1) 85 | .padding(-4) 86 | ) 87 | } 88 | } 89 | 90 | private struct WarningIcon: View { 91 | var body: some View { 92 | Image(systemName: "exclamationmark.triangle.fill") 93 | .font(.system(size: AlertConstants.warningIconSize)) 94 | .foregroundColor(.orange) 95 | .offset(x: AlertConstants.warningIconOffset, y: 4) 96 | } 97 | } 98 | 99 | private struct PathSection: View { 100 | let path: URL 101 | 102 | var body: some View { 103 | VStack(alignment: .leading, spacing: 8) { 104 | HStack { 105 | Text(path.path) 106 | .font(.system(size: 13)) 107 | .foregroundColor(.blue) 108 | .padding(8) 109 | .background( 110 | RoundedRectangle(cornerRadius: 6) 111 | .fill(Color.blue.opacity(0.05)) 112 | ) 113 | .overlay( 114 | RoundedRectangle(cornerRadius: 6) 115 | .stroke(Color.blue.opacity(0.1), lineWidth: 0.5) 116 | ) 117 | .onTapGesture { 118 | openInFinder(path) 119 | } 120 | } 121 | } 122 | } 123 | 124 | private func openInFinder(_ path: URL) { 125 | NSWorkspace.shared.activateFileViewerSelecting([path]) 126 | } 127 | } 128 | 129 | private struct ButtonSection: View { 130 | let onUseExisting: () -> Void 131 | let onRedownload: () -> Void 132 | let onCancel: () -> Void 133 | 134 | var body: some View { 135 | VStack(spacing: 12) { 136 | ActionButton( 137 | title: String(localized: "使用现有程序"), 138 | icon: "checkmark.circle", 139 | color: .blue, 140 | action: onUseExisting 141 | ) 142 | 143 | ActionButton( 144 | title: String(localized: "重新下载"), 145 | icon: "arrow.down.circle", 146 | color: .green, 147 | action: onRedownload 148 | ) 149 | 150 | ActionButton( 151 | title: String(localized: "取消"), 152 | icon: "xmark.circle", 153 | color: .red, 154 | action: onCancel, 155 | isCancel: true 156 | ) 157 | } 158 | } 159 | } 160 | 161 | private struct ActionButton: View { 162 | let title: String 163 | let icon: String 164 | let color: Color 165 | let action: () -> Void 166 | var isCancel: Bool = false 167 | 168 | var body: some View { 169 | Button(action: action) { 170 | Label(title, systemImage: icon) 171 | .frame(minWidth: 0, maxWidth: AlertConstants.buttonWidth) 172 | .frame(height: AlertConstants.buttonHeight) 173 | .font(.system(size: AlertConstants.buttonFontSize)) 174 | .foregroundColor(.white) 175 | } 176 | .buttonStyle(BeautifulButtonStyle(baseColor: color)) 177 | .if(isCancel) { view in 178 | view.keyboardShortcut(.cancelAction) 179 | } 180 | } 181 | } 182 | 183 | private struct BackgroundView: View { 184 | var body: some View { 185 | Color(NSColor.windowBackgroundColor) 186 | .cornerRadius(AlertConstants.cornerRadius) 187 | .shadow(radius: AlertConstants.shadowRadius) 188 | } 189 | } 190 | 191 | extension View { 192 | @ViewBuilder 193 | fileprivate func `if`( 194 | _ condition: Bool, 195 | transform: (Self) -> Transform 196 | ) -> some View { 197 | if condition { 198 | transform(self) 199 | } else { 200 | self 201 | } 202 | } 203 | } 204 | 205 | struct ExistingFileAlertView_Previews: PreviewProvider { 206 | static var previews: some View { 207 | Group { 208 | ExistingFileAlertView( 209 | path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), 210 | onUseExisting: {}, 211 | onRedownload: {}, 212 | onCancel: {}, 213 | iconImage: NSImage(named: "PHSP") 214 | ) 215 | .background(Color.black.opacity(0.3)) 216 | .previewDisplayName("Light Mode") 217 | 218 | ExistingFileAlertView( 219 | path: URL(fileURLWithPath: "/Users/username/Downloads/Adobe/Adobe Downloader PHSP_25.0-en_US-macuniversal"), 220 | onUseExisting: {}, 221 | onRedownload: {}, 222 | onCancel: {}, 223 | iconImage: NSImage(named: "PHSP") 224 | ) 225 | .background(Color.black.opacity(0.3)) 226 | .preferredColorScheme(.dark) 227 | .previewDisplayName("Dark Mode") 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/LanguagePickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2024/10/30. 5 | // 6 | import SwiftUI 7 | 8 | struct BeautifulLanguageSearchField: View { 9 | @Binding var text: String 10 | 11 | var body: some View { 12 | HStack(spacing: 8) { 13 | Image(systemName: "magnifyingglass") 14 | .font(.system(size: 13)) 15 | .foregroundColor(.secondary.opacity(0.7)) 16 | 17 | TextField("搜索语言", text: $text) 18 | .textFieldStyle(PlainTextFieldStyle()) 19 | .font(.system(size: 13)) 20 | 21 | if !text.isEmpty { 22 | Button(action: { text = "" }) { 23 | Image(systemName: "xmark.circle.fill") 24 | .font(.system(size: 13)) 25 | .foregroundColor(.secondary.opacity(0.7)) 26 | } 27 | .buttonStyle(.plain) 28 | .transition(.opacity) 29 | } 30 | } 31 | .padding(8) 32 | .background( 33 | RoundedRectangle(cornerRadius: 8) 34 | .fill(Color(NSColor.controlBackgroundColor).opacity(0.5)) 35 | .overlay( 36 | RoundedRectangle(cornerRadius: 8) 37 | .stroke(Color.secondary.opacity(0.1), lineWidth: 1) 38 | ) 39 | ) 40 | } 41 | } 42 | 43 | struct LanguagePickerView: View { 44 | let languages: [(code: String, name: String)] 45 | let onLanguageSelected: (String) -> Void 46 | @Environment(\.dismiss) private var dismiss 47 | @State private var searchText: String = "" 48 | @State private var selectedLanguage: String = "" 49 | 50 | private var filteredLanguages: [(code: String, name: String)] { 51 | guard !searchText.isEmpty else { 52 | return languages 53 | } 54 | 55 | let searchTerms = searchText.lowercased() 56 | return languages.filter { language in 57 | language.name.lowercased().contains(searchTerms) || 58 | language.code.lowercased().contains(searchTerms) 59 | } 60 | } 61 | 62 | var body: some View { 63 | VStack(spacing: 0) { 64 | HStack { 65 | Text("选择安装语言") 66 | .font(.system(size: 16, weight: .medium)) 67 | .foregroundColor(.primary.opacity(0.9)) 68 | Spacer() 69 | Button("取消") { 70 | dismiss() 71 | } 72 | .font(.system(size: 14)) 73 | .foregroundColor(.blue) 74 | .buttonStyle(.plain) 75 | } 76 | .padding(.horizontal, 16) 77 | .padding(.vertical, 12) 78 | .background( 79 | Color(NSColor.windowBackgroundColor) 80 | .overlay( 81 | Rectangle() 82 | .frame(height: 0.5) 83 | .foregroundColor(Color.secondary.opacity(0.2)), 84 | alignment: .bottom 85 | ) 86 | ) 87 | 88 | BeautifulLanguageSearchField(text: $searchText) 89 | .padding(.horizontal, 16) 90 | .padding(.vertical, 12) 91 | 92 | Rectangle() 93 | .fill(Color.secondary.opacity(0.1)) 94 | .frame(height: 1) 95 | 96 | ScrollView(showsIndicators: false) { 97 | LazyVStack(spacing: 0) { 98 | ForEach(Array(filteredLanguages.enumerated()), id: \.element.code) { index, language in 99 | LanguageRow( 100 | language: language, 101 | isSelected: language.code == selectedLanguage, 102 | onSelect: { 103 | withAnimation(.easeInOut(duration: 0.2)) { 104 | selectedLanguage = language.code 105 | } 106 | onLanguageSelected(language.code) 107 | dismiss() 108 | } 109 | ) 110 | 111 | if index < filteredLanguages.count - 1 { 112 | Rectangle() 113 | .fill(Color.secondary.opacity(0.06)) 114 | .frame(height: 0.5) 115 | .padding(.leading, 46) 116 | } 117 | } 118 | } 119 | .padding(.vertical, 4) 120 | } 121 | 122 | if filteredLanguages.isEmpty { 123 | VStack(spacing: 16) { 124 | ZStack { 125 | Circle() 126 | .fill(Color.secondary.opacity(0.1)) 127 | .frame(width: 80, height: 80) 128 | 129 | Image(systemName: "magnifyingglass") 130 | .font(.system(size: 36)) 131 | .foregroundColor(.secondary) 132 | } 133 | 134 | VStack(spacing: 8) { 135 | Text("未找到语言") 136 | .font(.system(size: 16, weight: .medium)) 137 | 138 | Text("尝试其他搜索关键词") 139 | .font(.system(size: 14)) 140 | .foregroundColor(.secondary) 141 | } 142 | } 143 | .frame(maxWidth: .infinity, maxHeight: .infinity) 144 | .offset(y: -20) 145 | } 146 | } 147 | .frame(width: 320, height: 400) 148 | } 149 | } 150 | 151 | struct LanguageRow: View { 152 | let language: (code: String, name: String) 153 | let isSelected: Bool 154 | let onSelect: () -> Void 155 | 156 | var body: some View { 157 | Button(action: onSelect) { 158 | HStack(spacing: 14) { 159 | ZStack { 160 | Circle() 161 | .fill(Color.blue.opacity(0.15)) 162 | .frame(width: 32, height: 32) 163 | 164 | Image(systemName: getLanguageIcon(language.code)) 165 | .font(.system(size: 14, weight: .medium)) 166 | .foregroundColor(.blue) 167 | } 168 | 169 | Text(language.name) 170 | .font(.system(size: 14, weight: isSelected ? .medium : .regular)) 171 | .foregroundColor(isSelected ? .primary : .primary.opacity(0.8)) 172 | .frame(maxWidth: .infinity, alignment: .leading) 173 | 174 | Text(language.code) 175 | .font(.system(size: 12)) 176 | .foregroundColor(.secondary.opacity(0.8)) 177 | 178 | if isSelected { 179 | Image(systemName: "checkmark") 180 | .font(.system(size: 12, weight: .medium)) 181 | .foregroundColor(.blue) 182 | .frame(width: 20) 183 | } 184 | } 185 | .padding(.horizontal, 14) 186 | .padding(.vertical, 10) 187 | .contentShape(Rectangle()) 188 | } 189 | .buttonStyle(.plain) 190 | .background(isSelected ? Color.blue.opacity(0.08) : Color.clear) 191 | .animation(.easeInOut(duration: 0.2), value: isSelected) 192 | } 193 | 194 | private func getLanguageIcon(_ code: String) -> String { 195 | switch code { 196 | case "zh_CN", "zh_TW": 197 | return "character.textbox" 198 | case "en_US", "en_GB": 199 | return "a.square" 200 | case "ja_JP": 201 | return "j.square" 202 | case "ko_KR": 203 | return "k.square" 204 | case "fr_FR": 205 | return "f.square" 206 | case "de_DE": 207 | return "d.square" 208 | case "es_ES": 209 | return "e.square" 210 | case "it_IT": 211 | return "i.square" 212 | case "ru_RU": 213 | return "r.square" 214 | case "ALL": 215 | return "globe" 216 | default: 217 | return "character.square" 218 | } 219 | } 220 | } 221 | 222 | #Preview { 223 | LanguagePickerView( 224 | languages: AppStatics.supportedLanguages, 225 | onLanguageSelected: { _ in } 226 | ) 227 | } 228 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/LogEntryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogEntryView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 3/28/25. 6 | // 7 | import SwiftUI 8 | 9 | struct LogEntryView: View { 10 | let log: CleanupLog 11 | @State private var showCopyButton = false 12 | 13 | private var statusIconName: String { 14 | statusIcon(for: log.status) 15 | } 16 | 17 | private var statusColorValue: Color { 18 | statusColor(for: log.status) 19 | } 20 | 21 | private var timeFormatted: String { 22 | timeString(from: log.timestamp) 23 | } 24 | 25 | private var displayText: String { 26 | #if DEBUG 27 | return log.command 28 | #else 29 | return CleanupLog.getCleanupDescription(for: log.command) 30 | #endif 31 | } 32 | 33 | private var errorDisplayText: String? { 34 | if log.status == .error && !log.message.isEmpty { 35 | return truncatedErrorMessage(log.message) 36 | } 37 | return nil 38 | } 39 | 40 | var body: some View { 41 | HStack { 42 | Image(systemName: statusIconName) 43 | .foregroundColor(statusColorValue) 44 | 45 | Text(timeFormatted) 46 | .font(.system(size: 11)) 47 | .foregroundColor(.secondary) 48 | 49 | Text(displayText) 50 | .font(.system(size: 11, design: .monospaced)) 51 | .lineLimit(1) 52 | .truncationMode(.middle) 53 | 54 | Spacer() 55 | 56 | if let errorText = errorDisplayText { 57 | HStack(spacing: 4) { 58 | Text(errorText) 59 | .font(.system(size: 11)) 60 | .foregroundColor(.secondary) 61 | 62 | Button(action: { 63 | copyToClipboard(log.message) 64 | }) { 65 | Image(systemName: "doc.on.doc") 66 | .font(.system(size: 11)) 67 | } 68 | .buttonStyle(.plain) 69 | .help("复制完整错误信息") 70 | } 71 | } else { 72 | Text(log.message) 73 | .font(.system(size: 11)) 74 | .foregroundColor(.secondary) 75 | } 76 | } 77 | .padding(.vertical, 4) 78 | .padding(.horizontal, 8) 79 | } 80 | 81 | private func truncatedErrorMessage(_ message: String) -> String { 82 | if message.hasPrefix("执行失败:") { 83 | let errorMessage = String(message.dropFirst(5)) 84 | if errorMessage.count > 30 { 85 | return "执行失败:" + errorMessage.prefix(30) + "..." 86 | } 87 | } 88 | return message 89 | } 90 | 91 | private func copyToClipboard(_ message: String) { 92 | NSPasteboard.general.clearContents() 93 | NSPasteboard.general.setString(message, forType: .string) 94 | } 95 | 96 | private func statusIcon(for status: CleanupLog.LogStatus) -> String { 97 | switch status { 98 | case .running: 99 | return "arrow.triangle.2.circlepath" 100 | case .success: 101 | return "checkmark.circle.fill" 102 | case .error: 103 | return "exclamationmark.circle.fill" 104 | case .cancelled: 105 | return "xmark.circle.fill" 106 | } 107 | } 108 | 109 | private func statusColor(for status: CleanupLog.LogStatus) -> Color { 110 | switch status { 111 | case .running: 112 | return .blue 113 | case .success: 114 | return .green 115 | case .error: 116 | return .red 117 | case .cancelled: 118 | return .orange 119 | } 120 | } 121 | 122 | private func timeString(from date: Date) -> String { 123 | let formatter = DateFormatter() 124 | formatter.dateFormat = "HH:mm:ss" 125 | return formatter.string(from: date) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/MainContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MainContentView: View { 4 | let loadingState: LoadingState 5 | let filteredProducts: [UniqueProduct] 6 | let onRetry: () -> Void 7 | 8 | var body: some View { 9 | ZStack { 10 | switch loadingState { 11 | case .idle, .loading: 12 | ProgressView("正在加载...") 13 | .frame(maxWidth: .infinity, maxHeight: .infinity) 14 | 15 | case .failed(let error): 16 | VStack(spacing: 20) { 17 | Image(systemName: "exclamationmark.triangle") 18 | .font(.system(size: 48)) 19 | .foregroundColor(.red) 20 | 21 | Text("加载失败") 22 | .font(.title2) 23 | .fontWeight(.medium) 24 | 25 | Text(error.localizedDescription) 26 | .foregroundColor(.secondary) 27 | .multilineTextAlignment(.center) 28 | .frame(maxWidth: 300) 29 | .padding(.bottom, 10) 30 | 31 | Button(action: onRetry) { 32 | HStack { 33 | Image(systemName: "arrow.clockwise") 34 | Text("重试") 35 | } 36 | } 37 | .buttonStyle(.borderedProminent) 38 | .controlSize(.large) 39 | } 40 | .frame(maxWidth: .infinity, maxHeight: .infinity) 41 | 42 | case .success: 43 | if filteredProducts.isEmpty { 44 | EmptyStateView() 45 | } else { 46 | ProductGridView(products: filteredProducts) 47 | } 48 | } 49 | } 50 | .background(Color(.clear)) 51 | } 52 | } 53 | 54 | struct EmptyStateView: View { 55 | var body: some View { 56 | VStack { 57 | Image(systemName: "magnifyingglass") 58 | .font(.system(size: 36)) 59 | .foregroundColor(.secondary) 60 | Text("没有找到产品") 61 | .font(.headline) 62 | .padding(.top) 63 | Text("尝试使用不同的搜索关键词") 64 | .font(.subheadline) 65 | .foregroundColor(.secondary) 66 | } 67 | .frame(maxWidth: .infinity, maxHeight: .infinity) 68 | } 69 | } 70 | 71 | struct ProductGridView: View { 72 | let products: [UniqueProduct] 73 | 74 | var body: some View { 75 | ScrollViewReader { proxy in 76 | ScrollView(showsIndicators: false) { 77 | LazyVGrid( 78 | columns: [ 79 | GridItem(.adaptive(minimum: 240, maximum: 300), spacing: 20) 80 | ], 81 | spacing: 20 82 | ) { 83 | ForEach(products, id: \.id) { uniqueProduct in 84 | AppCardView(uniqueProduct: uniqueProduct) 85 | .id(uniqueProduct.id) 86 | .modifier(AppearAnimationModifier()) 87 | } 88 | } 89 | .padding() 90 | 91 | 92 | HStack(spacing: 8) { 93 | Capsule() 94 | .fill(Color.green) 95 | .frame(width: 6, height: 6) 96 | Text("获取到 \(products.count) 款产品") 97 | .font(.system(size: 12)) 98 | .foregroundColor(.secondary) 99 | } 100 | .padding(.bottom, 16) 101 | } 102 | } 103 | } 104 | } 105 | 106 | struct AppearAnimationModifier: ViewModifier { 107 | @State private var opacity: Double = 0 108 | @State private var offset: CGFloat = 20 109 | @State private var scale: CGFloat = 0.95 110 | 111 | func body(content: Content) -> some View { 112 | content 113 | .opacity(opacity) 114 | .offset(y: offset) 115 | .scaleEffect(scale) 116 | .onAppear { 117 | withAnimation(.easeInOut(duration: 0.5)) { 118 | opacity = 1 119 | offset = 0 120 | scale = 1 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/QAView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QAView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 3/28/25. 6 | // 7 | import SwiftUI 8 | 9 | struct QAView: View { 10 | var body: some View { 11 | ScrollView { 12 | VStack(alignment: .leading, spacing: 16) { 13 | Group { 14 | QAItem( 15 | question: String(localized: "为什么需要安装 Helper?"), 16 | answer: String(localized: "Helper 是一个具有管理员权限的辅助工具,用于执行需要管理员权限的操作,如修改系统文件等。没有 Helper 将无法正常使用软件的某些功能。") 17 | ) 18 | 19 | QAItem( 20 | question: String(localized: "为什么需要下载 Setup 组件?"), 21 | answer: String(localized: "Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。") 22 | ) 23 | 24 | QAItem( 25 | question: String(localized: "为什么有时候下载会失败?"), 26 | answer: String(localized: "下载失败可能有多种原因:\n1. 网络连接不稳定\n2. Adobe 服务器响应超时\n3. 本地磁盘空间不足\n建议您检查网络连接并重试,如果问题持续存在,可以尝试使用代理或 VPN。") 27 | ) 28 | 29 | QAItem( 30 | question: String(localized: "如何修复安装失败的问题?"), 31 | answer: String(localized: "如果安装失败,您可以尝试以下步骤:\n1. 确保已正确安装并连接 Helper\n2. 确保已下载并处理 Setup 组件\n3. 检查磁盘剩余空间是否充足\n4. 尝试重新下载并安装\n如果问题仍然存在,可以尝试重新安装 Helper 和重新处理 Setup 组件。") 32 | ) 33 | } 34 | } 35 | .padding() 36 | } 37 | .frame(maxWidth: .infinity, maxHeight: .infinity) 38 | } 39 | } 40 | 41 | struct QAItem: View { 42 | let question: String 43 | let answer: String 44 | 45 | var body: some View { 46 | VStack(alignment: .leading, spacing: 8) { 47 | Text(question) 48 | .font(.headline) 49 | .foregroundColor(.primary) 50 | 51 | Text(answer) 52 | .font(.body) 53 | .foregroundColor(.secondary) 54 | .fixedSize(horizontal: false, vertical: true) 55 | 56 | Divider() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/SetupBackupAlertView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupBackupAlertView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 3/27/25. 6 | // 7 | import SwiftUI 8 | 9 | struct SetupBackupAlertView: View { 10 | let onConfirm: () -> Void 11 | let onCancel: () -> Void 12 | @State private var isHovering = false 13 | @State private var countdown = 10 14 | @State private var isCountdownActive = false 15 | private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 16 | 17 | var body: some View { 18 | VStack(spacing: 20) { 19 | Image(nsImage: NSImage(named: NSImage.applicationIconName)!) 20 | .resizable() 21 | .aspectRatio(contentMode: .fit) 22 | .frame(width: 80, height: 80) 23 | .padding(.top, 10) 24 | .overlay( 25 | Image(systemName: "exclamationmark.triangle.fill") 26 | .font(.system(size: 24)) 27 | .foregroundColor(.orange) 28 | .offset(x: 30, y: 30) 29 | ) 30 | 31 | Text("Setup未备份提示") 32 | .font(.headline) 33 | .padding(.top, 5) 34 | 35 | Text("检测到Setup文件尚未备份,如果你需要安装程序,则Setup必须被处理,点击确定后你需要输入密码,Adobe Downloader将自动处理并备份为Setup.original") 36 | .font(.system(size: 14)) 37 | .multilineTextAlignment(.center) 38 | .padding(.horizontal, 20) 39 | .foregroundColor(.secondary) 40 | 41 | HStack(spacing: 20) { 42 | Button(action: onCancel) { 43 | Text("取消") 44 | .frame(width: 120, height: 24) 45 | .foregroundColor(.white) 46 | } 47 | .buttonStyle(BeautifulButtonStyle(baseColor: Color.gray.opacity(0.8))) 48 | 49 | #if DEBUG 50 | Button(action: onConfirm) { 51 | Text("确定") 52 | .frame(width: 120, height: 24) 53 | .foregroundColor(.white) 54 | } 55 | .buttonStyle(BeautifulButtonStyle(baseColor: .blue)) 56 | .keyboardShortcut(.defaultAction) 57 | #else 58 | Button(action: isCountdownActive && countdown == 0 ? onConfirm : { 59 | isCountdownActive = true 60 | }) { 61 | Text(isCountdownActive && countdown > 0 ? "\(countdown)" : "确定") 62 | .frame(width: 120, height: 24) 63 | .foregroundColor(.white) 64 | } 65 | .buttonStyle(BeautifulButtonStyle(baseColor: isCountdownActive && countdown > 0 ? Color.blue.opacity(0.6) : .blue)) 66 | .disabled(isCountdownActive && countdown > 0) 67 | .keyboardShortcut(.defaultAction) 68 | .onReceive(timer) { _ in 69 | if isCountdownActive && countdown > 0 { 70 | countdown -= 1 71 | } 72 | } 73 | #endif 74 | } 75 | .padding(.bottom, 20) 76 | } 77 | .frame(width: 400) 78 | .cornerRadius(12) 79 | .shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 2) 80 | .onAppear { 81 | #if !DEBUG 82 | isCountdownActive = true 83 | #endif 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/SetupBackupResultView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupBackupResultView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 3/27/25. 6 | // 7 | import SwiftUI 8 | 9 | struct SetupBackupResultView: View { 10 | let isSuccess: Bool 11 | let message: String 12 | let onDismiss: () -> Void 13 | 14 | init(isSuccess: Bool, message: String, onDismiss: @escaping () -> Void) { 15 | self.isSuccess = isSuccess 16 | self.message = message 17 | self.onDismiss = onDismiss 18 | } 19 | 20 | var body: some View { 21 | VStack(spacing: 20) { 22 | 23 | Image(systemName: isSuccess ? "checkmark.circle.fill" : "xmark.circle.fill") 24 | .font(.system(size: 50)) 25 | .foregroundColor(isSuccess ? .green : .red) 26 | .padding(.top, 20) 27 | 28 | Text(isSuccess ? "备份成功" : "备份失败") 29 | .font(.title3) 30 | .bold() 31 | .padding(.top, 5) 32 | 33 | Text(message) 34 | .font(.system(size: 14)) 35 | .multilineTextAlignment(.center) 36 | .padding(.horizontal, 20) 37 | .foregroundColor(.secondary) 38 | 39 | Button(action: onDismiss) { 40 | Text("确定") 41 | .frame(width: 120, height: 24) 42 | .foregroundColor(.white) 43 | } 44 | .buttonStyle(BeautifulButtonStyle(baseColor: isSuccess ? Color.green : Color.blue)) 45 | .keyboardShortcut(.defaultAction) 46 | .padding(.bottom, 20) 47 | } 48 | .frame(width: 350) 49 | .cornerRadius(12) 50 | .shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 2) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/ShouldExistsSetUpView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShouldExistsSetUpView.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 11/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ShouldExistsSetUpView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @EnvironmentObject private var networkManager: NetworkManager 13 | @State private var showingAlert = false 14 | @State private var isDownloading = false 15 | @State private var downloadProgress: Double = 0 16 | @State private var downloadStatus: String = "" 17 | @State private var isCancelled = false 18 | @State private var showErrorAlert = false 19 | @State private var errorMessage = "" 20 | 21 | var body: some View { 22 | VStack(spacing: 20) { 23 | SetupAlertHeaderView() 24 | MessageView() 25 | ButtonsView( 26 | isDownloading: $isDownloading, 27 | downloadProgress: $downloadProgress, 28 | downloadStatus: $downloadStatus, 29 | isCancelled: $isCancelled, 30 | showingAlert: $showingAlert, 31 | showErrorAlert: $showErrorAlert, 32 | errorMessage: $errorMessage, 33 | dismiss: dismiss, 34 | networkManager: networkManager 35 | ) 36 | } 37 | .frame(width: 500) 38 | .padding() 39 | .background(Color(NSColor.windowBackgroundColor)) 40 | .cornerRadius(12) 41 | .shadow(radius: 10) 42 | .alert("下载失败", isPresented: $showErrorAlert) { 43 | Button("确定") { } 44 | } message: { 45 | Text(errorMessage) 46 | } 47 | } 48 | } 49 | 50 | private struct SetupAlertHeaderView: View { 51 | var body: some View { 52 | VStack { 53 | Image(systemName: "exclamationmark.triangle.fill") 54 | .font(.system(size: 64)) 55 | .foregroundColor(.orange) 56 | .padding(.bottom, 5) 57 | .frame(alignment: .bottomTrailing) 58 | 59 | Text("未检测到 Adobe CC 组件") 60 | .font(.system(size: 24)) 61 | .bold() 62 | } 63 | } 64 | } 65 | 66 | private struct MessageView: View { 67 | var body: some View { 68 | VStack(spacing: 4) { 69 | Text("程序检测到你的系统中不存在 Adobe CC 组件") 70 | .multilineTextAlignment(.center) 71 | 72 | Text("可能导致无法使用安装功能,请确保是否使用安装功能") 73 | .multilineTextAlignment(.center) 74 | } 75 | } 76 | } 77 | 78 | private struct ButtonsView: View { 79 | @Binding var isDownloading: Bool 80 | @Binding var downloadProgress: Double 81 | @Binding var downloadStatus: String 82 | @Binding var isCancelled: Bool 83 | @Binding var showingAlert: Bool 84 | @Binding var showErrorAlert: Bool 85 | @Binding var errorMessage: String 86 | let dismiss: DismissAction 87 | let networkManager: NetworkManager 88 | 89 | var body: some View { 90 | VStack(spacing: 16) { 91 | notUseButton 92 | downloadButton 93 | creativeCloudButton 94 | exitButton 95 | } 96 | } 97 | 98 | private var notUseButton: some View { 99 | Button(action: { showingAlert = true }) { 100 | Label("不使用安装功能", systemImage: "exclamationmark.triangle.fill") 101 | .frame(minWidth: 0, maxWidth: 360) 102 | .frame(height: 32) 103 | .font(.system(size: 14)) 104 | } 105 | .buttonStyle(.borderedProminent) 106 | .tint(.orange) 107 | .alert("确认", isPresented: $showingAlert) { 108 | Button("取消", role: .cancel) { } 109 | Button("确定", role: .destructive) { 110 | dismiss() 111 | } 112 | } message: { 113 | Text("你确定不使用安装功能吗?") 114 | } 115 | .disabled(isDownloading) 116 | } 117 | 118 | private var downloadButton: some View { 119 | Button(action: startDownload) { 120 | if isDownloading { 121 | downloadProgressView 122 | } else { 123 | Label("下载 X1a0He CC", systemImage: "arrow.down") 124 | .frame(minWidth: 0, maxWidth: 360) 125 | .frame(height: 32) 126 | .font(.system(size: 14)) 127 | } 128 | } 129 | .buttonStyle(.borderedProminent) 130 | .tint(.blue) 131 | .disabled(isDownloading) 132 | } 133 | 134 | private var downloadProgressView: some View { 135 | VStack { 136 | ProgressView(value: downloadProgress) { 137 | Text(downloadStatus) 138 | .font(.system(size: 14)) 139 | } 140 | Text("\(Int(downloadProgress * 100))%") 141 | .font(.system(size: 12)) 142 | .foregroundColor(.secondary) 143 | Button("取消") { 144 | isCancelled = true 145 | } 146 | .buttonStyle(.borderless) 147 | } 148 | .frame(maxWidth: 360) 149 | .progressViewStyle(.linear) 150 | .tint(.green) 151 | } 152 | 153 | private var creativeCloudButton: some View { 154 | Button(action: openCreativeCloud) { 155 | Label("前往 Adobe Creative Cloud", systemImage: "cloud.fill") 156 | .frame(minWidth: 0, maxWidth: 360) 157 | .frame(height: 32) 158 | .font(.system(size: 14)) 159 | } 160 | .disabled(isDownloading) 161 | } 162 | 163 | private var exitButton: some View { 164 | Button(action: exitApp) { 165 | Label("退出", systemImage: "xmark") 166 | .frame(minWidth: 0, maxWidth: 360) 167 | .frame(height: 32) 168 | .font(.system(size: 14)) 169 | } 170 | .buttonStyle(.borderedProminent) 171 | .tint(.red) 172 | .keyboardShortcut(.cancelAction) 173 | .disabled(isDownloading) 174 | } 175 | 176 | private func startDownload() { 177 | isDownloading = true 178 | isCancelled = false 179 | Task { 180 | do { 181 | try await globalNewDownloadUtils.downloadX1a0HeCCPackages( 182 | progressHandler: { progress, status in 183 | Task { @MainActor in 184 | downloadProgress = progress 185 | downloadStatus = status 186 | } 187 | }, 188 | cancellationHandler: { isCancelled } 189 | ) 190 | await MainActor.run { 191 | dismiss() 192 | } 193 | } catch NetworkError.cancelled { 194 | await MainActor.run { 195 | isDownloading = false 196 | } 197 | } catch { 198 | await MainActor.run { 199 | isDownloading = false 200 | errorMessage = error.localizedDescription 201 | showErrorAlert = true 202 | } 203 | } 204 | } 205 | } 206 | 207 | private func openCreativeCloud() { 208 | if let url = URL(string: "https://creativecloud.adobe.com/apps/download/creative-cloud") { 209 | NSWorkspace.shared.open(url) 210 | dismiss() 211 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 212 | NSApplication.shared.terminate(nil) 213 | } 214 | } 215 | } 216 | 217 | private func exitApp() { 218 | dismiss() 219 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 220 | NSApplication.shared.terminate(nil) 221 | } 222 | } 223 | } 224 | 225 | #Preview { 226 | ShouldExistsSetUpView() 227 | .environmentObject(NetworkManager()) 228 | } 229 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/Styles/BeautifulButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He. 5 | // 6 | 7 | import SwiftUI 8 | 9 | public struct BeautifulButtonStyle: ButtonStyle { 10 | var baseColor: Color 11 | @State private var isHovering = false 12 | 13 | public func makeBody(configuration: Configuration) -> some View { 14 | configuration.label 15 | .padding(.vertical, 6) 16 | .padding(.horizontal, 12) 17 | .background( 18 | RoundedRectangle(cornerRadius: 6) 19 | .fill(configuration.isPressed ? baseColor.opacity(0.7) : baseColor) 20 | ) 21 | .overlay( 22 | RoundedRectangle(cornerRadius: 6) 23 | .strokeBorder(baseColor.opacity(0.2), lineWidth: 1) 24 | .opacity(configuration.isPressed ? 0 : 1) 25 | ) 26 | .scaleEffect(configuration.isPressed ? 0.97 : 1.0) 27 | .animation(.easeInOut(duration: 0.15), value: configuration.isPressed) 28 | } 29 | } -------------------------------------------------------------------------------- /Adobe Downloader/Views/Styles/BeautifulGroupBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BeautifulGroupBox.swift 3 | // Adobe Downloader 4 | // 5 | // Created by X1a0He on 3/28/25. 6 | // 7 | import SwiftUI 8 | 9 | struct BeautifulGroupBox: View { 10 | let label: Label 11 | let content: Content 12 | 13 | init(label: @escaping () -> Label, @ViewBuilder content: () -> Content) { 14 | self.label = label() 15 | self.content = content() 16 | } 17 | 18 | var body: some View { 19 | VStack(alignment: .leading, spacing: 4) { 20 | label 21 | .font(.system(size: 14, weight: .medium)) 22 | .foregroundColor(.primary.opacity(0.85)) 23 | 24 | VStack(alignment: .leading, spacing: 0) { 25 | content 26 | } 27 | .padding(8) 28 | .background( 29 | RoundedRectangle(cornerRadius: 8) 30 | .fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) 31 | .overlay( 32 | RoundedRectangle(cornerRadius: 8) 33 | .stroke(Color.secondary.opacity(0.15), lineWidth: 1) 34 | ) 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/TipsSheetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adobe Downloader 3 | // 4 | // Created by X1a0He on 2024/11/18. 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct TipsSheetView: View { 10 | @ObservedObject private var storage = StorageData.shared 11 | @EnvironmentObject private var networkManager: NetworkManager 12 | @Binding var showTipsSheet: Bool 13 | @Binding var showLanguagePicker: Bool 14 | 15 | var body: some View { 16 | VStack(spacing: 20) { 17 | Text("Adobe Downloader 已为你默认设定如下值") 18 | .font(.headline) 19 | 20 | VStack(spacing: 12) { 21 | HStack { 22 | Toggle("使用默认语言", isOn: Binding( 23 | get: { storage.useDefaultLanguage }, 24 | set: { storage.useDefaultLanguage = $0 } 25 | )) 26 | .padding(.leading, 5) 27 | Spacer() 28 | Text(getLanguageName(code: storage.defaultLanguage)) 29 | .foregroundColor(.secondary) 30 | Button("选择") { 31 | showLanguagePicker = true 32 | } 33 | .padding(.trailing, 5) 34 | } 35 | 36 | Divider() 37 | 38 | HStack { 39 | Toggle("使用默认目录", isOn: Binding( 40 | get: { storage.useDefaultDirectory }, 41 | set: { storage.useDefaultDirectory = $0 } 42 | )) 43 | .padding(.leading, 5) 44 | Spacer() 45 | Text(formatPath(storage.defaultDirectory)) 46 | .foregroundColor(.secondary) 47 | .lineLimit(1) 48 | .truncationMode(.middle) 49 | Button("选择") { 50 | selectDirectory() 51 | } 52 | .padding(.trailing, 5) 53 | } 54 | 55 | Divider() 56 | 57 | HStack { 58 | Toggle("重新下载时需要确认", isOn: Binding( 59 | get: { storage.confirmRedownload }, 60 | set: { 61 | storage.confirmRedownload = $0 62 | NotificationCenter.default.post(name: .storageDidChange, object: nil) 63 | } 64 | )) 65 | .padding(.leading, 5) 66 | Spacer() 67 | } 68 | 69 | Divider() 70 | 71 | HStack { 72 | Toggle("下载 Apple Silicon 架构", isOn: Binding( 73 | get: { storage.downloadAppleSilicon }, 74 | set: { newValue in 75 | storage.downloadAppleSilicon = newValue 76 | Task { 77 | await networkManager.fetchProducts() 78 | } 79 | } 80 | )) 81 | .padding(.leading, 5) 82 | Spacer() 83 | Text("当前架构: \(AppStatics.cpuArchitecture)") 84 | .foregroundColor(.secondary) 85 | .lineLimit(1) 86 | .truncationMode(.middle) 87 | } 88 | } 89 | .padding() 90 | .background(Color(NSColor.controlBackgroundColor)) 91 | .cornerRadius(8) 92 | 93 | Text("你可以在设置中随时更改以上选项") 94 | .font(.headline) 95 | 96 | Button("确定") { 97 | showTipsSheet = false 98 | } 99 | .buttonStyle(.borderedProminent) 100 | } 101 | .padding() 102 | .frame(width: 500) 103 | } 104 | 105 | private func formatPath(_ path: String) -> String { 106 | if path.isEmpty { return String(localized: "未设置") } 107 | return URL(fileURLWithPath: path).lastPathComponent 108 | } 109 | 110 | private func selectDirectory() { 111 | let panel = NSOpenPanel() 112 | panel.title = "选择默认下载目录" 113 | panel.canCreateDirectories = true 114 | panel.canChooseDirectories = true 115 | panel.canChooseFiles = false 116 | 117 | if panel.runModal() == .OK { 118 | storage.defaultDirectory = panel.url?.path ?? "Downloads" 119 | storage.useDefaultDirectory = true 120 | } 121 | } 122 | 123 | private func getLanguageName(code: String) -> String { 124 | AppStatics.supportedLanguages.first { $0.code == code }?.name ?? code 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Adobe Downloader/Views/ToolbarView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BeautifulSearchField: View { 4 | @Binding var text: String 5 | 6 | var body: some View { 7 | HStack(spacing: 8) { 8 | Image(systemName: "magnifyingglass") 9 | .font(.system(size: 13)) 10 | .foregroundColor(.secondary.opacity(0.7)) 11 | 12 | TextField("搜索应用", text: $text) 13 | .textFieldStyle(PlainTextFieldStyle()) 14 | .font(.system(size: 13)) 15 | 16 | if !text.isEmpty { 17 | Button(action: { text = "" }) { 18 | Image(systemName: "xmark.circle.fill") 19 | .font(.system(size: 13)) 20 | .foregroundColor(.secondary.opacity(0.7)) 21 | } 22 | .buttonStyle(.plain) 23 | } 24 | } 25 | .padding(8) 26 | .background( 27 | RoundedRectangle(cornerRadius: 8) 28 | .fill(Color(NSColor.controlBackgroundColor).opacity(0.3)) 29 | .overlay( 30 | RoundedRectangle(cornerRadius: 8) 31 | .stroke(Color.secondary.opacity(0.1), lineWidth: 1) 32 | ) 33 | ) 34 | } 35 | } 36 | 37 | struct FlatToggleStyle: ToggleStyle { 38 | var onColor: Color = .blue 39 | var offColor: Color = .gray.opacity(0.3) 40 | var thumbColor: Color = .white 41 | 42 | func makeBody(configuration: Configuration) -> some View { 43 | HStack { 44 | configuration.label 45 | 46 | ZStack { 47 | RoundedRectangle(cornerRadius: 16) 48 | .fill(configuration.isOn ? onColor : offColor) 49 | .frame(width: 50, height: 29) 50 | .overlay( 51 | RoundedRectangle(cornerRadius: 16) 52 | .stroke(configuration.isOn ? onColor.opacity(0.2) : offColor.opacity(0.6), lineWidth: 1) 53 | ) 54 | 55 | Circle() 56 | .fill(thumbColor) 57 | .shadow(color: Color.black.opacity(0.08), radius: 1, x: 0, y: 1) 58 | .frame(width: 24, height: 24) 59 | .offset(x: configuration.isOn ? 11 : -11) 60 | .animation(.spring(response: 0.35, dampingFraction: 0.7), value: configuration.isOn) 61 | } 62 | .onTapGesture { 63 | withAnimation { 64 | configuration.isOn.toggle() 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | struct FlatSegmentedPickerStyle: ViewModifier { 72 | func body(content: Content) -> some View { 73 | content 74 | .padding(.vertical, 4) 75 | .padding(.horizontal, 1) 76 | .background( 77 | RoundedRectangle(cornerRadius: 8) 78 | .fill(Color(NSColor.windowBackgroundColor).opacity(0.3)) 79 | .overlay( 80 | RoundedRectangle(cornerRadius: 8) 81 | .stroke(Color.secondary.opacity(0.15), lineWidth: 1) 82 | ) 83 | ) 84 | } 85 | } 86 | 87 | struct ToolbarView: View { 88 | @Binding var downloadAppleSilicon: Bool 89 | @Binding var currentApiVersion: String 90 | @Binding var searchText: String 91 | @Binding var showDownloadManager: Bool 92 | let isRefreshing: Bool 93 | let downloadTasksCount: Int 94 | let onRefresh: () -> Void 95 | let openSettings: () -> Void 96 | 97 | var body: some View { 98 | HStack(spacing: 16) { 99 | Toggle(isOn: $downloadAppleSilicon) { 100 | Text("下载 Apple Silicon") 101 | .font(.system(size: 14, weight: .medium)) 102 | } 103 | .toggleStyle(FlatToggleStyle(onColor: .green, offColor: .gray.opacity(0.25))) 104 | .disabled(isRefreshing) 105 | 106 | HStack(spacing: 10) { 107 | Text("API:") 108 | .font(.system(size: 14, weight: .medium)) 109 | .foregroundColor(.primary.opacity(0.8)) 110 | 111 | HStack(spacing: 1) { 112 | ForEach(["4", "5", "6"], id: \.self) { version in 113 | Button(action: { 114 | withAnimation(.easeInOut(duration: 0.2)) { 115 | currentApiVersion = version 116 | } 117 | }) { 118 | Text("v\(version)") 119 | .font(.system(size: 13, weight: .medium)) 120 | .frame(width: 40, height: 28) 121 | .background( 122 | RoundedRectangle(cornerRadius: 5) 123 | .fill(currentApiVersion == version ? 124 | Color.blue.opacity(0.15) : 125 | Color.clear) 126 | .animation(.easeInOut(duration: 0.2), value: currentApiVersion) 127 | ) 128 | .foregroundColor(currentApiVersion == version ? 129 | Color.blue.opacity(0.9) : 130 | Color.secondary.opacity(0.7)) 131 | } 132 | .buttonStyle(.plain) 133 | } 134 | } 135 | .padding(2) 136 | .background( 137 | RoundedRectangle(cornerRadius: 6) 138 | .fill(Color(NSColor.windowBackgroundColor).opacity(0.2)) 139 | .overlay( 140 | RoundedRectangle(cornerRadius: 6) 141 | .stroke(Color.secondary.opacity(0.15), lineWidth: 0.5) 142 | ) 143 | ) 144 | } 145 | .disabled(isRefreshing) 146 | 147 | HStack(spacing: 8) { 148 | BeautifulSearchField(text: $searchText) 149 | .frame(maxWidth: 200) 150 | 151 | Button(action: openSettings) { 152 | ZStack { 153 | Circle() 154 | .fill(Color.secondary.opacity(0.1)) 155 | .frame(width: 34, height: 34) 156 | 157 | Image(systemName: "gearshape") 158 | .font(.system(size: 15, weight: .medium)) 159 | .foregroundColor(.secondary) 160 | } 161 | } 162 | .buttonStyle(.plain) 163 | 164 | Button(action: onRefresh) { 165 | ZStack { 166 | Circle() 167 | .fill(isRefreshing ? Color.secondary.opacity(0.05) : Color.blue.opacity(0.1)) 168 | .frame(width: 34, height: 34) 169 | 170 | Image(systemName: "arrow.clockwise") 171 | .font(.system(size: 15, weight: .medium)) 172 | .foregroundColor(isRefreshing ? .secondary.opacity(0.5) : .blue) 173 | } 174 | } 175 | .disabled(isRefreshing) 176 | .buttonStyle(.plain) 177 | 178 | Button(action: { showDownloadManager.toggle() }) { 179 | ZStack { 180 | Circle() 181 | .fill(downloadTasksCount > 0 ? Color.blue.opacity(0.1) : Color.secondary.opacity(0.1)) 182 | .frame(width: 34, height: 34) 183 | 184 | Image(systemName: "arrow.down.circle") 185 | .font(.system(size: 15, weight: .medium)) 186 | .foregroundColor(downloadTasksCount > 0 ? .blue : .secondary) 187 | } 188 | .overlay( 189 | Group { 190 | if downloadTasksCount > 0 { 191 | ZStack { 192 | Circle() 193 | .fill(Color.blue) 194 | .frame(width: 18, height: 18) 195 | 196 | Text("\(downloadTasksCount)") 197 | .font(.system(size: 10, weight: .bold)) 198 | .foregroundColor(.white) 199 | } 200 | .offset(x: 12, y: -12) 201 | } 202 | } 203 | ) 204 | } 205 | .disabled(isRefreshing) 206 | .buttonStyle(.plain) 207 | } 208 | .frame(maxWidth: .infinity, alignment: .trailing) 209 | } 210 | .padding(.horizontal) 211 | .padding(.vertical, 8) 212 | .background(Color(.clear)) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.temporary-exception.files.absolute-path.read-write 6 | 7 | / 8 | 9 | com.apple.security.temporary-exception.files.home-relative-path.read-write 10 | 11 | / 12 | 13 | com.apple.security.application-groups 14 | 15 | com.x1a0he.macOS.Adobe-Downloader 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /AdobeDownloaderHelperTool/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.x1a0he.macOS.Adobe-Downloader.helper 7 | CFBundleName 8 | com.x1a0he.macOS.Adobe-Downloader.helper 9 | SMAuthorizedClients 10 | 11 | identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ 12 | 13 | MachServices 14 | 15 | com.x1a0he.macOS.Adobe-Downloader.helper 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AdobeDownloaderHelperTool/Launchd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.x1a0he.macOS.Adobe-Downloader.helper 7 | MachServices 8 | 9 | com.x1a0he.macOS.Adobe-Downloader.helper 10 | 11 | 12 | Program 13 | /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper 14 | ProgramArguments 15 | 16 | /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper 17 | 18 | RunAtLoad 19 | 20 | KeepAlive 21 | 22 | StandardErrorPath 23 | /tmp/com.x1a0he.macOS.Adobe-Downloader.helper.err 24 | StandardOutPath 25 | /tmp/com.x1a0he.macOS.Adobe-Downloader.helper.out 26 | 27 | -------------------------------------------------------------------------------- /AdobeDownloaderHelperTool/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | @objc enum CommandType: Int { 5 | case install 6 | case uninstall 7 | case moveFile 8 | case setPermissions 9 | case shellCommand 10 | } 11 | 12 | class SecureCommandHandler { 13 | static func createCommand(type: CommandType, path1: String, path2: String = "", permissions: Int = 0) -> String? { 14 | if type == .shellCommand { 15 | return path1 16 | } 17 | 18 | if type != .shellCommand { 19 | if !validatePath(path1) || (!path2.isEmpty && !validatePath(path2)) { 20 | return nil 21 | } 22 | } 23 | 24 | switch type { 25 | case .install: 26 | return "installer -pkg \"\(path1)\" -target /" 27 | case .uninstall: 28 | if path1.contains("*") { 29 | return "rm -rf \(path1)" 30 | } 31 | if path1.hasPrefix("\"") && path1.hasSuffix("\"") { 32 | return "rm -rf \(path1)" 33 | } 34 | return "rm -rf \"\(path1)\"" 35 | case .moveFile: 36 | let source = path1.hasPrefix("\"") ? path1 : "\"\(path1)\"" 37 | let dest = path2.hasPrefix("\"") ? path2 : "\"\(path2)\"" 38 | return "cp \(source) \(dest)" 39 | case .setPermissions: 40 | return "chmod \(permissions) \"\(path1)\"" 41 | case .shellCommand: 42 | return path1 43 | } 44 | } 45 | 46 | static func validatePath(_ path: String) -> Bool { 47 | let cleanPath = path.trimmingCharacters(in: .init(charactersIn: "\"'")) 48 | let allowedPaths = ["/Library/Application Support/Adobe"] 49 | if allowedPaths.contains(where: { cleanPath.hasPrefix($0) }) { 50 | return true 51 | } 52 | 53 | let forbiddenPaths = ["/System", "/usr", "/bin", "/sbin", "/var"] 54 | return !forbiddenPaths.contains { cleanPath.hasPrefix($0) } 55 | } 56 | } 57 | 58 | @objc(HelperToolProtocol) protocol HelperToolProtocol { 59 | @objc(executeCommand:path1:path2:permissions:withReply:) 60 | func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void) 61 | func getInstallationOutput(withReply reply: @escaping (String) -> Void) 62 | } 63 | 64 | class HelperTool: NSObject, HelperToolProtocol { 65 | private let listener: NSXPCListener 66 | private var connections: Set = [] 67 | private var currentTask: Process? 68 | private var outputPipe: Pipe? 69 | private var outputBuffer: String = "" 70 | private let logger = Logger(subsystem: "com.x1a0he.macOS.Adobe-Downloader.helper", category: "Helper") 71 | private let operationQueue = DispatchQueue(label: "com.x1a0he.macOS.Adobe-Downloader.helper.operation") 72 | 73 | override init() { 74 | listener = NSXPCListener(machServiceName: "com.x1a0he.macOS.Adobe-Downloader.helper") 75 | super.init() 76 | listener.delegate = self 77 | logger.notice("HelperTool 初始化完成") 78 | } 79 | 80 | func run() { 81 | logger.notice("Helper 服务开始运行") 82 | ProcessInfo.processInfo.disableSuddenTermination() 83 | ProcessInfo.processInfo.disableAutomaticTermination("Helper is running") 84 | 85 | listener.resume() 86 | logger.notice("XPC Listener 已启动") 87 | 88 | RunLoop.current.run() 89 | } 90 | 91 | func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void) { 92 | operationQueue.async { 93 | guard let shellCommand = SecureCommandHandler.createCommand(type: type, path1: path1, path2: path2, permissions: permissions) else { 94 | self.logger.error("不安全的路径访问被拒绝") 95 | reply("Error: Invalid path access") 96 | return 97 | } 98 | 99 | #if DEBUG 100 | self.logger.notice("收到安全命令执行请求: \(shellCommand, privacy: .public)") 101 | #else 102 | self.logger.notice("收到安全命令执行请求") 103 | #endif 104 | 105 | let isSetupCommand = shellCommand.contains("Setup") && shellCommand.contains("--install") 106 | 107 | let task = Process() 108 | let outputPipe = Pipe() 109 | let errorPipe = Pipe() 110 | 111 | task.standardOutput = outputPipe 112 | task.standardError = errorPipe 113 | task.arguments = ["-c", shellCommand] 114 | task.executableURL = URL(fileURLWithPath: "/bin/sh") 115 | 116 | if isSetupCommand { 117 | self.currentTask = task 118 | self.outputPipe = outputPipe 119 | self.outputBuffer = "" 120 | 121 | let outputHandle = outputPipe.fileHandleForReading 122 | outputHandle.readabilityHandler = { [weak self] handle in 123 | guard let self = self else { return } 124 | let data = handle.availableData 125 | if let output = String(data: data, encoding: .utf8) { 126 | self.outputBuffer += output 127 | } 128 | } 129 | 130 | do { 131 | try task.run() 132 | self.logger.debug("Setup命令开始执行") 133 | reply("Started") 134 | } catch { 135 | let errorMsg = "Error: \(error.localizedDescription)" 136 | self.logger.error("执行失败: \(errorMsg, privacy: .public)") 137 | reply(errorMsg) 138 | } 139 | return 140 | } 141 | 142 | do { 143 | try task.run() 144 | self.logger.debug("安全命令开始执行") 145 | } catch { 146 | let errorMsg = "Error: \(error.localizedDescription)" 147 | self.logger.error("执行失败: \(errorMsg, privacy: .public)") 148 | reply(errorMsg) 149 | return 150 | } 151 | 152 | let outputHandle = outputPipe.fileHandleForReading 153 | var output = "" 154 | 155 | outputHandle.readabilityHandler = { handle in 156 | let data = handle.availableData 157 | if let newOutput = String(data: data, encoding: .utf8) { 158 | output += newOutput 159 | } 160 | } 161 | 162 | task.waitUntilExit() 163 | 164 | outputHandle.readabilityHandler = nil 165 | errorPipe.fileHandleForReading.readabilityHandler = nil 166 | 167 | if task.terminationStatus == 0 { 168 | self.logger.notice("命令执行成功") 169 | reply(output.isEmpty ? "Success" : output) 170 | } else { 171 | self.logger.error("命令执行失败,退出码: \(task.terminationStatus, privacy: .public)") 172 | reply("Error: Command failed with exit code \(task.terminationStatus)") 173 | } 174 | } 175 | } 176 | 177 | func getInstallationOutput(withReply reply: @escaping (String) -> Void) { 178 | guard let task = currentTask else { 179 | reply("") 180 | return 181 | } 182 | 183 | if !task.isRunning { 184 | let exitCode = task.terminationStatus 185 | reply("Exit Code: \(exitCode)") 186 | cleanup() 187 | return 188 | } 189 | 190 | if !outputBuffer.isEmpty { 191 | let output = outputBuffer 192 | outputBuffer = "" 193 | reply(output) 194 | } else { 195 | reply("") 196 | } 197 | } 198 | 199 | func cleanup() { 200 | if let pipe = outputPipe { 201 | pipe.fileHandleForReading.readabilityHandler = nil 202 | } 203 | currentTask?.terminate() 204 | currentTask = nil 205 | outputPipe = nil 206 | } 207 | } 208 | 209 | extension HelperTool: NSXPCListenerDelegate { 210 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 211 | logger.notice("收到新的XPC连接请求") 212 | 213 | let pid = newConnection.processIdentifier 214 | 215 | var codeRef: SecCode? 216 | let codeSigningResult = SecCodeCopyGuestWithAttributes(nil, 217 | [kSecGuestAttributePid: pid] as CFDictionary, 218 | [], &codeRef) 219 | 220 | guard codeSigningResult == errSecSuccess, 221 | let code = codeRef else { 222 | logger.error("代码签名验证失败: \(pid)") 223 | return false 224 | } 225 | 226 | var staticCode: SecStaticCode? 227 | let staticCodeResult = SecCodeCopyStaticCode(code, [], &staticCode) 228 | guard staticCodeResult == errSecSuccess, 229 | let staticCodeRef = staticCode else { 230 | logger.error("获取静态代码失败: \(staticCodeResult)") 231 | return false 232 | } 233 | 234 | var requirement: SecRequirement? 235 | let requirementString = "identifier \"com.x1a0he.macOS.Adobe-Downloader\" and anchor apple generic and certificate leaf[subject.CN] = \"Apple Development: x1a0he@outlook.com (LFN2762T4F)\" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */" 236 | guard SecRequirementCreateWithString(requirementString as CFString, 237 | [], &requirement) == errSecSuccess, 238 | let req = requirement else { 239 | self.logger.error("签名要求创建失败") 240 | return false 241 | } 242 | 243 | let validityResult = SecStaticCodeCheckValidity(staticCodeRef, [], req) 244 | if validityResult != errSecSuccess { 245 | self.logger.error("代码签名验证不匹配: \(validityResult), 要求字符串: \(requirementString)") 246 | 247 | var signingInfo: CFDictionary? 248 | if SecCodeCopySigningInformation(staticCodeRef, [], &signingInfo) == errSecSuccess, 249 | let info = signingInfo as? [String: Any] { 250 | self.logger.notice("实际签名信息: \(String(describing: info))") 251 | } 252 | 253 | return false 254 | } 255 | 256 | let interface = NSXPCInterface(with: HelperToolProtocol.self) 257 | 258 | interface.setClasses(NSSet(array: [NSString.self, NSNumber.self]) as! Set, 259 | for: #selector(HelperToolProtocol.executeCommand(type:path1:path2:permissions:withReply:)), 260 | argumentIndex: 1, 261 | ofReply: false) 262 | 263 | newConnection.exportedInterface = interface 264 | newConnection.exportedObject = self 265 | 266 | newConnection.invalidationHandler = { [weak self] in 267 | guard let self = self else { return } 268 | self.logger.notice("XPC连接已断开") 269 | self.connections.remove(newConnection) 270 | if self.connections.isEmpty { 271 | self.cleanup() 272 | } 273 | } 274 | 275 | newConnection.interruptionHandler = { [weak self] in 276 | guard let self = self else { return } 277 | self.logger.error("XPC连接中断") 278 | self.connections.remove(newConnection) 279 | if self.connections.isEmpty { 280 | self.cleanup() 281 | } 282 | } 283 | 284 | self.connections.insert(newConnection) 285 | newConnection.resume() 286 | logger.notice("新的XPC连接已成功建立,当前活动连接数: \(self.connections.count)") 287 | 288 | return true 289 | } 290 | } 291 | 292 | autoreleasepool { 293 | let helperTool = HelperTool() 294 | helperTool.run() 295 | } 296 | -------------------------------------------------------------------------------- /analyze/channels.md: -------------------------------------------------------------------------------- 1 | # channels 的分析 2 | 3 | > 关于 Adobe 的 JSON 文件中 channels 的分析 4 | 5 | channels是一个数组,有两个成员,两个成员结构大致相同,分别是 6 | 7 | - sti 8 | - ccm 9 | 10 | ## sti 11 | 12 | sti 里的产品一般是 Adobe App 中的必要组件,主要有以下 13 | > 数据更新日为: 2025.02.25 14 | 15 | - CAI 16 | - Camera Raw 17 | - Camera Raw CC 18 | - Adobe Character Animator (Preview 4) 19 | - CCX Process 20 | - Codecs 21 | - STI_ColorCommonSet_CMYK_HD 22 | - STI_Color_MotionPicture_HD 23 | - STI_Color_Photoshop_HD 24 | - STI_Color_HD 25 | - STI_ColorCommonSet_RGB_HD 26 | - CoreSync 27 | - Adobe Captivate Assets 2017 28 | - Adobe Captivate Voices 2017 29 | - Camera Raw Elements 30 | - Animate HLAN 31 | - HD_ASU 32 | - Adobe Fonts 33 | - CC Library 34 | - Adobe Preview CC 35 | - Substance 3D Viewer for Photoshop 36 | - SeashellPSPlugin 37 | 38 | ## ccm 39 | 40 | ccm 里的产品一般是 Adobe App 中的主程序版本,主要有以下 41 | > 数据更新日为: 2025.02.25 42 | 43 | - Adobe Application Manager 44 | - After Effects 45 | - After Effects (Beta) 46 | - Aero (Beta) 47 | - Aero (Desktop) 48 | - InCopy 49 | - Media Encoder 50 | - Media Encoder (Beta) 51 | - Acrobat 52 | - Scout CC 53 | - Audition 54 | - Audition (Beta) 55 | - Character Animator 56 | - Character Animator (Beta) 57 | - Dreamweaver 58 | - Dimension 59 | - Flash Builder Premium 60 | - Animate 61 | - Fireworks CS6 62 | - Gaming SDK 1.4 63 | - InDesign 64 | - InDesign (Beta) 65 | - Illustrator 66 | - Illustrator (Prerelease) 67 | - Bridge 68 | - Bridge (Beta) 69 | - Creative Cloud 70 | - Extension Manager CC 71 | - Extendscript Toolkit CC 72 | - Lightroom 73 | - Lightroom Classic 74 | - Muse CC 75 | - Muse CC (2017) 76 | - Photoshop 77 | - Premiere Pro 78 | - Premiere Pro (Beta) 79 | - Premiere Rush 80 | - Premiere Rush CC 81 | - Premiere Rush (Beta) 82 | - Substance 3D Sampler 83 | - Substance Alchemist 84 | - Substance 3D Designer 85 | - Substance Designer 86 | - Substance 3D Painter 87 | - Substance Painter 88 | - Substance 3D Viewer (Beta) 89 | - Substance 3D Stager 90 | - Touch App Plugins 91 | - UXP Developer Tools -------------------------------------------------------------------------------- /change-log-generate.py: -------------------------------------------------------------------------------- 1 | def generate_xml(changelog_cn, changelog_en, ps_cn, ps_en): 2 | xml_template = """ 3 | 4 | ul{{margin-top: 0;margin-bottom: 7;padding-left: 18;}} 6 |

Adobe Downloader 更新日志:

7 |
    8 | {changelog_cn} 9 |
10 |

PS: {ps_cn}

11 |
12 |

Adobe Downloader Changes:

13 |
    14 | {changelog_en} 15 |
16 |

PS: {ps_en}

17 | ]]> 18 |
19 | """ 20 | 21 | changelog_cn_list = "\n".join([f"
  • {item}
  • " for item in changelog_cn]) 22 | changelog_en_list = "\n".join([f"
  • {item}
  • " for item in changelog_en]) 23 | 24 | return xml_template.format( 25 | changelog_cn=changelog_cn_list, 26 | changelog_en=changelog_en_list, 27 | ps_cn="
    ".join(ps_cn), 28 | ps_en="
    ".join(ps_en) 29 | ) 30 | 31 | 32 | def parse_input(text): 33 | sections = text.split("====================") 34 | 35 | if len(sections) < 2: 36 | raise ValueError("输入格式错误,必须包含 '====================' 作为分隔符") 37 | 38 | cn_lines = [line.strip() for line in sections[0].split("\n") if line.strip()] 39 | en_lines = [line.strip() for line in sections[1].split("\n") if line.strip()] 40 | 41 | changelog_cn, ps_cn, changelog_en, ps_en = [], [], [], [] 42 | 43 | for line in cn_lines: 44 | if line.startswith("PS:"): 45 | ps_cn.append(line.replace("PS: ", "")) 46 | else: 47 | changelog_cn.append(line) 48 | 49 | for line in en_lines: 50 | if line.startswith("PS:"): 51 | ps_en.append(line.replace("PS: ", "")) 52 | else: 53 | changelog_en.append(line) 54 | 55 | return changelog_cn, changelog_en, ps_cn, ps_en 56 | 57 | 58 | def main(): 59 | txt = """1. 修复部分情况下,Helper 无法重新连接的情况 60 | 2. 修复部分情况下,重新安装程序以及重新安装 Helper 的无法连接的情况 61 | 3. 调整 X1a0He CC 部分,1.5.0 版本可以选择 "下载并处理" 和 "仅下载" 62 | 4. 调整了部分 Setup 组件的内容翻译 63 | 5. 程序设置页中添加 「清理工具」 和 「常见问题」功能 64 | 6. 程序设置页中,添加当前版本显示 65 | 66 | PS: 当前版本添加的 「清理工具」功能为实验性功能,如有清理不全,请及时反馈 67 | PS: ⚠️ 1.5.0 版本将会是最后一个开源版本,请知晓 68 | 69 | ==================== 70 | 71 | 1. Fixed the issue of Helper not being able to reconnect in some cases 72 | 2. Fixed the issue of not being able to reconnect after reinstalling the program and reinstalling Helper 73 | 3. Adjusted the content translation of X1a0He CC, version 1.5.0 can choose "Download and Process" and "Only Download" 74 | 4. Adjusted the translation of some Setup component content 75 | 5. Added "Cleanup Tool" and "Common Issues" functions in the program settings page 76 | 6. Added the current version display in the program settings page 77 | 78 | PS: The "Cleanup Tool" function in the current version is an experimental feature. If some files are not cleaned up, please feedback in time 79 | PS: ⚠️ 1.5.0 version will be the last open source version, please be aware""" 80 | 81 | changelog_cn, changelog_en, ps_cn, ps_en = parse_input(txt) 82 | xml_output = generate_xml(changelog_cn, changelog_en, ps_cn, ps_en) 83 | print(xml_output) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /imgs/Adobe Downloader 2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/imgs/Adobe Downloader 2.0.0.png -------------------------------------------------------------------------------- /imgs/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/imgs/download.png -------------------------------------------------------------------------------- /imgs/language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/imgs/language.png -------------------------------------------------------------------------------- /imgs/preview-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/imgs/preview-dark.png -------------------------------------------------------------------------------- /imgs/preview-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/imgs/preview-light.png -------------------------------------------------------------------------------- /imgs/version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/785be094a542b8dd734d966117c78fb82790eb43/imgs/version.png -------------------------------------------------------------------------------- /readme-en.md: -------------------------------------------------------------------------------- 1 | # Adobe Downloader 2 | 3 | ![Adobe Downloader 2.0.0](imgs/Adobe%20Downloader%202.0.0.png) 4 | 5 | # **[中文版本](readme.md)** 6 | 7 | ## Before Use 8 | 9 | > ⚠️ This repository does not support any PR submission 10 | 11 | **🍎Only for macOS 12.0+.** 12 | 13 | > **If you like Adobe Downloader, or it helps you, please Star🌟 it.** 14 | > 15 | > 1. Before installing Adobe products, the Adobe Setup component must be present on your system; otherwise, the 16 | installation feature will not work. You can download it through the built-in “Settings” in the program or 17 | from [Adobe Creative Cloud](https://creativecloud.adobe.com/apps/download/creative-cloud). 18 | > 2. To enable smooth installation after downloading, Adobe Downloader needs to modify Adobe’s Setup program. This 19 | process is fully automated by the program and requires no user intervention. Many thanks 20 | to [QiuChenly](https://github.com/QiuChenly) for providing the solution. 21 | > 3. If you encounter any problems, don't panic, contact [@X1a0He](https://t.me/X1a0He_bot) on Telegram or use the 22 | Python 23 | version. Many thanks to [Drovosek01](https://github.com/Drovosek01) for 24 | the [adobe-packager](https://github.com/Drovosek01/adobe-packager) 25 | > 4. ⚠️⚠️⚠️ **All Adobe apps in Adobe Downloader are from official Adobe channels and are not cracked versions.** 26 | > 5. ❌❌❌ **Do not use an external hard drive or any USB to store it, as this will cause permission issues, I do not have 27 | the patience to solve any about permission issues** 28 | 29 | ## FAQ 30 | 31 | **This section will be updated periodically with meaningful issues that have been raised.** 32 | 33 | ### **[NEW] About error codes and Helper** 34 | 35 | Before version 1.3.0, many operations required users to enter passwords because root permissions or higher permissions 36 | were not obtained 37 | 38 | Therefore, we introduced the Helper mechanism in version 1.3.0. You only need to install the Helper and then the 39 | subsequent Setup component processing. Product installation no longer requires entering a password 40 | 41 | You may see relevant prompts in the upper right corner. Please rest assured that your system is very safe. This is 42 | because of macOS's Helper mechanism and signed pop-up windows 43 | 44 | If you are still worried about problems, please find a professional to check the relevant code, although it is futile 45 | 46 | ### Questions about the Setup Component 47 | 48 | > It’s mentioned in the usage instructions that to use the installation feature, you need to modify Adobe’s setup 49 | > component. You can find details in the code. 50 | 51 | Why is this necessary? Without modifications, installation will fail with error code 2700. 52 | 53 | > **Does the setup modification require user intervention?** 54 | 55 | No, Adobe Downloader automates the setup component handling, including backup. All you need to do is enter your password 56 | when prompted. 57 | 58 | 59 | 60 | 61 | 62 | Star History Chart 63 | 64 | 65 | 66 | ## 📔Latest Log 67 | 68 | - For historical update logs, please go to [Update Log](update-log.md) 69 | 70 | - 2025-04-23 Update Log 71 | 72 | ```markdown 73 | 1. Fixed an issue where dependency package detection on Intel-based models mistakenly identified osx10 as ARM architecture, leading to incorrect downloads. 74 | 2. Resolved text display issues in the settings interface header. 75 | 3. Adjusted the height of the download management interface from 500 to 600. 76 | 4. Added functionality to copy all product and package list information in download tasks. 77 | 5. Optimized display handling for the product Substance Alchemist. 78 | ``` 79 | 80 | ### Language friendly 81 | 82 | - [x] Chinese 83 | - [x] English 84 | 85 | ## ⚠️ Warning 86 | 87 | **If you have any optimization suggestions or questions about Adobe Downloader, please open an issue or 88 | contact [@X1a0He](https://t.me/X1a0He_bot) 89 | via Telegram.** 90 | 91 | ## ✨ Features 92 | 93 | - [x] Basic 📦 94 | - [x] Download Acrobat Pro 95 | - [x] Download other Adobe products 96 | - [x] Support installation of non-Acrobat products 97 | - [x] Support multiple products download at the same time 98 | - [x] Supports using default language and default directory 99 | - [x] Support task record persistence 100 | - [x] Installation 📦 101 | - [x] Cleanup 🧹 (1.5.0 added) 102 | - [x] Adobe applications 103 | - [x] Adobe Creative Cloud 104 | - [x] Adobe Preferences 105 | - [x] Adobe Cache files 106 | - [x] Adobe License files 107 | - [x] Adobe Log files 108 | - [x] Adobe Services 109 | - [x] Adobe Keychain 110 | - [x] Adobe Genuine Service 111 | - [x] Adobe hosts 112 | 113 | ## 👀 Preview 114 | 115 | ### Light Mode & Dark Mode 116 | 117 | ![light](imgs/preview-light.png) 118 | ![dark](imgs/preview-dark.png) 119 | 120 | ### Version Picker 121 | 122 | ![version picker](imgs/version.png) 123 | 124 | ### Language Picker 125 | 126 | ![language picker](imgs/language.png) 127 | 128 | ### Download Management 129 | 130 | ![download management](imgs/download.png) 131 | 132 | ## 🔗 References 133 | 134 | - [QiuChenly/InjectLib](https://github.com/QiuChenly/InjectLib/) 135 | 136 | ## 👨🏻‍💻Author 137 | 138 | Adobe Downloader © X1a0He 139 | 140 | Released under GPLv3. Created on 2024.11.05. 141 | 142 | > GitHub [@X1a0He](https://github.com/X1a0He) \ 143 | > Telegram [@X1a0He](https://t.me/X1a0He_bot) 144 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Adobe Downloader 2 | 3 | ![Adobe Downloader 2.0.0](imgs/Adobe%20Downloader%202.0.0.png) 4 | 5 | # **[English version](readme-en.md)** 6 | 7 | ## 使用须知 8 | 9 | > ⚠️ 本仓库不支持任何 Pr 提交 10 | 11 | **🍎仅支持 macOS 12.0+** 12 | 13 | > **如果你也喜欢 Adobe Downloader, 或者对你有帮助, 请 Star 仓库吧 🌟, 你的支持是我更新的动力** 14 | > 15 | > 1. 在对Adobe产品进行安装前,系统必须存在 Adobe Setup 组件,否则无法使用安装功能,可通过程序内置的"设置" 16 | 中进行下载,或前往[Adobe Creative Cloud](https://creativecloud.adobe.com/apps/download/creative-cloud)进行下载 17 | > 2. 为了能够在下载后顺利安装,Adobe Downloader 需要对 Adobe 的 Setup 18 | 程序做出修改,该过程由程序全自动,无需用户介入,非常感谢 [QiuChenly](https://github.com/QiuChenly) 19 | 提供的解决方案 20 | > 3. 如果在使用过程中遇到问题, 请通过 Telegram 联系我: [@X1a0He](https://t.me/X1a0He_bot) 21 | > 4. ⚠️⚠️⚠️ **Adobe Downloader 中的所有 Adobe 应用均来自 Adobe 官方渠道,并非破解版本。** 22 | > 5. ❌❌❌ **不要将下载目录设置为外接移动硬盘或者USB设备,这会导致出现权限问题,我并没有时间也没有耐心处理任何权限问题** 23 | 24 | ## 常见问题 25 | 26 | **这里会不定时更新一些 issues 出现过的,并且有意义的问题** 27 | 28 | ### **[NEW] 关于 错误代码 和 Helper 的问题** 29 | 30 | 在 1.3.0 版本以前,由于没有获取到 root 权限或者更高的权限,导致非常多的操作都需要用户输入密码 31 | 32 | 所以,我们在 1.3.0 版本中引入了 Helper 的机制,只需要安装了 Helper 后续的 Setup 组件处理,产品安装均不再需要输入密码 33 | 34 | 也许你会在右上角看到相关提示,请放心,你的系统非常安全,这是因为 macOS 的 Helper 机制和签名的弹窗 35 | 36 | 如果你还担心存在问题,请找专业人士查阅相关代码,尽管这是徒劳 37 | 38 | ### 关于 setup 组件的问题 39 | 40 | > 使用须知中强调了,如果需要使用安装功能,那么就必须对 Adobe 的 setup 组件进行修改处理,你可以在代码中找到 41 | 42 | 为什么要处理,因为不处理无法安装,会出现2700错误代码 43 | 44 | > **setup处理是否需要用户手动处理?** 45 | 46 | 并不需要,Adobe Downloader已完成了全自动处理且备份 setup 组件的功能 47 | 48 | 49 | 50 | 51 | 52 | Star History Chart 53 | 54 | 55 | 56 | ## 📔 最新日志 57 | 58 | - 更多关于 App 的更新日志,请查看 [Update Log](update-log.md) 59 | 60 | - 2025-04-23 更新日志 61 | 62 | ```markdown 63 | 1. 修复了 Intel 机型下依赖包判断遇到 osx10 被误判为 arm 导致下载错误的问题 64 | 2. 修复了设置界面上方的文字显示问题 65 | 3. 调整下载管理界面的高度: 500 -> 600 66 | 4. 新增下载任务复制所有产品和包列表信息 67 | 5. 优化产品 Substance Alchemist 的显示问题 68 | ``` 69 | 70 | ### 语言支持 71 | 72 | - [x] 中文 73 | - [x] English 74 | 75 | ## ⚠️ 注意 76 | 77 | **如果你对 Adobe Downloader 有任何优化建议或疑问,请提出 issue 或通过 Telegram 联系 [@X1a0He](https://t.me/X1a0He_bot)** 78 | 79 | ## ✨ 特点 80 | 81 | - [x] 基本功能 📦 82 | - [x] Acrobat Pro 的下载 83 | - [x] 其他 Adobe 产品的下载 84 | - [x] 支持安装非 Acrobat 产品 85 | - [x] 支持多个产品同时下载 86 | - [x] 支持使用默认语言和默认目录 87 | - [x] 支持任务记录持久化 88 | - [x] 安装功能 📦 89 | - [x] 清理功能 🧹 (1.5.0新增) 90 | - [x] Adobe 应用程序 91 | - [x] Adobe Creative Cloud 92 | - [x] Adobe 偏好设置 93 | - [x] Adobe 缓存文件 94 | - [x] Adobe 许可文件 95 | - [x] Adobe 日志文件 96 | - [x] Adobe 服务 97 | - [x] Adobe 钥匙串 98 | - [x] Adobe 正版验证服务 99 | - [x] Adobe Hosts 100 | 101 | ## 👀 预览 102 | 103 | ### 浅色模式 & 深色模式 104 | 105 | ![light](imgs/preview-light.png) 106 | ![dark](imgs/preview-dark.png) 107 | 108 | ### 版本选择 109 | 110 | ![version picker](imgs/version.png) 111 | 112 | ### 语言选择 113 | 114 | ![language picker](imgs/language.png) 115 | 116 | ### 下载任务管理 117 | 118 | ![download management](imgs/download.png) 119 | 120 | ## 🔗 引用 121 | 122 | - [QiuChenly/InjectLib](https://github.com/QiuChenly/InjectLib/) 123 | 124 | ## 👨🏻‍💻作者 125 | 126 | Adobe Downloader © X1a0He 127 | 128 | Released under GPLv3. Created on 2024.11.05. 129 | 130 | > GitHub [@X1a0He](https://github.com/X1a0He/) \ 131 | > Telegram [@X1a0He](https://t.me/X1a0He_bot) 132 | --------------------------------------------------------------------------------