tangled
alpha
login
or
join now
coreyja.com
/
backup-blue
1
fork
atom
this repo has no description
1
fork
atom
overview
issues
pulls
pipelines
Dynamic Island
Corey Alexander
6 months ago
9c7f3f77
c244eddd
+786
-1
16 changed files
expand all
collapse all
unified
split
AtProtoBackup
AtProtoBackupApp.swift
DownloadActivityAttributes.swift
DownloadManager.swift
Info.plist
LiveActivityManager.swift
AtProtoBackup.xcodeproj
project.pbxproj
xcuserdata
coreyja.xcuserdatad
xcschemes
xcschememanagement.plist
WidgetExtension
Assets.xcassets
AccentColor.colorset
Contents.json
AppIcon.appiconset
Contents.json
Contents.json
WidgetBackground.colorset
Contents.json
DownloadActivityAttributes.swift
DownloadLiveActivityView.swift
Info.plist
WidgetExtension.entitlements
WidgetExtensionBundle.swift
+199
AtProtoBackup.xcodeproj/project.pbxproj
···
9
/* Begin PBXBuildFile section */
10
16A25DB92E5FE9060070BFFD /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 16A25DB82E5FE9060070BFFD /* ZIPFoundation */; };
11
16A25DBE2E5FED820070BFFD /* ATProtoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 16A25DBD2E5FED820070BFFD /* ATProtoKit */; };
0
0
0
12
/* End PBXBuildFile section */
13
14
/* Begin PBXContainerItemProxy section */
···
26
remoteGlobalIDString = 16A25D772E5CA4790070BFFD;
27
remoteInfo = AtProtoBackup;
28
};
0
0
0
0
0
0
0
29
/* End PBXContainerItemProxy section */
30
0
0
0
0
0
0
0
0
0
0
0
0
0
0
31
/* Begin PBXFileReference section */
32
16A25D782E5CA4790070BFFD /* AtProtoBackup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AtProtoBackup.app; sourceTree = BUILT_PRODUCTS_DIR; };
33
16A25D892E5CA47B0070BFFD /* AtProtoBackupTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AtProtoBackupTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
34
16A25D932E5CA47B0070BFFD /* AtProtoBackupUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AtProtoBackupUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0
0
0
35
/* End PBXFileReference section */
36
37
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
···
41
Info.plist,
42
);
43
target = 16A25D772E5CA4790070BFFD /* AtProtoBackup */;
0
0
0
0
0
0
0
44
};
45
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
46
···
63
path = AtProtoBackupUITests;
64
sourceTree = "<group>";
65
};
0
0
0
0
0
0
0
0
66
/* End PBXFileSystemSynchronizedRootGroup section */
67
68
/* Begin PBXFrameworksBuildPhase section */
···
89
);
90
runOnlyForDeploymentPostprocessing = 0;
91
};
0
0
0
0
0
0
0
0
0
92
/* End PBXFrameworksBuildPhase section */
93
94
/* Begin PBXGroup section */
···
98
16A25D7A2E5CA4790070BFFD /* AtProtoBackup */,
99
16A25D8C2E5CA47B0070BFFD /* AtProtoBackupTests */,
100
16A25D962E5CA47B0070BFFD /* AtProtoBackupUITests */,
0
0
101
16A25D792E5CA4790070BFFD /* Products */,
102
);
103
sourceTree = "<group>";
···
108
16A25D782E5CA4790070BFFD /* AtProtoBackup.app */,
109
16A25D892E5CA47B0070BFFD /* AtProtoBackupTests.xctest */,
110
16A25D932E5CA47B0070BFFD /* AtProtoBackupUITests.xctest */,
0
111
);
112
name = Products;
0
0
0
0
0
0
0
0
0
113
sourceTree = "<group>";
114
};
115
/* End PBXGroup section */
···
122
16A25D742E5CA4790070BFFD /* Sources */,
123
16A25D752E5CA4790070BFFD /* Frameworks */,
124
16A25D762E5CA4790070BFFD /* Resources */,
0
125
);
126
buildRules = (
127
);
128
dependencies = (
0
129
);
130
fileSystemSynchronizedGroups = (
131
16A25D7A2E5CA4790070BFFD /* AtProtoBackup */,
···
185
productReference = 16A25D932E5CA47B0070BFFD /* AtProtoBackupUITests.xctest */;
186
productType = "com.apple.product-type.bundle.ui-testing";
187
};
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
188
/* End PBXNativeTarget section */
189
190
/* Begin PBXProject section */
···
206
CreatedOnToolsVersion = 16.4;
207
TestTargetID = 16A25D772E5CA4790070BFFD;
208
};
0
0
0
209
};
210
};
211
buildConfigurationList = 16A25D732E5CA4790070BFFD /* Build configuration list for PBXProject "AtProtoBackup" */;
···
229
16A25D772E5CA4790070BFFD /* AtProtoBackup */,
230
16A25D882E5CA47B0070BFFD /* AtProtoBackupTests */,
231
16A25D922E5CA47B0070BFFD /* AtProtoBackupUITests */,
0
232
);
233
};
234
/* End PBXProject section */
···
255
);
256
runOnlyForDeploymentPostprocessing = 0;
257
};
0
0
0
0
0
0
0
258
/* End PBXResourcesBuildPhase section */
259
260
/* Begin PBXSourcesBuildPhase section */
···
279
);
280
runOnlyForDeploymentPostprocessing = 0;
281
};
0
0
0
0
0
0
0
282
/* End PBXSourcesBuildPhase section */
283
284
/* Begin PBXTargetDependency section */
···
291
isa = PBXTargetDependency;
292
target = 16A25D772E5CA4790070BFFD /* AtProtoBackup */;
293
targetProxy = 16A25D942E5CA47B0070BFFD /* PBXContainerItemProxy */;
0
0
0
0
0
294
};
295
/* End PBXTargetDependency section */
296
···
585
};
586
name = Release;
587
};
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
588
/* End XCBuildConfiguration section */
589
590
/* Begin XCConfigurationList section */
···
620
buildConfigurations = (
621
16A25DA52E5CA47B0070BFFD /* Debug */,
622
16A25DA62E5CA47B0070BFFD /* Release */,
0
0
0
0
0
0
0
0
0
623
);
624
defaultConfigurationIsVisible = 0;
625
defaultConfigurationName = Release;
···
9
/* Begin PBXBuildFile section */
10
16A25DB92E5FE9060070BFFD /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 16A25DB82E5FE9060070BFFD /* ZIPFoundation */; };
11
16A25DBE2E5FED820070BFFD /* ATProtoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 16A25DBD2E5FED820070BFFD /* ATProtoKit */; };
12
+
16A25DCE2E60978E0070BFFD /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16A25DCD2E60978E0070BFFD /* WidgetKit.framework */; };
13
+
16A25DD02E60978E0070BFFD /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16A25DCF2E60978E0070BFFD /* SwiftUI.framework */; };
14
+
16A25DDE2E6097900070BFFD /* WidgetExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 16A25DCB2E60978D0070BFFD /* WidgetExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
15
/* End PBXBuildFile section */
16
17
/* Begin PBXContainerItemProxy section */
···
29
remoteGlobalIDString = 16A25D772E5CA4790070BFFD;
30
remoteInfo = AtProtoBackup;
31
};
32
+
16A25DDC2E6097900070BFFD /* PBXContainerItemProxy */ = {
33
+
isa = PBXContainerItemProxy;
34
+
containerPortal = 16A25D702E5CA4790070BFFD /* Project object */;
35
+
proxyType = 1;
36
+
remoteGlobalIDString = 16A25DCA2E60978D0070BFFD;
37
+
remoteInfo = WidgetExtensionExtension;
38
+
};
39
/* End PBXContainerItemProxy section */
40
41
+
/* Begin PBXCopyFilesBuildPhase section */
42
+
16A25DE32E6097900070BFFD /* Embed Foundation Extensions */ = {
43
+
isa = PBXCopyFilesBuildPhase;
44
+
buildActionMask = 2147483647;
45
+
dstPath = "";
46
+
dstSubfolderSpec = 13;
47
+
files = (
48
+
16A25DDE2E6097900070BFFD /* WidgetExtensionExtension.appex in Embed Foundation Extensions */,
49
+
);
50
+
name = "Embed Foundation Extensions";
51
+
runOnlyForDeploymentPostprocessing = 0;
52
+
};
53
+
/* End PBXCopyFilesBuildPhase section */
54
+
55
/* Begin PBXFileReference section */
56
16A25D782E5CA4790070BFFD /* AtProtoBackup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AtProtoBackup.app; sourceTree = BUILT_PRODUCTS_DIR; };
57
16A25D892E5CA47B0070BFFD /* AtProtoBackupTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AtProtoBackupTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
58
16A25D932E5CA47B0070BFFD /* AtProtoBackupUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AtProtoBackupUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
59
+
16A25DCB2E60978D0070BFFD /* WidgetExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
60
+
16A25DCD2E60978E0070BFFD /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = /System/Library/Frameworks/WidgetKit.framework; sourceTree = "<absolute>"; };
61
+
16A25DCF2E60978E0070BFFD /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = /System/Library/Frameworks/SwiftUI.framework; sourceTree = "<absolute>"; };
62
/* End PBXFileReference section */
63
64
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
···
68
Info.plist,
69
);
70
target = 16A25D772E5CA4790070BFFD /* AtProtoBackup */;
71
+
};
72
+
16A25DE22E6097900070BFFD /* Exceptions for "WidgetExtension" folder in "WidgetExtensionExtension" target */ = {
73
+
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
74
+
membershipExceptions = (
75
+
Info.plist,
76
+
);
77
+
target = 16A25DCA2E60978D0070BFFD /* WidgetExtensionExtension */;
78
};
79
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
80
···
97
path = AtProtoBackupUITests;
98
sourceTree = "<group>";
99
};
100
+
16A25DD12E60978E0070BFFD /* WidgetExtension */ = {
101
+
isa = PBXFileSystemSynchronizedRootGroup;
102
+
exceptions = (
103
+
16A25DE22E6097900070BFFD /* Exceptions for "WidgetExtension" folder in "WidgetExtensionExtension" target */,
104
+
);
105
+
path = WidgetExtension;
106
+
sourceTree = "<group>";
107
+
};
108
/* End PBXFileSystemSynchronizedRootGroup section */
109
110
/* Begin PBXFrameworksBuildPhase section */
···
131
);
132
runOnlyForDeploymentPostprocessing = 0;
133
};
134
+
16A25DC82E60978D0070BFFD /* Frameworks */ = {
135
+
isa = PBXFrameworksBuildPhase;
136
+
buildActionMask = 2147483647;
137
+
files = (
138
+
16A25DD02E60978E0070BFFD /* SwiftUI.framework in Frameworks */,
139
+
16A25DCE2E60978E0070BFFD /* WidgetKit.framework in Frameworks */,
140
+
);
141
+
runOnlyForDeploymentPostprocessing = 0;
142
+
};
143
/* End PBXFrameworksBuildPhase section */
144
145
/* Begin PBXGroup section */
···
149
16A25D7A2E5CA4790070BFFD /* AtProtoBackup */,
150
16A25D8C2E5CA47B0070BFFD /* AtProtoBackupTests */,
151
16A25D962E5CA47B0070BFFD /* AtProtoBackupUITests */,
152
+
16A25DD12E60978E0070BFFD /* WidgetExtension */,
153
+
16A25DCC2E60978E0070BFFD /* Frameworks */,
154
16A25D792E5CA4790070BFFD /* Products */,
155
);
156
sourceTree = "<group>";
···
161
16A25D782E5CA4790070BFFD /* AtProtoBackup.app */,
162
16A25D892E5CA47B0070BFFD /* AtProtoBackupTests.xctest */,
163
16A25D932E5CA47B0070BFFD /* AtProtoBackupUITests.xctest */,
164
+
16A25DCB2E60978D0070BFFD /* WidgetExtensionExtension.appex */,
165
);
166
name = Products;
167
+
sourceTree = "<group>";
168
+
};
169
+
16A25DCC2E60978E0070BFFD /* Frameworks */ = {
170
+
isa = PBXGroup;
171
+
children = (
172
+
16A25DCD2E60978E0070BFFD /* WidgetKit.framework */,
173
+
16A25DCF2E60978E0070BFFD /* SwiftUI.framework */,
174
+
);
175
+
name = Frameworks;
176
sourceTree = "<group>";
177
};
178
/* End PBXGroup section */
···
185
16A25D742E5CA4790070BFFD /* Sources */,
186
16A25D752E5CA4790070BFFD /* Frameworks */,
187
16A25D762E5CA4790070BFFD /* Resources */,
188
+
16A25DE32E6097900070BFFD /* Embed Foundation Extensions */,
189
);
190
buildRules = (
191
);
192
dependencies = (
193
+
16A25DDD2E6097900070BFFD /* PBXTargetDependency */,
194
);
195
fileSystemSynchronizedGroups = (
196
16A25D7A2E5CA4790070BFFD /* AtProtoBackup */,
···
250
productReference = 16A25D932E5CA47B0070BFFD /* AtProtoBackupUITests.xctest */;
251
productType = "com.apple.product-type.bundle.ui-testing";
252
};
253
+
16A25DCA2E60978D0070BFFD /* WidgetExtensionExtension */ = {
254
+
isa = PBXNativeTarget;
255
+
buildConfigurationList = 16A25DDF2E6097900070BFFD /* Build configuration list for PBXNativeTarget "WidgetExtensionExtension" */;
256
+
buildPhases = (
257
+
16A25DC72E60978D0070BFFD /* Sources */,
258
+
16A25DC82E60978D0070BFFD /* Frameworks */,
259
+
16A25DC92E60978D0070BFFD /* Resources */,
260
+
);
261
+
buildRules = (
262
+
);
263
+
dependencies = (
264
+
);
265
+
fileSystemSynchronizedGroups = (
266
+
16A25DD12E60978E0070BFFD /* WidgetExtension */,
267
+
);
268
+
name = WidgetExtensionExtension;
269
+
packageProductDependencies = (
270
+
);
271
+
productName = WidgetExtensionExtension;
272
+
productReference = 16A25DCB2E60978D0070BFFD /* WidgetExtensionExtension.appex */;
273
+
productType = "com.apple.product-type.app-extension";
274
+
};
275
/* End PBXNativeTarget section */
276
277
/* Begin PBXProject section */
···
293
CreatedOnToolsVersion = 16.4;
294
TestTargetID = 16A25D772E5CA4790070BFFD;
295
};
296
+
16A25DCA2E60978D0070BFFD = {
297
+
CreatedOnToolsVersion = 16.4;
298
+
};
299
};
300
};
301
buildConfigurationList = 16A25D732E5CA4790070BFFD /* Build configuration list for PBXProject "AtProtoBackup" */;
···
319
16A25D772E5CA4790070BFFD /* AtProtoBackup */,
320
16A25D882E5CA47B0070BFFD /* AtProtoBackupTests */,
321
16A25D922E5CA47B0070BFFD /* AtProtoBackupUITests */,
322
+
16A25DCA2E60978D0070BFFD /* WidgetExtensionExtension */,
323
);
324
};
325
/* End PBXProject section */
···
346
);
347
runOnlyForDeploymentPostprocessing = 0;
348
};
349
+
16A25DC92E60978D0070BFFD /* Resources */ = {
350
+
isa = PBXResourcesBuildPhase;
351
+
buildActionMask = 2147483647;
352
+
files = (
353
+
);
354
+
runOnlyForDeploymentPostprocessing = 0;
355
+
};
356
/* End PBXResourcesBuildPhase section */
357
358
/* Begin PBXSourcesBuildPhase section */
···
377
);
378
runOnlyForDeploymentPostprocessing = 0;
379
};
380
+
16A25DC72E60978D0070BFFD /* Sources */ = {
381
+
isa = PBXSourcesBuildPhase;
382
+
buildActionMask = 2147483647;
383
+
files = (
384
+
);
385
+
runOnlyForDeploymentPostprocessing = 0;
386
+
};
387
/* End PBXSourcesBuildPhase section */
388
389
/* Begin PBXTargetDependency section */
···
396
isa = PBXTargetDependency;
397
target = 16A25D772E5CA4790070BFFD /* AtProtoBackup */;
398
targetProxy = 16A25D942E5CA47B0070BFFD /* PBXContainerItemProxy */;
399
+
};
400
+
16A25DDD2E6097900070BFFD /* PBXTargetDependency */ = {
401
+
isa = PBXTargetDependency;
402
+
target = 16A25DCA2E60978D0070BFFD /* WidgetExtensionExtension */;
403
+
targetProxy = 16A25DDC2E6097900070BFFD /* PBXContainerItemProxy */;
404
};
405
/* End PBXTargetDependency section */
406
···
695
};
696
name = Release;
697
};
698
+
16A25DE02E6097900070BFFD /* Debug */ = {
699
+
isa = XCBuildConfiguration;
700
+
buildSettings = {
701
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
702
+
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
703
+
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
704
+
CODE_SIGN_IDENTITY = "Apple Development";
705
+
CODE_SIGN_STYLE = Automatic;
706
+
CURRENT_PROJECT_VERSION = 1;
707
+
DEVELOPMENT_TEAM = 2U6LPQ7AH5;
708
+
ENABLE_HARDENED_RUNTIME = YES;
709
+
GENERATE_INFOPLIST_FILE = YES;
710
+
INFOPLIST_FILE = WidgetExtension/Info.plist;
711
+
INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension;
712
+
INFOPLIST_KEY_NSHumanReadableCopyright = "";
713
+
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
714
+
LD_RUNPATH_SEARCH_PATHS = (
715
+
"$(inherited)",
716
+
"@executable_path/Frameworks",
717
+
"@executable_path/../../Frameworks",
718
+
);
719
+
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = (
720
+
"$(inherited)",
721
+
"@executable_path/../Frameworks",
722
+
"@executable_path/../../../../Frameworks",
723
+
);
724
+
MACOSX_DEPLOYMENT_TARGET = 14.0;
725
+
MARKETING_VERSION = 1.0;
726
+
PRODUCT_BUNDLE_IDENTIFIER = com.coreyja.AtProtoBackup.WidgetExtension;
727
+
PRODUCT_NAME = "$(TARGET_NAME)";
728
+
SDKROOT = auto;
729
+
SKIP_INSTALL = YES;
730
+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
731
+
SWIFT_EMIT_LOC_STRINGS = YES;
732
+
SWIFT_VERSION = 5.0;
733
+
TARGETED_DEVICE_FAMILY = "1,2,7";
734
+
XROS_DEPLOYMENT_TARGET = 2.5;
735
+
};
736
+
name = Debug;
737
+
};
738
+
16A25DE12E6097900070BFFD /* Release */ = {
739
+
isa = XCBuildConfiguration;
740
+
buildSettings = {
741
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
742
+
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
743
+
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
744
+
CODE_SIGN_IDENTITY = "Apple Development";
745
+
CODE_SIGN_STYLE = Automatic;
746
+
CURRENT_PROJECT_VERSION = 1;
747
+
DEVELOPMENT_TEAM = 2U6LPQ7AH5;
748
+
ENABLE_HARDENED_RUNTIME = YES;
749
+
GENERATE_INFOPLIST_FILE = YES;
750
+
INFOPLIST_FILE = WidgetExtension/Info.plist;
751
+
INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension;
752
+
INFOPLIST_KEY_NSHumanReadableCopyright = "";
753
+
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
754
+
LD_RUNPATH_SEARCH_PATHS = (
755
+
"$(inherited)",
756
+
"@executable_path/Frameworks",
757
+
"@executable_path/../../Frameworks",
758
+
);
759
+
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = (
760
+
"$(inherited)",
761
+
"@executable_path/../Frameworks",
762
+
"@executable_path/../../../../Frameworks",
763
+
);
764
+
MACOSX_DEPLOYMENT_TARGET = 14.0;
765
+
MARKETING_VERSION = 1.0;
766
+
PRODUCT_BUNDLE_IDENTIFIER = com.coreyja.AtProtoBackup.WidgetExtension;
767
+
PRODUCT_NAME = "$(TARGET_NAME)";
768
+
SDKROOT = auto;
769
+
SKIP_INSTALL = YES;
770
+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
771
+
SWIFT_EMIT_LOC_STRINGS = YES;
772
+
SWIFT_VERSION = 5.0;
773
+
TARGETED_DEVICE_FAMILY = "1,2,7";
774
+
XROS_DEPLOYMENT_TARGET = 2.5;
775
+
};
776
+
name = Release;
777
+
};
778
/* End XCBuildConfiguration section */
779
780
/* Begin XCConfigurationList section */
···
810
buildConfigurations = (
811
16A25DA52E5CA47B0070BFFD /* Debug */,
812
16A25DA62E5CA47B0070BFFD /* Release */,
813
+
);
814
+
defaultConfigurationIsVisible = 0;
815
+
defaultConfigurationName = Release;
816
+
};
817
+
16A25DDF2E6097900070BFFD /* Build configuration list for PBXNativeTarget "WidgetExtensionExtension" */ = {
818
+
isa = XCConfigurationList;
819
+
buildConfigurations = (
820
+
16A25DE02E6097900070BFFD /* Debug */,
821
+
16A25DE12E6097900070BFFD /* Release */,
822
);
823
defaultConfigurationIsVisible = 0;
824
defaultConfigurationName = Release;
+5
AtProtoBackup.xcodeproj/xcuserdata/coreyja.xcuserdatad/xcschemes/xcschememanagement.plist
···
9
<key>orderHint</key>
10
<integer>0</integer>
11
</dict>
0
0
0
0
0
12
</dict>
13
</dict>
14
</plist>
···
9
<key>orderHint</key>
10
<integer>0</integer>
11
</dict>
12
+
<key>WidgetExtensionExtension.xcscheme_^#shared#^_</key>
13
+
<dict>
14
+
<key>orderHint</key>
15
+
<integer>1</integer>
16
+
</dict>
17
</dict>
18
</dict>
19
</plist>
+8
AtProtoBackup/AtProtoBackupApp.swift
···
7
8
import SwiftUI
9
import SwiftData
0
10
11
@main
12
struct AtProtoBackupApp: App {
0
0
0
0
0
0
0
13
var sharedModelContainer: ModelContainer = {
14
let schema = Schema([
15
Account.self,
···
7
8
import SwiftUI
9
import SwiftData
10
+
import ActivityKit
11
12
@main
13
struct AtProtoBackupApp: App {
14
+
init() {
15
+
// Initialize Live Activity permissions check
16
+
Task {
17
+
await LiveActivityManager.shared.checkActivityPermissions()
18
+
}
19
+
}
20
+
21
var sharedModelContainer: ModelContainer = {
22
let schema = Schema([
23
Account.self,
+23
AtProtoBackup/DownloadActivityAttributes.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import ActivityKit
2
+
import Foundation
3
+
4
+
struct DownloadActivityAttributes: ActivityAttributes {
5
+
public struct ContentState: Codable, Hashable {
6
+
var progress: Double
7
+
var downloadedBlobs: Int
8
+
var totalBlobs: Int?
9
+
var accountHandle: String
10
+
var isPaused: Bool
11
+
var status: DownloadStatus
12
+
13
+
enum DownloadStatus: String, Codable {
14
+
case fetchingData = "Fetching repository data..."
15
+
case downloading = "Downloading"
16
+
case paused = "Paused"
17
+
case completed = "Completed"
18
+
}
19
+
}
20
+
21
+
var accountDid: String
22
+
var accountHandle: String
23
+
}
+100
-1
AtProtoBackup/DownloadManager.swift
···
8
import SwiftUI
9
import Combine
10
import ATProtoKit
0
11
12
struct DownloadInfo: Identifiable {
13
let id = UUID()
···
21
class DownloadManager: ObservableObject {
22
@Published private var downloads: [String: DownloadInfo] = [:]
23
private let blobDownloader = BlobDownloader()
0
24
25
func getDownload(for account: Account) -> DownloadInfo? {
26
downloads[account.did]
···
35
downloads[account.did]?.progress = downloads[account.did]?.progress ?? 0
36
}
37
0
0
38
39
Task {
40
await MainActor.run {
41
downloads[accountDid]?.isDownloading = true
42
}
0
0
0
43
44
do {
45
let tempDirectory = FileManager.default.temporaryDirectory
···
86
downloads[accountDid]?.progress = 0 // Reset progress for blob downloads
87
}
88
0
0
0
89
// guard let saveUrl = saveLocation.fileURL else {
90
// throw GenericIntentError.message(
91
// "Was not able to get a valid url for the save location")
···
112
let _ = try await blobDownloader.downloadBlobs(repo: account.did, pdsURL: account.pds, cids: allBlobCids, saveLocationBookmark: bookmarkData) { [weak self] downloaded, total in
113
Task { @MainActor in
114
if let totalBlobs = self?.downloads[accountDid]?.totalBlobs {
115
-
self?.downloads[accountDid]?.progress = Double(downloaded) / Double(totalBlobs)
0
0
0
116
}
117
}
118
}
···
120
await MainActor.run {
121
downloads[accountDid]?.progress = 1.0
122
downloads[accountDid]?.isDownloading = false
0
0
0
0
0
0
0
0
0
123
}
124
125
} catch {
···
178
179
let decoder = JSONDecoder()
180
return try decoder.decode(ComAtprotoLexicon.Sync.ListBlobsOutput.self, from: data)
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
181
}
182
}
183
}
···
8
import SwiftUI
9
import Combine
10
import ATProtoKit
11
+
import ActivityKit
12
13
struct DownloadInfo: Identifiable {
14
let id = UUID()
···
22
class DownloadManager: ObservableObject {
23
@Published private var downloads: [String: DownloadInfo] = [:]
24
private let blobDownloader = BlobDownloader()
25
+
private var liveActivities: [String: Activity<DownloadActivityAttributes>] = [:]
26
27
func getDownload(for account: Account) -> DownloadInfo? {
28
downloads[account.did]
···
37
downloads[account.did]?.progress = downloads[account.did]?.progress ?? 0
38
}
39
40
+
// Start Live Activity
41
+
startLiveActivity(for: account)
42
43
Task {
44
await MainActor.run {
45
downloads[accountDid]?.isDownloading = true
46
}
47
+
48
+
// Update Live Activity to fetching state
49
+
updateLiveActivity(for: accountDid, status: .fetchingData, progress: 0, downloadedBlobs: 0, totalBlobs: nil, isPaused: false)
50
51
do {
52
let tempDirectory = FileManager.default.temporaryDirectory
···
93
downloads[accountDid]?.progress = 0 // Reset progress for blob downloads
94
}
95
96
+
// Update Live Activity to downloading state
97
+
updateLiveActivity(for: accountDid, status: .downloading, progress: 0, downloadedBlobs: 0, totalBlobs: totalCount, isPaused: false)
98
+
99
// guard let saveUrl = saveLocation.fileURL else {
100
// throw GenericIntentError.message(
101
// "Was not able to get a valid url for the save location")
···
122
let _ = try await blobDownloader.downloadBlobs(repo: account.did, pdsURL: account.pds, cids: allBlobCids, saveLocationBookmark: bookmarkData) { [weak self] downloaded, total in
123
Task { @MainActor in
124
if let totalBlobs = self?.downloads[accountDid]?.totalBlobs {
125
+
let progress = Double(downloaded) / Double(totalBlobs)
126
+
self?.downloads[accountDid]?.progress = progress
127
+
// Update Live Activity progress
128
+
self?.updateLiveActivity(for: accountDid, status: .downloading, progress: progress, downloadedBlobs: downloaded, totalBlobs: totalBlobs, isPaused: false)
129
}
130
}
131
}
···
133
await MainActor.run {
134
downloads[accountDid]?.progress = 1.0
135
downloads[accountDid]?.isDownloading = false
136
+
}
137
+
138
+
// Update Live Activity to completed
139
+
updateLiveActivity(for: accountDid, status: .completed, progress: 1.0, downloadedBlobs: totalCount, totalBlobs: totalCount, isPaused: false)
140
+
141
+
// End Live Activity after a delay
142
+
Task {
143
+
try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
144
+
endLiveActivity(for: accountDid)
145
}
146
147
} catch {
···
200
201
let decoder = JSONDecoder()
202
return try decoder.decode(ComAtprotoLexicon.Sync.ListBlobsOutput.self, from: data)
203
+
}
204
+
}
205
+
206
+
// MARK: - Live Activity Management
207
+
208
+
private func startLiveActivity(for account: Account) {
209
+
print("[LiveActivity] Checking if activities are enabled...")
210
+
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
211
+
print("[LiveActivity] Activities are not enabled")
212
+
return
213
+
}
214
+
215
+
print("[LiveActivity] Starting Live Activity for account: \(account.handle)")
216
+
217
+
let attributes = DownloadActivityAttributes(
218
+
accountDid: account.did,
219
+
accountHandle: account.handle
220
+
)
221
+
222
+
let initialState = DownloadActivityAttributes.ContentState(
223
+
progress: 0,
224
+
downloadedBlobs: 0,
225
+
totalBlobs: nil,
226
+
accountHandle: account.handle,
227
+
isPaused: false,
228
+
status: .fetchingData
229
+
)
230
+
231
+
let content = ActivityContent(state: initialState, staleDate: nil)
232
+
233
+
do {
234
+
let activity = try Activity.request(
235
+
attributes: attributes,
236
+
content: content,
237
+
pushType: nil
238
+
)
239
+
liveActivities[account.did] = activity
240
+
print("[LiveActivity] Successfully started Live Activity with ID: \(activity.id)")
241
+
} catch {
242
+
print("[LiveActivity] Failed to start Live Activity: \(error)")
243
+
}
244
+
}
245
+
246
+
private func updateLiveActivity(
247
+
for accountDid: String,
248
+
status: DownloadActivityAttributes.ContentState.DownloadStatus,
249
+
progress: Double,
250
+
downloadedBlobs: Int,
251
+
totalBlobs: Int?,
252
+
isPaused: Bool
253
+
) {
254
+
guard let activity = liveActivities[accountDid] else {
255
+
print("[LiveActivity] No activity found for account \(accountDid)")
256
+
return
257
+
}
258
+
259
+
Task {
260
+
let updatedState = DownloadActivityAttributes.ContentState(
261
+
progress: progress,
262
+
downloadedBlobs: downloadedBlobs,
263
+
totalBlobs: totalBlobs,
264
+
accountHandle: activity.attributes.accountHandle,
265
+
isPaused: isPaused,
266
+
status: status
267
+
)
268
+
269
+
await activity.update(using: updatedState)
270
+
print("[LiveActivity] Updated activity for \(accountDid) - Status: \(status), Progress: \(Int(progress * 100))%")
271
+
}
272
+
}
273
+
274
+
private func endLiveActivity(for accountDid: String) {
275
+
guard let activity = liveActivities[accountDid] else { return }
276
+
277
+
Task {
278
+
await activity.end(dismissalPolicy: .immediate)
279
+
liveActivities.removeValue(forKey: accountDid)
280
}
281
}
282
}
+2
AtProtoBackup/Info.plist
···
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
<plist version="1.0">
4
<dict>
0
0
5
<key>UIBackgroundModes</key>
6
<array>
7
<string>remote-notification</string>
···
2
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
<plist version="1.0">
4
<dict>
5
+
<key>NSSupportsLiveActivities</key>
6
+
<true/>
7
<key>UIBackgroundModes</key>
8
<array>
9
<string>remote-notification</string>
+32
AtProtoBackup/LiveActivityManager.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import ActivityKit
2
+
import Foundation
3
+
4
+
class LiveActivityManager {
5
+
static let shared = LiveActivityManager()
6
+
7
+
private init() {
8
+
// Check Activity permissions on init
9
+
Task {
10
+
await checkActivityPermissions()
11
+
}
12
+
}
13
+
14
+
func checkActivityPermissions() async {
15
+
let authorizationInfo = ActivityAuthorizationInfo()
16
+
17
+
print("[LiveActivity] Authorization Info:")
18
+
print(" - Activities Enabled: \(authorizationInfo.areActivitiesEnabled)")
19
+
print(" - Frequent Push Enabled: \(authorizationInfo.frequentPushesEnabled)")
20
+
// print(" - Push to Start Enabled: \(authorizationInfo.pushToStartEnabled)")
21
+
22
+
if !authorizationInfo.areActivitiesEnabled {
23
+
print("[LiveActivity] WARNING: Live Activities are not enabled in Settings!")
24
+
}
25
+
}
26
+
27
+
func requestPermissionsIfNeeded() async {
28
+
// Live Activities don't require explicit permission request
29
+
// but we can check if they're enabled
30
+
await checkActivityPermissions()
31
+
}
32
+
}
+11
WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json
···
0
0
0
0
0
0
0
0
0
0
0
···
1
+
{
2
+
"colors" : [
3
+
{
4
+
"idiom" : "universal"
5
+
}
6
+
],
7
+
"info" : {
8
+
"author" : "xcode",
9
+
"version" : 1
10
+
}
11
+
}
+85
WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
{
2
+
"images" : [
3
+
{
4
+
"idiom" : "universal",
5
+
"platform" : "ios",
6
+
"size" : "1024x1024"
7
+
},
8
+
{
9
+
"appearances" : [
10
+
{
11
+
"appearance" : "luminosity",
12
+
"value" : "dark"
13
+
}
14
+
],
15
+
"idiom" : "universal",
16
+
"platform" : "ios",
17
+
"size" : "1024x1024"
18
+
},
19
+
{
20
+
"appearances" : [
21
+
{
22
+
"appearance" : "luminosity",
23
+
"value" : "tinted"
24
+
}
25
+
],
26
+
"idiom" : "universal",
27
+
"platform" : "ios",
28
+
"size" : "1024x1024"
29
+
},
30
+
{
31
+
"idiom" : "mac",
32
+
"scale" : "1x",
33
+
"size" : "16x16"
34
+
},
35
+
{
36
+
"idiom" : "mac",
37
+
"scale" : "2x",
38
+
"size" : "16x16"
39
+
},
40
+
{
41
+
"idiom" : "mac",
42
+
"scale" : "1x",
43
+
"size" : "32x32"
44
+
},
45
+
{
46
+
"idiom" : "mac",
47
+
"scale" : "2x",
48
+
"size" : "32x32"
49
+
},
50
+
{
51
+
"idiom" : "mac",
52
+
"scale" : "1x",
53
+
"size" : "128x128"
54
+
},
55
+
{
56
+
"idiom" : "mac",
57
+
"scale" : "2x",
58
+
"size" : "128x128"
59
+
},
60
+
{
61
+
"idiom" : "mac",
62
+
"scale" : "1x",
63
+
"size" : "256x256"
64
+
},
65
+
{
66
+
"idiom" : "mac",
67
+
"scale" : "2x",
68
+
"size" : "256x256"
69
+
},
70
+
{
71
+
"idiom" : "mac",
72
+
"scale" : "1x",
73
+
"size" : "512x512"
74
+
},
75
+
{
76
+
"idiom" : "mac",
77
+
"scale" : "2x",
78
+
"size" : "512x512"
79
+
}
80
+
],
81
+
"info" : {
82
+
"author" : "xcode",
83
+
"version" : 1
84
+
}
85
+
}
+6
WidgetExtension/Assets.xcassets/Contents.json
···
0
0
0
0
0
0
···
1
+
{
2
+
"info" : {
3
+
"author" : "xcode",
4
+
"version" : 1
5
+
}
6
+
}
+11
WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json
···
0
0
0
0
0
0
0
0
0
0
0
···
1
+
{
2
+
"colors" : [
3
+
{
4
+
"idiom" : "universal"
5
+
}
6
+
],
7
+
"info" : {
8
+
"author" : "xcode",
9
+
"version" : 1
10
+
}
11
+
}
+23
WidgetExtension/DownloadActivityAttributes.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import ActivityKit
2
+
import Foundation
3
+
4
+
struct DownloadActivityAttributes: ActivityAttributes {
5
+
public struct ContentState: Codable, Hashable {
6
+
var progress: Double
7
+
var downloadedBlobs: Int
8
+
var totalBlobs: Int?
9
+
var accountHandle: String
10
+
var isPaused: Bool
11
+
var status: DownloadStatus
12
+
13
+
enum DownloadStatus: String, Codable {
14
+
case fetchingData = "Fetching repository data..."
15
+
case downloading = "Downloading"
16
+
case paused = "Paused"
17
+
case completed = "Completed"
18
+
}
19
+
}
20
+
21
+
var accountDid: String
22
+
var accountHandle: String
23
+
}
+242
WidgetExtension/DownloadLiveActivityView.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import ActivityKit
2
+
import SwiftUI
3
+
import WidgetKit
4
+
5
+
struct DownloadLiveActivityView: View {
6
+
let context: ActivityViewContext<DownloadActivityAttributes>
7
+
8
+
var body: some View {
9
+
HStack(spacing: 16) {
10
+
VStack(alignment: .leading, spacing: 4) {
11
+
Text(context.attributes.accountHandle)
12
+
.font(.caption2)
13
+
.foregroundColor(.secondary)
14
+
15
+
switch context.state.status {
16
+
case .fetchingData:
17
+
Text("Fetching data...")
18
+
.font(.caption)
19
+
.foregroundColor(.primary)
20
+
case .downloading:
21
+
Text("Downloading")
22
+
.font(.caption)
23
+
.foregroundColor(.primary)
24
+
case .paused:
25
+
Text("Paused")
26
+
.font(.caption)
27
+
.foregroundColor(.orange)
28
+
case .completed:
29
+
Text("Complete")
30
+
.font(.caption)
31
+
.foregroundColor(.green)
32
+
}
33
+
}
34
+
35
+
Spacer()
36
+
37
+
if context.state.status == .downloading || context.state.status == .paused {
38
+
VStack(alignment: .trailing, spacing: 4) {
39
+
Text("\(Int(context.state.progress * 100))%")
40
+
.font(.system(.title3, design: .rounded))
41
+
.fontWeight(.semibold)
42
+
.foregroundColor(.primary)
43
+
44
+
if let totalBlobs = context.state.totalBlobs {
45
+
Text("\(context.state.downloadedBlobs)/\(totalBlobs)")
46
+
.font(.caption2)
47
+
.foregroundColor(.secondary)
48
+
}
49
+
}
50
+
} else if context.state.status == .completed {
51
+
Image(systemName: "checkmark.circle.fill")
52
+
.foregroundColor(.green)
53
+
.font(.title2)
54
+
}
55
+
}
56
+
.padding(.horizontal, 20)
57
+
.padding(.vertical, 12)
58
+
.activityBackgroundTint(Color.clear)
59
+
.activitySystemActionForegroundColor(Color.primary)
60
+
}
61
+
}
62
+
63
+
struct DownloadLiveActivityExpandedView: View {
64
+
let context: ActivityViewContext<DownloadActivityAttributes>
65
+
66
+
var body: some View {
67
+
VStack(spacing: 12) {
68
+
HStack {
69
+
VStack(alignment: .leading, spacing: 4) {
70
+
Text("Downloading Backup")
71
+
.font(.headline)
72
+
Text(context.attributes.accountHandle)
73
+
.font(.subheadline)
74
+
.foregroundColor(.secondary)
75
+
}
76
+
Spacer()
77
+
}
78
+
79
+
if context.state.status == .downloading || context.state.status == .paused {
80
+
VStack(spacing: 8) {
81
+
ProgressView(value: context.state.progress)
82
+
.progressViewStyle(.linear)
83
+
.tint(context.state.status == .paused ? .orange : .accentColor)
84
+
85
+
HStack {
86
+
Text(statusText)
87
+
.font(.caption)
88
+
.foregroundColor(.secondary)
89
+
Spacer()
90
+
Text("\(Int(context.state.progress * 100))%")
91
+
.font(.caption)
92
+
.fontWeight(.medium)
93
+
}
94
+
95
+
if let totalBlobs = context.state.totalBlobs {
96
+
Text("\(context.state.downloadedBlobs) of \(totalBlobs) blobs")
97
+
.font(.caption2)
98
+
.foregroundColor(.secondary)
99
+
}
100
+
}
101
+
} else if context.state.status == .fetchingData {
102
+
HStack {
103
+
ProgressView()
104
+
.progressViewStyle(.circular)
105
+
.scaleEffect(0.8)
106
+
Text("Fetching repository data...")
107
+
.font(.caption)
108
+
.foregroundColor(.secondary)
109
+
}
110
+
} else if context.state.status == .completed {
111
+
HStack(spacing: 8) {
112
+
Image(systemName: "checkmark.circle.fill")
113
+
.foregroundColor(.green)
114
+
.font(.title3)
115
+
Text("Download complete")
116
+
.font(.subheadline)
117
+
.foregroundColor(.green)
118
+
}
119
+
}
120
+
}
121
+
.padding()
122
+
.activityBackgroundTint(Color.clear)
123
+
}
124
+
125
+
private var statusText: String {
126
+
switch context.state.status {
127
+
case .downloading:
128
+
return "Downloading..."
129
+
case .paused:
130
+
return "Download paused"
131
+
default:
132
+
return ""
133
+
}
134
+
}
135
+
}
136
+
137
+
struct DownloadActivityWidget: Widget {
138
+
var body: some WidgetConfiguration {
139
+
ActivityConfiguration(for: DownloadActivityAttributes.self) { context in
140
+
DownloadLiveActivityExpandedView(context: context)
141
+
.background(Color(UIColor.systemBackground))
142
+
} dynamicIsland: { context in
143
+
DynamicIsland {
144
+
DynamicIslandExpandedRegion(.leading) {
145
+
VStack(alignment: .leading, spacing: 2) {
146
+
Text(context.attributes.accountHandle)
147
+
.font(.caption2)
148
+
.foregroundColor(.secondary)
149
+
statusLabel(for: context.state.status)
150
+
}
151
+
}
152
+
153
+
DynamicIslandExpandedRegion(.trailing) {
154
+
if context.state.status == .downloading || context.state.status == .paused {
155
+
VStack(alignment: .trailing, spacing: 2) {
156
+
Text("\(Int(context.state.progress * 100))%")
157
+
.font(.system(.title3, design: .rounded))
158
+
.fontWeight(.semibold)
159
+
if let totalBlobs = context.state.totalBlobs {
160
+
Text("\(context.state.downloadedBlobs)/\(totalBlobs)")
161
+
.font(.caption2)
162
+
.foregroundColor(.secondary)
163
+
}
164
+
}
165
+
} else if context.state.status == .completed {
166
+
Image(systemName: "checkmark.circle.fill")
167
+
.foregroundColor(.green)
168
+
.font(.title2)
169
+
}
170
+
}
171
+
172
+
DynamicIslandExpandedRegion(.bottom) {
173
+
if context.state.status == .downloading || context.state.status == .paused {
174
+
ProgressView(value: context.state.progress)
175
+
.progressViewStyle(.linear)
176
+
.tint(context.state.status == .paused ? .orange : .accentColor)
177
+
}
178
+
}
179
+
} compactLeading: {
180
+
Image(systemName: iconName(for: context.state.status))
181
+
.foregroundColor(iconColor(for: context.state.status))
182
+
} compactTrailing: {
183
+
if context.state.status == .downloading || context.state.status == .paused {
184
+
Text("\(Int(context.state.progress * 100))%")
185
+
.font(.caption)
186
+
.fontWeight(.semibold)
187
+
} else if context.state.status == .fetchingData {
188
+
ProgressView()
189
+
.progressViewStyle(.circular)
190
+
.scaleEffect(0.6)
191
+
}
192
+
} minimal: {
193
+
Image(systemName: iconName(for: context.state.status))
194
+
.foregroundColor(iconColor(for: context.state.status))
195
+
}
196
+
.widgetURL(URL(string: "atprotobackup://download/\(context.attributes.accountDid)"))
197
+
.keylineTint(iconColor(for: context.state.status))
198
+
}
199
+
}
200
+
201
+
private func iconName(for status: DownloadActivityAttributes.ContentState.DownloadStatus) -> String {
202
+
switch status {
203
+
case .fetchingData, .downloading:
204
+
return "arrow.down.circle.fill"
205
+
case .paused:
206
+
return "pause.circle.fill"
207
+
case .completed:
208
+
return "checkmark.circle.fill"
209
+
}
210
+
}
211
+
212
+
private func iconColor(for status: DownloadActivityAttributes.ContentState.DownloadStatus) -> Color {
213
+
switch status {
214
+
case .fetchingData, .downloading:
215
+
return .accentColor
216
+
case .paused:
217
+
return .orange
218
+
case .completed:
219
+
return .green
220
+
}
221
+
}
222
+
223
+
@ViewBuilder
224
+
private func statusLabel(for status: DownloadActivityAttributes.ContentState.DownloadStatus) -> some View {
225
+
switch status {
226
+
case .fetchingData:
227
+
Text("Fetching...")
228
+
.font(.caption)
229
+
case .downloading:
230
+
Text("Downloading")
231
+
.font(.caption)
232
+
case .paused:
233
+
Text("Paused")
234
+
.font(.caption)
235
+
.foregroundColor(.orange)
236
+
case .completed:
237
+
Text("Complete")
238
+
.font(.caption)
239
+
.foregroundColor(.green)
240
+
}
241
+
}
242
+
}
+15
WidgetExtension/Info.plist
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<?xml version="1.0" encoding="UTF-8"?>
2
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+
<plist version="1.0">
4
+
<dict>
5
+
<key>NSExtension</key>
6
+
<dict>
7
+
<key>NSExtensionPointIdentifier</key>
8
+
<string>com.apple.widgetkit-extension</string>
9
+
</dict>
10
+
<key>NSSupportsLiveActivities</key>
11
+
<true/>
12
+
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
13
+
<true/>
14
+
</dict>
15
+
</plist>
+8
WidgetExtension/WidgetExtension.entitlements
···
0
0
0
0
0
0
0
0
···
1
+
<?xml version="1.0" encoding="UTF-8"?>
2
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+
<plist version="1.0">
4
+
<dict>
5
+
<key>com.apple.security.app-sandbox</key>
6
+
<true/>
7
+
</dict>
8
+
</plist>
+16
WidgetExtension/WidgetExtensionBundle.swift
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//
2
+
// WidgetExtensionBundle.swift
3
+
// WidgetExtension
4
+
//
5
+
// Created by Corey Alexander on 8/28/25.
6
+
//
7
+
8
+
import WidgetKit
9
+
import SwiftUI
10
+
11
+
@main
12
+
struct WidgetExtensionBundle: WidgetBundle {
13
+
var body: some Widget {
14
+
DownloadActivityWidget()
15
+
}
16
+
}