Commit b4b94d54cb
Unsigned
Layout: unified · split
DomainDig.xcodeproj/project.pbxproj +164 −18
| @@ -13,8 +13,9 @@ | |||
| 13 | 8BBFEF0C2F9874AE00E8E144 /* LookupSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */; }; | 13 | 8BBFEF0C2F9874AE00E8E144 /* LookupSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */; }; |
| 14 | 8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; }; | 14 | 8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; }; |
| 15 | 8BCA3CC12F9C8D57004B742C /* LocalAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */; }; | 15 | 8BCA3CC12F9C8D57004B742C /* LocalAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */; }; |
| 16 | 8BF9DA872F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; }; | ||
| 17 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | 16 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; |
| 17 | 8BDB00010000000000000002 /* DomainDigShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||
| 18 | 8BF9DA872F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; }; | ||
| 18 | /* End PBXBuildFile section */ | 19 | /* End PBXBuildFile section */ |
| 19 | 20 | ||
| 20 | /* Begin PBXContainerItemProxy section */ | 21 | /* Begin PBXContainerItemProxy section */ |
| @@ -25,6 +26,13 @@ | |||
| 25 | remoteGlobalIDString = 8BDA00010000000000000006; | 26 | remoteGlobalIDString = 8BDA00010000000000000006; |
| 26 | remoteInfo = DomainDigWidgetExtension; | 27 | remoteInfo = DomainDigWidgetExtension; |
| 27 | }; | 28 | }; |
| 29 | 8BDB00010000000000000003 /* PBXContainerItemProxy */ = { | ||
| 30 | isa = PBXContainerItemProxy; | ||
| 31 | containerPortal = 8B7800612F6090E300933221 /* Project object */; | ||
| 32 | proxyType = 1; | ||
| 33 | remoteGlobalIDString = 8BDB00010000000000000006; | ||
| 34 | remoteInfo = DomainDigShareExtension; | ||
| 35 | }; | ||
| 28 | /* End PBXContainerItemProxy section */ | 36 | /* End PBXContainerItemProxy section */ |
| 29 | 37 | ||
| 30 | /* Begin PBXCopyFilesBuildPhase section */ | 38 | /* Begin PBXCopyFilesBuildPhase section */ |
| @@ -35,6 +43,7 @@ | |||
| 35 | dstSubfolderSpec = 13; | 43 | dstSubfolderSpec = 13; |
| 36 | files = ( | 44 | files = ( |
| 37 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */, | 45 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */, |
| 46 | 8BDB00010000000000000002 /* DomainDigShareExtension.appex in Embed Foundation Extensions */, | ||
| 38 | ); | 47 | ); |
| 39 | name = "Embed Foundation Extensions"; | 48 | name = "Embed Foundation Extensions"; |
| 40 | runOnlyForDeploymentPostprocessing = 0; | 49 | runOnlyForDeploymentPostprocessing = 0; |
| @@ -49,8 +58,9 @@ | |||
| 49 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupSnapshot.swift; sourceTree = "<group>"; }; | 58 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupSnapshot.swift; sourceTree = "<group>"; }; |
| 50 | 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIModels.swift; sourceTree = "<group>"; }; | 59 | 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIModels.swift; sourceTree = "<group>"; }; |
| 51 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; }; | 60 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; }; |
| 52 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; | ||
| 53 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | 61 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 62 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||
| 63 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; | ||
| 54 | /* End PBXFileReference section */ | 64 | /* End PBXFileReference section */ |
| 55 | 65 | ||
| 56 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ | 66 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| @@ -70,6 +80,13 @@ | |||
| 70 | ); | 80 | ); |
| 71 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; | 81 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; |
| 72 | }; | 82 | }; |
| 83 | 8BDB0001000000000000000F /* Exceptions for "DomainDigShareExtension" folder in "DomainDigShareExtension" target */ = { | ||
| 84 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; | ||
| 85 | membershipExceptions = ( | ||
| 86 | Info.plist, | ||
| 87 | ); | ||
| 88 | target = 8BDB00010000000000000006 /* DomainDigShareExtension */; | ||
| 89 | }; | ||
| 73 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ | 90 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| 74 | 91 | ||
| 75 | /* Begin PBXFileSystemSynchronizedRootGroup section */ | 92 | /* Begin PBXFileSystemSynchronizedRootGroup section */ |
| @@ -94,6 +111,14 @@ | |||
| 94 | path = DomainDigWidget; | 111 | path = DomainDigWidget; |
| 95 | sourceTree = "<group>"; | 112 | sourceTree = "<group>"; |
| 96 | }; | 113 | }; |
| 114 | 8BDB0001000000000000000E /* DomainDigShareExtension */ = { | ||
| 115 | isa = PBXFileSystemSynchronizedRootGroup; | ||
| 116 | exceptions = ( | ||
| 117 | 8BDB0001000000000000000F /* Exceptions for "DomainDigShareExtension" folder in "DomainDigShareExtension" target */, | ||
| 118 | ); | ||
| 119 | path = DomainDigShareExtension; | ||
| 120 | sourceTree = "<group>"; | ||
| 121 | }; | ||
| 97 | /* End PBXFileSystemSynchronizedRootGroup section */ | 122 | /* End PBXFileSystemSynchronizedRootGroup section */ |
| 98 | 123 | ||
| 99 | /* Begin PBXFrameworksBuildPhase section */ | 124 | /* Begin PBXFrameworksBuildPhase section */ |
| @@ -111,6 +136,13 @@ | |||
| 111 | ); | 136 | ); |
| 112 | runOnlyForDeploymentPostprocessing = 0; | 137 | runOnlyForDeploymentPostprocessing = 0; |
| 113 | }; | 138 | }; |
| 139 | 8BDB00010000000000000008 /* Frameworks */ = { | ||
| 140 | isa = PBXFrameworksBuildPhase; | ||
| 141 | buildActionMask = 2147483647; | ||
| 142 | files = ( | ||
| 143 | ); | ||
| 144 | runOnlyForDeploymentPostprocessing = 0; | ||
| 145 | }; | ||
| 114 | /* End PBXFrameworksBuildPhase section */ | 146 | /* End PBXFrameworksBuildPhase section */ |
| 115 | 147 | ||
| 116 | /* Begin PBXGroup section */ | 148 | /* Begin PBXGroup section */ |
| @@ -120,6 +152,7 @@ | |||
| 120 | 8B78006B2F6090E300933221 /* DomainDig */, | 152 | 8B78006B2F6090E300933221 /* DomainDig */, |
| 121 | 8BDA0001000000000000000D /* Shared */, | 153 | 8BDA0001000000000000000D /* Shared */, |
| 122 | 8BDA0001000000000000000E /* DomainDigWidget */, | 154 | 8BDA0001000000000000000E /* DomainDigWidget */, |
| 155 | 8BDB0001000000000000000E /* DomainDigShareExtension */, | ||
| 123 | 8B78006A2F6090E300933221 /* Products */, | 156 | 8B78006A2F6090E300933221 /* Products */, |
| 124 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, | 157 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, |
| 125 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, | 158 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, |
| @@ -136,6 +169,7 @@ | |||
| 136 | children = ( | 169 | children = ( |
| 137 | 8B7800692F6090E300933221 /* DomainDig.app */, | 170 | 8B7800692F6090E300933221 /* DomainDig.app */, |
| 138 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, | 171 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, |
| 172 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */, | ||
| 139 | ); | 173 | ); |
| 140 | name = Products; | 174 | name = Products; |
| 141 | sourceTree = "<group>"; | 175 | sourceTree = "<group>"; |
| @@ -156,6 +190,7 @@ | |||
| 156 | ); | 190 | ); |
| 157 | dependencies = ( | 191 | dependencies = ( |
| 158 | 8BDA00010000000000000004 /* PBXTargetDependency */, | 192 | 8BDA00010000000000000004 /* PBXTargetDependency */, |
| 193 | 8BDB00010000000000000004 /* PBXTargetDependency */, | ||
| 159 | ); | 194 | ); |
| 160 | fileSystemSynchronizedGroups = ( | 195 | fileSystemSynchronizedGroups = ( |
| 161 | 8B78006B2F6090E300933221 /* DomainDig */, | 196 | 8B78006B2F6090E300933221 /* DomainDig */, |
| @@ -191,15 +226,30 @@ | |||
| 191 | productReference = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; | 226 | productReference = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; |
| 192 | productType = "com.apple.product-type.app-extension"; | 227 | productType = "com.apple.product-type.app-extension"; |
| 193 | }; | 228 | }; |
| 194 | /* End PBXNativeTarget section */ | 229 | 8BDB00010000000000000006 /* DomainDigShareExtension */ = { |
| 195 | 230 | isa = PBXNativeTarget; | |
| 196 | /* Begin PBXTargetDependency section */ | 231 | buildConfigurationList = 8BDB0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigShareExtension" */; |
| 197 | 8BDA00010000000000000004 /* PBXTargetDependency */ = { | 232 | buildPhases = ( |
| 198 | isa = PBXTargetDependency; | 233 | 8BDB00010000000000000007 /* Sources */, |
| 199 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; | 234 | 8BDB00010000000000000008 /* Frameworks */, |
| 200 | targetProxy = 8BDA00010000000000000003 /* PBXContainerItemProxy */; | 235 | 8BDB00010000000000000009 /* Resources */, |
| 236 | ); | ||
| 237 | buildRules = ( | ||
| 238 | ); | ||
| 239 | dependencies = ( | ||
| 240 | ); | ||
| 241 | fileSystemSynchronizedGroups = ( | ||
| 242 | 8BDA0001000000000000000D /* Shared */, | ||
| 243 | 8BDB0001000000000000000E /* DomainDigShareExtension */, | ||
| 244 | ); | ||
| 245 | name = DomainDigShareExtension; | ||
| 246 | packageProductDependencies = ( | ||
| 247 | ); | ||
| 248 | productName = DomainDigShareExtension; | ||
| 249 | productReference = 8BDB00010000000000000001 /* DomainDigShareExtension.appex */; | ||
| 250 | productType = "com.apple.product-type.app-extension"; | ||
| 201 | }; | 251 | }; |
| 202 | /* End PBXTargetDependency section */ | 252 | /* End PBXNativeTarget section */ |
| 203 | 253 | ||
| 204 | /* Begin PBXProject section */ | 254 | /* Begin PBXProject section */ |
| 205 | 8B7800612F6090E300933221 /* Project object */ = { | 255 | 8B7800612F6090E300933221 /* Project object */ = { |
| @@ -220,6 +270,9 @@ | |||
| 220 | 8BDA00010000000000000006 = { | 270 | 8BDA00010000000000000006 = { |
| 221 | CreatedOnToolsVersion = 26.3; | 271 | CreatedOnToolsVersion = 26.3; |
| 222 | }; | 272 | }; |
| 273 | 8BDB00010000000000000006 = { | ||
| 274 | CreatedOnToolsVersion = 26.3; | ||
| 275 | }; | ||
| 223 | }; | 276 | }; |
| 224 | }; | 277 | }; |
| 225 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; | 278 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; |
| @@ -238,6 +291,7 @@ | |||
| 238 | targets = ( | 291 | targets = ( |
| 239 | 8B7800682F6090E300933221 /* DomainDig */, | 292 | 8B7800682F6090E300933221 /* DomainDig */, |
| 240 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, | 293 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, |
| 294 | 8BDB00010000000000000006 /* DomainDigShareExtension */, | ||
| 241 | ); | 295 | ); |
| 242 | }; | 296 | }; |
| 243 | /* End PBXProject section */ | 297 | /* End PBXProject section */ |
| @@ -257,6 +311,13 @@ | |||
| 257 | ); | 311 | ); |
| 258 | runOnlyForDeploymentPostprocessing = 0; | 312 | runOnlyForDeploymentPostprocessing = 0; |
| 259 | }; | 313 | }; |
| 314 | 8BDB00010000000000000009 /* Resources */ = { | ||
| 315 | isa = PBXResourcesBuildPhase; | ||
| 316 | buildActionMask = 2147483647; | ||
| 317 | files = ( | ||
| 318 | ); | ||
| 319 | runOnlyForDeploymentPostprocessing = 0; | ||
| 320 | }; | ||
| 260 | /* End PBXResourcesBuildPhase section */ | 321 | /* End PBXResourcesBuildPhase section */ |
| 261 | 322 | ||
| 262 | /* Begin PBXSourcesBuildPhase section */ | 323 | /* Begin PBXSourcesBuildPhase section */ |
| @@ -281,8 +342,28 @@ | |||
| 281 | ); | 342 | ); |
| 282 | runOnlyForDeploymentPostprocessing = 0; | 343 | runOnlyForDeploymentPostprocessing = 0; |
| 283 | }; | 344 | }; |
| 345 | 8BDB00010000000000000007 /* Sources */ = { | ||
| 346 | isa = PBXSourcesBuildPhase; | ||
| 347 | buildActionMask = 2147483647; | ||
| 348 | files = ( | ||
| 349 | ); | ||
| 350 | runOnlyForDeploymentPostprocessing = 0; | ||
| 351 | }; | ||
| 284 | /* End PBXSourcesBuildPhase section */ | 352 | /* End PBXSourcesBuildPhase section */ |
| 285 | 353 | ||
| 354 | /* Begin PBXTargetDependency section */ | ||
| 355 | 8BDA00010000000000000004 /* PBXTargetDependency */ = { | ||
| 356 | isa = PBXTargetDependency; | ||
| 357 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; | ||
| 358 | targetProxy = 8BDA00010000000000000003 /* PBXContainerItemProxy */; | ||
| 359 | }; | ||
| 360 | 8BDB00010000000000000004 /* PBXTargetDependency */ = { | ||
| 361 | isa = PBXTargetDependency; | ||
| 362 | target = 8BDB00010000000000000006 /* DomainDigShareExtension */; | ||
| 363 | targetProxy = 8BDB00010000000000000003 /* PBXContainerItemProxy */; | ||
| 364 | }; | ||
| 365 | /* End PBXTargetDependency section */ | ||
| 366 | |||
| 286 | /* Begin XCBuildConfiguration section */ | 367 | /* Begin XCBuildConfiguration section */ |
| 287 | 8B7800722F6090E400933221 /* Debug */ = { | 368 | 8B7800722F6090E400933221 /* Debug */ = { |
| 288 | isa = XCBuildConfiguration; | 369 | isa = XCBuildConfiguration; |
| @@ -414,7 +495,7 @@ | |||
| 414 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 495 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 415 | CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; | 496 | CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; |
| 416 | CODE_SIGN_STYLE = Automatic; | 497 | CODE_SIGN_STYLE = Automatic; |
| 417 | CURRENT_PROJECT_VERSION = 37; | 498 | CURRENT_PROJECT_VERSION = 38; |
| 418 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 499 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 419 | ENABLE_PREVIEWS = YES; | 500 | ENABLE_PREVIEWS = YES; |
| 420 | GENERATE_INFOPLIST_FILE = YES; | 501 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -431,7 +512,7 @@ | |||
| 431 | "$(inherited)", | 512 | "$(inherited)", |
| 432 | "@executable_path/Frameworks", | 513 | "@executable_path/Frameworks", |
| 433 | ); | 514 | ); |
| 434 | MARKETING_VERSION = 4.5.0; | 515 | MARKETING_VERSION = 4.6.0; |
| 435 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; | 516 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; |
| 436 | PRODUCT_NAME = "$(TARGET_NAME)"; | 517 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 437 | STRING_CATALOG_GENERATE_SYMBOLS = YES; | 518 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -451,7 +532,7 @@ | |||
| 451 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 532 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 452 | CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; | 533 | CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; |
| 453 | CODE_SIGN_STYLE = Automatic; | 534 | CODE_SIGN_STYLE = Automatic; |
| 454 | CURRENT_PROJECT_VERSION = 37; | 535 | CURRENT_PROJECT_VERSION = 38; |
| 455 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 536 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 456 | ENABLE_PREVIEWS = YES; | 537 | ENABLE_PREVIEWS = YES; |
| 457 | GENERATE_INFOPLIST_FILE = YES; | 538 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -468,7 +549,7 @@ | |||
| 468 | "$(inherited)", | 549 | "$(inherited)", |
| 469 | "@executable_path/Frameworks", | 550 | "@executable_path/Frameworks", |
| 470 | ); | 551 | ); |
| 471 | MARKETING_VERSION = 4.5.0; | 552 | MARKETING_VERSION = 4.6.0; |
| 472 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; | 553 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; |
| 473 | PRODUCT_NAME = "$(TARGET_NAME)"; | 554 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 474 | STRING_CATALOG_GENERATE_SYMBOLS = YES; | 555 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -487,7 +568,7 @@ | |||
| 487 | APPLICATION_EXTENSION_API_ONLY = YES; | 568 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 488 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; | 569 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; |
| 489 | CODE_SIGN_STYLE = Automatic; | 570 | CODE_SIGN_STYLE = Automatic; |
| 490 | CURRENT_PROJECT_VERSION = 37; | 571 | CURRENT_PROJECT_VERSION = 38; |
| 491 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 572 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 492 | GENERATE_INFOPLIST_FILE = NO; | 573 | GENERATE_INFOPLIST_FILE = NO; |
| 493 | INFOPLIST_FILE = DomainDigWidget/Info.plist; | 574 | INFOPLIST_FILE = DomainDigWidget/Info.plist; |
| @@ -497,7 +578,7 @@ | |||
| 497 | "@executable_path/Frameworks", | 578 | "@executable_path/Frameworks", |
| 498 | "@executable_path/../../Frameworks", | 579 | "@executable_path/../../Frameworks", |
| 499 | ); | 580 | ); |
| 500 | MARKETING_VERSION = 4.5.0; | 581 | MARKETING_VERSION = 4.6.0; |
| 501 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; | 582 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; |
| 502 | PRODUCT_NAME = "$(TARGET_NAME)"; | 583 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 503 | SKIP_INSTALL = YES; | 584 | SKIP_INSTALL = YES; |
| @@ -515,7 +596,7 @@ | |||
| 515 | APPLICATION_EXTENSION_API_ONLY = YES; | 596 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 516 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; | 597 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; |
| 517 | CODE_SIGN_STYLE = Automatic; | 598 | CODE_SIGN_STYLE = Automatic; |
| 518 | CURRENT_PROJECT_VERSION = 37; | 599 | CURRENT_PROJECT_VERSION = 38; |
| 519 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 600 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 520 | GENERATE_INFOPLIST_FILE = NO; | 601 | GENERATE_INFOPLIST_FILE = NO; |
| 521 | INFOPLIST_FILE = DomainDigWidget/Info.plist; | 602 | INFOPLIST_FILE = DomainDigWidget/Info.plist; |
| @@ -525,7 +606,7 @@ | |||
| 525 | "@executable_path/Frameworks", | 606 | "@executable_path/Frameworks", |
| 526 | "@executable_path/../../Frameworks", | 607 | "@executable_path/../../Frameworks", |
| 527 | ); | 608 | ); |
| 528 | MARKETING_VERSION = 4.5.0; | 609 | MARKETING_VERSION = 4.6.0; |
| 529 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; | 610 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; |
| 530 | PRODUCT_NAME = "$(TARGET_NAME)"; | 611 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 531 | SKIP_INSTALL = YES; | 612 | SKIP_INSTALL = YES; |
| @@ -537,6 +618,62 @@ | |||
| 537 | }; | 618 | }; |
| 538 | name = Release; | 619 | name = Release; |
| 539 | }; | 620 | }; |
| 621 | 8BDB0001000000000000000B /* Debug */ = { | ||
| 622 | isa = XCBuildConfiguration; | ||
| 623 | buildSettings = { | ||
| 624 | APPLICATION_EXTENSION_API_ONLY = YES; | ||
| 625 | CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements; | ||
| 626 | CODE_SIGN_STYLE = Automatic; | ||
| 627 | CURRENT_PROJECT_VERSION = 38; | ||
| 628 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | ||
| 629 | GENERATE_INFOPLIST_FILE = NO; | ||
| 630 | INFOPLIST_FILE = DomainDigShareExtension/Info.plist; | ||
| 631 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | ||
| 632 | LD_RUNPATH_SEARCH_PATHS = ( | ||
| 633 | "$(inherited)", | ||
| 634 | "@executable_path/Frameworks", | ||
| 635 | "@executable_path/../../Frameworks", | ||
| 636 | ); | ||
| 637 | MARKETING_VERSION = 4.6.0; | ||
| 638 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension; | ||
| 639 | PRODUCT_NAME = "$(TARGET_NAME)"; | ||
| 640 | SKIP_INSTALL = YES; | ||
| 641 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||
| 642 | SWIFT_EMIT_LOC_STRINGS = YES; | ||
| 643 | SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||
| 644 | SWIFT_VERSION = 5.0; | ||
| 645 | TARGETED_DEVICE_FAMILY = "1,2"; | ||
| 646 | }; | ||
| 647 | name = Debug; | ||
| 648 | }; | ||
| 649 | 8BDB0001000000000000000C /* Release */ = { | ||
| 650 | isa = XCBuildConfiguration; | ||
| 651 | buildSettings = { | ||
| 652 | APPLICATION_EXTENSION_API_ONLY = YES; | ||
| 653 | CODE_SIGN_ENTITLEMENTS = DomainDigShareExtension/DomainDigShareExtension.entitlements; | ||
| 654 | CODE_SIGN_STYLE = Automatic; | ||
| 655 | CURRENT_PROJECT_VERSION = 38; | ||
| 656 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | ||
| 657 | GENERATE_INFOPLIST_FILE = NO; | ||
| 658 | INFOPLIST_FILE = DomainDigShareExtension/Info.plist; | ||
| 659 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | ||
| 660 | LD_RUNPATH_SEARCH_PATHS = ( | ||
| 661 | "$(inherited)", | ||
| 662 | "@executable_path/Frameworks", | ||
| 663 | "@executable_path/../../Frameworks", | ||
| 664 | ); | ||
| 665 | MARKETING_VERSION = 4.6.0; | ||
| 666 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigShareExtension; | ||
| 667 | PRODUCT_NAME = "$(TARGET_NAME)"; | ||
| 668 | SKIP_INSTALL = YES; | ||
| 669 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||
| 670 | SWIFT_EMIT_LOC_STRINGS = YES; | ||
| 671 | SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||
| 672 | SWIFT_VERSION = 5.0; | ||
| 673 | TARGETED_DEVICE_FAMILY = "1,2"; | ||
| 674 | }; | ||
| 675 | name = Release; | ||
| 676 | }; | ||
| 540 | /* End XCBuildConfiguration section */ | 677 | /* End XCBuildConfiguration section */ |
| 541 | 678 | ||
| 542 | /* Begin XCConfigurationList section */ | 679 | /* Begin XCConfigurationList section */ |
| @@ -567,6 +704,15 @@ | |||
| 567 | defaultConfigurationIsVisible = 0; | 704 | defaultConfigurationIsVisible = 0; |
| 568 | defaultConfigurationName = Release; | 705 | defaultConfigurationName = Release; |
| 569 | }; | 706 | }; |
| 707 | 8BDB0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigShareExtension" */ = { | ||
| 708 | isa = XCConfigurationList; | ||
| 709 | buildConfigurations = ( | ||
| 710 | 8BDB0001000000000000000B /* Debug */, | ||
| 711 | 8BDB0001000000000000000C /* Release */, | ||
| 712 | ); | ||
| 713 | defaultConfigurationIsVisible = 0; | ||
| 714 | defaultConfigurationName = Release; | ||
| 715 | }; | ||
| 570 | /* End XCConfigurationList section */ | 716 | /* End XCConfigurationList section */ |
| 571 | }; | 717 | }; |
| 572 | rootObject = 8B7800612F6090E300933221 /* Project object */; | 718 | rootObject = 8B7800612F6090E300933221 /* Project object */; |
DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist +6 −1
| @@ -9,11 +9,16 @@ | |||
| 9 | <key>orderHint</key> | 9 | <key>orderHint</key> |
| 10 | <integer>0</integer> | 10 | <integer>0</integer> |
| 11 | </dict> | 11 | </dict> |
| 12 | <key>DomainDigWidgetExtension.xcscheme_^#shared#^_</key> | 12 | <key>DomainDigShareExtension.xcscheme_^#shared#^_</key> |
| 13 | <dict> | 13 | <dict> |
| 14 | <key>orderHint</key> | 14 | <key>orderHint</key> |
| 15 | <integer>1</integer> | 15 | <integer>1</integer> |
| 16 | </dict> | 16 | </dict> |
| 17 | <key>DomainDigWidgetExtension.xcscheme_^#shared#^_</key> | ||
| 18 | <dict> | ||
| 19 | <key>orderHint</key> | ||
| 20 | <integer>2</integer> | ||
| 21 | </dict> | ||
| 17 | </dict> | 22 | </dict> |
| 18 | <key>SuppressBuildableAutocreation</key> | 23 | <key>SuppressBuildableAutocreation</key> |
| 19 | <dict> | 24 | <dict> |
DomainDig/AppVersion.swift +1 −1
| @@ -2,6 +2,6 @@ import Foundation | |||
| 2 | 2 | ||
| 3 | enum AppVersion { | 3 | enum AppVersion { |
| 4 | nonisolated static var current: String { | 4 | nonisolated static var current: String { |
| 5 | "4.5.0" | 5 | "4.6.0" |
| 6 | } | 6 | } |
| 7 | } | 7 | } |
DomainDig/DomainDigApp.swift +9
| @@ -40,6 +40,7 @@ struct DomainDigApp: App { | |||
| 40 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | 40 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() |
| 41 | IntegrationService.shared.processQueueNow() | 41 | IntegrationService.shared.processQueueNow() |
| 42 | viewModel.refreshWidgetData() | 42 | viewModel.refreshWidgetData() |
| 43 | consumeShareInbox() | ||
| 43 | } | 44 | } |
| 44 | .onReceive(NotificationCenter.default.publisher(for: .cloudSyncDidApplyChanges)) { _ in | 45 | .onReceive(NotificationCenter.default.publisher(for: .cloudSyncDidApplyChanges)) { _ in |
| 45 | viewModel.refreshPersistedData() | 46 | viewModel.refreshPersistedData() |
| @@ -59,6 +60,14 @@ struct DomainDigApp: App { | |||
| 59 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | 60 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() |
| 60 | IntegrationService.shared.processQueueNow() | 61 | IntegrationService.shared.processQueueNow() |
| 61 | viewModel.refreshWidgetData() | 62 | viewModel.refreshWidgetData() |
| 63 | consumeShareInbox() | ||
| 62 | } | 64 | } |
| 63 | } | 65 | } |
| 66 | |||
| 67 | /// Picks up a domain shared via the share extension and routes it into an | ||
| 68 | /// inspection through the intent router (consumed by `RootTabView`). | ||
| 69 | private func consumeShareInbox() { | ||
| 70 | guard let domain = DomainDigShareInbox.consume() else { return } | ||
| 71 | DomainDigIntentRouter.shared.pendingAction = .inspect(domain) | ||
| 72 | } | ||
| 64 | } | 73 | } |
DomainDig/DomainViewModel.swift +16
| @@ -2834,6 +2834,11 @@ final class DomainViewModel { | |||
| 2834 | customPortScanTask?.cancel() | 2834 | customPortScanTask?.cancel() |
| 2835 | batchTask?.cancel() | 2835 | batchTask?.cancel() |
| 2836 | 2836 | ||
| 2837 | SweepActivityController.shared.begin( | ||
| 2838 | title: source == .watchlistRefresh ? "Watchlist Sweep" : "Batch Lookup", | ||
| 2839 | total: domains.count | ||
| 2840 | ) | ||
| 2841 | |||
| 2837 | batchTask = Task { [weak self] in | 2842 | batchTask = Task { [weak self] in |
| 2838 | guard let self else { return } | 2843 | guard let self else { return } |
| 2839 | self.notificationsAuthorized = await LocalNotificationService.shared.requestAuthorizationIfNeeded() | 2844 | self.notificationsAuthorized = await LocalNotificationService.shared.requestAuthorizationIfNeeded() |
| @@ -2928,6 +2933,11 @@ final class DomainViewModel { | |||
| 2928 | errorMessage: "Lookup cancelled" | 2933 | errorMessage: "Lookup cancelled" |
| 2929 | ) | 2934 | ) |
| 2930 | batchCompletedCount += 1 | 2935 | batchCompletedCount += 1 |
| 2936 | SweepActivityController.shared.update( | ||
| 2937 | completed: batchCompletedCount, | ||
| 2938 | total: batchTotalCount, | ||
| 2939 | currentDomain: batchCurrentDomain | ||
| 2940 | ) | ||
| 2931 | return | 2941 | return |
| 2932 | } | 2942 | } |
| 2933 | 2943 | ||
| @@ -2956,6 +2966,11 @@ final class DomainViewModel { | |||
| 2956 | errorMessage: payload.snapshot.statusMessage | 2966 | errorMessage: payload.snapshot.statusMessage |
| 2957 | ) | 2967 | ) |
| 2958 | batchCompletedCount += 1 | 2968 | batchCompletedCount += 1 |
| 2969 | SweepActivityController.shared.update( | ||
| 2970 | completed: batchCompletedCount, | ||
| 2971 | total: batchTotalCount, | ||
| 2972 | currentDomain: batchCurrentDomain | ||
| 2973 | ) | ||
| 2959 | } | 2974 | } |
| 2960 | 2975 | ||
| 2961 | private func finishBatchLookup(source: BatchLookupSource) { | 2976 | private func finishBatchLookup(source: BatchLookupSource) { |
| @@ -2989,6 +3004,7 @@ final class DomainViewModel { | |||
| 2989 | generatedAt: Date() | 3004 | generatedAt: Date() |
| 2990 | ) | 3005 | ) |
| 2991 | latestBatchSweepSummary = summary | 3006 | latestBatchSweepSummary = summary |
| 3007 | SweepActivityController.shared.end(changed: changedCount, warnings: warningCount) | ||
| 2992 | 3008 | ||
| 2993 | if source == .workflow, let activeWorkflowRunID, let activeWorkflowRunName { | 3009 | if source == .workflow, let activeWorkflowRunID, let activeWorkflowRunName { |
| 2994 | let workflowReports: [DomainReport] = summary.results.compactMap { result in | 3010 | let workflowReports: [DomainReport] = summary.results.compactMap { result in |
DomainDig/Info.plist +2
| @@ -23,6 +23,8 @@ | |||
| 23 | </array> | 23 | </array> |
| 24 | <key>CKSharingSupported</key> | 24 | <key>CKSharingSupported</key> |
| 25 | <true/> | 25 | <true/> |
| 26 | <key>NSSupportsLiveActivities</key> | ||
| 27 | <true/> | ||
| 26 | <key>UIBackgroundModes</key> | 28 | <key>UIBackgroundModes</key> |
| 27 | <array> | 29 | <array> |
| 28 | <string>fetch</string> | 30 | <string>fetch</string> |
DomainDig/LocalNotificationService.swift +59 −6
| @@ -7,8 +7,27 @@ final class LocalNotificationService { | |||
| 7 | 7 | ||
| 8 | private init() {} | 8 | private init() {} |
| 9 | 9 | ||
| 10 | static let domainUserInfoKey = "domain" | ||
| 11 | static let domainCategoryIdentifier = "domain-event" | ||
| 12 | static let reinspectActionIdentifier = "reinspect" | ||
| 13 | |||
| 10 | func configureForegroundPresentation() { | 14 | func configureForegroundPresentation() { |
| 11 | UNUserNotificationCenter.current().delegate = NotificationCenterDelegate.shared | 15 | let center = UNUserNotificationCenter.current() |
| 16 | center.delegate = NotificationCenterDelegate.shared | ||
| 17 | |||
| 18 | let reinspect = UNNotificationAction( | ||
| 19 | identifier: Self.reinspectActionIdentifier, | ||
| 20 | title: "Re-inspect", | ||
| 21 | options: [] | ||
| 22 | ) | ||
| 23 | center.setNotificationCategories([ | ||
| 24 | UNNotificationCategory( | ||
| 25 | identifier: Self.domainCategoryIdentifier, | ||
| 26 | actions: [reinspect], | ||
| 27 | intentIdentifiers: [], | ||
| 28 | options: [] | ||
| 29 | ) | ||
| 30 | ]) | ||
| 12 | } | 31 | } |
| 13 | 32 | ||
| 14 | func requestAuthorizationIfNeeded() async -> Bool { | 33 | func requestAuthorizationIfNeeded() async -> Bool { |
| @@ -44,7 +63,8 @@ final class LocalNotificationService { | |||
| 44 | identifier: "domain-change-\(domain)", | 63 | identifier: "domain-change-\(domain)", |
| 45 | title: domain, | 64 | title: domain, |
| 46 | body: message, | 65 | body: message, |
| 47 | interruptionLevel: severity == .high ? .timeSensitive : .active | 66 | interruptionLevel: severity == .high ? .timeSensitive : .active, |
| 67 | domain: domain | ||
| 48 | ) | 68 | ) |
| 49 | } | 69 | } |
| 50 | 70 | ||
| @@ -53,7 +73,8 @@ final class LocalNotificationService { | |||
| 53 | identifier: "cert-warning-\(domain)", | 73 | identifier: "cert-warning-\(domain)", |
| 54 | title: domain, | 74 | title: domain, |
| 55 | body: "Certificate expires in \(daysRemaining) days", | 75 | body: "Certificate expires in \(daysRemaining) days", |
| 56 | interruptionLevel: .timeSensitive | 76 | interruptionLevel: .timeSensitive, |
| 77 | domain: domain | ||
| 57 | ) | 78 | ) |
| 58 | } | 79 | } |
| 59 | 80 | ||
| @@ -74,7 +95,8 @@ final class LocalNotificationService { | |||
| 74 | identifier: "monitoring-\(domain)-\(UUID().uuidString)", | 95 | identifier: "monitoring-\(domain)-\(UUID().uuidString)", |
| 75 | title: domain, | 96 | title: domain, |
| 76 | body: message, | 97 | body: message, |
| 77 | interruptionLevel: interruptionLevel | 98 | interruptionLevel: interruptionLevel, |
| 99 | domain: domain | ||
| 78 | ) | 100 | ) |
| 79 | } | 101 | } |
| 80 | 102 | ||
| @@ -103,7 +125,8 @@ final class LocalNotificationService { | |||
| 103 | identifier: "monitoring-summary-\(domain)-\(UUID().uuidString)", | 125 | identifier: "monitoring-summary-\(domain)-\(UUID().uuidString)", |
| 104 | title: domain, | 126 | title: domain, |
| 105 | body: body, | 127 | body: body, |
| 106 | interruptionLevel: interruptionLevel | 128 | interruptionLevel: interruptionLevel, |
| 129 | domain: domain | ||
| 107 | ) | 130 | ) |
| 108 | } | 131 | } |
| 109 | 132 | ||
| @@ -127,13 +150,20 @@ final class LocalNotificationService { | |||
| 127 | identifier: String, | 150 | identifier: String, |
| 128 | title: String, | 151 | title: String, |
| 129 | body: String, | 152 | body: String, |
| 130 | interruptionLevel: UNNotificationInterruptionLevel | 153 | interruptionLevel: UNNotificationInterruptionLevel, |
| 154 | domain: String? = nil | ||
| 131 | ) async { | 155 | ) async { |
| 132 | let content = UNMutableNotificationContent() | 156 | let content = UNMutableNotificationContent() |
| 133 | content.title = title | 157 | content.title = title |
| 134 | content.body = body | 158 | content.body = body |
| 135 | content.sound = .default | 159 | content.sound = .default |
| 136 | content.interruptionLevel = interruptionLevel | 160 | content.interruptionLevel = interruptionLevel |
| 161 | if let domain { | ||
| 162 | // Group alerts per domain and let taps/actions route back into it. | ||
| 163 | content.threadIdentifier = domain | ||
| 164 | content.userInfo = [Self.domainUserInfoKey: domain] | ||
| 165 | content.categoryIdentifier = Self.domainCategoryIdentifier | ||
| 166 | } | ||
| 137 | 167 | ||
| 138 | let request = UNNotificationRequest( | 168 | let request = UNNotificationRequest( |
| 139 | identifier: identifier, | 169 | identifier: identifier, |
| @@ -154,4 +184,27 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent | |||
| 154 | ) async -> UNNotificationPresentationOptions { | 184 | ) async -> UNNotificationPresentationOptions { |
| 155 | [.banner, .list, .sound] | 185 | [.banner, .list, .sound] |
| 156 | } | 186 | } |
| 187 | |||
| 188 | func userNotificationCenter( | ||
| 189 | _ center: UNUserNotificationCenter, | ||
| 190 | didReceive response: UNNotificationResponse | ||
| 191 | ) async { | ||
| 192 | let userInfo = response.notification.request.content.userInfo | ||
| 193 | guard let domain = userInfo[LocalNotificationService.domainUserInfoKey] as? String, | ||
| 194 | !domain.isEmpty | ||
| 195 | else { return } | ||
| 196 | |||
| 197 | let action: DomainDigDeepLink.Action | ||
| 198 | switch response.actionIdentifier { | ||
| 199 | case LocalNotificationService.reinspectActionIdentifier: | ||
| 200 | action = .inspect(domain) | ||
| 201 | default: | ||
| 202 | // Default tap: open the tracked domain's detail. | ||
| 203 | action = .detail(domain) | ||
| 204 | } | ||
| 205 | |||
| 206 | await MainActor.run { | ||
| 207 | DomainDigIntentRouter.shared.pendingAction = action | ||
| 208 | } | ||
| 209 | } | ||
| 157 | } | 210 | } |
DomainDig/RootTabView.swift +93 −43
| @@ -1,15 +1,36 @@ | |||
| 1 | import SwiftUI | 1 | import SwiftUI |
| 2 | 2 | ||
| 3 | private enum RootTab: Hashable { | 3 | private enum RootTab: Hashable, CaseIterable { |
| 4 | case dashboard | 4 | case dashboard |
| 5 | case audit | 5 | case audit |
| 6 | case history | 6 | case history |
| 7 | case inspect | 7 | case inspect |
| 8 | case settings | 8 | case settings |
| 9 | |||
| 10 | var title: String { | ||
| 11 | switch self { | ||
| 12 | case .dashboard: return "Dashboard" | ||
| 13 | case .audit: return "Audit" | ||
| 14 | case .history: return "History" | ||
| 15 | case .inspect: return "Inspect" | ||
| 16 | case .settings: return "Settings" | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | var systemImage: String { | ||
| 21 | switch self { | ||
| 22 | case .dashboard: return "square.grid.2x2" | ||
| 23 | case .audit: return "checklist" | ||
| 24 | case .history: return "clock.arrow.trianglehead.counterclockwise.rotate.90" | ||
| 25 | case .inspect: return "magnifyingglass" | ||
| 26 | case .settings: return "gearshape" | ||
| 27 | } | ||
| 28 | } | ||
| 9 | } | 29 | } |
| 10 | 30 | ||
| 11 | struct RootTabView: View { | 31 | struct RootTabView: View { |
| 12 | @Bindable var viewModel: DomainViewModel | 32 | @Bindable var viewModel: DomainViewModel |
| 33 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass | ||
| 13 | @State private var purchaseService = PurchaseService.shared | 34 | @State private var purchaseService = PurchaseService.shared |
| 14 | @State private var intentRouter = DomainDigIntentRouter.shared | 35 | @State private var intentRouter = DomainDigIntentRouter.shared |
| 15 | @State private var detailDomain: TrackedDomain? | 36 | @State private var detailDomain: TrackedDomain? |
| @@ -18,44 +39,12 @@ struct RootTabView: View { | |||
| 18 | var body: some View { | 39 | var body: some View { |
| 19 | let _ = purchaseService.currentTier | 40 | let _ = purchaseService.currentTier |
| 20 | 41 | ||
| 21 | TabView(selection: $selectedTab) { | 42 | Group { |
| 22 | NavigationStack { | 43 | if horizontalSizeClass == .regular { |
| 23 | DashboardView(viewModel: viewModel) | 44 | splitLayout |
| 24 | } | 45 | } else { |
| 25 | .tabItem { | 46 | tabLayout |
| 26 | Label("Dashboard", systemImage: "square.grid.2x2") | ||
| 27 | } | ||
| 28 | .tag(RootTab.dashboard) | ||
| 29 | |||
| 30 | NavigationStack { | ||
| 31 | AuditListView(viewModel: viewModel) | ||
| 32 | } | ||
| 33 | .tabItem { | ||
| 34 | Label("Audit", systemImage: "checklist") | ||
| 35 | } | ||
| 36 | .tag(RootTab.audit) | ||
| 37 | |||
| 38 | NavigationStack { | ||
| 39 | HistoryView(viewModel: viewModel) | ||
| 40 | } | ||
| 41 | .tabItem { | ||
| 42 | Label("History", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90") | ||
| 43 | } | ||
| 44 | .tag(RootTab.history) | ||
| 45 | |||
| 46 | ContentView(viewModel: viewModel) | ||
| 47 | .tabItem { | ||
| 48 | Label("Inspect", systemImage: "magnifyingglass") | ||
| 49 | } | ||
| 50 | .tag(RootTab.inspect) | ||
| 51 | |||
| 52 | NavigationStack { | ||
| 53 | SettingsView(viewModel: viewModel) | ||
| 54 | } | ||
| 55 | .tabItem { | ||
| 56 | Label("Settings", systemImage: "gearshape") | ||
| 57 | } | 47 | } |
| 58 | .tag(RootTab.settings) | ||
| 59 | } | 48 | } |
| 60 | .sheet(isPresented: Binding( | 49 | .sheet(isPresented: Binding( |
| 61 | get: { viewModel.isPaywallPresented }, | 50 | get: { viewModel.isPaywallPresented }, |
| @@ -79,16 +68,16 @@ struct RootTabView: View { | |||
| 79 | } | 68 | } |
| 80 | ) | 69 | ) |
| 81 | } | 70 | } |
| 82 | .onChange(of: purchaseService.currentTier) { _, newValue in | ||
| 83 | if newValue != .free, selectedTab == .inspect, viewModel.trackedDomains.isEmpty == false { | ||
| 84 | selectedTab = .dashboard | ||
| 85 | } | ||
| 86 | } | ||
| 87 | .sheet(item: $detailDomain) { trackedDomain in | 71 | .sheet(item: $detailDomain) { trackedDomain in |
| 88 | NavigationStack { | 72 | NavigationStack { |
| 89 | TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain) | 73 | TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain) |
| 90 | } | 74 | } |
| 91 | } | 75 | } |
| 76 | .onChange(of: purchaseService.currentTier) { _, newValue in | ||
| 77 | if newValue != .free, selectedTab == .inspect, viewModel.trackedDomains.isEmpty == false { | ||
| 78 | selectedTab = .dashboard | ||
| 79 | } | ||
| 80 | } | ||
| 92 | .onOpenURL { url in | 81 | .onOpenURL { url in |
| 93 | guard let action = DomainDigDeepLink.action(from: url) else { return } | 82 | guard let action = DomainDigDeepLink.action(from: url) else { return } |
| 94 | perform(action) | 83 | perform(action) |
| @@ -101,6 +90,67 @@ struct RootTabView: View { | |||
| 101 | } | 90 | } |
| 102 | } | 91 | } |
| 103 | 92 | ||
| 93 | // MARK: Layouts | ||
| 94 | |||
| 95 | /// Compact (iPhone, iPad slide-over): the classic tab bar. | ||
| 96 | private var tabLayout: some View { | ||
| 97 | TabView(selection: $selectedTab) { | ||
| 98 | ForEach(RootTab.allCases, id: \.self) { tab in | ||
| 99 | section(for: tab) | ||
| 100 | .tabItem { | ||
| 101 | Label(tab.title, systemImage: tab.systemImage) | ||
| 102 | } | ||
| 103 | .tag(tab) | ||
| 104 | } | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | /// Regular width (iPad, large iPhone landscape): two-column split view. | ||
| 109 | private var splitLayout: some View { | ||
| 110 | NavigationSplitView { | ||
| 111 | List(RootTab.allCases, id: \.self, selection: sidebarSelection) { tab in | ||
| 112 | Label(tab.title, systemImage: tab.systemImage) | ||
| 113 | .tag(tab) | ||
| 114 | } | ||
| 115 | .navigationTitle("DomainDig") | ||
| 116 | } detail: { | ||
| 117 | section(for: selectedTab) | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | private var sidebarSelection: Binding<RootTab?> { | ||
| 122 | Binding( | ||
| 123 | get: { selectedTab }, | ||
| 124 | set: { selectedTab = $0 ?? selectedTab } | ||
| 125 | ) | ||
| 126 | } | ||
| 127 | |||
| 128 | @ViewBuilder | ||
| 129 | private func section(for tab: RootTab) -> some View { | ||
| 130 | switch tab { | ||
| 131 | case .dashboard: | ||
| 132 | NavigationStack { | ||
| 133 | DashboardView(viewModel: viewModel) | ||
| 134 | } | ||
| 135 | case .audit: | ||
| 136 | NavigationStack { | ||
| 137 | AuditListView(viewModel: viewModel) | ||
| 138 | } | ||
| 139 | case .history: | ||
| 140 | NavigationStack { | ||
| 141 | HistoryView(viewModel: viewModel) | ||
| 142 | } | ||
| 143 | case .inspect: | ||
| 144 | ContentView(viewModel: viewModel) | ||
| 145 | case .settings: | ||
| 146 | NavigationStack { | ||
| 147 | SettingsView(viewModel: viewModel) | ||
| 148 | } | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | // MARK: Intent / deep-link routing | ||
| 153 | |||
| 104 | private func consume(_ action: DomainDigDeepLink.Action?) { | 154 | private func consume(_ action: DomainDigDeepLink.Action?) { |
| 105 | guard let action else { return } | 155 | guard let action else { return } |
| 106 | intentRouter.pendingAction = nil | 156 | intentRouter.pendingAction = nil |
DomainDig/SweepActivityController.swift added +63
| @@ -0,0 +1,63 @@ | |||
| 1 | import ActivityKit | ||
| 2 | import Foundation | ||
| 3 | |||
| 4 | /// Starts, updates, and ends the sweep Live Activity around a batch run. | ||
| 5 | @MainActor | ||
| 6 | final class SweepActivityController { | ||
| 7 | static let shared = SweepActivityController() | ||
| 8 | |||
| 9 | private var activity: Activity<SweepActivityAttributes>? | ||
| 10 | |||
| 11 | private init() {} | ||
| 12 | |||
| 13 | func begin(title: String, total: Int) { | ||
| 14 | guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } | ||
| 15 | // A previous activity that never ended (e.g. app killed mid-sweep) | ||
| 16 | // would otherwise linger; replace it. | ||
| 17 | end(changed: 0, warnings: 0, immediately: true) | ||
| 18 | |||
| 19 | let state = SweepActivityAttributes.ContentState( | ||
| 20 | completed: 0, | ||
| 21 | total: total, | ||
| 22 | currentDomain: nil, | ||
| 23 | changed: 0, | ||
| 24 | warnings: 0 | ||
| 25 | ) | ||
| 26 | activity = try? Activity.request( | ||
| 27 | attributes: SweepActivityAttributes(title: title, startedAt: Date()), | ||
| 28 | content: ActivityContent(state: state, staleDate: nil) | ||
| 29 | ) | ||
| 30 | } | ||
| 31 | |||
| 32 | func update(completed: Int, total: Int, currentDomain: String?) { | ||
| 33 | guard let activity else { return } | ||
| 34 | let state = SweepActivityAttributes.ContentState( | ||
| 35 | completed: completed, | ||
| 36 | total: total, | ||
| 37 | currentDomain: currentDomain, | ||
| 38 | changed: 0, | ||
| 39 | warnings: 0 | ||
| 40 | ) | ||
| 41 | Task { | ||
| 42 | await activity.update(ActivityContent(state: state, staleDate: nil)) | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | func end(changed: Int, warnings: Int, immediately: Bool = false) { | ||
| 47 | guard let activity else { return } | ||
| 48 | self.activity = nil | ||
| 49 | let state = SweepActivityAttributes.ContentState( | ||
| 50 | completed: activity.content.state.total, | ||
| 51 | total: activity.content.state.total, | ||
| 52 | currentDomain: nil, | ||
| 53 | changed: changed, | ||
| 54 | warnings: warnings | ||
| 55 | ) | ||
| 56 | Task { | ||
| 57 | await activity.end( | ||
| 58 | ActivityContent(state: state, staleDate: nil), | ||
| 59 | dismissalPolicy: immediately ? .immediate : .after(Date().addingTimeInterval(60)) | ||
| 60 | ) | ||
| 61 | } | ||
| 62 | } | ||
| 63 | } | ||
DomainDigShareExtension/DomainDigShareExtension.entitlements added +10
| @@ -0,0 +1,10 @@ | |||
| 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.application-groups</key> | ||
| 6 | <array> | ||
| 7 | <string>group.net.cleberg.DomainDig</string> | ||
| 8 | </array> | ||
| 9 | </dict> | ||
| 10 | </plist> | ||
DomainDigShareExtension/Info.plist added +39
| @@ -0,0 +1,39 @@ | |||
| 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>CFBundleDevelopmentRegion</key> | ||
| 6 | <string>$(DEVELOPMENT_LANGUAGE)</string> | ||
| 7 | <key>CFBundleDisplayName</key> | ||
| 8 | <string>Dig Domain</string> | ||
| 9 | <key>CFBundleExecutable</key> | ||
| 10 | <string>$(EXECUTABLE_NAME)</string> | ||
| 11 | <key>CFBundleIdentifier</key> | ||
| 12 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||
| 13 | <key>CFBundleInfoDictionaryVersion</key> | ||
| 14 | <string>6.0</string> | ||
| 15 | <key>CFBundleName</key> | ||
| 16 | <string>$(PRODUCT_NAME)</string> | ||
| 17 | <key>CFBundlePackageType</key> | ||
| 18 | <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> | ||
| 19 | <key>CFBundleShortVersionString</key> | ||
| 20 | <string>$(MARKETING_VERSION)</string> | ||
| 21 | <key>CFBundleVersion</key> | ||
| 22 | <string>$(CURRENT_PROJECT_VERSION)</string> | ||
| 23 | <key>NSExtension</key> | ||
| 24 | <dict> | ||
| 25 | <key>NSExtensionAttributes</key> | ||
| 26 | <dict> | ||
| 27 | <key>NSExtensionActivationRule</key> | ||
| 28 | <dict> | ||
| 29 | <key>NSExtensionActivationSupportsWebURLWithMaxCount</key> | ||
| 30 | <integer>1</integer> | ||
| 31 | </dict> | ||
| 32 | </dict> | ||
| 33 | <key>NSExtensionPointIdentifier</key> | ||
| 34 | <string>com.apple.share-services</string> | ||
| 35 | <key>NSExtensionPrincipalClass</key> | ||
| 36 | <string>$(PRODUCT_MODULE_NAME).ShareViewController</string> | ||
| 37 | </dict> | ||
| 38 | </dict> | ||
| 39 | </plist> | ||
DomainDigShareExtension/ShareViewController.swift added +65
| @@ -0,0 +1,65 @@ | |||
| 1 | import UIKit | ||
| 2 | import UniformTypeIdentifiers | ||
| 3 | |||
| 4 | /// Receives a web URL from the share sheet, extracts its host, and hands it to | ||
| 5 | /// the app through the App Group inbox. Shows a brief confirmation and closes; | ||
| 6 | /// the app picks up the domain the next time it becomes active. | ||
| 7 | final class ShareViewController: UIViewController { | ||
| 8 | private let label = UILabel() | ||
| 9 | |||
| 10 | override func viewDidLoad() { | ||
| 11 | super.viewDidLoad() | ||
| 12 | view.backgroundColor = .systemBackground | ||
| 13 | |||
| 14 | label.text = "Saving…" | ||
| 15 | label.font = .preferredFont(forTextStyle: .headline) | ||
| 16 | label.textAlignment = .center | ||
| 17 | label.numberOfLines = 0 | ||
| 18 | label.translatesAutoresizingMaskIntoConstraints = false | ||
| 19 | view.addSubview(label) | ||
| 20 | NSLayoutConstraint.activate([ | ||
| 21 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor), | ||
| 22 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor), | ||
| 23 | label.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 24), | ||
| 24 | label.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -24) | ||
| 25 | ]) | ||
| 26 | |||
| 27 | extractSharedDomain { [weak self] domain in | ||
| 28 | DispatchQueue.main.async { | ||
| 29 | self?.finish(domain: domain) | ||
| 30 | } | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | private func extractSharedDomain(completion: @escaping (String?) -> Void) { | ||
| 35 | let providers = (extensionContext?.inputItems ?? []) | ||
| 36 | .compactMap { $0 as? NSExtensionItem } | ||
| 37 | .flatMap { $0.attachments ?? [] } | ||
| 38 | |||
| 39 | guard let provider = providers.first(where: { | ||
| 40 | $0.hasItemConformingToTypeIdentifier(UTType.url.identifier) | ||
| 41 | }) else { | ||
| 42 | completion(nil) | ||
| 43 | return | ||
| 44 | } | ||
| 45 | |||
| 46 | provider.loadItem(forTypeIdentifier: UTType.url.identifier) { item, _ in | ||
| 47 | let url = item as? URL ?? (item as? Data).flatMap { URL(dataRepresentation: $0, relativeTo: nil) } | ||
| 48 | let host = url?.host?.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| 49 | completion(host?.isEmpty == false ? host : nil) | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | private func finish(domain: String?) { | ||
| 54 | if let domain { | ||
| 55 | DomainDigShareInbox.write(domain: domain) | ||
| 56 | label.text = "Saved \(domain) — open DomainDig to inspect it." | ||
| 57 | } else { | ||
| 58 | label.text = "No web address found." | ||
| 59 | } | ||
| 60 | |||
| 61 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.4) { [weak self] in | ||
| 62 | self?.extensionContext?.completeRequest(returningItems: nil) | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
DomainDigWidget/DomainDigWidgetBundle.swift +1
| @@ -5,5 +5,6 @@ import WidgetKit | |||
| 5 | struct DomainDigWidgetBundle: WidgetBundle { | 5 | struct DomainDigWidgetBundle: WidgetBundle { |
| 6 | var body: some Widget { | 6 | var body: some Widget { |
| 7 | DomainDigPortfolioWidget() | 7 | DomainDigPortfolioWidget() |
| 8 | SweepLiveActivity() | ||
| 8 | } | 9 | } |
| 9 | } | 10 | } |
DomainDigWidget/SweepLiveActivity.swift added +96
| @@ -0,0 +1,96 @@ | |||
| 1 | import ActivityKit | ||
| 2 | import SwiftUI | ||
| 3 | import WidgetKit | ||
| 4 | |||
| 5 | struct SweepLiveActivity: Widget { | ||
| 6 | var body: some WidgetConfiguration { | ||
| 7 | ActivityConfiguration(for: SweepActivityAttributes.self) { context in | ||
| 8 | // Lock Screen / banner presentation. | ||
| 9 | SweepActivityBannerView(context: context) | ||
| 10 | .padding() | ||
| 11 | .activityBackgroundTint(Color(.systemBackground).opacity(0.85)) | ||
| 12 | } dynamicIsland: { context in | ||
| 13 | DynamicIsland { | ||
| 14 | DynamicIslandExpandedRegion(.leading) { | ||
| 15 | Label(context.attributes.title, systemImage: "arrow.trianglehead.2.clockwise") | ||
| 16 | .font(.caption) | ||
| 17 | .foregroundStyle(.secondary) | ||
| 18 | } | ||
| 19 | DynamicIslandExpandedRegion(.trailing) { | ||
| 20 | Text("\(context.state.completed)/\(context.state.total)") | ||
| 21 | .font(.caption) | ||
| 22 | .monospacedDigit() | ||
| 23 | } | ||
| 24 | DynamicIslandExpandedRegion(.bottom) { | ||
| 25 | VStack(alignment: .leading, spacing: 4) { | ||
| 26 | ProgressView(value: context.state.fractionComplete) | ||
| 27 | .tint(.cyan) | ||
| 28 | if let current = context.state.currentDomain { | ||
| 29 | Text(current) | ||
| 30 | .font(.caption2) | ||
| 31 | .foregroundStyle(.secondary) | ||
| 32 | .lineLimit(1) | ||
| 33 | } | ||
| 34 | } | ||
| 35 | } | ||
| 36 | } compactLeading: { | ||
| 37 | Image(systemName: "arrow.trianglehead.2.clockwise") | ||
| 38 | .foregroundStyle(.cyan) | ||
| 39 | } compactTrailing: { | ||
| 40 | Text("\(context.state.completed)/\(context.state.total)") | ||
| 41 | .font(.caption2) | ||
| 42 | .monospacedDigit() | ||
| 43 | } minimal: { | ||
| 44 | ProgressView(value: context.state.fractionComplete) | ||
| 45 | .progressViewStyle(.circular) | ||
| 46 | .tint(.cyan) | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | private struct SweepActivityBannerView: View { | ||
| 53 | let context: ActivityViewContext<SweepActivityAttributes> | ||
| 54 | |||
| 55 | var body: some View { | ||
| 56 | VStack(alignment: .leading, spacing: 8) { | ||
| 57 | HStack { | ||
| 58 | Label(context.attributes.title, systemImage: "arrow.trianglehead.2.clockwise") | ||
| 59 | .font(.caption) | ||
| 60 | .fontWeight(.semibold) | ||
| 61 | .foregroundStyle(.secondary) | ||
| 62 | Spacer() | ||
| 63 | Text("\(context.state.completed) of \(context.state.total)") | ||
| 64 | .font(.caption) | ||
| 65 | .monospacedDigit() | ||
| 66 | } | ||
| 67 | |||
| 68 | ProgressView(value: context.state.fractionComplete) | ||
| 69 | .tint(.cyan) | ||
| 70 | |||
| 71 | HStack { | ||
| 72 | if let current = context.state.currentDomain { | ||
| 73 | Text(current) | ||
| 74 | .font(.caption2) | ||
| 75 | .foregroundStyle(.secondary) | ||
| 76 | .lineLimit(1) | ||
| 77 | } else { | ||
| 78 | Text(context.state.completed >= context.state.total ? "Finished" : "Working…") | ||
| 79 | .font(.caption2) | ||
| 80 | .foregroundStyle(.secondary) | ||
| 81 | } | ||
| 82 | Spacer() | ||
| 83 | if context.state.changed > 0 { | ||
| 84 | Text("\(context.state.changed) changed") | ||
| 85 | .font(.caption2) | ||
| 86 | .foregroundStyle(.orange) | ||
| 87 | } | ||
| 88 | if context.state.warnings > 0 { | ||
| 89 | Text("\(context.state.warnings) warnings") | ||
| 90 | .font(.caption2) | ||
| 91 | .foregroundStyle(.red) | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
Shared/DomainDigShareInbox.swift added +28
| @@ -0,0 +1,28 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | /// App Group handoff from the share extension to the app. | ||
| 4 | /// | ||
| 5 | /// Share extensions cannot reliably open their host app, so the extension | ||
| 6 | /// drops the shared domain here and the app consumes it the next time it | ||
| 7 | /// becomes active, routing it into an inspection. | ||
| 8 | enum DomainDigShareInbox { | ||
| 9 | private static let key = "pendingInspectDomain" | ||
| 10 | |||
| 11 | private static var defaults: UserDefaults? { | ||
| 12 | UserDefaults(suiteName: DomainDigWidgetStore.appGroupID) | ||
| 13 | } | ||
| 14 | |||
| 15 | static func write(domain: String) { | ||
| 16 | defaults?.set(domain, forKey: key) | ||
| 17 | } | ||
| 18 | |||
| 19 | /// Returns and clears the pending domain, if any. | ||
| 20 | static func consume() -> String? { | ||
| 21 | guard let defaults, | ||
| 22 | let domain = defaults.string(forKey: key), | ||
| 23 | !domain.isEmpty | ||
| 24 | else { return nil } | ||
| 25 | defaults.removeObject(forKey: key) | ||
| 26 | return domain | ||
| 27 | } | ||
| 28 | } | ||
Shared/SweepActivityAttributes.swift added +25
| @@ -0,0 +1,25 @@ | |||
| 1 | import ActivityKit | ||
| 2 | import Foundation | ||
| 3 | |||
| 4 | /// Live Activity contract for an in-flight watchlist sweep or batch lookup. | ||
| 5 | /// | ||
| 6 | /// Lives in `Shared/` because the app starts/updates the activity and the | ||
| 7 | /// widget extension renders it. | ||
| 8 | struct SweepActivityAttributes: ActivityAttributes { | ||
| 9 | struct ContentState: Codable, Hashable { | ||
| 10 | var completed: Int | ||
| 11 | var total: Int | ||
| 12 | var currentDomain: String? | ||
| 13 | var changed: Int | ||
| 14 | var warnings: Int | ||
| 15 | |||
| 16 | var fractionComplete: Double { | ||
| 17 | guard total > 0 else { return 0 } | ||
| 18 | return Double(completed) / Double(total) | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | /// User-facing title for the run, e.g. "Watchlist Sweep" or "Batch Lookup". | ||
| 23 | var title: String | ||
| 24 | var startedAt: Date | ||
| 25 | } | ||