Init: Initially complete the basic download and installation functions
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.idea/
|
||||
Adobe-Downloader.xcodeproj
|
||||
Adobe-Downloader.xcodeproj/*.workspace
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
341
Adobe Downloader.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,341 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3CCC3AE02CC67B8F006E22B4 /* Adobe-Downloader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Adobe-Downloader.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
3CCC3AE22CC67B8F006E22B4 /* Adobe Downloader */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "Adobe Downloader";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3CCC3B112CC67F7A006E22B4 /* Localizables */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = Localizables;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
3CCC3ADD2CC67B8F006E22B4 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
3CCC3AD72CC67B8F006E22B4 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3CCC3AE22CC67B8F006E22B4 /* Adobe Downloader */,
|
||||
3CCC3AE12CC67B8F006E22B4 /* Products */,
|
||||
3CCC3B112CC67F7A006E22B4 /* Localizables */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3CCC3AE12CC67B8F006E22B4 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3CCC3AE02CC67B8F006E22B4 /* Adobe-Downloader.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
3CCC3ADF2CC67B8F006E22B4 /* Adobe-Downloader */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3CCC3B052CC67B91006E22B4 /* Build configuration list for PBXNativeTarget "Adobe-Downloader" */;
|
||||
buildPhases = (
|
||||
3CCC3ADC2CC67B8F006E22B4 /* Sources */,
|
||||
3CCC3ADD2CC67B8F006E22B4 /* Frameworks */,
|
||||
3CCC3ADE2CC67B8F006E22B4 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
3CCC3AE22CC67B8F006E22B4 /* Adobe Downloader */,
|
||||
3CCC3B112CC67F7A006E22B4 /* Localizables */,
|
||||
);
|
||||
name = "Adobe-Downloader";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "Adobe-Downloader";
|
||||
productReference = 3CCC3AE02CC67B8F006E22B4 /* Adobe-Downloader.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
3CCC3AD82CC67B8F006E22B4 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1600;
|
||||
LastUpgradeCheck = 1610;
|
||||
TargetAttributes = {
|
||||
3CCC3ADF2CC67B8F006E22B4 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 3CCC3ADB2CC67B8F006E22B4 /* Build configuration list for PBXProject "Adobe Downloader" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 3CCC3AD72CC67B8F006E22B4;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 3CCC3AE12CC67B8F006E22B4 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
3CCC3ADF2CC67B8F006E22B4 /* Adobe-Downloader */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
3CCC3ADE2CC67B8F006E22B4 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
3CCC3ADC2CC67B8F006E22B4 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
3CCC3B032CC67B91006E22B4 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3CCC3B042CC67B91006E22B4 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3CCC3B062CC67B91006E22B4 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Adobe Downloader";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3CCC3B072CC67B91006E22B4 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = "Adobe Downloader/Adobe Downloader.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Adobe Downloader";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
3CCC3ADB2CC67B8F006E22B4 /* Build configuration list for PBXProject "Adobe Downloader" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3CCC3B032CC67B91006E22B4 /* Debug */,
|
||||
3CCC3B042CC67B91006E22B4 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3CCC3B052CC67B91006E22B4 /* Build configuration list for PBXNativeTarget "Adobe-Downloader" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3CCC3B062CC67B91006E22B4 /* Debug */,
|
||||
3CCC3B072CC67B91006E22B4 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 3CCC3AD82CC67B8F006E22B4 /* Project object */;
|
||||
}
|
||||
7
Adobe Downloader.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "05600D7B-4F3A-44C5-8A39-5E4971936E92"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>Adobe-Downloader.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
14
Adobe Downloader/Adobe Downloader.entitlements
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
|
||||
<array>
|
||||
<string>/Library/Application Support/Adobe/</string>
|
||||
</array>
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
|
||||
<array>
|
||||
<string>/Downloads/</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
20
Adobe Downloader/Adobe DownloaderApp.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct Adobe_DownloaderApp: App {
|
||||
@StateObject private var networkManager = NetworkManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(networkManager)
|
||||
.frame(width: 850, height: 700)
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.windowResizability(.contentSize)
|
||||
|
||||
Settings {
|
||||
AboutView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Acrobat.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "acrobat-pro.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Acrobat.imageset/acrobat-pro.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="256px" height="256px" viewBox="0 0 256 256" style="enable-background:new 0 0 256 256;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#B30B00;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M45.5,3.2h165.1c24.7,0,45.5,19.9,45.5,45.5v158.7c0,24.7-19.9,45.5-45.5,45.5H45.5
|
||||
C20.7,252.8,0,232.9,0,207.4V48.6C0,23.1,19.9,3.2,45.5,3.2z"/>
|
||||
<path class="st1" d="M204.2,147.5c-12-12.8-44.7-7.2-52.6-6.4c-11.2-11.2-19.1-23.9-22.3-28.7c4-12,7.2-25.5,7.2-38.3
|
||||
c0-12-4.8-23.9-17.5-23.9c-4.8,0-8.8,2.4-11.2,6.4c-5.6,9.6-3.2,28.7,5.6,48.6c-4.8,14.4-12.8,35.9-22.3,52.6
|
||||
c-12.8,4.8-40.7,17.5-43.1,31.9c-0.8,4,0.8,8.8,4,11.2c3.2,3.2,7.2,4,11.2,4c16.7,0,33.5-23.1,45.5-43.9c9.6-3.2,24.7-8,39.9-10.4
|
||||
c17.5,16,33.5,18.3,41.5,18.3c11.2,0,15.2-4.8,16.7-8.8C208.9,156.3,207.4,150.7,204.2,147.5z M193,155.5c-0.8,3.2-4.8,6.4-12,4.8
|
||||
c-8.8-2.4-16.7-6.4-23.1-12c5.6-0.8,19.1-2.4,28.7-0.8C189.8,148.3,193.8,150.7,193,155.5z M115.6,59.8c0.8-1.6,2.4-2.4,4-2.4
|
||||
c4,0,4.8,4.8,4.8,8.8c-0.8,9.6-2.4,19.9-5.6,28.7C112.4,77.4,113.2,64.6,115.6,59.8z M114.8,149.9c4-7.2,8.8-20.7,10.4-25.5
|
||||
c4,7.2,11.2,15.2,14.4,19.1C140.4,142.8,126,145.9,114.8,149.9z M87.7,168.3C76.6,185.8,66.2,197,59.8,197c-0.8,0-2.4,0-3.2-0.8
|
||||
c-0.8-1.6-1.6-3.2-0.8-4.8C56.6,185,69.4,176.2,87.7,168.3z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Adobe Downloader/Assets.xcassets/After Effects (Beta).imageset/192x192.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
21
Adobe Downloader/Assets.xcassets/After Effects (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/After Effects.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "after-effects.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
36
Adobe Downloader/Assets.xcassets/After Effects.imageset/after-effects.svg
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 240 234" style="enable-background:new 0 0 240 234;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#00005B;}
|
||||
.st1{fill:#9999FF;}
|
||||
</style>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Surfaces">
|
||||
<g id="Video_Audio_Surface">
|
||||
<g id="Outline_no_shadow">
|
||||
<path class="st0" d="M42.5,0h155C221,0,240,19,240,42.5v149c0,23.5-19,42.5-42.5,42.5h-155C19,234,0,215,0,191.5v-149
|
||||
C0,19,19,0,42.5,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Outlined_Mnemonics_Logos">
|
||||
<g id="Ae">
|
||||
<path class="st1" d="M96.4,140H59.2l-7.6,23.6c-0.2,0.9-1,1.5-1.9,1.4H30.9c-1.1,0-1.4-0.6-1.1-1.8L62,70.9c0.3-1,0.6-1.9,1-3.1
|
||||
c0.4-2.1,0.6-4.3,0.6-6.5c-0.1-0.5,0.3-1,0.8-1.1c0.1,0,0.2,0,0.3,0h25.6c0.7,0,1.2,0.3,1.3,0.8l36.5,102.5c0.3,1.1,0,1.6-1,1.6
|
||||
h-20.9c-0.7,0.1-1.4-0.4-1.6-1.1L96.4,140z M65,120.1h25.4c-0.6-2.1-1.4-4.6-2.3-7.2c-0.9-2.7-1.8-5.6-2.7-8.6
|
||||
c-1-3.1-1.9-6.1-2.9-9.2c-1-3.1-1.9-6-2.7-8.9c-0.8-2.8-1.5-5.4-2.2-7.8h-0.2c-0.9,4.3-2,8.6-3.4,12.9c-1.5,4.8-3,9.8-4.6,14.8
|
||||
C68.1,111.2,66.5,115.9,65,120.1z"/>
|
||||
<path class="st1" d="M187.4,130.8h-31.7c0.4,3.1,1.4,6.2,3.1,8.9c1.8,2.7,4.3,4.8,7.3,6c4,1.7,8.4,2.6,12.8,2.5
|
||||
c3.5-0.1,7-0.4,10.4-1.1c3.1-0.4,6.1-1.2,8.9-2.3c0.5-0.4,0.8-0.2,0.8,0.8v15.3c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.3-0.4,0.5-0.7,0.7
|
||||
c-3.2,1.4-6.5,2.4-10,3c-4.7,0.9-9.4,1.3-14.2,1.2c-7.6,0-14-1.2-19.2-3.5c-4.9-2.1-9.2-5.4-12.6-9.5c-3.2-3.9-5.5-8.3-6.9-13.1
|
||||
c-1.4-4.7-2.1-9.6-2.1-14.6c0-5.4,0.8-10.7,2.5-15.9c1.6-5,4.1-9.6,7.5-13.7c3.3-4,7.4-7.2,12.1-9.5c4.7-2.3,10.3-3.1,16.7-3.1
|
||||
c5.3-0.1,10.6,0.9,15.5,3.1c4.1,1.8,7.7,4.5,10.5,8c2.6,3.4,4.7,7.2,6,11.4c1.3,4,1.9,8.1,1.9,12.2c0,2.4-0.1,4.5-0.2,6.4
|
||||
c-0.2,1.9-0.3,3.3-0.4,4.2c-0.1,0.7-0.7,1.3-1.4,1.3c-0.6,0-1.7,0.1-3.3,0.2c-1.6,0.2-3.5,0.3-5.8,0.3
|
||||
C192.4,131.2,190,130.8,187.4,130.8z M155.7,116.2h21.1c2.6,0,4.5,0,5.7-0.1c0.8-0.1,1.6-0.3,2.3-0.8v-1c0-1.3-0.2-2.5-0.6-3.7
|
||||
c-1.8-5.6-7.1-9.4-13-9.2c-5.5-0.3-10.7,2.6-13.3,7.6C156.7,111.3,156,113.7,155.7,116.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
21
Adobe Downloader/Assets.xcassets/Animate.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "animate.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Animate.imageset/animate.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#00005b;}.cls-2{fill:#99f;}</style></defs><title>Asset 148</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="Video_Audio_Surface" data-name="Video/Audio Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="An"><path class="cls-2" d="M97.04315,140H59.85223l-7.6377,23.46769a1.89429,1.89429,0,0,1-1.93212,1.449H31.44549q-1.61133,0-1.127-1.771L62.65236,70.87622c.32227-.96582.644-1.82674.96631-3.06209a34.47808,34.47808,0,0,0,.644-6.52027.99642.99642,0,0,1,1.127-1.12719H90.9883q1.125,0,1.28808.80517l36.41325,102.33472q.48267,1.61133-.96631,1.61011H106.79315a1.48825,1.48825,0,0,1-1.60987-1.127ZM65.71144,120.14233h25.438q-.96606-3.21863-2.25391-7.24511-1.29053-4.02246-2.73681-8.61353-1.44946-4.58826-2.89844-9.177-1.44873-4.58826-2.65625-8.855-1.20777-4.26379-2.17334-7.80835h-.16113a130.10721,130.10721,0,0,1-3.38086,12.87988q-2.2566,7.24512-4.58887,14.812Q67.96413,113.70484,65.71144,120.14233Z"/><path class="cls-2" d="M139.95981,163.38436V104.04224q0-2.7356-.08007-6.11792-.0835-3.38086-.2417-6.43994c-.1084-2.03785-.21631-3.93059-.32178-5.113a.9562.9562,0,0,1,.16064-.96607,1.31358,1.31358,0,0,1,.96631-.322h16.07145a3.59919,3.59919,0,0,1,1.61036.322,1.51637,1.51637,0,0,1,.80468,1.127q.32081.96606.72461,2.17358a16.52607,16.52607,0,0,1,.56348,3.2614,32.34487,32.34487,0,0,1,10.626-6.19849A36.57379,36.57379,0,0,1,182.919,83.75635a30.62737,30.62737,0,0,1,9.41846,1.52954,23.79268,23.79268,0,0,1,15.21435,14.812c1.60987,4.13306,2.24138,9.47363,2.24138,16.01929v47.26717q0,1.44909-1.28809,1.449L189.6924,165a1.42233,1.42233,0,0,1-1.60986-1.61011V118.544a22.1973,22.1973,0,0,0-1.94385-8.70557,11.50377,11.50377,0,0,0-4.10547-4.991,12.1157,12.1157,0,0,0-6.84229-1.771,20.031,20.031,0,0,0-7.56689,1.3684,19.08763,19.08763,0,0,0-5.957,3.78345v55.15609q0,1.44909-1.28809,1.449H141.409A1.28093,1.28093,0,0,1,139.95981,163.38436Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/128.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/16.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/256 1x.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/256 2x.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/32 1x.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/32 2x.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/512 2x.png
Normal file
BIN
Adobe Downloader/Assets.xcassets/AppIcon.appiconset/64 1.png
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32 2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32 1x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "64 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256 2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256 1x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512 2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "Adobe Downloader- 512 x 513.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "Adobe Downloader - 1024 x 1025.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Adobe Downloader/Assets.xcassets/Audition (Beta).imageset/192x192 (1).png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
21
Adobe Downloader/Assets.xcassets/Audition (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (1).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Audition.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "audition.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Audition.imageset/audition.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#00005b;}.cls-2{fill:#99f;}</style></defs><title>Artboard 1</title><rect class="cls-1" width="240" height="234" rx="42.5"/><path class="cls-2" d="M94.32324,140H57.13184l-7.5669,23.551A1.89358,1.89358,0,0,1,47.63281,165H28.7959q-1.61133,0-1.127-1.771L59.86914,70.87622q.48267-1.44873.96582-3.30054a29.74633,29.74633,0,0,0,.78573-6.37858.99662.99662,0,0,1,1.12695-1.12719H88.34676q1.12426,0,1.28809.80517L126.04,163.38989q.48267,1.61133-.96582,1.61011H104.144a1.48792,1.48792,0,0,1-1.60986-1.12695Zm-31.395-19.85767h25.438q-.96606-3.21863-2.25439-7.24511-1.29054-4.02246-2.73682-8.61353-1.44873-4.58826-2.89795-9.177-1.44873-4.58826-2.65674-8.855-1.207-4.26379-2.17334-7.80835h-.16113A129.83529,129.83529,0,0,1,72.105,91.32324q-2.25659,7.24512-4.58838,14.812Q65.18091,113.70484,62.92822,120.14233Z"/><path class="cls-2" d="M202.83691,86.21053v61.30168q0,4.02648.08057,7.16455.07909,3.13953.32178,5.47412c.16113,1.55713.29443,2.4895.40283,3.561.10547.86036-.269,1.28809-1.12695,1.28809h-17.227a1.8896,1.8896,0,0,1-1.93213-1.12695,24.25482,24.25482,0,0,1-.56348-2.49561,10.58926,10.58926,0,0,1-.2417-2.11206,28.73423,28.73423,0,0,1-11.27,6.19849,43.559,43.559,0,0,1-11.10888,1.52954,33.20536,33.20536,0,0,1-10.86719-1.69068,21.20047,21.20047,0,0,1-8.53321-5.47387,25.79177,25.79177,0,0,1-5.63476-9.66016,44.18714,44.18714,0,0,1-2.0127-14.24829v-49.549a1.1382,1.1382,0,0,1,1.28809-1.28809h19.13314a1.13916,1.13916,0,0,1,1.28808,1.28809v46.97282q0,6.60351,2.898,10.38452c1.93213,2.52319,5.82194,3.78345,10.32976,3.78345a18.26761,18.26761,0,0,0,6.76171-1.20752,25.49323,25.49323,0,0,0,6.34392-3.3003V86.21053c0-.74976.48291-1.1272,1.44922-1.1272H201.71A.9976.9976,0,0,1,202.83691,86.21053Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Adobe Downloader/Assets.xcassets/Bridge (Beta).imageset/192x192.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
21
Adobe Downloader/Assets.xcassets/Bridge (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Bridge.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bridge.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Bridge.imageset/bridge.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#000b1d;}.cls-2{fill:#fff;}</style></defs><title>Artboard 1</title><rect class="cls-1" width="240" height="234" rx="42.5"/><path class="cls-2" d="M55.0625,164.1665,55,62.03052q0-1.44873.96631-1.60987,2.73559-.15857,8.37158-.24145c3.75586-.053,7.4303-.10571,11.83264-.16113q6.59985-.07911,12.07471-.08057c8.7998,0,16.00476.63477,21.21228,2.56689q7.80688,2.89783,11.99463,7.32544a24.1801,24.1801,0,0,1,5.91113,9.25757,31.04394,31.04394,0,0,1,1.44922,8.855,25.61447,25.61447,0,0,1-1.88672,9.41846,25.135,25.135,0,0,1-3.86377,6.84253,18.37823,18.37823,0,0,1-4.34717,4.0249,32.122,32.122,0,0,1,6.35938,4.42749,26.24885,26.24885,0,0,1,6.0376,7.96948,25.958,25.958,0,0,1,2.77453,12.19861,28.992,28.992,0,0,1-5.59192,17.26453q-5.39574,7.407-15.45605,11.51147A63.4326,63.4326,0,0,1,88.769,165.9375H78.0625c-3.38086,0-6.231-.02759-9.07324-.08057q-4.26783-.08313-7.40625-.16089-3.13917-.08313-5.39356-.08056Q55.06153,165.45692,55.0625,164.1665ZM76.83264,78.87024l.10486,22.27405H87.481q4.34693,0,8.5332.16089a29.9335,29.9335,0,0,1,5.95654.644,14.59966,14.59966,0,0,0,3.46192-4.34693,12.49237,12.49237,0,0,0,1.63659-6.1715,12.28643,12.28643,0,0,0-2.3612-7.35242,12.52579,12.52579,0,0,0-6.0376-4.10547,32.23991,32.23991,0,0,0-10.16736-1.26379H83.83411c-1.396,0-2.252.02783-3.37891.08056Q78.76477,78.8728,76.83264,78.87024Zm.10486,41.06726v26.76978c1.82422.10815,3.24316.18872,5.06934.24145q2.7356.08313,6.60107.08057a42.59823,42.59823,0,0,0,11.67236-1.449,15.09155,15.09155,0,0,0,7.64746-4.508,11.7948,11.7948,0,0,0,3.13477-7.5669,13.99712,13.99712,0,0,0-2.65137-7.56714c-1.39648-2.14575-3.97217-3.90771-7.728-4.87378a39.35908,39.35908,0,0,0-4.42774-.80493,46.77008,46.77008,0,0,0-5.87646-.322Z"/><path class="cls-2" d="M148.17627,85.01489H165.7251a2.18992,2.18992,0,0,1,2.09277,1.61011,7.78189,7.78189,0,0,1,.80518,2.25391,26.02512,26.02512,0,0,1,.48291,3.13964c.10595,1.127.16113,2.6858.16113,3.97364a38.56036,38.56036,0,0,1,10.70654-8.9646c4.34717-2.415,8.93262-2.96509,14.51514-2.96509a1.28058,1.28058,0,0,1,1.44873,1.449v19.17481q0,1.1283-1.60986,1.127a36.87973,36.87973,0,0,0-10.168.72461,39.61157,39.61157,0,0,0-8.37207,2.6565,20.00151,20.00151,0,0,0-5.9126,3.70288V163.551q0,1.44909-1.28809,1.449H149.625a1.42233,1.42233,0,0,1-1.60986-1.61011V108.3894q0-3.53979-.08057-7.48657-.08277-3.94335-.2417-7.80859a57.31046,57.31046,0,0,0-.64355-6.95215.925.925,0,0,1,1.12695-1.1272Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
Adobe Downloader/Assets.xcassets/Camera Raw.imageset/192x192 (5).png
vendored
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
21
Adobe Downloader/Assets.xcassets/Camera Raw.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (5).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Adobe Downloader/Assets.xcassets/Character Animator (Beta).imageset/192x192 (1).png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
21
Adobe Downloader/Assets.xcassets/Character Animator (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (1).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Character Animator.imageset/Character Animator.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#00005b;}.cls-2{fill:#99f;}</style></defs><title>Artboard 1</title><rect class="cls-1" width="240" height="234" rx="42.5"/><path class="cls-2" d="M114.83333,145.2583v16.583a2.124,2.124,0,0,1-1.28759,2.2539,40.74266,40.74266,0,0,1-9.48275,2.17359,103.546,103.546,0,0,1-12.63867.72461,71.17638,71.17638,0,0,1-17.79053-2.17359,52.60115,52.60115,0,0,1-15.37549-6.60107,46.89238,46.89238,0,0,1-11.91357-10.86743,48.17125,48.17125,0,0,1-7.728-14.9729,63.083,63.083,0,0,1-2.73731-19.32007q0-16.25976,7.084-28.57764a48.83838,48.83838,0,0,1,20.125-19.15893,66.09514,66.09514,0,0,1,31.07324-7.23845,101.80561,101.80561,0,0,1,12.47754.64405,37.34879,37.34879,0,0,1,8.45215,1.93213,2.07222,2.07222,0,0,1,.80517,1.93188V80.69727c0,.96606-.37744,1.28808-1.12695.96606a35.60969,35.60969,0,0,0-9.61291-2.93152,73.87457,73.87457,0,0,0-11.75293-.8855,36.76712,36.76712,0,0,0-18.40125,4.46106A30.14377,30.14377,0,0,0,63.08838,94.30176a37.1591,37.1591,0,0,0-4.186,18.11255,39.19347,39.19347,0,0,0,2.0127,13.2019,31.3052,31.3052,0,0,0,5.55469,9.82105,27.51331,27.51331,0,0,0,8.0498,6.60107,39.61266,39.61266,0,0,0,9.57959,3.62256A43.452,43.452,0,0,0,94,147.16667a108.58423,108.58423,0,0,0,10.86768-.48316,37.49154,37.49154,0,0,0,8.67806-2.23014q.64086-.483.96582-.24146A1.28143,1.28143,0,0,1,114.83333,145.2583Z"/><path class="cls-2" d="M151.7998,107.26221v56.4497c0,.86036-.37744,1.28809-1.12695,1.28809H130.58333a1.28093,1.28093,0,0,1-1.44922-1.449V52.36133c0-.74976.42774-1.127,1.28809-1.127h20.25065a.99742.99742,0,0,1,1.12695,1.127l.11687,37.99609a46.019,46.019,0,0,1,9.30159-4.74951c3.38086-1.23267,7.3252-1.52458,11.8335-1.52458a32.15363,32.15363,0,0,1,9.41845,1.449,22.33874,22.33874,0,0,1,8.77442,4.74455c2.68164,2.415,4.5638,5.69018,6.17366,9.821q2.41481,6.19959,2.415,16.01929V163.551q0,1.44909-1.28808,1.449H179.6932a1.42233,1.42233,0,0,1-1.60987-1.61011V118.37134a20.19262,20.19262,0,0,0-1.52978-8.45264,11.9783,11.9783,0,0,0-4.186-5.07129,13.74062,13.74062,0,0,0-7.36605-1.771,23.842,23.842,0,0,0-7.084,1.04638A19.51536,19.51536,0,0,0,151.7998,107.26221Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
21
Adobe Downloader/Assets.xcassets/Character Animator.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Character Animator.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
Adobe Downloader/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Dreamweaver.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "dreamweaver.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Dreamweaver.imageset/dreamweaver.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#470137;}.cls-2{fill:#ff61f6;}</style></defs><title>Asset 108</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="UI_UX_Surface" data-name="UI/UX Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="Dw"><path class="cls-2" d="M22.79165,163.77319V61.3772a1.154,1.154,0,0,1,1.07061-1.28784q2.29406-.15858,6.72978-.24146,4.43435-.07947,10.40037-.16113,5.9651-.07911,12.69512-.08057,18.35382,0,30.43711,6.60108a43.38642,43.38642,0,0,1,18.12421,18.11254q6.04026,11.51257,6.04187,26.48438a61.904,61.904,0,0,1-3.13574,20.52759,48.50033,48.50033,0,0,1-8.56531,15.37549,51.60105,51.60105,0,0,1-12.31242,10.70654A52.6561,52.6561,0,0,1,69.594,163.69287a61.57744,61.57744,0,0,1-15.44771,2.01245H42.75139q-6.04373,0-11.24182-.08056-5.20112-.08313-7.80039-.24146Q22.79142,165.3833,22.79165,163.77319Zm21.10689-84.364v66.65405q1.68105,0,3.0592.08057,1.37629.08313,2.82959.16089,1.45144.08313,3.28836.08056a39.105,39.105,0,0,0,13.61264-2.2539,27.65111,27.65111,0,0,0,10.47691-6.762A30.60459,30.60459,0,0,0,83.895,126.26025a44.13817,44.13817,0,0,0,2.37083-14.9729,39.56635,39.56635,0,0,0-2.29429-14.08764A26.24931,26.24931,0,0,0,66.91748,81.01929,43.24974,43.24974,0,0,0,53.68753,79.0874q-3.06153,0-5.12388.08032Q46.49851,79.25085,43.89854,79.40918Z"/><path class="cls-2" d="M197.8317,165.39883H179.11545a1.42488,1.42488,0,0,1-1.19792-.40259,4.85239,4.85239,0,0,1-.59851-1.20752q-2.84653-12.0747-4.79168-21.33227-1.94741-9.25635-3.21912-15.69751-1.27376-6.43762-2.09613-10.948-.82488-4.50549-1.27285-7.56714h-.1494q-1.49853,6.60353-2.77,12.79956-1.27443,6.19959-2.77,12.47754-1.49853,6.27906-3.369,13.84595-1.87385,7.56959-4.11779,16.42187c-.20117,1.07422-.74836,1.61011-1.647,1.61011H132.39975a2.25187,2.25187,0,0,1-1.42224-.322,2.7092,2.7092,0,0,1-.67389-1.12695l-19.46506-77.28q-.44889-1.28687,1.19792-1.28809h18.8661q1.34732,0,1.49717.96607,2.99367,13.20336,5.016,22.78149,2.02165,9.58082,3.1442,16.34155,1.12322,6.76209,1.8718,11.02857.74723,4.26746,1.19792,7.00341h.29926a41.10162,41.10162,0,0,1,.74881-4.66894q.59668-2.89819,1.49717-7.48657.89844-4.58862,2.246-10.86743,1.34732-6.27906,3.06927-14.57056,1.71991-8.29029,4.5669-18.91748a3.4155,3.4155,0,0,1,.37463-1.28809c.1494-.21386.524-.322,1.12254-.322h19.61492c.599,0,.94726.37744,1.04807,1.1272q2.54411,10.95043,4.26719,19.2395,1.71992,8.29395,2.9948,14.65088,1.27035,6.36217,1.94628,10.948.67366,4.58862,1.27285,7.728a48.99622,48.99622,0,0,1,.74881,5.07154h.29925q.74723-3.0564,1.34778-7.084.596-4.02246,1.57209-9.09668.972-5.07129,2.09614-11.18945,1.12321-6.11646,2.77-13.68481,1.64566-7.56592,3.74316-16.42212.29834-1.28687,1.34777-1.28809h17.51833q1.34732,0,1.04807,1.449l-21.5612,77.11914a3.5396,3.5396,0,0,1-.599,1.12695A1.6416,1.6416,0,0,1,197.8317,165.39883Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Adobe Downloader/Assets.xcassets/Illustrator (Prerelease).imageset/192x192 (3).png
vendored
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
21
Adobe Downloader/Assets.xcassets/Illustrator (Prerelease).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (3).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Adobe Downloader/Assets.xcassets/Illustrator Beta.imageset/192x192 (2).png
vendored
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
21
Adobe Downloader/Assets.xcassets/Illustrator Beta.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (2).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Illustrator.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "illustrator.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Illustrator.imageset/illustrator.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#300;}.cls-2{fill:#ff9a00;}</style></defs><title>Asset 104</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="Drawing_Surface" data-name="Drawing Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="Ai"><path class="cls-2" d="M116.30029,140.42822H79.10938l-7.5669,23.50611a1.89431,1.89431,0,0,1-1.93213,1.449H50.77344q-1.61133,0-1.127-1.771L81.84619,70.87622q.4834-1.44873.96631-3.30054a34.478,34.478,0,0,0,.644-6.52026.99643.99643,0,0,1,1.127-1.1272h25.59863q1.125,0,1.28808.80518l36.54737,103.03979q.48267,1.61133-.96631,1.61011H126.12109a1.48824,1.48824,0,0,1-1.60986-1.12695Zm-31.395-20.28589h25.438q-.96606-3.21863-2.2539-7.24511-1.29054-4.02246-2.73682-8.61353-1.44947-4.58826-2.89844-9.177-1.44873-4.58826-2.65625-8.855-1.20776-4.26379-2.17334-7.80835h-.16113a130.10721,130.10721,0,0,1-3.38086,12.87988q-2.2566,7.24512-4.58887,14.812Q87.158,113.70484,84.90527,120.14233Z"/><path class="cls-2" d="M169.75049,76.99438a11.67783,11.67783,0,0,1-8.855-3.542,12.73665,12.73665,0,0,1-3.38135-9.177,11.813,11.813,0,0,1,3.62256-8.93555,12.44131,12.44131,0,0,1,8.93555-3.46142q5.79638,0,9.09668,3.46142a12.4294,12.4294,0,0,1,3.30029,8.93555,12.57378,12.57378,0,0,1-3.46143,9.177A12.3536,12.3536,0,0,1,169.75049,76.99438Zm-11.10938,86.77881v-76.958c0-.96582.42774-1.449,1.28809-1.449h19.80322q1.28687,0,1.28809,1.449v76.958q0,1.61133-1.28809,1.61011H160.09033Q158.6416,165.3833,158.64111,163.77319Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
21
Adobe Downloader/Assets.xcassets/InCopy.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "incopy.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/InCopy.imageset/incopy.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#49021f;}.cls-2{fill:#f36;}</style></defs><title>Asset 100</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="Publishing_Surface" data-name="Publishing Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="Ic"><path class="cls-2" d="M96.49609,61.21631V163.77319q0,1.61133-1.44873,1.61011H75.56592q-1.28981,0-1.2876-1.61011V61.21631q0-1.28686,1.44873-1.28809H95.208A1.13916,1.13916,0,0,1,96.49609,61.21631Z"/><path class="cls-2" d="M173.293,146.54639v16.583a1.58323,1.58323,0,0,1-.80469,1.449,32.223,32.223,0,0,1-8.5332,1.93188q-4.82959.483-9.33789.48316a49.15747,49.15747,0,0,1-17.8711-3.05908,36.90879,36.90879,0,0,1-13.36279-8.6941,39.42369,39.42369,0,0,1-8.45264-13.2019,43.96084,43.96084,0,0,1-2.97851-16.261,41.341,41.341,0,0,1,5.63525-21.65454,40.06428,40.06428,0,0,1,15.8584-14.89258q10.22241-5.47155,23.9082-5.47387a61.44138,61.44138,0,0,1,9.499.56347,44.25928,44.25928,0,0,1,5.47412,1.20752,1.8953,1.8953,0,0,1,.80517,1.771l-.16113,16.583q0,1.44909-1.28808,1.127a29.04977,29.04977,0,0,0-5.47413-1.36841,49.79892,49.79892,0,0,0-8.0498-.56347,28.46821,28.46821,0,0,0-11.91406,2.41479,18.9705,18.9705,0,0,0-8.37207,7.24512q-3.06006,4.83-3.05909,12.23608,0,8.373,3.62256,13.20191a19.61436,19.61436,0,0,0,9.17725,6.84253,32.59531,32.59531,0,0,0,11.18945,2.01245,68.49913,68.49913,0,0,0,7.728-.40259,28.146,28.146,0,0,0,5.47412-1.04639Q173.2915,145.25953,173.293,146.54639Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Adobe Downloader/Assets.xcassets/InDesign (Beta).imageset/192x192.png
vendored
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
21
Adobe Downloader/Assets.xcassets/InDesign (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/InDesign.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "indesign.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/InDesign.imageset/indesign.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><defs><style>.cls-1{fill:#49021f;}.cls-2{fill:#f36;}</style></defs><title>256</title><g id="Surfaces"><g id="Publishing_Surface" data-name="Publishing Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" x="8" y="4" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="Id"><path class="cls-2" d="M95.1582,65.21631V167.77319q0,1.61133-1.44873,1.61011H74.228q-1.28981,0-1.2876-1.61011V65.21631q0-1.28686,1.44873-1.28809h19.481A1.13916,1.13916,0,0,1,95.1582,65.21631Z"/><path class="cls-2" d="M152.79639,170.99341a49.88223,49.88223,0,0,1-21.49366-4.50806,34.17219,34.17219,0,0,1-15.05322-13.60449q-5.47559-9.09411-5.47412-22.78149a42.04123,42.04123,0,0,1,5.47412-21.09107,40.1871,40.1871,0,0,1,15.939-15.45605q10.46337-5.796,25.27685-5.7959.80347,0,2.09278.08056,1.28687.08277,3.05908.24146v-31.717c0-.74976.32226-1.127.96631-1.127h20.28564a.854.854,0,0,1,.96631.96582v95.15112q0,2.73962.24121,5.957.24169,3.22228.40283,5.7959a1.66418,1.66418,0,0,1-.96631,1.61011,79.86,79.86,0,0,1-16.26074,4.82983A87.29931,87.29931,0,0,1,152.79639,170.99341Zm9.8208-19.96411V107.07642a15.97072,15.97072,0,0,0-2.65625-.48316,32.10968,32.10968,0,0,0-3.30078-.16089,24.86085,24.86085,0,0,0-11.27,2.57593,22.00524,22.00524,0,0,0-8.45215,7.406q-3.30176,4.83-3.30078,12.719a28.39124,28.39124,0,0,0,1.69043,10.304,19.58772,19.58772,0,0,0,4.5083,7.084,17.16656,17.16656,0,0,0,6.76172,4.02515,26.49038,26.49038,0,0,0,8.2915,1.28784q2.25293,0,4.186-.16089A17.23154,17.23154,0,0,0,162.61719,151.0293Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
21
Adobe Downloader/Assets.xcassets/Lightroom Classic.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Lightroom Classic.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Lightroom Classic.imageset/Lightroom Classic.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}</style></defs><title>Asset 180</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="Photo_Surface" data-name="Photo Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="LrC"><path class="cls-2" d="M84.192,165.3833H27.10131q-1.44954,0-1.44888-1.771V61.3772q0-1.44873,1.3043-1.449H44.63461c.67456,0,1.01425.37744,1.01425,1.1272v84.042H87.23519q1.30386,0,1.01426,1.449L85.6413,163.93433q-.14634.96606-.57964,1.20752A1.77863,1.77863,0,0,1,84.192,165.3833Z"/><path class="cls-2" d="M98.53617,85.36621h15.79394q1.30386,0,1.8835,1.61011a8.35226,8.35226,0,0,1,.72466,2.2539,28.71923,28.71923,0,0,1,.43462,3.13965q.143,1.69044.145,3.62232a34.80174,34.80174,0,0,1,9.63589-8.61328,24.98991,24.98991,0,0,1,13.40332-3.62256q1.30385,0,1.30385,1.449v19.481q0,1.1283-1.44887,1.127a32.23561,32.23561,0,0,0-9.49087.72461,33.15877,33.15877,0,0,0-7.53487,2.6565,15.39786,15.39786,0,0,0-4.85419,3.70288v51.03711q0,1.44909-1.15928,1.449H99.84q-1.4502,0-1.44888-1.61011V108.3894q0-3.53979-.07251-7.48657-.07449-3.94335-.21753-7.80859a56.54581,56.54581,0,0,0-.5792-6.60083,1.35031,1.35031,0,0,1,.21709-.8855A1.0463,1.0463,0,0,1,98.53617,85.36621Z"/><path class="cls-2" d="M212.62149,70.60112a25.83378,25.83378,0,0,0-6.67765-1.70558,75.39944,75.39944,0,0,0-10.19685-.58619c-9.80813,0-18.388,2.10233-25.50092,6.24858a42.56967,42.56967,0,0,0-16.555,17.51031c-3.85522,7.44733-5.81023,16.20145-5.81023,26.01991a63.596,63.596,0,0,0,2.24138,17.56717,46.1497,46.1497,0,0,0,6.353,13.67058,40.59588,40.59588,0,0,0,9.81019,9.9415,41.323,41.323,0,0,0,12.65638,6.03819,53.131,53.131,0,0,0,14.58865,1.98137,76.40988,76.40988,0,0,0,10.32505-.65856,32.8752,32.8752,0,0,0,8.05575-2.0005,2.55936,2.55936,0,0,0,1.4629-2.65854V147.05247a1.80324,1.80324,0,0,0-.50762-1.467c-.25019-.20729-.78056-.4549-1.56008.14267a28.74176,28.74176,0,0,1-6.96711,1.65984,78.829,78.829,0,0,1-8.72258.43008,30.76187,30.76187,0,0,1-7.84072-1.06279A29.61771,29.61771,0,0,1,180.1948,143.57a22.35682,22.35682,0,0,1-6.323-5.76526,28.55726,28.55726,0,0,1-4.38973-8.62954,38.36439,38.36439,0,0,1-1.59729-11.667,35.99278,35.99278,0,0,1,3.31762-15.99054,25.412,25.412,0,0,1,9.39562-10.5127,27.17413,27.17413,0,0,1,14.62793-3.8056,53.422,53.422,0,0,1,9.39355.78573,23.36188,23.36188,0,0,1,7.18113,2.3998,1.26058,1.26058,0,0,0,1.27991.00931,1.72374,1.72374,0,0,0,.60893-1.49443V72.96811A2.56436,2.56436,0,0,0,212.62149,70.60112Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
21
Adobe Downloader/Assets.xcassets/Lightroom.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "lightroom.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
31
Adobe Downloader/Assets.xcassets/Lightroom.imageset/lightroom.svg
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 240 234" style="enable-background:new 0 0 240 234;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#001E36;}
|
||||
.st1{fill:#31A8FF;}
|
||||
</style>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Surfaces">
|
||||
<g id="Photo_Surface">
|
||||
<g id="Outline_no_shadow">
|
||||
<path class="st0" d="M42.5,0h155C221,0,240,19,240,42.5v149c0,23.5-19,42.5-42.5,42.5h-155C19,234,0,215,0,191.5v-149
|
||||
C0,19,19,0,42.5,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Outlined_Mnemonics_Logos">
|
||||
<g id="Lr">
|
||||
<path class="st1" d="M126,165.4H62.6c-1.1,0-1.6-0.6-1.6-1.8V61.4c-0.1-0.7,0.4-1.3,1.1-1.4c0.1,0,0.2,0,0.4,0h19.6
|
||||
c0.5-0.1,1.1,0.3,1.1,0.8c0,0.1,0,0.2,0,0.3v84h46.2c1,0,1.3,0.5,1.1,1.4l-2.9,17.4c0,0.5-0.3,0.9-0.6,1.2
|
||||
C126.7,165.3,126.4,165.4,126,165.4z"/>
|
||||
<path class="st1" d="M142,85.4h17.5c1,0,1.8,0.7,2.1,1.6c0.4,0.7,0.7,1.5,0.8,2.3c0.2,1,0.4,2.1,0.5,3.1c0.1,1.1,0.2,2.3,0.2,3.6
|
||||
c3-3.5,6.6-6.4,10.7-8.6c4.6-2.5,9.7-3.7,14.9-3.6c0.7-0.1,1.3,0.4,1.4,1.1c0,0.1,0,0.2,0,0.4v19.5c0,0.8-0.5,1.1-1.6,1.1
|
||||
c-6.5-0.4-13,0.8-18.9,3.4c-2,0.9-3.9,2.1-5.4,3.7v51c0,1-0.4,1.4-1.3,1.4h-19.5c-0.8,0.1-1.5-0.4-1.6-1.2c0-0.1,0-0.3,0-0.4
|
||||
v-55.4c0-2.4,0-4.9-0.1-7.5c-0.1-2.6-0.1-5.2-0.2-7.8c-0.1-2.2-0.3-4.4-0.6-6.6c-0.1-0.5,0.2-1,0.7-1.1
|
||||
C141.7,85.3,141.8,85.3,142,85.4L142,85.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Adobe Downloader/Assets.xcassets/Media Encoder (Beta).imageset/192x192 (1).png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
21
Adobe Downloader/Assets.xcassets/Media Encoder (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (1).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Media Encoder.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "media-encoder.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Media Encoder.imageset/media-encoder.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#00005b;}.cls-2{fill:#99f;}</style></defs><title>Asset 106</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="Video_Audio_Surface" data-name="Video/Audio Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="Me"><path class="cls-2" d="M33.84038,60.89844a1.07979,1.07979,0,0,1,1.22368-.96606H63.20688a1.52823,1.52823,0,0,1,1.68245,1.1272q1.068,4.18835,2.6,10.0625,1.52729,5.87879,3.28836,12.96045,1.75689,7.08654,3.5945,14.49,1.83554,7.40735,3.59451,14.40942,1.7562,7.00343,2.9822,12.88013,1.22253,5.87879,1.9886,9.74048h.15308q.60882-3.38123,2.14121-9.01612,1.52658-5.63379,3.5175-12.63843,1.98651-7.00341,4.05327-14.40966,2.06514-7.40333,4.05328-14.65088,1.9858-7.24512,3.74712-13.44361,1.75689-6.19592,2.82959-10.38427a1.33567,1.33567,0,0,1,1.37675-1.1272h27.37791a1.21606,1.21606,0,0,1,1.37676,1.1272l3.67058,102.8789a1.073,1.073,0,0,1-.22915,1.04639,1.31054,1.31054,0,0,1-.99453.40259H116.12763a1.19507,1.19507,0,0,1-.76491-.24146,1.069,1.069,0,0,1-.30569-.8855q0-8.21118-.07654-17.22705-.07932-9.01464-.22962-18.11255-.15377-9.09375,0-17.4685.151-8.36938.15308-15.45606,0-7.08252.07654-12.3164.07515-5.22986.07654-7.8086H114.904q-.91776,4.0265-2.44736,10.54566-1.53077,6.52038-3.59405,14.65088-2.06512,8.13171-4.43549,16.82446-2.37337,8.69421-4.58858,17.22705-2.21891,8.53564-4.28288,15.939-2.06445,7.40735-3.44143,13.041a1.69161,1.69161,0,0,1-1.68245,1.28809H73.14848a1.6445,1.6445,0,0,1-1.83552-1.28809q-1.37629-5.63379-3.13528-13.041-1.76037-7.40332-3.59451-15.45581-1.83481-8.049-3.90019-16.50269-2.06443-8.45252-3.9002-16.34131-1.83482-7.88672-3.28835-14.812-1.45562-6.92066-2.67651-12.23608h-.30616v12.719q0,7.40734-.22915,16.34155-.2296,8.93555-.45876,18.676-.22962,9.743-.5353,20.125-.30825,10.38427-.76492,20.52734c0,.86035-.40867,1.28809-1.22368,1.28809H29.558a1.83142,1.83142,0,0,1-.99406-.24146q-.38409-.24169-.22962-1.20752Z"/><path class="cls-2" d="M196.67432,130.25122h-31.7168a20.74008,20.74008,0,0,0,3.05908,8.45264,16.56436,16.56436,0,0,0,7.3252,6.03735q4.90942,2.2566,12.7998,2.25415a58.43054,58.43054,0,0,0,10.38428-.8855,43.29305,43.29305,0,0,0,9.41846-2.81762c.53564-.42749.80517-.16089.80517.80517v15.29492a2.38427,2.38427,0,0,1-.2417,1.20752,2.30981,2.30981,0,0,1-.72461.72437,45.14814,45.14814,0,0,1-10.46484,3.46167,70.76093,70.76093,0,0,1-14.168,1.20752q-11.43384,0-19.15918-3.542a34.14679,34.14679,0,0,1-12.55762-9.499,37.19779,37.19779,0,0,1-6.92334-13.12158,51.668,51.668,0,0,1-2.09277-14.57056,50.74576,50.74576,0,0,1,2.49561-15.85839,41.42459,41.42459,0,0,1,7.48632-13.68506,35.96394,35.96394,0,0,1,12.0752-9.499,37.76341,37.76341,0,0,1,16.74365-3.46142,36.33453,36.33453,0,0,1,15.53662,3.05884,28.09974,28.09974,0,0,1,10.54541,8.2915,36.61534,36.61534,0,0,1,5.957,11.35059,40.44772,40.44772,0,0,1,1.93213,12.23608q0,3.54309-.2417,6.43994-.241,2.89784-.40234,4.186a1.45875,1.45875,0,0,1-1.44922,1.28808q-.96606,0-3.30029.24146-2.3357.24133-5.7959.322Q200.53565,130.25365,196.67432,130.25122Zm-31.7168-15.61694h21.09082q3.86425,0,5.71533-.08057a11.3564,11.3564,0,0,0,2.335-.24145v-.96607a12.88035,12.88035,0,0,0-.644-3.70288,13.15261,13.15261,0,0,0-13.041-9.177,13.98587,13.98587,0,0,0-13.28271,7.56689A20.32457,20.32457,0,0,0,164.95752,114.63428Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Adobe Downloader/Assets.xcassets/Photoshop (Beta).imageset/192x192 (2).png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
21
Adobe Downloader/Assets.xcassets/Photoshop (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (2).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Photoshop.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "photoshop.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Photoshop.imageset/photoshop.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 234"><defs><style>.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}</style></defs><title>Asset 116</title><g id="Layer_2" data-name="Layer 2"><g id="Surfaces"><g id="Photo_Surface" data-name="Photo Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" width="240" height="234" rx="42.5"/></g></g></g><g id="Outlined_Mnemonics_Logos" data-name="Outlined Mnemonics & Logos"><g id="Ps"><path class="cls-2" d="M54.04167,164.09521V61.21631c0-.74976.32226-1.127.96631-1.127,1.71533,0,3.28157-.02515,5.64388-.08057q3.53979-.07911,7.64746-.16089,4.106-.07947,8.69433-.16113,4.5879-.07911,9.09619-.08057,12.23366,0,20.60791,3.05908a35.755,35.755,0,0,1,13.44385,8.21094,31.496,31.496,0,0,1,7.3252,11.35059,37.64894,37.64894,0,0,1,2.25439,12.96045q0,12.88256-5.957,21.252a33.65844,33.65844,0,0,1-16.1001,12.15552c-6.7622,2.52319-14.27636,3.3789-22.54,3.3789q-3.54345,0-4.99121-.08056-1.44873-.07947-4.34668-.08057v32.12183a1.28093,1.28093,0,0,1-1.44922,1.449H55.16862C54.41667,165.3833,54.04167,164.95557,54.04167,164.09521Zm21.74446-84.686v33.55493q2.09034.16224,3.86377.16089h5.313a37.7594,37.7594,0,0,0,11.51172-1.83765,17.35824,17.35824,0,0,0,8.21094-5.313q3.13915-3.70167,3.13965-10.304a16.28281,16.28281,0,0,0-2.335-8.85522,15.01394,15.01394,0,0,0-7.00341-5.71534A29.83951,29.83951,0,0,0,86.73389,79.0874q-3.86427,0-6.84229.08032Q76.91065,79.25085,75.78613,79.40918Z"/><path class="cls-2" d="M191.97114,106.863a37.6431,37.6431,0,0,0-9.57959-3.3811,50.875,50.875,0,0,0-11.18946-1.28809,20.82175,20.82175,0,0,0-6.03759.72461,5.42475,5.42475,0,0,0-3.13965,2.01245,5.0699,5.0699,0,0,0-.80469,2.73706,4.27537,4.27537,0,0,0,.96582,2.57593,10.95825,10.95825,0,0,0,3.38086,2.65649,67.449,67.449,0,0,0,7.084,3.30054,70.20083,70.20083,0,0,1,15.37549,7.32544,23.38242,23.38242,0,0,1,7.88916,8.2915A22.10738,22.10738,0,0,1,198.25,142.122a23.143,23.143,0,0,1-3.86377,13.28247,25.41573,25.41573,0,0,1-11.18995,8.93531q-7.32788,3.219-18.1123,3.22021a65.50368,65.50368,0,0,1-13.60449-1.28808,43.40843,43.40843,0,0,1-10.22363-3.22,2.08508,2.08508,0,0,1-1.127-1.93213V143.73187a.94571.94571,0,0,1,.40283-.8855.781.781,0,0,1,.88526.08057,43.01131,43.01131,0,0,0,12.397,4.9104,51.12181,51.12181,0,0,0,11.75293,1.52954q5.63379,0,8.2915-1.449a4.5512,4.5512,0,0,0,2.65674-4.186q0-2.09034-2.415-4.02491-2.41479-1.93212-9.82129-4.66918a59.18392,59.18392,0,0,1-14.24853-7.24488,24.5718,24.5718,0,0,1-7.5669-8.45263,22.20192,22.20192,0,0,1-2.33447-10.22339,23.08045,23.08045,0,0,1,3.38086-12.075,24.57046,24.57046,0,0,1,10.46533-9.177q7.08252-3.53943,17.71-3.542a78.40115,78.40115,0,0,1,12.397.8855,32.49681,32.49681,0,0,1,8.63066,2.33447,1.46829,1.46829,0,0,1,.96582.8855,4.44869,4.44869,0,0,1,.16113,1.20752v16.261a1.08221,1.08221,0,0,1-.48291.96606A1.556,1.556,0,0,1,191.97114,106.863Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Adobe Downloader/Assets.xcassets/Premiere Pro (Beta).imageset/192x192 (3).png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
21
Adobe Downloader/Assets.xcassets/Premiere Pro (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (3).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Premiere Pro.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "premiere-pro.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
34
Adobe Downloader/Assets.xcassets/Premiere Pro.imageset/premiere-pro.svg
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 240 234" style="enable-background:new 0 0 240 234;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#00005B;}
|
||||
.st1{fill:#9999FF;}
|
||||
</style>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Surfaces">
|
||||
<g id="Video_Audio_Surface">
|
||||
<g id="Outline_no_shadow">
|
||||
<path class="st0" d="M42.5,0h155C221,0,240,19,240,42.5v149c0,23.5-19,42.5-42.5,42.5h-155C19,234,0,215,0,191.5v-149
|
||||
C0,19,19,0,42.5,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Outlined_Mnemonics_Logos">
|
||||
<g id="Pr">
|
||||
<path class="st1" d="M57,164.1V61.2c0-0.7,0.3-1.1,1-1.1c1.7,0,3.3,0,5.6-0.1c2.4-0.1,4.9-0.1,7.6-0.2c2.7-0.1,5.6-0.1,8.7-0.2
|
||||
c3.1-0.1,6.1-0.1,9.1-0.1c8.2,0,15,1,20.6,3.1c5,1.7,9.6,4.5,13.4,8.2c3.2,3.2,5.7,7.1,7.3,11.4c1.5,4.2,2.3,8.5,2.3,13
|
||||
c0,8.6-2,15.7-6,21.3s-9.6,9.8-16.1,12.2c-6.8,2.5-14.3,3.4-22.5,3.4c-2.4,0-4,0-5-0.1s-2.4-0.1-4.3-0.1V164
|
||||
c0.1,0.7-0.4,1.3-1.1,1.4c-0.1,0-0.2,0-0.4,0h-19C57.4,165.4,57,165,57,164.1z M78.8,79.4V113c1.4,0.1,2.7,0.2,3.9,0.2H88
|
||||
c3.9,0,7.8-0.6,11.5-1.8c3.2-0.9,6-2.8,8.2-5.3c2.1-2.5,3.1-5.9,3.1-10.3c0.1-3.1-0.7-6.2-2.3-8.9c-1.7-2.6-4.1-4.6-7-5.7
|
||||
c-3.7-1.5-7.7-2.1-11.8-2c-2.6,0-4.9,0-6.8,0.1C80.9,79.2,79.5,79.3,78.8,79.4L78.8,79.4z"/>
|
||||
<path class="st1" d="M146.6,85.2h17.5c1,0,1.8,0.7,2.1,1.6c0.3,0.8,0.5,1.6,0.6,2.5c0.2,1,0.4,2.1,0.5,3.1
|
||||
c0.1,1.1,0.2,2.3,0.2,3.6c3-3.5,6.6-6.4,10.7-8.6c4.6-2.6,9.9-3.9,15.2-3.9c0.7-0.1,1.3,0.4,1.4,1.1c0,0.1,0,0.2,0,0.4v19.5
|
||||
c0,0.8-0.5,1.1-1.6,1.1c-3.6-0.1-7.3,0.2-10.8,1c-2.9,0.6-5.7,1.5-8.4,2.7c-1.9,0.9-3.7,2.1-5.1,3.7v51c0,1-0.4,1.4-1.3,1.4
|
||||
h-19.7c-0.8,0.1-1.5-0.4-1.6-1.2c0-0.1,0-0.3,0-0.4v-55.4c0-2.4,0-4.9-0.1-7.5s-0.1-5.2-0.2-7.8c0-2.3-0.2-4.5-0.4-6.8
|
||||
c-0.1-0.5,0.2-1,0.7-1.1C146.3,85.1,146.5,85.1,146.6,85.2L146.6,85.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Adobe Downloader/Assets.xcassets/Premiere Rush (Beta).imageset/192x192 (4).png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
21
Adobe Downloader/Assets.xcassets/Premiere Rush (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "192x192 (4).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Adobe Downloader/Assets.xcassets/Premiere Rush.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Premiere Rush.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Adobe Downloader/Assets.xcassets/Premiere Rush.imageset/Premiere Rush.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><defs><style>.cls-1{fill:#00005b;}.cls-2{fill:#99f;}</style></defs><title>256</title><g id="Surfaces"><g id="Video_Audio_Surface" data-name="Video/Audio Surface"><g id="Outline_no_shadow" data-name="Outline no shadow"><rect class="cls-1" x="8" y="4" width="240" height="234" rx="42.5"/></g></g></g><g id="Live_Text" data-name="Live Text"><g id="Ru"><path class="cls-2" d="M68.67,169.38H49.52c-.86,0-1.29-.48-1.29-1.45V65.54c0-1,.32-1.45,1-1.45q5.64-.17,9.66-.24l7.73-.16c2.47-.06,5-.08,7.65-.08H83q13.53,0,22.7,3.78a30.67,30.67,0,0,1,14.09,11Q124.7,85.5,124.7,96A28.09,28.09,0,0,1,122.13,108a30.3,30.3,0,0,1-7.09,9.58,34.67,34.67,0,0,1-10.46,6.52q1.61,2.1,4.51,6.36t6.36,9.5q3.45,5.24,6.92,10.55t6.28,9.82c1.87,3,3.3,5.37,4.26,7.08a1.75,1.75,0,0,1,.41,1.29.66.66,0,0,1-.73.64H109.25a3.36,3.36,0,0,1-1.45-.24,2.14,2.14,0,0,1-.81-.72c-1.18-1.72-2.65-4-4.43-6.76s-3.64-5.91-5.63-9.34-4-6.82-6-10.15-3.73-6.33-5.23-9A11,11,0,0,0,83.89,131a3.24,3.24,0,0,0-2-.56H70v37.51C70,168.9,69.53,169.38,68.67,169.38Zm1.29-58H82.52a32.18,32.18,0,0,0,10.55-1.53,13.89,13.89,0,0,0,6.84-4.75,13,13,0,0,0,2.41-8q0-7.4-5.47-10.7t-14.49-3.3c-2.58,0-5,0-7.16.08s-4,.13-5.24.24Z"/><path class="cls-2" d="M209.55,90.49v61c0,2.69,0,5.07.08,7.17s.16,3.92.32,5.47.3,2.87.4,3.95c.11.86-.27,1.28-1.12,1.28H192a1.88,1.88,0,0,1-1.93-1.12c-.22-.75-.4-1.59-.57-2.5a14.48,14.48,0,0,1-.24-2.49A28.61,28.61,0,0,1,178,169.46,43.45,43.45,0,0,1,166.88,171,33.08,33.08,0,0,1,156,169.3a21.18,21.18,0,0,1-8.54-5.47,25.83,25.83,0,0,1-5.63-9.66,44.05,44.05,0,0,1-2-14.25V90.65a1.13,1.13,0,0,1,1.28-1.28h19.81a1.13,1.13,0,0,1,1.28,1.28v46.69q0,6.61,2.9,10.39t9.66,3.78a18.15,18.15,0,0,0,6.76-1.21,21.07,21.07,0,0,0,5.64-3.3V90.49c0-.75.48-1.12,1.45-1.12h19.8A1,1,0,0,1,209.55,90.49Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
22
Adobe Downloader/Assets.xcassets/Substance 3D Sampler (Beta).imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Substance 3D Sampler (Beta) - 96x96.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Substance 3D Sampler (Beta) - 192x192.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
22
Adobe Downloader/Assets.xcassets/Substance 3D Sampler.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Substance 3D Sampler - 96x96.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Substance 3D Sampler - 192x192.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Adobe Downloader/Assets.xcassets/Substance 3D Sampler.imageset/Substance 3D Sampler - 192x192.png
vendored
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Adobe Downloader/Assets.xcassets/Substance 3D Sampler.imageset/Substance 3D Sampler - 96x96.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
439
Adobe Downloader/Commons/Enums.swift
Normal file
@@ -0,0 +1,439 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
enum NetworkError: Error, LocalizedError {
|
||||
case noConnection
|
||||
case timeout(TimeInterval)
|
||||
case serverUnreachable(String)
|
||||
|
||||
case invalidURL(String)
|
||||
case invalidRequest(String)
|
||||
case invalidResponse
|
||||
|
||||
case invalidData(String)
|
||||
case parsingError(Error, String)
|
||||
case dataValidationError(String)
|
||||
|
||||
case httpError(Int, String?)
|
||||
case serverError(Int)
|
||||
case clientError(Int)
|
||||
|
||||
case downloadError(String, Error?)
|
||||
case downloadCancelled
|
||||
case insufficientStorage(Int64, Int64)
|
||||
|
||||
case fileSystemError(String, Error?)
|
||||
case fileExists(String)
|
||||
case fileNotFound(String)
|
||||
case filePermissionDenied(String)
|
||||
|
||||
case applicationInfoError(String, Error?)
|
||||
case unsupportedPlatform(String)
|
||||
case incompatibleVersion(String, String)
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .noConnection: return 1001
|
||||
case .timeout: return 1002
|
||||
case .serverUnreachable: return 1003
|
||||
case .invalidURL: return 2001
|
||||
case .invalidRequest: return 2002
|
||||
case .invalidResponse: return 2003
|
||||
case .invalidData: return 3001
|
||||
case .parsingError: return 3002
|
||||
case .dataValidationError: return 3003
|
||||
case .httpError: return 4001
|
||||
case .serverError: return 4002
|
||||
case .clientError: return 4003
|
||||
case .downloadError: return 5001
|
||||
case .downloadCancelled: return 5002
|
||||
case .insufficientStorage: return 5003
|
||||
case .fileSystemError: return 6001
|
||||
case .fileExists: return 6002
|
||||
case .fileNotFound: return 6003
|
||||
case .filePermissionDenied: return 6004
|
||||
case .applicationInfoError: return 7001
|
||||
case .unsupportedPlatform: return 7002
|
||||
case .incompatibleVersion: return 7003
|
||||
}
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noConnection:
|
||||
return NSLocalizedString("没有网络连接", comment: "Network error")
|
||||
case .timeout(let duration):
|
||||
return NSLocalizedString("请求超时: \(duration)", comment: "Network timeout")
|
||||
case .serverUnreachable(let server):
|
||||
return NSLocalizedString("无法连接到服务器: \(server)", comment: "Server unreachable")
|
||||
case .invalidURL(let url):
|
||||
return NSLocalizedString("无效的URL: \(url)", comment: "Invalid URL")
|
||||
case .invalidRequest(let reason):
|
||||
return NSLocalizedString("无效的请求: \(reason)", comment: "Invalid request")
|
||||
case .invalidResponse:
|
||||
return NSLocalizedString("服务器响应无效", comment: "Invalid response")
|
||||
case .invalidData(let detail):
|
||||
return NSLocalizedString("数据无效: \(detail)", comment: "Invalid data")
|
||||
case .parsingError(let error, let context):
|
||||
return NSLocalizedString("解析错误: \(context) - \(error.localizedDescription)", comment: "Parsing error")
|
||||
case .dataValidationError(let reason):
|
||||
return NSLocalizedString("数据验证失败: \(reason)", comment: "Data validation error")
|
||||
case .httpError(let code, let message):
|
||||
return NSLocalizedString("HTTP错误 \(code): \(message ?? "")", comment: "HTTP error")
|
||||
case .serverError(let code):
|
||||
return NSLocalizedString("服务器错误: \(code)", comment: "Server error")
|
||||
case .clientError(let code):
|
||||
return NSLocalizedString("客户端错误: \(code)", comment: "Client error")
|
||||
case .downloadError(let message, let error):
|
||||
if let error = error {
|
||||
return NSLocalizedString("\(message): \(error.localizedDescription)", comment: "Download error")
|
||||
}
|
||||
return NSLocalizedString(message, comment: "Download error")
|
||||
case .downloadCancelled:
|
||||
return NSLocalizedString("下载已取消", comment: "Download cancelled")
|
||||
case .insufficientStorage(let needed, let available):
|
||||
return NSLocalizedString("存储空间不足: 需要 \(needed)字节, 可用 \(available)字节", comment: "Insufficient storage")
|
||||
case .fileSystemError(let operation, let error):
|
||||
if let error = error {
|
||||
return NSLocalizedString("文件系统错误(\(operation)): \(error.localizedDescription)", comment: "File system error")
|
||||
}
|
||||
return NSLocalizedString("文件系统错误: \(operation)", comment: "File system error")
|
||||
case .fileExists(let path):
|
||||
return NSLocalizedString("文件已存在: \(path)", comment: "File exists")
|
||||
case .fileNotFound(let path):
|
||||
return NSLocalizedString("文件不存在: \(path)", comment: "File not found")
|
||||
case .filePermissionDenied(let path):
|
||||
return NSLocalizedString("文件访问权限被拒绝: \(path)", comment: "File permission denied")
|
||||
case .applicationInfoError(let message, let error):
|
||||
if let error = error {
|
||||
return NSLocalizedString("应用信息错误(\(message)): \(error.localizedDescription)", comment: "Application info error")
|
||||
}
|
||||
return NSLocalizedString("应用信息错误: \(message)", comment: "Application info error")
|
||||
case .unsupportedPlatform(let platform):
|
||||
return NSLocalizedString("不支持的平台: \(platform)", comment: "Unsupported platform")
|
||||
case .incompatibleVersion(let current, let required):
|
||||
return NSLocalizedString("版本不兼容: 当前版本 \(current), 需要版本 \(required)", comment: "Incompatible version")
|
||||
}
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
return "Error \(errorCode): \(errorDescription ?? "")"
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus: Equatable {
|
||||
case waiting
|
||||
case preparing(PrepareInfo)
|
||||
case downloading(DownloadInfo)
|
||||
case paused(PauseInfo)
|
||||
case completed(CompletionInfo)
|
||||
case failed(FailureInfo)
|
||||
case retrying(RetryInfo)
|
||||
|
||||
struct PrepareInfo {
|
||||
let message: String
|
||||
let timestamp: Date
|
||||
let stage: PrepareStage
|
||||
|
||||
enum PrepareStage {
|
||||
case initializing
|
||||
case creatingInstaller
|
||||
case signingApp
|
||||
case fetchingInfo
|
||||
case validatingSetup
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInfo {
|
||||
let fileName: String
|
||||
let currentPackageIndex: Int
|
||||
let totalPackages: Int
|
||||
let startTime: Date
|
||||
let estimatedTimeRemaining: TimeInterval?
|
||||
}
|
||||
|
||||
struct PauseInfo {
|
||||
let reason: PauseReason
|
||||
let timestamp: Date
|
||||
let resumable: Bool
|
||||
|
||||
enum PauseReason {
|
||||
case userRequested
|
||||
case networkIssue
|
||||
case systemSleep
|
||||
case other(String)
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletionInfo {
|
||||
let timestamp: Date
|
||||
let totalTime: TimeInterval
|
||||
let totalSize: Int64
|
||||
}
|
||||
|
||||
struct FailureInfo {
|
||||
let message: String
|
||||
let error: Error?
|
||||
let timestamp: Date
|
||||
let recoverable: Bool
|
||||
}
|
||||
|
||||
struct RetryInfo {
|
||||
let attempt: Int
|
||||
let maxAttempts: Int
|
||||
let reason: String
|
||||
let nextRetryDate: Date
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .waiting:
|
||||
return NSLocalizedString("等待中", comment: "Download status waiting")
|
||||
case .preparing(let info):
|
||||
return NSLocalizedString("准备中: \(info.message)", comment: "Download status preparing")
|
||||
case .downloading(let info):
|
||||
return String(format: NSLocalizedString("正在下载 %@ (%d/%d)", comment: "Download status downloading"),
|
||||
info.fileName, info.currentPackageIndex + 1, info.totalPackages)
|
||||
case .paused(let info):
|
||||
switch info.reason {
|
||||
case .userRequested:
|
||||
return NSLocalizedString("已暂停", comment: "Download status paused")
|
||||
case .networkIssue:
|
||||
return NSLocalizedString("网络中断", comment: "Download status network paused")
|
||||
case .systemSleep:
|
||||
return NSLocalizedString("系统休眠", comment: "Download status system sleep")
|
||||
case .other(let reason):
|
||||
return NSLocalizedString("已暂停: \(reason)", comment: "Download status paused with reason")
|
||||
}
|
||||
case .completed(let info):
|
||||
let duration = formatDuration(info.totalTime)
|
||||
return NSLocalizedString("已完成 (用时: \(duration))", comment: "Download status completed")
|
||||
case .failed(let info):
|
||||
return NSLocalizedString("失败: \(info.message)", comment: "Download status failed")
|
||||
case .retrying(let info):
|
||||
return String(format: NSLocalizedString("重试中 (%d/%d)", comment: "Download status retrying"),
|
||||
info.attempt, info.maxAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
var sortOrder: Int {
|
||||
switch self {
|
||||
case .downloading: return 0
|
||||
case .preparing: return 1
|
||||
case .waiting: return 2
|
||||
case .paused: return 3
|
||||
case .retrying: return 4
|
||||
case .failed: return 5
|
||||
case .completed: return 6
|
||||
}
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
switch self {
|
||||
case .downloading, .preparing, .retrying:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isFinished: Bool {
|
||||
switch self {
|
||||
case .completed, .failed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var canRetry: Bool {
|
||||
switch self {
|
||||
case .failed(let info):
|
||||
return info.recoverable
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var canPause: Bool {
|
||||
switch self {
|
||||
case .downloading, .preparing, .waiting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var canResume: Bool {
|
||||
switch self {
|
||||
case .paused(let info):
|
||||
return info.resumable
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus {
|
||||
static func == (lhs: DownloadStatus, rhs: DownloadStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.waiting, .waiting):
|
||||
return true
|
||||
case (.preparing(let lInfo), .preparing(let rInfo)):
|
||||
return lInfo.message == rInfo.message &&
|
||||
lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.stage == rInfo.stage
|
||||
case (.downloading(let lInfo), .downloading(let rInfo)):
|
||||
return lInfo.fileName == rInfo.fileName &&
|
||||
lInfo.currentPackageIndex == rInfo.currentPackageIndex &&
|
||||
lInfo.totalPackages == rInfo.totalPackages
|
||||
case (.paused(let lInfo), .paused(let rInfo)):
|
||||
return lInfo.reason == rInfo.reason &&
|
||||
lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.resumable == rInfo.resumable
|
||||
case (.completed(let lInfo), .completed(let rInfo)):
|
||||
return lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.totalTime == rInfo.totalTime &&
|
||||
lInfo.totalSize == rInfo.totalSize
|
||||
case (.failed(let lInfo), .failed(let rInfo)):
|
||||
return lInfo.message == rInfo.message &&
|
||||
lInfo.timestamp == rInfo.timestamp &&
|
||||
lInfo.recoverable == rInfo.recoverable
|
||||
case (.retrying(let lInfo), .retrying(let rInfo)):
|
||||
return lInfo.attempt == rInfo.attempt &&
|
||||
lInfo.maxAttempts == rInfo.maxAttempts &&
|
||||
lInfo.reason == rInfo.reason &&
|
||||
lInfo.nextRetryDate == rInfo.nextRetryDate
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PrepareInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.PrepareInfo, rhs: DownloadStatus.PrepareInfo) -> Bool {
|
||||
return lhs.message == rhs.message &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.stage == rhs.stage
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PrepareInfo.PrepareStage: Equatable {
|
||||
static func == (lhs: DownloadStatus.PrepareInfo.PrepareStage, rhs: DownloadStatus.PrepareInfo.PrepareStage) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.initializing, .initializing):
|
||||
return true
|
||||
case (.creatingInstaller, .creatingInstaller):
|
||||
return true
|
||||
case (.signingApp, .signingApp):
|
||||
return true
|
||||
case (.fetchingInfo, .fetchingInfo):
|
||||
return true
|
||||
case (.validatingSetup, .validatingSetup):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PauseInfo.PauseReason: Equatable {
|
||||
static func == (lhs: DownloadStatus.PauseInfo.PauseReason, rhs: DownloadStatus.PauseInfo.PauseReason) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.userRequested, .userRequested):
|
||||
return true
|
||||
case (.networkIssue, .networkIssue):
|
||||
return true
|
||||
case (.systemSleep, .systemSleep):
|
||||
return true
|
||||
case (.other(let lhsReason), .other(let rhsReason)):
|
||||
return lhsReason == rhsReason
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoadingState: Equatable {
|
||||
case idle
|
||||
case loading
|
||||
case failed(Error)
|
||||
case success
|
||||
|
||||
static func == (lhs: LoadingState, rhs: LoadingState) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.idle, .idle):
|
||||
return true
|
||||
case (.loading, .loading):
|
||||
return true
|
||||
case (.success, .success):
|
||||
return true
|
||||
case (.failed(let lError), .failed(let rError)):
|
||||
return lError.localizedDescription == rError.localizedDescription
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDuration(_ seconds: TimeInterval) -> String {
|
||||
if seconds < 60 {
|
||||
return String(format: "%.1f秒", seconds)
|
||||
} else if seconds < 3600 {
|
||||
let minutes = Int(seconds / 60)
|
||||
let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60))
|
||||
return "\(minutes)分\(remainingSeconds)秒"
|
||||
} else {
|
||||
let hours = Int(seconds / 3600)
|
||||
let minutes = Int((seconds.truncatingRemainder(dividingBy: 3600)) / 60)
|
||||
let remainingSeconds = Int(seconds.truncatingRemainder(dividingBy: 60))
|
||||
return "\(hours)小时\(minutes)分\(remainingSeconds)秒"
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.DownloadInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.DownloadInfo, rhs: DownloadStatus.DownloadInfo) -> Bool {
|
||||
return lhs.fileName == rhs.fileName &&
|
||||
lhs.currentPackageIndex == rhs.currentPackageIndex &&
|
||||
lhs.totalPackages == rhs.totalPackages &&
|
||||
lhs.startTime == rhs.startTime &&
|
||||
lhs.estimatedTimeRemaining == rhs.estimatedTimeRemaining
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.PauseInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.PauseInfo, rhs: DownloadStatus.PauseInfo) -> Bool {
|
||||
return lhs.reason == rhs.reason &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.resumable == rhs.resumable
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.CompletionInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.CompletionInfo, rhs: DownloadStatus.CompletionInfo) -> Bool {
|
||||
return lhs.timestamp == rhs.timestamp &&
|
||||
lhs.totalTime == rhs.totalTime &&
|
||||
lhs.totalSize == rhs.totalSize
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.FailureInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.FailureInfo, rhs: DownloadStatus.FailureInfo) -> Bool {
|
||||
return lhs.message == rhs.message &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.recoverable == rhs.recoverable
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadStatus.RetryInfo: Equatable {
|
||||
static func == (lhs: DownloadStatus.RetryInfo, rhs: DownloadStatus.RetryInfo) -> Bool {
|
||||
return lhs.attempt == rhs.attempt &&
|
||||
lhs.maxAttempts == rhs.maxAttempts &&
|
||||
lhs.reason == rhs.reason &&
|
||||
lhs.nextRetryDate == rhs.nextRetryDate
|
||||
}
|
||||
}
|
||||
238
Adobe Downloader/Commons/Extensions.swift
Normal file
@@ -0,0 +1,238 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
extension FileManager {
|
||||
func volumeAvailableCapacity(for url: URL) throws -> Int64 {
|
||||
let resourceValues = try url.resourceValues(forKeys: [.volumeAvailableCapacityKey])
|
||||
return Int64(resourceValues.volumeAvailableCapacity ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension Product.ProductVersion {
|
||||
var size: Int64 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadTask {
|
||||
var startTime: Date {
|
||||
switch status {
|
||||
case .downloading(let info):
|
||||
return info.startTime
|
||||
case .completed(let info):
|
||||
return info.timestamp.addingTimeInterval(-info.totalTime)
|
||||
case .preparing(let info):
|
||||
return info.timestamp
|
||||
case .paused(let info):
|
||||
return info.timestamp
|
||||
case .retrying(let info):
|
||||
return info.nextRetryDate.addingTimeInterval(-60)
|
||||
case .failed(let info):
|
||||
return info.timestamp
|
||||
case .waiting:
|
||||
return Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NetworkManager {
|
||||
func handleDownloadCompletion(taskId: UUID, packageIndex: Int) async {
|
||||
await MainActor.run {
|
||||
guard let taskIndex = downloadTasks.firstIndex(where: { $0.id == taskId }) else { return }
|
||||
|
||||
downloadTasks[taskIndex].packages[packageIndex].downloaded = true
|
||||
downloadTasks[taskIndex].packages[packageIndex].progress = 1.0
|
||||
downloadTasks[taskIndex].packages[packageIndex].status = .completed
|
||||
|
||||
if let nextPackageIndex = downloadTasks[taskIndex].packages.firstIndex(where: { !$0.downloaded }) {
|
||||
downloadTasks[taskIndex].status = .downloading(DownloadTask.DownloadStatus.DownloadInfo(
|
||||
fileName: downloadTasks[taskIndex].packages[nextPackageIndex].name,
|
||||
currentPackageIndex: nextPackageIndex,
|
||||
totalPackages: downloadTasks[taskIndex].packages.count,
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
))
|
||||
Task {
|
||||
await resumeDownload(taskId: taskId)
|
||||
}
|
||||
} else {
|
||||
let startTime = downloadTasks[taskIndex].startTime
|
||||
let totalTime = Date().timeIntervalSince(startTime)
|
||||
|
||||
downloadTasks[taskIndex].status = .completed(DownloadTask.DownloadStatus.CompletionInfo(
|
||||
timestamp: Date(),
|
||||
totalTime: totalTime,
|
||||
totalSize: downloadTasks[taskIndex].totalSize
|
||||
))
|
||||
downloadTasks[taskIndex].progress = 1.0
|
||||
progressObservers[taskId]?.invalidate()
|
||||
progressObservers.removeValue(forKey: taskId)
|
||||
|
||||
if activeDownloadTaskId == taskId {
|
||||
activeDownloadTaskId = nil
|
||||
}
|
||||
|
||||
updateDockBadge()
|
||||
objectWillChange.send()
|
||||
Task {
|
||||
do {
|
||||
try await downloadUtils.clearExtendedAttributes(at: downloadTasks[taskIndex].destinationURL)
|
||||
print("Successfully cleared extended attributes for \(downloadTasks[taskIndex].destinationURL.path)")
|
||||
} catch {
|
||||
print("Failed to clear extended attributes: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NetworkManager {
|
||||
func getApplicationInfo(buildGuid: String) async throws -> ApplicationInfo {
|
||||
guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
|
||||
throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
|
||||
var headers = NetworkConstants.adobeRequestHeaders
|
||||
headers["x-adobe-build-guid"] = buildGuid
|
||||
headers["Accept"] = "application/json"
|
||||
headers["Connection"] = "keep-alive"
|
||||
headers["Cookie"] = generateCookie()
|
||||
|
||||
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
guard (200...299).contains(httpResponse.statusCode) else {
|
||||
throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
|
||||
}
|
||||
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let applicationInfo: ApplicationInfo = try decoder.decode(ApplicationInfo.self, from: data)
|
||||
return applicationInfo
|
||||
} catch {
|
||||
throw NetworkError.parsingError(error, "Failed to parse application info")
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProductsData() async throws -> ([String: Product], String) {
|
||||
var components = URLComponents(string: NetworkConstants.productsXmlURL)
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "_type", value: "xml"),
|
||||
URLQueryItem(name: "channel", value: "ccm"),
|
||||
URLQueryItem(name: "channel", value: "sti"),
|
||||
URLQueryItem(name: "platform", value: "osx10-64,osx10,macarm64,macuniversal"),
|
||||
URLQueryItem(name: "productType", value: "Desktop")
|
||||
]
|
||||
|
||||
guard let url = components?.url else {
|
||||
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
guard (200...299).contains(httpResponse.statusCode) else {
|
||||
throw NetworkError.httpError(httpResponse.statusCode, nil)
|
||||
}
|
||||
|
||||
guard let xmlString = String(data: data, encoding: .utf8) else {
|
||||
throw NetworkError.invalidData("无法解码XML数据")
|
||||
}
|
||||
|
||||
let result: ([String: Product], String) = try await Task.detached(priority: .userInitiated) {
|
||||
let parseResult = try XHXMLParser.parse(
|
||||
xmlString: xmlString,
|
||||
urlVersion: 6,
|
||||
allowedPlatforms: Set(["osx10-64", "osx10", "macuniversal", "macarm64"])
|
||||
)
|
||||
return (parseResult.products, parseResult.cdn)
|
||||
}.value
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getDownloadPath(for fileName: String) async throws -> URL {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
DispatchQueue.main.async {
|
||||
let panel = NSOpenPanel()
|
||||
panel.title = "选择保存位置"
|
||||
panel.canCreateDirectories = true
|
||||
panel.canChooseDirectories = true
|
||||
panel.canChooseFiles = false
|
||||
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||
|
||||
if panel.runModal() == .OK {
|
||||
if let baseURL = panel.url {
|
||||
continuation.resume(returning: baseURL)
|
||||
} else {
|
||||
continuation.resume(throwing: NetworkError.fileSystemError("未选择保存位置", nil))
|
||||
}
|
||||
} else {
|
||||
continuation.resume(throwing: NetworkError.fileSystemError("用户取消了操作", nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func configureNetworkMonitor() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
let wasConnected = self.isConnected
|
||||
self.isConnected = path.status == .satisfied
|
||||
|
||||
if !wasConnected && self.isConnected {
|
||||
for task in self.downloadTasks where task.status.isPaused {
|
||||
if case .paused(let info) = task.status,
|
||||
info.reason == .networkIssue {
|
||||
await self.resumeDownload(taskId: task.id)
|
||||
}
|
||||
}
|
||||
} else if wasConnected && !self.isConnected {
|
||||
for task in self.downloadTasks where task.status.isActive {
|
||||
await self.downloadUtils.pauseDownloadTask(
|
||||
taskId: task.id,
|
||||
reason: DownloadTask.DownloadStatus.PauseInfo.PauseReason.networkIssue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
monitor.start(queue: DispatchQueue.global(qos: .utility))
|
||||
}
|
||||
|
||||
func generateCookie() -> String {
|
||||
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
let randomString = String((0..<26).map { _ in letters.randomElement()! })
|
||||
return "fg=\(randomString)======"
|
||||
}
|
||||
|
||||
func updateDockBadge() {
|
||||
let activeCount = downloadTasks.filter { $0.status.isActive }.count
|
||||
if activeCount > 0 {
|
||||
NSApplication.shared.dockTile.badgeLabel = "\(activeCount)"
|
||||
} else {
|
||||
NSApplication.shared.dockTile.badgeLabel = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Adobe Downloader/Commons/Statics.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
struct AppStatics {
|
||||
static let supportedLanguages: [(code: String, name: String)] = [
|
||||
("zh_CN", "简体中文"),
|
||||
("zh_TW", "繁體中文"),
|
||||
("en_US", "English (US)"),
|
||||
("en_GB", "English (UK)"),
|
||||
("ja_JP", "日本語"),
|
||||
("ko_KR", "한국어"),
|
||||
("fr_FR", "Français"),
|
||||
("de_DE", "Deutsch"),
|
||||
("es_ES", "Español"),
|
||||
("it_IT", "Italiano"),
|
||||
("ru_RU", "Русский"),
|
||||
("pt_BR", "Português (Brasil)"),
|
||||
("nl_NL", "Nederlands"),
|
||||
("pl_PL", "Polski"),
|
||||
("tr_TR", "Türkçe"),
|
||||
("sv_SE", "Svenska"),
|
||||
("da_DK", "Dansk"),
|
||||
("fi_FI", "Suomi"),
|
||||
("nb_NO", "Norsk"),
|
||||
("cs_CZ", "Čeština"),
|
||||
("hu_HU", "Magyar"),
|
||||
("ALL", "所有语言")
|
||||
]
|
||||
}
|
||||
423
Adobe Downloader/Commons/Structs.swift
Normal file
@@ -0,0 +1,423 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
struct NetworkConstants {
|
||||
static let downloadTimeout: TimeInterval = 300
|
||||
static let maxRetryAttempts = 3
|
||||
static let retryDelay: UInt64 = 3_000_000_000
|
||||
static let bufferSize = 1024 * 1024
|
||||
static let maxConcurrentDownloads = 3
|
||||
static let progressUpdateInterval: TimeInterval = 1
|
||||
|
||||
static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications"
|
||||
static let productsXmlURL = "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v6/products/all"
|
||||
|
||||
static let adobeRequestHeaders = [
|
||||
"X-Adobe-App-Id": "accc-apps-panel-desktop",
|
||||
"User-Agent": "Adobe Application Manager 2.0",
|
||||
"X-Api-Key": "CC_HD_ESD_1_0",
|
||||
"Cookie": "fg=QZ6PFIT595NDL6186O9FNYYQOQ======"
|
||||
]
|
||||
|
||||
static let downloadHeaders = [
|
||||
"User-Agent": "Creative Cloud"
|
||||
]
|
||||
|
||||
static let ADOBE_CC_MAC_ICON_PATH = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Install.app/Contents/Resources/CreativeCloudInstaller.icns"
|
||||
static let MAC_VOLUME_ICON_PATH = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/CDAudioVolumeIcon.icns"
|
||||
|
||||
// 这里好像不怎么需要这个script了
|
||||
static let INSTALL_APP_APPLE_SCRIPT = """
|
||||
const app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
ObjC.import('Cocoa')
|
||||
ObjC.import('stdio')
|
||||
ObjC.import('stdlib')
|
||||
|
||||
ObjC.registerSubclass({
|
||||
name: 'HandleDataAction',
|
||||
methods: {
|
||||
'outData:': {
|
||||
types: ['void', ['id']],
|
||||
implementation: function(sender) {
|
||||
const data = sender.object.availableData
|
||||
if (data.length !== 0) {
|
||||
const output = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding).js
|
||||
const res = parseOutput(output)
|
||||
if (res) {
|
||||
switch (res.type) {
|
||||
case 'progress':
|
||||
Progress.additionalDescription = `Progress: ${res.data}%`
|
||||
Progress.completedUnitCount = res.data
|
||||
break
|
||||
case 'exit':
|
||||
if (res.data === 0) {
|
||||
$.puts(JSON.stringify({ title: 'Installation succeeded' }))
|
||||
} else {
|
||||
$.puts(JSON.stringify({ title: `Failed with error code ${res.data}` }))
|
||||
}
|
||||
$.exit(0)
|
||||
break
|
||||
}
|
||||
}
|
||||
sender.object.waitForDataInBackgroundAndNotify
|
||||
} else {
|
||||
$.NSNotificationCenter.defaultCenter.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function parseOutput(output) {
|
||||
let matches
|
||||
|
||||
matches = output.match(/Progress: ([0-9]{1,3})%/)
|
||||
if (matches) {
|
||||
return {
|
||||
type: 'progress',
|
||||
data: parseInt(matches[1], 10)
|
||||
}
|
||||
}
|
||||
|
||||
matches = output.match(/Exit Code: ([0-9]{1,3})/)
|
||||
if (matches) {
|
||||
return {
|
||||
type: 'exit',
|
||||
data: parseInt(matches[1], 10)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function shellescape(a) {
|
||||
var ret = []
|
||||
|
||||
a.forEach(function(s) {
|
||||
if (/[^A-Za-z0-9_\\/:=-]/.test(s)) {
|
||||
s = "'"+s.replace(/'/g,"'\\''")+"'"
|
||||
s = s.replace(/^(?:'')+/g, '') // unduplicate single-quote at the beginning
|
||||
.replace(/\\'''/g, "\\'") // remove non-escaped single-quote if there are enclosed between 2 escaped
|
||||
}
|
||||
ret.push(s)
|
||||
})
|
||||
|
||||
return ret.join(' ')
|
||||
}
|
||||
|
||||
function run() {
|
||||
const appPath = app.pathTo(this).toString()
|
||||
const driverPath = appPath + '/Contents/Resources/products/driver.xml'
|
||||
const hyperDrivePath = '/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup'
|
||||
|
||||
if (!$.NSProcessInfo && parseFloat(app.doShellScript('sw_vers -productVersion')) >= 11.0) {
|
||||
app.displayAlert('GUI unavailable in Big Sur', {
|
||||
message: 'JXA is currently broken in Big Sur.\\nInstall in Terminal instead?',
|
||||
buttons: ['Cancel', 'Install in Terminal'],
|
||||
defaultButton: 'Install in Terminal',
|
||||
cancelButton: 'Cancel'
|
||||
})
|
||||
const cmd = shellescape([ 'sudo', hyperDrivePath, '--install=1', '--driverXML=' + driverPath ])
|
||||
app.displayDialog('Run this command in Terminal to install (press \\'OK\\' to copy to clipboard)', { defaultAnswer: cmd })
|
||||
app.setTheClipboardTo(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
const args = $.NSProcessInfo.processInfo.arguments
|
||||
const argv = []
|
||||
const argc = args.count
|
||||
for (var i = 0; i < argc; i++) {
|
||||
argv.push(ObjC.unwrap(args.objectAtIndex(i)))
|
||||
}
|
||||
delete args
|
||||
|
||||
const installFlag = argv.indexOf('-y') > -1
|
||||
|
||||
if (!installFlag) {
|
||||
app.displayAlert('Adobe Package Installer', {
|
||||
message: 'Start installation now?',
|
||||
buttons: ['Cancel', 'Install'],
|
||||
defaultButton: 'Install',
|
||||
cancelButton: 'Cancel'
|
||||
})
|
||||
|
||||
const output = app.doShellScript(`"${appPath}/Contents/MacOS/applet" -y`, { administratorPrivileges: true })
|
||||
const alert = JSON.parse(output)
|
||||
alert.params ? app.displayAlert(alert.title, alert.params) : app.displayAlert(alert.title)
|
||||
return
|
||||
}
|
||||
|
||||
const stdout = $.NSPipe.pipe
|
||||
const task = $.NSTask.alloc.init
|
||||
|
||||
task.executableURL = $.NSURL.alloc.initFileURLWithPath(hyperDrivePath)
|
||||
task.arguments = $(['--install=1', '--driverXML=' + driverPath])
|
||||
task.standardOutput = stdout
|
||||
|
||||
const dataAction = $.HandleDataAction.alloc.init
|
||||
$.NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(dataAction, 'outData:', $.NSFileHandleDataAvailableNotification, stdout.fileHandleForReading)
|
||||
|
||||
stdout.fileHandleForReading.waitForDataInBackgroundAndNotify
|
||||
|
||||
let err = $.NSError.alloc.initWithDomainCodeUserInfo('', 0, '')
|
||||
const ret = task.launchAndReturnError(err)
|
||||
if (!ret) {
|
||||
$.puts(JSON.stringify({
|
||||
title: 'Error',
|
||||
params: {
|
||||
message: 'Failed to launch task: ' + err.localizedDescription.UTF8String
|
||||
}
|
||||
}))
|
||||
$.exit(0)
|
||||
}
|
||||
|
||||
Progress.description = "Installing packages..."
|
||||
Progress.additionalDescription = "Preparing…"
|
||||
Progress.totalUnitCount = 100
|
||||
|
||||
task.waitUntilExit
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
struct ApplicationInfo: Codable {
|
||||
let Name: String?
|
||||
let SAPCode: String?
|
||||
let CodexVersion: String?
|
||||
let AssetGuid: String?
|
||||
let ProductVersion: String?
|
||||
let BaseVersion: String?
|
||||
let Platform: String?
|
||||
let LbsUrl: String?
|
||||
let LanguageSet: String?
|
||||
let Packages: PackagesContainer
|
||||
let SupportedLanguages: SupportedLanguages?
|
||||
let ConflictingProcesses: ConflictingProcesses?
|
||||
let AMTConfig: AMTConfig?
|
||||
let SystemRequirement: SystemRequirement?
|
||||
let version: String?
|
||||
let NglLicensingInfo: NglLicensingInfo?
|
||||
let AppLineage: String?
|
||||
let FamilyName: String?
|
||||
let BuildGuid: String?
|
||||
let selfServeBuild: Bool?
|
||||
let HDBuilderVersion: String?
|
||||
let IsSTI: Bool?
|
||||
let AppsPanelFullAppUpdateConfig: AppsPanelFullAppUpdateConfig?
|
||||
let Cdn: CdnInfo?
|
||||
let WhatsNewUrl: UrlContainer?
|
||||
let TutorialUrl: UrlContainer?
|
||||
let AppLaunch: String?
|
||||
let InstallDir: InstallDir?
|
||||
let MoreInfoUrl: UrlContainer?
|
||||
let AddRemoveInfo: AddRemoveInfo?
|
||||
let AutoUpdate: String?
|
||||
let AppsPanelPreviousVersionConfig: AppsPanelPreviousVersionConfig?
|
||||
let ProductDescription: ProductDescription?
|
||||
let IsNonCCProduct: Bool?
|
||||
let CompressionType: String?
|
||||
let MinimumSupportedClientVersion: String?
|
||||
}
|
||||
|
||||
struct PackagesContainer: Codable {
|
||||
let Package: [Package]
|
||||
|
||||
struct Package: Codable {
|
||||
let PackageType: String?
|
||||
let PackageName: String?
|
||||
let PackageVersion: String?
|
||||
let DownloadSize: Int64?
|
||||
let ExtractSize: Int64?
|
||||
let Path: String
|
||||
let Format: String?
|
||||
let ValidationURL: String?
|
||||
let packageHashKey: String?
|
||||
let DeltaPackages: [DeltaPackage]?
|
||||
let ValidationURLs: ValidationURLs?
|
||||
let Condition: String?
|
||||
let InstallSequenceNumber: Int?
|
||||
let fullPackageName: String?
|
||||
let PackageValidation: String?
|
||||
let AliasPackageName: String?
|
||||
let PackageScheme: String?
|
||||
let Features: Features?
|
||||
|
||||
var size: Int64 { DownloadSize ?? 0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct DeltaPackage: Codable {
|
||||
let SchemaVersion: String?
|
||||
let PackageName: String?
|
||||
let Path: String?
|
||||
let BasePackageVersion: String?
|
||||
let ValidationURL: String?
|
||||
let DownloadSize: Int64?
|
||||
let ExtractSize: Int64?
|
||||
let packageHashKey: String?
|
||||
}
|
||||
|
||||
struct ValidationURLs: Codable {
|
||||
let TYPE1: String?
|
||||
let TYPE2: String?
|
||||
}
|
||||
|
||||
struct Features: Codable {
|
||||
let Feature: [FeatureItem]
|
||||
|
||||
struct FeatureItem: Codable {
|
||||
let name: String?
|
||||
let value: String?
|
||||
}
|
||||
}
|
||||
|
||||
struct CdnInfo: Codable {
|
||||
let Secure: String
|
||||
let NonSecure: String
|
||||
}
|
||||
|
||||
struct UrlContainer: Codable {
|
||||
let Stage: LanguageContainer
|
||||
let Prod: LanguageContainer
|
||||
|
||||
struct LanguageContainer: Codable {
|
||||
let Language: [LanguageValue]
|
||||
}
|
||||
|
||||
struct LanguageValue: Codable {
|
||||
let value: String
|
||||
let locale: String
|
||||
}
|
||||
}
|
||||
|
||||
struct InstallDir: Codable {
|
||||
let value: String?
|
||||
let maxPath: String?
|
||||
}
|
||||
|
||||
struct AddRemoveInfo: Codable {
|
||||
let DisplayName: LanguageContainer
|
||||
let DisplayVersion: LanguageContainer?
|
||||
let URLInfoAbout: LanguageContainer?
|
||||
|
||||
struct LanguageContainer: Codable {
|
||||
let Language: [LanguageValue]
|
||||
}
|
||||
|
||||
struct LanguageValue: Codable {
|
||||
let value: String
|
||||
let locale: String
|
||||
}
|
||||
}
|
||||
|
||||
struct AppsPanelPreviousVersionConfig: Codable {
|
||||
let ListInPreviousVersion: Bool
|
||||
let BrandingName: String
|
||||
}
|
||||
|
||||
struct ProductDescription: Codable {
|
||||
let Tagline: LanguageContainer?
|
||||
let DetailedDescription: LanguageContainer?
|
||||
|
||||
struct LanguageContainer: Codable {
|
||||
let Language: [LanguageValue]
|
||||
|
||||
struct LanguageValue: Codable {
|
||||
let value: String
|
||||
let locale: String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppsPanelFullAppUpdateConfig: Codable {
|
||||
let PreviousVersionRange: VersionRange
|
||||
let ShowDialogBox: Bool
|
||||
let ImportPreferenceCheckBox: PreferenceCheckBox
|
||||
let RemovePreviousVersionCheckBox: PreferenceCheckBox
|
||||
|
||||
struct VersionRange: Codable {
|
||||
let min: String
|
||||
}
|
||||
|
||||
struct PreferenceCheckBox: Codable {
|
||||
let DefaultValue: Bool
|
||||
let Show: Bool
|
||||
let AllowToggle: Bool
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportedLanguages: Codable {
|
||||
let Language: [LanguageInfo]
|
||||
|
||||
struct LanguageInfo: Codable {
|
||||
let value: String
|
||||
let locale: String
|
||||
}
|
||||
}
|
||||
|
||||
struct ConflictingProcesses: Codable {
|
||||
let ConflictingProcess: [ConflictingProcess]
|
||||
|
||||
struct ConflictingProcess: Codable {
|
||||
let RegularExpression: String
|
||||
let ProcessDisplayName: String
|
||||
let Reason: String
|
||||
let RelativePath: String
|
||||
let headless: Bool
|
||||
let forceKillAllowed: Bool
|
||||
let adobeOwned: Bool
|
||||
}
|
||||
}
|
||||
|
||||
struct AMTConfig: Codable {
|
||||
let path: String
|
||||
let LEID: String
|
||||
let appID: String
|
||||
}
|
||||
|
||||
struct SystemRequirement: Codable {
|
||||
let OsVersion: OsVersion?
|
||||
let SupportedOsVersionRange: [OsVersionRange]?
|
||||
let ExternalUrl: ExternalUrl
|
||||
let CheckCompatibility: CheckCompatibility
|
||||
|
||||
struct OsVersion: Codable {
|
||||
let min: String
|
||||
}
|
||||
|
||||
struct OsVersionRange: Codable {
|
||||
let min: String
|
||||
}
|
||||
|
||||
struct ExternalUrl: Codable {
|
||||
let Stage: LanguageUrls
|
||||
let Prod: LanguageUrls
|
||||
|
||||
struct LanguageUrls: Codable {
|
||||
let Language: [LanguageUrl]
|
||||
|
||||
struct LanguageUrl: Codable {
|
||||
let value: String
|
||||
let locale: String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckCompatibility: Codable {
|
||||
let Content: String
|
||||
}
|
||||
}
|
||||
|
||||
struct NglLicensingInfo: Codable {
|
||||
let AppId: String
|
||||
let AppVersion: String
|
||||
let LibVersion: String
|
||||
let BuildId: String
|
||||
let ImsClientId: String
|
||||
}
|
||||
298
Adobe Downloader/ContentView.swift
Normal file
@@ -0,0 +1,298 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@State private var isRefreshing = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var showDownloadManager = false
|
||||
@State private var searchText = ""
|
||||
@State private var useDefaultLanguage = true
|
||||
@State private var useDefaultDirectory = true
|
||||
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN"
|
||||
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
|
||||
@State private var showLanguagePicker = false
|
||||
|
||||
private var filteredProducts: [Product] {
|
||||
let products = networkManager.products.values
|
||||
.filter { !$0.hidden && !$0.versions.isEmpty }
|
||||
.sorted { $0.displayName < $1.displayName }
|
||||
|
||||
if searchText.isEmpty {
|
||||
return Array(products)
|
||||
}
|
||||
|
||||
return products.filter {
|
||||
$0.displayName.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.sapCode.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 20) {
|
||||
HStack {
|
||||
Text("Adobe Downloader")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
.frame(minWidth: 200)
|
||||
HStack {
|
||||
SettingsView(
|
||||
useDefaultLanguage: $useDefaultLanguage,
|
||||
useDefaultDirectory: $useDefaultDirectory,
|
||||
onSelectLanguage: selectLanguage,
|
||||
onSelectDirectory: selectDirectory
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
HStack(spacing: 8) {
|
||||
SearchField(text: $searchText)
|
||||
.frame(width: 160)
|
||||
|
||||
Button(action: refreshData) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.imageScale(.large)
|
||||
}
|
||||
.disabled(isRefreshing)
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
Button(action: { showDownloadManager.toggle() }) {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.imageScale(.large)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.overlay(
|
||||
Group {
|
||||
if !networkManager.downloadTasks.isEmpty {
|
||||
Text("\(networkManager.downloadTasks.count)")
|
||||
.font(.caption2)
|
||||
.padding(4)
|
||||
.background(Color.blue)
|
||||
.clipShape(Circle())
|
||||
.foregroundColor(.white)
|
||||
.offset(x: 10, y: -10)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.frame(width: 220)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
|
||||
ZStack {
|
||||
Color(NSColor.windowBackgroundColor)
|
||||
.ignoresSafeArea()
|
||||
|
||||
switch networkManager.loadingState {
|
||||
case .idle, .loading:
|
||||
ProgressView("正在加载...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
case .failed(let error):
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(.red)
|
||||
|
||||
Text("加载失败")
|
||||
.font(.title2)
|
||||
.fontWeight(.medium)
|
||||
|
||||
Text(error.localizedDescription)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: 300)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Button(action: {
|
||||
networkManager.retryFetchData()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
Text("重试")
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
case .success:
|
||||
if filteredProducts.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"没有找到产品",
|
||||
systemImage: "magnifyingglass",
|
||||
description: Text("尝试使用不同的搜索关键词")
|
||||
)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVGrid(
|
||||
columns: [GridItem(.adaptive(minimum: 250))],
|
||||
spacing: 20
|
||||
) {
|
||||
ForEach(filteredProducts) { product in
|
||||
AppCardView(product: product)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showLanguagePicker) {
|
||||
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
|
||||
defaultLanguage = language
|
||||
showLanguagePicker = false
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showDownloadManager) {
|
||||
DownloadManagerView()
|
||||
.environmentObject(networkManager)
|
||||
}
|
||||
.onAppear {
|
||||
if networkManager.products.isEmpty {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshData() {
|
||||
isRefreshing = true
|
||||
errorMessage = nil
|
||||
|
||||
Task {
|
||||
await networkManager.fetchProducts()
|
||||
await MainActor.run {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectLanguage() {
|
||||
showLanguagePicker = true
|
||||
}
|
||||
|
||||
private func selectDirectory() {
|
||||
let panel = NSOpenPanel()
|
||||
panel.title = "选择默认下载目录"
|
||||
panel.canCreateDirectories = true
|
||||
panel.canChooseDirectories = true
|
||||
panel.canChooseFiles = false
|
||||
|
||||
if panel.runModal() == .OK {
|
||||
defaultDirectory = panel.url?.path ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchField: View {
|
||||
@Binding var text: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
TextField("搜索应用", text: $text)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
if !text.isEmpty {
|
||||
Button(action: { text = "" }) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let networkManager = NetworkManager()
|
||||
|
||||
let mockProducts: [String: Product] = [
|
||||
"PHSP": Product(
|
||||
id: "PHSP",
|
||||
hidden: false,
|
||||
displayName: "Photoshop",
|
||||
sapCode: "PHSP",
|
||||
versions: [
|
||||
"25.0.0": Product.ProductVersion(
|
||||
sapCode: "PHSP",
|
||||
baseVersion: "25.0.0",
|
||||
productVersion: "25.0.0",
|
||||
apPlatform: "macuniversal",
|
||||
dependencies: [],
|
||||
buildGuid: ""
|
||||
)
|
||||
],
|
||||
icons: [
|
||||
Product.ProductIcon(
|
||||
size: "192x192",
|
||||
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/25.0.0/192x192.png"
|
||||
)
|
||||
]
|
||||
),
|
||||
"ILST": Product(
|
||||
id: "ILST",
|
||||
hidden: false,
|
||||
displayName: "Illustrator",
|
||||
sapCode: "ILST",
|
||||
versions: [
|
||||
"28.0.0": Product.ProductVersion(
|
||||
sapCode: "ILST",
|
||||
baseVersion: "28.0.0",
|
||||
productVersion: "28.0.0",
|
||||
apPlatform: "macuniversal",
|
||||
dependencies: [],
|
||||
buildGuid: ""
|
||||
)
|
||||
],
|
||||
icons: [
|
||||
Product.ProductIcon(
|
||||
size: "192x192",
|
||||
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/ILST/28.0.0/192x192.png"
|
||||
)
|
||||
]
|
||||
),
|
||||
"AEFT": Product(
|
||||
id: "AEFT",
|
||||
hidden: false,
|
||||
displayName: "After Effects",
|
||||
sapCode: "AEFT",
|
||||
versions: [
|
||||
"24.0.0": Product.ProductVersion(
|
||||
sapCode: "AEFT",
|
||||
baseVersion: "24.0.0",
|
||||
productVersion: "24.0.0",
|
||||
apPlatform: "macuniversal",
|
||||
dependencies: [],
|
||||
buildGuid: ""
|
||||
)
|
||||
],
|
||||
icons: [
|
||||
Product.ProductIcon(
|
||||
size: "192x192",
|
||||
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/AEFT/24.0.0/192x192.png"
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
Task { @MainActor in
|
||||
networkManager.products = mockProducts
|
||||
networkManager.loadingState = .success
|
||||
}
|
||||
|
||||
return ContentView()
|
||||
.environmentObject(networkManager)
|
||||
.frame(width: 850, height: 700)
|
||||
}
|
||||
|
||||
39
Adobe Downloader/Info.plist
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- 允许访问网络 -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
<!-- 允许访问本地网络 -->
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
|
||||
<!-- 添加以下键值对 -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
<!-- 添加以下权限声明 -->
|
||||
<key>NSDownloadsFolderUsageDescription</key>
|
||||
<string>需要访问下载文件夹来保存Adobe安装文件</string>
|
||||
|
||||
<!-- 添加以下权限 -->
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
|
||||
<array>
|
||||
<string>/Downloads/</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
268
Adobe Downloader/Models/DownloadTask.swift
Normal file
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
class DownloadTask: Identifiable, ObservableObject, Equatable {
|
||||
let id = UUID()
|
||||
let sapCode: String
|
||||
let version: String
|
||||
let language: String
|
||||
let productName: String
|
||||
@Published var status: DownloadStatus
|
||||
@Published var progress: Double
|
||||
@Published var downloadedSize: Int64
|
||||
@Published var totalSize: Int64
|
||||
@Published var speed: Double
|
||||
@Published var currentFileName: String
|
||||
let destinationURL: URL
|
||||
var priority: Priority
|
||||
var retryCount: Int
|
||||
let createdAt: Date
|
||||
@Published var lastUpdated: Date
|
||||
@Published var lastRecordedSize: Int64
|
||||
@Published var packages: [Package]
|
||||
@Published var detailedStatus: String = ""
|
||||
|
||||
enum Priority: Int {
|
||||
case low = 0
|
||||
case normal = 1
|
||||
case high = 2
|
||||
}
|
||||
|
||||
enum DownloadStatus {
|
||||
case waiting
|
||||
case preparing(PrepareInfo)
|
||||
case downloading(DownloadInfo)
|
||||
case paused(PauseInfo)
|
||||
case completed(CompletionInfo)
|
||||
case failed(FailureInfo)
|
||||
case retrying(RetryInfo)
|
||||
|
||||
struct PrepareInfo: Equatable {
|
||||
let message: String
|
||||
let timestamp: Date
|
||||
let stage: PrepareStage
|
||||
|
||||
enum PrepareStage: Equatable {
|
||||
case initializing
|
||||
case creatingInstaller
|
||||
case signingApp
|
||||
case fetchingInfo
|
||||
case validatingSetup
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInfo: Equatable {
|
||||
let fileName: String
|
||||
let currentPackageIndex: Int
|
||||
let totalPackages: Int
|
||||
let startTime: Date
|
||||
let estimatedTimeRemaining: TimeInterval?
|
||||
}
|
||||
|
||||
struct PauseInfo: Equatable {
|
||||
let reason: PauseReason
|
||||
let timestamp: Date
|
||||
let resumable: Bool
|
||||
|
||||
enum PauseReason: Equatable {
|
||||
case userRequested
|
||||
case networkIssue
|
||||
case systemSleep
|
||||
case other(String)
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletionInfo: Equatable {
|
||||
let timestamp: Date
|
||||
let totalTime: TimeInterval
|
||||
let totalSize: Int64
|
||||
}
|
||||
|
||||
struct FailureInfo: Equatable {
|
||||
let message: String
|
||||
let error: Error?
|
||||
let timestamp: Date
|
||||
let recoverable: Bool
|
||||
|
||||
static func == (lhs: FailureInfo, rhs: FailureInfo) -> Bool {
|
||||
lhs.message == rhs.message &&
|
||||
lhs.timestamp == rhs.timestamp &&
|
||||
lhs.recoverable == rhs.recoverable
|
||||
}
|
||||
}
|
||||
|
||||
struct RetryInfo: Equatable {
|
||||
let attempt: Int
|
||||
let maxAttempts: Int
|
||||
let reason: String
|
||||
let nextRetryDate: Date
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .waiting:
|
||||
return "等待中"
|
||||
case .preparing(let info):
|
||||
return "准备中: \(info.message)"
|
||||
case .downloading(let info):
|
||||
return "下载中: \(info.fileName) (\(info.currentPackageIndex + 1)/\(info.totalPackages))"
|
||||
case .paused(let info):
|
||||
switch info.reason {
|
||||
case .userRequested: return "已暂停"
|
||||
case .networkIssue: return "网络中断"
|
||||
case .systemSleep: return "系统休眠"
|
||||
case .other(let reason): return "已暂停: \(reason)"
|
||||
}
|
||||
case .completed(let info):
|
||||
let duration = String(format: "%.1f", info.totalTime)
|
||||
return "已完成 (用时: \(duration)秒)"
|
||||
case .failed(let info):
|
||||
return "失败: \(info.message)"
|
||||
case .retrying(let info):
|
||||
return "重试中 (\(info.attempt)/\(info.maxAttempts))"
|
||||
}
|
||||
}
|
||||
|
||||
var sortOrder: Int {
|
||||
switch self {
|
||||
case .downloading: return 0
|
||||
case .preparing: return 1
|
||||
case .waiting: return 2
|
||||
case .paused: return 3
|
||||
case .retrying: return 4
|
||||
case .failed: return 5
|
||||
case .completed: return 6
|
||||
}
|
||||
}
|
||||
|
||||
var isFinished: Bool {
|
||||
switch self {
|
||||
case .completed, .failed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isPaused: Bool {
|
||||
if case .paused = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
switch self {
|
||||
case .downloading, .preparing, .retrying:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isCompleted: Bool {
|
||||
if case .completed = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isFailed: Bool {
|
||||
if case .failed = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
enum PackageStatus {
|
||||
case waiting
|
||||
case downloading
|
||||
case paused
|
||||
case completed
|
||||
case failed(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .waiting: return "等待中"
|
||||
case .downloading: return "下载中"
|
||||
case .paused: return "已暂停"
|
||||
case .completed: return "已完成"
|
||||
case .failed(let message): return "失败: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Package: Identifiable {
|
||||
let id = UUID()
|
||||
var name: String
|
||||
var Path: String
|
||||
var size: Int64
|
||||
var downloadedSize: Int64 = 0
|
||||
var progress: Double = 0
|
||||
var speed: Double = 0
|
||||
var status: PackageStatus = .waiting
|
||||
var type: String
|
||||
var downloaded: Bool = false
|
||||
var lastUpdated: Date = Date()
|
||||
var lastRecordedSize: Int64 = 0
|
||||
}
|
||||
|
||||
init(sapCode: String, version: String, language: String, productName: String,
|
||||
status: DownloadStatus = .waiting, progress: Double = 0,
|
||||
downloadedSize: Int64 = 0, totalSize: Int64 = 0, speed: Double = 0,
|
||||
currentFileName: String = "", destinationURL: URL,
|
||||
priority: Priority = .normal, retryCount: Int = 0,
|
||||
packages: [Package] = [], detailedStatus: String = "") {
|
||||
self.sapCode = sapCode
|
||||
self.version = version
|
||||
self.language = language
|
||||
self.productName = productName
|
||||
self.status = status
|
||||
self.progress = progress
|
||||
self.downloadedSize = downloadedSize
|
||||
self.totalSize = totalSize
|
||||
self.speed = speed
|
||||
self.currentFileName = currentFileName
|
||||
self.destinationURL = destinationURL
|
||||
self.priority = priority
|
||||
self.retryCount = retryCount
|
||||
self.createdAt = Date()
|
||||
self.lastUpdated = Date()
|
||||
self.lastRecordedSize = 0
|
||||
self.packages = packages
|
||||
self.detailedStatus = detailedStatus
|
||||
}
|
||||
|
||||
private func updateProgress(_ newProgress: Double) {
|
||||
objectWillChange.send()
|
||||
progress = newProgress
|
||||
}
|
||||
|
||||
private func updateSpeed(_ newSpeed: Double) {
|
||||
objectWillChange.send()
|
||||
speed = newSpeed
|
||||
}
|
||||
|
||||
static func == (lhs: DownloadTask, rhs: DownloadTask) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadTask.DownloadStatus: Equatable {
|
||||
static func == (lhs: DownloadTask.DownloadStatus, rhs: DownloadTask.DownloadStatus) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.waiting, .waiting): return true
|
||||
case (.downloading, .downloading): return true
|
||||
case (.paused, .paused): return true
|
||||
case (.completed, .completed): return true
|
||||
case (.failed(let lhsMessage), .failed(let rhsMessage)): return lhsMessage == rhsMessage
|
||||
case (.retrying(let lhsCount), .retrying(let rhsCount)): return lhsCount == rhsCount
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
612
Adobe Downloader/NetworkManager.swift
Normal file
@@ -0,0 +1,612 @@
|
||||
import Foundation
|
||||
import Network
|
||||
import Combine
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
|
||||
private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
||||
var completionHandler: (URL?, URLResponse?, Error?) -> Void
|
||||
var progressHandler: ((Int64, Int64, Int64) -> Void)?
|
||||
var destinationDirectory: URL
|
||||
var fileName: String
|
||||
|
||||
init(destinationDirectory: URL,
|
||||
fileName: String,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void,
|
||||
progressHandler: ((Int64, Int64, Int64) -> Void)? = nil) {
|
||||
self.destinationDirectory = destinationDirectory
|
||||
self.fileName = fileName
|
||||
self.completionHandler = completionHandler
|
||||
self.progressHandler = progressHandler
|
||||
super.init()
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
do {
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectory.path) {
|
||||
try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
let destinationURL = destinationDirectory.appendingPathComponent(fileName)
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
try FileManager.default.moveItem(at: location, to: destinationURL)
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? Int64 ?? 0
|
||||
completionHandler(destinationURL, downloadTask.response, nil)
|
||||
} else {
|
||||
completionHandler(nil, downloadTask.response, NetworkError.fileSystemError("文件移动后不存在", nil))
|
||||
}
|
||||
} catch {
|
||||
print("File operation error in delegate: \(error.localizedDescription)")
|
||||
completionHandler(nil, downloadTask.response, error)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let error = error else { return }
|
||||
switch (error as NSError).code {
|
||||
case NSURLErrorCancelled:
|
||||
return
|
||||
case NSURLErrorTimedOut:
|
||||
completionHandler(nil, task.response, NetworkError.downloadError("下载超时", error))
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
completionHandler(nil, task.response, NetworkError.noConnection)
|
||||
default:
|
||||
completionHandler(nil, task.response, error)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64) {
|
||||
guard totalBytesExpectedToWrite > 0 else { return }
|
||||
guard bytesWritten > 0 else { return }
|
||||
|
||||
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
}
|
||||
func cleanup() {
|
||||
completionHandler = { _, _, _ in }
|
||||
progressHandler = nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class NetworkManager: ObservableObject {
|
||||
typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64)
|
||||
@Published var isConnected = false
|
||||
@Published var products: [String: Product] = [:]
|
||||
@Published var cdn: String = ""
|
||||
@Published var loadingState: LoadingState = .idle
|
||||
@Published var downloadTasks: [DownloadTask] = []
|
||||
@Published var installationState: InstallationState = .idle
|
||||
private let cancelTracker = CancelTracker()
|
||||
internal var downloadUtils: DownloadUtils!
|
||||
internal var progressObservers: [UUID: NSKeyValueObservation] = [:]
|
||||
internal var activeDownloadTaskId: UUID?
|
||||
internal var monitor = NWPathMonitor()
|
||||
internal var isFetchingProducts = false
|
||||
private let installManager = InstallManager()
|
||||
|
||||
enum InstallationState {
|
||||
case idle
|
||||
case installing(progress: Double, status: String)
|
||||
case completed
|
||||
case failed(Error)
|
||||
}
|
||||
|
||||
init() {
|
||||
self.downloadUtils = DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
||||
setupNetworkMonitoring()
|
||||
}
|
||||
|
||||
func fetchProducts() async {
|
||||
await fetchProductsWithRetry()
|
||||
}
|
||||
|
||||
func startDownload(sapCode: String, version: String, language: String, destinationURL: URL) async throws {
|
||||
try await validateAndStartDownload(sapCode: sapCode, version: version, language: language, destinationURL: destinationURL)
|
||||
}
|
||||
|
||||
func pauseDownload(taskId: UUID) {
|
||||
Task {
|
||||
await downloadUtils.pauseDownloadTask(
|
||||
taskId: taskId,
|
||||
reason: DownloadTask.DownloadStatus.PauseInfo.PauseReason.userRequested
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func resumeDownload(taskId: UUID) async {
|
||||
await downloadUtils.resumeDownloadTask(taskId: taskId)
|
||||
}
|
||||
|
||||
func cancelDownload(taskId: UUID, removeFiles: Bool = false) {
|
||||
Task {
|
||||
await downloadUtils.cancelDownloadTask(taskId: taskId, removeFiles: removeFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func clearCompletedTasks() {
|
||||
Task {
|
||||
await clearCompletedDownloadTasks()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNetworkMonitoring() {
|
||||
configureNetworkMonitor()
|
||||
}
|
||||
|
||||
private func validateAndStartDownload(sapCode: String, version: String, language: String, destinationURL: URL) async throws {
|
||||
if downloadTasks.contains(where: { task in
|
||||
task.sapCode == sapCode &&
|
||||
task.version == version &&
|
||||
!({
|
||||
if case .failed = task.status {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}())
|
||||
}) {
|
||||
throw NetworkError.downloadError("该版本已在下载队列中", nil)
|
||||
}
|
||||
|
||||
guard let productInfo = products[sapCode]?.versions[version] else {
|
||||
throw NetworkError.invalidData("无法获取产品信息")
|
||||
}
|
||||
|
||||
let installerURL: URL
|
||||
if sapCode == "APRO" {
|
||||
let fileName = "Acrobat_DC_Web_WWMUI.dmg"
|
||||
installerURL = destinationURL.appendingPathComponent(fileName)
|
||||
} else {
|
||||
let appName = "Install \(sapCode)_\(version)-\(language)-\(productInfo.apPlatform).app"
|
||||
let baseDirectory: URL
|
||||
if destinationURL.pathExtension == "app" {
|
||||
baseDirectory = destinationURL.deletingLastPathComponent()
|
||||
} else {
|
||||
baseDirectory = destinationURL
|
||||
}
|
||||
installerURL = baseDirectory.appendingPathComponent(appName)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: installerURL.path) {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "安装程序已存在"
|
||||
alert.informativeText = "在目标位置已找到相同版本的安装程序,您想要如何处理?"
|
||||
alert.addButton(withTitle: "使用已有程序")
|
||||
alert.addButton(withTitle: "重新下载")
|
||||
alert.addButton(withTitle: "取消")
|
||||
|
||||
let response = await MainActor.run {
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
switch response {
|
||||
case .alertFirstButtonReturn:
|
||||
let task = DownloadTask(
|
||||
sapCode: sapCode,
|
||||
version: version,
|
||||
language: language,
|
||||
productName: products[sapCode]?.displayName ?? "",
|
||||
status: .completed(DownloadTask.DownloadStatus.CompletionInfo(
|
||||
timestamp: Date(),
|
||||
totalTime: 0,
|
||||
totalSize: 0
|
||||
)),
|
||||
progress: 1.0,
|
||||
downloadedSize: 0,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
currentFileName: "",
|
||||
destinationURL: installerURL,
|
||||
packages: []
|
||||
)
|
||||
downloadTasks.append(task)
|
||||
return
|
||||
|
||||
case .alertSecondButtonReturn:
|
||||
try? FileManager.default.removeItem(at: installerURL)
|
||||
default:
|
||||
throw NetworkError.downloadCancelled
|
||||
}
|
||||
}
|
||||
|
||||
let task = DownloadTask(
|
||||
sapCode: sapCode,
|
||||
version: version,
|
||||
language: language,
|
||||
productName: products[sapCode]?.displayName ?? "",
|
||||
status: .preparing(DownloadTask.DownloadStatus.PrepareInfo(
|
||||
message: "正在初始化...",
|
||||
timestamp: Date(),
|
||||
stage: .initializing
|
||||
)),
|
||||
progress: 0,
|
||||
downloadedSize: 0,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
currentFileName: "",
|
||||
destinationURL: installerURL,
|
||||
packages: []
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
downloadTasks.append(task)
|
||||
updateDockBadge()
|
||||
}
|
||||
|
||||
try await performDownload(task: task, productInfo: productInfo)
|
||||
}
|
||||
|
||||
private func performDownload(task: DownloadTask, productInfo: Product.ProductVersion) async throws {
|
||||
if task.sapCode == "APRO" {
|
||||
try await downloadUtils.downloadAPRO(task: task, productInfo: productInfo)
|
||||
return
|
||||
}
|
||||
|
||||
try downloadUtils.createInstallerApp(
|
||||
for: task.sapCode,
|
||||
version: task.version,
|
||||
language: task.language,
|
||||
at: task.destinationURL
|
||||
)
|
||||
|
||||
try await downloadUtils.signApp(at: task.destinationURL)
|
||||
|
||||
await updateTaskStatus(task.id, .preparing(DownloadTask.DownloadStatus.PrepareInfo(
|
||||
message: "正在获取 \(task.productName) 的下载信息...",
|
||||
timestamp: Date(),
|
||||
stage: .fetchingInfo
|
||||
)))
|
||||
let appInfo = try await getApplicationInfo(buildGuid: productInfo.buildGuid)
|
||||
|
||||
let packages = appInfo.Packages.Package.map { package in
|
||||
DownloadTask.Package(
|
||||
name: package.PackageName ?? "",
|
||||
Path: package.Path,
|
||||
size: package.size,
|
||||
downloadedSize: 0,
|
||||
progress: 0,
|
||||
speed: 0,
|
||||
status: .waiting,
|
||||
type: package.PackageType ?? "",
|
||||
downloaded: false,
|
||||
lastUpdated: Date(),
|
||||
lastRecordedSize: 0
|
||||
)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if let index = downloadTasks.firstIndex(where: { $0.id == task.id }) {
|
||||
downloadTasks[index].packages = packages
|
||||
downloadTasks[index].totalSize = packages.reduce(0) { $0 + $1.size }
|
||||
}
|
||||
}
|
||||
|
||||
let productDir = task.destinationURL.appendingPathComponent("Contents/Resources/products/\(task.sapCode)")
|
||||
try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
let jsonData = try encoder.encode(appInfo)
|
||||
try jsonData.write(to: productDir.appendingPathComponent("application.json"))
|
||||
|
||||
await MainActor.run {
|
||||
if let taskIndex = downloadTasks.firstIndex(where: { $0.id == task.id }) {
|
||||
downloadTasks[taskIndex].status = .downloading(DownloadTask.DownloadStatus.DownloadInfo(
|
||||
fileName: packages[0].name,
|
||||
currentPackageIndex: 0,
|
||||
totalPackages: packages.count,
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
await resumeDownload(taskId: task.id)
|
||||
|
||||
let driverXml = downloadUtils.generateDriverXML(
|
||||
sapCode: task.sapCode,
|
||||
version: task.version,
|
||||
language: task.language,
|
||||
productInfo: productInfo,
|
||||
displayName: task.productName
|
||||
)
|
||||
|
||||
let productsDir = task.destinationURL.appendingPathComponent("Contents/Resources/products")
|
||||
try driverXml.write(to: productsDir.appendingPathComponent("driver.xml"),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
}
|
||||
|
||||
private func handleDownloadError(taskId: UUID, error: Error) async {
|
||||
await MainActor.run {
|
||||
guard let index = downloadTasks.firstIndex(where: { $0.id == taskId }) else { return }
|
||||
|
||||
let (errorMessage, isRecoverable) = classifyError(error)
|
||||
|
||||
if isRecoverable && downloadTasks[index].retryCount < NetworkConstants.maxRetryAttempts {
|
||||
downloadTasks[index].retryCount += 1
|
||||
let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000))
|
||||
downloadTasks[index].status = .retrying(DownloadTask.DownloadStatus.RetryInfo(
|
||||
attempt: downloadTasks[index].retryCount,
|
||||
maxAttempts: NetworkConstants.maxRetryAttempts,
|
||||
reason: errorMessage,
|
||||
nextRetryDate: nextRetryDate
|
||||
))
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
|
||||
if await !cancelTracker.isCancelled(taskId) {
|
||||
await downloadUtils.resumeDownloadTask(taskId: taskId)
|
||||
}
|
||||
} catch {
|
||||
print("Retry cancelled for task: \(taskId)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
downloadTasks[index].status = .failed(DownloadTask.DownloadStatus.FailureInfo(
|
||||
message: errorMessage,
|
||||
error: error,
|
||||
timestamp: Date(),
|
||||
recoverable: isRecoverable
|
||||
))
|
||||
|
||||
progressObservers[taskId]?.invalidate()
|
||||
progressObservers.removeValue(forKey: taskId)
|
||||
|
||||
if let currentPackage = downloadTasks[index].packages.first(where: { !$0.downloaded }) {
|
||||
let destinationDir = downloadTasks[index].destinationURL
|
||||
.appendingPathComponent("Contents/Resources/products/\(downloadTasks[index].sapCode)")
|
||||
let fileName = currentPackage.Path.components(separatedBy: "/").last ?? ""
|
||||
let fileURL = destinationDir.appendingPathComponent(fileName)
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
updateDockBadge()
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func classifyError(_ error: Error) -> (message: String, recoverable: Bool) {
|
||||
switch error {
|
||||
case let networkError as NetworkError:
|
||||
switch networkError {
|
||||
case .noConnection:
|
||||
return ("网络连接已断开", true)
|
||||
case .timeout:
|
||||
return ("下载超时", true)
|
||||
case .serverUnreachable:
|
||||
return ("服务器无法访问", true)
|
||||
case .insufficientStorage:
|
||||
return ("存储空间不足", false)
|
||||
case .filePermissionDenied:
|
||||
return ("没有入权限", false)
|
||||
default:
|
||||
return (networkError.localizedDescription, false)
|
||||
}
|
||||
case let urlError as URLError:
|
||||
switch urlError.code {
|
||||
case .notConnectedToInternet:
|
||||
return ("网络连接已断开", true)
|
||||
case .timedOut:
|
||||
return ("连接超时", true)
|
||||
case .cancelled:
|
||||
return ("下载已取消", false)
|
||||
default:
|
||||
return (urlError.localizedDescription, true)
|
||||
}
|
||||
default:
|
||||
return (error.localizedDescription, false)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateProgress(for taskId: UUID, progress: ProgressUpdate) {
|
||||
guard let index = downloadTasks.firstIndex(where: { $0.id == taskId }) else { return }
|
||||
let task = downloadTasks[index]
|
||||
guard let packageIndex = task.packages.firstIndex(where: { !$0.downloaded }) else { return }
|
||||
let now = Date()
|
||||
let timeDiff = now.timeIntervalSince(task.packages[packageIndex].lastUpdated)
|
||||
guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
|
||||
downloadTasks[index].packages[packageIndex].downloadedSize = progress.totalWritten
|
||||
downloadTasks[index].packages[packageIndex].progress =
|
||||
clampProgress(Double(progress.totalWritten) / Double(progress.expectedToWrite))
|
||||
let byteDiff = progress.totalWritten - task.packages[packageIndex].lastRecordedSize
|
||||
if byteDiff > 0 {
|
||||
let speed = Double(byteDiff) / timeDiff
|
||||
downloadTasks[index].packages[packageIndex].speed = speed
|
||||
downloadTasks[index].speed = speed
|
||||
}
|
||||
var totalDownloaded: Int64 = 0
|
||||
for (i, package) in task.packages.enumerated() {
|
||||
if package.downloaded {
|
||||
totalDownloaded += package.size
|
||||
} else if i == packageIndex {
|
||||
totalDownloaded += min(progress.totalWritten, package.size)
|
||||
}
|
||||
}
|
||||
downloadTasks[index].downloadedSize = totalDownloaded
|
||||
downloadTasks[index].progress = clampProgress(Double(totalDownloaded) / Double(task.totalSize))
|
||||
if progress.totalWritten >= progress.expectedToWrite {
|
||||
downloadTasks[index].packages[packageIndex].downloaded = true
|
||||
downloadTasks[index].packages[packageIndex].downloadedSize = downloadTasks[index].packages[packageIndex].size
|
||||
downloadTasks[index].packages[packageIndex].progress = 1.0
|
||||
downloadTasks[index].packages[packageIndex].speed = 0
|
||||
}
|
||||
downloadTasks[index].packages[packageIndex].lastRecordedSize = progress.totalWritten
|
||||
downloadTasks[index].packages[packageIndex].lastUpdated = now
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
private func updateTaskStatus(_ taskId: UUID, _ status: DownloadTask.DownloadStatus) async {
|
||||
await MainActor.run {
|
||||
guard let index = downloadTasks.firstIndex(where: { $0.id == taskId }) else { return }
|
||||
downloadTasks[index].status = status
|
||||
switch status {
|
||||
case .completed:
|
||||
progressObservers[taskId]?.invalidate()
|
||||
progressObservers.removeValue(forKey: taskId)
|
||||
if activeDownloadTaskId == taskId {
|
||||
activeDownloadTaskId = nil
|
||||
}
|
||||
|
||||
case .failed:
|
||||
progressObservers[taskId]?.invalidate()
|
||||
progressObservers.removeValue(forKey: taskId)
|
||||
if activeDownloadTaskId == taskId {
|
||||
activeDownloadTaskId = nil
|
||||
}
|
||||
|
||||
case .downloading:
|
||||
activeDownloadTaskId = taskId
|
||||
|
||||
case .paused:
|
||||
if activeDownloadTaskId == taskId {
|
||||
activeDownloadTaskId = nil
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
updateDockBadge()
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private func clampProgress(_ value: Double) -> Double {
|
||||
min(1.0, max(0.0, value))
|
||||
}
|
||||
|
||||
func retryFetchData() {
|
||||
Task {
|
||||
isFetchingProducts = false
|
||||
loadingState = .idle
|
||||
await fetchProducts()
|
||||
}
|
||||
}
|
||||
|
||||
func getActiveTaskId() async -> UUID? {
|
||||
await MainActor.run { activeDownloadTaskId }
|
||||
}
|
||||
|
||||
func setTaskStatus(_ taskId: UUID, _ status: DownloadTask.DownloadStatus) async {
|
||||
await updateTaskStatus(taskId, status)
|
||||
}
|
||||
|
||||
func getTasks() async -> [DownloadTask] {
|
||||
await MainActor.run { downloadTasks }
|
||||
}
|
||||
|
||||
func handleError(_ taskId: UUID, _ error: Error) async {
|
||||
await handleDownloadError(taskId: taskId, error: error)
|
||||
}
|
||||
func updateDownloadProgress(for taskId: UUID, progress: ProgressUpdate) {
|
||||
updateProgress(for: taskId, progress: progress)
|
||||
}
|
||||
|
||||
var cdnUrl: String {
|
||||
get async {
|
||||
await MainActor.run { cdn }
|
||||
}
|
||||
}
|
||||
|
||||
func removeTask(taskId: UUID, removeFiles: Bool = false) {
|
||||
Task {
|
||||
if removeFiles {
|
||||
if let task = downloadTasks.first(where: { $0.id == taskId }) {
|
||||
try? FileManager.default.removeItem(at: task.destinationURL)
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
downloadTasks.removeAll { $0.id == taskId }
|
||||
updateDockBadge()
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchProductsWithRetry() async {
|
||||
guard !isFetchingProducts else { return }
|
||||
|
||||
isFetchingProducts = true
|
||||
loadingState = .loading
|
||||
|
||||
let maxRetries = 3
|
||||
var retryCount = 0
|
||||
|
||||
while retryCount < maxRetries {
|
||||
do {
|
||||
let (products, cdn) = try await fetchProductsData()
|
||||
await MainActor.run {
|
||||
self.products = products
|
||||
self.cdn = cdn
|
||||
self.loadingState = .success
|
||||
self.isFetchingProducts = false
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
retryCount += 1
|
||||
if retryCount == maxRetries {
|
||||
await MainActor.run {
|
||||
self.loadingState = .failed(error)
|
||||
self.isFetchingProducts = false
|
||||
}
|
||||
} else {
|
||||
try? await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(retryCount))) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearCompletedDownloadTasks() async {
|
||||
await MainActor.run {
|
||||
downloadTasks.removeAll { task in
|
||||
task.status.isCompleted || task.status.isFailed
|
||||
}
|
||||
updateDockBadge()
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func installProduct(at path: URL) async {
|
||||
await MainActor.run {
|
||||
installationState = .installing(progress: 0, status: "准备安装...")
|
||||
}
|
||||
|
||||
do {
|
||||
try await installManager.install(at: path) { progress, status in
|
||||
Task { @MainActor in
|
||||
if status.contains("完成") || status.contains("成功") {
|
||||
self.installationState = .completed
|
||||
} else if progress >= 1.0 {
|
||||
self.installationState = .completed
|
||||
} else {
|
||||
self.installationState = .installing(progress: progress, status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
installationState = .completed
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
installationState = .failed(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelInstallation() {
|
||||
Task {
|
||||
await installManager.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
79
Adobe Downloader/Utils/CancelTracker.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
actor CancelTracker {
|
||||
private var cancelledIds: Set<UUID> = []
|
||||
private var pausedIds: Set<UUID> = []
|
||||
private var downloadTasks: [UUID: URLSessionDownloadTask] = [:]
|
||||
private var sessions: [UUID: URLSession] = [:]
|
||||
private var resumeData: [UUID: Data] = [:]
|
||||
|
||||
func registerTask(_ id: UUID, task: URLSessionDownloadTask, session: URLSession) {
|
||||
downloadTasks[id] = task
|
||||
sessions[id] = session
|
||||
}
|
||||
|
||||
func cancel(_ id: UUID) async {
|
||||
cancelledIds.insert(id)
|
||||
pausedIds.remove(id)
|
||||
resumeData.removeValue(forKey: id)
|
||||
|
||||
if let task = downloadTasks[id] {
|
||||
await withCheckedContinuation { continuation in
|
||||
task.cancel { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
downloadTasks.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if let session = sessions[id] {
|
||||
session.invalidateAndCancel()
|
||||
sessions.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func pause(_ id: UUID) async {
|
||||
if !cancelledIds.contains(id) {
|
||||
pausedIds.insert(id)
|
||||
if let task = downloadTasks[id] {
|
||||
let data = await withCheckedContinuation { continuation in
|
||||
task.cancel(byProducingResumeData: { data in
|
||||
continuation.resume(returning: data)
|
||||
})
|
||||
}
|
||||
if let data = data {
|
||||
resumeData[id] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getResumeData(_ id: UUID) -> Data? {
|
||||
return resumeData[id]
|
||||
}
|
||||
|
||||
func resume(_ id: UUID) async {
|
||||
pausedIds.remove(id)
|
||||
}
|
||||
|
||||
func isPaused(_ id: UUID) async -> Bool {
|
||||
pausedIds.contains(id)
|
||||
}
|
||||
|
||||
func isCancelled(_ id: UUID) async -> Bool {
|
||||
cancelledIds.contains(id)
|
||||
}
|
||||
|
||||
func clearTask(_ id: UUID) {
|
||||
cancelledIds.remove(id)
|
||||
pausedIds.remove(id)
|
||||
downloadTasks.removeValue(forKey: id)
|
||||
sessions.removeValue(forKey: id)
|
||||
resumeData.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
394
Adobe Downloader/Utils/DownloadUtils.swift
Normal file
@@ -0,0 +1,394 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
import Network
|
||||
import Combine
|
||||
import AppKit
|
||||
|
||||
class DownloadUtils {
|
||||
typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64)
|
||||
|
||||
private weak var networkManager: NetworkManager?
|
||||
private let cancelTracker: CancelTracker
|
||||
|
||||
init(networkManager: NetworkManager, cancelTracker: CancelTracker) {
|
||||
self.networkManager = networkManager
|
||||
self.cancelTracker = cancelTracker
|
||||
}
|
||||
|
||||
private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
||||
var completionHandler: (URL?, URLResponse?, Error?) -> Void
|
||||
var progressHandler: ((Int64, Int64, Int64) -> Void)?
|
||||
var destinationDirectory: URL
|
||||
var fileName: String
|
||||
|
||||
init(destinationDirectory: URL,
|
||||
fileName: String,
|
||||
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void,
|
||||
progressHandler: ((Int64, Int64, Int64) -> Void)? = nil) {
|
||||
self.destinationDirectory = destinationDirectory
|
||||
self.fileName = fileName
|
||||
self.completionHandler = completionHandler
|
||||
self.progressHandler = progressHandler
|
||||
super.init()
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
do {
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectory.path) {
|
||||
try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
let destinationURL = destinationDirectory.appendingPathComponent(fileName)
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: location, to: destinationURL)
|
||||
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? Int64 ?? 0
|
||||
print("File size verification - Expected: \(downloadTask.countOfBytesExpectedToReceive), Actual: \(fileSize)")
|
||||
|
||||
completionHandler(destinationURL, downloadTask.response, nil)
|
||||
} else {
|
||||
completionHandler(nil, downloadTask.response, NetworkError.fileSystemError("文件移动后不存在", nil))
|
||||
}
|
||||
} catch {
|
||||
print("File operation error in delegate: \(error.localizedDescription)")
|
||||
completionHandler(nil, downloadTask.response, error)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let error = error else { return }
|
||||
|
||||
switch (error as NSError).code {
|
||||
case NSURLErrorCancelled:
|
||||
return
|
||||
case NSURLErrorTimedOut:
|
||||
completionHandler(nil, task.response, NetworkError.downloadError("下载超时", error))
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
completionHandler(nil, task.response, NetworkError.noConnection)
|
||||
default:
|
||||
completionHandler(nil, task.response, error)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64) {
|
||||
guard totalBytesExpectedToWrite > 0 else { return }
|
||||
guard bytesWritten > 0 else { return }
|
||||
|
||||
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
}
|
||||
}
|
||||
|
||||
func pauseDownloadTask(taskId: UUID, reason: DownloadTask.DownloadStatus.PauseInfo.PauseReason = .userRequested) async {
|
||||
await cancelTracker.pause(taskId)
|
||||
await networkManager?.setTaskStatus(taskId, .paused(DownloadTask.DownloadStatus.PauseInfo(
|
||||
reason: reason,
|
||||
timestamp: Date(),
|
||||
resumable: true
|
||||
)))
|
||||
}
|
||||
|
||||
func resumeDownloadTask(taskId: UUID) async {
|
||||
guard let networkManager = networkManager,
|
||||
let task = await networkManager.getTasks().first(where: { $0.id == taskId }) else { return }
|
||||
|
||||
if let activeId = await networkManager.getActiveTaskId(), activeId != taskId {
|
||||
await cancelTracker.cancel(activeId)
|
||||
}
|
||||
|
||||
guard let packageIndex = task.packages.firstIndex(where: { !$0.downloaded }) else {
|
||||
await networkManager.setTaskStatus(taskId, .completed(DownloadTask.DownloadStatus.CompletionInfo(
|
||||
timestamp: Date(),
|
||||
totalTime: Date().timeIntervalSince(task.startTime),
|
||||
totalSize: task.totalSize
|
||||
)))
|
||||
return
|
||||
}
|
||||
|
||||
let package = task.packages[packageIndex]
|
||||
|
||||
let delegate = DownloadDelegate(
|
||||
destinationDirectory: task.destinationURL.appendingPathComponent("Contents/Resources/products/\(task.sapCode)"),
|
||||
fileName: package.Path.components(separatedBy: "/").last ?? "",
|
||||
completionHandler: { [weak networkManager] localURL, response, error in
|
||||
guard let networkManager = networkManager else { return }
|
||||
|
||||
Task {
|
||||
if let error = error {
|
||||
await networkManager.handleError(taskId, error)
|
||||
return
|
||||
}
|
||||
|
||||
if let localURL = localURL {
|
||||
do {
|
||||
let fileSize = try FileManager.default.attributesOfItem(atPath: localURL.path)[.size] as? Int64 ?? 0
|
||||
guard fileSize >= package.size else {
|
||||
throw NetworkError.dataValidationError("文件大小不正确")
|
||||
}
|
||||
|
||||
await networkManager.handleDownloadCompletion(taskId: taskId, packageIndex: packageIndex)
|
||||
} catch {
|
||||
print("File validation error: \(error.localizedDescription)")
|
||||
await networkManager.handleError(taskId, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
|
||||
guard let networkManager = networkManager else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
networkManager.updateDownloadProgress(for: taskId, progress: (
|
||||
bytesWritten: bytesWritten,
|
||||
totalWritten: totalBytesWritten,
|
||||
expectedToWrite: totalBytesExpectedToWrite
|
||||
))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForResource = NetworkConstants.downloadTimeout
|
||||
config.timeoutIntervalForRequest = NetworkConstants.downloadTimeout
|
||||
|
||||
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
||||
var downloadTask: URLSessionDownloadTask
|
||||
|
||||
if let resumeData = await cancelTracker.getResumeData(taskId) {
|
||||
downloadTask = session.downloadTask(withResumeData: resumeData)
|
||||
} else {
|
||||
let downloadURL: String
|
||||
if task.sapCode == "APRO" {
|
||||
downloadURL = await package.Path.hasPrefix("https://") ? package.Path : networkManager.cdn + package.Path
|
||||
} else {
|
||||
downloadURL = await networkManager.cdn + package.Path
|
||||
}
|
||||
|
||||
guard let url = URL(string: downloadURL) else {
|
||||
await networkManager.handleError(taskId, NetworkError.invalidURL(downloadURL))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
|
||||
downloadTask = session.downloadTask(with: request)
|
||||
}
|
||||
|
||||
await cancelTracker.registerTask(taskId, task: downloadTask, session: session)
|
||||
|
||||
await networkManager.setTaskStatus(taskId, .downloading(DownloadTask.DownloadStatus.DownloadInfo(
|
||||
fileName: package.name,
|
||||
currentPackageIndex: packageIndex,
|
||||
totalPackages: task.packages.count,
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
)))
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
|
||||
await cancelTracker.cancel(taskId)
|
||||
|
||||
if removeFiles {
|
||||
if let task = await networkManager?.getTasks().first(where: { $0.id == taskId }) {
|
||||
try? FileManager.default.removeItem(at: task.destinationURL)
|
||||
}
|
||||
}
|
||||
|
||||
await networkManager?.setTaskStatus(taskId, .failed(DownloadTask.DownloadStatus.FailureInfo(
|
||||
message: "下载已取消",
|
||||
error: NetworkError.downloadCancelled,
|
||||
timestamp: Date(),
|
||||
recoverable: false
|
||||
)))
|
||||
}
|
||||
|
||||
func downloadAPRO(task: DownloadTask, productInfo: Product.ProductVersion) async throws {
|
||||
guard let networkManager = networkManager else { return }
|
||||
|
||||
let manifestURL = await networkManager.cdnUrl + productInfo.buildGuid
|
||||
print("Manifest URL:", manifestURL)
|
||||
guard let url = URL(string: manifestURL) else {
|
||||
throw NetworkError.invalidURL(manifestURL)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||
|
||||
let (manifestData, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
let manifestXML = try XMLDocument(data: manifestData)
|
||||
|
||||
guard let downloadPath = try manifestXML.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
|
||||
let assetSizeStr = try manifestXML.nodes(forXPath: "//asset_list/asset/asset_size").first?.stringValue,
|
||||
let assetSize = Int64(assetSizeStr) else {
|
||||
throw NetworkError.invalidData("无法从manifest中获取下载信息")
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if let index = networkManager.downloadTasks.firstIndex(where: { $0.id == task.id }) {
|
||||
networkManager.downloadTasks[index].packages = [
|
||||
DownloadTask.Package(
|
||||
name: "Acrobat_DC_Web_WWMUI.dmg",
|
||||
Path: downloadPath,
|
||||
size: assetSize,
|
||||
downloadedSize: 0,
|
||||
progress: 0,
|
||||
speed: 0,
|
||||
status: .waiting,
|
||||
type: "core",
|
||||
downloaded: false,
|
||||
lastUpdated: Date(),
|
||||
lastRecordedSize: 0
|
||||
)
|
||||
]
|
||||
networkManager.downloadTasks[index].totalSize = assetSize
|
||||
}
|
||||
}
|
||||
|
||||
await networkManager.resumeDownload(taskId: task.id)
|
||||
}
|
||||
|
||||
func signApp(at url: URL) async throws {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
|
||||
process.arguments = ["--force", "--deep", "--sign", "-", url.path]
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
}
|
||||
|
||||
func createInstallerApp(for sapCode: String, version: String, language: String, at destinationURL: URL) throws {
|
||||
let parentDirectory = destinationURL.deletingLastPathComponent()
|
||||
if !FileManager.default.fileExists(atPath: parentDirectory.path) {
|
||||
try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/osacompile")
|
||||
|
||||
let tempScriptURL = FileManager.default.temporaryDirectory.appendingPathComponent("installer.js")
|
||||
try NetworkConstants.INSTALL_APP_APPLE_SCRIPT.write(to: tempScriptURL, atomically: true, encoding: .utf8)
|
||||
|
||||
process.arguments = [
|
||||
"-l", "JavaScript",
|
||||
"-o", destinationURL.path,
|
||||
tempScriptURL.path
|
||||
]
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
throw NetworkError.fileSystemError(
|
||||
"Failed to create installer app: Exit code \(process.terminationStatus)",
|
||||
nil
|
||||
)
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(at: tempScriptURL)
|
||||
|
||||
let iconDestination = destinationURL.appendingPathComponent("Contents/Resources/applet.icns")
|
||||
if FileManager.default.fileExists(atPath: iconDestination.path) {
|
||||
try FileManager.default.removeItem(at: iconDestination)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: NetworkConstants.ADOBE_CC_MAC_ICON_PATH) {
|
||||
try FileManager.default.copyItem(
|
||||
at: URL(fileURLWithPath: NetworkConstants.ADOBE_CC_MAC_ICON_PATH),
|
||||
to: iconDestination
|
||||
)
|
||||
} else {
|
||||
try FileManager.default.copyItem(
|
||||
at: URL(fileURLWithPath: NetworkConstants.MAC_VOLUME_ICON_PATH),
|
||||
to: iconDestination
|
||||
)
|
||||
}
|
||||
|
||||
try FileManager.default.createDirectory(
|
||||
at: destinationURL.appendingPathComponent("Contents/Resources/products"),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
}
|
||||
|
||||
func generateDriverXML(sapCode: String, version: String, language: String,
|
||||
productInfo: Product.ProductVersion, displayName: String) -> String {
|
||||
let dependencies = productInfo.dependencies.map { dependency in
|
||||
"""
|
||||
<Dependency>
|
||||
<SAPCode>\(dependency.sapCode)</SAPCode>
|
||||
<BaseVersion>\(dependency.version)</BaseVersion>
|
||||
<EsdDirectory>./\(dependency.sapCode)</EsdDirectory>
|
||||
</Dependency>
|
||||
"""
|
||||
}.joined(separator: "\n")
|
||||
|
||||
return """
|
||||
<DriverInfo>
|
||||
<ProductInfo>
|
||||
<Name>Adobe \(displayName)</Name>
|
||||
<SAPCode>\(sapCode)</SAPCode>
|
||||
<CodexVersion>\(version)</CodexVersion>
|
||||
<Platform>\(productInfo.apPlatform)</Platform>
|
||||
<EsdDirectory>./\(sapCode)</EsdDirectory>
|
||||
<Dependencies>
|
||||
\(dependencies)
|
||||
</Dependencies>
|
||||
</ProductInfo>
|
||||
<RequestInfo>
|
||||
<InstallDir>/Applications</InstallDir>
|
||||
<InstallLanguage>\(language)</InstallLanguage>
|
||||
</RequestInfo>
|
||||
</DriverInfo>
|
||||
"""
|
||||
}
|
||||
|
||||
func clearExtendedAttributes(at url: URL) async throws {
|
||||
let escapedPath = url.path.replacingOccurrences(of: "'", with: "'\\''")
|
||||
let script = """
|
||||
do shell script "sudo xattr -cr '\(escapedPath)'" with administrator privileges
|
||||
"""
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||
process.arguments = ["-e", script]
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
let data = try pipe.fileHandleForReading.readToEnd() ?? Data()
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
print("xattr command output:", output)
|
||||
}
|
||||
}
|
||||
|
||||
print("Successfully cleared extended attributes for \(url.path)")
|
||||
} catch {
|
||||
print("Error executing xattr command:", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
153
Adobe Downloader/Utils/InstallManager.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
actor InstallManager {
|
||||
enum InstallError: Error {
|
||||
case setupNotFound
|
||||
case installationFailed(String)
|
||||
case cancelled
|
||||
case permissionDenied
|
||||
}
|
||||
|
||||
private var installationProcess: Process?
|
||||
private var progressHandler: ((Double, String) -> Void)?
|
||||
|
||||
func install(at appPath: URL, progressHandler: @escaping (Double, String) -> Void) async throws {
|
||||
self.progressHandler = progressHandler
|
||||
|
||||
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
||||
let driverPath = appPath.appendingPathComponent("Contents/Resources/products/driver.xml").path
|
||||
|
||||
guard FileManager.default.fileExists(atPath: setupPath) else {
|
||||
throw InstallError.setupNotFound
|
||||
}
|
||||
|
||||
let authProcess = Process()
|
||||
authProcess.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||
let authScript = """
|
||||
tell application "System Events"
|
||||
display dialog "请输入管理员密码以继续安装" default answer "" with hidden answer ¬
|
||||
buttons {"取消", "确定"} default button "确定" ¬
|
||||
with icon caution ¬
|
||||
with title "需要管理员权限"
|
||||
if button returned of result is "确定" then
|
||||
return text returned of result
|
||||
else
|
||||
return ""
|
||||
end if
|
||||
end tell
|
||||
"""
|
||||
|
||||
let authPipe = Pipe()
|
||||
authProcess.standardOutput = authPipe
|
||||
authProcess.arguments = ["-e", authScript]
|
||||
|
||||
try authProcess.run()
|
||||
authProcess.waitUntilExit()
|
||||
|
||||
guard let passwordData = try? authPipe.fileHandleForReading.readToEnd(),
|
||||
let password = String(data: passwordData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!password.isEmpty else {
|
||||
throw InstallError.permissionDenied
|
||||
}
|
||||
|
||||
let installProcess = Process()
|
||||
installProcess.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
|
||||
installProcess.arguments = ["-S", setupPath, "--install=1", "--driverXML=\(driverPath)"]
|
||||
let inputPipe = Pipe()
|
||||
let outputPipe = Pipe()
|
||||
installProcess.standardInput = inputPipe
|
||||
installProcess.standardOutput = outputPipe
|
||||
installProcess.standardError = outputPipe
|
||||
|
||||
Task {
|
||||
do {
|
||||
for try await line in outputPipe.fileHandleForReading.bytes.lines {
|
||||
// print("Install output:", line)
|
||||
if let progress = parseProgress(from: line) {
|
||||
await MainActor.run {
|
||||
progressHandler(progress.progress, progress.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error reading output:", error)
|
||||
}
|
||||
}
|
||||
|
||||
installationProcess = installProcess
|
||||
|
||||
do {
|
||||
await MainActor.run {
|
||||
progressHandler(0.0, "正在准备安装...")
|
||||
}
|
||||
|
||||
try installProcess.run()
|
||||
|
||||
try inputPipe.fileHandleForWriting.write(contentsOf: "\(password)\n".data(using: .utf8)!)
|
||||
inputPipe.fileHandleForWriting.closeFile()
|
||||
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
Task.detached {
|
||||
installProcess.waitUntilExit()
|
||||
|
||||
let terminationStatus = installProcess.terminationStatus
|
||||
if terminationStatus != 0 {
|
||||
if let errorData = try? outputPipe.fileHandleForReading.readToEnd(),
|
||||
let errorOutput = String(data: errorData, encoding: .utf8) {
|
||||
continuation.resume(throwing: InstallError.installationFailed("安装失败 (退出代码: \(terminationStatus)): \(errorOutput)"))
|
||||
} else {
|
||||
continuation.resume(throwing: InstallError.installationFailed("安装失败 (退出代码: \(terminationStatus))"))
|
||||
}
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
progressHandler(1.0, "安装完成")
|
||||
}
|
||||
} catch {
|
||||
if case InstallError.cancelled = error {
|
||||
throw error
|
||||
}
|
||||
throw InstallError.installationFailed(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
installationProcess?.terminate()
|
||||
}
|
||||
|
||||
private func parseProgress(from line: String) -> (progress: Double, status: String)? {
|
||||
if let range = line.range(of: "Exit Code: ([0-9]+)", options: .regularExpression),
|
||||
let codeStr = line[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
||||
let exitCode = Int(codeStr) {
|
||||
if exitCode == 0 {
|
||||
return (1.0, "安装完成")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if let range = line.range(of: "Progress: ([0-9]{1,3})%", options: .regularExpression),
|
||||
let progressStr = line[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
||||
let progressValue = Double(progressStr.replacingOccurrences(of: "%", with: "")) {
|
||||
return (progressValue / 100.0, "正在安装...")
|
||||
}
|
||||
|
||||
if line.contains("Installing packages") {
|
||||
return (0.0, "正在安装包...")
|
||||
} else if line.contains("Preparing") {
|
||||
return (0.0, "正在准备...")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
228
Adobe Downloader/Utils/XHXMLParser.swift
Normal file
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
struct Product: Identifiable {
|
||||
let id: String
|
||||
var hidden: Bool
|
||||
var displayName: String
|
||||
var sapCode: String
|
||||
var versions: [String: ProductVersion]
|
||||
var icons: [ProductIcon]
|
||||
|
||||
struct ProductVersion {
|
||||
var sapCode: String
|
||||
var baseVersion: String
|
||||
var productVersion: String
|
||||
var apPlatform: String
|
||||
var dependencies: [Dependency]
|
||||
var buildGuid: String
|
||||
}
|
||||
|
||||
struct Dependency {
|
||||
var sapCode: String
|
||||
var version: String
|
||||
}
|
||||
|
||||
struct ProductIcon {
|
||||
let size: String
|
||||
let url: String
|
||||
|
||||
var dimension: Int {
|
||||
let components = size.split(separator: "x")
|
||||
if components.count == 2,
|
||||
let dimension = Int(components[0]) {
|
||||
return dimension
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var isValid: Bool {
|
||||
return !sapCode.isEmpty &&
|
||||
!displayName.isEmpty &&
|
||||
!versions.isEmpty
|
||||
}
|
||||
|
||||
func getBestIcon() -> ProductIcon? {
|
||||
if let icon = icons.first(where: { $0.size == "192x192" }) {
|
||||
return icon
|
||||
}
|
||||
|
||||
return icons.max(by: { $0.dimension < $1.dimension })
|
||||
}
|
||||
}
|
||||
|
||||
struct ParseResult {
|
||||
var products: [String: Product]
|
||||
var cdn: String
|
||||
}
|
||||
|
||||
class XHXMLParser {
|
||||
|
||||
static func parseProductsXML(xmlData: Data, urlVersion: Int, allowedPlatforms: Set<String>) throws -> ParseResult {
|
||||
let xml = try XMLDocument(data: xmlData)
|
||||
|
||||
let prefix = urlVersion == 6 ? "channels/" : ""
|
||||
|
||||
guard let cdn = try xml.nodes(forXPath: "//" + prefix + "channel/cdn/secure").first?.stringValue else {
|
||||
throw ParserError.missingCDN
|
||||
}
|
||||
|
||||
var products: [String: Product] = [:]
|
||||
|
||||
let productNodes = try xml.nodes(forXPath: "//" + prefix + "channel/products/product")
|
||||
|
||||
let parentMap = createParentMap(xml.rootElement())
|
||||
|
||||
for productNode in productNodes {
|
||||
guard let element = productNode as? XMLElement else { continue }
|
||||
|
||||
let sap = element.attribute(forName: "id")?.stringValue ?? ""
|
||||
let parentElement = parentMap[parentMap[element] ?? element]
|
||||
let hidden = (parentElement as? XMLElement)?.attribute(forName: "name")?.stringValue != "ccm"
|
||||
let displayName = try element.nodes(forXPath: "displayName").first?.stringValue ?? ""
|
||||
let productVersion = element.attribute(forName: "version")?.stringValue ?? ""
|
||||
|
||||
if products[sap] == nil {
|
||||
let productIcons = try element.nodes(forXPath: "productIcons/icon").compactMap { iconNode -> Product.ProductIcon? in
|
||||
guard let iconElement = iconNode as? XMLElement,
|
||||
let size = iconElement.attribute(forName: "size")?.stringValue,
|
||||
let url = iconElement.stringValue
|
||||
else { return nil }
|
||||
return Product.ProductIcon(size: size, url: url)
|
||||
}
|
||||
|
||||
products[sap] = Product(
|
||||
id: sap,
|
||||
hidden: hidden,
|
||||
displayName: displayName,
|
||||
sapCode: sap,
|
||||
versions: [:],
|
||||
icons: productIcons
|
||||
)
|
||||
}
|
||||
|
||||
let platforms = try element.nodes(forXPath: "platforms/platform")
|
||||
for platformNode in platforms {
|
||||
guard let platform = platformNode as? XMLElement else { continue }
|
||||
|
||||
let appPlatform = platform.attribute(forName: "id")?.stringValue ?? ""
|
||||
|
||||
guard let languageSet = try platform.nodes(forXPath: "languageSet").first as? XMLElement else { continue }
|
||||
|
||||
let baseVersion = languageSet.attribute(forName: "baseVersion")?.stringValue ?? ""
|
||||
var buildGuid = languageSet.attribute(forName: "buildGuid")?.stringValue ?? ""
|
||||
let currentProductVersion = productVersion
|
||||
|
||||
if let existingVersion = products[sap]?.versions[productVersion],
|
||||
allowedPlatforms.contains(existingVersion.apPlatform) {
|
||||
continue
|
||||
}
|
||||
|
||||
if sap == "APRO" {
|
||||
let baseVersion = productVersion
|
||||
var currentProductVersion = productVersion
|
||||
|
||||
if urlVersion == 4 || urlVersion == 5 {
|
||||
if let appVersion = try languageSet.nodes(forXPath: "nglLicensingInfo/appVersion").first?.stringValue {
|
||||
currentProductVersion = appVersion
|
||||
}
|
||||
} else if urlVersion == 6 {
|
||||
currentProductVersion = productVersion
|
||||
|
||||
let builds = try xml.nodes(forXPath: "//builds/build")
|
||||
for build in builds {
|
||||
guard let buildElement = build as? XMLElement,
|
||||
buildElement.attribute(forName: "id")?.stringValue == sap,
|
||||
buildElement.attribute(forName: "version")?.stringValue == baseVersion else {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
buildGuid = try languageSet.nodes(forXPath: "urls/manifestURL").first?.stringValue ?? buildGuid
|
||||
|
||||
if !buildGuid.isEmpty && allowedPlatforms.contains(appPlatform) {
|
||||
let version = Product.ProductVersion(
|
||||
sapCode: sap,
|
||||
baseVersion: baseVersion,
|
||||
productVersion: currentProductVersion,
|
||||
apPlatform: appPlatform,
|
||||
dependencies: [],
|
||||
buildGuid: buildGuid
|
||||
)
|
||||
|
||||
products[sap]?.versions[currentProductVersion] = version
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
let dependencies = try languageSet.nodes(forXPath: "dependencies/dependency").compactMap { node -> Product.Dependency? in
|
||||
guard let element = node as? XMLElement,
|
||||
let sapCode = try element.nodes(forXPath: "sapCode").first?.stringValue,
|
||||
let version = try element.nodes(forXPath: "baseVersion").first?.stringValue
|
||||
else { return nil }
|
||||
return Product.Dependency(sapCode: sapCode, version: version)
|
||||
}
|
||||
|
||||
if !buildGuid.isEmpty && allowedPlatforms.contains(appPlatform) {
|
||||
let version = Product.ProductVersion(
|
||||
sapCode: sap,
|
||||
baseVersion: baseVersion,
|
||||
productVersion: currentProductVersion,
|
||||
apPlatform: appPlatform,
|
||||
dependencies: dependencies,
|
||||
buildGuid: buildGuid
|
||||
)
|
||||
|
||||
products[sap]?.versions[currentProductVersion] = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let validProducts = products.filter { product in
|
||||
!product.value.hidden &&
|
||||
product.value.isValid &&
|
||||
!product.value.versions.isEmpty
|
||||
}
|
||||
|
||||
return ParseResult(products: validProducts, cdn: cdn)
|
||||
}
|
||||
|
||||
private static func createParentMap(_ root: XMLNode?) -> [XMLNode: XMLNode] {
|
||||
var parentMap: [XMLNode: XMLNode] = [:]
|
||||
|
||||
func traverse(_ node: XMLNode) {
|
||||
for child in node.children ?? [] {
|
||||
parentMap[child] = node
|
||||
traverse(child)
|
||||
}
|
||||
}
|
||||
|
||||
if let root = root {
|
||||
traverse(root)
|
||||
}
|
||||
|
||||
return parentMap
|
||||
}
|
||||
}
|
||||
|
||||
enum ParserError: Error {
|
||||
case missingCDN
|
||||
case invalidXML
|
||||
case missingRequired
|
||||
}
|
||||
|
||||
extension XHXMLParser {
|
||||
static func parse(xmlString: String, urlVersion: Int, allowedPlatforms: Set<String>) throws -> ParseResult {
|
||||
guard let data = xmlString.data(using: .utf8) else {
|
||||
throw ParserError.invalidXML
|
||||
}
|
||||
return try parseProductsXML(xmlData: data, urlVersion: urlVersion, allowedPlatforms: allowedPlatforms)
|
||||
}
|
||||
}
|
||||
89
Adobe Downloader/Views/AboutView.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
GeneralSettingsView()
|
||||
.tabItem {
|
||||
Label("General", systemImage: "gear")
|
||||
}
|
||||
|
||||
AboutAppView()
|
||||
.tabItem {
|
||||
Label("About", systemImage: "info.circle")
|
||||
}
|
||||
}
|
||||
.frame(width: 375, height: 150)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct GeneralSettingsView: View {
|
||||
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN"
|
||||
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
|
||||
@AppStorage("useDefaultLanguage") private var useDefaultLanguage: Bool = true
|
||||
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
GroupBox(label: Text("下载设置")) {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle("使用默认语言", isOn: $useDefaultLanguage)
|
||||
if useDefaultLanguage {
|
||||
Text("当前语言:\(getLanguageName(code: defaultLanguage))")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Toggle("使用默认目录", isOn: $useDefaultDirectory)
|
||||
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
||||
Text("当前目录:\(defaultDirectory)")
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func getLanguageName(code: String) -> String {
|
||||
AppStatics.supportedLanguages.first { $0.code == code }?.name ?? code
|
||||
}
|
||||
}
|
||||
|
||||
struct AboutAppView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 96, height: 96)
|
||||
|
||||
Text("Welcome to Adobe Downloader")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
|
||||
Text("By X1a0He. ❤️ Love from China. ❤️")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Released under GPLv3.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AboutView()
|
||||
}
|
||||
229
Adobe Downloader/Views/AppCardView.swift
Normal file
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class IconCache {
|
||||
static let shared = IconCache()
|
||||
private var cache: [String: NSImage] = [:]
|
||||
private let queue = DispatchQueue(label: "com.adobe.downloader.iconcache")
|
||||
|
||||
func getIcon(for url: String) -> NSImage? {
|
||||
queue.sync {
|
||||
return cache[url]
|
||||
}
|
||||
}
|
||||
|
||||
func setIcon(_ image: NSImage, for url: String) {
|
||||
queue.sync {
|
||||
self.cache[url] = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppCardView: View {
|
||||
let product: Product
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
|
||||
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true
|
||||
@State private var showError: Bool = false
|
||||
@State private var errorMessage: String = ""
|
||||
@State private var showVersionPicker = false
|
||||
@State private var selectedVersion: String = ""
|
||||
@State private var iconImage: NSImage? = nil
|
||||
|
||||
private var isDownloading: Bool {
|
||||
networkManager.downloadTasks.contains { task in
|
||||
if task.sapCode == product.sapCode {
|
||||
if case .downloading = task.status {
|
||||
return true
|
||||
}
|
||||
if case .preparing = task.status {
|
||||
return true
|
||||
}
|
||||
if case .waiting = task.status {
|
||||
return true
|
||||
}
|
||||
if case .retrying = task.status {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Group {
|
||||
if let iconImage = iconImage {
|
||||
Image(nsImage: iconImage)
|
||||
.resizable()
|
||||
.interpolation(.high)
|
||||
.scaledToFit()
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 64, height: 64)
|
||||
.onAppear {
|
||||
loadIcon()
|
||||
}
|
||||
|
||||
Text(product.displayName)
|
||||
.font(.system(size: 16))
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("可用版本: \(product.versions.count)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(height: 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: { showVersionPicker = true }) {
|
||||
Label(isDownloading ? "下载中" : "下载",
|
||||
systemImage: isDownloading ? "hourglass.circle.fill" : "arrow.down.circle")
|
||||
.font(.system(size: 14))
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.frame(height: 32)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(isDownloading ? .gray : .blue)
|
||||
.disabled(isDownloading)
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 250, height: 200)
|
||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.black.opacity(0.05)))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.1), lineWidth: 2)
|
||||
)
|
||||
.sheet(isPresented: $showVersionPicker) {
|
||||
VersionPickerView(product: product) { version in
|
||||
selectedVersion = version
|
||||
startDownload(version)
|
||||
}
|
||||
}
|
||||
.alert("下载错误", isPresented: $showError) {
|
||||
Button("确定", role: .cancel) { }
|
||||
Button("重试") {
|
||||
if !selectedVersion.isEmpty {
|
||||
startDownload(selectedVersion)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadIcon() {
|
||||
guard let bestIcon = product.getBestIcon(),
|
||||
let iconURL = URL(string: bestIcon.url) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) {
|
||||
self.iconImage = cachedImage
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
var request = URLRequest(url: iconURL)
|
||||
request.timeoutInterval = 10
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode),
|
||||
let image = NSImage(data: data) else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
IconCache.shared.setIcon(image, for: bestIcon.url)
|
||||
|
||||
await MainActor.run {
|
||||
self.iconImage = image
|
||||
}
|
||||
} catch {
|
||||
if let localImage = NSImage(named: product.displayName) {
|
||||
await MainActor.run {
|
||||
self.iconImage = localImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startDownload(_ version: String) {
|
||||
Task {
|
||||
do {
|
||||
let destinationURL: URL
|
||||
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
||||
destinationURL = URL(fileURLWithPath: defaultDirectory)
|
||||
.appendingPathComponent("Install \(product.displayName)_\(version)-zh_CN.app")
|
||||
} else {
|
||||
let panel = NSOpenPanel()
|
||||
panel.title = "选择保存位置"
|
||||
panel.canCreateDirectories = true
|
||||
panel.canChooseDirectories = true
|
||||
panel.canChooseFiles = false
|
||||
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||
|
||||
guard await MainActor.run(body: { panel.runModal() == .OK }),
|
||||
let selectedURL = panel.url else {
|
||||
return
|
||||
}
|
||||
destinationURL = selectedURL
|
||||
.appendingPathComponent("Install \(product.displayName)_\(version)-zh_CN.app")
|
||||
}
|
||||
try await networkManager.startDownload(
|
||||
sapCode: product.sapCode,
|
||||
version: version,
|
||||
language: "zh_CN",
|
||||
destinationURL: destinationURL
|
||||
)
|
||||
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
errorMessage = error.localizedDescription
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AppCardView(product: Product(
|
||||
id: "PHSP",
|
||||
hidden: false,
|
||||
displayName: "Photoshop",
|
||||
sapCode: "PHSP",
|
||||
versions: [
|
||||
"25.0.0": Product.ProductVersion(
|
||||
sapCode: "PHSP",
|
||||
baseVersion: "25.0.0",
|
||||
productVersion: "25.0.0",
|
||||
apPlatform: "macuniversal",
|
||||
dependencies: [],
|
||||
buildGuid: ""
|
||||
)
|
||||
],
|
||||
icons: [
|
||||
Product.ProductIcon(
|
||||
size: "192x192",
|
||||
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/25.0.0/192x192.png"
|
||||
)
|
||||
]
|
||||
))
|
||||
.environmentObject(NetworkManager())
|
||||
}
|
||||
124
Adobe Downloader/Views/DownloadManagerView.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadManagerView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var sortOrder: SortOrder = .addTime
|
||||
|
||||
enum SortOrder {
|
||||
case addTime
|
||||
case name
|
||||
case status
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .addTime: return "按添加时间"
|
||||
case .name: return "按名称"
|
||||
case .status: return "按状态"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeTask(_ task: DownloadTask) {
|
||||
networkManager.removeTask(taskId: task.id)
|
||||
}
|
||||
|
||||
private func sortTasks(_ tasks: [DownloadTask]) -> [DownloadTask] {
|
||||
switch sortOrder {
|
||||
case .addTime:
|
||||
return tasks
|
||||
case .name:
|
||||
return tasks.sorted { $0.productName < $1.productName }
|
||||
case .status:
|
||||
return tasks.sorted { $0.status.sortOrder < $1.status.sortOrder }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
Text("下载管理")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Menu {
|
||||
ForEach([SortOrder.addTime, .name, .status], id: \.self) { order in
|
||||
Button(action: {
|
||||
sortOrder = order
|
||||
}) {
|
||||
HStack {
|
||||
Text(order.description)
|
||||
if sortOrder == order {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
Text(sortOrder.description)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
Button("全部暂停", action: {})
|
||||
Button("全部继续", action: {})
|
||||
Button("清理已完成", action: {
|
||||
Task {
|
||||
networkManager.clearCompletedTasks()
|
||||
}
|
||||
})
|
||||
|
||||
Button("关闭") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(sortTasks(networkManager.downloadTasks)) { task in
|
||||
DownloadProgressView(
|
||||
task: task,
|
||||
onCancel: {
|
||||
networkManager.cancelDownload(taskId: task.id)
|
||||
},
|
||||
onPause: {
|
||||
networkManager.pauseDownload(taskId: task.id)
|
||||
},
|
||||
onResume: {
|
||||
Task {
|
||||
await networkManager.resumeDownload(taskId: task.id)
|
||||
}
|
||||
},
|
||||
onRetry: {
|
||||
Task {
|
||||
await networkManager.resumeDownload(taskId: task.id)
|
||||
}
|
||||
},
|
||||
onRemove: {
|
||||
removeTask(task)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
.frame(width: 600, height: 400)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadManagerView.SortOrder: Hashable {}
|
||||
|
||||
#Preview {
|
||||
DownloadManagerView()
|
||||
.environmentObject(NetworkManager())
|
||||
}
|
||||
466
Adobe Downloader/Views/DownloadProgressView.swift
Normal file
@@ -0,0 +1,466 @@
|
||||
//
|
||||
// Adobe-Downloader
|
||||
//
|
||||
// Created by X1a0He on 2024/10/30.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadProgressView: View {
|
||||
@EnvironmentObject private var networkManager: NetworkManager
|
||||
let task: DownloadTask
|
||||
let onCancel: () -> Void
|
||||
let onPause: () -> Void
|
||||
let onResume: () -> Void
|
||||
let onRetry: () -> Void
|
||||
let onRemove: () -> Void
|
||||
|
||||
@State private var showInstallPrompt = false
|
||||
@State private var isInstalling = false
|
||||
@State private var isPackageListExpanded: Bool = false
|
||||
|
||||
private var statusLabel: some View {
|
||||
Text(task.status.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(statusColor)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 6)
|
||||
.background(statusBackgroundColor)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch task.status {
|
||||
case .downloading:
|
||||
return .white
|
||||
case .preparing:
|
||||
return .white
|
||||
case .completed:
|
||||
return .white
|
||||
case .failed:
|
||||
return .white
|
||||
case .paused:
|
||||
return .white
|
||||
case .waiting:
|
||||
return .white
|
||||
case .retrying:
|
||||
return .white
|
||||
}
|
||||
}
|
||||
|
||||
private var statusBackgroundColor: Color {
|
||||
switch task.status {
|
||||
case .downloading:
|
||||
return Color.blue
|
||||
case .preparing:
|
||||
return Color.purple.opacity(0.8)
|
||||
case .completed:
|
||||
return Color.green.opacity(0.8)
|
||||
case .failed:
|
||||
return Color.red.opacity(0.8)
|
||||
case .paused:
|
||||
return Color.orange.opacity(0.8)
|
||||
case .waiting:
|
||||
return Color.gray.opacity(0.8)
|
||||
case .retrying:
|
||||
return Color.yellow.opacity(0.8)
|
||||
}
|
||||
}
|
||||
|
||||
private var actionButtons: some View {
|
||||
HStack(spacing: 8) {
|
||||
switch task.status {
|
||||
case .downloading, .preparing, .waiting:
|
||||
Button(action: onPause) {
|
||||
Label("暂停", systemImage: "pause.fill")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.orange)
|
||||
|
||||
Button(action: onCancel) {
|
||||
Label("取消", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
|
||||
case .paused:
|
||||
Button(action: onResume) {
|
||||
Label("继续", systemImage: "play.fill")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
|
||||
Button(action: onCancel) {
|
||||
Label("取消", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
|
||||
case .failed(let info):
|
||||
if info.recoverable {
|
||||
Button(action: onRetry) {
|
||||
Label("重试", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
}
|
||||
|
||||
Button(action: onRemove) {
|
||||
Label("移除", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
|
||||
case .completed:
|
||||
HStack(spacing: 8) {
|
||||
Button(action: { showInstallPrompt = true }) {
|
||||
Label("安装", systemImage: "square.and.arrow.down.on.square")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.green)
|
||||
|
||||
Button(action: {
|
||||
networkManager.removeTask(taskId: task.id, removeFiles: true)
|
||||
}) {
|
||||
Label("删除", systemImage: "trash")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
}
|
||||
|
||||
case .retrying:
|
||||
Button(action: onCancel) {
|
||||
Label("取消", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
.controlSize(.small)
|
||||
.sheet(isPresented: $showInstallPrompt) {
|
||||
VStack(spacing: 20) {
|
||||
Text("是否要安装 \(task.productName)?")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button("取消") {
|
||||
showInstallPrompt = false
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button("安装") {
|
||||
showInstallPrompt = false
|
||||
isInstalling = true
|
||||
Task {
|
||||
await networkManager.installProduct(at: task.destinationURL)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
}
|
||||
.sheet(isPresented: $isInstalling) {
|
||||
Group {
|
||||
if case .installing(let progress, let status) = networkManager.installationState {
|
||||
InstallProgressView(
|
||||
productName: task.productName,
|
||||
progress: progress,
|
||||
status: status
|
||||
) {
|
||||
networkManager.cancelInstallation()
|
||||
isInstalling = false
|
||||
}
|
||||
} else if case .completed = networkManager.installationState {
|
||||
InstallProgressView(
|
||||
productName: task.productName,
|
||||
progress: 1.0,
|
||||
status: "安装完成"
|
||||
) {
|
||||
isInstalling = false
|
||||
}
|
||||
} else {
|
||||
InstallProgressView(
|
||||
productName: task.productName,
|
||||
progress: 0,
|
||||
status: "准备安装..."
|
||||
) {
|
||||
networkManager.cancelInstallation()
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 400, minHeight: 200)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
private func formatFileSize(_ size: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: size)
|
||||
}
|
||||
|
||||
private func formatSpeed(_ bytesPerSecond: Double) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
formatter.includesUnit = true
|
||||
formatter.isAdaptive = true
|
||||
return formatter.string(fromByteCount: Int64(bytesPerSecond)) + "/s"
|
||||
}
|
||||
|
||||
private func openInFinder(_ url: URL) {
|
||||
NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: url.deletingLastPathComponent().path)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(task.productName)
|
||||
.font(.headline)
|
||||
Text(task.destinationURL.path)
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.onTapGesture {
|
||||
openInFinder(task.destinationURL)
|
||||
}
|
||||
.onHover { hovering in
|
||||
if hovering {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(task.version)
|
||||
.foregroundColor(.secondary)
|
||||
statusLabel
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 6)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Text(formatFileSize(task.downloadedSize))
|
||||
Text("/")
|
||||
Text(formatFileSize(task.totalSize))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text("\(Int(task.progress * 100))%")
|
||||
.foregroundColor(.primary)
|
||||
|
||||
if task.speed > 0 {
|
||||
Text(formatSpeed(task.speed))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
ProgressView(value: task.progress)
|
||||
.progressViewStyle(.linear)
|
||||
}
|
||||
|
||||
if task.packages.count > 0 {
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isPackageListExpanded.toggle()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: isPackageListExpanded ? "chevron.down" : "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
Text("包列表 (\(task.packages.count))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if isPackageListExpanded {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ForEach(task.packages.indices, id: \.self) { index in
|
||||
let package = task.packages[index]
|
||||
PackageProgressView(package: package, index: index, total: task.packages.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 120)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
actionButtons
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: Color.primary.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct PackageProgressView: View {
|
||||
let package: DownloadTask.Package
|
||||
let index: Int
|
||||
let total: Int
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
Text("\(package.name)")
|
||||
.font(.caption)
|
||||
.foregroundColor(package.downloaded ? .secondary : .primary)
|
||||
|
||||
Text("(\(index + 1)/\(total))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
if package.downloaded {
|
||||
Text("已完成")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
} else if package.downloadedSize > 0 {
|
||||
HStack(spacing: 4) {
|
||||
Text("\(Int(package.progress * 100))%")
|
||||
Text(formatSpeed(package.speed))
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
} else {
|
||||
Text("等待中")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if !package.downloaded && package.downloadedSize > 0 {
|
||||
ProgressView(value: package.progress)
|
||||
.scaleEffect(x: 1, y: 0.5, anchor: .center)
|
||||
|
||||
HStack {
|
||||
Text(formatFileSize(package.downloadedSize))
|
||||
Text("/")
|
||||
Text(formatFileSize(package.size))
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private func formatFileSize(_ size: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: size)
|
||||
}
|
||||
|
||||
private func formatSpeed(_ bytesPerSecond: Double) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
formatter.includesUnit = true
|
||||
formatter.isAdaptive = true
|
||||
return formatter.string(fromByteCount: Int64(bytesPerSecond)) + "/s"
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DownloadProgressView(
|
||||
task: DownloadTask(
|
||||
sapCode: "PHSP",
|
||||
version: "24.0",
|
||||
language: "zh_CN",
|
||||
productName: "Adobe Photoshop",
|
||||
status: .downloading(DownloadTask.DownloadStatus.DownloadInfo(
|
||||
fileName: "PhotoshopSupport.dmg",
|
||||
currentPackageIndex: 1,
|
||||
totalPackages: 4,
|
||||
startTime: Date(),
|
||||
estimatedTimeRemaining: nil
|
||||
)),
|
||||
progress: 0.45,
|
||||
downloadedSize: 1024 * 1024 * 450,
|
||||
totalSize: 1024 * 1024 * 1000,
|
||||
speed: 1024 * 1024 * 2,
|
||||
currentFileName: "PhotoshopSupport.dmg",
|
||||
destinationURL: URL(fileURLWithPath: "/tmp"),
|
||||
packages: [
|
||||
.init(
|
||||
name: "PhotoshopCore.dmg",
|
||||
Path: "/path/to/core",
|
||||
size: 1024 * 1024 * 300,
|
||||
downloadedSize: 1024 * 1024 * 300,
|
||||
progress: 1.0,
|
||||
speed: 0,
|
||||
status: .completed,
|
||||
type: "core",
|
||||
downloaded: true,
|
||||
lastUpdated: Date(),
|
||||
lastRecordedSize: 1024 * 1024 * 300
|
||||
),
|
||||
.init(
|
||||
name: "PhotoshopSupport.dmg",
|
||||
Path: "/path/to/support",
|
||||
size: 1024 * 1024 * 400,
|
||||
downloadedSize: 1024 * 1024 * 150,
|
||||
progress: 0.375,
|
||||
speed: 1024 * 1024 * 2,
|
||||
status: .downloading,
|
||||
type: "support",
|
||||
downloaded: false,
|
||||
lastUpdated: Date(),
|
||||
lastRecordedSize: 1024 * 1024 * 150
|
||||
),
|
||||
.init(
|
||||
name: "PhotoshopOptional.dmg",
|
||||
Path: "/path/to/optional",
|
||||
size: 1024 * 1024 * 200,
|
||||
downloadedSize: 0,
|
||||
progress: 0,
|
||||
speed: 0,
|
||||
status: .waiting,
|
||||
type: "optional",
|
||||
downloaded: false,
|
||||
lastUpdated: Date(),
|
||||
lastRecordedSize: 0
|
||||
)
|
||||
]
|
||||
),
|
||||
onCancel: {},
|
||||
onPause: {},
|
||||
onResume: {},
|
||||
onRetry: {},
|
||||
onRemove: {}
|
||||
)
|
||||
.environmentObject(NetworkManager())
|
||||
.padding()
|
||||
.frame(width: 500)
|
||||
}
|
||||