Commit b4b94d54cb
Unsigned
Layout: unified · split
DomainDig.xcodeproj/project.pbxproj +164 −18
| @@ -13,8 +13,9 @@ | ||
| 13 | 13 | 8BBFEF0C2F9874AE00E8E144 /* LookupSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */; }; |
| 14 | 14 | 8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; }; |
| 15 | 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 | 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 | 19 | /* End PBXBuildFile section */ |
| 19 | 20 | |
| 20 | 21 | /* Begin PBXContainerItemProxy section */ |
| @@ -25,6 +26,13 @@ | ||
| 25 | 26 | remoteGlobalIDString = 8BDA00010000000000000006; |
| 26 | 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 | 36 | /* End PBXContainerItemProxy section */ |
| 29 | 37 | |
| 30 | 38 | /* Begin PBXCopyFilesBuildPhase section */ |
| @@ -35,6 +43,7 @@ | ||
| 35 | 43 | dstSubfolderSpec = 13; |
| 36 | 44 | files = ( |
| 37 | 45 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */, |
| 46 | 8BDB00010000000000000002 /* DomainDigShareExtension.appex in Embed Foundation Extensions */, | |
| 38 | 47 | ); |
| 39 | 48 | name = "Embed Foundation Extensions"; |
| 40 | 49 | runOnlyForDeploymentPostprocessing = 0; |
| @@ -49,8 +58,9 @@ | ||
| 49 | 58 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LookupSnapshot.swift; sourceTree = "<group>"; }; |
| 50 | 59 | 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIModels.swift; sourceTree = "<group>"; }; |
| 51 | 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 | 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 | 64 | /* End PBXFileReference section */ |
| 55 | 65 | |
| 56 | 66 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| @@ -70,6 +80,13 @@ | ||
| 70 | 80 | ); |
| 71 | 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 | 90 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| 74 | 91 | |
| 75 | 92 | /* Begin PBXFileSystemSynchronizedRootGroup section */ |
| @@ -94,6 +111,14 @@ | ||
| 94 | 111 | path = DomainDigWidget; |
| 95 | 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 | 122 | /* End PBXFileSystemSynchronizedRootGroup section */ |
| 98 | 123 | |
| 99 | 124 | /* Begin PBXFrameworksBuildPhase section */ |
| @@ -111,6 +136,13 @@ | ||
| 111 | 136 | ); |
| 112 | 137 | runOnlyForDeploymentPostprocessing = 0; |
| 113 | 138 | }; |
| 139 | 8BDB00010000000000000008 /* Frameworks */ = { | |
| 140 | isa = PBXFrameworksBuildPhase; | |
| 141 | buildActionMask = 2147483647; | |
| 142 | files = ( | |
| 143 | ); | |
| 144 | runOnlyForDeploymentPostprocessing = 0; | |
| 145 | }; | |
| 114 | 146 | /* End PBXFrameworksBuildPhase section */ |
| 115 | 147 | |
| 116 | 148 | /* Begin PBXGroup section */ |
| @@ -120,6 +152,7 @@ | ||
| 120 | 152 | 8B78006B2F6090E300933221 /* DomainDig */, |
| 121 | 153 | 8BDA0001000000000000000D /* Shared */, |
| 122 | 154 | 8BDA0001000000000000000E /* DomainDigWidget */, |
| 155 | 8BDB0001000000000000000E /* DomainDigShareExtension */, | |
| 123 | 156 | 8B78006A2F6090E300933221 /* Products */, |
| 124 | 157 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, |
| 125 | 158 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, |
| @@ -136,6 +169,7 @@ | ||
| 136 | 169 | children = ( |
| 137 | 170 | 8B7800692F6090E300933221 /* DomainDig.app */, |
| 138 | 171 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, |
| 172 | 8BDB00010000000000000001 /* DomainDigShareExtension.appex */, | |
| 139 | 173 | ); |
| 140 | 174 | name = Products; |
| 141 | 175 | sourceTree = "<group>"; |
| @@ -156,6 +190,7 @@ | ||
| 156 | 190 | ); |
| 157 | 191 | dependencies = ( |
| 158 | 192 | 8BDA00010000000000000004 /* PBXTargetDependency */, |
| 193 | 8BDB00010000000000000004 /* PBXTargetDependency */, | |
| 159 | 194 | ); |
| 160 | 195 | fileSystemSynchronizedGroups = ( |
| 161 | 196 | 8B78006B2F6090E300933221 /* DomainDig */, |
| @@ -191,15 +226,30 @@ | ||
| 191 | 226 | productReference = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; |
| 192 | 227 | productType = "com.apple.product-type.app-extension"; |
| 193 | 228 | }; |
| 194 | /* End PBXNativeTarget section */ | |
| 195 | ||
| 196 | /* Begin PBXTargetDependency section */ | |
| 197 | 8BDA00010000000000000004 /* PBXTargetDependency */ = { | |
| 198 | isa = PBXTargetDependency; | |
| 199 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; | |
| 200 | targetProxy = 8BDA00010000000000000003 /* PBXContainerItemProxy */; | |
| 229 | 8BDB00010000000000000006 /* DomainDigShareExtension */ = { | |
| 230 | isa = PBXNativeTarget; | |
| 231 | buildConfigurationList = 8BDB0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigShareExtension" */; | |
| 232 | buildPhases = ( | |
| 233 | 8BDB00010000000000000007 /* Sources */, | |
| 234 | 8BDB00010000000000000008 /* Frameworks */, | |
| 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 | 254 | /* Begin PBXProject section */ |
| 205 | 255 | 8B7800612F6090E300933221 /* Project object */ = { |
| @@ -220,6 +270,9 @@ | ||
| 220 | 270 | 8BDA00010000000000000006 = { |
| 221 | 271 | CreatedOnToolsVersion = 26.3; |
| 222 | 272 | }; |
| 273 | 8BDB00010000000000000006 = { | |
| 274 | CreatedOnToolsVersion = 26.3; | |
| 275 | }; | |
| 223 | 276 | }; |
| 224 | 277 | }; |
| 225 | 278 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; |
| @@ -238,6 +291,7 @@ | ||
| 238 | 291 | targets = ( |
| 239 | 292 | 8B7800682F6090E300933221 /* DomainDig */, |
| 240 | 293 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, |
| 294 | 8BDB00010000000000000006 /* DomainDigShareExtension */, | |
| 241 | 295 | ); |
| 242 | 296 | }; |
| 243 | 297 | /* End PBXProject section */ |
| @@ -257,6 +311,13 @@ | ||
| 257 | 311 | ); |
| 258 | 312 | runOnlyForDeploymentPostprocessing = 0; |
| 259 | 313 | }; |
| 314 | 8BDB00010000000000000009 /* Resources */ = { | |
| 315 | isa = PBXResourcesBuildPhase; | |
| 316 | buildActionMask = 2147483647; | |
| 317 | files = ( | |
| 318 | ); | |
| 319 | runOnlyForDeploymentPostprocessing = 0; | |
| 320 | }; | |
| 260 | 321 | /* End PBXResourcesBuildPhase section */ |
| 261 | 322 | |
| 262 | 323 | /* Begin PBXSourcesBuildPhase section */ |
| @@ -281,8 +342,28 @@ | ||
| 281 | 342 | ); |
| 282 | 343 | runOnlyForDeploymentPostprocessing = 0; |
| 283 | 344 | }; |
| 345 | 8BDB00010000000000000007 /* Sources */ = { | |
| 346 | isa = PBXSourcesBuildPhase; | |
| 347 | buildActionMask = 2147483647; | |
| 348 | files = ( | |
| 349 | ); | |
| 350 | runOnlyForDeploymentPostprocessing = 0; | |
| 351 | }; | |
| 284 | 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 | 367 | /* Begin XCBuildConfiguration section */ |
| 287 | 368 | 8B7800722F6090E400933221 /* Debug */ = { |
| 288 | 369 | isa = XCBuildConfiguration; |
| @@ -414,7 +495,7 @@ | ||
| 414 | 495 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 415 | 496 | CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; |
| 416 | 497 | CODE_SIGN_STYLE = Automatic; |
| 417 | CURRENT_PROJECT_VERSION = 37; | |
| 498 | CURRENT_PROJECT_VERSION = 38; | |
| 418 | 499 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 419 | 500 | ENABLE_PREVIEWS = YES; |
| 420 | 501 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -431,7 +512,7 @@ | ||
| 431 | 512 | "$(inherited)", |
| 432 | 513 | "@executable_path/Frameworks", |
| 433 | 514 | ); |
| 434 | MARKETING_VERSION = 4.5.0; | |
| 515 | MARKETING_VERSION = 4.6.0; | |
| 435 | 516 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; |
| 436 | 517 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 437 | 518 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -451,7 +532,7 @@ | ||
| 451 | 532 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 452 | 533 | CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements; |
| 453 | 534 | CODE_SIGN_STYLE = Automatic; |
| 454 | CURRENT_PROJECT_VERSION = 37; | |
| 535 | CURRENT_PROJECT_VERSION = 38; | |
| 455 | 536 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 456 | 537 | ENABLE_PREVIEWS = YES; |
| 457 | 538 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -468,7 +549,7 @@ | ||
| 468 | 549 | "$(inherited)", |
| 469 | 550 | "@executable_path/Frameworks", |
| 470 | 551 | ); |
| 471 | MARKETING_VERSION = 4.5.0; | |
| 552 | MARKETING_VERSION = 4.6.0; | |
| 472 | 553 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig; |
| 473 | 554 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 474 | 555 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -487,7 +568,7 @@ | ||
| 487 | 568 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 488 | 569 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; |
| 489 | 570 | CODE_SIGN_STYLE = Automatic; |
| 490 | CURRENT_PROJECT_VERSION = 37; | |
| 571 | CURRENT_PROJECT_VERSION = 38; | |
| 491 | 572 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 492 | 573 | GENERATE_INFOPLIST_FILE = NO; |
| 493 | 574 | INFOPLIST_FILE = DomainDigWidget/Info.plist; |
| @@ -497,7 +578,7 @@ | ||
| 497 | 578 | "@executable_path/Frameworks", |
| 498 | 579 | "@executable_path/../../Frameworks", |
| 499 | 580 | ); |
| 500 | MARKETING_VERSION = 4.5.0; | |
| 581 | MARKETING_VERSION = 4.6.0; | |
| 501 | 582 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; |
| 502 | 583 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 503 | 584 | SKIP_INSTALL = YES; |
| @@ -515,7 +596,7 @@ | ||
| 515 | 596 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 516 | 597 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; |
| 517 | 598 | CODE_SIGN_STYLE = Automatic; |
| 518 | CURRENT_PROJECT_VERSION = 37; | |
| 599 | CURRENT_PROJECT_VERSION = 38; | |
| 519 | 600 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 520 | 601 | GENERATE_INFOPLIST_FILE = NO; |
| 521 | 602 | INFOPLIST_FILE = DomainDigWidget/Info.plist; |
| @@ -525,7 +606,7 @@ | ||
| 525 | 606 | "@executable_path/Frameworks", |
| 526 | 607 | "@executable_path/../../Frameworks", |
| 527 | 608 | ); |
| 528 | MARKETING_VERSION = 4.5.0; | |
| 609 | MARKETING_VERSION = 4.6.0; | |
| 529 | 610 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; |
| 530 | 611 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 531 | 612 | SKIP_INSTALL = YES; |
| @@ -537,6 +618,62 @@ | ||
| 537 | 618 | }; |
| 538 | 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 | 677 | /* End XCBuildConfiguration section */ |
| 541 | 678 | |
| 542 | 679 | /* Begin XCConfigurationList section */ |
| @@ -567,6 +704,15 @@ | ||
| 567 | 704 | defaultConfigurationIsVisible = 0; |
| 568 | 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 | 716 | /* End XCConfigurationList section */ |
| 571 | 717 | }; |
| 572 | 718 | rootObject = 8B7800612F6090E300933221 /* Project object */; |
DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist +6 −1
| @@ -9,11 +9,16 @@ | ||
| 9 | 9 | <key>orderHint</key> |
| 10 | 10 | <integer>0</integer> |
| 11 | 11 | </dict> |
| 12 | <key>DomainDigWidgetExtension.xcscheme_^#shared#^_</key> | |
| 12 | <key>DomainDigShareExtension.xcscheme_^#shared#^_</key> | |
| 13 | 13 | <dict> |
| 14 | 14 | <key>orderHint</key> |
| 15 | 15 | <integer>1</integer> |
| 16 | 16 | </dict> |
| 17 | <key>DomainDigWidgetExtension.xcscheme_^#shared#^_</key> | |
| 18 | <dict> | |
| 19 | <key>orderHint</key> | |
| 20 | <integer>2</integer> | |
| 21 | </dict> | |
| 17 | 22 | </dict> |
| 18 | 23 | <key>SuppressBuildableAutocreation</key> |
| 19 | 24 | <dict> |
DomainDig/AppVersion.swift +1 −1
| @@ -2,6 +2,6 @@ import Foundation | ||
| 2 | 2 | |
| 3 | 3 | enum AppVersion { |
| 4 | 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 | 40 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() |
| 41 | 41 | IntegrationService.shared.processQueueNow() |
| 42 | 42 | viewModel.refreshWidgetData() |
| 43 | consumeShareInbox() | |
| 43 | 44 | } |
| 44 | 45 | .onReceive(NotificationCenter.default.publisher(for: .cloudSyncDidApplyChanges)) { _ in |
| 45 | 46 | viewModel.refreshPersistedData() |
| @@ -59,6 +60,14 @@ struct DomainDigApp: App { | ||
| 59 | 60 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() |
| 60 | 61 | IntegrationService.shared.processQueueNow() |
| 61 | 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 | 2834 | customPortScanTask?.cancel() |
| 2835 | 2835 | batchTask?.cancel() |
| 2836 | 2836 | |
| 2837 | SweepActivityController.shared.begin( | |
| 2838 | title: source == .watchlistRefresh ? "Watchlist Sweep" : "Batch Lookup", | |
| 2839 | total: domains.count | |
| 2840 | ) | |
| 2841 | ||
| 2837 | 2842 | batchTask = Task { [weak self] in |
| 2838 | 2843 | guard let self else { return } |
| 2839 | 2844 | self.notificationsAuthorized = await LocalNotificationService.shared.requestAuthorizationIfNeeded() |
| @@ -2928,6 +2933,11 @@ final class DomainViewModel { | ||
| 2928 | 2933 | errorMessage: "Lookup cancelled" |
| 2929 | 2934 | ) |
| 2930 | 2935 | batchCompletedCount += 1 |
| 2936 | SweepActivityController.shared.update( | |
| 2937 | completed: batchCompletedCount, | |
| 2938 | total: batchTotalCount, | |
| 2939 | currentDomain: batchCurrentDomain | |
| 2940 | ) | |
| 2931 | 2941 | return |
| 2932 | 2942 | } |
| 2933 | 2943 | |
| @@ -2956,6 +2966,11 @@ final class DomainViewModel { | ||
| 2956 | 2966 | errorMessage: payload.snapshot.statusMessage |
| 2957 | 2967 | ) |
| 2958 | 2968 | batchCompletedCount += 1 |
| 2969 | SweepActivityController.shared.update( | |
| 2970 | completed: batchCompletedCount, | |
| 2971 | total: batchTotalCount, | |
| 2972 | currentDomain: batchCurrentDomain | |
| 2973 | ) | |
| 2959 | 2974 | } |
| 2960 | 2975 | |
| 2961 | 2976 | private func finishBatchLookup(source: BatchLookupSource) { |
| @@ -2989,6 +3004,7 @@ final class DomainViewModel { | ||
| 2989 | 3004 | generatedAt: Date() |
| 2990 | 3005 | ) |
| 2991 | 3006 | latestBatchSweepSummary = summary |
| 3007 | SweepActivityController.shared.end(changed: changedCount, warnings: warningCount) | |
| 2992 | 3008 | |
| 2993 | 3009 | if source == .workflow, let activeWorkflowRunID, let activeWorkflowRunName { |
| 2994 | 3010 | let workflowReports: [DomainReport] = summary.results.compactMap { result in |
DomainDig/Info.plist +2
| @@ -23,6 +23,8 @@ | ||
| 23 | 23 | </array> |
| 24 | 24 | <key>CKSharingSupported</key> |
| 25 | 25 | <true/> |
| 26 | <key>NSSupportsLiveActivities</key> | |
| 27 | <true/> | |
| 26 | 28 | <key>UIBackgroundModes</key> |
| 27 | 29 | <array> |
| 28 | 30 | <string>fetch</string> |
DomainDig/LocalNotificationService.swift +59 −6
| @@ -7,8 +7,27 @@ final class LocalNotificationService { | ||
| 7 | 7 | |
| 8 | 8 | private init() {} |
| 9 | 9 | |
| 10 | static let domainUserInfoKey = "domain" | |
| 11 | static let domainCategoryIdentifier = "domain-event" | |
| 12 | static let reinspectActionIdentifier = "reinspect" | |
| 13 | ||
| 10 | 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 | 33 | func requestAuthorizationIfNeeded() async -> Bool { |
| @@ -44,7 +63,8 @@ final class LocalNotificationService { | ||
| 44 | 63 | identifier: "domain-change-\(domain)", |
| 45 | 64 | title: domain, |
| 46 | 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 | 73 | identifier: "cert-warning-\(domain)", |
| 54 | 74 | title: domain, |
| 55 | 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 | 95 | identifier: "monitoring-\(domain)-\(UUID().uuidString)", |
| 75 | 96 | title: domain, |
| 76 | 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 | 125 | identifier: "monitoring-summary-\(domain)-\(UUID().uuidString)", |
| 104 | 126 | title: domain, |
| 105 | 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 | 150 | identifier: String, |
| 128 | 151 | title: String, |
| 129 | 152 | body: String, |
| 130 | interruptionLevel: UNNotificationInterruptionLevel | |
| 153 | interruptionLevel: UNNotificationInterruptionLevel, | |
| 154 | domain: String? = nil | |
| 131 | 155 | ) async { |
| 132 | 156 | let content = UNMutableNotificationContent() |
| 133 | 157 | content.title = title |
| 134 | 158 | content.body = body |
| 135 | 159 | content.sound = .default |
| 136 | 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 | 168 | let request = UNNotificationRequest( |
| 139 | 169 | identifier: identifier, |
| @@ -154,4 +184,27 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent | ||
| 154 | 184 | ) async -> UNNotificationPresentationOptions { |
| 155 | 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 | 1 | import SwiftUI |
| 2 | 2 | |
| 3 | private enum RootTab: Hashable { | |
| 3 | private enum RootTab: Hashable, CaseIterable { | |
| 4 | 4 | case dashboard |
| 5 | 5 | case audit |
| 6 | 6 | case history |
| 7 | 7 | case inspect |
| 8 | 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 | 31 | struct RootTabView: View { |
| 12 | 32 | @Bindable var viewModel: DomainViewModel |
| 33 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass | |
| 13 | 34 | @State private var purchaseService = PurchaseService.shared |
| 14 | 35 | @State private var intentRouter = DomainDigIntentRouter.shared |
| 15 | 36 | @State private var detailDomain: TrackedDomain? |
| @@ -18,44 +39,12 @@ struct RootTabView: View { | ||
| 18 | 39 | var body: some View { |
| 19 | 40 | let _ = purchaseService.currentTier |
| 20 | 41 | |
| 21 | TabView(selection: $selectedTab) { | |
| 22 | NavigationStack { | |
| 23 | DashboardView(viewModel: viewModel) | |
| 24 | } | |
| 25 | .tabItem { | |
| 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") | |
| 42 | Group { | |
| 43 | if horizontalSizeClass == .regular { | |
| 44 | splitLayout | |
| 45 | } else { | |
| 46 | tabLayout | |
| 57 | 47 | } |
| 58 | .tag(RootTab.settings) | |
| 59 | 48 | } |
| 60 | 49 | .sheet(isPresented: Binding( |
| 61 | 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 | 71 | .sheet(item: $detailDomain) { trackedDomain in |
| 88 | 72 | NavigationStack { |
| 89 | 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 | 81 | .onOpenURL { url in |
| 93 | 82 | guard let action = DomainDigDeepLink.action(from: url) else { return } |
| 94 | 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 | 154 | private func consume(_ action: DomainDigDeepLink.Action?) { |
| 105 | 155 | guard let action else { return } |
| 106 | 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 | 5 | struct DomainDigWidgetBundle: WidgetBundle { |
| 6 | 6 | var body: some Widget { |
| 7 | 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 | } | |