diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 36edd9a106..06e4ce3ba6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2412,6 +2412,26 @@ BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */; }; BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + BD384AC92BBC821A00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; + BD384ACA2BBC821A00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BD384ACB2BBC821B00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; + BD384ACC2BBC821B00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BDA7647C2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA7647D2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */; }; + BDA7648E2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */; }; + BDA764912BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */; }; + BDA764922BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */; }; + BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDADBDC82BD2BC2200421B9B /* Lottie */; }; + BDADBDCB2BD2BC2800421B9B /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDADBDCA2BD2BC2800421B9B /* Lottie */; }; + BDADBDCC2BD2BC4D00421B9B /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; + BDADBDCD2BD2BC5700421B9B /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; + BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; @@ -3904,6 +3924,11 @@ B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; + BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; + BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; + BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = ""; }; + BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatterTests.swift; sourceTree = ""; }; + BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNLocationFormatter.swift; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; @@ -4067,6 +4092,7 @@ 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, + BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */, 7BFCB74E2ADE7E1A00DA3EA7 /* PixelKit in Frameworks */, EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */, EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */, @@ -4082,6 +4108,7 @@ 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + BDADBDCB2BD2BC2800421B9B /* Lottie in Frameworks */, 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */, 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */, @@ -4987,6 +5014,7 @@ isa = PBXGroup; children = ( 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */, + BDE981DB2BBD110800645880 /* Assets */, 4B4D606B2A0B29FA00BCD287 /* Invite */, 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, @@ -5560,11 +5588,13 @@ 4BCF15E32ABB987F0083F6DF /* NetworkProtection */ = { isa = PBXGroup; children = ( + BDA7648F2BC4E56200D0400C /* Mocks */, 7BBE65122BC67EF6008F4EE9 /* Support */, 4BCF15E62ABB98A20083F6DF /* Resources */, 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, + BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */, ); path = NetworkProtection; @@ -7886,6 +7916,23 @@ path = View; sourceTree = ""; }; + BDA7648F2BC4E56200D0400C /* Mocks */ = { + isa = PBXGroup; + children = ( + BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + BDE981DB2BBD110800645880 /* Assets */ = { + isa = PBXGroup; + children = ( + BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */, + BD384AC72BBC821100EF3735 /* vpn-light-mode.json */, + ); + path = Assets; + sourceTree = ""; + }; C13909F22B85FD60001626ED /* Autofill */ = { isa = PBXGroup; children = ( @@ -7957,6 +8004,7 @@ EEA3EEAF2B24EB5100E8333A /* VPNLocation */ = { isa = PBXGroup; children = ( + BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */, EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */, EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */, EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */, @@ -8219,6 +8267,7 @@ 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, EE2F9C5A2B90F2FF00D45FC9 /* Subscription */, + BDADBDC82BD2BC2200421B9B /* Lottie */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8251,6 +8300,7 @@ 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, 4BCBE45B2BA7E18500FC75A1 /* Subscription */, + BDADBDCA2BD2BC2800421B9B /* Lottie */, ); productName = DuckDuckGoAgentAppStore; productReference = 4B2D06692A13318400DE1F49 /* DuckDuckGo VPN App Store.app */; @@ -8708,7 +8758,9 @@ 3706FCE6293F65D500E42796 /* social_images in Resources */, 3706FCE7293F65D500E42796 /* shield-dot-mouse-over.json in Resources */, 3706FCE9293F65D500E42796 /* fb-sdk.js in Resources */, + BD384ACB2BBC821B00EF3735 /* vpn-dark-mode.json in Resources */, 3706FCEA293F65D500E42796 /* PasswordManager.storyboard in Resources */, + BD384ACC2BBC821B00EF3735 /* vpn-light-mode.json in Resources */, 3706FCEB293F65D500E42796 /* dark-flame-mouse-over.json in Resources */, 3706FCEC293F65D500E42796 /* flame-mouse-over.json in Resources */, 3706FCED293F65D500E42796 /* httpsMobileV2Bloom.bin in Resources */, @@ -8770,7 +8822,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */, 7BA7CC482AD11E5C0042E5CE /* Assets.xcassets in Resources */, + BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8778,7 +8832,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + BDADBDCD2BD2BC5700421B9B /* vpn-light-mode.json in Resources */, 7BA7CC472AD11E5C0042E5CE /* Assets.xcassets in Resources */, + BDADBDCC2BD2BC4D00421B9B /* vpn-dark-mode.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8888,7 +8944,9 @@ EA18D1CA272F0DC8006DC101 /* social_images in Resources */, AA7EB6E927E880A600036718 /* shield-dot-mouse-over.json in Resources */, EAC80DE0271F6C0100BBF02D /* fb-sdk.js in Resources */, + BD384AC92BBC821A00EF3735 /* vpn-dark-mode.json in Resources */, 85625994269C8F9600EE44BC /* PasswordManager.storyboard in Resources */, + BD384ACA2BBC821A00EF3735 /* vpn-light-mode.json in Resources */, AA7EB6E327E7D05500036718 /* dark-flame-mouse-over.json in Resources */, AA7EB6E227E7D05500036718 /* flame-mouse-over.json in Resources */, 4B677433255DBEB800025BD8 /* httpsMobileV2Bloom.bin in Resources */, @@ -9710,6 +9768,7 @@ 3706FBCB293F65D500E42796 /* BookmarkHTMLImporter.swift in Sources */, 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */, 987799F72999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, + BDA7647D2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, @@ -10192,6 +10251,7 @@ 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */, 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, + BDA764922BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, 56D145E929E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */, @@ -10288,6 +10348,7 @@ B603975229C1FFAD00902A34 /* ExpectedNavigationExtension.swift in Sources */, 3706FE85293F661700E42796 /* BWRequestTests.swift in Sources */, 3706FE86293F661700E42796 /* FileDownloadManagerTests.swift in Sources */, + BDA7648E2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10415,8 +10476,10 @@ 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, + BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, + BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 7BA7CC442AD11E490042E5CE /* UserText.swift in Sources */, 4BF0E5142AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, @@ -10449,8 +10512,10 @@ 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, + BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, + BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, @@ -11242,6 +11307,7 @@ 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, + BDA7647C2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, @@ -11444,6 +11510,7 @@ 4B59CC8C290083240058F2F6 /* ConnectBitwardenViewModelTests.swift in Sources */, B68412202B6A30680092F66A /* StringExtensionTests.swift in Sources */, B6106BB526A809E60013B453 /* GeolocationProviderTests.swift in Sources */, + BDA764912BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, B6E6BA232BA2EDDE008AA7E1 /* FileReadResult.swift in Sources */, B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */, B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, @@ -11524,6 +11591,7 @@ 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */, 4B723E0626B0003E00E14D75 /* CSVParserTests.swift in Sources */, B60C6F8429B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, + BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */, B626A7642992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, @@ -12578,7 +12646,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 137.0.0; + version = 138.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13170,6 +13238,16 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Configuration; }; + BDADBDC82BD2BC2200421B9B /* Lottie */ = { + isa = XCSwiftPackageProductDependency; + package = 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */; + productName = Lottie; + }; + BDADBDCA2BD2BC2800421B9B /* Lottie */ = { + isa = XCSwiftPackageProductDependency; + package = 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */; + productName = Lottie; + }; CBC83E3529B63D380008E19C /* Configuration */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 44d670ba46..462a0cc553 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "4ce049682cb47a9fb510237070666e5e8bf1e07b", - "version" : "137.0.0" + "revision" : "b8f0e5db431c63943b509d522c157f870ef03ae0", + "version" : "138.0.0" } }, { diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index bdc9fe1e61..003cf5500b 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -147,7 +147,7 @@ final class URLEventHandler { case AppLaunchCommand.shareFeedback.launchURL: WindowControllersManager.shared.showShareFeedbackModal() case AppLaunchCommand.justOpen.launchURL: - WindowControllersManager.shared.showNewWindow() + WindowControllersManager.shared.showMainWindow() case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 2a5ec04f2d..4dd17342ea 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -42,8 +42,8 @@ extension UserText { static let networkProtectionInviteSuccessMessage = "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere." // MARK: - Navigation Bar Status View - // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Send VPN Feedback' in the VPN status view that's shown in the navigation bar - static let networkProtectionNavBarStatusViewShareFeedback = "Send VPN Feedback…" + // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Share VPN Feedback' in the VPN status view that's shown in the navigation bar + static let networkProtectionNavBarStatusViewShareFeedback = "Share VPN Feedback…" // "network.protection.status.menu.vpn.settings" - The status menu 'VPN Settings' menu item static let networkProtectionNavBarStatusMenuVPNSettings = "VPN Settings…" // "network.protection.status.menu.faq" - The status menu 'FAQ' menu item @@ -299,7 +299,7 @@ extension UserText { // "vpn.location.description.nearest" - Nearest city setting description static let vpnLocationNearest = "Nearest" // "vpn.location.description.nearest.available" - Nearest available location setting description - static let vpnLocationNearestAvailable = "Nearest available" + static let vpnLocationNearestAvailable = "Nearest Location" // "vpn.location.nearest.available.title" - Subtitle underneath the nearest available vpn location preference text. static let vpnLocationNearestAvailableSubtitle = "Automatically connect to the nearest server we can find." @@ -330,6 +330,15 @@ extension UserText { static let uninstallVPNAlertTitle = "Are you sure you want to uninstall the VPN?" // "vpn.uninstall.alert.informative.text" - Informative text for the alert that comes up when the user decides to uninstall our VPN static let uninstallVPNInformativeText = "Uninstalling the DuckDuckGo VPN will disconnect the VPN and remove it from your device." + + // MARK: - VPN Screen + // "network.protection.vpn.location.nearest" - Description of the location type in the VPN status view + static let netPVPNLocationNearest = "(Nearest)" + + // "network.protection.vpn.location.subtitle.formatted.city.and.country" - Subtitle for the preferred location item that formats a city and country. E.g Chicago, United States + static func netPVPNSettingsLocationSubtitleFormattedCityAndCountry(city: String, country: String) -> String { + return "\(city), \(country)" + } } #if DBP diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 7862fce068..aab0bb6f42 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -93,7 +93,8 @@ final class MainViewController: NSViewController { serverInfoObserver: ipcClient.ipcServerInfoObserver, connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: connectivityIssuesObserver, - controllerErrorMessageObserver: controllerErrorMessageObserver + controllerErrorMessageObserver: controllerErrorMessageObserver, + dataVolumeObserver: ipcClient.ipcDataVolumeObserver ) }() diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index 211dcc460d..2e4fdacb8d 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -64,6 +64,12 @@ final class IPCClientMock: NetworkProtectionIPCClient { } var ipcControllerErrorMessageObserver: any NetworkProtection.ControllerErrorMesssageObserver = ControllerErrorMesssageObserverMock() + final class DataVolumeObserverMock: NetworkProtection.DataVolumeObserver { + var publisher: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + var recentValue: DataVolume = .init() + } + var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver = DataVolumeObserverMock() + func start(completion: @escaping (Error?) -> Void) { completion(nil) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Assets/vpn-dark-mode.json b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Assets/vpn-dark-mode.json new file mode 100644 index 0000000000..2b97395d0e --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Assets/vpn-dark-mode.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":370,"w":128,"h":96,"nm":"Comp 1","ddd":0,"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"shadow","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[42.2357292175293,8.99384593963623],[41.2205810546875,0],[53.23579788208008,24.99367523193359],[21.23579597473145,56.99367141723633],[0,48.93241500854492],[2.235732555389404,48.99384307861328],[42.2357292175293,8.99384593963623]],"i":[[0,0],[0.664215087890625,2.89088249206543],[0,-10.11254501342773],[17.6731128692627,0],[5.649600028991699,5.015373229980469],[-0.750247597694397,0],[0,22.09139251708984]],"o":[[0,-3.092463970184326],[7.324420928955078,5.864182472229004],[0,17.6731128692627],[-8.148801803588867,0],[0.7400614619255066,0.040771484375],[22.09139251708984,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.16862745098039217,0.3333333333333333,0.792156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[69.3820571899414,35.50293731689453],"ix":2},"a":{"a":0,"k":[26.61789894104004,28.49683570861816],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":40,"s":[40],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[50],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"dot-nw","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[7,7],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[46.5,13.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[50.5,6.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-14.74848774275709,-23.38554600445718],"to":[-12.74848774275709,16.61445399554282]},{"t":80,"s":[53.5,66.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":81,"s":[57.5,-2.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":200,"s":[57.5,-2.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-20.75,-24.25],"to":[-20.75,23.08333333333333]},{"t":370,"s":[56.5,68.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"dot-ne","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[7,7],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[69.5,21.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[63.5,17.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-6.951289398280807,-10.83253740846864],"to":[9.048710601719193,4.500795924864676]},{"t":80,"s":[87.5,40.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[26.75000000000003,-1.75],"to":[-10.58333333333334,-19.75]},{"t":81,"s":[31.5,13.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":150,"s":[31.5,13.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-9.750000000000028,-19.41666666666666],"to":[27.58333333333334,-1.416666666666664]},{"t":300,"s":[87.5,40.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"dot-s","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[7,7],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[59.5,49.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[69.5,50.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[10.97134670487107,11.24832855778413],"to":[-17.69531996179562,-0.08500477554918007]},{"t":80,"s":[26.5,31.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-20.749999999999986,-1.75],"to":[10.58333333333333,11.58333333333333]},{"t":81,"s":[73.5,52.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":130,"s":[73.5,50.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[12.58333333333336,13.250000000000014],"to":[-18.75,0.5833333333333144]},{"t":230,"s":[26.5,32.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[10056,10032.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[17,30.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-34,-13.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[9,-30.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-13,28.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[34,0.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"globe","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[32,63.99999999999999],[0,32],[32,0],[63.99999999999999,32],[32,63.99999999999999]],"i":[[0,0],[0,17.67311330810018],[-17.67311330810018,0],[0,-17.67311330810018],[17.67311330810018,0]],"o":[[-17.67311330810018,0],[0,-17.67311330810018],[17.67311330810018,0],[0,17.67311330810018],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.8,0.8,0.8],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.3333333333333333,0.4980392156862745,0.9529411764705882],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[64,32],"ix":2},"a":{"a":0,"k":[32,32],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0}]},{"id":"1","layers":[{"ddd":0,"ind":12,"ty":4,"nm":"_mask","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"td":1,"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[55,35],[35,55],[35,61.1272168529097],[32.27027277742516,63.99888239316815],[32,64],[0,32],[32,0],[64,32],[63.99888239502384,32.27027632020794],[61.12722036453148,35],[55,35]],"i":[[0,0],[1.3527074874795e-15,-11.04569435119629],[0,-2.603342616466307],[1.549197989173602,-0.01281636670575817],[0.09017852706661245,0],[-2.164332234077084e-15,17.6731128692627],[-17.6731128692627,-2.164332234077084e-15],[2.164332445835321e-15,-17.67311477661133],[0.0007445845155729103,-0.09000398946863442],[1.549249531796702,0],[0,0]],"o":[[-11.04569244384766,-1.352707275721263e-15],[0,0],[0,1.549251002554159],[-0.09000283819779753,0.0007445848671139288],[-17.6731128692627,-2.164332234077084e-15],[2.164332022318847e-15,-17.67311096191406],[17.67311096191406,2.164332022318847e-15],[0,0.09017973898513532],[-0.01281618456122568,1.549196519872671],[-2.603342959208661,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[64,32],"ix":2},"a":{"a":0,"k":[32,32],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"refId":"0","w":20000,"h":20000,"ind":13,"ty":0,"nm":"globe","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"tt":1}]}],"layers":[{"ddd":0,"ind":14,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[64,48],"ix":2},"a":{"a":0,"k":[64,35],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":1,"k":[{"t":10,"s":[87,54],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":30,"s":[87,48],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":40,"s":[87,58],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":50,"s":[87,54],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[87,54],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":14},{"ddd":0,"ind":16,"ty":4,"nm":"lock-hook","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":15,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,9],[0,4.048697948455811],[1.171572897169325,1.185836362838745],[4,2.333394250525001e-14],[6.828427208794487,1.185835719108582],[8,4.048697519302368],[8,8.46090259552002]],"i":[[0,0],[0,0],[-0.7501454883151584,0.7592780649662018],[-1.060865932040744,1.151999967419215e-7],[-0.7501456472608777,-0.7592779576778412],[-3.203726414034867e-7,-1.073781502246857],[0,0]],"o":[[0,0],[9.274384638047195e-8,-1.073781502246857],[0.7501454883151584,-0.7592780649662018],[1.060865932040744,-1.151999967419215e-7],[0.7501456472608777,0.7592779576778412],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.06666666666666667,0.06666666666666667,0.06666666666666667],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.666666746139526,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":30,"s":[85],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[87,50.5],"ix":2},"a":{"a":0,"k":[4,4.5],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":1,"k":[{"t":30,"s":[196],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":-180,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"lock-base","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":15,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[2.000000000000001,0],[14,0],[16,2.000000000000002],[16,7.333333015441899],[14,9.333333015441895],[1.999999999999996,9.333333015441895],[0,7.333333015441892],[0,1.999999999999995],[2.000000000000001,0]],"i":[[0,0],[-3.074004826695607,0],[0,-1.104569499661587],[0,-1.574570024376729],[1.104569499661587,0],[3.074004826695605,0],[0,1.104569499661587],[0,1.574570024376728],[-1.104569499661587,0]],"o":[[3.074004826695607,0],[1.104569499661587,0],[0,1.57457002437673],[0,1.104569499661587],[-3.074004826695608,0],[-1.104569499661587,0],[0,-1.57457002437673],[0,-1.104569499661587],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[0.06666666666666667,0.06666666666666667,0.06666666666666667],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[87,58.66666793823242],"ix":2},"a":{"a":0,"k":[8,4.666666507720947],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":180,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":-180,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"bg","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":15,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[32,16],[16,32],[0,16],[16,0],[32,16]],"i":[[0,0],[8.836555727066532,0],[0,8.836555727066532],[-8.836555727066532,0],[0,-8.836555727066532]],"o":[[0,8.836555727066532],[-8.836555727066532,0],[0,-8.836555727066532],[8.836555727066532,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":30,"s":[0.9764705882352941,0.7450980392156863,0.10196078431372549],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[0.2980392156862745,0.7294117647058823,0.23529411764705882],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[87,54],"ix":2},"a":{"a":0,"k":[16,16],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"refId":"1","w":20000,"h":20000,"ind":13,"ty":0,"nm":"globe","sr":1,"ks":{"p":{"a":0,"k":[64,32],"ix":2},"a":{"a":0,"k":[10064,10032],"ix":2},"s":{"a":1,"k":[{"t":40,"s":[100,100],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":60,"s":[85,85],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":70,"s":[105,105],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":80,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":14},{"ddd":0,"ind":19,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[64,38.42728805541992],"ix":2},"a":{"a":0,"k":[64,38.42728805541992],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":14},{"ddd":0,"ind":20,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[108.5400009155273,55],"ix":2},"a":{"a":0,"k":[108.5400009155273,55.00000190734863],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":19},{"ddd":0,"ind":21,"ty":4,"nm":"bling-N","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":20,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,0],[3.329999923706055,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.8,0.8,0.8],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.5,"ix":2},"lc":2,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":40,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":47,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":174,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":176,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":183,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[107.4273300170898,45.32266616821289],"ix":2},"a":{"a":0,"k":[1.664999961853027,0],"ix":2},"s":{"a":0,"k":[99.9999982885729,99.99999828857288],"ix":2},"r":{"a":0,"k":-45,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"bling","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":20,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,0],[3.329999923706055,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.8,0.8,0.8],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.5,"ix":2},"lc":2,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":47,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":53,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":175,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":176,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":183,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":189,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[109.1650009155273,55],"ix":2},"a":{"a":0,"k":[1.664999961853027,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"bling-S","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":20,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,0],[3.329999923706055,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.8,0.8,0.8],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.5,"ix":2},"lc":2,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":53,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":60,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":175,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":176,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":189,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":190,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":196,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[107.4273300170898,64.67733001708984],"ix":2},"a":{"a":0,"k":[1.664999961853027,0],"ix":2},"s":{"a":0,"k":[99.9999982885729,99.99999828857288],"ix":2},"r":{"a":0,"k":45,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[64,45.49990844726562],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":19},{"ddd":0,"ind":25,"ty":4,"nm":"dot-L","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[2,4],[0,2],[2,0],[4,2],[2,4]],"i":[[0,0],[0,1.100000023841858],[-1.100000023841858,0],[0,-1.100000023841858],[1.100000023841858,0]],"o":[[-1.100000023841858,0],[0,-1.100000023841858],[1.100000023841858,0],[0,1.100000023841858],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.10196078431372549],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[-1,-15.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[-41,6.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[2,2],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"dot-R","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1.5,3],[0,1.5],[1.5,0],[3,1.5],[1.5,3]],"i":[[0,0],[0,0.8250000178813934],[-0.8250000178813934,0],[0,-0.8250000178813934],[0.8250000178813934,0]],"o":[[-0.8250000178813934,0],[0,-0.8250000178813934],[0.8250000178813934,0],[0,0.8250000178813934],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.10196078431372549],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[3.5,-14],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[62.5,-11],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[62.5,-11],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[1.5,1.5],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":4,"nm":"spark-L","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[7.371442522321428,3.199999128069196],[8,4],[7.371442522321428,4.800000871930803],[5.828578404017857,5.199999128069196],[5.199986049107142,5.828569684709821],[4.800013950892857,7.371429443359375],[4,8],[3.199986049107143,7.371429443359375],[2.800013950892857,5.828569684709821],[2.171421595982143,5.199999128069196],[0.6285574776785714,4.800000871930803],[0,4],[0.6285574776785714,3.199999128069196],[2.171421595982143,2.800000871930803],[2.800013950892857,2.171430315290178],[3.199986049107143,0.628570556640625],[4,0],[4.800013950892857,0.628570556640625],[5.199986049107142,2.171430315290178],[5.828578404017857,2.800000871930803],[7.371442522321428,3.199999128069196]],"i":[[0,0],[0,-0.3999999931880406],[0.3999999931880406,-0.1142857159887041],[0,0],[0.05714285799435206,-0.2857142857142857],[0,0],[0.3999999931880406,0],[0.1142857159887041,0.3999999931880406],[0,0],[0.2857142857142857,0.05714285799435206],[0,0],[0,0.3999999931880406],[-0.3999999931880406,0.1142857159887041],[0,0],[-0.05714285799435206,0.2857142857142857],[0,0],[-0.3999999931880406,0],[-0.1142857159887041,-0.3999999931880406],[0,0],[-0.2857142857142857,-0.05714285799435206],[0,0]],"o":[[0.3428571564810616,0.05714285799435206],[0,0.3428571564810616],[0,0],[-0.2857142857142857,0.05714285799435206],[0,0],[-0.05714285799435206,0.3428571564810616],[-0.3428571564810616,0],[0,0],[-0.05714285799435206,-0.2857142857142857],[0,0],[-0.3428571564810616,-0.05714285799435206],[0,-0.3428571564810616],[0,0],[0.2857142857142857,-0.05714285799435206],[0,0],[0.05714285799435206,-0.3428571564810616],[0.3428571564810616,0],[0,0],[0.05714285799435206,0.2857142857142857],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.10196078431372549],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[-1,-16.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[-57,-7.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[4,4],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":1,"k":[{"t":40,"s":[360],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":130,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":370,"s":[-180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"spark-Big","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[11.05716378348214,4.799997929164549],[12,5.999999046325684],[11.05716378348214,7.200000163486817],[8.742867606026785,7.799997452327391],[7.799979073660714,8.74285313742543],[7.200020926339286,11.05714240755333],[6,11.99999809265137],[4.799979073660714,11.05714240755333],[4.200020926339286,8.74285313742543],[3.257132393973214,7.799997452327391],[0.9428362165178571,7.200000163486817],[0,5.999999046325684],[0.9428362165178571,4.799997929164549],[3.257132393973214,4.200000640323976],[4.200020926339286,3.257144955225937],[4.799979073660714,0.9428556850980385],[6,0],[7.200020926339286,0.9428556850980385],[7.799979073660714,3.257144955225937],[8.742867606026785,4.200000640323976],[11.05716378348214,4.799997929164549]],"i":[[0,0],[0,-0.5999998944146309],[0.5999999897820608,-0.1714285467352182],[0,0],[0.0857142869915281,-0.4285713604518345],[0,0],[0.5999999897820608,0],[0.1714285739830562,0.5999998944146309],[0,0],[0.4285714285714285,0.08571427336760909],[0,0],[0,0.5999998944146309],[-0.5999999897820608,0.1714285467352182],[0,0],[-0.0857142869915281,0.4285713604518345],[0,0],[-0.5999999897820608,0],[-0.1714285739830562,-0.5999998944146309],[0,0],[-0.4285714285714285,-0.08571427336760909],[0,0]],"o":[[0.5142857347215924,0.08571427336760909],[0,0.5142856529780764],[0,0],[-0.4285714285714285,0.08571427336760909],[0,0],[-0.0857142869915281,0.5142856529780764],[-0.5142857347215924,0],[0,0],[-0.0857142869915281,-0.4285713604518345],[0,0],[-0.5142857347215924,-0.08571427336760909],[0,-0.5142856529780764],[0,0],[0.4285714285714285,-0.08571427336760909],[0,0],[0.0857142869915281,-0.5142856529780764],[0.5142857347215924,0],[0,0],[0.0857142869915281,0.4285713604518345],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.10196078431372549],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[-1,-15.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[-43,-28.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[-43,-28.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[6,5.999999046325684],"ix":2},"s":{"a":1,"k":[{"t":130,"s":[100,-100],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":250,"s":[50,-50],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":370,"s":[100,-100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":40,"s":[360],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"spark-R","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[7.371442794799805,3.199999173482259],[8,4],[7.371442794799805,4.80000082651774],[5.828578313191731,5.199999173482259],[5.199986139933268,5.828570048014322],[4.800013860066731,7.371429443359375],[4,8],[3.199986139933268,7.371429443359375],[2.800013860066731,5.828570048014322],[2.171421686808268,5.199999173482259],[0.6285574833552042,4.80000082651774],[0,4],[0.6285574833552042,3.199999173482259],[2.171421686808268,2.800000826517741],[2.800013860066731,2.171430269877116],[3.199986139933268,0.628570556640625],[4,0],[4.800013860066731,0.628570556640625],[5.199986139933268,2.171430269877116],[5.828578313191731,2.800000826517741],[7.371442794799805,3.199999173482259]],"i":[[0,0],[0,-0.4000000158945719],[0.4000000158945719,-0.1142857174078623],[0,0],[0.05714285870393117,-0.2857142885526021],[0,0],[0.4000000158945719,0],[0.1142857174078623,0.4000000158945719],[0,0],[0.2857142885526021,0.05714285870393117],[0,0],[0,0.4000000158945719],[-0.4000000158945719,0.1142857174078623],[0,0],[-0.05714285870393117,0.2857142885526021],[0,0],[-0.4000000158945719,0],[-0.1142857174078623,-0.4000000158945719],[0,0],[-0.2857142885526021,-0.05714285870393117],[0,0]],"o":[[0.3428571621576945,0.05714285870393117],[0,0.3428571621576945],[0,0],[-0.2857142885526021,0.05714285870393117],[0,0],[-0.05714285870393117,0.3428571621576945],[-0.3428571621576945,0],[0,0],[-0.05714285870393117,-0.2857142885526021],[0,0],[-0.3428571621576945,-0.05714285870393117],[0,-0.3428571621576945],[0,0],[0.2857142885526021,-0.05714285870393117],[0,0],[0.05714285870393117,-0.3428571621576945],[0.3428571621576945,0],[0,0],[0.05714285870393117,0.2857142885526021],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.10196078431372549],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[2,-14.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[45,-22.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[45,-22.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[4,4],"ix":2},"s":{"a":1,"k":[{"t":130,"s":[100,-100],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":250,"s":[250,-250],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":370,"s":[100,-100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":40,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Assets/vpn-light-mode.json b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Assets/vpn-light-mode.json new file mode 100644 index 0000000000..fc062b9223 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Assets/vpn-light-mode.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":370,"w":128,"h":96,"nm":"Comp1","ddd":0,"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"shadow","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[42.2357292175293,8.99384593963623],[41.2205810546875,0],[53.23579788208008,24.99367523193359],[21.23579597473145,56.99367141723633],[0,48.93241500854492],[2.235732555389404,48.99384307861328],[42.2357292175293,8.99384593963623]],"i":[[0,0],[0.664215087890625,2.89088249206543],[0,-10.11254501342773],[17.6731128692627,0],[5.649600028991699,5.015373229980469],[-0.750247597694397,0],[0,22.09139251708984]],"o":[[0,-3.092463970184326],[7.324420928955078,5.864182472229004],[0,17.6731128692627],[-8.148801803588867,0],[0.7400614619255066,0.040771484375],[22.09139251708984,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.1686274509803922,0.3333333333333333,0.792156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[69.3820571899414,35.50293731689453],"ix":2},"a":{"a":0,"k":[26.61789894104004,28.49683570861816],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":40,"s":[40],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[50],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"dot-nw","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[7,7],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[46.5,13.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[50.5,6.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-14.74848774275709,-23.38554600445718],"to":[-12.74848774275709,16.61445399554282]},{"t":80,"s":[53.5,66.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":81,"s":[57.5,-2.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":200,"s":[57.5,-2.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-20.75,-24.25],"to":[-20.75,23.08333333333333]},{"t":370,"s":[56.5,68.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"dot-ne","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[7,7],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[69.5,21.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[63.5,17.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-6.951289398280807,-10.83253740846864],"to":[9.048710601719193,4.500795924864676]},{"t":80,"s":[87.5,40.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[26.75000000000003,-1.75],"to":[-10.58333333333334,-19.75]},{"t":81,"s":[31.5,13.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":150,"s":[31.5,13.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-9.750000000000028,-19.41666666666666],"to":[27.58333333333334,-1.416666666666664]},{"t":300,"s":[87.5,40.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"dot-s","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[7,7],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[59.5,49.5],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[69.5,50.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[10.97134670487107,11.24832855778413],"to":[-17.69531996179562,-0.08500477554918007]},{"t":80,"s":[26.5,31.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[-20.749999999999986,-1.75],"to":[10.58333333333333,11.58333333333333]},{"t":81,"s":[73.5,52.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":130,"s":[73.5,50.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]},"ti":[12.58333333333336,13.250000000000014],"to":[-18.75,0.5833333333333144]},{"t":230,"s":[26.5,32.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[10056,10032.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[17,30.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-34,-13.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[9,-30.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-13,28.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Oval","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":5,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[48,96],[0,48],[48,0],[96,48],[48,96]],"i":[[0,0],[0,26.50966996215028],[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0]],"o":[[-26.50966996215028,0],[0,-26.50966996215028],[26.50966996215028,0],[0,26.50966996215028],[0,0]]}}},{"ty":"st","c":{"a":1,"k":[{"t":40,"s":[0.9333333333333333,0.9333333333333333,0.9333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.6784313725490196,0.7607843137254902,0.9882352941176471],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[34,0.5],"ix":2},"a":{"a":0,"k":[48,48],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"globe","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[32,63.99999999999999],[0,32],[32,0],[63.99999999999999,32],[32,63.99999999999999]],"i":[[0,0],[0,17.67311330810018],[-17.67311330810018,0],[0,-17.67311330810018],[17.67311330810018,0]],"o":[[-17.67311330810018,0],[0,-17.67311330810018],[17.67311330810018,0],[0,17.67311330810018],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":40,"s":[0.8,0.8,0.8],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":50,"s":[0.3333333333333333,0.4980392156862745,0.9529411764705882],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[64,32],"ix":2},"a":{"a":0,"k":[32,32],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0}]},{"id":"1","layers":[{"ddd":0,"ind":12,"ty":4,"nm":"_mask","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"td":1,"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[55,35],[35,55],[35,61.1272168529097],[32.27027277742516,63.99888239316815],[32,64],[0,32],[32,0],[64,32],[63.99888239502384,32.27027632020794],[61.12722036453148,35],[55,35]],"i":[[0,0],[1.3527074874795e-15,-11.04569435119629],[0,-2.603342616466307],[1.549197989173602,-0.01281636670575817],[0.09017852706661245,0],[-2.164332234077084e-15,17.6731128692627],[-17.6731128692627,-2.164332234077084e-15],[2.164332445835321e-15,-17.67311477661133],[0.0007445845155729103,-0.09000398946863442],[1.549249531796702,0],[0,0]],"o":[[-11.04569244384766,-1.352707275721263e-15],[0,0],[0,1.549251002554159],[-0.09000283819779753,0.0007445848671139288],[-17.6731128692627,-2.164332234077084e-15],[2.164332022318847e-15,-17.67311096191406],[17.67311096191406,2.164332022318847e-15],[0,0.09017973898513532],[-0.01281618456122568,1.549196519872671],[-2.603342959208661,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[64,32],"ix":2},"a":{"a":0,"k":[32,32],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"refId":"0","w":20000,"h":20000,"ind":13,"ty":0,"nm":"globe","sr":1,"ks":{"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"tt":1}]}],"layers":[{"ddd":0,"ind":14,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[64,48],"ix":2},"a":{"a":0,"k":[64,35],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":1,"k":[{"t":10,"s":[87,54],"i":{"x":[0.58],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":30,"s":[87,48],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":40,"s":[87,58],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":50,"s":[87,54],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[87,54],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":14},{"ddd":0,"ind":16,"ty":4,"nm":"lock-hook","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":15,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,9],[0,4.048697948455811],[1.171572897169325,1.185836362838745],[4,2.333394250525001e-14],[6.828427208794487,1.185835719108582],[8,4.048697519302368],[8,8.46090259552002]],"i":[[0,0],[0,0],[-0.7501454883151584,0.7592780649662018],[-1.060865932040744,1.151999967419215e-7],[-0.7501456472608777,-0.7592779576778412],[-3.203726414034867e-7,-1.073781502246857],[0,0]],"o":[[0,0],[9.274384638047195e-8,-1.073781502246857],[0.7501454883151584,-0.7592780649662018],[1.060865932040744,-1.151999967419215e-7],[0.7501456472608777,0.7592779576778412],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.666666746139526,"ix":2},"lc":1,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":30,"s":[85],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[87,50.5],"ix":2},"a":{"a":0,"k":[4,4.5],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":1,"k":[{"t":30,"s":[196],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":-180,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"lock-base","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":15,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[2.000000000000001,0],[14,0],[16,2.000000000000002],[16,7.333333015441899],[14,9.333333015441895],[1.999999999999996,9.333333015441895],[0,7.333333015441892],[0,1.999999999999995],[2.000000000000001,0]],"i":[[0,0],[-3.074004826695607,0],[0,-1.104569499661587],[0,-1.574570024376729],[1.104569499661587,0],[3.074004826695605,0],[0,1.104569499661587],[0,1.574570024376728],[-1.104569499661587,0]],"o":[[3.074004826695607,0],[1.104569499661587,0],[0,1.57457002437673],[0,1.104569499661587],[-3.074004826695608,0],[-1.104569499661587,0],[0,-1.57457002437673],[0,-1.104569499661587],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[87,58.66666793823242],"ix":2},"a":{"a":0,"k":[8,4.666666507720947],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":0,"k":180,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":-180,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"bg","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":15,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[32,16],[16,32],[0,16],[16,0],[32,16]],"i":[[0,0],[8.836555727066532,0],[0,8.836555727066532],[-8.836555727066532,0],[0,-8.836555727066532]],"o":[[0,8.836555727066532],[-8.836555727066532,0],[0,-8.836555727066532],[8.836555727066532,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":30,"s":[0.9764705882352941,0.7450980392156863,0.1019607843137255],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":40,"s":[0.2980392156862745,0.7294117647058823,0.2352941176470588],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[87,54],"ix":2},"a":{"a":0,"k":[16,16],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"refId":"1","w":20000,"h":20000,"ind":13,"ty":0,"nm":"globe","sr":1,"ks":{"p":{"a":0,"k":[64,32],"ix":2},"a":{"a":0,"k":[10064,10032],"ix":2},"s":{"a":1,"k":[{"t":40,"s":[100,100],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":60,"s":[85,85],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":70,"s":[105,105],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":80,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":14},{"ddd":0,"ind":19,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[64,38.42728805541992],"ix":2},"a":{"a":0,"k":[64,38.42728805541992],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":14},{"ddd":0,"ind":20,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[108.5400009155273,55],"ix":2},"a":{"a":0,"k":[108.5400009155273,55.00000190734863],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":19},{"ddd":0,"ind":21,"ty":4,"nm":"bling-N","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":20,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,0],[3.329999923706055,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.8,0.8,0.8],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.5,"ix":2},"lc":2,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":40,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":47,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":174,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":176,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":183,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[107.4273300170898,45.32266616821289],"ix":2},"a":{"a":0,"k":[1.664999961853027,0],"ix":2},"s":{"a":0,"k":[99.9999982885729,99.99999828857288],"ix":2},"r":{"a":0,"k":-45,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"bling","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":20,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,0],[3.329999923706055,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.8,0.8,0.8],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.5,"ix":2},"lc":2,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":47,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":53,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":175,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":176,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":183,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":189,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[109.1650009155273,55],"ix":2},"a":{"a":0,"k":[1.664999961853027,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"bling-S","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":20,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,0],[3.329999923706055,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.8,0.8,0.8],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2.5,"ix":2},"lc":2,"lj":1},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":53,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":60,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":175,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":176,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":189,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":190,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":196,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[107.4273300170898,64.67733001708984],"ix":2},"a":{"a":0,"k":[1.664999961853027,0],"ix":2},"s":{"a":0,"k":[99.9999982885729,99.99999828857288],"ix":2},"r":{"a":0,"k":45,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[64,45.49990844726562],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"ip":0,"op":371,"st":0,"bm":0,"parent":19},{"ddd":0,"ind":25,"ty":4,"nm":"dot-L","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[2,4],[0,2],[2,0],[4,2],[2,4]],"i":[[0,0],[0,1.100000023841858],[-1.100000023841858,0],[0,-1.100000023841858],[1.100000023841858,0]],"o":[[-1.100000023841858,0],[0,-1.100000023841858],[1.100000023841858,0],[0,1.100000023841858],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.1019607843137255],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[-1,-15.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[-41,6.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[2,2],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"dot-R","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1.5,3],[0,1.5],[1.5,0],[3,1.5],[1.5,3]],"i":[[0,0],[0,0.8250000178813934],[-0.8250000178813934,0],[0,-0.8250000178813934],[0.8250000178813934,0]],"o":[[-0.8250000178813934,0],[0,-0.8250000178813934],[0.8250000178813934,0],[0,0.8250000178813934],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.1019607843137255],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[3.5,-14],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[62.5,-11],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[62.5,-11],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[1.5,1.5],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":4,"nm":"spark-L","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[7.371442522321428,3.199999128069196],[8,4],[7.371442522321428,4.800000871930803],[5.828578404017857,5.199999128069196],[5.199986049107142,5.828569684709821],[4.800013950892857,7.371429443359375],[4,8],[3.199986049107143,7.371429443359375],[2.800013950892857,5.828569684709821],[2.171421595982143,5.199999128069196],[0.6285574776785714,4.800000871930803],[0,4],[0.6285574776785714,3.199999128069196],[2.171421595982143,2.800000871930803],[2.800013950892857,2.171430315290178],[3.199986049107143,0.628570556640625],[4,0],[4.800013950892857,0.628570556640625],[5.199986049107142,2.171430315290178],[5.828578404017857,2.800000871930803],[7.371442522321428,3.199999128069196]],"i":[[0,0],[0,-0.3999999931880406],[0.3999999931880406,-0.1142857159887041],[0,0],[0.05714285799435206,-0.2857142857142857],[0,0],[0.3999999931880406,0],[0.1142857159887041,0.3999999931880406],[0,0],[0.2857142857142857,0.05714285799435206],[0,0],[0,0.3999999931880406],[-0.3999999931880406,0.1142857159887041],[0,0],[-0.05714285799435206,0.2857142857142857],[0,0],[-0.3999999931880406,0],[-0.1142857159887041,-0.3999999931880406],[0,0],[-0.2857142857142857,-0.05714285799435206],[0,0]],"o":[[0.3428571564810616,0.05714285799435206],[0,0.3428571564810616],[0,0],[-0.2857142857142857,0.05714285799435206],[0,0],[-0.05714285799435206,0.3428571564810616],[-0.3428571564810616,0],[0,0],[-0.05714285799435206,-0.2857142857142857],[0,0],[-0.3428571564810616,-0.05714285799435206],[0,-0.3428571564810616],[0,0],[0.2857142857142857,-0.05714285799435206],[0,0],[0.05714285799435206,-0.3428571564810616],[0.3428571564810616,0],[0,0],[0.05714285799435206,0.2857142857142857],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.1019607843137255],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[-1,-16.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[-57,-7.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[4,4],"ix":2},"s":{"a":0,"k":[100,-100],"ix":2},"r":{"a":1,"k":[{"t":40,"s":[360],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":130,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":370,"s":[-180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"spark-Big","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[11.05716378348214,4.799997929164549],[12,5.999999046325684],[11.05716378348214,7.200000163486817],[8.742867606026785,7.799997452327391],[7.799979073660714,8.74285313742543],[7.200020926339286,11.05714240755333],[6,11.99999809265137],[4.799979073660714,11.05714240755333],[4.200020926339286,8.74285313742543],[3.257132393973214,7.799997452327391],[0.9428362165178571,7.200000163486817],[0,5.999999046325684],[0.9428362165178571,4.799997929164549],[3.257132393973214,4.200000640323976],[4.200020926339286,3.257144955225937],[4.799979073660714,0.9428556850980385],[6,0],[7.200020926339286,0.9428556850980385],[7.799979073660714,3.257144955225937],[8.742867606026785,4.200000640323976],[11.05716378348214,4.799997929164549]],"i":[[0,0],[0,-0.5999998944146309],[0.5999999897820608,-0.1714285467352182],[0,0],[0.0857142869915281,-0.4285713604518345],[0,0],[0.5999999897820608,0],[0.1714285739830562,0.5999998944146309],[0,0],[0.4285714285714285,0.08571427336760909],[0,0],[0,0.5999998944146309],[-0.5999999897820608,0.1714285467352182],[0,0],[-0.0857142869915281,0.4285713604518345],[0,0],[-0.5999999897820608,0],[-0.1714285739830562,-0.5999998944146309],[0,0],[-0.4285714285714285,-0.08571427336760909],[0,0]],"o":[[0.5142857347215924,0.08571427336760909],[0,0.5142856529780764],[0,0],[-0.4285714285714285,0.08571427336760909],[0,0],[-0.0857142869915281,0.5142856529780764],[-0.5142857347215924,0],[0,0],[-0.0857142869915281,-0.4285713604518345],[0,0],[-0.5142857347215924,-0.08571427336760909],[0,-0.5142856529780764],[0,0],[0.4285714285714285,-0.08571427336760909],[0,0],[0.0857142869915281,-0.5142856529780764],[0.5142857347215924,0],[0,0],[0.0857142869915281,0.4285713604518345],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.1019607843137255],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[-1,-15.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[-43,-28.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[-43,-28.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[6,5.999999046325684],"ix":2},"s":{"a":1,"k":[{"t":130,"s":[100,-100],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":250,"s":[50,-50],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":370,"s":[100,-100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":40,"s":[360],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"spark-R","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"parent":24,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[7.371442794799805,3.199999173482259],[8,4],[7.371442794799805,4.80000082651774],[5.828578313191731,5.199999173482259],[5.199986139933268,5.828570048014322],[4.800013860066731,7.371429443359375],[4,8],[3.199986139933268,7.371429443359375],[2.800013860066731,5.828570048014322],[2.171421686808268,5.199999173482259],[0.6285574833552042,4.80000082651774],[0,4],[0.6285574833552042,3.199999173482259],[2.171421686808268,2.800000826517741],[2.800013860066731,2.171430269877116],[3.199986139933268,0.628570556640625],[4,0],[4.800013860066731,0.628570556640625],[5.199986139933268,2.171430269877116],[5.828578313191731,2.800000826517741],[7.371442794799805,3.199999173482259]],"i":[[0,0],[0,-0.4000000158945719],[0.4000000158945719,-0.1142857174078623],[0,0],[0.05714285870393117,-0.2857142885526021],[0,0],[0.4000000158945719,0],[0.1142857174078623,0.4000000158945719],[0,0],[0.2857142885526021,0.05714285870393117],[0,0],[0,0.4000000158945719],[-0.4000000158945719,0.1142857174078623],[0,0],[-0.05714285870393117,0.2857142885526021],[0,0],[-0.4000000158945719,0],[-0.1142857174078623,-0.4000000158945719],[0,0],[-0.2857142885526021,-0.05714285870393117],[0,0]],"o":[[0.3428571621576945,0.05714285870393117],[0,0.3428571621576945],[0,0],[-0.2857142885526021,0.05714285870393117],[0,0],[-0.05714285870393117,0.3428571621576945],[-0.3428571621576945,0],[0,0],[-0.05714285870393117,-0.2857142885526021],[0,0],[-0.3428571621576945,-0.05714285870393117],[0,-0.3428571621576945],[0,0],[0.2857142885526021,-0.05714285870393117],[0,0],[0.05714285870393117,-0.3428571621576945],[0.3428571621576945,0],[0,0],[0.05714285870393117,0.2857142885526021],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":1,"k":[{"t":75,"s":[0.9764705882352941,0.7450980392156863,0.1019607843137255],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[0.5333333333333333,0.5333333333333333,0.5333333333333333],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":75,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":1,"k":[{"t":40,"s":[2,-14.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[45,-22.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[45,-22.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[4,4],"ix":2},"s":{"a":1,"k":[{"t":130,"s":[100,-100],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":250,"s":[250,-250],"i":{"x":[0.8],"y":[1]},"o":{"x":[0.4],"y":[0]}},{"t":370,"s":[100,-100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":40,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":75,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":100,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":371,"st":0,"bm":0}],"markers":[]} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index aaefd02988..1cd0015748 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -29,6 +29,7 @@ protocol NetworkProtectionIPCClient { var ipcStatusObserver: ConnectionStatusObserver { get } var ipcServerInfoObserver: ConnectionServerInfoObserver { get } var ipcConnectionErrorObserver: ConnectionErrorObserver { get } + var ipcDataVolumeObserver: DataVolumeObserver { get } func start(completion: @escaping (Error?) -> Void) func stop(completion: @escaping (Error?) -> Void) @@ -38,6 +39,7 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } public var ipcConnectionErrorObserver: any NetworkProtection.ConnectionErrorObserver { connectionErrorObserver } + public var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver { dataVolumeObserver } } final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { @@ -57,7 +59,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { // swiftlint:disable:next function_body_length func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { - let popover = networkProtectionPopover ?? { + let popover = { let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) @@ -66,7 +68,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { serverInfoObserver: ipcClient.ipcServerInfoObserver, connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.ipcDataVolumeObserver ) let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher @@ -111,6 +114,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { agentLoginItem: LoginItem.vpnMenu, isMenuBarStatusView: false, userDefaults: .netP, + locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) }) @@ -132,6 +136,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { if let networkProtectionPopover, networkProtectionPopover.isShown { networkProtectionPopover.close() + self.networkProtectionPopover = nil } else { let featureVisibility = DefaultNetworkProtectionVisibility() @@ -145,5 +150,6 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { func close() { networkProtectionPopover?.close() + networkProtectionPopover = nil } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift new file mode 100644 index 0000000000..5f2bb50530 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift @@ -0,0 +1,104 @@ +// +// DefaultVPNLocationFormatter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import NetworkProtection + +struct DefaultVPNLocationFormatter: VPNLocationFormatting { + func emoji(for country: String?, + preferredLocation someLocation: VPNSettings.SelectedLocation) -> String? { + if let country { + return NetworkProtectionVPNCountryLabelsModel(country: country, useFullCountryName: true).emoji + } + + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) + switch preferredLocation.icon { + case .defaultIcon: + return nil + case .emoji(let emoji): + return emoji + } + } + + func string(from location: String?, + preferredLocation someLocation: VPNSettings.SelectedLocation) -> String { + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) + + if let location { + return preferredLocation.isNearest ? "\(location) (Nearest)" : location + } + + return preferredLocation.title + } + + @available(macOS 12, *) + func string(from location: String?, + preferredLocation someLocation: VPNSettings.SelectedLocation, + locationTextColor: Color, + preferredLocationTextColor: Color) -> AttributedString { + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) + + if let location { + var attributedString = AttributedString( + preferredLocation.isNearest ? "\(location) \(UserText.netPVPNLocationNearest)" : location + ) + attributedString.foregroundColor = locationTextColor + if let range = attributedString.range(of: UserText.netPVPNLocationNearest) { + attributedString[range].foregroundColor = preferredLocationTextColor + } + return attributedString + } + + var attributedString = AttributedString(preferredLocation.title) + attributedString.foregroundColor = locationTextColor + return attributedString + } +} + +final class VPNLocationModel: ObservableObject { + enum LocationIcon { + case defaultIcon + case emoji(String) + } + + let title: String + let icon: LocationIcon + let isNearest: Bool + + init(selectedLocation: VPNSettings.SelectedLocation) { + switch selectedLocation { + case .nearest: + title = UserText.vpnLocationNearestAvailable + icon = .defaultIcon + isNearest = true + case .location(let location): + let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country, useFullCountryName: true) + if let city = location.city { + title = UserText.netPVPNSettingsLocationSubtitleFormattedCityAndCountry( + city: city, + country: countryLabelsModel.title + ) + } else { + title = countryLabelsModel.title + } + icon = .emoji(countryLabelsModel.emoji) + isNearest = false + } + } +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift index 28c6a16b19..00a3f034ea 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift @@ -23,8 +23,13 @@ struct NetworkProtectionVPNCountryLabelsModel { let emoji: String let title: String - init(country: String) { - self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized + init(country: String, useFullCountryName: Bool = true) { + if useFullCountryName { + self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized + } else { + self.title = country.localizedUppercase + } + self.emoji = Self.flag(country: country) } diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index c24d64bf6e..b4e463df17 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -121,7 +121,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { serverInfoObserver: ipcClient.serverInfoObserver, connectionErrorObserver: ipcClient.connectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.dataVolumeObserver ) // Force refresh just in case. A refresh is requested when the IPC client is created, but distributed notifications don't guarantee delivery diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index f28e90c26b..10758ce56d 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -237,7 +237,7 @@ extension WindowControllersManager { } } - func showNewWindow() { + func showMainWindow() { guard WindowControllersManager.shared.lastKeyMainWindowController == nil else { return } let tabCollection = TabCollection(tabs: []) let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) diff --git a/DuckDuckGoDBPBackgroundAgent/UserText.swift b/DuckDuckGoDBPBackgroundAgent/UserText.swift index 5b460321e5..608cab9e14 100644 --- a/DuckDuckGoDBPBackgroundAgent/UserText.swift +++ b/DuckDuckGoDBPBackgroundAgent/UserText.swift @@ -21,6 +21,6 @@ import Foundation final class UserText { // MARK: - Status Menu - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Send VPN Feedback...", comment: "The status menu 'Send VPN Feedback' menu item") - static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo...", comment: "The status menu 'Open DuckDuckGo' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") + static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index bc013ff4aa..b19518730d 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -184,12 +184,18 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) + let dataVolumeObserver = DataVolumeObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + return DefaultNetworkProtectionStatusReporter( statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, connectionErrorObserver: errorObserver, connectivityIssuesObserver: DisabledConnectivityIssueObserver(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: dataVolumeObserver ) }() @@ -242,17 +248,18 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in await self?.appLauncher.launchApp(withCommand: .showFAQ) }), + StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in + await self?.appLauncher.launchApp(withCommand: .shareFeedback) + }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in await self?.appLauncher.launchApp(withCommand: .justOpen) }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .shareFeedback) - }) ] }, agentLoginItem: nil, isMenuBarStatusView: true, userDefaults: .netP, + locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in guard let self else { return } await self.vpnUninstaller.uninstall(includingSystemExtension: true) diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index e8df373d16..971dc24522 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -50,6 +50,7 @@ final class TunnelControllerIPCService { subscribeToErrorChanges() subscribeToStatusUpdates() subscribeToServerChanges() + subscribeToDataVolumeUpdates() server.serverDelegate = self } @@ -84,6 +85,15 @@ final class TunnelControllerIPCService { } .store(in: &cancellables) } + + private func subscribeToDataVolumeUpdates() { + statusReporter.dataVolumeObserver.publisher + .subscribe(on: DispatchQueue.main) + .sink { [weak self] dataVolume in + self?.server.dataVolumeUpdated(dataVolume) + } + .store(in: &cancellables) + } } // MARK: - Requests from the client diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 57ea5e3c3b..4c64c664eb 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -24,5 +24,5 @@ final class UserText { static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "Frequently Asked Questions…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Send VPN Feedback…", comment: "The status menu 'Send VPN Feedback' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index df2e1a5447..51d8ee4755 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "137.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 17669b5f8a..c08127a1af 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "137.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift new file mode 100644 index 0000000000..ee605da45a --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift @@ -0,0 +1,40 @@ +// +// DataVolumeObserverThroughIPC.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkProtection + +public final class DataVolumeObserverThroughIPC: DataVolumeObserver { + + private let subject = CurrentValueSubject(.init()) + + // MARK: - DataVolumeObserver + + public lazy var publisher = subject.eraseToAnyPublisher() + + public var recentValue: DataVolume { + subject.value + } + + // MARK: - Publishing Updates + + func publish(_ dataVolume: DataVolume) { + subject.send(dataVolume) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index f5158c239b..61996e3945 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -26,6 +26,7 @@ public protocol IPCClientInterface: AnyObject { func errorChanged(_ error: String?) func serverInfoChanged(_ serverInfo: NetworkProtectionStatusServerInfo) func statusChanged(_ status: ConnectionStatus) + func dataVolumeUpdated(_ dataVolume: DataVolume) } /// This is the XPC interface with parameters that can be packed properly @@ -34,6 +35,7 @@ protocol XPCClientInterface { func errorChanged(error: String?) func serverInfoChanged(payload: Data) func statusChanged(payload: Data) + func dataVolumeUpdated(payload: Data) } public final class TunnelControllerIPCClient { @@ -47,6 +49,7 @@ public final class TunnelControllerIPCClient { public var serverInfoObserver = ConnectionServerInfoObserverThroughIPC() public var connectionErrorObserver = ConnectionErrorObserverThroughIPC() public var connectionStatusObserver = ConnectionStatusObserverThroughIPC() + public var dataVolumeObserver = DataVolumeObserverThroughIPC() /// The delegate. /// @@ -65,7 +68,8 @@ public final class TunnelControllerIPCClient { clientDelegate: self.clientDelegate, serverInfoObserver: self.serverInfoObserver, connectionErrorObserver: self.connectionErrorObserver, - connectionStatusObserver: self.connectionStatusObserver + connectionStatusObserver: self.connectionStatusObserver, + dataVolumeObserver: self.dataVolumeObserver ) xpc = XPCClient( @@ -97,15 +101,18 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { let serverInfoObserver: ConnectionServerInfoObserverThroughIPC let connectionErrorObserver: ConnectionErrorObserverThroughIPC let connectionStatusObserver: ConnectionStatusObserverThroughIPC + let dataVolumeObserver: DataVolumeObserverThroughIPC init(clientDelegate: IPCClientInterface?, serverInfoObserver: ConnectionServerInfoObserverThroughIPC, connectionErrorObserver: ConnectionErrorObserverThroughIPC, - connectionStatusObserver: ConnectionStatusObserverThroughIPC) { + connectionStatusObserver: ConnectionStatusObserverThroughIPC, + dataVolumeObserver: DataVolumeObserverThroughIPC) { self.clientDelegate = clientDelegate self.serverInfoObserver = serverInfoObserver self.connectionErrorObserver = connectionErrorObserver self.connectionStatusObserver = connectionStatusObserver + self.dataVolumeObserver = dataVolumeObserver } func errorChanged(error: String?) { @@ -130,6 +137,15 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { connectionStatusObserver.publish(status) clientDelegate?.statusChanged(status) } + + func dataVolumeUpdated(payload: Data) { + guard let dataVolume = try? JSONDecoder().decode(DataVolume.self, from: payload) else { + return + } + + dataVolumeObserver.publish(dataVolume) + clientDelegate?.dataVolumeUpdated(dataVolume) + } } // MARK: - Outgoing communication to the server diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index f2a7adc439..0d72a0d7ce 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -143,6 +143,20 @@ extension TunnelControllerIPCServer: IPCClientInterface { client.statusChanged(payload: payload) } } + + public func dataVolumeUpdated(_ dataVolume: DataVolume) { + let payload: Data + + do { + payload = try JSONEncoder().encode(dataVolume) + } catch { + return + } + + xpc.forEachClient { client in + client.dataVolumeUpdated(payload: payload) + } + } } // MARK: - Incoming communication from a client diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift new file mode 100644 index 0000000000..bfa0e14367 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift @@ -0,0 +1,51 @@ +// +// LottieView+withIntro.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Lottie + +extension LottieView where Placeholder: View { + public struct LoopWithIntroTiming { + let skipIntro: Bool + let introStartFrame: AnimationFrameTime + let introEndFrame: AnimationFrameTime + let loopStartFrame: AnimationFrameTime + let loopEndFrame: AnimationFrameTime + } + + public func playing(withIntro timing: LoopWithIntroTiming, isAnimating: Binding = .constant(true)) -> Lottie.LottieView { + configure { uiView in + if uiView.isAnimationPlaying, !isAnimating.wrappedValue { + uiView.stop() + return + } + + guard isAnimating.wrappedValue, !uiView.isAnimationPlaying else { return } + + if uiView.loopMode == .playOnce, uiView.currentProgress == 1 { return } + + if timing.skipIntro { + uiView.play(fromFrame: timing.loopStartFrame, toFrame: timing.loopEndFrame, loopMode: .loop) + } else { + uiView.play(fromFrame: timing.introStartFrame, toFrame: timing.introEndFrame, loopMode: .playOnce) { _ in + uiView.play(fromFrame: timing.loopStartFrame, toFrame: timing.loopEndFrame, loopMode: .loop) + } + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 402f85098f..0236302a96 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -19,7 +19,8 @@ import Foundation final class UserText { - static let networkProtectionStatusViewFeatureDesc = NSLocalizedString("network.protection.status.view.feature.description", value: "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere.", comment: "Feature description shown in NetworkProtection's status view.") + static let networkProtectionStatusHeaderMessageOff = NSLocalizedString("network.protection.status.header.message.off", value: "Connect to secure all of your device’s\nInternet traffic.", comment: "Message label text for the status view when VPN is disconnected") + static let networkProtectionStatusHeaderMessageOn = NSLocalizedString("network.protection.status.header.message.on", value: "All device Internet traffic is being secured\nthrough the VPN.", comment: "Message label text for the status view when VPN is connected") static let networkProtectionStatusViewConnDetails = NSLocalizedString("network.protection.status.view.connection.details", value: "Connection Details", comment: "Connection details label shown in NetworkProtection's status view.") static let networkProtectionStatusViewConnLabel = NSLocalizedString("network.protection.status.view.connection.label", value: "VPN", comment: "Connection label shown in NetworkProtection's status view.") static let networkProtectionStatusViewLocation = NSLocalizedString("network.protection.status.view.location", value: "Location", comment: "Location label shown in NetworkProtection's status view.") @@ -28,6 +29,11 @@ final class UserText { static let networkProtectionStatusViewFeatureOn = NSLocalizedString("network.protection.status.view.feature.on", value: "DuckDuckGo VPN is ON", comment: "Text shown in NetworkProtection's status view when NetP is ON.") static let networkProtectionStatusViewTimerZero = "00:00:00" + static let netPVPNLocationNearest = NSLocalizedString("network.protection.vpn.location.nearest", value: "(Nearest)", comment: "Description of the location type in the VPN status view") + static let vpnLocationConnected = NSLocalizedString("network.protection.vpn.location.connected", value: "Connected Location", comment: "Description of the location type in the VPN status view") + static let vpnLocationSelected = NSLocalizedString("network.protection.vpn.location.selected", value: "Selected Location", comment: "Description of the location type in the VPN status view") + static let vpnDataVolume = NSLocalizedString("network.protection.vpn.data-volume", value: "Data Volume", comment: "Title for the data volume section in the VPN status view") + // MARK: - Onboarding static let networkProtectionOnboardingInstallExtensionTitle = NSLocalizedString("network.protection.onboarding.install.extension.title", value: "Install VPN System Extension", comment: "Title for the onboarding install-vpn-extension step") diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 1833f8482e..5e02caa125 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -32,7 +32,18 @@ public final class StatusBarMenu: NSObject { private let model: StatusBarMenuModel private let statusItem: NSStatusItem - private let popover: NetworkProtectionPopover + private var popover: NetworkProtectionPopover? + + private let controller: TunnelController + private let statusReporter: NetworkProtectionStatusReporter + private let onboardingStatusPublisher: OnboardingStatusPublisher + private let appLauncher: AppLaunching + private let menuItems: () -> [MenuItem] + private let agentLoginItem: LoginItem? + private let isMenuBarStatusView: Bool + private let userDefaults: UserDefaults + private let locationFormatter: VPNLocationFormatting + private let uninstallHandler: () async -> Void // MARK: - NetP Icon publisher @@ -58,6 +69,7 @@ public final class StatusBarMenu: NSObject { agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, userDefaults: UserDefaults, + locationFormatter: VPNLocationFormatting, uninstallHandler: @escaping () async -> Void) { self.model = model @@ -65,17 +77,16 @@ public final class StatusBarMenu: NSObject { self.statusItem = statusItem self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: statusReporter, iconProvider: iconProvider) - popover = NetworkProtectionPopover(controller: controller, - onboardingStatusPublisher: onboardingStatusPublisher, - statusReporter: statusReporter, - appLauncher: appLauncher, - menuItems: menuItems, - agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView, - userDefaults: userDefaults, - uninstallHandler: uninstallHandler) - - popover.behavior = .transient + self.controller = controller + self.statusReporter = statusReporter + self.onboardingStatusPublisher = onboardingStatusPublisher + self.appLauncher = appLauncher + self.menuItems = menuItems + self.agentLoginItem = agentLoginItem + self.isMenuBarStatusView = isMenuBarStatusView + self.userDefaults = userDefaults + self.locationFormatter = locationFormatter + self.uninstallHandler = uninstallHandler super.init() @@ -112,22 +123,35 @@ public final class StatusBarMenu: NSObject { // MARK: - Popover private func togglePopover(isOptionKeyPressed: Bool) { - if popover.isShown { + if let popover, popover.isShown { popover.close() + self.popover = nil } else { guard let button = statusItem.button else { return } - popover.setShowsDebugInformation(isOptionKeyPressed) - popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) + popover = NetworkProtectionPopover(controller: controller, + onboardingStatusPublisher: onboardingStatusPublisher, + statusReporter: statusReporter, + appLauncher: appLauncher, + menuItems: menuItems, + agentLoginItem: agentLoginItem, + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + locationFormatter: locationFormatter, + uninstallHandler: uninstallHandler) + popover?.behavior = .transient + + popover?.setShowsDebugInformation(isOptionKeyPressed) + popover?.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } } // MARK: - Context private func showContextMenu() { - if popover.isShown { + if let popover, popover.isShown { popover.close() } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift index dfa7a23850..a8288b7dbd 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift @@ -19,11 +19,12 @@ import Foundation public enum NetworkProtectionAsset: String, CaseIterable { - case ipAddressIcon = "IP-16" - case serverLocationIcon = "Server-Location-16" - case vpnDisabledImage = "VPN-Disabled-128" - case vpnEnabledImage = "VPN-128" + case vpnDisabledImage = "VPNDisabled" + case vpnEnabledImage = "VPN" case vpnIcon = "VPN-16" + case nearestAvailable = "VPNLocation" + case dataReceived = "VPNDownload" + case dataSent = "VPNUpload" // Apple Icons case appleVaultIcon = "apple-vault-icon" diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift index 010085527a..2145e2da36 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift @@ -31,6 +31,7 @@ extension Color { /// enum NetworkProtectionColor: String { case defaultText = "TextColor" + case secondaryText = "SecondaryColor" case linkColor = "LinkBlueColor" case onboardingButtonBackgroundColor = "OnboardingButtonBackgroundColor" #if swift(<5.9) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index 073db45255..4236e963aa 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -58,6 +58,7 @@ public final class NetworkProtectionPopover: NSPopover { agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, userDefaults: UserDefaults, + locationFormatter: VPNLocationFormatting, uninstallHandler: @escaping () async -> Void) { self.statusReporter = statusReporter @@ -70,6 +71,7 @@ public final class NetworkProtectionPopover: NSPopover { agentLoginItem: agentLoginItem, isMenuBarStatusView: isMenuBarStatusView, userDefaults: userDefaults, + locationFormatter: locationFormatter, uninstallHandler: uninstallHandler) super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json new file mode 100644 index 0000000000..8f7e96555c --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.850", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf deleted file mode 100644 index f68365bd0a..0000000000 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf and /dev/null differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf deleted file mode 100644 index b1f213c7b9..0000000000 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf and /dev/null differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Network-Protetion-VPN-128.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Network-Protetion-VPN-128.pdf deleted file mode 100644 index 54953a618c..0000000000 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Network-Protetion-VPN-128.pdf and /dev/null differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Network-Protetion-VPN-Disabled-128.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Network-Protetion-VPN-Disabled-128.pdf deleted file mode 100644 index bee84f6a0a..0000000000 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Network-Protetion-VPN-Disabled-128.pdf and /dev/null differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN.imageset/Contents.json similarity index 70% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN.imageset/Contents.json index 86f2574f00..7d29a35569 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-128.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Network-Protetion-VPN-128.pdf", + "filename" : "VPN.pdf", "idiom" : "universal" } ], diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN.imageset/VPN.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN.imageset/VPN.pdf new file mode 100644 index 0000000000..e4423e08d1 Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN.imageset/VPN.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDisabled.imageset/Contents.json similarity index 66% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDisabled.imageset/Contents.json index 20e5eff4ab..4c65d209a8 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPN-Disabled-128.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDisabled.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Network-Protetion-VPN-Disabled-128.pdf", + "filename" : "VPNDisabled.pdf", "idiom" : "universal" } ], diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDisabled.imageset/VPNDisabled.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDisabled.imageset/VPNDisabled.pdf new file mode 100644 index 0000000000..1577b6fc56 Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDisabled.imageset/VPNDisabled.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json similarity index 62% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json index e020cd9994..e030aab433 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Server-Location-16.pdf", + "filename" : "vpn-download.pdf", "idiom" : "universal" } ], @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "template-rendering-intent" : "template" + "template-rendering-intent" : "original" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf new file mode 100644 index 0000000000..d5288602de Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json new file mode 100644 index 0000000000..09ed1f69f3 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "VPNLocation.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf new file mode 100644 index 0000000000..ddcabe468b Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json similarity index 64% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json index 657dbb1f2c..c7fec6a1a8 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "IP-16.pdf", + "filename" : "vpn-upload.pdf", "idiom" : "universal" } ], @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "template-rendering-intent" : "template" + "template-rendering-intent" : "original" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf new file mode 100644 index 0000000000..4cc3e7eced Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift new file mode 100644 index 0000000000..cdd8bd79c8 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift @@ -0,0 +1,88 @@ +// +// MenuItemCustomButton.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +struct MenuItemCustomButton: View { + @Environment(\.colorScheme) private var colorScheme + private var label: (Bool) -> Label + private let action: () async -> Void + + private let highlightAnimationStepSpeed = 0.05 + + @State private var isHovered = false + @State private var animatingTap = false + + init(action: @escaping () async -> Void, @ViewBuilder label: @escaping (Bool) -> Label) { + self.action = action + self.label = label + } + + var body: some View { + Button(action: { + buttonTapped() + }) { + HStack { + label(isHovered) + }.padding([.top, .bottom], 3) + .padding([.leading, .trailing], 9) + } + .frame(maxWidth: .infinity, alignment: .leading) + .background( + buttonBackground(highlighted: isHovered) + ) + .contentShape(Rectangle()) + .cornerRadius(4) + .onTapGesture { + buttonTapped() + } + .onHover { hovering in + if !animatingTap { + isHovered = hovering + } + } + .buttonStyle(PlainButtonStyle()) + } + + private func buttonBackground(highlighted: Bool) -> some View { + if highlighted { + return AnyView( + VisualEffectView(material: .selection, blendingMode: .withinWindow, state: .active, isEmphasized: true)) + } else { + return AnyView(Color.clear) + } + } + + private func buttonTapped() { + animatingTap = true + isHovered = false + + DispatchQueue.main.asyncAfter(deadline: .now() + highlightAnimationStepSpeed) { + isHovered = true + + DispatchQueue.main.asyncAfter(deadline: .now() + highlightAnimationStepSpeed) { + animatingTap = false + + Task { + await action() + } + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index aae033b17d..bc3caa84c3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -82,7 +82,7 @@ public struct NetworkProtectionStatusView: View { bottomMenuView() } .padding(5) - .frame(maxWidth: 350, alignment: .top) + .frame(width: 350, alignment: .top) .transition(.slide) } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 7df51daf58..eb5509a68f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -119,6 +119,7 @@ extension NetworkProtectionStatusView { isMenuBarStatusView: Bool, runLoopMode: RunLoop.Mode? = nil, userDefaults: UserDefaults, + locationFormatter: VPNLocationFormatting, uninstallHandler: @escaping () async -> Void) { self.tunnelController = controller @@ -135,6 +136,8 @@ extension NetworkProtectionStatusView { tunnelControllerViewModel = TunnelControllerViewModel(controller: tunnelController, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, + vpnSettings: .init(defaults: userDefaults), + locationFormatter: locationFormatter, appLauncher: appLauncher) connectionStatus = statusReporter.statusObserver.recentValue diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 5f724d601d..c4ede6adeb 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -20,6 +20,7 @@ import SwiftUI import SwiftUIExtensions import Combine import NetworkProtection +import Lottie fileprivate extension Font { enum NetworkProtection { @@ -27,6 +28,14 @@ fileprivate extension Font { .system(size: 13, weight: .regular, design: .default) } + static var dataVolume: Font { + .system(size: 13, weight: .regular, design: .default) + } + + static var location: Font { + .system(size: 13, weight: .regular, design: .default) + } + static var content: Font { .system(size: 13, weight: .regular, design: .default) } @@ -59,9 +68,12 @@ private enum Opacity { colorScheme == .light ? Double(0.6) : Double(0.5) } + static func dataVolume(colorScheme: ColorScheme) -> Double { + colorScheme == .light ? Double(0.6) : Double(0.5) + } + static let content = Double(0.58) static let label = Double(0.9) - static let description = Double(0.9) static let link = Double(1) static func sectionHeader(colorScheme: ColorScheme) -> Double { @@ -84,16 +96,25 @@ fileprivate extension View { .foregroundColor(Color(.defaultText)) } + func applyDataVolumeAttributes(colorScheme: ColorScheme) -> some View { + opacity(Opacity.dataVolume(colorScheme: colorScheme)) + .font(.NetworkProtection.dataVolume) + .foregroundColor(Color(.defaultText)) + } + + func applyLocationAttributes() -> some View { + font(.NetworkProtection.location) + } + func applyContentAttributes(colorScheme: ColorScheme) -> some View { opacity(Opacity.content) .font(.NetworkProtection.content) .foregroundColor(Color(.defaultText)) } - func applyDescriptionAttributes(colorScheme: ColorScheme) -> some View { - opacity(Opacity.description) - .font(.NetworkProtection.description) - .foregroundColor(Color(.defaultText)) + func applyDescriptionAttributes() -> some View { + font(.NetworkProtection.description) + .foregroundColor(Color(.secondaryText)) } func applyLabelAttributes(colorScheme: ColorScheme) -> some View { @@ -151,6 +172,8 @@ public struct TunnelControllerView: View { Divider() .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) + locationView() + if model.showServerDetails { connectionStatusView() .disabled(on: !isEnabled) @@ -164,26 +187,108 @@ public struct TunnelControllerView: View { /// private func headerView() -> some View { VStack(spacing: 0) { - HStack { - Spacer() - Image(model.mainImageAsset) - Spacer() - } + headerAnimationView() + .frame(width: 100, height: 75) Text(model.featureStatusDescription) .applyTitleAttributes(colorScheme: colorScheme) .padding([.top], 8) .multilineText() - Text(UserText.networkProtectionStatusViewFeatureDesc) + Text(model.isToggleOn.wrappedValue ? UserText.networkProtectionStatusHeaderMessageOn : UserText.networkProtectionStatusHeaderMessageOff) .multilineText() .multilineTextAlignment(.center) - .applyDescriptionAttributes(colorScheme: colorScheme) + .applyDescriptionAttributes() .fixedSize(horizontal: false, vertical: true) .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) } } + @ViewBuilder + private func headerAnimationView() -> some View { + if colorScheme == .light { + headerAnimationView("vpn-light-mode") + } else { + headerAnimationView("vpn-dark-mode") + } + } + + @ViewBuilder + private func headerAnimationView(_ animationName: String) -> some View { + LottieView(animation: .named(animationName)) + .playing(withIntro: .init( + skipIntro: model.isVPNEnabled && !model.isToggleDisabled, + introStartFrame: 0, + introEndFrame: 100, + loopStartFrame: 130, + loopEndFrame: 370 + ), isAnimating: $model.isVPNEnabled) +} + + @ViewBuilder + private func statusBadge(isConnected: Bool) -> some View { + Circle() + .fill(isConnected ? .green : .yellow) + .frame(width: 8, height: 8) + } + + /// Connected/Selected location + /// + private func locationView() -> some View { + VStack(alignment: .leading, spacing: 0) { + Text(model.isVPNEnabled ? UserText.vpnLocationConnected : UserText.vpnLocationSelected) + .applySectionHeaderAttributes(colorScheme: colorScheme) + .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) + + MenuItemCustomButton { + model.showLocationSettings() + dismiss() + } label: { isHovered in + HStack(alignment: .center, spacing: 10) { + if let emoji = model.emoji { + Text(emoji) + .font(.system(size: 13)) + .frame(width: 26, height: 26) + .background(Color(hex: "B2B2B2").opacity(0.3)) + .clipShape(Circle()) + } else if model.wantsNearestLocation { + ZStack { + Circle() + .fill(Color(hex: "B2B2B2").opacity(0.3)) + .frame(width: 26, height: 26) + if isHovered { + Image(NetworkProtectionAsset.nearestAvailable) + .renderingMode(.template) + .foregroundColor(.white) + .frame(width: 16, height: 16) + } else { + Image(NetworkProtectionAsset.nearestAvailable) + .renderingMode(colorScheme == .light ? .original : .template) + .frame(width: 16, height: 16) + } + } + } + if #available(macOS 12, *) { + if isHovered { + Text(model.plainLocation) + .applyLocationAttributes() + .foregroundColor(.white) + } else { + Text(model.formattedLocation(colorScheme: colorScheme)) + .applyLocationAttributes() + } + } else { + Text(model.plainLocation) + .applyLocationAttributes() + .foregroundColor(isHovered ? .white: Color(.defaultText)) + } + } + } + + dividerRow() + } + } + /// Connection status: server IP address and location /// private func connectionStatusView() -> some View { @@ -192,19 +297,11 @@ public struct TunnelControllerView: View { .applySectionHeaderAttributes(colorScheme: colorScheme) .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) - MenuItemButton( - iconName: .serverLocationIcon, - title: UserText.networkProtectionStatusViewLocation, - detailTitle: model.serverLocation, - textColor: Color(.defaultText)) { - model.showLocationSettings() - dismiss() - }.applyMenuAttributes() - - connectionStatusRow(icon: .ipAddressIcon, - title: UserText.networkProtectionStatusViewIPAddress, + connectionStatusRow(title: UserText.networkProtectionStatusViewIPAddress, details: model.serverAddress) + dataVolumeRow(title: UserText.vpnDataVolume, dataVolume: model.formattedDataVolume) + dividerRow() } } @@ -227,6 +324,8 @@ public struct TunnelControllerView: View { Spacer(minLength: 8) + statusBadge(isConnected: model.isToggleOn.wrappedValue) + Text(model.connectionStatusDescription) .applyTimerAttributes(colorScheme: colorScheme) .fixedSize() @@ -241,11 +340,8 @@ public struct TunnelControllerView: View { .padding(EdgeInsets(top: 3, leading: 9, bottom: 3, trailing: 9)) } - private func connectionStatusRow(icon: NetworkProtectionAsset, title: String, details: String) -> some View { + private func connectionStatusRow(title: String, details: String) -> some View { HStack(spacing: 0) { - Image(icon) - .padding([.trailing], 8) - Text(title) .applyLabelAttributes(colorScheme: colorScheme) .fixedSize() @@ -257,6 +353,32 @@ public struct TunnelControllerView: View { .applyConnectionStatusDetailAttributes(colorScheme: colorScheme) .fixedSize() } + .padding(EdgeInsets(top: 6, leading: 10, bottom: 0, trailing: 9)) + } + + private func dataVolumeRow(title: String, dataVolume: TunnelControllerViewModel.FormattedDataVolume) -> some View { + HStack(spacing: 0) { + Text(title) + .applyLabelAttributes(colorScheme: colorScheme) + .fixedSize() + + Spacer(minLength: 2) + + Group { + Image(NetworkProtectionAsset.dataReceived) + .renderingMode(colorScheme == .light ? .original : .template) + .frame(width: 12, height: 12) + Text(dataVolume.dataReceived) + .applyDataVolumeAttributes(colorScheme: colorScheme) + Image(NetworkProtectionAsset.dataSent) + .renderingMode(colorScheme == .light ? .original : .template) + .frame(width: 12, height: 12) + .padding(.leading, 4) + Text(dataVolume.dataSent) + .applyDataVolumeAttributes(colorScheme: colorScheme) + } + .fixedSize() + } .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 90aa7d5933..5c63ecea7e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -23,11 +23,20 @@ import SwiftUI @MainActor public final class TunnelControllerViewModel: ObservableObject { + public struct FormattedDataVolume: Equatable { + public let dataSent: String + public let dataReceived: String + } /// The NetP service. /// private let tunnelController: TunnelController + /// Whether the VPN is enabled + /// This is determined based on the connection status, same as the iOS version + @Published + public var isVPNEnabled = false + /// The type of extension that's being used for NetP /// @Published @@ -47,6 +56,17 @@ public final class TunnelControllerViewModel: ObservableObject { /// private let statusReporter: NetworkProtectionStatusReporter + private let vpnSettings: VPNSettings + + private let locationFormatter: VPNLocationFormatting + + private static let byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + formatter.allowsNonnumericFormatting = false + formatter.allowedUnits = [.useKB, .useMB, .useGB] + return formatter + }() + private let appLauncher: AppLaunching // MARK: - Misc @@ -61,23 +81,7 @@ public final class TunnelControllerViewModel: ObservableObject { private static let statusDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.statusDispatchQueue", qos: .userInteractive) private static let connectivityIssuesDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.connectivityIssuesDispatchQueue", qos: .userInteractive) private static let serverInfoDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.serverInfoDispatchQueue", qos: .userInteractive) - - // MARK: - Feature Image - - var mainImageAsset: NetworkProtectionAsset { - switch connectionStatus { - case .connected: - return .vpnEnabledImage - case .disconnecting: - if case .connected = previousConnectionStatus { - return .vpnEnabledImage - } else { - return .vpnDisabledImage - } - default: - return .vpnDisabledImage - } - } + private static let dataVolumeDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.dataVolumeDispatchQueue", qos: .userInteractive) // MARK: - Initialization & Deinitialization @@ -85,17 +89,23 @@ public final class TunnelControllerViewModel: ObservableObject { onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, runLoopMode: RunLoop.Mode? = nil, + vpnSettings: VPNSettings, + locationFormatter: VPNLocationFormatting, appLauncher: AppLaunching) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher self.statusReporter = statusReporter self.runLoopMode = runLoopMode + self.vpnSettings = vpnSettings + self.locationFormatter = locationFormatter self.appLauncher = appLauncher connectionStatus = statusReporter.statusObserver.recentValue + formattedDataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress - internalServerLocation = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation + internalServerAttributes = statusReporter.serverInfoObserver.recentValue.serverLocation + internalServerLocation = internalServerAttributes?.serverLocation // Particularly useful when unit testing with an initial status of our choosing. refreshInternalIsRunning() @@ -103,6 +113,7 @@ public final class TunnelControllerViewModel: ObservableObject { subscribeToOnboardingStatusChanges() subscribeToStatusChanges() subscribeToServerInfoChanges() + subscribeToDataVolumeUpdates() } deinit { @@ -130,6 +141,12 @@ public final class TunnelControllerViewModel: ObservableObject { Task { @MainActor in self.connectionStatus = status + switch status { + case .connected, .connecting: + self.isVPNEnabled = true + default: + self.isVPNEnabled = false + } } } .store(in: &cancellables) @@ -146,12 +163,22 @@ public final class TunnelControllerViewModel: ObservableObject { Task { @MainActor in self.internalServerAddress = serverInfo.serverAddress - self.internalServerLocation = serverInfo.serverLocation?.serverLocation + self.internalServerAttributes = serverInfo.serverLocation + self.internalServerLocation = self.internalServerAttributes?.serverLocation } } .store(in: &cancellables) } + private func subscribeToDataVolumeUpdates() { + statusReporter.dataVolumeObserver.publisher + .subscribe(on: Self.dataVolumeDispatchQueue) + .map { $0.formatted(using: Self.byteCountFormatter) } + .receive(on: DispatchQueue.main) + .assign(to: \.formattedDataVolume, onWeaklyHeld: self) + .store(in: &cancellables) + } + // MARK: - ON/OFF Toggle private func startTimer() { @@ -435,6 +462,36 @@ public final class TunnelControllerViewModel: ObservableObject { } } + @Published + private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? + + @Published + var formattedDataVolume: FormattedDataVolume + + var wantsNearestLocation: Bool { + guard case .nearest = vpnSettings.selectedLocation else { return false } + return true + } + + var emoji: String? { + locationFormatter.emoji(for: internalServerAttributes?.country, + preferredLocation: vpnSettings.selectedLocation) + } + + var plainLocation: String { + locationFormatter.string(from: internalServerLocation, + preferredLocation: vpnSettings.selectedLocation) + } + + @available(macOS 12, *) + func formattedLocation(colorScheme: ColorScheme) -> AttributedString { + let opacity = colorScheme == .light ? Double(0.6) : Double(0.5) + return locationFormatter.string(from: internalServerLocation, + preferredLocation: vpnSettings.selectedLocation, + locationTextColor: Color(.defaultText), + preferredLocationTextColor: Color(.defaultText).opacity(opacity)) + } + // MARK: - Toggling VPN /// Start the VPN. @@ -473,3 +530,10 @@ public final class TunnelControllerViewModel: ObservableObject { } } } + +extension DataVolume { + func formatted(using formatter: ByteCountFormatter) -> TunnelControllerViewModel.FormattedDataVolume { + .init(dataSent: formatter.string(fromByteCount: bytesSent), + dataReceived: formatter.string(fromByteCount: bytesReceived)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift new file mode 100644 index 0000000000..05abc170a5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift @@ -0,0 +1,36 @@ +// +// MockVPNLocationFormatter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import NetworkProtection + +struct MockVPNLocationFormatter: VPNLocationFormatting { + func emoji(for country: String?, preferredLocation someLocation: VPNSettings.SelectedLocation) -> String? { + nil + } + + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation) -> String { + "" + } + + @available(macOS 12, *) + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation, locationTextColor: Color, preferredLocationTextColor: Color) -> AttributedString { + "" + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift index 31f40a4ab6..b49d24d5b4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift @@ -28,11 +28,12 @@ final class NetworkProtectionAssetTests: XCTestCase { /// func testAssetEnumValuesAreUnchanged() { let assetsAndExpectedRawValues: [NetworkProtectionAsset: String] = [ - .ipAddressIcon: "IP-16", - .serverLocationIcon: "Server-Location-16", - .vpnDisabledImage: "VPN-Disabled-128", - .vpnEnabledImage: "VPN-128", + .vpnDisabledImage: "VPNDisabled", + .vpnEnabledImage: "VPN", .vpnIcon: "VPN-16", + .nearestAvailable: "VPNLocation", + .dataReceived: "VPNDownload", + .dataSent: "VPNUpload", .appleVaultIcon: "apple-vault-icon", .appleVPNIcon: "apple-vpn-icon", .appleSystemSettingsIcon: "apple-system-settings-icon", diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index 5b9e7e0dea..967f92c995 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -58,6 +58,7 @@ final class StatusBarMenuTests: XCTestCase { agentLoginItem: nil, isMenuBarStatusView: false, userDefaults: .standard, + locationFormatter: MockVPNLocationFormatter(), uninstallHandler: { }) menu.show() @@ -83,6 +84,7 @@ final class StatusBarMenuTests: XCTestCase { agentLoginItem: nil, isMenuBarStatusView: false, userDefaults: .standard, + locationFormatter: MockVPNLocationFormatter(), uninstallHandler: { }) menu.hide() diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index 83caf2cff6..e7785da3f4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -36,12 +36,14 @@ final class TunnelControllerViewModelTests: XCTestCase { let connectionErrorObserver: ConnectionErrorObserver let connectivityIssuesObserver: ConnectivityIssueObserver let controllerErrorMessageObserver: ControllerErrorMesssageObserver + let dataVolumeObserver: DataVolumeObserver init(status: ConnectionStatus, isHavingConnectivityIssues: Bool = false, serverInfo: NetworkProtectionStatusServerInfo = MockStatusReporter.defaultServerInfo, tunnelErrorMessage: String? = nil, - controllerErrorMessage: String? = nil) { + controllerErrorMessage: String? = nil, + dataVolume: DataVolume = .init()) { let mockStatusObserver = MockConnectionStatusObserver() mockStatusObserver.subject.send(status) @@ -62,6 +64,10 @@ final class TunnelControllerViewModelTests: XCTestCase { let mockControllerErrorMessageObserver = MockControllerErrorMesssageObserver() mockControllerErrorMessageObserver.subject.send(controllerErrorMessage) controllerErrorMessageObserver = mockControllerErrorMessageObserver + + let mockDataVolumeObserver = MockDataVolumeObserver() + mockDataVolumeObserver.subject.send(dataVolume) + dataVolumeObserver = mockDataVolumeObserver } func forceRefresh() { @@ -104,6 +110,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let isToggleOn = model.isToggleOn.wrappedValue @@ -124,6 +132,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) XCTAssertEqual(model.connectionStatusDescription, UserText.networkProtectionStatusDisconnecting) @@ -151,6 +161,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let isToggleOn = model.isToggleOn.wrappedValue @@ -160,7 +172,7 @@ final class TunnelControllerViewModelTests: XCTestCase { XCTAssertEqual(model.featureStatusDescription, UserText.networkProtectionStatusViewFeatureOn) XCTAssertTrue(model.showServerDetails) XCTAssertEqual(model.serverAddress, mockServerIP) - XCTAssertEqual(model.serverLocation, "El Segundo, CA...") + XCTAssertEqual(model.serverLocation, "El Segundo, United States...") } /// We expect the model to properly reflect the connecting status. @@ -173,6 +185,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) XCTAssertEqual(model.connectionStatusDescription, UserText.networkProtectionStatusConnecting) @@ -181,6 +195,24 @@ final class TunnelControllerViewModelTests: XCTestCase { XCTAssertFalse(model.showServerDetails) } + /// We expect the model to properly reflect the data volume. + /// + @MainActor + func testProperlyReflectsDataVolume() async throws { + let controller = MockTunnelController() + let statusReporter = MockStatusReporter(status: .connected(connectedDate: Date()), + dataVolume: .init(bytesSent: 512000, bytesReceived: 1024000)) + let model = TunnelControllerViewModel( + controller: controller, + onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), + statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), + appLauncher: MockAppLauncher()) + + XCTAssertEqual(model.formattedDataVolume, .init(dataSent: "512 KB", dataReceived: "1 MB")) + } + /// We expect that setting the model's `isRunning` to `true`, will start the VPN. /// @MainActor @@ -191,6 +223,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let networkProtectionWasStarted = expectation(description: "The model started the VPN when appropriate") @@ -221,6 +255,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let networkProtectionWasStopped = expectation(description: "The model stopped the VPN when appropriate") diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 524e9ef352..9092bb8c50 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "137.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift new file mode 100644 index 0000000000..e9abed7230 --- /dev/null +++ b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift @@ -0,0 +1,90 @@ +// +// DefaultVPNLocationFormatterTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import DuckDuckGo_Privacy_Browser +@testable import NetworkProtection + +final class DefaultVPNLocationFormatterTests: XCTestCase { + private var formatter: DefaultVPNLocationFormatter! + + override func setUp() { + formatter = DefaultVPNLocationFormatter() + } + + func testUSLocation() { + let server = NetworkProtectionServerInfo.ServerAttributes(city: "Lafayette", country: "us", state: "la") + let preferredLocation = VPNSettings.SelectedLocation.location(.init(country: "us")) + let otherPreferredLocation = VPNSettings.SelectedLocation.location(.init(country: "gb")) + + XCTAssertNil(formatter.emoji(for: nil, preferredLocation: .nearest)) + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: preferredLocation), "🇺🇸") + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: otherPreferredLocation), "🇬🇧") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇺🇸") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇺🇸") + + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest Location") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "United States") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, United States (Nearest)") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Lafayette, United States") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: otherPreferredLocation), "Lafayette, United States") + + if #available(macOS 12, *) { + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: .nearest, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Lafayette, United States (Nearest)") + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: preferredLocation, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Lafayette, United States") + } + } + + func testCALocation() { + let server = NetworkProtectionServerInfo.ServerAttributes(city: "Toronto", country: "ca", state: "on") + let preferredLocation = VPNSettings.SelectedLocation.location(.init(country: "ca")) + let otherPreferredLocation = VPNSettings.SelectedLocation.location(.init(country: "gb")) + + XCTAssertNil(formatter.emoji(for: nil, preferredLocation: .nearest)) + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: preferredLocation), "🇨🇦") + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: otherPreferredLocation), "🇬🇧") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇨🇦") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇨🇦") + + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest Location") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "Canada") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Toronto, Canada (Nearest)") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Toronto, Canada") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: otherPreferredLocation), "Toronto, Canada") + + if #available(macOS 12, *) { + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: .nearest, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Toronto, Canada (Nearest)") + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: preferredLocation, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Toronto, Canada") + } + } +} diff --git a/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift b/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift new file mode 100644 index 0000000000..579ba6e4ff --- /dev/null +++ b/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift @@ -0,0 +1,37 @@ +// +// MockVPNLocationFormatter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import NetworkProtection +@testable import DuckDuckGo_Privacy_Browser + +struct MockVPNLocationFormatter: VPNLocationFormatting { + func emoji(for country: String?, preferredLocation someLocation: VPNSettings.SelectedLocation) -> String? { + nil + } + + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation) -> String { + "" + } + + @available(macOS 12, *) + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation, locationTextColor: Color, preferredLocationTextColor: Color) -> AttributedString { + "" + } +} diff --git a/UnitTests/NetworkProtection/Support/NetworkProtectionTestingSupport.swift b/UnitTests/NetworkProtection/Support/NetworkProtectionTestingSupport.swift index e298ad553d..4e6de202e7 100644 --- a/UnitTests/NetworkProtection/Support/NetworkProtectionTestingSupport.swift +++ b/UnitTests/NetworkProtection/Support/NetworkProtectionTestingSupport.swift @@ -101,13 +101,19 @@ struct MockConnectionErrorObserver: ConnectionErrorObserver { var recentValue: String? } -struct MockIPCClient: NetworkProtectionIPCClient { +struct MockDataVolumeObserver: DataVolumeObserver { + var publisher: AnyPublisher = Just(.init()).eraseToAnyPublisher() + + var recentValue: DataVolume = .init() +} +struct MockIPCClient: NetworkProtectionIPCClient { private let error: Error? var ipcStatusObserver: NetworkProtection.ConnectionStatusObserver = MockConnectionStatusObserver() var ipcServerInfoObserver: NetworkProtection.ConnectionServerInfoObserver = MockServerInfoObserver() var ipcConnectionErrorObserver: NetworkProtection.ConnectionErrorObserver = MockConnectionErrorObserver() + var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver = MockDataVolumeObserver() init(error: Error? = nil) { self.error = error