Init: Initially complete the basic download and installation functions

This commit is contained in:
X1a0He
2024-10-31 22:35:22 +08:00
commit 319a3d744b
106 changed files with 6652 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.idea/
Adobe-Downloader.xcodeproj
Adobe-Downloader.xcodeproj/*.workspace
.DS_Store
*.DS_Store

View 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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "05600D7B-4F3A-44C5-8A39-5E4971936E92"
type = "1"
version = "2.0">
</Bucket>

View 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>SchemeUserState</key>
<dict>
<key>Adobe-Downloader.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View 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>

View 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()
}
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View 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
}
}

View 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
}
}

View 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

View 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
}
}

View 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 &amp; 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

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View 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
}
}

View 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
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View 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
}
}

View 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
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View 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
}
}

View 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

View 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
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
}
}

View 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 &amp; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View 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
}
}

View 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
}
}

View 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 &amp; 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

View 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
}
}

View 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 &amp; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View 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
}
}

View 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
}
}

View 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 &amp; 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

View 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
}
}

View 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 &amp; 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

View 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
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View 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
}
}

View 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
}
}

View 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 &amp; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View 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
}
}

View 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
}
}

View 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 &amp; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View 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
}
}

View 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
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View 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
}
}

View 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
}
}

View 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

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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
}
}

View 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
}
}
}

View 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", "所有语言")
]
}

View 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
}

View 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)
}

View 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>

View 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
}
}
}

View 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()
}
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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)
}
}

View 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)
}
}
}

View 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
}
}

View 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)
}
}

View 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()
}

View 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())
}

View 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())
}

View 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)
}

Some files were not shown because too many files have changed in this diff Show More