├── .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 |
10 | PS: {ps_cn}
11 |
12 | Adobe Downloader Changes:
13 |
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 | 
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 |
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 | 
118 | 
119 |
120 | ### Version Picker
121 |
122 | 
123 |
124 | ### Language Picker
125 |
126 | 
127 |
128 | ### Download Management
129 |
130 | 
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 | 
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 |
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 | 
106 | 
107 |
108 | ### 版本选择
109 |
110 | 
111 |
112 | ### 语言选择
113 |
114 | 
115 |
116 | ### 下载任务管理
117 |
118 | 
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 |
--------------------------------------------------------------------------------