From 717adb87148d948201da928f5e374c9876a26a02 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Wed, 11 Sep 2024 18:07:20 +0200 Subject: [PATCH] Add support for Duck Player (#4663) Task/Issue URL: https://app.asana.com/0/1205008441501016/1207588004626729/f ### Description - Add Duck Player support - Navigate from Duck Player back to YouTube - Add Duck Player settings - Open YT URLs in Duck Player if setting is Enabled(Always) - Add Duck Player Prime Modal - Add Contingency settings ### Steps to test this PR https://app.asana.com/0/0/1207704461779423/f Note: There have been changes to the JS integration, so you might need to test these changes on the top of the stack _User preferences "Always" open Duck Player_ - [x] Open Settings -> Duck Player -> Set to always - [x] Type a YT URL on the omnibar - [x] Check Duck Player is loaded - [x] Navigate back and check you're going to the previous page your were visiting _User preferences "Always ask" trigger overlay_ - [x] Open Settings -> Duck Player -> Set to always ask - [x] Type a YT URL on the omnibar - [x] Check overlay is loaded in YT - [x] Check that watch here removes the overlay, and watch in Duck player navigates to Duck Player _User preferences "Never" stays in YT_ - [x] Open Settings -> Duck Player -> Set to never - [x] Type a YT URL on the omnibar - [x] Check video is loaded normally _Feature 1_ - [x] Open Duck Player - [x] Click Info button - [x] Check prime modal is correctly shown in both landscape and portrait _Feature 1_ - [x] See https://app.asana.com/0/1205008441501016/1207714050281768/f (How to test, at the bottom of the description) _Feature 1_ - [x] Open a video in Duck Player with settings to Always Ask - [x] Click the watch in YouTube Button - [x] Check overlay isn't shown ### UI changes See https://app.asana.com/app/asana/-/get_asset?asset_id=1207785858877769 ![Screenshot_20240802_180603](https://github.com/user-attachments/assets/b88b89cb-002d-4c6c-872c-9e4e23064a92) ![Screenshot_20240802_180616](https://github.com/user-attachments/assets/ff4b62e7-f637-4300-a653-95909f89da06) ### UI changes ![image](https://github.com/user-attachments/assets/f1cb7a9f-c612-4e5c-95d9-e14c8318fe78) ![image](https://github.com/user-attachments/assets/39b3fb78-2daf-43f8-aee5-8e6942cda3db) --------- Co-authored-by: Marcos Holgado --- .gitignore | 3 + app/build.gradle | 3 + .../app/browser/BrowserTabViewModelTest.kt | 463 ++- .../app/browser/BrowserWebViewClientTest.kt | 5 + .../browser/WebViewRequestInterceptorTest.kt | 67 +- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 73 +- .../referencetests/DomainsReferenceTest.kt | 3 + .../referencetests/SurrogatesReferenceTest.kt | 3 + app/src/main/AndroidManifest.xml | 3 + .../app/browser/BrowserTabFragment.kt | 101 +- .../app/browser/BrowserTabViewModel.kt | 113 +- .../app/browser/BrowserWebViewClient.kt | 24 +- .../app/browser/SpecialUrlDetector.kt | 43 +- .../app/browser/WebViewRequestInterceptor.kt | 12 +- .../app/browser/commands/Command.kt | 7 + .../app/browser/di/BrowserModule.kt | 15 +- .../browser/duckplayer/DuckPlayerJSHelper.kt | 240 ++ .../app/browser/omnibar/QueryUrlConverter.kt | 4 +- .../app/browser/viewstate/BrowserViewState.kt | 1 + .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 26 +- .../com/duckduckgo/app/pixels/AppPixelName.kt | 7 + .../app/settings/SettingsActivity.kt | 24 + .../app/settings/SettingsViewModel.kt | 6 + app/src/main/res/drawable/ic_duckplayer.xml | 58 + .../res/layout/content_settings_settings.xml | 7 + .../res/layout/include_omnibar_toolbar.xml | 10 + .../app/browser/SpecialUrlDetectorImplTest.kt | 50 +- .../app/cta/ui/OnboardingDaxDialogTests.kt | 3 + .../app/settings/SettingsViewModelTest.kt | 44 + .../impl/DuckPlayerSettingsViewModelTest.kt | 88 + .../impl/remoteconfig/AutoconsentFeature.kt | 2 +- .../app/browser/SpecialUrlDetector.kt | 1 + .../com/duckduckgo/app/browser/UriString.kt | 17 + .../common/ui/view/KeyboardAwareEditText.kt | 5 +- .../com/duckduckgo/common/utils/UrlScheme.kt | 1 + .../impl/RealContentScopeScripts.kt | 5 +- .../ContentScopeScriptsJsMessaging.kt | 23 +- .../impl/RealContentScopeScriptsTest.kt | 18 +- duckplayer/duckplayer-api/.gitignore | 0 duckplayer/duckplayer-api/build.gradle | 54 + .../duckduckgo/duckplayer/api/DuckPlayer.kt | 193 + .../duckplayer/api/DuckPlayerFeatureName.kt | 22 + .../api/DuckPlayerSettingsScreens.kt | 24 + .../duckplayer/api/PrivatePlayerMode.kt | 33 + duckplayer/duckplayer-impl/build.gradle | 91 + .../src/main/AndroidManifest.xml | 26 + .../DuckPlayerContentScopeConfigPlugin.kt | 59 + .../duckplayer/impl/DuckPlayerDataStore.kt | 281 ++ .../impl/DuckPlayerDataStoreModule.kt | 43 + .../DuckPlayerEnabledRMFMatchingAttribute.kt | 74 + .../duckplayer/impl/DuckPlayerFeature.kt | 45 + .../impl/DuckPlayerFeaturePlugin.kt | 39 + .../impl/DuckPlayerFeatureRepository.kt | 199 + .../impl/DuckPlayerFeatureSettingsStore.kt | 91 + .../impl/DuckPlayerLocalFilesPath.kt | 49 + ...DuckPlayerOnboardedRMFMatchingAttribute.kt | 68 + .../duckplayer/impl/DuckPlayerPixelName.kt | 34 + .../impl/DuckPlayerScriptsJsMessaging.kt | 133 + .../duckplayer/impl/DuckPlayerSettings.kt | 47 + .../impl/DuckPlayerSettingsActivity.kt | 176 + .../impl/DuckPlayerSettingsViewModel.kt | 114 + .../duckplayer/impl/JSONObjectAdapter.kt | 49 + .../duckplayer/impl/RealDuckPlayer.kt | 381 ++ .../duckplayer/impl/di/DuckPlayerModule.kt | 42 + .../impl/ui/DuckPlayerPrimeBottomSheet.kt | 84 + .../impl/ui/DuckPlayerPrimeDialogFragment.kt | 91 + .../src/main/res/drawable/clean_tube_128.xml | 20 + .../duck_player_animation_background.xml | 21 + ...ed_top_corners_bottom_sheet_background.xml | 23 + .../main/res/drawable/youtube_warning_96.xml | 21 + .../layout/activity_duck_player_settings.xml | 133 + .../src/main/res/layout/modal_duck_player.xml | 100 + .../src/main/res/raw/duckplayer.json | 1 + .../main/res/values-bg/strings-duckplayer.xml | 37 + .../main/res/values-cs/strings-duckplayer.xml | 37 + .../main/res/values-da/strings-duckplayer.xml | 37 + .../main/res/values-de/strings-duckplayer.xml | 37 + .../main/res/values-el/strings-duckplayer.xml | 37 + .../main/res/values-es/strings-duckplayer.xml | 37 + .../main/res/values-et/strings-duckplayer.xml | 37 + .../main/res/values-fi/strings-duckplayer.xml | 37 + .../main/res/values-fr/strings-duckplayer.xml | 37 + .../main/res/values-hr/strings-duckplayer.xml | 37 + .../main/res/values-hu/strings-duckplayer.xml | 37 + .../main/res/values-it/strings-duckplayer.xml | 37 + .../main/res/values-lt/strings-duckplayer.xml | 37 + .../main/res/values-lv/strings-duckplayer.xml | 37 + .../main/res/values-nb/strings-duckplayer.xml | 37 + .../main/res/values-nl/strings-duckplayer.xml | 37 + .../main/res/values-pl/strings-duckplayer.xml | 37 + .../main/res/values-pt/strings-duckplayer.xml | 37 + .../main/res/values-ro/strings-duckplayer.xml | 37 + .../main/res/values-ru/strings-duckplayer.xml | 37 + .../main/res/values-sk/strings-duckplayer.xml | 37 + .../main/res/values-sl/strings-duckplayer.xml | 37 + .../main/res/values-sv/strings-duckplayer.xml | 37 + .../main/res/values-tr/strings-duckplayer.xml | 37 + .../main/res/values/strings-duckplayer.xml | 36 + .../src/main/res/values/styles.xml | 32 + .../duckplayer/impl/RealDuckPlayerTest.kt | 771 ++++ duckplayer/readme.md | 9 + .../pages/duckplayer/assets/img/cog.svg | 3 + .../pages/duckplayer/assets/img/dax.svg | 15 + .../pages/duckplayer/assets/img/eyeball.svg | 8 + .../pages/duckplayer/assets/img/info-icon.svg | 12 + .../pages/duckplayer/assets/img/open.svg | 4 + .../pages/duckplayer/assets/img/player-bg.png | Bin 0 -> 205117 bytes .../pages/duckplayer/assets/player.css | 263 ++ .../build/android/pages/duckplayer/index.html | 14 + .../android/pages/duckplayer/js/index.css | 954 +++++ .../android/pages/duckplayer/js/index.js | 3603 +++++++++++++++++ .../android/pages/duckplayer/js/inline.js | 14 + .../pages/duckplayer/js/messages.example.js | 43 + .../android/pages/duckplayer/js/messages.js | 150 + .../duckplayer/js/mobile-bg-GCRU67TC.jpg | Bin 0 -> 7700 bytes .../duckplayer/js/player-bg-F7QLKTXS.jpg | Bin 0 -> 8788 bytes .../android/pages/duckplayer/js/storage.js | 32 + .../android/pages/duckplayer/js/utils.js | 40 + .../duckplayer/locales/bg/duckplayer.json | 38 + .../duckplayer/locales/cs/duckplayer.json | 38 + .../duckplayer/locales/da/duckplayer.json | 38 + .../duckplayer/locales/de/duckplayer.json | 38 + .../duckplayer/locales/el/duckplayer.json | 38 + .../duckplayer/locales/en/duckplayer.json | 39 + .../duckplayer/locales/es/duckplayer.json | 38 + .../duckplayer/locales/et/duckplayer.json | 38 + .../duckplayer/locales/fi/duckplayer.json | 38 + .../duckplayer/locales/fr/duckplayer.json | 38 + .../duckplayer/locales/hr/duckplayer.json | 38 + .../duckplayer/locales/hu/duckplayer.json | 38 + .../duckplayer/locales/it/duckplayer.json | 38 + .../duckplayer/locales/lt/duckplayer.json | 38 + .../duckplayer/locales/lv/duckplayer.json | 38 + .../duckplayer/locales/nb/duckplayer.json | 38 + .../duckplayer/locales/nl/duckplayer.json | 38 + .../duckplayer/locales/pl/duckplayer.json | 38 + .../duckplayer/locales/pt/duckplayer.json | 38 + .../duckplayer/locales/ro/duckplayer.json | 38 + .../duckplayer/locales/ru/duckplayer.json | 38 + .../duckplayer/locales/sk/duckplayer.json | 38 + .../duckplayer/locales/sl/duckplayer.json | 38 + .../duckplayer/locales/sv/duckplayer.json | 38 + .../duckplayer/locales/tr/duckplayer.json | 38 + .../settings/api/ProSettingsPlugin.kt | 5 + .../settings/impl/DuckPlayerSettingModule.kt | 27 + .../subscriptions/api/Subscriptions.kt | 4 +- .../subscriptions/impl/SubscriptionsDummy.kt | 2 +- .../subscriptions/impl/RealSubscriptions.kt | 5 +- 148 files changed, 12110 insertions(+), 214 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt create mode 100644 app/src/main/res/drawable/ic_duckplayer.xml create mode 100644 app/src/test/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModelTest.kt create mode 100644 duckplayer/duckplayer-api/.gitignore create mode 100644 duckplayer/duckplayer-api/build.gradle create mode 100644 duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt create mode 100644 duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerFeatureName.kt create mode 100644 duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerSettingsScreens.kt create mode 100644 duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/PrivatePlayerMode.kt create mode 100644 duckplayer/duckplayer-impl/build.gradle create mode 100644 duckplayer/duckplayer-impl/src/main/AndroidManifest.xml create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerContentScopeConfigPlugin.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStore.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStoreModule.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerEnabledRMFMatchingAttribute.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeaturePlugin.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureRepository.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureSettingsStore.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerLocalFilesPath.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerOnboardedRMFMatchingAttribute.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsActivity.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/JSONObjectAdapter.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/di/DuckPlayerModule.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeBottomSheet.kt create mode 100644 duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeDialogFragment.kt create mode 100644 duckplayer/duckplayer-impl/src/main/res/drawable/clean_tube_128.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/drawable/duck_player_animation_background.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/drawable/rounded_top_corners_bottom_sheet_background.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/drawable/youtube_warning_96.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/layout/activity_duck_player_settings.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/layout/modal_duck_player.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/raw/duckplayer.json create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-bg/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-cs/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-da/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-de/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-el/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-es/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-fi/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-fr/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-hr/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-hu/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-it/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-lt/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-lv/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-nb/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-nl/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-pl/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-pt/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-ro/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-ru/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-sk/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-sl/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-sv/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values-tr/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values/strings-duckplayer.xml create mode 100644 duckplayer/duckplayer-impl/src/main/res/values/styles.xml create mode 100644 duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt create mode 100644 duckplayer/readme.md create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/cog.svg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/dax.svg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/eyeball.svg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/info-icon.svg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/open.svg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/player-bg.png create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/player.css create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.css create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.example.js create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.js create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/mobile-bg-GCRU67TC.jpg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/player-bg-F7QLKTXS.jpg create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/storage.js create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/utils.js create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/bg/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/cs/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/da/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/de/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/el/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/en/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/es/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/et/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fi/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fr/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hr/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hu/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/it/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lt/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lv/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nb/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nl/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pl/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pt/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ro/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ru/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sk/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sl/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sv/duckplayer.json create mode 100644 node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/tr/duckplayer.json create mode 100644 settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/DuckPlayerSettingModule.kt diff --git a/.gitignore b/.gitignore index 7e118aad0eb4..a3e28221200c 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,7 @@ report /node_modules/@duckduckgo/content-scope-scripts/build/* !/node_modules/@duckduckgo/content-scope-scripts/build/android/ /node_modules/@duckduckgo/content-scope-scripts/build/android/* +!/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/ +/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/* +!/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/ !/node_modules/@duckduckgo/content-scope-scripts/build/android/contentScope.js \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 120a7e94448b..5dd704c19a31 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,7 @@ android { } assets { srcDirs += files("$projectDir/../node_modules/@duckduckgo/privacy-dashboard/build/app".toString()) + srcDirs += files("$projectDir/../node_modules/@duckduckgo/content-scope-scripts/build/android/pages".toString()) } } } @@ -182,6 +183,8 @@ fladle { dependencies { implementation project(":custom-tabs-impl") implementation project(":custom-tabs-api") + implementation project(":duckplayer-impl") + implementation project(":duckplayer-api") implementation project(":history-impl") implementation project(":history-api") implementation project(":data-store-impl") diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index f21fc43a6f78..5984899c4c83 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -76,12 +76,16 @@ import com.duckduckgo.app.browser.commands.Command import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxDialog import com.duckduckgo.app.browser.commands.Command.LaunchPrivacyPro import com.duckduckgo.app.browser.commands.Command.LoadExtractedUrl +import com.duckduckgo.app.browser.commands.Command.ShareLink import com.duckduckgo.app.browser.commands.Command.ShowBackNavigationHistory import com.duckduckgo.app.browser.commands.Command.ShowPrivacyProtectionDisabledConfirmation import com.duckduckgo.app.browser.commands.Command.ShowPrivacyProtectionEnabledConfirmation import com.duckduckgo.app.browser.commands.NavigationCommand import com.duckduckgo.app.browser.commands.NavigationCommand.Navigate import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames +import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_FEATURE_NAME +import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_PAGE_FEATURE_NAME +import com.duckduckgo.app.browser.duckplayer.DuckPlayerJSHelper import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favicon.FaviconSource import com.duckduckgo.app.browser.history.NavigationHistoryEntry @@ -140,6 +144,9 @@ import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_BANNER_SHOWN +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_SEARCH_CUSTOM import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_VISIT_SITE_CUSTOM import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature @@ -162,6 +169,7 @@ import com.duckduckgo.app.trackerdetection.model.TrackerType import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.widget.ui.WidgetCapabilities +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager @@ -176,6 +184,12 @@ import com.duckduckgo.common.utils.device.DeviceInfo import com.duckduckgo.downloads.api.DownloadStateListener import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.history.api.HistoryEntry.VisitedPage @@ -237,7 +251,6 @@ import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -262,146 +275,105 @@ class BrowserTabViewModelTest { @get:Rule var coroutineRule = CoroutineTestRule() - @Mock - private lateinit var mockEntityLookup: EntityLookup + private val mockEntityLookup: EntityLookup = mock() - @Mock - private lateinit var mockNetworkLeaderboardDao: NetworkLeaderboardDao + private val mockNetworkLeaderboardDao: NetworkLeaderboardDao = mock() - @Mock - private lateinit var mockStatisticsUpdater: StatisticsUpdater + private val mockStatisticsUpdater: StatisticsUpdater = mock() - @Mock - private lateinit var mockCommandObserver: Observer + private val mockCommandObserver: Observer = mock() - @Mock - private lateinit var mockSettingsStore: SettingsDataStore + private val mockSettingsStore: SettingsDataStore = mock() - @Mock - private lateinit var mockSavedSitesRepository: SavedSitesRepository + private val mockSavedSitesRepository: SavedSitesRepository = mock() - @Mock - private lateinit var mockNavigationHistory: NavigationHistory + private val mockNavigationHistory: NavigationHistory = mock() - @Mock - private lateinit var mockLongPressHandler: LongPressHandler + private val mockLongPressHandler: LongPressHandler = mock() - @Mock - private lateinit var mockOmnibarConverter: OmnibarEntryConverter + private val mockOmnibarConverter: OmnibarEntryConverter = mock() - @Mock - private lateinit var mockTabRepository: TabRepository + private val mockTabRepository: TabRepository = mock() - @Mock - private lateinit var webViewSessionStorage: WebViewSessionStorage + private val webViewSessionStorage: WebViewSessionStorage = mock() - @Mock - private lateinit var mockFaviconManager: FaviconManager + private val mockFaviconManager: FaviconManager = mock() - @Mock - private lateinit var mockAddToHomeCapabilityDetector: AddToHomeCapabilityDetector + private val mockAddToHomeCapabilityDetector: AddToHomeCapabilityDetector = mock() - @Mock - private lateinit var mockDismissedCtaDao: DismissedCtaDao + private val mockDismissedCtaDao: DismissedCtaDao = mock() - @Mock - private lateinit var mockSearchCountDao: SearchCountDao + private val mockSearchCountDao: SearchCountDao = mock() - @Mock - private lateinit var mockAppInstallStore: AppInstallStore + private val mockAppInstallStore: AppInstallStore = mock() - @Mock - private lateinit var mockPixel: Pixel + private val mockPixel: Pixel = mock() - @Mock - private lateinit var mockNewTabPixels: NewTabPixels + private val mockNewTabPixels: NewTabPixels = mock() - @Mock - private lateinit var mockHttpErrorPixels: HttpErrorPixels + private val mockHttpErrorPixels: HttpErrorPixels = mock() - @Mock - private lateinit var mockOnboardingStore: OnboardingStore + private val mockOnboardingStore: OnboardingStore = mock() - @Mock - private lateinit var mockAutoCompleteService: AutoCompleteService + private val mockAutoCompleteService: AutoCompleteService = mock() - @Mock - private lateinit var mockAutoCompleteScorer: AutoCompleteScorer + private val mockAutoCompleteScorer: AutoCompleteScorer = mock() - @Mock - private lateinit var mockWidgetCapabilities: WidgetCapabilities + private val mockWidgetCapabilities: WidgetCapabilities = mock() - @Mock - private lateinit var mockUserStageStore: UserStageStore + private val mockUserStageStore: UserStageStore = mock() - @Mock - private lateinit var mockContentBlocking: ContentBlocking + private val mockContentBlocking: ContentBlocking = mock() - @Mock - private lateinit var mockNavigationAwareLoginDetector: NavigationAwareLoginDetector + private val mockNavigationAwareLoginDetector: NavigationAwareLoginDetector = mock() - @Mock - private lateinit var mockUserEventsStore: UserEventsStore + private val mockUserEventsStore: UserEventsStore = mock() - @Mock - private lateinit var mockFileDownloader: FileDownloader + private val mockFileDownloader: FileDownloader = mock() - @Mock - private lateinit var geoLocationPermissions: GeoLocationPermissions + private val geoLocationPermissions: GeoLocationPermissions = mock() - @Mock - private lateinit var fireproofDialogsEventHandler: FireproofDialogsEventHandler + private val fireproofDialogsEventHandler: FireproofDialogsEventHandler = mock() - @Mock - private lateinit var mockEmailManager: EmailManager + private val mockEmailManager: EmailManager = mock() - @Mock - private lateinit var mockSpecialUrlDetector: SpecialUrlDetector + private val mockSpecialUrlDetector: SpecialUrlDetector = mock() - @Mock - private lateinit var mockAppLinksHandler: AppLinksHandler + private val mockAppLinksHandler: AppLinksHandler = mock() - @Mock - private lateinit var mockFeatureToggle: FeatureToggle + private val mockFeatureToggle: FeatureToggle = mock() - @Mock - private lateinit var mockGpcRepository: GpcRepository + private val mockGpcRepository: GpcRepository = mock() - @Mock - private lateinit var mockUnprotectedTemporary: UnprotectedTemporary + private val mockUnprotectedTemporary: UnprotectedTemporary = mock() - @Mock - private lateinit var mockAmpLinks: AmpLinks + private val mockAmpLinks: AmpLinks = mock() - @Mock - private lateinit var mockTrackingParameters: TrackingParameters + private val mockTrackingParameters: TrackingParameters = mock() - @Mock - private lateinit var mockDownloadCallback: DownloadStateListener + private val mockDownloadCallback: DownloadStateListener = mock() - @Mock - private lateinit var mockRemoteMessagingRepository: RemoteMessagingRepository + private val mockRemoteMessagingRepository: RemoteMessagingRepository = mock() - @Mock - private lateinit var voiceSearchAvailability: VoiceSearchAvailability + private val voiceSearchAvailability: VoiceSearchAvailability = mock() - @Mock - private lateinit var voiceSearchPixelLogger: VoiceSearchAvailabilityPixelLogger + private val voiceSearchPixelLogger: VoiceSearchAvailabilityPixelLogger = mock() - @Mock - private lateinit var mockSettingsDataStore: SettingsDataStore + private val mockSettingsDataStore: SettingsDataStore = mock() - @Mock - private lateinit var mockAdClickManager: AdClickManager + private val mockAdClickManager: AdClickManager = mock() - @Mock - private lateinit var mockUserAllowListRepository: UserAllowListRepository + private val mockUserAllowListRepository: UserAllowListRepository = mock() - @Mock - private lateinit var mockBrokenSiteContext: BrokenSiteContext + private val mockBrokenSiteContext: BrokenSiteContext = mock() - @Mock - private lateinit var mockFileChooserCallback: ValueCallback> + private val mockFileChooserCallback: ValueCallback> = mock() + + private val mockDuckPlayer: DuckPlayer = mock() + + private val mockAppBuildConfig: AppBuildConfig = mock() + + private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock() private lateinit var remoteMessagingModel: RemoteMessagingModel @@ -485,7 +457,7 @@ class BrowserTabViewModelTest { private val mockAutoCompleteRepository: AutoCompleteRepository = mock() @Before - fun before() { + fun before() = runTest { MockitoAnnotations.openMocks(this) db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) .allowMainThreadQueries() @@ -508,6 +480,7 @@ class BrowserTabViewModelTest { lazyFaviconManager, ) + whenever(mockDuckPlayer.observeUserPreferences()).thenReturn(flowOf(UserPreferences(false, Disabled))) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow()) whenever(mockTabRepository.flowTabs).thenReturn(flowOf(emptyList())) whenever(mockTabRepository.liveTabs).thenReturn(tabsLiveData) @@ -520,6 +493,11 @@ class BrowserTabViewModelTest { whenever(mockSSLCertificatesFeature.allowBypass()).thenReturn(mockEnabledToggle) whenever(mockExtendedOnboardingFeatureToggles.aestheticUpdates()).thenReturn(mockEnabledToggle) whenever(subscriptions.shouldLaunchPrivacyProForUrl(any())).thenReturn(false) + whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoUrl(any())).thenReturn(false) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(any())).thenReturn(false) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false) + whenever(mockDuckPlayer.isDuckPlayerUri(anyString())).thenReturn(false) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider) @@ -537,6 +515,7 @@ class BrowserTabViewModelTest { duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(), extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles, subscriptions = mock(), + duckPlayer = mockDuckPlayer, ) val siteFactory = SiteFactoryImpl( @@ -567,6 +546,8 @@ class BrowserTabViewModelTest { whenever(fireproofDialogsEventHandler.event).thenReturn(fireproofDialogsEventHandlerLiveData) whenever(cameraHardwareChecker.hasCameraHardware()).thenReturn(true) whenever(mockPrivacyProtectionsPopupManager.viewState).thenReturn(flowOf(PrivacyProtectionsPopupViewState.Gone)) + whenever(mockAppBuildConfig.buildType).thenReturn("debug") + whenever(mockDuckPlayer.observeUserPreferences()).thenReturn(flowOf(UserPreferences(false, AlwaysAsk))) testee = BrowserTabViewModel( statisticsUpdater = mockStatisticsUpdater, @@ -630,6 +611,8 @@ class BrowserTabViewModelTest { history = mockNavigationHistory, newTabPixels = { mockNewTabPixels }, httpErrorPixels = { mockHttpErrorPixels }, + duckPlayer = mockDuckPlayer, + duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel, mockDuckDuckGoUrlDetector), ) testee.loadData("abc", null, false, false) @@ -2661,7 +2644,7 @@ class BrowserTabViewModelTest { } @Test - fun whenUserClicksOnRemoveFireproofingSnackbarUndoActionThenPixelSent() { + fun whenUserClicksOnRemoveFireproofingSnackbarUndoActionThenPixelSent() = runTest { givenFireproofWebsiteDomain("example.com") loadUrl("http://example.com/", isBrowserShowing = true) testee.onFireproofWebsiteMenuClicked() @@ -3754,9 +3737,10 @@ class BrowserTabViewModelTest { @Test fun whenEmailSignOutEventThenEmailSignEventCommandSent() = runTest { + emailStateFlow.emit(true) emailStateFlow.emit(false) - assertCommandIssued() + assertCommandIssuedTimes(2) } @Test @@ -4839,7 +4823,13 @@ class BrowserTabViewModelTest { fun whenProcessJsCallbackMessageWebShareSendCommand() = runTest { val url = "someUrl" loadUrl(url) - testee.processJsCallbackMessage("myFeature", "webShare", "myId", JSONObject("""{ "my":"object"}""")) + testee.processJsCallbackMessage( + "myFeature", + "webShare", + "myId", + JSONObject("""{ "my":"object"}"""), + "someUrl", + ) assertCommandIssued { assertEquals("object", this.data.params.getString("my")) assertEquals("myFeature", this.data.featureName) @@ -4853,7 +4843,13 @@ class BrowserTabViewModelTest { val url = "someUrl" loadUrl(url) whenever(mockSitePermissionsManager.getPermissionsQueryResponse(eq(url), any(), any())).thenReturn(SitePermissionQueryResponse.Granted) - testee.processJsCallbackMessage("myFeature", "permissionsQuery", "myId", JSONObject("""{ "name":"somePermission"}""")) + testee.processJsCallbackMessage( + "myFeature", + "permissionsQuery", + "myId", + JSONObject("""{ "name":"somePermission"}"""), + "someUrl", + ) assertCommandIssued { assertEquals("granted", this.data.params.getString("state")) assertEquals("myFeature", this.data.featureName) @@ -4865,14 +4861,26 @@ class BrowserTabViewModelTest { @Test fun whenProcessJsCallbackMessageScreenLockNotEnabledDoNotSendCommand() = runTest { whenever(mockEnabledToggle.isEnabled()).thenReturn(false) - testee.processJsCallbackMessage("myFeature", "screenLock", "myId", JSONObject("""{ "my":"object"}""")) + testee.processJsCallbackMessage( + "myFeature", + "screenLock", + "myId", + JSONObject("""{ "my":"object"}"""), + "someUrl", + ) assertCommandNotIssued() } @Test fun whenProcessJsCallbackMessageScreenLockEnabledSendCommand() = runTest { whenever(mockEnabledToggle.isEnabled()).thenReturn(true) - testee.processJsCallbackMessage("myFeature", "screenLock", "myId", JSONObject("""{ "my":"object"}""")) + testee.processJsCallbackMessage( + "myFeature", + "screenLock", + "myId", + JSONObject("""{ "my":"object"}"""), + "someUrl", + ) assertCommandIssued { assertEquals("object", this.data.params.getString("my")) assertEquals("myFeature", this.data.featureName) @@ -4884,17 +4892,189 @@ class BrowserTabViewModelTest { @Test fun whenProcessJsCallbackMessageScreenUnlockNotEnabledDoNotSendCommand() = runTest { whenever(mockEnabledToggle.isEnabled()).thenReturn(false) - testee.processJsCallbackMessage("myFeature", "screenUnlock", "myId", JSONObject("""{ "my":"object"}""")) + testee.processJsCallbackMessage( + "myFeature", + "screenUnlock", + "myId", + JSONObject("""{ "my":"object"}"""), + "someUrl", + ) assertCommandNotIssued() } @Test fun whenProcessJsCallbackMessageScreenUnlockEnabledSendCommand() = runTest { whenever(mockEnabledToggle.isEnabled()).thenReturn(true) - testee.processJsCallbackMessage("myFeature", "screenUnlock", "myId", JSONObject("""{ "my":"object"}""")) + testee.processJsCallbackMessage( + "myFeature", + "screenUnlock", + "myId", + JSONObject("""{ "my":"object"}"""), + "someUrl", + ) assertCommandIssued() } + @Test + fun whenProcessJsCallbackMessageGetUserPreferencesFromOverlayThenSendCommand() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "getUserValues", + "id", + data = null, + "someUrl", + ) + assertCommandIssued() + } + + @Test + fun whenProcessJsCallbackMessageSetUserPreferencesDisabledFromDuckPlayerOverlayThenSendCommand() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "setUserValues", + "id", + JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {disabled: {} }}"""), + "someUrl", + ) + assertCommandIssued() + verify(mockDuckPlayer).setUserPreferences(any(), any()) + verify(mockPixel).fire(DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE) + } + + @Test + fun whenProcessJsCallbackMessageSetUserPreferencesEnabledFromDuckPlayerOverlayThenSendCommand() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "setUserValues", + "id", + JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {enabled: {} }}"""), + "someUrl", + ) + assertCommandIssued() + verify(mockDuckPlayer).setUserPreferences(any(), any()) + verify(mockPixel).fire(DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE) + } + + @Test + fun whenProcessJsCallbackMessageSetUserPreferencesFromDuckPlayerPageThenSendCommand() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_PAGE_FEATURE_NAME, + "setUserValues", + "id", + JSONObject("""{ overlayInteracted: "true", privatePlayerMode: {enabled: {} }}"""), + "someUrl", + ) + assertCommandIssued() + verify(mockDuckPlayer).setUserPreferences(true, "enabled") + verify(mockPixel).fire(DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER) + } + + @Test + fun whenProcessJsCallbackMessageSendDuckPlayerPixelThenSendPixel() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "sendDuckPlayerPixel", + "id", + JSONObject("""{ pixelName: "pixel", params: {}}"""), + "someUrl", + ) + verify(mockDuckPlayer).sendDuckPlayerPixel("pixel", mapOf()) + } + + @Test + fun whenProcessJsCallbackMessageOpenDuckPlayerWithUrlThenNavigate() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "openDuckPlayer", + "id", + JSONObject("""{ href: "duck://player/1234" }"""), + "someUrl", + ) + assertCommandIssued() + } + + @Test + fun whenProcessJsCallbackMessageOpenDuckPlayerWithoutUrlThenDoNotNavigate() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "openDuckPlayer", + "id", + null, + "someUrl", + ) + assertCommandNotIssued() + } + + @Test + fun whenJsCallbackMessageInitialSetupFromOverlayThenSendResponseToJs() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_FEATURE_NAME, + "initialSetup", + "id", + null, + "someUrl", + ) + assertCommandIssued() + } + + @Test + fun whenJsCallbackMessageInitialSetupFromDuckPlayerPageThenSendResponseToDuckPlayer() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(overlayInteracted = true, privatePlayerMode = AlwaysAsk)) + testee.processJsCallbackMessage( + DUCK_PLAYER_PAGE_FEATURE_NAME, + "initialSetup", + "id", + null, + "someUrl", + ) + assertCommandIssued() + } + + @Test + fun whenJsCallbackMessageOpenSettingsThenOpenSettings() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + testee.processJsCallbackMessage( + DUCK_PLAYER_PAGE_FEATURE_NAME, + "openSettings", + "id", + null, + "someUrl", + ) + assertCommandIssued() + } + + @Test + fun whenJsCallbackMessageOpenInfoThenOpenInfo() = runTest { + whenever(mockEnabledToggle.isEnabled()).thenReturn(true) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) + + testee.processJsCallbackMessage( + DUCK_PLAYER_PAGE_FEATURE_NAME, + "openInfo", + "id", + null, + "someUrl", + ) + assertCommandIssued() + } + @Test fun whenPrivacyProtectionMenuClickedThenListenerIsInvoked() = runTest { loadUrl("http://www.example.com/home.html") @@ -5570,6 +5750,70 @@ class BrowserTabViewModelTest { assertCommandIssued() } + @Test + fun whenNewPageWithUrlYouTubeNoCookieThenReplaceUrlWithDuckPlayer() = runTest { + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn(true) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("duck://player/1234".toUri())).thenReturn(false) + whenever(mockDuckPlayer.createDuckPlayerUriFromYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn( + "duck://player/1234", + ) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) + testee.browserViewState.value = browserViewState().copy(browserShowing = true) + + testee.navigationStateChanged(buildWebNavigation("https://youtube-nocookie.com/?videoID=1234")) + + assertEquals("duck://player/1234", omnibarViewState().omnibarText) + } + + @Test + fun whenNewPageWithUrlYouTubeNoCookieThenShowDuckPlayerIcon() = runTest { + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn(true) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("duck://player/1234".toUri())).thenReturn(false) + whenever(mockDuckPlayer.createDuckPlayerUriFromYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn( + "duck://player/1234", + ) + whenever(mockDuckPlayer.isDuckPlayerUri("duck://player/1234")).thenReturn(true) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) + + testee.browserViewState.value = browserViewState().copy(browserShowing = true) + + testee.navigationStateChanged(buildWebNavigation("https://youtube-nocookie.com/?videoID=1234")) + + assertTrue(browserViewState().showDuckPlayerIcon) + } + + @Test + fun whenUrlUpdatedWithUrlYouTubeNoCookieThenReplaceUrlWithDuckPlayer() = runTest { + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn(true) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("duck://player/1234".toUri())).thenReturn(false) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie("http://example.com".toUri())).thenReturn(false) + whenever(mockDuckPlayer.createDuckPlayerUriFromYoutubeNoCookie("https://youtube-nocookie.com/?videoID=1234".toUri())).thenReturn( + "duck://player/1234", + ) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) + + testee.browserViewState.value = browserViewState().copy(browserShowing = true) + + testee.navigationStateChanged(buildWebNavigation("http://example.com")) + testee.navigationStateChanged(buildWebNavigation("https://youtube-nocookie.com/?videoID=1234")) + + assertEquals("duck://player/1234", omnibarViewState().omnibarText) + } + + @Test + fun whenSharingDuckPlayerUrlThenReplaceWithYouTubeUrl() = runTest { + givenCurrentSite("duck://player/1234") + whenever(mockDuckPlayer.isDuckPlayerUri("duck://player/1234")).thenReturn(true) + whenever(mockDuckPlayer.createYoutubeWatchUrlFromDuckPlayer(any())).thenReturn("https://youtube.com/watch?v=1234") + + testee.onShareSelected() + + assertCommandIssued { + assertEquals("https://youtube.com/watch?v=1234", this.url) + } + } + private fun aCredential(): LoginCredentials { return LoginCredentials(domain = null, username = null, password = null) } @@ -5737,7 +5981,11 @@ class BrowserTabViewModelTest { url: String?, title: String? = null, isBrowserShowing: Boolean = true, - ) { + ) = runTest { + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyUri())).thenReturn(false) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(DISABLED) + whenever(mockDuckPlayer.observeUserPreferences()).thenReturn(flowOf(UserPreferences(false, Disabled))) + setBrowserShowing(isBrowserShowing) testee.navigationStateChanged(buildWebNavigation(originalUrl = url, currentUrl = url, title = title)) } @@ -5747,7 +5995,8 @@ class BrowserTabViewModelTest { originalUrl: String?, currentUrl: String?, isBrowserShowing: Boolean, - ) { + ) = runTest { + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyUri())).thenReturn(false) setBrowserShowing(isBrowserShowing) testee.navigationStateChanged(buildWebNavigation(originalUrl = originalUrl, currentUrl = currentUrl)) } @@ -5837,6 +6086,8 @@ class BrowserTabViewModelTest { private fun browserGlobalLayoutViewState() = testee.globalLayoutState.value!! as GlobalLayoutViewState.Browser private fun accessibilityViewState() = testee.accessibilityViewState.value!! + fun anyUri(): Uri = any() + class FakeCapabilityChecker(var enabled: Boolean) : AutofillCapabilityChecker { override suspend fun isAutofillEnabledByConfiguration(url: String) = enabled override suspend fun canInjectCredentialsToWebView(url: String) = enabled diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index cc6877535a8a..be278749888f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -68,6 +68,7 @@ import com.duckduckgo.common.utils.CurrentTimeProvider import com.duckduckgo.common.utils.device.DeviceInfo import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.cookies.api.CookieManagerProvider +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.subscriptions.api.Subscriptions @@ -132,6 +133,7 @@ class BrowserWebViewClientTest { private val pagePaintedHandler: PagePaintedHandler = mock() private val mediaPlayback: MediaPlayback = mock() private val subscriptions: Subscriptions = mock() + private val mockDuckPlayer: DuckPlayer = mock() private val navigationHistory: NavigationHistory = mock() @UiThreadTest @@ -165,6 +167,7 @@ class BrowserWebViewClientTest { navigationHistory, mediaPlayback, subscriptions, + mockDuckPlayer, ) testee.webViewClientListener = listener whenever(webResourceRequest.url).thenReturn(Uri.EMPTY) @@ -804,6 +807,8 @@ class BrowserWebViewClientTest { whenever(mockWebView.progress).thenReturn(100) whenever(mockWebView.safeCopyBackForwardList()).thenReturn(TestBackForwardList()) whenever(mockWebView.settings).thenReturn(mock()) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false) + whenever(mockDuckPlayer.isYoutubeWatchUrl(any())).thenReturn(false) testee.onPageStarted(mockWebView, EXAMPLE_URL, null) whenever(currentTimeProvider.elapsedRealtime()).thenReturn(10) testee.onPageFinished(mockWebView, EXAMPLE_URL) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt index ca97197be1f3..2753e38780ee 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt @@ -19,7 +19,12 @@ package com.duckduckgo.app.browser import android.net.Uri -import android.webkit.* +import android.webkit.WebBackForwardList +import android.webkit.WebHistoryItem +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView import androidx.core.net.toUri import androidx.test.annotation.UiThreadTest import com.duckduckgo.adclick.api.AdClickManager @@ -38,6 +43,7 @@ import com.duckduckgo.app.trackerdetection.model.TrackerStatus import com.duckduckgo.app.trackerdetection.model.TrackerType import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.httpsupgrade.api.HttpsUpgrader import com.duckduckgo.privacy.config.api.Gpc @@ -47,13 +53,20 @@ import com.duckduckgo.user.agent.api.UserAgentProvider import com.duckduckgo.user.agent.impl.RealUserAgentProvider import com.duckduckgo.user.agent.impl.UserAgent import kotlinx.coroutines.test.runTest -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyMap import org.mockito.ArgumentMatchers.anyString -import org.mockito.kotlin.* +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever class WebViewRequestInterceptorTest { @@ -72,6 +85,7 @@ class WebViewRequestInterceptorTest { private val mockAdClickManager: AdClickManager = mock() private val mockCloakedCnameDetector: CloakedCnameDetector = mock() private val mockRequestFilterer: RequestFilterer = mock() + private val mockDuckPlayer: DuckPlayer = mock() private val fakeUserAgent: UserAgent = UserAgentFake() private val fakeToggle: FeatureToggle = FeatureToggleFake() private val fakeUserAllowListRepository = UserAllowListRepositoryFake() @@ -102,6 +116,7 @@ class WebViewRequestInterceptorTest { adClickManager = mockAdClickManager, cloakedCnameDetector = mockCloakedCnameDetector, requestFilterer = mockRequestFilterer, + duckPlayer = mockDuckPlayer, ) } @@ -158,6 +173,47 @@ class WebViewRequestInterceptorTest { verify(mockHttpsUpgrader, never()).upgrade(any()) } + @Test + fun whenInterceptUrlAndShouldUpgradeThenShouldUpgradeIsCalledAndNotDuckPlayer() = runTest { + configureShouldUpgrade() + configureDuckPlayer() + testee.shouldIntercept( + request = mockRequest, + documentUri = null, + webView = webView, + webViewClientListener = null, + ) + verify(mockHttpsUpgrader).upgrade(any()) + verify(mockDuckPlayer, never()).intercept(any(), any(), any()) + } + + @Test + fun whenInterceptUrlWithNullUrlThenDuckPlayerInterceptNotCalled() = runTest { + configureDuckPlayer() + whenever(mockRequest.url).thenReturn(null) + + testee.shouldIntercept( + request = mockRequest, + documentUri = null, + webView = webView, + webViewClientListener = null, + ) + + verify(mockDuckPlayer, never()).intercept(any(), any(), any()) + } + + @Test + fun whenInterceptUrlDuckPlayerInterceptIsCalled() = runTest { + configureDuckPlayer() + testee.shouldIntercept( + request = mockRequest, + documentUri = null, + webView = webView, + webViewClientListener = null, + ) + verify(mockDuckPlayer).intercept(any(), any(), any()) + } + @Test fun whenUrlShouldBeUpgradedButUrlIsNullThenNotUpgraded() = runTest { configureShouldUpgrade() @@ -779,6 +835,11 @@ class WebViewRequestInterceptorTest { whenever(mockRequest.isForMainFrame).thenReturn(true) } + private fun configureDuckPlayer() = runTest { + whenever(mockRequest.url).thenReturn(validUri()) + whenever(mockDuckPlayer.intercept(any(), any(), any())).thenReturn(mock()) + } + private fun configureShouldNotUpgrade() = runTest { whenever(mockHttpsUpgrader.shouldUpgrade(any())).thenReturn(false) diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 42cd11a662ae..112d9b18d791 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.cta.ui import android.content.Context import android.net.Uri import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.core.net.toUri import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl @@ -40,6 +41,7 @@ import com.duckduckgo.app.privacy.model.TestEntity import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.model.Entity @@ -49,6 +51,11 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.subscriptions.api.Subscriptions import java.util.concurrent.TimeUnit @@ -62,8 +69,7 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations +import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.* @FlowPreview @@ -83,38 +89,29 @@ class CtaViewModelTest { private lateinit var db: AppDatabase - @Mock - private lateinit var mockWidgetCapabilities: WidgetCapabilities + private val mockWidgetCapabilities: WidgetCapabilities = mock() - @Mock - private lateinit var mockDismissedCtaDao: DismissedCtaDao + private val mockDismissedCtaDao: DismissedCtaDao = mock() - @Mock - private lateinit var mockPixel: Pixel + private val mockPixel: Pixel = mock() - @Mock - private lateinit var mockAppInstallStore: AppInstallStore + private val mockAppInstallStore: AppInstallStore = mock() - @Mock - private lateinit var mockSettingsDataStore: SettingsDataStore + private val mockSettingsDataStore: SettingsDataStore = mock() - @Mock - private lateinit var mockOnboardingStore: OnboardingStore + private val mockOnboardingStore: OnboardingStore = mock() - @Mock - private lateinit var mockUserAllowListRepository: UserAllowListRepository + private val mockUserAllowListRepository: UserAllowListRepository = mock() - @Mock - private lateinit var mockUserStageStore: UserStageStore + private val mockUserStageStore: UserStageStore = mock() - @Mock - private lateinit var mockTabRepository: TabRepository + private val mockTabRepository: TabRepository = mock() - @Mock - private lateinit var mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles + private val mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock() - @Mock - private lateinit var mockSubscriptions: Subscriptions + private val mockDuckPlayer: DuckPlayer = mock() + + private val mockSubscriptions: Subscriptions = mock() private val requiredDaxOnboardingCtas: List = listOf( CtaId.DAX_INTRO, @@ -132,8 +129,7 @@ class CtaViewModelTest { private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false } @Before - fun before() { - MockitoAnnotations.openMocks(this) + fun before() = runTest { db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) .allowMainThreadQueries() .build() @@ -145,6 +141,11 @@ class CtaViewModelTest { whenever(mockUserAllowListRepository.isDomainInUserAllowList(any())).thenReturn(false) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(db.dismissedCtaDao().dismissedCtas()) whenever(mockTabRepository.flowTabs).thenReturn(db.tabsDao().flowTabs()) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(DISABLED) + whenever(mockDuckPlayer.isDuckPlayerUri(any())).thenReturn(false) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(false, AlwaysAsk)) + whenever(mockDuckPlayer.isYouTubeUrl(any())).thenReturn(false) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false) testee = CtaViewModel( appInstallStore = mockAppInstallStore, @@ -160,6 +161,7 @@ class CtaViewModelTest { duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(), extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles, subscriptions = mockSubscriptions, + duckPlayer = mockDuckPlayer, ) } @@ -738,11 +740,28 @@ class CtaViewModelTest { @Test fun givenPrivacyProSiteWhenRefreshCtaWhileBrowsingThenReturnNull() = runTest { val privacyProUrl = "https://duckduckgo.com/pro" - whenever(mockSubscriptions.isPrivacyProUrl(privacyProUrl)).thenReturn(true) + whenever(mockSubscriptions.isPrivacyProUrl(privacyProUrl.toUri())).thenReturn(true) givenDaxOnboardingActive() val site = site(url = privacyProUrl) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) + verify(mockPixel, never()).fire(eq(ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE), any(), any(), eq(UNIQUE)) + assertNull(value) + } + + @Test + fun givenDuckPlayerSiteWhenRefreshCtaWhileBrowsingThenReturnNull() = runTest { + givenDaxOnboardingActive() + val site = site(url = "duck://player/12345") + + whenever(mockDuckPlayer.isDuckPlayerUri(any())).thenReturn(true) + whenever(mockDuckPlayer.getDuckPlayerState()).thenReturn(ENABLED) + whenever(mockDuckPlayer.getUserPreferences()).thenReturn(UserPreferences(false, AlwaysAsk)) + whenever(mockDuckPlayer.isYouTubeUrl(any())).thenReturn(false) + whenever(mockDuckPlayer.isSimulatedYoutubeNoCookie(anyString())).thenReturn(false) + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) + verify(mockPixel).fire(eq(ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE), any(), any(), eq(UNIQUE)) assertNull(value) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt index 62141e153cec..9bc809b2efdf 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt @@ -54,6 +54,7 @@ import com.duckduckgo.app.trackerdetection.db.TdsEntityDao import com.duckduckgo.app.trackerdetection.db.WebTrackersBlockedDao import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.httpsupgrade.api.HttpsUpgrader import com.duckduckgo.privacy.config.api.ContentBlocking @@ -103,6 +104,7 @@ class DomainsReferenceTest(private val testCase: TestCase) { private var mockRequest: WebResourceRequest = mock() private val mockPrivacyProtectionCountDao: PrivacyProtectionCountDao = mock() private val mockRequestFilterer: RequestFilterer = mock() + private val mockDuckPlayer: DuckPlayer = mock() private val mockUserAllowListRepository: UserAllowListRepository = mock() private val fakeUserAgent: UserAgent = UserAgentFake() private val fakeToggle: FeatureToggle = FeatureToggleFake() @@ -172,6 +174,7 @@ class DomainsReferenceTest(private val testCase: TestCase) { adClickManager = mockAdClickManager, cloakedCnameDetector = CloakedCnameDetectorImpl(tdsCnameEntityDao, mockTrackerAllowlist, mockUserAllowListRepository), requestFilterer = mockRequestFilterer, + duckPlayer = mockDuckPlayer, ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt index 1eac7fe79a32..83fb7607d59f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt @@ -52,6 +52,7 @@ import com.duckduckgo.app.trackerdetection.db.TdsEntityDao import com.duckduckgo.app.trackerdetection.db.WebTrackersBlockedDao import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.httpsupgrade.api.HttpsUpgrader import com.duckduckgo.privacy.config.api.ContentBlocking @@ -99,6 +100,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) { private var mockRequest: WebResourceRequest = mock() private val mockPrivacyProtectionCountDao: PrivacyProtectionCountDao = mock() private val mockRequestFilterer: RequestFilterer = mock() + private val mockDuckPlayer: DuckPlayer = mock() private val fakeUserAgent: UserAgent = UserAgentFake() private val fakeToggle: FeatureToggle = FeatureToggleFake() private val fakeUserAllowListRepository = UserAllowListRepositoryFake() @@ -168,6 +170,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) { adClickManager = mockAdClickManager, cloakedCnameDetector = mockCloakedCnameDetector, requestFilterer = mockRequestFilterer, + duckPlayer = mockDuckPlayer, ) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f8c8b211695..e6474231c8e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -247,6 +247,7 @@ + @@ -258,6 +259,7 @@ + @@ -280,6 +282,7 @@ + diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 39b1f3ffbaa6..850d66dd613a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -22,7 +22,11 @@ import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.app.ActivityOptions import android.app.PendingIntent -import android.content.* +import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo @@ -31,7 +35,12 @@ import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.net.Uri -import android.os.* +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.os.Message import android.print.PrintAttributes import android.print.PrintManager import android.provider.MediaStore @@ -40,8 +49,14 @@ import android.text.Spannable import android.text.SpannableString import android.text.Spanned import android.text.style.StyleSpan -import android.view.* -import android.view.View.* +import android.view.ContextMenu +import android.view.KeyEvent +import android.view.MenuItem +import android.view.View +import android.view.View.GONE +import android.view.View.OnFocusChangeListener +import android.view.View.VISIBLE +import android.view.ViewGroup import android.view.ViewGroup.LayoutParams import android.view.inputmethod.EditorInfo import android.webkit.PermissionRequest @@ -53,7 +68,9 @@ import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebView.FindListener import android.webkit.WebView.HitTestResult -import android.webkit.WebView.HitTestResult.* +import android.webkit.WebView.HitTestResult.IMAGE_TYPE +import android.webkit.WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE +import android.webkit.WebView.HitTestResult.UNKNOWN_TYPE import android.widget.EditText import android.widget.FrameLayout import android.widget.TextView @@ -68,13 +85,21 @@ import androidx.core.net.toUri import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.core.text.toSpannable -import androidx.core.view.* +import androidx.core.view.doOnLayout +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.core.view.postDelayed import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.commitNow import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.transaction -import androidx.lifecycle.* +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.webkit.JavaScriptReplyProxy import androidx.webkit.WebMessageCompat @@ -157,7 +182,11 @@ import com.duckduckgo.app.browser.webshare.WebShareChooser import com.duckduckgo.app.browser.webview.WebContentDebugging import com.duckduckgo.app.browser.webview.WebViewBlobDownloadFeature import com.duckduckgo.app.browser.webview.safewebview.SafeWebViewFeature -import com.duckduckgo.app.cta.ui.* +import com.duckduckgo.app.cta.ui.Cta +import com.duckduckgo.app.cta.ui.CtaViewModel +import com.duckduckgo.app.cta.ui.DaxBubbleCta +import com.duckduckgo.app.cta.ui.HomePanelCta +import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.website @@ -253,6 +282,8 @@ import com.duckduckgo.downloads.api.DownloadConfirmationDialogListener import com.duckduckgo.downloads.api.DownloadsFileActions import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.js.messaging.api.JsMessageCallback import com.duckduckgo.js.messaging.api.JsMessaging @@ -291,10 +322,16 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Provider import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.json.JSONObject import timber.log.Timber @@ -457,6 +494,10 @@ class BrowserTabFragment : @Named("ContentScopeScripts") lateinit var contentScopeScripts: JsMessaging + @Inject + @Named("DuckPlayer") + lateinit var duckPlayerScripts: JsMessaging + @Inject lateinit var webContentDebugging: WebContentDebugging @@ -505,6 +546,9 @@ class BrowserTabFragment : @Inject lateinit var webBrokenSiteForm: WebBrokenSiteForm + @Inject + lateinit var duckPlayer: DuckPlayer + /** * We use this to monitor whether the user was seeing the in-context Email Protection signup prompt * This is needed because the activity stack will be cleared if an external link is opened in our browser @@ -1550,6 +1594,7 @@ class BrowserTabFragment : is Command.WebViewError -> showError(it.errorType, it.url) is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data) + is Command.SendResponseToDuckPlayer -> duckPlayerScripts.onResponse(it.data) is Command.WebShareRequest -> webShareRequest.launch(it.data) is Command.ScreenLock -> screenLock(it.data) is Command.ScreenUnlock -> screenUnlock() @@ -1561,6 +1606,21 @@ class BrowserTabFragment : is Command.HideOnboardingDaxDialog -> hideOnboardingDaxDialog(it.onboardingCta) is Command.ShowRemoveSearchSuggestionDialog -> showRemoveSearchSuggestionDialog(it.suggestion) is Command.AutocompleteItemRemoved -> autocompleteItemRemoved() + is Command.OpenDuckPlayerSettings -> globalActivityStarter.start(binding.root.context, DuckPlayerSettingsNoParams) + is Command.OpenDuckPlayerPageInfo -> { + context?.resources?.configuration?.let { + duckPlayer.showDuckPlayerPrimeModal(it, childFragmentManager, fromDuckPlayerPage = true) + } + } + is Command.OpenDuckPlayerOverlayInfo -> { + context?.resources?.configuration?.let { + duckPlayer.showDuckPlayerPrimeModal(it, childFragmentManager, fromDuckPlayerPage = false) + } + } + is Command.SendSubscriptions -> { + contentScopeScripts.sendSubscriptionEvent(it.cssData) + duckPlayerScripts.sendSubscriptionEvent(it.duckPlayerData) + } else -> { // NO OP } @@ -2369,7 +2429,24 @@ class BrowserTabFragment : id: String?, data: JSONObject?, ) { - viewModel.processJsCallbackMessage(featureName, method, id, data) + appCoroutineScope.launch(dispatchers.main()) { + viewModel.processJsCallbackMessage(featureName, method, id, data, it.url) + } + } + }, + ) + duckPlayerScripts.register( + it, + object : JsMessageCallback() { + override fun process( + featureName: String, + method: String, + id: String?, + data: JSONObject?, + ) { + appCoroutineScope.launch(dispatchers.main()) { + viewModel.processJsCallbackMessage(featureName, method, id, data, it.url) + } } }, ) @@ -3904,11 +3981,13 @@ class BrowserTabFragment : private fun renderToolbarMenus(viewState: BrowserViewState) { if (viewState.browserShowing) { omnibar.daxIcon?.isVisible = viewState.showDaxIcon - omnibar.shieldIcon?.isInvisible = !viewState.showPrivacyShield.isEnabled() || viewState.showDaxIcon + omnibar.duckPlayerIcon.isVisible = viewState.showDuckPlayerIcon + omnibar.shieldIcon?.isInvisible = !viewState.showPrivacyShield.isEnabled() || viewState.showDaxIcon || viewState.showDuckPlayerIcon omnibar.clearTextButton?.isVisible = viewState.showClearButton omnibar.searchIcon?.isVisible = viewState.showSearchIcon } else { omnibar.daxIcon.isVisible = false + omnibar.duckPlayerIcon.isVisible = false omnibar.shieldIcon?.isVisible = false omnibar.clearTextButton?.isVisible = viewState.showClearButton omnibar.searchIcon?.isVisible = true diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index dd4c9501c791..077bd38972d9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -74,6 +74,9 @@ import com.duckduckgo.app.browser.commands.Command import com.duckduckgo.app.browser.commands.Command.* import com.duckduckgo.app.browser.commands.NavigationCommand import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames +import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_FEATURE_NAME +import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_PAGE_FEATURE_NAME +import com.duckduckgo.app.browser.duckplayer.DuckPlayerJSHelper import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.favicon.FaviconSource.ImageFavicon import com.duckduckgo.app.browser.favicon.FaviconSource.UrlFavicon @@ -171,6 +174,8 @@ import com.duckduckgo.downloads.api.DownloadCommand import com.duckduckgo.downloads.api.DownloadStateListener import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.js.messaging.api.JsCallbackData import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels @@ -272,6 +277,8 @@ class BrowserTabViewModel @Inject constructor( private val history: NavigationHistory, private val newTabPixels: Lazy, // Lazy to construct the instance and deps only when actually sending the pixel private val httpErrorPixels: Lazy, + private val duckPlayer: DuckPlayer, + private val duckPlayerJSHelper: DuckPlayerJSHelper, ) : WebViewClientListener, EditSavedSiteListener, DeleteBookmarkListener, @@ -506,6 +513,13 @@ class BrowserTabViewModel @Inject constructor( browserViewState.value = currentBrowserViewState().copy(privacyProtectionsPopupViewState = popupViewState) } .launchIn(viewModelScope) + + duckPlayer.observeUserPreferences() + .onEach { preferences -> + command.value = duckPlayerJSHelper.userPreferencesUpdated(preferences) + } + .flowOn(dispatchers.main()) + .launchIn(viewModelScope) } fun loadData( @@ -860,7 +874,6 @@ class BrowserTabViewModel @Inject constructor( clearPreviousUrl() } - Timber.w("SSLError: navigate to $urlToNavigate") site?.nextUrl = urlToNavigate command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) } @@ -1148,11 +1161,40 @@ class BrowserTabViewModel @Inject constructor( canGoForward = newWebNavigationState.canGoForward, ) - Timber.v("SSL Error: navigationStateChanged: $stateChange") when (stateChange) { - is WebNavigationStateChange.NewPage -> pageChanged(stateChange.url, stateChange.title) + is WebNavigationStateChange.NewPage -> { + val uri = stateChange.url.toUri() + viewModelScope.launch(dispatchers.io()) { + if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) { + duckPlayer.createDuckPlayerUriFromYoutubeNoCookie(uri)?.let { + withContext(dispatchers.main()) { + pageChanged(it, stateChange.title) + } + } + } else { + withContext(dispatchers.main()) { + pageChanged(stateChange.url, stateChange.title) + } + } + } + } is WebNavigationStateChange.PageCleared -> pageCleared() - is WebNavigationStateChange.UrlUpdated -> urlUpdated(stateChange.url) + is WebNavigationStateChange.UrlUpdated -> { + val uri = stateChange.url.toUri() + viewModelScope.launch(dispatchers.io()) { + if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) { + duckPlayer.createDuckPlayerUriFromYoutubeNoCookie(uri)?.let { + withContext(dispatchers.main()) { + urlUpdated(it) + } + } + } else { + withContext(dispatchers.main()) { + urlUpdated(stateChange.url) + } + } + } + } is WebNavigationStateChange.PageNavigationCleared -> disableUserNavigation() else -> {} } @@ -1223,6 +1265,7 @@ class BrowserTabViewModel @Inject constructor( isFireproofWebsite = isFireproofWebsite(), showDaxIcon = shouldShowDaxIcon(url, true), canPrintPage = domain != null, + showDuckPlayerIcon = shouldShowDuckPlayerIcon(url, true), ) if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { @@ -1297,6 +1340,14 @@ class BrowserTabViewModel @Inject constructor( return showPrivacyShield && duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url) } + private fun shouldShowDuckPlayerIcon( + currentUrl: String?, + showPrivacyShield: Boolean, + ): Boolean { + val url = currentUrl ?: return false + return showPrivacyShield && duckPlayer.isDuckPlayerUri(url) + } + private suspend fun updateLoadingStatePrivacy(domain: String) { val privacyProtectionDisabled = isPrivacyProtectionDisabled(domain) withContext(dispatchers.main()) { @@ -1334,20 +1385,20 @@ class BrowserTabViewModel @Inject constructor( } } - private suspend fun getBookmark(url: String): SavedSite.Bookmark? { + private suspend fun getBookmark(url: String): Bookmark? { return withContext(dispatchers.io()) { savedSitesRepository.getBookmark(url) } } - private suspend fun getBookmarkFolder(bookmark: SavedSite.Bookmark?): BookmarkFolder? { + private suspend fun getBookmarkFolder(bookmark: Bookmark?): BookmarkFolder? { if (bookmark == null) return null return withContext(dispatchers.io()) { savedSitesRepository.getFolder(bookmark.parentId) } } - private suspend fun getFavorite(url: String): SavedSite.Favorite? { + private suspend fun getFavorite(url: String): Favorite? { return withContext(dispatchers.io()) { savedSitesRepository.getFavorite(url) } @@ -1911,7 +1962,7 @@ class BrowserTabViewModel @Inject constructor( val autoCompleteSuggestionsEnabled = appSettingsPreferencesStore.autoCompleteSuggestionsEnabled val showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && hasQueryChanged && autoCompleteSuggestionsEnabled val showFavoritesAsSuggestions = if (!showAutoCompleteSuggestions) { - val urlFocused = hasFocus && query.isNotBlank() && !hasQueryChanged && UriString.isWebUrl(query) + val urlFocused = hasFocus && query.isNotBlank() && !hasQueryChanged && (UriString.isWebUrl(query) || duckPlayer.isDuckPlayerUri(query)) val emptyQueryBrowsing = query.isBlank() && currentBrowserViewState().browserShowing val favoritesAvailable = currentAutoCompleteViewState().favorites.isNotEmpty() hasFocus && (urlFocused || emptyQueryBrowsing) && favoritesAvailable @@ -1969,6 +2020,7 @@ class BrowserTabViewModel @Inject constructor( urlLoaded = url ?: "", ), showDaxIcon = shouldShowDaxIcon(url, showPrivacyShield), + showDuckPlayerIcon = shouldShowDuckPlayerIcon(url, showPrivacyShield), ) Timber.d("showPrivacyShield=$showPrivacyShield, showSearchIcon=$showSearchIcon, showClearButton=$showClearButton") @@ -2170,7 +2222,7 @@ class BrowserTabViewModel @Inject constructor( fun onEditSavedSiteRequested(savedSite: SavedSite) { viewModelScope.launch(dispatchers.io()) { val bookmarkFolder = - if (savedSite is SavedSite.Bookmark) { + if (savedSite is Bookmark) { getBookmarkFolder(savedSite) } else { null @@ -2403,7 +2455,13 @@ class BrowserTabViewModel @Inject constructor( fun onShareSelected() { url?.let { - command.value = ShareLink(removeAtbAndSourceParamsFromSearch(it), title.orEmpty()) + viewModelScope.launch(dispatchers.io()) { + transformUrlToShare(it).let { + withContext(dispatchers.main()) { + command.value = ShareLink(it, title.orEmpty()) + } + } + } } } @@ -2421,6 +2479,16 @@ class BrowserTabViewModel @Inject constructor( command.value = NavigationCommand.NavigateToHistory(stackIndex) } + private suspend fun transformUrlToShare(url: String): String { + return if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { + removeAtbAndSourceParamsFromSearch(url) + } else if (duckPlayer.isDuckPlayerUri(url)) { + transformDuckPlayerUrl(url) + } else { + url + } + } + private fun removeAtbAndSourceParamsFromSearch(url: String): String { if (!duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { return url @@ -2439,6 +2507,14 @@ class BrowserTabViewModel @Inject constructor( return builder.build().toString() } + private suspend fun transformDuckPlayerUrl(url: String): String { + return if (duckPlayer.isDuckPlayerUri(url)) { + duckPlayer.createYoutubeWatchUrlFromDuckPlayer(url.toUri()) ?: url + } else { + url + } + } + fun saveWebViewState( webView: WebView?, tabId: String, @@ -3091,11 +3167,12 @@ class BrowserTabViewModel @Inject constructor( ) } - fun processJsCallbackMessage( + suspend fun processJsCallbackMessage( featureName: String, method: String, id: String?, data: JSONObject?, + url: String?, ) { when (method) { "webShare" -> if (id != null && data != null) { @@ -3120,6 +3197,20 @@ class BrowserTabViewModel @Inject constructor( // NOOP } } + + when (featureName) { + DUCK_PLAYER_FEATURE_NAME, DUCK_PLAYER_PAGE_FEATURE_NAME -> { + withContext(dispatchers.io()) { + val response = duckPlayerJSHelper.processJsCallbackMessage(featureName, method, id, data, url) + withContext(dispatchers.main()) { + response?.let { + command.value = it + } + } + } + } + else -> {} + } } private fun webShare( diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 2fcf246be0dd..587ec3f3d211 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -68,6 +68,8 @@ import com.duckduckgo.common.utils.CurrentTimeProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.cookies.api.CookieManagerProvider +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.subscriptions.api.Subscriptions @@ -106,6 +108,7 @@ class BrowserWebViewClient @Inject constructor( private val navigationHistory: NavigationHistory, private val mediaPlayback: MediaPlayback, private val subscriptions: Subscriptions, + private val duckPlayer: DuckPlayer, ) : WebViewClient() { var webViewClientListener: WebViewClientListener? = null @@ -268,6 +271,13 @@ class BrowserWebViewClient @Inject constructor( } false } + is SpecialUrlDetector.UrlType.DuckScheme -> { + webViewClientListener?.let { listener -> + loadUrl(listener, webView, url.toString()) + true + } + false + } } } catch (e: Throwable) { appCoroutineScope.launch(dispatcherProvider.io()) { @@ -374,7 +384,19 @@ class BrowserWebViewClient @Inject constructor( pageLoadedHandler.onPageLoaded(it, navigationList.currentItem?.title, safeStart, currentTimeProvider.elapsedRealtime()) shouldSendPagePaintedPixel(webView = webView, url = it) appCoroutineScope.launch(dispatcherProvider.io()) { - navigationHistory.saveToHistory(url, navigationList.currentItem?.title) + if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(url)) { + duckPlayer.createDuckPlayerUriFromYoutubeNoCookie(url.toUri())?.let { + navigationHistory.saveToHistory( + it, + navigationList.currentItem?.title, + ) + } + } else { + if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isYoutubeWatchUrl(url.toUri())) { + duckPlayer.duckPlayerNavigatedToYoutube() + } + navigationHistory.saveToHistory(url, navigationList.currentItem?.title) + } } start = null } diff --git a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt index 2a5583425e7b..90f470be0920 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt @@ -23,13 +23,18 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.net.Uri +import androidx.core.net.toUri import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.privacy.config.api.AmpLinkType import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.privacy.config.api.TrackingParameters import com.duckduckgo.subscriptions.api.Subscriptions import java.net.URISyntaxException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import timber.log.Timber class SpecialUrlDetectorImpl( @@ -38,6 +43,8 @@ class SpecialUrlDetectorImpl( private val trackingParameters: TrackingParameters, private val subscriptions: Subscriptions, private val externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature, + private val duckPlayer: DuckPlayer, + private val scope: CoroutineScope, ) : SpecialUrlDetector { override fun determineType(initiatingUrl: String?, uri: Uri): UrlType { @@ -52,6 +59,7 @@ class SpecialUrlDetectorImpl( HTTP_SCHEME, HTTPS_SCHEME, DATA_SCHEME -> processUrl(initiatingUrl, uriString) JAVASCRIPT_SCHEME, ABOUT_SCHEME, FILE_SCHEME, SITE_SCHEME, BLOB_SCHEME -> UrlType.SearchQuery(uriString) FILETYPE_SCHEME, IN_TITLE_SCHEME, IN_URL_SCHEME -> UrlType.SearchQuery(uriString) + DUCK_SCHEME -> UrlType.DuckScheme(uriString) null -> { if (subscriptions.shouldLaunchPrivacyProForUrl("https://$uriString")) { UrlType.ShouldLaunchPrivacyProLink @@ -80,22 +88,30 @@ class SpecialUrlDetectorImpl( return UrlType.TrackingParameterLink(cleanedUrl = cleanedUrl) } - try { - val browsableIntent = Intent.parseUri(uriString, URI_ANDROID_APP_SCHEME).apply { - addCategory(Intent.CATEGORY_BROWSABLE) - } - val activities = queryActivities(browsableIntent) - val activity = getDefaultActivity(browsableIntent) ?: activities.firstOrNull() + val uri = uriString.toUri() + + val willNavigateToDuckPlayerDeferred = scope.async { duckPlayer.willNavigateToDuckPlayer(uri) } + + val willNavigateToDuckPlayer = runBlocking { willNavigateToDuckPlayerDeferred.await() } + + if (!willNavigateToDuckPlayer) { + try { + val browsableIntent = Intent.parseUri(uriString, URI_ANDROID_APP_SCHEME).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + } + val activities = queryActivities(browsableIntent) + val activity = getDefaultActivity(browsableIntent) ?: activities.firstOrNull() - val nonBrowserActivities = keepNonBrowserActivities(activities) - .filter { it.activityInfo.packageName == activity?.activityInfo?.packageName } + val nonBrowserActivities = keepNonBrowserActivities(activities) + .filter { it.activityInfo.packageName == activity?.activityInfo?.packageName } - nonBrowserActivities.singleOrNull()?.let { resolveInfo -> - val nonBrowserIntent = buildNonBrowserIntent(resolveInfo, uriString) - return UrlType.AppLink(appIntent = nonBrowserIntent, uriString = uriString) + nonBrowserActivities.singleOrNull()?.let { resolveInfo -> + val nonBrowserIntent = buildNonBrowserIntent(resolveInfo, uriString) + return UrlType.AppLink(appIntent = nonBrowserIntent, uriString = uriString) + } + } catch (e: URISyntaxException) { + Timber.w(e, "Failed to parse uri $uriString") } - } catch (e: URISyntaxException) { - Timber.w(e, "Failed to parse uri $uriString") } ampLinks.extractCanonicalFromAmpLink(uriString)?.let { ampLinkType -> @@ -204,6 +220,7 @@ class SpecialUrlDetectorImpl( private const val FILETYPE_SCHEME = "filetype" private const val IN_TITLE_SCHEME = "intitle" private const val IN_URL_SCHEME = "inurl" + private const val DUCK_SCHEME = "duck" const val SMS_MAX_LENGTH = 400 const val PHONE_MAX_LENGTH = 20 const val EMAIL_MAX_LENGTH = 1000 diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt index e7af2f3512b4..5ca57ebb0029 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt @@ -33,6 +33,7 @@ import com.duckduckgo.common.utils.AppUrl import com.duckduckgo.common.utils.DefaultDispatcherProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.isHttp +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.httpsupgrade.api.HttpsUpgrader import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.request.filterer.api.RequestFilterer @@ -69,6 +70,7 @@ class WebViewRequestInterceptor( private val adClickManager: AdClickManager, private val cloakedCnameDetector: CloakedCnameDetector, private val requestFilterer: RequestFilterer, + private val duckPlayer: DuckPlayer, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), ) : RequestInterceptor { @@ -92,7 +94,7 @@ class WebViewRequestInterceptor( documentUri: Uri?, webViewClientListener: WebViewClientListener?, ): WebResourceResponse? { - val url = request.url + val url: Uri? = request.url if (requestFilterer.shouldFilterOutRequest(request, documentUri.toString())) return WebResourceResponse(null, null, null) @@ -109,7 +111,7 @@ class WebViewRequestInterceptor( if (appUrlPixel(url)) return null if (shouldUpgrade(request)) { - val newUri = httpsUpgrader.upgrade(url) + val newUri = url?.let { httpsUpgrader.upgrade(url) } withContext(dispatchers.main()) { webView.loadUrl(newUri.toString(), getHeaders(request)) @@ -120,7 +122,11 @@ class WebViewRequestInterceptor( return WebResourceResponse(null, null, null) } - if (shouldAddGcpHeaders(request) && !requestWasInTheStack(url, webView)) { + if (url != null) { + duckPlayer.intercept(request, url, webView)?.let { return it } + } + + if (url != null && shouldAddGcpHeaders(request) && !requestWasInTheStack(url, webView)) { withContext(dispatchers.main()) { webViewClientListener?.redirectTriggeredByGpc() webView.loadUrl(url.toString(), getHeaders(request)) diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 7e4c351df519..599092851335 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -40,6 +40,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.js.messaging.api.JsCallbackData +import com.duckduckgo.js.messaging.api.SubscriptionEventData import com.duckduckgo.savedsites.api.models.SavedSite import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions @@ -219,7 +220,10 @@ sealed class Command { val url: String, ) : Command() + // TODO (cbarreiro) Rename to SendResponseToCSS data class SendResponseToJs(val data: JsCallbackData) : Command() + data class SendResponseToDuckPlayer(val data: JsCallbackData) : Command() + data class SendSubscriptions(val cssData: SubscriptionEventData, val duckPlayerData: SubscriptionEventData) : Command() data class WebShareRequest(val data: JsCallbackData) : Command() data class ScreenLock(val data: JsCallbackData) : Command() object ScreenUnlock : Command() @@ -233,4 +237,7 @@ sealed class Command { data class HideOnboardingDaxDialog(val onboardingCta: OnboardingDaxDialogCta) : Command() data class ShowRemoveSearchSuggestionDialog(val suggestion: AutoCompleteSuggestion) : Command() data object AutocompleteItemRemoved : Command() + object OpenDuckPlayerSettings : Command() + object OpenDuckPlayerOverlayInfo : Command() + object OpenDuckPlayerPageInfo : Command() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 4b67e980d882..274fc0c747cf 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -81,6 +81,7 @@ import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.impl.AndroidFileDownloader import com.duckduckgo.downloads.impl.DataUriDownloader import com.duckduckgo.downloads.impl.FileDownloadCallback +import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.experiments.api.VariantManager import com.duckduckgo.httpsupgrade.api.HttpsUpgrader import com.duckduckgo.privacy.config.api.AmpLinks @@ -185,7 +186,17 @@ class BrowserModule { trackingParameters: TrackingParameters, subscriptions: Subscriptions, externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature, - ): SpecialUrlDetector = SpecialUrlDetectorImpl(packageManager, ampLinks, trackingParameters, subscriptions, externalAppIntentFlagsFeature) + duckPlayer: DuckPlayer, + @AppCoroutineScope appCoroutineScope: CoroutineScope, + ): SpecialUrlDetector = SpecialUrlDetectorImpl( + packageManager, + ampLinks, + trackingParameters, + subscriptions, + externalAppIntentFlagsFeature, + duckPlayer, + appCoroutineScope, + ) @Provides fun webViewRequestInterceptor( @@ -198,6 +209,7 @@ class BrowserModule { adClickManager: AdClickManager, cloakedCnameDetector: CloakedCnameDetector, requestFilterer: RequestFilterer, + duckPlayer: DuckPlayer, ): RequestInterceptor = WebViewRequestInterceptor( resourceSurrogates, @@ -209,6 +221,7 @@ class BrowserModule { adClickManager, cloakedCnameDetector, requestFilterer, + duckPlayer, ) @Provides diff --git a/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt new file mode 100644 index 000000000000..0c2f9a11bf35 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.app.browser.duckplayer + +import androidx.core.net.toUri +import com.duckduckgo.app.browser.DuckDuckGoUrlDetector +import com.duckduckgo.app.browser.commands.Command +import com.duckduckgo.app.browser.commands.Command.OpenDuckPlayerOverlayInfo +import com.duckduckgo.app.browser.commands.Command.OpenDuckPlayerPageInfo +import com.duckduckgo.app.browser.commands.Command.OpenDuckPlayerSettings +import com.duckduckgo.app.browser.commands.Command.SendResponseToDuckPlayer +import com.duckduckgo.app.browser.commands.Command.SendResponseToJs +import com.duckduckgo.app.browser.commands.Command.SendSubscriptions +import com.duckduckgo.app.browser.commands.NavigationCommand.Navigate +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_ALWAYS_SERP +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE +import com.duckduckgo.app.pixels.AppPixelName.DUCK_PLAYER_SETTING_NEVER_SERP +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences +import com.duckduckgo.duckplayer.api.PrivatePlayerMode +import com.duckduckgo.js.messaging.api.JsCallbackData +import com.duckduckgo.js.messaging.api.SubscriptionEventData +import javax.inject.Inject +import org.json.JSONObject +import timber.log.Timber + +const val DUCK_PLAYER_PAGE_FEATURE_NAME = "duckPlayerPage" +const val DUCK_PLAYER_FEATURE_NAME = "duckPlayer" +private const val OVERLAY_INTERACTED = "overlayInteracted" +private const val PRIVATE_PLAYER_MODE = "privatePlayerMode" + +class DuckPlayerJSHelper @Inject constructor( + private val duckPlayer: DuckPlayer, + private val appBuildConfig: AppBuildConfig, + private val pixel: Pixel, + private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, +) { + private suspend fun getUserPreferences(featureName: String, method: String, id: String): JsCallbackData { + val userValues = duckPlayer.getUserPreferences() + + return JsCallbackData( + JSONObject( + """ + { + $OVERLAY_INTERACTED: ${userValues.overlayInteracted}, + $PRIVATE_PLAYER_MODE: { + "${userValues.privatePlayerMode.value}": {} + } + } + """, + ), + featureName, + method, + id, + ) + } + + fun userPreferencesUpdated(userPreferences: UserPreferences): SendSubscriptions { + return JSONObject( + """ + { + $OVERLAY_INTERACTED: ${userPreferences.overlayInteracted}, + $PRIVATE_PLAYER_MODE: { + "${userPreferences.privatePlayerMode.value}": {} + } + } + """, + ).let { json -> + SendSubscriptions( + cssData = SubscriptionEventData(DUCK_PLAYER_FEATURE_NAME, "onUserValuesChanged", json), + duckPlayerData = SubscriptionEventData(DUCK_PLAYER_PAGE_FEATURE_NAME, "onUserValuesChanged", json), + ) + } + } + + private suspend fun getInitialSetup(featureName: String, method: String, id: String): JsCallbackData { + val userValues = duckPlayer.getUserPreferences() + val privatePlayerMode = if (duckPlayer.getDuckPlayerState() == ENABLED) userValues.privatePlayerMode else PrivatePlayerMode.Disabled + + val jsonObject = JSONObject( + """ + { + "settings": { + "pip": { + "state": "disabled" + } + }, + "userValues": { + $OVERLAY_INTERACTED: ${userValues.overlayInteracted}, + $PRIVATE_PLAYER_MODE: { + "${privatePlayerMode.value}": {} + } + }, + ui: { + "allowFirstVideo": ${duckPlayer.shouldHideDuckPlayerOverlay()} + } + } + """, + ) + duckPlayer.duckPlayerOverlayHidden() + + when (featureName) { + DUCK_PLAYER_PAGE_FEATURE_NAME -> { + jsonObject.put("platform", JSONObject("""{ name: "android" }""")) + jsonObject.put("locale", java.util.Locale.getDefault().language) + jsonObject.put("env", if (appBuildConfig.isDebug) "development" else "production") + } + DUCK_PLAYER_FEATURE_NAME -> { + jsonObject.put("platform", JSONObject("""{ name: "android" }""")) + jsonObject.put("locale", java.util.Locale.getDefault().language) + } + } + + return JsCallbackData( + jsonObject, + featureName, + method, + id, + ) + } + + private suspend fun setUserPreferences(data: JSONObject) { + val overlayInteracted = data.getBoolean(OVERLAY_INTERACTED) + val privatePlayerModeObject = data.getJSONObject(PRIVATE_PLAYER_MODE) + duckPlayer.setUserPreferences(overlayInteracted, privatePlayerModeObject.keys().next()) + } + + private suspend fun sendDuckPlayerPixel(data: JSONObject) { + val pixelName = data.getString("pixelName") + val paramsMap = data.getJSONObject("params").keys().asSequence().associateWith { + data.getJSONObject("params").getString(it) + } + duckPlayer.sendDuckPlayerPixel(pixelName, paramsMap) + } + + suspend fun processJsCallbackMessage( + featureName: String, + method: String, + id: String?, + data: JSONObject?, + url: String?, + ): Command? { + when (method) { + "getUserValues" -> if (id != null) { + return SendResponseToJs(getUserPreferences(featureName, method, id)) + } + + "setUserValues" -> if (id != null && data != null) { + setUserPreferences(data) + return when (featureName) { + DUCK_PLAYER_FEATURE_NAME -> { + SendResponseToJs(getUserPreferences(featureName, method, id)).also { + if (data.getJSONObject(PRIVATE_PLAYER_MODE).keys().next() == "enabled") { + if (url != null && duckDuckGoUrlDetector.isDuckDuckGoUrl(url)) { + pixel.fire(DUCK_PLAYER_SETTING_ALWAYS_SERP) + } else { + pixel.fire(DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE) + } + } else if (data.getJSONObject(PRIVATE_PLAYER_MODE).keys().next() == "disabled") { + if (url != null && duckDuckGoUrlDetector.isDuckDuckGoUrl(url)) { + pixel.fire(DUCK_PLAYER_SETTING_NEVER_SERP) + } else { + pixel.fire(DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE) + } + } + } + } + DUCK_PLAYER_PAGE_FEATURE_NAME -> { + SendResponseToDuckPlayer(getUserPreferences(featureName, method, id)).also { + if (data.getJSONObject(PRIVATE_PLAYER_MODE).keys().next() == "enabled") { + pixel.fire(DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER) + } + } + } else -> { + null + } + } + } + + "sendDuckPlayerPixel" -> if (data != null) { + sendDuckPlayerPixel(data) + return null + } + "openDuckPlayer" -> { + return data?.getString("href")?.let { + Navigate(it.toUri().buildUpon().appendQueryParameter("origin", "overlay").build().toString(), mapOf()) + } + } + "initialSetup" -> { + return when (featureName) { + DUCK_PLAYER_FEATURE_NAME -> { + SendResponseToJs(getInitialSetup(featureName, method, id ?: "")) + } + DUCK_PLAYER_PAGE_FEATURE_NAME -> { + SendResponseToDuckPlayer(getInitialSetup(featureName, method, id ?: "")) + } + else -> { + null + } + } + } + "reportPageException", "reportInitException" -> { + Timber.tag(method).d(data.toString()) + } + "openSettings" -> { + return OpenDuckPlayerSettings + } + "openInfo" -> { + return when (featureName) { + DUCK_PLAYER_FEATURE_NAME -> OpenDuckPlayerOverlayInfo + DUCK_PLAYER_PAGE_FEATURE_NAME -> OpenDuckPlayerPageInfo + else -> null + } + } + else -> { + return null + } + } + return null + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/QueryUrlConverter.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/QueryUrlConverter.kt index 9ca17326b3da..20d67ab92ebe 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/QueryUrlConverter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/QueryUrlConverter.kt @@ -38,14 +38,14 @@ class QueryUrlConverter @Inject constructor(private val requestRewriter: Request ): String { val isUrl = when (queryOrigin) { is QueryOrigin.FromAutocomplete -> queryOrigin.isNav - is QueryOrigin.FromUser -> UriString.isWebUrl(searchQuery) + is QueryOrigin.FromUser -> UriString.isWebUrl(searchQuery) || UriString.isDuckUri(searchQuery) } if (isUrl == true) { return convertUri(searchQuery) } - if (URLUtil.isDataUrl(searchQuery)) { + if (URLUtil.isDataUrl(searchQuery) || URLUtil.isAssetUrl(searchQuery)) { return searchQuery } diff --git a/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt b/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt index 937a1f691bb4..66dd8df17782 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt @@ -58,6 +58,7 @@ data class BrowserViewState( val browserError: WebViewErrorResponse = WebViewErrorResponse.OMITTED, val sslError: SSLErrorType = SSLErrorType.NONE, val privacyProtectionsPopupViewState: PrivacyProtectionsPopupViewState = PrivacyProtectionsPopupViewState.Gone, + val showDuckPlayerIcon: Boolean = false, ) sealed class HighlightableButton { diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 3b0199f50f9c..a44aa5f1e79e 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.cta.ui import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread +import androidx.core.net.toUri import com.duckduckgo.app.browser.DuckDuckGoUrlDetector import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId @@ -30,13 +31,18 @@ import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.subscriptions.api.Subscriptions import dagger.SingleInstanceIn import javax.inject.Inject @@ -62,6 +68,7 @@ class CtaViewModel @Inject constructor( private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles, private val subscriptions: Subscriptions, + private val duckPlayer: DuckPlayer, ) { @ExperimentalCoroutinesApi @VisibleForTesting @@ -308,8 +315,23 @@ class CtaViewModel @Inject constructor( } } - private fun isSiteNotAllowedForOnboarding(url: String?): Boolean { - return url == null || subscriptions.isPrivacyProUrl(url) + private suspend fun isSiteNotAllowedForOnboarding(url: String?): Boolean { + val uri = url?.toUri() ?: return true + + if (subscriptions.isPrivacyProUrl(uri)) return true + + val isDuckPlayerUrl = + duckPlayer.getDuckPlayerState() == DuckPlayerState.ENABLED && + ( + (duckPlayer.getUserPreferences().privatePlayerMode == AlwaysAsk && duckPlayer.isYouTubeUrl(uri)) || + duckPlayer.isDuckPlayerUri(url) || duckPlayer.isSimulatedYoutubeNoCookie(url) + ) + + if (isDuckPlayerUrl) { + pixel.fire(pixel = ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE, type = UNIQUE) + } + + return isDuckPlayerUrl } private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index 119ef2a391d5..8bfa5c38847a 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -46,6 +46,7 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { ONBOARDING_DAX_ALL_CTA_HIDDEN("m_odc_h"), ONBOARDING_DAX_CTA_OK_BUTTON("m_odc_ok"), ONBOARDING_DAX_CTA_CANCEL_BUTTON("m_onboarding_dax_cta_cancel"), + ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE("m_onboarding_skip_major_network_unique"), BROWSER_MENU_ALLOWLIST_ADD("mb_wla"), BROWSER_MENU_ALLOWLIST_REMOVE("mb_wlr"), @@ -347,6 +348,12 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { TAB_MANAGER_LIST_VIEW_BUTTON_CLICKED("m_tab_manager_list_view_button_clicked"), TAB_MANAGER_VIEW_MODE_TOGGLED_DAILY("m_tab_manager_view_mode_toggled_daily"), + DUCK_PLAYER_SETTING_ALWAYS_OVERLAY_YOUTUBE("duck-player_setting_always_overlay_youtube"), + DUCK_PLAYER_SETTING_ALWAYS_SERP("duck-player_setting_always_overlay_serp"), + DUCK_PLAYER_SETTING_NEVER_SERP("duck-player_setting_never_overlay_serp"), + DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE("duck-player_setting_never_overlay_youtube"), + DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER("duck-player_setting_always_duck-player"), + ADD_BOOKMARK_CONFIRM_EDITED("m_add_bookmark_confirm_edit"), REFERRAL_INSTALL_UTM_CAMPAIGN("m_android_install"), diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index 907d4570239d..b8ae6427a0aa 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -64,6 +64,7 @@ import com.duckduckgo.macos.api.MacOsScreenWithEmptyParams import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerActivityWithEmptyParams import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.settings.api.DuckPlayerSettingsPlugin import com.duckduckgo.settings.api.ProSettingsPlugin import com.duckduckgo.sync.api.SyncActivityWithEmptyParams import com.duckduckgo.windows.api.ui.WindowsScreenWithEmptyParams @@ -101,6 +102,12 @@ class SettingsActivity : DuckDuckGoActivity() { _proSettingsPlugin.getPlugins() } + @Inject + lateinit var _duckPlayerSettingsPlugin: PluginPoint + private val duckPlayerSettingsPlugin by lazy { + _duckPlayerSettingsPlugin.getPlugins() + } + private val viewsPrivacy get() = binding.includeSettings.contentSettingsPrivacy @@ -169,6 +176,14 @@ class SettingsActivity : DuckDuckGoActivity() { viewsPro.addView(plugin.getView(this)) } } + + if (duckPlayerSettingsPlugin.isEmpty()) { + viewsSettings.settingsSectionDuckPlayer.gone() + } else { + duckPlayerSettingsPlugin.forEach { plugin -> + viewsSettings.settingsSectionDuckPlayer.addView(plugin.getView(this)) + } + } } private fun configureInternalFeatures() { @@ -200,6 +215,7 @@ class SettingsActivity : DuckDuckGoActivity() { updateSyncSetting(visible = it.showSyncSetting) updateAutoconsent(it.isAutoconsentEnabled) updatePrivacyPro(it.isPrivacyProEnabled) + updateDuckPlayer(it.isDuckPlayerEnabled) } }.launchIn(lifecycleScope) @@ -218,6 +234,14 @@ class SettingsActivity : DuckDuckGoActivity() { } } + private fun updateDuckPlayer(isDuckPlayerEnabled: Boolean) { + if (isDuckPlayerEnabled) { + viewsSettings.settingsSectionDuckPlayer.show() + } else { + viewsSettings.settingsSectionDuckPlayer.gone() + } + } + private fun updateAutofill(autofillEnabled: Boolean) = with(viewsSettings.autofillLoginsSetting) { visibility = if (autofillEnabled) { View.VISIBLE diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt index 59823b92dfee..22abfba72720 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt @@ -32,6 +32,9 @@ import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED import com.duckduckgo.mobile.android.app.tracking.AppTrackingProtection import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.sync.api.DeviceSyncState @@ -58,6 +61,7 @@ class SettingsViewModel @Inject constructor( private val dispatcherProvider: DispatcherProvider, private val autoconsent: Autoconsent, private val subscriptions: Subscriptions, + private val duckPlayer: DuckPlayer, ) : ViewModel(), DefaultLifecycleObserver { data class ViewState( @@ -70,6 +74,7 @@ class SettingsViewModel @Inject constructor( val showSyncSetting: Boolean = false, val isAutoconsentEnabled: Boolean = false, val isPrivacyProEnabled: Boolean = false, + val isDuckPlayerEnabled: Boolean = false, ) sealed class Command { @@ -130,6 +135,7 @@ class SettingsViewModel @Inject constructor( showSyncSetting = deviceSyncState.isFeatureEnabled(), isAutoconsentEnabled = autoconsent.isSettingEnabled(), isPrivacyProEnabled = subscriptions.isEligible(), + isDuckPlayerEnabled = duckPlayer.getDuckPlayerState().let { it == ENABLED || it == DISABLED_WIH_HELP_LINK }, ), ) } diff --git a/app/src/main/res/drawable/ic_duckplayer.xml b/app/src/main/res/drawable/ic_duckplayer.xml new file mode 100644 index 000000000000..5a7c406a36d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_duckplayer.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/content_settings_settings.xml b/app/src/main/res/layout/content_settings_settings.xml index b0d6ef4333dd..6a2745a92d24 100644 --- a/app/src/main/res/layout/content_settings_settings.xml +++ b/app/src/main/res/layout/content_settings_settings.xml @@ -79,6 +79,13 @@ android:layout_height="wrap_content" app:primaryText="@string/settingsAccessibility" /> + + + + + = emptyList() - private val handlers: List = listOf(ContentScopeHandler(), breakageHandler) + private val handlers: List = listOf(ContentScopeHandler(), breakageHandler, DuckPlayerHandler()) @JavascriptInterface override fun process(message: String, secret: String) { @@ -119,4 +119,25 @@ class ContentScopeScriptsJsMessaging @Inject constructor( override val featureName: String = "webCompat" override val methods: List = listOf("webShare", "permissionsQuery", "screenLock", "screenUnlock") } + + inner class DuckPlayerHandler : JsMessageHandler { + override fun process(jsMessage: JsMessage, secret: String, jsMessageCallback: JsMessageCallback?) { + // TODO (cbarreiro): Add again when https://app.asana.com/0/0/1207602010403610/f is fixed + // if (jsMessage.id == null) return + jsMessageCallback?.process(featureName, jsMessage.method, jsMessage.id ?: "", jsMessage.params) + } + + override val allowedDomains: List = emptyList() + override val featureName: String = "duckPlayer" + override val methods: List = listOf( + "getUserValues", + "sendDuckPlayerPixel", + "setUserValues", + "openDuckPlayer", + "openInfo", + "initialSetup", + "reportPageException", + "reportInitException", + ) + } } diff --git a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/RealContentScopeScriptsTest.kt b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/RealContentScopeScriptsTest.kt index 452df739ecad..770248ec05fe 100644 --- a/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/RealContentScopeScriptsTest.kt +++ b/content-scope-scripts/content-scope-scripts-impl/src/test/java/com/duckduckgo/contentscopescripts/impl/RealContentScopeScriptsTest.kt @@ -96,7 +96,8 @@ class RealContentScopeScriptsTest { "\"unprotectedTemporary\":\\[" + "\\{\"domain\":\"example\\.com\",\"reason\":\"reason\"\\}," + "\\{\"domain\":\"foo\\.com\",\"reason\":\"reason2\"\\}\\]\\}, \\[\"foo\\.com\"\\], " + - "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"sessionKey\":\"5678\",\"desktopModeEnabled\":false," + + "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"locale\":\"en\"," + + "\"sessionKey\":\"5678\",\"desktopModeEnabled\":false," + "\"messageSecret\":\"([\\da-f]{32})\"," + "\"messageCallback\":\"([\\da-f]{32})\"," + "\"javascriptInterface\":\"([\\da-f]{32})\"\\}\\)$", @@ -122,7 +123,8 @@ class RealContentScopeScriptsTest { "\"unprotectedTemporary\":\\[" + "\\{\"domain\":\"example\\.com\",\"reason\":\"reason\"\\}," + "\\{\"domain\":\"foo\\.com\",\"reason\":\"reason2\"\\}\\]\\}, \\[\"example\\.com\"\\], " + - "\\{\"globalPrivacyControlValue\":false,\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"sessionKey\":\"5678\"," + + "\\{\"globalPrivacyControlValue\":false,\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\}," + + "\"locale\":\"en\",\"sessionKey\":\"5678\"," + "\"desktopModeEnabled\":false,\"messageSecret\":\"([\\da-f]{32})\"," + "\"messageCallback\":\"([\\da-f]{32})\"," + "\"javascriptInterface\":\"([\\da-f]{32})\"\\}\\)$", @@ -147,7 +149,8 @@ class RealContentScopeScriptsTest { "\"unprotectedTemporary\":\\[" + "\\{\"domain\":\"example\\.com\",\"reason\":\"reason\"\\}," + "\\{\"domain\":\"foo\\.com\",\"reason\":\"reason2\"\\}\\]\\}, \\[\"example\\.com\"\\], " + - "\\{\"globalPrivacyControlValue\":true,\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"sessionKey\":\"5678\"," + + "\\{\"globalPrivacyControlValue\":true,\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"locale\":\"en\"," + + "\"sessionKey\":\"5678\"," + "\"desktopModeEnabled\":false,\"messageSecret\":\"([\\da-f]{32})\"," + "\"messageCallback\":\"([\\da-f]{32})\"," + "\"javascriptInterface\":\"([\\da-f]{32})\"\\}\\)$", @@ -173,7 +176,8 @@ class RealContentScopeScriptsTest { "\"config2\":\\{\"state\":\"disabled\"\\}\\}," + "\"unprotectedTemporary\":\\[" + "\\{\"domain\":\"example\\.com\",\"reason\":\"reason\"\\}\\]\\}, \\[\"example\\.com\"\\], " + - "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"sessionKey\":\"5678\",\"desktopModeEnabled\":false," + + "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"locale\":\"en\",\"sessionKey\":\"5678\"," + + "\"desktopModeEnabled\":false," + "\"messageSecret\":\"([\\da-f]{32})\"," + "\"messageCallback\":\"([\\da-f]{32})\"," + "\"javascriptInterface\":\"([\\da-f]{32})\"\\}\\)$", @@ -199,7 +203,8 @@ class RealContentScopeScriptsTest { "\"unprotectedTemporary\":\\[" + "\\{\"domain\":\"example\\.com\",\"reason\":\"reason\"\\}," + "\\{\"domain\":\"foo\\.com\",\"reason\":\"reason2\"\\}\\]\\}, \\[\"example\\.com\"\\], " + - "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"sessionKey\":\"5678\",\"desktopModeEnabled\":true," + + "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"locale\":\"en\"," + + "\"sessionKey\":\"5678\",\"desktopModeEnabled\":true," + "\"messageSecret\":\"([\\da-f]{32})\"," + "\"messageCallback\":\"([\\da-f]{32})\"," + "\"javascriptInterface\":\"([\\da-f]{32})\"\\}\\)$", @@ -279,7 +284,8 @@ class RealContentScopeScriptsTest { "\"unprotectedTemporary\":\\[" + "\\{\"domain\":\"example\\.com\",\"reason\":\"reason\"\\}," + "\\{\"domain\":\"foo\\.com\",\"reason\":\"reason2\"\\}\\]\\}, \\[\"example\\.com\"\\], " + - "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"sessionKey\":\"5678\",\"desktopModeEnabled\":false," + + "\\{\"versionNumber\":1234,\"platform\":\\{\"name\":\"android\"\\},\"locale\":\"en\"," + + "\"sessionKey\":\"5678\",\"desktopModeEnabled\":false," + "\"messageSecret\":\"([\\da-f]{32})\"," + "\"messageCallback\":\"([\\da-f]{32})\"," + "\"javascriptInterface\":\"([\\da-f]{32})\"\\}\\)$", diff --git a/duckplayer/duckplayer-api/.gitignore b/duckplayer/duckplayer-api/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/duckplayer/duckplayer-api/build.gradle b/duckplayer/duckplayer-api/build.gradle new file mode 100644 index 000000000000..9dea13c49d65 --- /dev/null +++ b/duckplayer/duckplayer-api/build.gradle @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * 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. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +apply from: "$rootProject.projectDir/gradle/android-library.gradle" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + implementation Kotlin.stdlib.jdk7 + implementation AndroidX.core.ktx + implementation KotlinX.coroutines.core + implementation AndroidX.fragment.ktx + coreLibraryDesugaring Android.tools.desugarJdkLibs + implementation project(path: ':common-utils') + implementation project(':navigation-api') +} + + +android { + namespace "com.duckduckgo.duckplayer.api" + testOptions { + unitTests { + includeAndroidResources = true + } + } + compileOptions { + coreLibraryDesugaringEnabled = true + } +} diff --git a/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt new file mode 100644 index 000000000000..e0341f35986a --- /dev/null +++ b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayer.kt @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.api + +import android.content.res.Configuration +import android.net.Uri +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import androidx.fragment.app.FragmentManager +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import kotlinx.coroutines.flow.Flow + +/** + * DuckPlayer interface provides a set of methods for interacting with the DuckPlayer. + */ +interface DuckPlayer { + + /** + * Retrieves the current state of the DuckPlayer. + * + * This method is used to check the current state of the DuckPlayer. The state can be one of the following: + * - ENABLED: The DuckPlayer is enabled and can be used. + * - DISABLED: The DuckPlayer is disabled and cannot be used. + * - DISABLED_WIH_HELP_LINK: The DuckPlayer is disabled and cannot be used, but a help link is provided for troubleshooting. + * + * @return The current state of the DuckPlayer as a DuckPlayerState enum. + */ + suspend fun getDuckPlayerState(): DuckPlayerState + + /** + * Sends a pixel with the given name and data. + * + * @param pixelName The name of the pixel. + * @param pixelData The data associated with the pixel. + */ + suspend fun sendDuckPlayerPixel(pixelName: String, pixelData: Map) + + /** + * Retrieves the user preferences. + * + * @return The user values. + */ + suspend fun getUserPreferences(): UserPreferences + + /** + * Checks if the DuckPlayer overlay should be hidden after navigating back from Duck Player + * + * @return True if the overlay should be hidden, false otherwise. + */ + fun shouldHideDuckPlayerOverlay(): Boolean + + /** + * Notifies the DuckPlayer that the overlay was hidden after navigating back from Duck Player + */ + fun duckPlayerOverlayHidden() + + /** + * Notifies the DuckPlayer that the user navigated to YouTube successfully, so subsequent requests would redirect to Duck Player + */ + fun duckPlayerNavigatedToYoutube() + + /** + * Retrieves a flow of user preferences. + * + * @return The flow user preferences. + */ + fun observeUserPreferences(): Flow + + /** + * Sets the user preferences. + * + * @param overlayInteracted A boolean indicating whether the overlay was interacted with. + * @param privatePlayerMode The mode of the private player. + */ + suspend fun setUserPreferences(overlayInteracted: Boolean, privatePlayerMode: String) + + /** + * Creates a DuckPlayer URI from a YouTube no-cookie URI. + * + * @param uri The YouTube no-cookie URI. + * @return The DuckPlayer URI. + */ + suspend fun createDuckPlayerUriFromYoutubeNoCookie(uri: Uri): String? + + /** + * Checks if a string is a DuckPlayer URI. + * + * @param uri The string to check. + * @return True if the string is a DuckPlayer URI, false otherwise. + */ + fun isDuckPlayerUri(uri: String): Boolean + + /** + * Creates a YouTube URI from a DuckPlayer URI. + * @param uri The DuckPlayer URI. + * @return The YouTube URI. + */ + suspend fun createYoutubeWatchUrlFromDuckPlayer(uri: Uri): String? + + /** + * Checks if a URI is a simulated YouTube no-cookie URI. + * + * @param uri The URI to check. + * @return True if the URI is a YouTube no-cookie URI, false otherwise. + */ + suspend fun isSimulatedYoutubeNoCookie(uri: Uri): Boolean + + /** + * Checks if a URI is a YouTube watch URL. + * + * @param uri The URI to check. + * @return True if the URI is a YouTube no-cookie URI, false otherwise. + */ + suspend fun isYoutubeWatchUrl(uri: Uri): Boolean + + /** + * Checks if a URI is a YouTube URL. + * + * @param uri The URI to check. + * @return True if the URI is a YouTube no-cookie URI, false otherwise. + */ + fun isYouTubeUrl(uri: Uri): Boolean + + /** + * Checks if a string is a YouTube no-cookie URI. + * + * @param uri The string to check. + * @return True if the string is a YouTube no-cookie URI, false otherwise. + */ + suspend fun isSimulatedYoutubeNoCookie(uri: String): Boolean + + /** + * Notify Duck Player of a resource request and allow Duck Player to return the data. + * + * If the return value is null, it means Duck Player won't add any response data. + * Otherwise, the return response and data will be used. + */ + suspend fun intercept( + request: WebResourceRequest, + url: Uri, + webView: WebView, + ): WebResourceResponse? + + /** + * Shows the Duck Player Prime modal. + * + * @param configuration The configuration of the device. + * @param fragmentManager The fragment manager. + */ + fun showDuckPlayerPrimeModal(configuration: Configuration, fragmentManager: FragmentManager, fromDuckPlayerPage: Boolean) + + /** + * Checks whether a URL will trigger Duck Player loading based on URL and user settings + * + * @param destinationUrl The destination URL. + * @return True if the URL should launch Duck Player, false otherwise. + */ + suspend fun willNavigateToDuckPlayer(destinationUrl: Uri): Boolean + + /** + * Data class representing user preferences for Duck Player. + * + * @property overlayInteracted A boolean indicating whether the overlay was interacted with. + * @property privatePlayerMode The mode of the private player. [Enabled], [AlwaysAsk], or [Disabled] + */ + data class UserPreferences( + val overlayInteracted: Boolean, + val privatePlayerMode: PrivatePlayerMode, + ) + + enum class DuckPlayerState { + ENABLED, + DISABLED, + DISABLED_WIH_HELP_LINK, + } +} diff --git a/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerFeatureName.kt b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerFeatureName.kt new file mode 100644 index 000000000000..732cd7625f69 --- /dev/null +++ b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerFeatureName.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.api + +/** List of [DuckPlayerFeatureName] that belong to the DuckPlayer feature */ +enum class DuckPlayerFeatureName(val value: String) { + DuckPlayer("duckPlayer"), +} diff --git a/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerSettingsScreens.kt b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerSettingsScreens.kt new file mode 100644 index 000000000000..886fcdb528c4 --- /dev/null +++ b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/DuckPlayerSettingsScreens.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.api + +import com.duckduckgo.navigation.api.GlobalActivityStarter + +/** + * Use this model to launch the Duck Player Settings screen + */ +object DuckPlayerSettingsNoParams : GlobalActivityStarter.ActivityParams diff --git a/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/PrivatePlayerMode.kt b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/PrivatePlayerMode.kt new file mode 100644 index 000000000000..9ec15c22f7ad --- /dev/null +++ b/duckplayer/duckplayer-api/src/main/java/com/duckduckgo/duckplayer/api/PrivatePlayerMode.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.api + +sealed class PrivatePlayerMode { + abstract val value: String + + data object Enabled : PrivatePlayerMode() { + override val value: String = "enabled" + } + + data object AlwaysAsk : PrivatePlayerMode() { + override val value: String = "alwaysAsk" + } + + data object Disabled : PrivatePlayerMode() { + override val value: String = "disabled" + } +} diff --git a/duckplayer/duckplayer-impl/build.gradle b/duckplayer/duckplayer-impl/build.gradle new file mode 100644 index 000000000000..58806aa61260 --- /dev/null +++ b/duckplayer/duckplayer-impl/build.gradle @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * 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. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'com.squareup.anvil' + id 'com.google.devtools.ksp' version "$ksp_version" +} + +apply from: "$rootProject.projectDir/gradle/android-library.gradle" + +dependencies { + implementation project(":duckplayer-api") + implementation project(':feature-toggles-api') + implementation project(':common-utils') + implementation project(':browser-api') + + anvil project(path: ':anvil-compiler') + implementation project(path: ':anvil-annotations') + implementation project(path: ':di') + implementation project(':common-ui') + implementation project(':content-scope-scripts-api') + implementation project(':app-build-config-api') + implementation project(':js-messaging-api') + implementation project(':navigation-api') + implementation project(':remote-messaging-api') + implementation project(':settings-api') + implementation project(':statistics-api') + api AndroidX.dataStore.preferences + + + ksp AndroidX.room.compiler + + implementation AndroidX.appCompat + implementation KotlinX.coroutines.android + implementation KotlinX.coroutines.core + implementation AndroidX.constraintLayout + implementation AndroidX.core.ktx + implementation AndroidX.lifecycle.runtime.ktx + implementation AndroidX.lifecycle.viewModelKtx + implementation Google.android.material + implementation Google.dagger + implementation JakeWharton.timber + + implementation "com.airbnb.android:lottie:_" + implementation "com.squareup.logcat:logcat:_" + + testImplementation Testing.junit4 + testImplementation "org.mockito.kotlin:mockito-kotlin:_" + testImplementation project(path: ':common-test') + testImplementation "androidx.test.ext:junit-ktx:_" + testImplementation CashApp.turbine + testImplementation Testing.robolectric + testImplementation(KotlinX.coroutines.test) { + // https://github.com/Kotlin/kotlinx.coroutines/issues/2023 + // conflicts with mockito due to direct inclusion of byte buddy + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } + + coreLibraryDesugaring Android.tools.desugarJdkLibs +} + +android { + namespace "com.duckduckgo.duckplayer.impl" + anvil { + generateDaggerFactories = true // default is false + } + testOptions { + unitTests { + includeAndroidResources = true + } + } + compileOptions { + coreLibraryDesugaringEnabled = true + } +} + diff --git a/duckplayer/duckplayer-impl/src/main/AndroidManifest.xml b/duckplayer/duckplayer-impl/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..819a5674cbe8 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerContentScopeConfigPlugin.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerContentScopeConfigPlugin.kt new file mode 100644 index 000000000000..e19ce5fca4d2 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerContentScopeConfigPlugin.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.appbuildconfig.api.isInternalBuild +import com.duckduckgo.contentscopescripts.api.ContentScopeConfigPlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED +import com.duckduckgo.duckplayer.api.DuckPlayerFeatureName +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import kotlinx.coroutines.runBlocking +import org.json.JSONObject + +@ContributesMultibinding(AppScope::class) +class DuckPlayerContentScopeConfigPlugin @Inject constructor( + private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, + private val appBuildConfig: AppBuildConfig, + private val duckPlayer: DuckPlayer, +) : ContentScopeConfigPlugin { + + override fun config(): String { + val featureName = DuckPlayerFeatureName.DuckPlayer.value + + val config = duckPlayerFeatureRepository.getDuckPlayerRemoteConfigJson().let { jsonString -> + if (appBuildConfig.isInternalBuild() && runBlocking { duckPlayer.getDuckPlayerState() == ENABLED }) { + runCatching { + JSONObject(jsonString).takeIf { it.getString("state") == "internal" }?.apply { + put("state", "enabled") + }?.toString() ?: jsonString + }.getOrDefault(jsonString) + } else { + jsonString + } + } + + return "\"$featureName\":$config" + } + + override fun preferences(): String? { + return null + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStore.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStore.kt new file mode 100644 index 000000000000..14227f3c917a --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStore.kt @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2022 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.stringSetPreferencesKey +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.DUCK_PLAYER_DISABLED_HELP_PAGE +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.DUCK_PLAYER_RC +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.DUCK_PLAYER_USER_ONBOARDED +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.DUCK_PLAYER_YOUTUBE_PATH +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.DUCK_PLAYER_YOUTUBE_REFERRER_HEADERS +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.OVERLAY_INTERACTED +import com.duckduckgo.duckplayer.impl.SharedPreferencesDuckPlayerDataStore.Keys.PRIVATE_PLAYER_MODE +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +interface DuckPlayerDataStore { + suspend fun getDuckPlayerRemoteConfigJson(): String + + suspend fun setDuckPlayerRemoteConfigJson(value: String) + + suspend fun getOverlayInteracted(): Boolean + + fun observeOverlayInteracted(): Flow + + suspend fun setOverlayInteracted(value: Boolean) + + suspend fun getPrivatePlayerMode(): String + + fun observePrivatePlayerMode(): Flow + + suspend fun setPrivatePlayerMode(value: String) + + suspend fun getDuckPlayerDisabledHelpPageLink(): String? + + suspend fun storeDuckPlayerDisabledHelpPageLink(duckPlayerDisabledHelpPageLink: String?) + + suspend fun getYouTubeWatchPath(): String + + suspend fun storeYouTubeWatchPath(youtubePath: String) + + suspend fun getYoutubeEmbedUrl(): String + + suspend fun storeYoutubeEmbedUrl(embedUrl: String) + + suspend fun getYouTubeUrl(): String + + suspend fun storeYouTubeUrl(youtubeUrl: String) + + suspend fun getYouTubeVideoIDQueryParam(): String + + suspend fun storeYouTubeVideoIDQueryParam(youtubeVideoIDQueryParams: String) + + suspend fun getYouTubeReferrerQueryParams(): List + + suspend fun storeYouTubeReferrerQueryParams(youtubeReferrerQueryParams: List) + + suspend fun getYouTubeReferrerHeaders(): List + + suspend fun storeYouTubeReferrerHeaders(youtubeReferrerHeaders: List) + + suspend fun setUserOnboarded() + + suspend fun getUserOnboarded(): Boolean +} + +@ContributesBinding(AppScope::class) +class SharedPreferencesDuckPlayerDataStore @Inject constructor( + @DuckPlayer private val store: DataStore, +) : DuckPlayerDataStore { + + private object Keys { + val OVERLAY_INTERACTED = booleanPreferencesKey(name = "OVERLAY_INTERACTED") + val DUCK_PLAYER_RC = stringPreferencesKey(name = "DUCK_PLAYER_RC") + val PRIVATE_PLAYER_MODE = stringPreferencesKey(name = "PRIVATE_PLAYER_MODE") + val DUCK_PLAYER_DISABLED_HELP_PAGE = stringPreferencesKey(name = "DUCK_PLAYER_DISABLED_HELP_PAGE") + val DUCK_PLAYER_YOUTUBE_PATH = stringPreferencesKey(name = "DUCK_PLAYER_YOUTUBE_PATH") + val DUCK_PLAYER_YOUTUBE_REFERRER_HEADERS = stringSetPreferencesKey(name = "DUCK_PLAYER_YOUTUBE_REFERRER_HEADERS") + val DUCK_PLAYER_YOUTUBE_REFERRER_QUERY_PARAMS = stringSetPreferencesKey(name = "DUCK_PLAYER_YOUTUBE_REFERRER_QUERY_PARAMS") + val DUCK_PLAYER_YOUTUBE_URL = stringPreferencesKey(name = "DUCK_PLAYER_YOUTUBE_URL") + val DUCK_PLAYER_YOUTUBE_VIDEO_ID_QUERY_PARAMS = stringPreferencesKey(name = "DUCK_PLAYER_YOUTUBE_VIDEO_ID_QUERY_PARAMS") + val DUCK_PLAYER_YOUTUBE_EMBED_URL = stringPreferencesKey(name = "DUCK_PLAYER_YOUTUBE_EMBED_URL") + val DUCK_PLAYER_USER_ONBOARDED = booleanPreferencesKey(name = "DUCK_PLAYER_USER_ONBOARDED") + } + + private val overlayInteracted: Flow + get() = store.data + .map { prefs -> + prefs[OVERLAY_INTERACTED] ?: false + } + .distinctUntilChanged() + + private val duckPlayerRC: Flow + get() = store.data + .map { prefs -> + prefs[DUCK_PLAYER_RC] ?: "" + } + .distinctUntilChanged() + + private val privatePlayerMode: Flow + get() = store.data + .map { prefs -> + prefs[PRIVATE_PLAYER_MODE] ?: "ALWAYS_ASK" + } + .distinctUntilChanged() + + private val duckPlayerDisabledHelpPageLink: Flow + get() = store.data + .map { prefs -> + prefs[DUCK_PLAYER_DISABLED_HELP_PAGE] ?: "" + } + .distinctUntilChanged() + + private val youtubePath: Flow + get() = store.data + .map { prefs -> + prefs[DUCK_PLAYER_YOUTUBE_PATH] ?: "" + } + .distinctUntilChanged() + + private val youtubeReferrerHeaders: Flow> + get() = store.data + .map { prefs -> + prefs[DUCK_PLAYER_YOUTUBE_REFERRER_HEADERS]?.toList() ?: listOf() + } + .distinctUntilChanged() + + private val youtubeReferrerQueryParams: Flow> + get() = store.data + .map { prefs -> + prefs[Keys.DUCK_PLAYER_YOUTUBE_REFERRER_QUERY_PARAMS]?.toList() ?: listOf() + } + .distinctUntilChanged() + + private val youtubeUrl: Flow + get() = store.data + .map { prefs -> + prefs[Keys.DUCK_PLAYER_YOUTUBE_URL] ?: "" + } + .distinctUntilChanged() + + private val youtubeVideoIDQueryParams: Flow + get() = store.data + .map { prefs -> + prefs[Keys.DUCK_PLAYER_YOUTUBE_VIDEO_ID_QUERY_PARAMS] ?: "" + } + .distinctUntilChanged() + + private val youtubeEmbedUrl: Flow + get() = store.data + .map { prefs -> + prefs[Keys.DUCK_PLAYER_YOUTUBE_EMBED_URL] ?: "" + } + .distinctUntilChanged() + + private val duckPlayerUserOnboarded: Flow + get() = store.data + .map { prefs -> + prefs[Keys.DUCK_PLAYER_USER_ONBOARDED] ?: false + } + .distinctUntilChanged() + + override suspend fun getDuckPlayerRemoteConfigJson(): String { + return duckPlayerRC.first() + } + + override suspend fun setDuckPlayerRemoteConfigJson(value: String) { + store.edit { prefs -> prefs[DUCK_PLAYER_RC] = value } + } + + override suspend fun getOverlayInteracted(): Boolean { + return overlayInteracted.first() + } + + override fun observeOverlayInteracted(): Flow { + return overlayInteracted + } + + override suspend fun setOverlayInteracted(value: Boolean) { + store.edit { prefs -> prefs[OVERLAY_INTERACTED] = value } + } + + override suspend fun getPrivatePlayerMode(): String { + return privatePlayerMode.first() + } + + override fun observePrivatePlayerMode(): Flow { + return privatePlayerMode + } + + override suspend fun setPrivatePlayerMode(value: String) { + store.edit { prefs -> prefs[PRIVATE_PLAYER_MODE] = value } + } + + override suspend fun storeDuckPlayerDisabledHelpPageLink(duckPlayerDisabledHelpPageLink: String?) { + store.edit { prefs -> prefs[DUCK_PLAYER_DISABLED_HELP_PAGE] = duckPlayerDisabledHelpPageLink ?: "" } + } + + override suspend fun getDuckPlayerDisabledHelpPageLink(): String? { + return duckPlayerDisabledHelpPageLink.first().let { it.ifBlank { null } } + } + + override suspend fun storeYouTubeWatchPath(youtubePath: String) { + store.edit { prefs -> prefs[DUCK_PLAYER_YOUTUBE_PATH] = youtubePath } + } + + override suspend fun storeYouTubeReferrerHeaders(youtubeReferrerHeaders: List) { + store.edit { prefs -> prefs[DUCK_PLAYER_YOUTUBE_REFERRER_HEADERS] = youtubeReferrerHeaders.toSet() } + } + + override suspend fun getYouTubeReferrerHeaders(): List { + return youtubeReferrerHeaders.first() + } + + override suspend fun storeYouTubeReferrerQueryParams(youtubeReferrerQueryParams: List) { + store.edit { prefs -> prefs[Keys.DUCK_PLAYER_YOUTUBE_REFERRER_QUERY_PARAMS] = youtubeReferrerQueryParams.toSet() } + } + + override suspend fun getYouTubeReferrerQueryParams(): List { + return youtubeReferrerQueryParams.first() + } + + override suspend fun storeYouTubeUrl(youtubeUrl: String) { + store.edit { prefs -> prefs[Keys.DUCK_PLAYER_YOUTUBE_URL] = youtubeUrl } + } + + override suspend fun getYouTubeUrl(): String { + return youtubeUrl.first() + } + + override suspend fun storeYouTubeVideoIDQueryParam(youtubeVideoIDQueryParams: String) { + store.edit { prefs -> prefs[Keys.DUCK_PLAYER_YOUTUBE_VIDEO_ID_QUERY_PARAMS] = youtubeVideoIDQueryParams } + } + + override suspend fun getYouTubeVideoIDQueryParam(): String { + return youtubeVideoIDQueryParams.first() + } + + override suspend fun storeYoutubeEmbedUrl(embedUrl: String) { + store.edit { prefs -> prefs[Keys.DUCK_PLAYER_YOUTUBE_EMBED_URL] = embedUrl } + } + + override suspend fun getYoutubeEmbedUrl(): String { + return youtubeEmbedUrl.first() + } + + override suspend fun getYouTubeWatchPath(): String { + return youtubePath.first() + } + + override suspend fun setUserOnboarded() { + store.edit { prefs -> prefs[DUCK_PLAYER_USER_ONBOARDED] = true } + } + + override suspend fun getUserOnboarded(): Boolean { + return duckPlayerUserOnboarded.first() + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStoreModule.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStoreModule.kt new file mode 100644 index 000000000000..9fc0ef68bb11 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerDataStoreModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import javax.inject.Qualifier + +@ContributesTo(AppScope::class) +@Module +object DuckPlayerDataStoreModule { + + private val Context.duckPlayerDataStore: DataStore by preferencesDataStore( + name = "duck_player", + ) + + @Provides + @DuckPlayer + fun provideDuckPlayerDataStore(context: Context): DataStore = context.duckPlayerDataStore +} + +@Qualifier +internal annotation class DuckPlayer diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerEnabledRMFMatchingAttribute.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerEnabledRMFMatchingAttribute.kt new file mode 100644 index 000000000000..06ad6f88f37b --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerEnabledRMFMatchingAttribute.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin +import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute +import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper +import com.duckduckgo.remote.messaging.api.MatchingAttribute +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject + +@ContributesMultibinding( + scope = AppScope::class, + boundType = JsonToMatchingAttributeMapper::class, +) +@ContributesMultibinding( + scope = AppScope::class, + boundType = AttributeMatcherPlugin::class, +) +@SingleInstanceIn(AppScope::class) +class DuckPlayerEnabledRMFMatchingAttribute @Inject constructor( + private val duckPlayer: DuckPlayer, + private val duckPlayerFeature: DuckPlayerFeature, +) : JsonToMatchingAttributeMapper, AttributeMatcherPlugin { + override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? { + val duckPlayerEnabled = duckPlayerFeature.self().isEnabled() && duckPlayerFeature.enableDuckPlayer().isEnabled() && + duckPlayer.getUserPreferences().privatePlayerMode.let { it == AlwaysAsk || it == Enabled } + return when (matchingAttribute) { + is DuckPlayerEnabledMatchingAttribute -> duckPlayerEnabled == matchingAttribute.remoteValue + else -> null + } + } + + override fun map( + key: String, + jsonMatchingAttribute: JsonMatchingAttribute, + ): MatchingAttribute? { + return when (key) { + DuckPlayerEnabledMatchingAttribute.KEY -> { + jsonMatchingAttribute.value?.let { + DuckPlayerEnabledMatchingAttribute(jsonMatchingAttribute.value as Boolean) + } + } + else -> null + } + } +} + +internal data class DuckPlayerEnabledMatchingAttribute( + val remoteValue: Boolean, +) : MatchingAttribute { + companion object { + const val KEY = "duckPlayerEnabled" + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt new file mode 100644 index 000000000000..cba33e4eb068 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeature.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "duckPlayer", + settingsStore = DuckPlayerFatureSettingsStore::class, +) +/** + * This is the class that represents the duckPlayer feature flags + */ +interface DuckPlayerFeature { + /** + * @return `true` when the remote config has the global "duckPlayer" feature flag enabled + * If the remote feature is not present defaults to `false` + */ + @Toggle.DefaultValue(false) + fun self(): Toggle + + /** + * @return `true` when the remote config has the "enableDuckPlayer" feature flag enabled + * If the remote feature is not present defaults to `false` + */ + @Toggle.DefaultValue(false) + fun enableDuckPlayer(): Toggle +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeaturePlugin.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeaturePlugin.kt new file mode 100644 index 000000000000..4c2580e45f3d --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeaturePlugin.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.api.DuckPlayerFeatureName +import com.duckduckgo.privacy.config.api.PrivacyFeaturePlugin +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class DuckPlayerFeaturePlugin @Inject constructor( + private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, +) : PrivacyFeaturePlugin { + + override fun store(featureName: String, jsonString: String): Boolean { + if (featureName == this.featureName) { + duckPlayerFeatureRepository.setDuckPlayerRemoteConfigJson(jsonString) + return true + } + return false + } + + override val featureName: String = DuckPlayerFeatureName.DuckPlayer.value +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureRepository.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureRepository.kt new file mode 100644 index 000000000000..139a38e1a986 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureRepository.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface DuckPlayerFeatureRepository { + fun getDuckPlayerRemoteConfigJson(): String + + fun setDuckPlayerRemoteConfigJson(jsonString: String) + + suspend fun getUserPreferences(): UserPreferences + + fun observeUserPreferences(): Flow + + suspend fun setUserPreferences(userPreferences: UserPreferences) + + suspend fun storeDuckPlayerDisabledHelpPageLink(duckPlayerDisabledHelpPageLink: String?) + + suspend fun getDuckPlayerDisabledHelpPageLink(): String? + + suspend fun storeYouTubePath(youtubePath: String) + + suspend fun storeYoutubeEmbedUrl(embedUrl: String) + + suspend fun storeYouTubeUrl(youtubeUrl: String) + + suspend fun storeYouTubeReferrerHeaders(youtubeReferrerHeaders: List) + + suspend fun storeYouTubeReferrerQueryParams(youtubeReferrerQueryParams: List) + + suspend fun storeYouTubeVideoIDQueryParam(youtubeVideoIDQueryParam: String) + + suspend fun getVideoIDQueryParam(): String + suspend fun getYouTubeReferrerQueryParams(): List + suspend fun getYouTubeReferrerHeaders(): List + suspend fun getYouTubeWatchPath(): String + suspend fun getYouTubeUrl(): String + suspend fun getYouTubeEmbedUrl(): String + suspend fun isOnboarded(): Boolean + suspend fun setUserOnboarded() +} + +@ContributesBinding(AppScope::class) +class RealDuckPlayerFeatureRepository @Inject constructor( + private val duckPlayerDataStore: DuckPlayerDataStore, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) : DuckPlayerFeatureRepository { + + private var duckPlayerRC = "" + + init { + loadToMemory() + } + + private fun loadToMemory() { + appCoroutineScope.launch(dispatcherProvider.io()) { + duckPlayerRC = + duckPlayerDataStore.getDuckPlayerRemoteConfigJson() + } + } + + override fun getDuckPlayerRemoteConfigJson(): String { + return duckPlayerRC + } + + override fun setDuckPlayerRemoteConfigJson(jsonString: String) { + appCoroutineScope.launch(dispatcherProvider.io()) { + duckPlayerDataStore.setDuckPlayerRemoteConfigJson(jsonString) + loadToMemory() + } + } + + override suspend fun setUserPreferences( + userPreferences: UserPreferences, + ) { + withContext(dispatcherProvider.io()) { + duckPlayerDataStore.setOverlayInteracted(userPreferences.overlayInteracted) + duckPlayerDataStore.setPrivatePlayerMode(userPreferences.privatePlayerMode.value) + } + } + + override fun observeUserPreferences(): Flow { + return duckPlayerDataStore.observePrivatePlayerMode() + .combine(duckPlayerDataStore.observeOverlayInteracted()) { privatePlayerMode, overlayInteracted -> + UserPreferences( + overlayInteracted = overlayInteracted, + privatePlayerMode = when (privatePlayerMode) { + Enabled.value -> Enabled + Disabled.value -> Disabled + else -> AlwaysAsk + }, + ) + } + } + + override suspend fun getUserPreferences(): UserPreferences { + return UserPreferences( + overlayInteracted = duckPlayerDataStore.getOverlayInteracted(), + privatePlayerMode = when (duckPlayerDataStore.getPrivatePlayerMode()) { + Enabled.value -> Enabled + Disabled.value -> Disabled + else -> AlwaysAsk + }, + ) + } + + override suspend fun storeDuckPlayerDisabledHelpPageLink(duckPlayerDisabledHelpPageLink: String?) { + duckPlayerDataStore.storeDuckPlayerDisabledHelpPageLink(duckPlayerDisabledHelpPageLink) + } + + override suspend fun getDuckPlayerDisabledHelpPageLink(): String? { + return duckPlayerDataStore.getDuckPlayerDisabledHelpPageLink() + } + + override suspend fun storeYouTubePath(youtubePath: String) { + duckPlayerDataStore.storeYouTubeWatchPath(youtubePath) + } + + override suspend fun storeYouTubeReferrerHeaders(youtubeReferrerHeaders: List) { + duckPlayerDataStore.storeYouTubeReferrerHeaders(youtubeReferrerHeaders) + } + + override suspend fun storeYouTubeReferrerQueryParams(youtubeReferrerQueryParams: List) { + duckPlayerDataStore.storeYouTubeReferrerQueryParams(youtubeReferrerQueryParams) + } + + override suspend fun storeYouTubeUrl(youtubeUrl: String) { + duckPlayerDataStore.storeYouTubeUrl(youtubeUrl) + } + + override suspend fun storeYouTubeVideoIDQueryParam(youtubeVideoIDQueryParam: String) { + duckPlayerDataStore.storeYouTubeVideoIDQueryParam(youtubeVideoIDQueryParam) + } + + override suspend fun storeYoutubeEmbedUrl(embedUrl: String) { + duckPlayerDataStore.storeYoutubeEmbedUrl(embedUrl) + } + + override suspend fun getVideoIDQueryParam(): String { + return duckPlayerDataStore.getYouTubeVideoIDQueryParam() + } + + override suspend fun getYouTubeReferrerQueryParams(): List { + return duckPlayerDataStore.getYouTubeReferrerQueryParams() + } + + override suspend fun getYouTubeReferrerHeaders(): List { + return duckPlayerDataStore.getYouTubeReferrerHeaders() + } + + override suspend fun getYouTubeWatchPath(): String { + return duckPlayerDataStore.getYouTubeWatchPath() + } + + override suspend fun getYouTubeUrl(): String { + return duckPlayerDataStore.getYouTubeUrl() + } + + override suspend fun getYouTubeEmbedUrl(): String { + return duckPlayerDataStore.getYoutubeEmbedUrl() + } + + override suspend fun isOnboarded(): Boolean { + return duckPlayerDataStore.getUserOnboarded() + } + + override suspend fun setUserOnboarded() { + duckPlayerDataStore.setUserOnboarded() + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureSettingsStore.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureSettingsStore.kt new file mode 100644 index 000000000000..8369d7c55e00 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerFeatureSettingsStore.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.FeatureSettings +import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.Json +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@ContributesBinding(AppScope::class) +@RemoteFeatureStoreNamed(DuckPlayerFeature::class) +class DuckPlayerFatureSettingsStore @Inject constructor( + private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, + @AppCoroutineScope private val coroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) : FeatureSettings.Store { + + private val jsonAdapter by lazy { buildJsonAdapter() } + + override fun store(jsonString: String) { + coroutineScope.launch(dispatcherProvider.io()) { + try { + jsonAdapter.fromJson(jsonString)?.let { + duckPlayerFeatureRepository.storeDuckPlayerDisabledHelpPageLink(it.duckPlayerDisabledHelpPageLink) + duckPlayerFeatureRepository.storeYouTubePath(it.youtubePath) + duckPlayerFeatureRepository.storeYoutubeEmbedUrl(it.youtubeEmbedUrl) + duckPlayerFeatureRepository.storeYouTubeUrl(it.youTubeUrl) + duckPlayerFeatureRepository.storeYouTubeReferrerHeaders(it.youTubeReferrerHeaders) + duckPlayerFeatureRepository.storeYouTubeReferrerQueryParams(it.youTubeReferrerQueryParams) + duckPlayerFeatureRepository.storeYouTubeVideoIDQueryParam(it.youTubeVideoIDQueryParam) + } ?: run { + // If no help link page present, we clear the stored one + duckPlayerFeatureRepository.storeDuckPlayerDisabledHelpPageLink(null) + } + } catch (e: Exception) { + Timber.d("Failed to store DuckPlayer settings", e) + // If no help link page present, we clear the stored one + duckPlayerFeatureRepository.storeDuckPlayerDisabledHelpPageLink(null) + } + } + } + + private fun buildJsonAdapter(): JsonAdapter { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + return moshi.adapter(DuckPlayerSetting::class.java) + } +} + +@JsonClass(generateAdapter = true) +data class DuckPlayerSetting( + @field:Json(name = "duckPlayerDisabledHelpPageLink") + val duckPlayerDisabledHelpPageLink: String?, + @field:Json(name = "youtubePath") + val youtubePath: String, + @field:Json(name = "youtubeEmbedUrl") + val youtubeEmbedUrl: String, + @field:Json(name = "youTubeUrl") + val youTubeUrl: String, + @field:Json(name = "youTubeReferrerHeaders") + val youTubeReferrerHeaders: List, + @field:Json(name = "youTubeReferrerQueryParams") + val youTubeReferrerQueryParams: List, + @field:Json(name = "youTubeVideoIDQueryParams") + val youTubeVideoIDQueryParam: String, + +) diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerLocalFilesPath.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerLocalFilesPath.kt new file mode 100644 index 000000000000..6d37816dc4a5 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerLocalFilesPath.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.content.res.AssetManager +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import java.io.IOException +import javax.inject.Inject + +interface DuckPlayerLocalFilesPath { + val assetsPath: List +} + +@ContributesBinding(AppScope::class) +class RealDuckPlayerLocalFilesPath @Inject constructor(private val assetManager: AssetManager) : DuckPlayerLocalFilesPath { + + override val assetsPath: List = getAllAssetFilePaths("duckplayer") + + private fun getAllAssetFilePaths(directory: String): List { + val filePaths = mutableListOf() + val files = assetManager.list(directory) ?: return emptyList() + + files.forEach { + val fullPath = "$directory/$it" + try { + assetManager.open(fullPath) + filePaths.add(fullPath.removePrefix("duckplayer/")) + } catch (e: IOException) { + filePaths.addAll(getAllAssetFilePaths(fullPath)) + } + } + return filePaths + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerOnboardedRMFMatchingAttribute.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerOnboardedRMFMatchingAttribute.kt new file mode 100644 index 000000000000..a94db5bae23d --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerOnboardedRMFMatchingAttribute.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin +import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute +import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper +import com.duckduckgo.remote.messaging.api.MatchingAttribute +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject + +@ContributesMultibinding( + scope = AppScope::class, + boundType = JsonToMatchingAttributeMapper::class, +) +@ContributesMultibinding( + scope = AppScope::class, + boundType = AttributeMatcherPlugin::class, +) +@SingleInstanceIn(AppScope::class) +class DuckPlayerOnboardedRMFMatchingAttribute @Inject constructor( + private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, +) : JsonToMatchingAttributeMapper, AttributeMatcherPlugin { + override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? { + return when (matchingAttribute) { + is DuckPlayerOnboardedMatchingAttribute -> duckPlayerFeatureRepository.isOnboarded() == matchingAttribute.remoteValue + else -> null + } + } + + override fun map( + key: String, + jsonMatchingAttribute: JsonMatchingAttribute, + ): MatchingAttribute? { + return when (key) { + DuckPlayerOnboardedMatchingAttribute.KEY -> { + jsonMatchingAttribute.value?.let { + DuckPlayerOnboardedMatchingAttribute(jsonMatchingAttribute.value as Boolean) + } + } + else -> null + } + } +} + +internal data class DuckPlayerOnboardedMatchingAttribute( + val remoteValue: Boolean, +) : MatchingAttribute { + companion object { + const val KEY = "duckPlayerOnboarded" + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt new file mode 100644 index 000000000000..60759dc26693 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerPixelName.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.duckduckgo.app.statistics.pixels.Pixel + +enum class DuckPlayerPixelName(override val pixelName: String) : Pixel.PixelName { + DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS("duck-player_overlay_youtube_impressions"), + DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY("duck-player_view-from_youtube_main-overlay"), + DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE("duck-player_overlay_youtube_watch_here"), + DUCK_PLAYER_WATCH_ON_YOUTUBE("duck-player_watch_on_youtube"), + DUCK_PLAYER_DAILY_UNIQUE_VIEW("duck-player_daily-unique-view"), + DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC("duck-player_view-from_youtube_automatic"), + DUCK_PLAYER_VIEW_FROM_OTHER("duck-player_view-from_other"), + DUCK_PLAYER_VIEW_FROM_SERP("duck-player_view-from_serp"), + DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS("duck-player_setting_always_settings"), + DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT("duck-player_setting_back-to-default"), + DUCK_PLAYER_SETTINGS_NEVER_SETTINGS("duck-player_setting_never_settings"), + DUCK_PLAYER_SETTINGS_PRESSED("duck_player_setting_pressed"), +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt new file mode 100644 index 000000000000..157d19fd7f2e --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerScriptsJsMessaging.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.webkit.JavascriptInterface +import android.webkit.WebView +import androidx.core.net.toUri +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.extensions.toTldPlusOne +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.js.messaging.api.JsCallbackData +import com.duckduckgo.js.messaging.api.JsMessage +import com.duckduckgo.js.messaging.api.JsMessageCallback +import com.duckduckgo.js.messaging.api.JsMessageHandler +import com.duckduckgo.js.messaging.api.JsMessageHelper +import com.duckduckgo.js.messaging.api.JsMessaging +import com.duckduckgo.js.messaging.api.JsRequestResponse +import com.duckduckgo.js.messaging.api.SubscriptionEvent +import com.duckduckgo.js.messaging.api.SubscriptionEventData +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.Moshi +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.runBlocking +import logcat.logcat + +@ContributesBinding(ActivityScope::class) +@Named("DuckPlayer") +class DuckPlayerScriptsJsMessaging @Inject constructor( + private val jsMessageHelper: JsMessageHelper, + private val dispatcherProvider: DispatcherProvider, +) : JsMessaging { + private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build() + + private lateinit var webView: WebView + private lateinit var jsMessageCallback: JsMessageCallback + + private val handlers = listOf( + DuckPlayerPageHandler(), + ) + + @JavascriptInterface + override fun process(message: String, secret: String) { + try { + val adapter = moshi.adapter(JsMessage::class.java) + val jsMessage = adapter.fromJson(message) + val url = runBlocking(dispatcherProvider.main()) { + webView.url?.toUri()?.host + } + jsMessage?.let { + logcat { jsMessage.toString() } + if (this.secret == secret && context == jsMessage.context && isUrlAllowed(url)) { + handlers.firstOrNull { + it.methods.contains(jsMessage.method) && it.featureName == jsMessage.featureName + }?.process(jsMessage, secret, jsMessageCallback) + } + } + } catch (e: Exception) { + logcat { "Exception is ${e.message}" } + } + } + + override fun register(webView: WebView, jsMessageCallback: JsMessageCallback?) { + if (jsMessageCallback == null) throw Exception("Callback cannot be null") + this.webView = webView + this.jsMessageCallback = jsMessageCallback + this.webView.addJavascriptInterface(this, context) + } + + override fun sendSubscriptionEvent(subscriptionEventData: SubscriptionEventData) { + val subscriptionEvent = SubscriptionEvent( + context, + subscriptionEventData.featureName, + subscriptionEventData.subscriptionName, + subscriptionEventData.params, + ) + jsMessageHelper.sendSubscriptionEvent(subscriptionEvent, callbackName, secret, webView) + } + + override fun onResponse(response: JsCallbackData) { + val jsResponse = JsRequestResponse.Success( + context = context, + featureName = response.featureName, + method = response.method, + id = response.id, + result = response.params, + ) + + jsMessageHelper.sendJsResponse(jsResponse, callbackName, secret, webView) + } + + override val context: String = "specialPages" + override val callbackName: String = "messageCallback" + override val secret: String = "duckduckgo-android-messaging-secret" + override val allowedDomains: List = listOf() + + private fun isUrlAllowed(url: String?): Boolean { + if (allowedDomains.isEmpty()) return true + val eTld = url?.toTldPlusOne() ?: return false + return (allowedDomains.contains(eTld)) + } + + inner class DuckPlayerPageHandler : JsMessageHandler { + override fun process(jsMessage: JsMessage, secret: String, jsMessageCallback: JsMessageCallback?) { + jsMessageCallback?.process(featureName, jsMessage.method, jsMessage.id ?: "", jsMessage.params) + } + + override val allowedDomains: List = emptyList() + override val featureName: String = "duckPlayerPage" + override val methods: List = listOf( + "initialSetup", + "openSettings", + "openInfo", + "setUserValues", + "reportPageException", + "reportInitException", + ) + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt new file mode 100644 index 000000000000..d9f94e14745e --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettings.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.content.Context +import android.view.View +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.ui.view.listitem.OneLineListItem +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_PRESSED +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.settings.api.DuckPlayerSettingsPlugin +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +@ContributesMultibinding(ActivityScope::class) +@PriorityKey(100) +class DuckPlayerSettingsTitle @Inject constructor( + private val globalActivityStarter: GlobalActivityStarter, + private val pixel: Pixel, +) : DuckPlayerSettingsPlugin { + override fun getView(context: Context): View { + return OneLineListItem(context).apply { + setPrimaryText(context.getString(R.string.duck_player_setting_title)) + setOnClickListener { + pixel.fire(DUCK_PLAYER_SETTINGS_PRESSED) + globalActivityStarter.start(this.context, DuckPlayerSettingsNoParams, null) + } + } + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsActivity.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsActivity.kt new file mode 100644 index 000000000000..7549457853c5 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsActivity.kt @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.os.Bundle +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.duckduckgo.anvil.annotations.ContributeToActivityStarter +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams +import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.view.addClickableLink +import com.duckduckgo.common.ui.view.dialog.RadioListAlertDialogBuilder +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams +import com.duckduckgo.duckplayer.api.PrivatePlayerMode +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.ViewState +import com.duckduckgo.duckplayer.impl.databinding.ActivityDuckPlayerSettingsBinding +import com.duckduckgo.navigation.api.GlobalActivityStarter +import javax.inject.Inject +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ActivityScope::class) +@ContributeToActivityStarter(DuckPlayerSettingsNoParams::class) +class DuckPlayerSettingsActivity : DuckDuckGoActivity() { + + private val viewModel: DuckPlayerSettingsViewModel by bindViewModel() + private val binding: ActivityDuckPlayerSettingsBinding by viewBinding() + + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + binding.duckPlayerSettingsText.addClickableLink( + annotation = "learn_more_link", + textSequence = getText(R.string.duck_player_settings_activity_description), + onClick = { viewModel.duckPlayerLearnMoreClicked() }, + ) + + setupToolbar(binding.includeToolbar.toolbar) + + configureUiEventHandlers() + observeViewModel() + } + + private fun configureUiEventHandlers() { + binding.duckPlayerModeSelector.setClickListener { + viewModel.duckPlayerModeSelectorClicked() + } + binding.duckPlayerDisabledLearnMoreButton.setOnClickListener { + viewModel.onContingencyLearnMoreClicked() + } + } + + private fun observeViewModel() { + viewModel.viewState + .flowWithLifecycle(lifecycle, Lifecycle.State.CREATED) + .onEach { viewState -> renderViewState(viewState) } + .launchIn(lifecycleScope) + + viewModel.commands + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .onEach { processCommand(it) } + .launchIn(lifecycleScope) + } + + private fun processCommand(it: DuckPlayerSettingsViewModel.Command) { + when (it) { + is DuckPlayerSettingsViewModel.Command.OpenPlayerModeSelector -> { + launchPlayerModeSelector(it.current) + } + is DuckPlayerSettingsViewModel.Command.OpenLearnMore -> { + globalActivityStarter.start( + this, + WebViewActivityWithParams( + url = it.learnMoreLink, + screenTitle = getString(R.string.duck_player_setting_title), + ), + ) + } + is DuckPlayerSettingsViewModel.Command.LaunchDuckPlayerContingencyPage -> { + globalActivityStarter.start( + this, + WebViewActivityWithParams( + url = it.helpPageLink, + screenTitle = getString(R.string.duck_player_unavailable), + ), + ) + } + } + } + + private fun launchPlayerModeSelector(privatePlayerMode: PrivatePlayerMode) { + val options = + listOf( + Pair(Enabled, R.string.duck_player_mode_always), + Pair(AlwaysAsk, R.string.duck_player_mode_always_ask), + Pair(Disabled, R.string.duck_player_mode_never), + ) + RadioListAlertDialogBuilder(this) + .setTitle(getString(R.string.duck_player_mode_dialog_title)) + .setOptions( + options.map { it.second }, + options.map { it.first }.indexOf(privatePlayerMode) + 1, + ) + .setPositiveButton(R.string.duck_player_save) + .setNegativeButton(R.string.duck_player_cancel) + .addEventListener( + object : RadioListAlertDialogBuilder.EventListener() { + override fun onPositiveButtonClicked(selectedItem: Int) { + val selectedPlayerMode = + when (selectedItem) { + 1 -> Enabled + 2 -> AlwaysAsk + else -> Disabled + } + viewModel.onPlayerModeSelected(selectedPlayerMode) + } + }, + ) + .show() + } + + private fun renderViewState(viewState: ViewState) { + when (viewState) { + is ViewState.Enabled -> { + binding.duckPlayerModeSelector.isEnabled = true + binding.duckPlayerDisabledSection.isVisible = false + setDuckPlayerSectionVisibility(true) + } + is ViewState.DisabledWithHelpLink -> { + binding.duckPlayerModeSelector.isEnabled = false + binding.duckPlayerDisabledSection.isVisible = true + setDuckPlayerSectionVisibility(false) + } + } + binding.duckPlayerModeSelector.setSecondaryText( + when (viewState.privatePlayerMode) { + Enabled -> getString(R.string.duck_player_mode_always) + Disabled -> getString(R.string.duck_player_mode_never) + else -> getString(R.string.duck_player_mode_always_ask) + }, + ) + } + + private fun setDuckPlayerSectionVisibility(isVisible: Boolean) { + binding.duckPlayerSettingsTitle.isVisible = isVisible + binding.duckPlayerSettingsIcon.isVisible = isVisible + binding.duckPlayerSettingsText.isVisible = isVisible + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt new file mode 100644 index 000000000000..d4e430440614 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/DuckPlayerSettingsViewModel.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK +import com.duckduckgo.duckplayer.api.PrivatePlayerMode +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_SETTINGS_NEVER_SETTINGS +import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.Command.OpenLearnMore +import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.Command.OpenPlayerModeSelector +import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.ViewState.DisabledWithHelpLink +import com.duckduckgo.duckplayer.impl.DuckPlayerSettingsViewModel.ViewState.Enabled +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +@ContributesViewModel(ActivityScope::class) +class DuckPlayerSettingsViewModel @Inject constructor( + private val duckPlayer: DuckPlayer, + private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, + private val pixel: Pixel, +) : ViewModel() { + + private val commandChannel = Channel(capacity = 1, onBufferOverflow = DROP_OLDEST) + val commands = commandChannel.receiveAsFlow() + + val viewState: StateFlow = duckPlayer.observeUserPreferences() + .map { + val helpPageLink = duckPlayerFeatureRepository.getDuckPlayerDisabledHelpPageLink() + if (duckPlayer.getDuckPlayerState() == DISABLED_WIH_HELP_LINK && helpPageLink?.isNotEmpty() == true) { + DisabledWithHelpLink(it.privatePlayerMode, helpPageLink) + } else { + Enabled(it.privatePlayerMode) + } + } + .stateIn( + viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = runBlocking { Enabled(duckPlayer.getUserPreferences().privatePlayerMode) }, + ) + + sealed class Command { + data class OpenPlayerModeSelector(val current: PrivatePlayerMode) : Command() + data class OpenLearnMore(val learnMoreLink: String) : Command() + data class LaunchDuckPlayerContingencyPage(val helpPageLink: String) : Command() + } + + sealed class ViewState(open val privatePlayerMode: PrivatePlayerMode = AlwaysAsk) { + data class Enabled(override val privatePlayerMode: PrivatePlayerMode) : ViewState(privatePlayerMode) + data class DisabledWithHelpLink(override val privatePlayerMode: PrivatePlayerMode, val helpPageLink: String) : ViewState(privatePlayerMode) + } + fun duckPlayerModeSelectorClicked() { + viewModelScope.launch { + commandChannel.send(OpenPlayerModeSelector(duckPlayer.getUserPreferences().privatePlayerMode)) + } + } + + fun onPlayerModeSelected(selectedPlayerMode: PrivatePlayerMode) { + viewModelScope.launch { + duckPlayer.setUserPreferences(overlayInteracted = false, privatePlayerMode = selectedPlayerMode.value).also { + val pixelName = when (selectedPlayerMode) { + is PrivatePlayerMode.Enabled -> { DUCK_PLAYER_SETTINGS_ALWAYS_SETTINGS } + is AlwaysAsk -> { DUCK_PLAYER_SETTINGS_BACK_TO_DEFAULT } + is Disabled -> { DUCK_PLAYER_SETTINGS_NEVER_SETTINGS } + } + pixel.fire(pixelName) + } + } + } + + fun duckPlayerLearnMoreClicked() { + viewModelScope.launch { + commandChannel.send(OpenLearnMore("https://duckduckgo.com/duckduckgo-help-pages/duck-player/")) + } + } + + fun onContingencyLearnMoreClicked() { + viewModelScope.launch { + duckPlayerFeatureRepository.getDuckPlayerDisabledHelpPageLink()?.let { + commandChannel.send(Command.LaunchDuckPlayerContingencyPage(it)) + } + } + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/JSONObjectAdapter.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/JSONObjectAdapter.kt new file mode 100644 index 000000000000..87f09b7415ea --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/JSONObjectAdapter.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.ToJson +import okio.Buffer +import org.json.JSONException +import org.json.JSONObject + +internal class JSONObjectAdapter { + + @FromJson + fun fromJson(reader: JsonReader): JSONObject? { + // Here we're expecting the JSON object, it is processed as Map by Moshi + return (reader.readJsonValue() as? Map<*, *>)?.let { data -> + try { + JSONObject(data) + } catch (e: JSONException) { + // Handle exception + return null + } + } + } + + @ToJson + fun toJson( + writer: JsonWriter, + value: JSONObject?, + ) { + value?.let { writer.run { value(Buffer().writeUtf8(value.toString())) } } + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt new file mode 100644 index 000000000000..4a9642ce6a5f --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/RealDuckPlayer.kt @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.content.res.Configuration +import android.net.Uri +import android.webkit.MimeTypeMap +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import androidx.core.net.toUri +import androidx.fragment.app.FragmentManager +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.UrlScheme.Companion.duck +import com.duckduckgo.common.utils.UrlScheme.Companion.https +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_DAILY_UNIQUE_VIEW +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_OTHER +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_SERP +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_WATCH_ON_YOUTUBE +import com.duckduckgo.duckplayer.impl.ui.DuckPlayerPrimeBottomSheet +import com.duckduckgo.duckplayer.impl.ui.DuckPlayerPrimeDialogFragment +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import java.io.InputStream +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +private const val YOUTUBE_HOST = "youtube.com" +private const val YOUTUBE_MOBILE_HOST = "m.youtube.com" +private const val DUCK_PLAYER_VIDEO_ID_QUERY_PARAM = "videoID" +private const val DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH = "openInYoutube" +private const val DUCK_PLAYER_DOMAIN = "player" +private const val DUCK_PLAYER_URL_BASE = "$duck://$DUCK_PLAYER_DOMAIN/" +private const val DUCK_PLAYER_ASSETS_PATH = "duckplayer/" +private const val DUCK_PLAYER_ASSETS_INDEX_PATH = "${DUCK_PLAYER_ASSETS_PATH}index.html" + +@SingleInstanceIn(AppScope::class) +@ContributesBinding(AppScope::class) +class RealDuckPlayer @Inject constructor( + private val duckPlayerFeatureRepository: DuckPlayerFeatureRepository, + private val duckPlayerFeature: DuckPlayerFeature, + private val pixel: Pixel, + private val duckPlayerLocalFilesPath: DuckPlayerLocalFilesPath, + private val mimeTypeMap: MimeTypeMap, + private val dispatchers: DispatcherProvider, +) : DuckPlayer { + + private var shouldForceYTNavigation = false + private var shouldHideOverlay = false + private val isFeatureEnabled: Boolean by lazy { + duckPlayerFeature.self().isEnabled() && duckPlayerFeature.enableDuckPlayer().isEnabled() + } + + private lateinit var duckPlayerDisabledHelpLink: String + + override suspend fun getDuckPlayerState(): DuckPlayerState { + if (!::duckPlayerDisabledHelpLink.isInitialized) { + duckPlayerDisabledHelpLink = duckPlayerFeatureRepository.getDuckPlayerDisabledHelpPageLink() ?: "" + } + return if (isFeatureEnabled) { + ENABLED + } else if (duckPlayerDisabledHelpLink.isNotBlank()) { + DISABLED_WIH_HELP_LINK + } else { + DISABLED + } + } + + override suspend fun setUserPreferences( + overlayInteracted: Boolean, + privatePlayerMode: String, + ) { + val playerMode = when { + privatePlayerMode.contains("disabled") -> Disabled + privatePlayerMode.contains("enabled") -> Enabled + else -> AlwaysAsk + } + duckPlayerFeatureRepository.setUserPreferences(UserPreferences(overlayInteracted, playerMode)) + } + + override suspend fun getUserPreferences(): UserPreferences { + return duckPlayerFeatureRepository.getUserPreferences().let { + UserPreferences(it.overlayInteracted, it.privatePlayerMode) + } + } + + override fun shouldHideDuckPlayerOverlay(): Boolean { + return shouldHideOverlay + } + + override fun duckPlayerOverlayHidden() { + shouldHideOverlay = false + } + + private suspend fun shouldNavigateToDuckPlayer(): Boolean { + if (!isFeatureEnabled) return false + val result = getUserPreferences().privatePlayerMode == Enabled && !shouldForceYTNavigation + return result + } + + override fun duckPlayerNavigatedToYoutube() { + shouldForceYTNavigation = false + } + + override fun observeUserPreferences(): Flow { + return duckPlayerFeatureRepository.observeUserPreferences().map { + UserPreferences(it.overlayInteracted, it.privatePlayerMode) + } + } + + override suspend fun sendDuckPlayerPixel( + pixelName: String, + pixelData: Map, + ) { + if (!isFeatureEnabled) return + val duckPlayerPixelName = when (pixelName) { + "overlay" -> DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS + "play.use" -> DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY + "play.do_not_use" -> DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE + else -> { null } + } + + duckPlayerPixelName?.let { + pixel.fire(duckPlayerPixelName, parameters = pixelData) + if (duckPlayerPixelName == DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS) { + duckPlayerFeatureRepository.setUserOnboarded() + } + } + } + + private suspend fun createYoutubeNoCookieFromDuckPlayer(uri: Uri): String? { + if (!isFeatureEnabled) return null + val embedUrl = duckPlayerFeatureRepository.getYouTubeEmbedUrl() + uri.pathSegments?.firstOrNull()?.let { videoID -> + return "$https://www.$embedUrl?$DUCK_PLAYER_VIDEO_ID_QUERY_PARAM=$videoID" + } + return null + } + + override suspend fun createYoutubeWatchUrlFromDuckPlayer(uri: Uri): String? { + val videoIdQueryParam = duckPlayerFeatureRepository.getVideoIDQueryParam() + val youTubeWatchPath = duckPlayerFeatureRepository.getYouTubeWatchPath() + val youTubeHost = duckPlayerFeatureRepository.getYouTubeUrl() + uri.getQueryParameter(videoIdQueryParam)?.let { videoID -> + return "$https://$youTubeHost/$youTubeWatchPath?$videoIdQueryParam=$videoID" + } ?: uri.pathSegments.firstOrNull { it != youTubeWatchPath }?.let { videoID -> + return "$https://$youTubeHost/$youTubeWatchPath?$videoIdQueryParam=$videoID" + } + return null + } + + private suspend fun youTubeRequestedFromDuckPlayer() { + shouldForceYTNavigation = true + if (getUserPreferences().privatePlayerMode == AlwaysAsk) { + shouldHideOverlay = true + } + if (isFeatureEnabled && + getUserPreferences().privatePlayerMode != Disabled + ) { + pixel.fire(DUCK_PLAYER_WATCH_ON_YOUTUBE) + } + } + private fun isDuckPlayerUri(uri: Uri): Boolean { + if (uri.normalizeScheme()?.scheme != duck) return false + if (uri.userInfo != null) return false + uri.host?.let { host -> + if (!host.contains(DUCK_PLAYER_DOMAIN)) return false + return !host.contains("!") + } + return false + } + + override fun isDuckPlayerUri(uri: String): Boolean { + return isDuckPlayerUri(uri.toUri()) + } + + override suspend fun isSimulatedYoutubeNoCookie(uri: Uri): Boolean { + val validPaths = duckPlayerLocalFilesPath.assetsPath + val embedUrl = duckPlayerFeatureRepository.getYouTubeEmbedUrl() + return ( + uri.host?.removePrefix("www.") == + embedUrl && ( + uri.pathSegments.firstOrNull() == null || + validPaths.any { uri.path?.contains(it) == true } || + (uri.pathSegments.firstOrNull() != "embed" && uri.getQueryParameter(DUCK_PLAYER_VIDEO_ID_QUERY_PARAM) != null) + ) + ) + } + + override suspend fun isSimulatedYoutubeNoCookie(uri: String): Boolean { + return isSimulatedYoutubeNoCookie(uri.toUri()) + } + + private fun getDuckPlayerAssetsPath(url: Uri): String? { + return url.path?.takeIf { it.isNotBlank() }?.removePrefix("/")?.let { "$DUCK_PLAYER_ASSETS_PATH$it" } + } + + override suspend fun isYoutubeWatchUrl(uri: Uri): Boolean { + val youTubeWatchPath = duckPlayerFeatureRepository.getYouTubeWatchPath() + return isYouTubeUrl(uri) && uri.pathSegments.firstOrNull() == youTubeWatchPath + } + + override fun isYouTubeUrl(uri: Uri): Boolean { + val host = uri.host?.removePrefix("www.") + return host == YOUTUBE_HOST || host == YOUTUBE_MOBILE_HOST + } + + override suspend fun createDuckPlayerUriFromYoutubeNoCookie(uri: Uri): String? { + if (!isFeatureEnabled) return null + return uri.getQueryParameter(DUCK_PLAYER_VIDEO_ID_QUERY_PARAM)?.let { + "$DUCK_PLAYER_URL_BASE$it" + } + } + + private suspend fun createDuckPlayerUriFromYoutube(uri: Uri): String { + val videoIdQueryParam = duckPlayerFeatureRepository.getVideoIDQueryParam() + return "$DUCK_PLAYER_URL_BASE${uri.getQueryParameter(videoIdQueryParam)}?origin=auto" + } + + override suspend fun intercept( + request: WebResourceRequest, + url: Uri, + webView: WebView, + ): WebResourceResponse? { + if (isDuckPlayerUri(url)) { + return processDuckPlayerUri(url, webView) + } else { + if (!isFeatureEnabled) return null + if (isYoutubeWatchUrl(url)) { + return processYouTubeWatchUri(request, url, webView) + } else if (isSimulatedYoutubeNoCookie(url)) { + return processSimulatedYouTubeNoCookieUri(url, webView) + } + } + return null + } + private fun processSimulatedYouTubeNoCookieUri( + url: Uri, + webView: WebView, + ): WebResourceResponse { + val path = getDuckPlayerAssetsPath(url) + val mimeType = mimeTypeMap.getMimeTypeFromExtension(path?.substringAfterLast(".")) + + if (path != null && mimeType != null) { + try { + val inputStream: InputStream = webView.context.assets.open(path) + return WebResourceResponse(mimeType, "UTF-8", inputStream) + } catch (e: Exception) { + return WebResourceResponse(null, null, null) + } + } else { + val inputStream: InputStream = webView.context.assets.open(DUCK_PLAYER_ASSETS_INDEX_PATH) + return WebResourceResponse("text/html", "UTF-8", inputStream).also { + pixel.fire(DUCK_PLAYER_DAILY_UNIQUE_VIEW, type = DAILY) + } + } + } + + private suspend fun processYouTubeWatchUri( + request: WebResourceRequest, + url: Uri, + webView: WebView, + ): WebResourceResponse? { + val referer = request.requestHeaders.keys.firstOrNull { it in duckPlayerFeatureRepository.getYouTubeReferrerHeaders() } + ?.let { url.getQueryParameter(it) } + val previousUrl = duckPlayerFeatureRepository.getYouTubeReferrerQueryParams() + .firstOrNull { url.getQueryParameter(it) != null } + ?.let { url.getQueryParameter(it) } + val currentUrl = withContext(dispatchers.main()) { webView.url } + + val videoIdQueryParam = duckPlayerFeatureRepository.getVideoIDQueryParam() + val requestedVideoId = url.getQueryParameter(videoIdQueryParam) + + val isSimulated: suspend (String?) -> Boolean = { uri -> + uri?.let { isSimulatedYoutubeNoCookie(it.toUri()) } == true + } + + val isMatchingVideoId: (String?) -> Boolean = { uri -> + uri?.toUri()?.getQueryParameter(DUCK_PLAYER_VIDEO_ID_QUERY_PARAM) == requestedVideoId + } + + if (isSimulated(referer) && isMatchingVideoId(referer) || + isSimulated(previousUrl) && isMatchingVideoId(previousUrl) + ) { + withContext(dispatchers.main()) { + webView.loadUrl("$DUCK_PLAYER_URL_BASE$DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH?$videoIdQueryParam=$requestedVideoId") + } + return WebResourceResponse(null, null, null) + } else if (isSimulated(currentUrl) && isMatchingVideoId(currentUrl)) { + return null + } else if (shouldNavigateToDuckPlayer()) { + withContext(dispatchers.main()) { + webView.loadUrl(createDuckPlayerUriFromYoutube(url)) + } + pixel.fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC) + return WebResourceResponse(null, null, null) + } + return null + } + + private suspend fun processDuckPlayerUri( + url: Uri, + webView: WebView, + ): WebResourceResponse { + if (url.pathSegments?.firstOrNull()?.equals(DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH, ignoreCase = true) == true || + !isFeatureEnabled || + getUserPreferences().privatePlayerMode == Disabled + ) { + createYoutubeWatchUrlFromDuckPlayer(url)?.let { youtubeUrl -> + youTubeRequestedFromDuckPlayer() + withContext(dispatchers.main()) { + webView.loadUrl(youtubeUrl) + } + } + } else { + createYoutubeNoCookieFromDuckPlayer(url)?.let { youtubeUrl -> + withContext(dispatchers.main()) { + webView.loadUrl(youtubeUrl) + } + val origin = url.getQueryParameter("origin") + if (origin == "serp") { + pixel.fire(DUCK_PLAYER_VIEW_FROM_SERP) + } else if (origin != "overlay" && origin != "auto") { + pixel.fire(DUCK_PLAYER_VIEW_FROM_OTHER) + } + } + } + return WebResourceResponse(null, null, null) + } + + override fun showDuckPlayerPrimeModal(configuration: Configuration, fragmentManager: FragmentManager, fromDuckPlayerPage: Boolean) { + if (!isFeatureEnabled) return + if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + DuckPlayerPrimeDialogFragment.newInstance(fromDuckPlayerPage).show(fragmentManager, null) + } else { + DuckPlayerPrimeBottomSheet.newInstance(fromDuckPlayerPage).show(fragmentManager, null) + } + } + + override suspend fun willNavigateToDuckPlayer( + destinationUrl: Uri, + ): Boolean { + return ( + isFeatureEnabled && + isYoutubeWatchUrl(destinationUrl) && + getUserPreferences().privatePlayerMode == Enabled + ) + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/di/DuckPlayerModule.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/di/DuckPlayerModule.kt new file mode 100644 index 000000000000..1e7b29dcd722 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/di/DuckPlayerModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl.di + +import android.content.Context +import android.content.res.AssetManager +import android.webkit.MimeTypeMap +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import dagger.SingleInstanceIn + +@Module +@ContributesTo(AppScope::class) +class DuckPlayerModule { + @Provides + @SingleInstanceIn(AppScope::class) + fun provideMimeTypeMap(): MimeTypeMap { + return MimeTypeMap.getSingleton() + } + + @Provides + @SingleInstanceIn(AppScope::class) + fun provideAssetManager(context: Context): AssetManager { + return context.assets + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeBottomSheet.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeBottomSheet.kt new file mode 100644 index 000000000000..29aa0b1f3207 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeBottomSheet.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl.ui + +import android.app.Dialog +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.lottie.LottieCompositionFactory +import com.airbnb.lottie.LottieDrawable +import com.duckduckgo.duckplayer.impl.R +import com.duckduckgo.duckplayer.impl.databinding.ModalDuckPlayerBinding +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class DuckPlayerPrimeBottomSheet : BottomSheetDialogFragment() { + + private lateinit var binding: ModalDuckPlayerBinding + private val isFromDuckPlayerPage: Boolean by lazy { requireArguments().getBoolean(FROM_DUCK_PLAYER_PAGE) } + + override fun getTheme(): Int = R.style.DuckPlayerBottomSheetDialogTheme + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = ModalDuckPlayerBinding.inflate(inflater, container, false) + LottieCompositionFactory.fromRawRes(context, R.raw.duckplayer) + binding.duckPlayerAnimation.setAnimation(R.raw.duckplayer) + binding.duckPlayerAnimation.playAnimation() + binding.duckPlayerAnimation.repeatCount = LottieDrawable.INFINITE + binding.title.text = + if (isFromDuckPlayerPage) { + getString(R.string.duck_player_info_modal_title_from_duck_player_page) + } else { + getString(R.string.duck_player_info_modal_title_from_overlay) + } + binding.dismissButton.setOnClickListener { + dismiss() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog + dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + return dialog + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + dismiss() + } + + companion object { + fun newInstance(fromDuckPlayerPage: Boolean): DuckPlayerPrimeBottomSheet = + DuckPlayerPrimeBottomSheet().also { + it.arguments = Bundle().apply { + putBoolean(FROM_DUCK_PLAYER_PAGE, fromDuckPlayerPage) + } + } + } +} diff --git a/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeDialogFragment.kt b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeDialogFragment.kt new file mode 100644 index 000000000000..7a98ce2fe9cc --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/java/com/duckduckgo/duckplayer/impl/ui/DuckPlayerPrimeDialogFragment.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl.ui + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.fragment.app.DialogFragment +import com.airbnb.lottie.LottieCompositionFactory +import com.airbnb.lottie.LottieDrawable +import com.duckduckgo.duckplayer.impl.R +import com.duckduckgo.duckplayer.impl.databinding.ModalDuckPlayerBinding + +const val FROM_DUCK_PLAYER_PAGE = "fromDuckPlayerPage" + +class DuckPlayerPrimeDialogFragment : DialogFragment() { + + private lateinit var binding: ModalDuckPlayerBinding + private val isFromDuckPlayerPage: Boolean by lazy { requireArguments().getBoolean(FROM_DUCK_PLAYER_PAGE) } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = ModalDuckPlayerBinding.inflate(inflater, container, false) + LottieCompositionFactory.fromRawRes(context, R.raw.duckplayer) + binding.duckPlayerAnimation.setAnimation(R.raw.duckplayer) + binding.duckPlayerAnimation.playAnimation() + binding.duckPlayerAnimation.repeatCount = LottieDrawable.INFINITE + binding.title.text = + if (isFromDuckPlayerPage) { + getString(R.string.duck_player_info_modal_title_from_duck_player_page) + } else { + getString(R.string.duck_player_info_modal_title_from_overlay) + } + binding.dismissButton.setOnClickListener { + dismiss() + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, com.duckduckgo.mobile.android.R.style.Widget_DuckDuckGo_DialogFullScreen) + } + override fun onStart() { + super.onStart() + dialog?.window?.let { + WindowCompat.getInsetsController(it, binding.root).apply { + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(WindowInsetsCompat.Type.statusBars()) + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + dismiss() + } + companion object { + fun newInstance(fromDuckPlayerPage: Boolean): DuckPlayerPrimeDialogFragment = + DuckPlayerPrimeDialogFragment().also { + it.arguments = Bundle().apply { + putBoolean(FROM_DUCK_PLAYER_PAGE, fromDuckPlayerPage) + } + } + } +} diff --git a/duckplayer/duckplayer-impl/src/main/res/drawable/clean_tube_128.xml b/duckplayer/duckplayer-impl/src/main/res/drawable/clean_tube_128.xml new file mode 100644 index 000000000000..9d0df9e99301 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/drawable/clean_tube_128.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/duckplayer/duckplayer-impl/src/main/res/drawable/duck_player_animation_background.xml b/duckplayer/duckplayer-impl/src/main/res/drawable/duck_player_animation_background.xml new file mode 100644 index 000000000000..b6fd54151c06 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/drawable/duck_player_animation_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/duckplayer/duckplayer-impl/src/main/res/drawable/rounded_top_corners_bottom_sheet_background.xml b/duckplayer/duckplayer-impl/src/main/res/drawable/rounded_top_corners_bottom_sheet_background.xml new file mode 100644 index 000000000000..7b719e31ea46 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/drawable/rounded_top_corners_bottom_sheet_background.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/drawable/youtube_warning_96.xml b/duckplayer/duckplayer-impl/src/main/res/drawable/youtube_warning_96.xml new file mode 100644 index 000000000000..a330b269a631 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/drawable/youtube_warning_96.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/duckplayer/duckplayer-impl/src/main/res/layout/activity_duck_player_settings.xml b/duckplayer/duckplayer-impl/src/main/res/layout/activity_duck_player_settings.xml new file mode 100644 index 000000000000..1a2969f97ded --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/layout/activity_duck_player_settings.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/layout/modal_duck_player.xml b/duckplayer/duckplayer-impl/src/main/res/layout/modal_duck_player.xml new file mode 100644 index 000000000000..1fe6affb4412 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/layout/modal_duck_player.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/raw/duckplayer.json b/duckplayer/duckplayer-impl/src/main/res/raw/duckplayer.json new file mode 100644 index 000000000000..cda1d1f38ec1 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/raw/duckplayer.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":368.000014988947,"w":320,"h":180,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":20,"h":20,"u":"","p":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSgBBwcHCggKEwoKEygaFhooKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKP/AABEIABQAFAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APprWtVg0i08+4ySTtRF6sa3w+HlXlyxIqVFBXZh6X4zt7u7SC4t2tw52q+/cM+/AxXZWyydOPNF3sYwxKk7NWOrrzDpOU+IGnz3dnbz26NIIC29VGTg45x7Y/WvTyytGnNxlpc5sTBySa6HE6Vp1xqN4kFujElhubHCD1Jr2a1aFGDlJnJCDm7I9hr5M9QKACgAoA//2Q==","e":1},{"id":"image_1","w":42,"h":6,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAGCAYAAAC1rQwWAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAKElEQVQokWP8////AYbBDy6wMDAw2A+0K4gBTAPtAGLBqEOpDYaMQwFnXQTaXe9dvQAAAABJRU5ErkJggg==","e":1},{"id":"image_2","w":67,"h":17,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAAARCAYAAACGjBGPAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABsUlEQVRYhe2WPS9EQRSGnxl31374WB+7IkE0ZEVIhEoiOoVKI9lo/AR+gl+gkvgNCoVGJWELjQaJRISCsGJ9rYRgl3tHoSFzd+9dca8NnvLMOZN33jlnMkIptcE/ADsGMPrTKioF+dMCKgmj6Ir5BFdLkFuDl2sQQagZgEQKoj0+SvQPoZRSWvT5BI5mIH9mUyKhbRZaprxX5y9pvTPMpxJGAFhwNg+BZmgc81ZeGby8KnIPCpur/URDjSAYELZruhk3KyWM+EBmoWLM2D402dwzeTWdc6WAoaRkuFc/uv6A5tbdKSicw+OBu1wPubyzSO+6MwLAUrC1b3F8YWlruhn5U/dKCln3uR6RvXWYiyJc2NTpZhgx9zvK0JeEfCfRkP38O9fpMd2M2kF3u4kgRPu+JOQ76WwVtMfLMyQREyQ7bPpAz0zB1TKoQukd45NQFS5LhBdIIZgYMTjKWOTunfPrItDVJgkYuoH2/4ybVTieA/RHBoBoP3QvgqwuT3llk7b/jjeNvx82kvwclxFomf6NRgDFOuMj+cz7j9Soh3A3yIBP0nwn7WzG36HImPxR3gAgSnf+tTIOIgAAAABJRU5ErkJggg==","e":1},{"id":"image_3","w":141,"h":49,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAAxCAYAAADnViqrAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAE6ElEQVR4nO2cTW8jNRjH/36ZSSZJ36sWVKkFLQtCohIcVkhw4MQNiQNfAS58BC6c+AYc+ATcEReEhFhx4bSIwyLx0iKBtIt2K9Ldtklmxh6bwzJJJnGaTJrMOIl/t9jJ+Innbz+PH4+HnJyc3IXDkQNOCHmnbCMciwUt2wDH4sFNhYQQcM7BGAMhpGibHCWjlEKSJJBSGuuHRMMYg+/7TiwrDGMMjDF4nocoiqCUytRnREMpHSkYologOjI2okgNoNUZmu2wAUIIKpUKOp1OpjwjGs/zsoLRErWrr1FrfwMuH4y8uAaB8F5Ga+0DRMFbs7XcUSqEEHieByFEr+z09FSnlUEQ9L6tQmz/+yn8+JdcjbTq7+Ny86Oh8nTKs83taa0hpRyagh09tNaZ2aY70wzezPWnX+QWDADUW19Bei+gU3+3W+Z5HjzPm8beQmCMGX23DYQxwXmLQ+n5tuMxjZ01CWZYTw9qwygaKh8jaH83tQGNyy8zouHcuEizhnS1GMdx2aZ0aYUU9/6s4WHTA1DM7Myoxu3nQxwfdYziSTFWVaKfQDC9tFlyBi7+mvr3q85lh+Lbn9fxsOmjKMEAQKIIfn0Q4O79NSTXTLpG0bCkeWMDmHzUMyZJbny9eWOTjT/+1kAky8u7nl14uP93MLK+EL+RTvuMsSKay0UaCNsimsdPOZpX5bvzP/6p4viwA2rQbmHW2RQv2MzZRfmCAQCZEJy3GHbWhgeT23uyDK3tSUnEI1xkobKmprlugdFaQ+vZroWrvj3L/rXA7LILEU2aVbQtsTcLZh0PHWzHuEdqpc84Ww2JRtUsYOPQ1+TmiThNe9H3sgoGmH1wH/garx6EM71mfjTeeLE9stYoGsmPbtgkgeCH3c/LKhjg2X+btds9Purg1nPlCIcSjTdvt7C3YX4sAhjhnqLq60joNpiaLl8TVe9As43uZ6XU0sUzKVrrmW8/EALceamNw90Yp48qaF5ySDXfgVfhCnsbEq8chCPdUkpXNJmAjvi42PwYm83PcmeGFanhcuPDTJmUcildVJrjmRf7mxL7m/O7/qQMBvvdXW4ACIIgc2Or7R+w/uRzUD3av/Uj2T6ebH8C6d8y1hNClkY481g52UqSJIii3rNUGdGYdqOJaiFofw8v/h1EXxkvqtgOYv81hMHbALEjOeWYHVEUZVaImTsshABjLBN/aFpHu/FecRY6rMKUUhiKTsMwnKufdiwGWmsIIYzbP0ZfEscxhBCglC5NDOKYHK31tQnLkQHIuB86VpflTJ445ooTjSM3TjSO3DjROHLjROPIzcTpW0rpUOJvlRh3qC49BlNk/6SbpUXn1SYSDed8KTcc88I5hxAic0QVeDagKpVKaf3DOUcURYXthY0dFpRSJ5g+PM8bmk3KFAzQe3FDYe2N+wLn3AlmgP4To7acTy/SjrGisaFDbKN/prEpxivKlrGtrMozI3noD4ZtemlAUbaMFY2U0glngP7VSpIkVvRPkXaMFY1SCkIIKzrGBoQQQyO6yJWLCaVUoSdYJ1pyp/mJVQ6Kr8vTKKUQhqHL0wxStJoXjfShpVXAntDfsTA40ThyQ23KMzgWA5p5o6fDMQF0a2trZVdEjumgvu9jb2/PCccxEY1GA0T/n5WK4xjn5+fodDpWpcYddkAIQb1ex+7uLv4DTsP3dx/5jJUAAAAASUVORK5CYII=","e":1},{"id":"image_4","w":141,"h":18,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAASCAYAAABmbl0zAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABzUlEQVRoge3YsUsCURwH8K+VHppGmBWXKIIQt4Z/gBwt0n/R0ugf4F8gTq5Jg6JTszlGuAUORdtNguDkkUle3B3na2qonmH6Xmr8Ptvdgy+/4cu9987HGLurVquoVCqwbRuE8ESjURQKBei6Dl+9XmflcnnZM5E1EAgEUCwW4dN1nY1Go2XPQ9ZELBbD1rTCHFsWLvp9HLruzIEvm5u4UlU8RCKiZiQrZjAYYIu3cOA4uDQMRDzv16Enr6841zQYodDCA5LVtMF7efr8PFdhAEBhDGemudBQZLVxvzTzFuZDaDL59JxKpZDL5RARuG11u120Wi2Mx2NhmWQ23NKIlEgkkM/n4ff7heYmk0mk02mUSiVMvpSUyMXdnkTKZrPCC/MhHo9D0zQp2WQ66aUJBoNS88PhsNR88p300hiGIS3b8zyp+YRPemna7TY6nY7wXMdx0Gg0MBwOhWeTn0k/CDPGUKvV0Gw2oaqqkEzXddHr9WBZlpA88jvc0jwueE542t7+9s40TZj0/+Zf4G5P9zs7uN7fnyvwdncXN3t7Cw1FVpsvk8mwaYtHto3029vMYT1FQVfybYks349nmr6ioK8ofzULWRPSb0/k/3kHh4mHa1Tzd/gAAAAASUVORK5CYII=","e":1},{"id":"image_5","w":29,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAECAYAAABySjRcAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAMElEQVQYlb3NQREAIAwEsYABrFQKUpBeB4eL7sy+I1TokKlXaByDbbxJEBaEi5pCP6MOPNpI69kPAAAAAElFTkSuQmCC","e":1},{"id":"image_6","w":101,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGUAAAAECAYAAACX1PEwAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAQUlEQVQ4jWO8e/fuf4ZRMCgAIyPjQiUlpQTG0UgZdGAi00C7YBSggI9MTEwLWBgYGBoH2iWjAAKYmJg2KCoqXgAApAUNMclgLKQAAAAASUVORK5CYII=","e":1},{"id":"image_7","w":141,"h":87,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_8","w":405,"h":708,"u":"","p":"data:image/jpeg;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":1,"nm":"Pale Orange Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.075],"y":[0.996]},"t":135,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":139,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0.013]},"t":150,"s":[100]},{"t":151.000006150356,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,90,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.326,"y":1},"o":{"x":0.739,"y":0.739},"t":139,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-8.289,-1.382],[-2.125,-2.625],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.75,1.125],[0.957,1.182],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[146.375,71.5],[158.375,65.625],[169.875,72.5],[169.5,68.25]],"c":true}]},{"i":{"x":0.302,"y":0.302},"o":{"x":0.752,"y":0},"t":144,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-11.125,-1],[-0.75,4.375],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.816,0.613],[0.275,-1.606],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[145.125,76],[157.5,85.25],[171.125,76.5],[169.5,68.25]],"c":true}]},{"t":150.000006109625,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-8.289,-1.382],[-2.125,-2.625],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.75,1.125],[0.957,1.182],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[146.375,71.5],[158.375,65.625],[169.875,72.5],[169.5,68.25]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[0.65986520052,0.609302341938,0.562351107597,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[1,0.939583837986,0.883195459843,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":139,"s":[0]},{"t":144.00000586524,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"sw":320,"sh":180,"sc":"#d4beb0","ip":130.000005295009,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":161,"s":[100]},{"t":188.000007657397,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[134.5,59.25,0],"ix":2},"a":{"a":0,"k":[-14,-11,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,18.987]},"t":161,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":188,"s":[90,90,100]},{"t":297.000012097058,"s":[90,96.41,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":4,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":6,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":15.1,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964307598039,0.948043763404,0.948043763404,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-14,-10.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":58.0000023623884,"op":958.00003902014,"st":58.0000023623884,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":144,"s":[100]},{"t":173.000007046434,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[136,76.5,0],"ix":2},"a":{"a":0,"k":[-14,-11,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,18.987]},"t":144,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":173,"s":[90,90,100]},{"t":297.000012097058,"s":[90,96.41,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":4,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":6,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":15.1,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964307598039,0.948043763404,0.948043763404,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-14,-10.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41.0000016699642,"op":941.000038327716,"st":41.0000016699642,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"Channel.eps","cl":"eps","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[218.75,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[218.75,47.75,0]}],"ix":2},"a":{"a":0,"k":[10,10,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[5.523,0],[0,-5.523],[-5.523,0],[0,5.523]],"o":[[-5.523,0],[0,5.523],[5.523,0],[0,-5.523]],"v":[[10,0],[0,10],[10,20],[20,10]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"Text Ad.eps","cl":"eps","refId":"image_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[118.75,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[118.75,43.75,0]}],"ix":2},"a":{"a":0,"k":[21,3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[1.657,0],[0,0],[0,-1.657],[-1.657,0],[0,0],[0,1.657]],"o":[[0,0],[-1.657,0],[0,1.657],[0,0],[1.657,0],[0,-1.657]],"v":[[39,0],[3,0],[0,3],[3,6],[39,6],[42,3]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"Ad.eps","cl":"eps","refId":"image_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[132,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[132,95.875,0]}],"ix":2},"a":{"a":0,"k":[33.5,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[4.694,0],[0,0],[0,-4.694],[-4.694,0],[0,0],[0,4.694]],"o":[[0,0],[-4.694,0],[0,4.694],[0,0],[4.694,0],[0,-4.694]],"v":[[58.5,0],[8.5,0],[0,8.5],[8.5,17],[58.5,17],[67,8.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"Description","refId":"image_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[163.167,143,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":59,"s":[162.667,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.667,204,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[163.167,143,0]}],"ix":2},"a":{"a":0,"k":[70.5,24.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0.046,0],[0,0],[0,-0.035],[0,0],[-4.418,0],[0,0],[0,4.119],[0,0]],"o":[[0,0],[-0.035,0],[0,0],[0,4.418],[0,0],[4.119,0],[0,0],[0,-0.046]],"v":[[141.208,-0.5],[-0.104,-0.5],[-0.167,-0.438],[-0.167,41.216],[7.833,49.216],[133.833,49.216],[141.292,41.757],[141.292,-0.416]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":2,"nm":"Top Bar.eps","cl":"eps","refId":"image_4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[99]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[3]},{"t":358.000014581639,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":47,"s":[163.042,24.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[163.042,24.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,9,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[2.401,0],[0,0],[0,-2.209],[0,-2.557],[0,0],[0,2.402]],"o":[[0,0],[-2.209,0],[0,2.209],[0,0],[0,-3.466],[0,-2.402]],"v":[[136.039,0.5],[4.396,0.5],[0.396,4.5],[0.41,17.432],[140.311,17.432],[140.387,4.848]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":2,"nm":"Line 9.pdf","cl":"pdf","parent":12,"refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[6.107,80.125,0],"ix":2},"a":{"a":0,"k":[0.25,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":346,"s":[161,100,100]},{"t":360.000014663101,"s":[100,100,100]}],"ix":6}},"ao":0,"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[1,0.800000071526,0.200000017881,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[0.93412989378,0.700597524643,0,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.951],"y":[0.71]},"t":47,"s":[100]},{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":346,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":2,"nm":"Line 10.pdf","cl":"pdf","parent":12,"refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.838,80.125,0],"ix":2},"a":{"a":0,"k":[101.076,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":346,"s":[83,100,100]},{"t":360.000014663101,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":2,"nm":"Duck PLayer.eps","cl":"eps","refId":"image_7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[162.917,75.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":61,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":83,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":348,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[162.917,75.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,43.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[0.987,0.987,1]},"o":{"x":[0.055,0.055,0.333],"y":[0.238,0.238,0]},"t":69,"s":[100,100,100]},{"i":{"x":[0.348,0.348,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":83,"s":[167.816,167.816,100]},{"i":{"x":[0.858,0.858,0.667],"y":[1.002,1.002,1]},"o":{"x":[0.527,0.527,0.167],"y":[0.276,0.276,0]},"t":348,"s":[167.816,167.816,100]},{"t":358.000014581639,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[-0.074,0],[0,0],[0,-0.029],[0,0],[0.006,0],[0,0],[0,-0.007],[0,0]],"o":[[0,0],[-0.055,0],[0,0],[0,-0.006],[0,0],[-0.007,0],[0,0],[0,0.074]],"v":[[140.892,0.447],[0.333,0.447],[0.235,0.5],[0.235,85.625],[0.223,85.614],[140.771,85.614],[140.758,85.627],[140.758,0.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[1.105,0],[0,0],[0,-1.105],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-1.105]],"v":[[138.758,0.447],[2.235,0.447],[0.235,2.447],[0.235,83.614],[2.235,85.614],[138.758,85.614],[140.758,83.614],[140.758,2.447]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":348,"s":[{"i":[[1.105,0],[0,0],[0,-1.105],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-1.105]],"v":[[138.758,0.447],[2.235,0.447],[0.235,2.447],[0.235,83.614],[2.235,85.614],[138.758,85.614],[140.758,83.614],[140.758,2.447]],"c":true}]},{"t":358.000014581639,"s":[{"i":[[-0.074,0],[0,0],[0,-0.029],[0,0],[0.006,0],[0,0],[0,-0.007],[0,0]],"o":[[0,0],[-0.055,0],[0,0],[0,-0.006],[0,0],[-0.007,0],[0,0],[0,0.074]],"v":[[140.892,0.447],[0.333,0.447],[0.235,0.5],[0.235,85.625],[0.223,85.614],[140.771,85.614],[140.758,85.627],[140.758,0.312]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":1,"nm":"Dark Gray Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[7]},{"i":{"x":[0],"y":[1.014]},"o":{"x":[0.077],"y":[0.601]},"t":69,"s":[6]},{"i":{"x":[0],"y":[9.742]},"o":{"x":[0.167],"y":[0]},"t":83,"s":[39]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":348,"s":[40]},{"t":358.000014581639,"s":[7]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.125,87.75,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[105,108.889,100],"ix":6}},"ao":0,"sw":320,"sh":180,"sc":"#2d2d2d","ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":2,"nm":"Rectangle.png","cl":"png","refId":"image_8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.022]},"o":{"x":[0.027],"y":[0.608]},"t":69,"s":[0]},{"i":{"x":[0],"y":[43.204]},"o":{"x":[0.167],"y":[0]},"t":83,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":348,"s":[100]},{"t":358.000014581639,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[132.5,-168.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[132.5,-105.5,0]}],"ix":2},"a":{"a":0,"k":[202.5,354,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":55.0000022401959,"op":955.000038897947,"st":55.0000022401959,"bm":0}],"markers":[{"tm":142.192505791619,"cm":"1","dr":0},{"tm":147.000005987433,"cm":"2","dr":0},{"tm":156.00000635401,"cm":"3","dr":0}]} \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-bg/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-bg/strings-duckplayer.xml new file mode 100644 index 000000000000..18d6960a88c0 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-bg/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Запази + Отмени + Duck Player + Duck Player + С Duck Player на DuckDuckGo можете да гледате YouTube без насочени реклами и вече гледаните видеоклипове няма да повлияят на препоръчаните.\nНаучете повече + Винаги + Никога + Питай всеки път + Отваряне на видеоклипове от YouTube в Duck Player? + Затваряне + Писна ли ви от рекламите в YouTube? Не и с Duck Player! + Писна ли Ви от рекламите в YouTube? Опитайте Duck Player! + С Duck Player на DuckDuckGo можете да гледате YouTube без насочени реклами и вече гледаните видеоклипове няма да повлияят на препоръчаните. + Разбрах! + Duck Player не е достъпен + Функционалността на Duck Player е засегната от последните промени в YouTube. Работим за отстраняване на тези проблеми и оценяваме вашето разбиране. + Научете повече + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-cs/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-cs/strings-duckplayer.xml new file mode 100644 index 000000000000..e19a895bb402 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-cs/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Uložit + Zrušit + Duck Player + Duck Player + Duck Player umožňuje dívat se na YouTube v prohlížeči DuckDuckGo bez cílených reklam. To, co sleduješ, nebude ovlivňovat tvoje doporučení.\nDalší informace + Vždy + Nikdy + Vždycky se ptát + Otevírat videa na YouTube v přehrávači Duck Player? + Zavřít + Topíš se v reklamách na YouTube? S přehrávačem Duck Player nebudeš! + Topíš se v reklamách na YouTube? Vyzkoušej přehrávač Duck Player! + Duck Player umožňuje dívat se na YouTube v prohlížeči DuckDuckGo bez cílených reklam. To, co sleduješ, nebude ovlivňovat tvoje doporučení. + Mám to! + Duck Player není dostupný + Funkčnost přehrávače Duck Player ovlivnily nedávné změny na YouTube. Problémy se snažíme opravit. Děkujeme za pochopení. + Více informací + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-da/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-da/strings-duckplayer.xml new file mode 100644 index 000000000000..7cf8ec1f79d4 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-da/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Gem + Annuller + Duck Player + Duck Player + Duck Player giver dig mulighed for at se YouTube uden målrettede annoncer i DuckDuckGo, og det, du ser, påvirker ikke dine anbefalinger.Få mere at vide + Altid + Aldrig + Spørg hver gang + Åbn YouTube-videoer i Duck Player? + Luk + Drukner du i annoncer på YouTube? Ikke med Duck Player! + Drukner du i annoncer på YouTube? Prøv Duck Player! + Duck Player giver dig mulighed for at se YouTube uden målrettede annoncer i DuckDuckGo, og det, du ser, påvirker ikke dine anbefalinger. + Forstået + Duck Player ikke tilgængelig + Duck Players funktionalitet er påvirket af de seneste ændringer på YouTube. Vi arbejder på at løse disse problemer og sætter pris på din forståelse. + Mere info + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-de/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-de/strings-duckplayer.xml new file mode 100644 index 000000000000..0ec129aaa019 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-de/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Speichern + Abbrechen + Duck Player + Duck Player + Mit dem Duck Player kannst du YouTube ohne gezielte Werbung in DuckDuckGo ansehen und was du dir ansiehst, hat keinen Einfluss auf deine Empfehlungen.\nMehr erfahren + Immer + Nie + Jedes Mal fragen + YouTube-Videos in Duck Player öffnen? + Schließen + Ertrinkst du in Werbung auf YouTube? Nicht mit Duck Player! + Ertrinkst du in Werbung auf YouTube? Teste Duck Player! + Mit dem Duck Player kannst du YouTube ohne gezielte Werbung in DuckDuckGo ansehen und was du dir ansiehst, hat keinen Einfluss auf deine Empfehlungen. + Verstanden. + Duck Player nicht verfügbar + Die Funktionalität des Duck Players wird durch die jüngsten Änderungen bei YouTube beeinträchtigt. Vielen Dank für dein Verständnis, während wir daran arbeiten, diese Probleme zu beheben. + Mehr erfahren + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-el/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-el/strings-duckplayer.xml new file mode 100644 index 000000000000..8c6ece74aeaf --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-el/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Αποθήκευση + Ακύρωση + Duck Player + Duck Player + Το Duck Player σάς επιτρέπει να παρακολουθείτε YouTube χωρίς στοχευμένες διαφημίσεις στο DuckDuckGo, ενώ το περιεχόμενο που παρακολουθείτε δεν θα επηρεάσει τις συστάσεις που θα λαμβάνετε.Μάθετε περισσότερα + Πάντα + Ποτέ + Να γίνεται ερώτηση κάθε φορά + Άνοιγμα βίντεο του YouTube στο Duck Player; + Κλείσιμο + Πνίγεστε από τις διαφημίσεις στο YouTube; Όχι με το Duck Player! + Πνίγεστε από τις διαφημίσεις στο YouTube; Δοκιμάστε το Duck Player! + Το Duck Player σάς επιτρέπει να παρακολουθείτε YouTube χωρίς στοχευμένες διαφημίσεις στο DuckDuckGo, ενώ το περιεχόμενο που παρακολουθείτε δεν θα επηρεάσει τις συστάσεις που θα λαμβάνετε. + Το κατάλαβα! + Το Duck Player δεν είναι διαθέσιμο + Η λειτουργία του Duck Player έχει επηρεαστεί από τις πρόσφατες αλλαγές στο YouTube. Προσπαθούμε να διορθώσουμε αυτά τα προβλήματα και εκτιμούμε την κατανόησή σας. + Μάθετε περισσότερα + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-es/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-es/strings-duckplayer.xml new file mode 100644 index 000000000000..6b6adfe694be --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-es/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Guardar + Cancelar + Duck Player + Duck Player + Duck Player te permite ver YouTube sin anuncios segmentados en DuckDuckGo y lo que veas no influirá en tus recomendaciones.Más información + Siempre + Nunca + Preguntar cada vez + ¿Abrir vídeos de Youtube en Duck Player? + Cerrar + ¿Te ahogas en anuncios en YouTube? ¡No con Duck Player! + ¿Te ahogas en anuncios en YouTube? ¡Prueba Duck Player! + Duck Player te permite ver YouTube sin anuncios segmentados en DuckDuckGo y lo que veas no influirá en tus recomendaciones. + Entendido + Duck Player no disponible + La funcionalidad de Duck Player se ha visto afectada por los cambios recientes en YouTube. Estamos trabajando para solucionar estos problemas y agradecemos tu comprensión. + Más información + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml new file mode 100644 index 000000000000..dfaecc078718 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-et/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Salvesta + Loobu + Duck Player + Duck Player + Duck Player võimaldab sul DuckDuckGo-s YouTube\'i vaadata ilma sihitud reklaamideta ja see, mida vaatad, ei mõjuta sinu soovitusi.\nLisateave + Alati + Mitte kunagi + Küsi iga kord + Kas avada Youtube\'i videod Duck Playeris? + Sulge + Kas näed Youtube\'i kasutades massiliselt reklaame? Mitte Duck Playeriga! + Kas näed Youtube\'i kasutades massiliselt reklaame? Proovi Duck Playerit! + Duck Player võimaldab sul DuckDuckGo-s YouTube\'i vaadata ilma sihitud reklaamideta ja see, mida vaatad, ei mõjuta sinu soovitusi. + Sain aru! + Duck Player ei ole saadaval + Duck Playeri funktsionaalsust on mõjutanud hiljutised YouTube\'i muudatused. Töötame nende probleemide lahendamise nimel ja täname sind mõistva suhtumise eest. + Loe edasi + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-fi/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-fi/strings-duckplayer.xml new file mode 100644 index 000000000000..8955e2204a77 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-fi/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Tallenna + Peruuta + Duck Player + Duck Player + Duck Playerin avulla voit katsella YouTubea ilman kohdennettuja mainoksia DuckDuckGossa, eivätkä katsomasi videot vaikuta suosituksiisi.Lue lisää + Aina + Ei koskaan + Kysy joka kerta + Avataanko Youtube-videot Duck Playerissa? + Sulje + Hukutko mainoksiin YouTubessa? Duck Playerin avulla et huku! + Hukutko mainoksiin YouTubessa? Kokeile Duck Playeria! + Duck Playerin avulla voit katsella YouTubea ilman kohdennettuja mainoksia DuckDuckGossa, eivätkä katsomasi videot vaikuta suosituksiisi. + Selvä! + Duck Player ei ole käytettävissä + YouTuben viimeaikaiset muutokset ovat vaikuttaneet Duck Playerin toimintaan. Pyrimme korjaamaan nämä ongelmat. Kiitos ymmärryksestä. + Lue lisää + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-fr/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-fr/strings-duckplayer.xml new file mode 100644 index 000000000000..4a2360fbe2af --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-fr/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Enregistrer + Annuler + Duck Player + Duck Player + Duck Player vous permet de regarder YouTube sans publicités ciblées dans DuckDuckGo. De plus, ce que vous regardez n\'influence pas vos recommandations.\nEn savoir plus + Toujours + Jamais + Toujours demander + Ouvrir les vidéos YouTube dans Duck Player ? + Fermer + YouTube vous inonde de publicités ? Pas avec Duck Player ! + YouTube vous inonde de publicités ? Essayez Duck Player ! + Duck Player vous permet de regarder YouTube sans publicités ciblées dans DuckDuckGo. De plus, ce que vous regardez n\'influence pas vos recommandations. + J\'ai compris ! + Duck Player n\'est pas disponible + Les récents changements apportés à YouTube ont affecté la fonctionnalité de Duck Player. Nous nous efforçons de résoudre ces problèmes et vous remercions de votre compréhension. + En savoir plus + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-hr/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-hr/strings-duckplayer.xml new file mode 100644 index 000000000000..b368316cfc88 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-hr/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Spremi + Odustani + Duck Player + Duck Player + Duck Player ti omogućuje gledanje YouTubea bez ciljanih oglasa u DuckDuckGou, a ono što gledaš neće utjecati na tvoje preporuke.Saznaj više + Uvijek + Nikada + Pitaj svaki puta + Otvaraj YouTube videozapise u Duck Playeru? + Zatvori + Utapaš se u oglasima na YouTubeu? Ne s Duck Playerom! + Utapaš se u oglasima na YouTubeu? Isprobaj Duck Player! + Duck Player ti omogućuje gledanje YouTubea bez ciljanih oglasa u DuckDuckGou, a ono što gledaš neće utjecati na tvoje preporuke. + Shvaćam! + Duck Player je nedostupan + Na funkcionalnost Duck Playera utjecale su nedavne promjene na YouTubeu. Radimo na rješavanju ovih problema i cijenimo tvoje razumijevanje. + Saznajte više + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-hu/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-hu/strings-duckplayer.xml new file mode 100644 index 000000000000..cf534271aa8b --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-hu/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Mentés + Mégsem + Duck Player + Duck Player + A Duck Player segítségével a DuckDuckGóban célzott hirdetések nélkül nézheted a YouTube-ot, és a megtekintett tartalmak nem befolyásolják a neked szóló ajánlásokat.\nTovábbi tudnivalók + Mindig + Soha + Kérdezzen rá minden alkalommal + YouTube-videók megnyitása a Duck Playerben? + Bezárás + Elárasztanak a YouTube-hirdetések? A Duck Player használatakor nem! + Elárasztanak a YouTube-hirdetések? Próbáld ki a Duck Player! + A Duck Player segítségével a DuckDuckGóban célzott hirdetések nélkül nézheted a YouTube-ot, és a megtekintett tartalmak nem befolyásolják a neked szóló ajánlásokat. + Megvan! + A Duck Player nem érhető el + A YouTube legutóbbi módosításai érintik a Duck Player működését. Dolgozunk a problémák megoldásán, és köszönjük a megértésedet. + További részletek + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-it/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-it/strings-duckplayer.xml new file mode 100644 index 000000000000..b1dd072b3f2a --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-it/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Salva + Annulla + Duck Player + Duck Player + Duck Player ti consente di guardare YouTube senza annunci mirati in DuckDuckGo e ciò che guardi non inciderà sui consigli che ricevi.\nUlteriori informazioni + Sempre + Mai + Chiedi ogni volta + Aprire i video di YouTube in Duck Player? + Chiudi + YouTube ti inonda di annunci? Non con Duck Player! + YouTube ti inonda di annunci? Prova Duck Player! + Duck Player ti consente di guardare YouTube senza annunci mirati in DuckDuckGo e ciò che guardi non inciderà sui consigli che ricevi. + Ho capito! + Duck Player non disponibile + La funzionalità di Duck Player è stata influenzata dalle recenti modifiche a YouTube. Stiamo lavorando per risolvere questi problemi. Ti ringraziamo per la comprensione. + Ulteriori informazioni + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-lt/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-lt/strings-duckplayer.xml new file mode 100644 index 000000000000..c5907c3ccb8d --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-lt/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Išsaugoti + Atšaukti + Duck Player + Duck Player + „Duck Player“ leidžia sistemoje „DuckDuckGo“ žiūrėti „YouTube“ be taikomų reklamų, o tai, ką žiūrite, neturės įtakos jūsų rekomendacijoms.Sužinokite daugiau + Visada + Niekada + Klausti kiekvieną kartą + Ar atidaryti „YouTube“ vaizdo įrašus „Duck Player“? + Uždaryti + Matote per daug „YouTube“ skelbimų? Ne su „Duck Player“! + Matote per daug „YouTube“ skelbimų? Išbandykite „Duck Player“! + „Duck Player“ leidžia sistemoje „DuckDuckGo“ žiūrėti „YouTube“ be taikomų reklamų, o tai, ką žiūrite, neturės įtakos jūsų rekomendacijoms. + Supratau! + „Duck Player“ nepasiekiamas + Neseniai atlikti „YouTube“ pakeitimai turėjo įtakos „Duck Player“ funkcijoms. Stengiamės pašalinti šias triktis ir dėkojame už supratingumą. + Sužinoti daugiau + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-lv/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-lv/strings-duckplayer.xml new file mode 100644 index 000000000000..b9a8b6c72c72 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-lv/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Saglabāt + Atcelt + Duck Player + Duck Player + Duck Player ļauj skatīties YouTube bez mērķētām reklāmām DuckDuckGo vidē, un skatītais saturs neietekmē tev sniegtos ieteikumus.\nUzzināt vairāk + Vienmēr + Nekad + Jautāt katru reizi + Atvērt YouTube video ar Duck Player? + Aizvērt + Vai tevi nomāc YouTube reklāmas? Ne ar Duck Player! + Vai tevi nomāc YouTube reklāmas? Izmēģini Duck Player! + Duck Player ļauj skatīties YouTube bez mērķētām reklāmām DuckDuckGo vidē, un skatītais saturs neietekmē tev sniegtos ieteikumus. + Sapratu! + Duck Player nav pieejams + Nesen ieviestās YouTube izmaiņas ir ietekmējušas Duck Player funkcionalitāti. Mēs strādājam, lai novērstu šīs problēmas, un novērtējam tavu sapratni. + Uzzināt vairāk + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-nb/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-nb/strings-duckplayer.xml new file mode 100644 index 000000000000..71d5f17366ff --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-nb/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Lagre + Avbryt + Duck Player + Duck Player + Med Duck Player kan du se på YouTube uten målrettede annonser i DuckDuckGo. Det du ser på, påvirker ikke anbefalingene du får.\nLes mer + Alltid + Aldri + Spør hver gang + Vil du åpne YouTube-videoer i Duck Player? + Lukk + Drukner du i annonser på YouTube? Ikke med Duck Player! + Drukner du i annonser på YouTube? Prøv Duck Player! + Med Duck Player kan du se på YouTube uten målrettede annonser i DuckDuckGo. Det du ser på, påvirker ikke anbefalingene du får. + Skjønner! + Duck Player er utilgjengelig + Duck Players funksjonalitet har blitt påvirket av nylige endringer i YouTube. Vi jobber med å løse disse problemene og setter pris på forståelsen din. + Finn ut mer + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-nl/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-nl/strings-duckplayer.xml new file mode 100644 index 000000000000..91b0679534ca --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-nl/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Opslaan + Annuleren + Duck Player + Duck Player + Met Duck Player kun je YouTube bekijken in DuckDuckGo, zonder gerichte advertenties. Wat je bekijkt heeft geen invloed op je aanbevelingen.\nMeer informatie + Altijd + Nooit + Elke keer vragen + YouTube-video\'s openen in Duck Player? + Sluiten + Te veel advertenties op YouTube? Niet met Duck Player! + Te veel advertenties op YouTube? Probeer Duck Player! + Met Duck Player kun je YouTube bekijken in DuckDuckGo, zonder gerichte advertenties. Wat je bekijkt heeft geen invloed op je aanbevelingen. + Ik snap het! + Duck Player is niet beschikbaar + Recente wijzigingen in YouTube hebben invloed op de functionaliteit van Duck Player. We werken aan een oplossing voor deze problemen. Bedankt voor je begrip. + Meer informatie + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-pl/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-pl/strings-duckplayer.xml new file mode 100644 index 000000000000..0830424d93f8 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-pl/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Zapisz + Anuluj + Duck Player + Duck Player + Duck Player umożliwia oglądanie YouTube bez ukierunkowanych reklam w DuckDuckGo i wpływu oglądanych treści na rekomendacje.\nDowiedz się więcej + Zawsze + Nigdy + Pytaj za każdym razem + Otwierać filmy z YouTube w odtwarzaczu Duck Player? + Zamknij + Za dużo reklam na YouTube? Nie z odtwarzaczem Duck Player! + Za dużo reklam na YouTube? Wypróbuj Duck Player! + Duck Player umożliwia oglądanie YouTube bez ukierunkowanych reklam w DuckDuckGo i wpływu oglądanych treści na rekomendacje. + Rozumiem! + Duck Player jest niedostępny + Ostatnie zmiany w serwisie YouTube miały wpływ na działanie Duck Player. Pracujemy nad rozwiązaniem tych problemów i dziękujemy za zrozumienie. + Dowiedz się więcej + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-pt/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-pt/strings-duckplayer.xml new file mode 100644 index 000000000000..dcc9eec6c708 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-pt/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Guardar + Cancelar + Duck Player + Duck Player + O Duck Player permite-te ver o YouTube sem anúncios segmentados no DuckDuckGo, e o que vês não vai influenciar as tuas recomendações.\nSabe mais + Sempre + Nunca + Pergunte todas as vezes + Abrir vídeos do YouTube no Duck Player? + Fechar + Já não suportas ver anúncios no YouTube? Experimenta o Duck Player! + Já não suportas ver anúncios no YouTube? Experimenta o Duck Player! + O Duck Player permite-te ver o YouTube sem anúncios segmentados no DuckDuckGo, e o que vês não vai influenciar as tuas recomendações. + Entendi! + Duck Player indisponível + A funcionalidade do Duck Player foi afetada por alterações recentes ao YouTube. Estamos a trabalhar para resolver estes problemas e agradecemos a tua compreensão. + Saiba mais + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-ro/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-ro/strings-duckplayer.xml new file mode 100644 index 000000000000..226867e8407a --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-ro/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Salvează + Anulare + Duck Player + Duck Player + Duck Player îți permite să vizionezi YouTube fără reclame țintite în DuckDuckGo, iar ceea ce vizionezi nu îți va influența recomandările.\nAflă mai multe + Întotdeauna + Niciodată + Întreabă de fiecare dată + Deschizi videoclipurile YouTube în Duck Player? + Închidere + Te copleșesc reclamele pe YouTube? Nu și cu Duck Player! + Te copleșesc reclamele pe YouTube? Încearcă Duck Player! + Duck Player îți permite să vizionezi YouTube fără reclame țintite în DuckDuckGo, iar ceea ce vizionezi nu îți va influența recomandările. + Am înțeles! + Duck Player indisponibil + Funcționalitatea Duck Player a fost afectată de modificările recente ale YouTube. Lucrăm pentru a remedia aceste probleme și apreciem înțelegerea ta. + Află mai multe + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-ru/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-ru/strings-duckplayer.xml new file mode 100644 index 000000000000..3e6ee5590eda --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-ru/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Сохранить + Отменить + Проигрыватель Duck Player + Проигрыватель Duck Player + Проигрыватель Duck Player позволяет смотреть видео из YouTube в браузере DuckDuckGo без целевой рекламы. Просмотренные ролики не влияют на рекомендации.\nПодробнее... + Всегда + Никогда + Спрашивать каждый раз + Открывать видео из YouTube в Duck Player? + Закрыть + Шквал рекламы на YouTube? Только не с Duck Player! + Шквал рекламы на YouTube? Попробуйте Duck Player! + Проигрыватель Duck Player позволяет смотреть видео из YouTube в браузере DuckDuckGo без целевой рекламы. Просмотренные ролики не влияют на рекомендации. + Понятно + Duck Player недоступен + Недавние изменения YouTube приводят к некорректной работе проигрывателя Duck Player. Мы занимаемся решением этой проблемы и благодарим вас за понимание. + Узнать больше + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-sk/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-sk/strings-duckplayer.xml new file mode 100644 index 000000000000..18aea4c5c26c --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-sk/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Uložiť + Zrušiť + Duck Player + Duck Player + Duck Player vám umožňuje sledovať YouTube v DuckDuckGo bez cielených reklám a to, čo sledujete, nebude mať vplyv na vaše odporúčania.\nZistiť viac + Vždy + Nikdy + Vždy sa opýtať + Otvárať videá z YouTube s Duck Player? + Zatvoriť + Utápať sa v reklamách na YouTube? Nie s Duck Player! + Utápať sa v reklamách na YouTube? Vyskúšajte Duck Player! + Prehrávač Duck Player vám umožňuje sledovať YouTube v prehliadači DuckDuckGo bez cielených reklám a to, čo sledujete, nebude mať vplyv na vaše odporúčania. + Rozumiem! + Duck Player nie je dostupný + Funkčnosť prehrávača Duck Player bola ovplyvnená nedávnymi zmenami v službe YouTube. Na odstránení týchto problémov pracujeme. Ďakujeme za pochopenie. + Zistite viac + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-sl/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-sl/strings-duckplayer.xml new file mode 100644 index 000000000000..1126e66c525b --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-sl/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Shrani + Prekliči + Duck Player + Duck Player + Predvajalnik Duck Player vam s pomočjo DuckDuckGo omogoča gledanje YouTuba brez ciljanih oglasov, kar gledate pa ne bo vplivalo na vaša priporočila.\nVeč o tem + Vedno + Nikoli + Vprašaj vsakič + Želite videoposnetke v YouTubu odpreti s predvajalnikom Duck Player? + Zapri + Se utapljate v oglasih na YouTubu? S predvajalnikom Duck Player bo to preteklost! + Se utapljate v oglasih na YouTubu? Preizkusite Duck Player! + Predvajalnik Duck Player vam s pomočjo DuckDuckGo omogoča gledanje YouTuba brez ciljanih oglasov, kar gledate pa ne bo vplivalo na vaša priporočila. + Razumem! + Predvajalnik Duck Player ni na voljo + Nedavne spremembe v YouTubu so vplivale na delovanje predvajalnika Duck Player. Prizadevamo si za odpravljanje teh težav in cenimo vaše razumevanje. + Več + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-sv/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-sv/strings-duckplayer.xml new file mode 100644 index 000000000000..485f2956e6b6 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-sv/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Spara + Avbryt + Duck Player + Duck Player + Med Duck Player kan du titta på YouTube utan riktade annonser i DuckDuckGo, och dina rekommendationer påverkas inte av vad du tittar på.Läs mer + Alltid + Aldrig + Fråga varje gång + Öppna Youtube-videor i Duck Player? + Stäng + Drunknar du i annonser på YouTube? Inte med Duck Player! + Drunknar du i annonser på YouTube? Prova Duck Player! + Med Duck Player kan du titta på YouTube utan riktade annonser i DuckDuckGo, och dina rekommendationer påverkas inte av vad du tittar på. + Jag förstår! + Duck Player är inte tillgänglig + De senaste ändringarna på YouTube har påverkat funktionaliteten hos Duck Player. Vi arbetar för att åtgärda problemen och tackar för din förståelse. + Läs mer + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values-tr/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values-tr/strings-duckplayer.xml new file mode 100644 index 000000000000..9d514a0c411c --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values-tr/strings-duckplayer.xml @@ -0,0 +1,37 @@ + + + + + + Kaydet + Vazgeç + Duck Player + Duck Player + Duck Player, DuckDuckGo\'da hedefli reklamlar olmadan YouTube\'u izlemenizi sağlar ve izlediğiniz şeyler önerilerinizi etkilemez.\nDaha Fazla Bilgi + Her zaman + Hiçbir zaman + Her seferinde sor + YouTube videoları Duck Player\'da açılsın mı? + Kapat + YouTube\'da reklama mı boğuluyorsunuz? Duck Player ile bundan kurtulun! + YouTube\'da reklama mı boğuluyorsunuz? Duck Player\'ı deneyin! + Duck Player, DuckDuckGo\'da hedefli reklamlar olmadan YouTube\'u izlemenizi sağlar ve izlediğiniz şeyler önerilerinizi etkilemez. + Anladım! + Duck Player Kullanılamıyor + Duck Player\'ın işlevselliği YouTube\'da yapılan son değişikliklerden etkilendi. Bu sorunları çözmeye çalışıyoruz. Anlayışınız için teşekkür ederiz. + Daha Fazla Bilgi + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values/strings-duckplayer.xml b/duckplayer/duckplayer-impl/src/main/res/values/strings-duckplayer.xml new file mode 100644 index 000000000000..f2b63bfa437e --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values/strings-duckplayer.xml @@ -0,0 +1,36 @@ + + + + + Save + Cancel + Duck Player + Duck Player + Duck Player lets you watch YouTube without targeted ads in DuckDuckGo and what you watch won’t influence your recommendations.\nLearn More + Always + Never + Ask Every Time + Open Youtube videos in Duck Player? + Close + Drowning in ads on YouTube? Not with Duck Player! + Drowning in ads on YouTube? Try Duck Player! + Duck Player lets you watch YouTube without targeted ads in DuckDuckGo and what you watch won’t influence your recommendations. + Got it! + Duck Player Unavailable + Duck Player\'s functionality has been affected by recent changes to YouTube. We’re working to fix these issues and appreciate your understanding. + Learn More + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/main/res/values/styles.xml b/duckplayer/duckplayer-impl/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d2e7a947b4ab --- /dev/null +++ b/duckplayer/duckplayer-impl/src/main/res/values/styles.xml @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt b/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt new file mode 100644 index 000000000000..92fcfeb67247 --- /dev/null +++ b/duckplayer/duckplayer-impl/src/test/kotlin/com/duckduckgo/duckplayer/impl/RealDuckPlayerTest.kt @@ -0,0 +1,771 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.duckplayer.impl + +import android.content.Context +import android.content.res.AssetManager +import android.net.Uri +import android.webkit.MimeTypeMap +import android.webkit.WebResourceRequest +import android.webkit.WebView +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY +import com.duckduckgo.common.utils.UrlScheme.Companion.duck +import com.duckduckgo.common.utils.UrlScheme.Companion.https +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK +import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED +import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled +import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_DAILY_UNIQUE_VIEW +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_OTHER +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY +import com.duckduckgo.duckplayer.impl.DuckPlayerPixelName.DUCK_PLAYER_WATCH_ON_YOUTUBE +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.State +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.never +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class RealDuckPlayerTest { + + @get:org.junit.Rule + var coroutineRule = com.duckduckgo.common.test.CoroutineTestRule() + + private val mockDuckPlayerFeatureRepository: DuckPlayerFeatureRepository = + mock() + private val mockDuckPlayerFeature: DuckPlayerFeature = mock() + private val mockPixel: Pixel = mock() + private val mockDuckPlayerLocalFilesPath: DuckPlayerLocalFilesPath = mock() + private val mimeType: MimeTypeMap = mock() + private val dispatcherProvider = coroutineRule.testDispatcherProvider + + private val testee = RealDuckPlayer( + mockDuckPlayerFeatureRepository, + mockDuckPlayerFeature, + mockPixel, + mockDuckPlayerLocalFilesPath, + mimeType, + dispatcherProvider, + ) + + @Before + fun setup() = runTest { + mockFeatureToggle(true) + whenever(mockDuckPlayerFeatureRepository.getDuckPlayerDisabledHelpPageLink()) + .thenReturn(null) + whenever(mockDuckPlayerFeatureRepository.getVideoIDQueryParam()).thenReturn("v") + whenever(mockDuckPlayerFeatureRepository.getYouTubeWatchPath()).thenReturn("watch") + whenever(mockDuckPlayerFeatureRepository.getYouTubeUrl()).thenReturn("youtube.com") + whenever(mockDuckPlayerFeatureRepository.getYouTubeEmbedUrl()).thenReturn("youtube-nocookie.com") + whenever(mockDuckPlayerFeatureRepository.getYouTubeReferrerHeaders()).thenReturn(listOf("Referer")) + whenever(mockDuckPlayerFeatureRepository.getYouTubeReferrerQueryParams()).thenReturn(listOf("embeds_referring_euri")) + } + + // region getDuckPlayerState + + @Test + fun whenDuckPlayerStateIsEnabled_getDuckPlayerStateReturnsEnabled() = runTest { + mockFeatureToggle(true) + + val result = testee.getDuckPlayerState() + + assertEquals(ENABLED, result) + } + + @Test + fun whenDuckPlayerStateIsDisabled_getDuckPlayerStateReturnsDisabled() = runTest { + mockFeatureToggle(false) + + val result = testee.getDuckPlayerState() + + assertEquals(DISABLED, result) + } + + @Test + fun whenDuckPlayerStateIsDisabledWithHelpLink_getDuckPlayerStateReturnsDisabledWithHelpLink() = runTest { + mockFeatureToggle(false) + whenever(mockDuckPlayerFeatureRepository.getDuckPlayerDisabledHelpPageLink()).thenReturn("help_link") + + val result = testee.getDuckPlayerState() + + assertEquals(DISABLED_WIH_HELP_LINK, result) + } + + // endregion + + // region setUserPreferences + @Test + fun whenOverlayInteracted_setUserPreferencesUpdatesOverlayInteracted() = runTest { + testee.setUserPreferences(true, "disabled") + + verify(mockDuckPlayerFeatureRepository).setUserPreferences(UserPreferences(true, Disabled)) + } + + @Test + fun whenPrivatePlayerModeEnabled_setUserPreferencesUpdatesPrivatePlayerMode() = runTest { + testee.setUserPreferences(false, "enabled") + + verify(mockDuckPlayerFeatureRepository).setUserPreferences(UserPreferences(false, Enabled)) + } + + @Test + fun whenPrivatePlayerModeAlwaysAsk_setUserPreferencesUpdatesPrivatePlayerMode() = runTest { + testee.setUserPreferences(false, "always_ask") + + verify(mockDuckPlayerFeatureRepository).setUserPreferences(UserPreferences(false, AlwaysAsk)) + } + + //endregion + + //region getUserPreferences + + @Test + fun whenGetUserPreferencesCalled_returnsUserPreferencesFromRepository() = runTest { + val expectedUserPreferences = UserPreferences(true, Enabled) + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(expectedUserPreferences) + + val actualUserPreferences = testee.getUserPreferences() + + assertEquals(expectedUserPreferences, actualUserPreferences) + } + + @Test + fun whenGetUserPreferencesCalled_returnsUserPreferencesWithOverlayInteracted() = runTest { + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Disabled)) + + val result = testee.getUserPreferences() + + assertTrue(result.overlayInteracted) + } + + @Test + fun whenGetUserPreferencesCalled_returnsUserPreferencesWithPrivatePlayerMode() = runTest { + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(false, AlwaysAsk)) + + val result = testee.getUserPreferences() + + assertEquals(AlwaysAsk, result.privatePlayerMode) + } + + // endregion + + // region shouldHideDuckPlayerOverlay + + @Test + fun whenOverlayHidden_shouldHideDuckPlayerOverlayReturnsFalse() { + testee.duckPlayerOverlayHidden() + + assertFalse(testee.shouldHideDuckPlayerOverlay()) + } + + @Test + fun whenOverlayNotHidden_shouldHideDuckPlayerOverlayReturnsFalse() { + assertFalse(testee.shouldHideDuckPlayerOverlay()) + } + + // endregion + + // region observeUserPreferences + @Test + fun observeUserPreferences_emitsUserPreferencesFromRepository() = runTest { + val expectedUserPreferences = UserPreferences(true, Enabled) + whenever(mockDuckPlayerFeatureRepository.observeUserPreferences()).thenReturn(flowOf(expectedUserPreferences)) + + val result = testee.observeUserPreferences().first() + + assertEquals(expectedUserPreferences, result) + } + + @Test + fun observeUserPreferences_emitsMultipleUserPreferencesFromRepository() = runTest { + val userPreferences1 = UserPreferences(true, Enabled) + val userPreferences2 = UserPreferences(false, Disabled) + whenever(mockDuckPlayerFeatureRepository.observeUserPreferences()).thenReturn(flowOf(userPreferences1, userPreferences2)) + + val results = testee.observeUserPreferences().take(2).toList() + + assertEquals(userPreferences1, results[0]) + assertEquals(userPreferences2, results[1]) + } + + // endregion + + // region sendDuckPlayerPixel + + @Test + fun sendDuckPlayerPixelWithOverlay_firesPixelWithCorrectNameAndData() = runTest { + val pixelName = "overlay" + val pixelData = mapOf("key" to "value") + + testee.sendDuckPlayerPixel(pixelName, pixelData) + + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS, pixelData, emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithOverlay_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { + val pixelName = "overlay" + + testee.sendDuckPlayerPixel(pixelName, emptyMap()) + + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_IMPRESSIONS, emptyMap(), emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayUse_firesPixelWithCorrectNameAndData() = runTest { + val pixelName = "play.use" + val pixelData = mapOf("key" to "value") + + testee.sendDuckPlayerPixel(pixelName, pixelData) + + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY, pixelData, emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayUse_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { + val pixelName = "play.use" + + testee.sendDuckPlayerPixel(pixelName, emptyMap()) + + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_MAIN_OVERLAY, emptyMap(), emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayDoNotUse_firesPixelWithCorrectNameAndData() = runTest { + val pixelName = "play.do_not_use" + val pixelData = mapOf("key" to "value") + + testee.sendDuckPlayerPixel(pixelName, pixelData) + + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE, pixelData, emptyMap(), COUNT) + } + + @Test + fun sendDuckPlayerPixelWithPlayDoNotUse_firesPixelWithEmptyDataWhenNoDataProvided() = runTest { + val pixelName = "play.do_not_use" + + testee.sendDuckPlayerPixel(pixelName, emptyMap()) + + verify(mockPixel).fire(DUCK_PLAYER_OVERLAY_YOUTUBE_WATCH_HERE, emptyMap(), emptyMap(), COUNT) + } + + // endregion + + // region createYouTubeWatchFromDuckPlayer + + @Test + fun createYoutubeWatchUrlFromDuckPlayer_returnsCorrectUrl_whenVideoIdQueryParamExists() = runTest { + val youTubeWatchPath = "watch" + val youTubeHost = "youtube.com" + val videoID = "12345" + val uri = Uri.parse("$duck://player/$videoID") + + val result = testee.createYoutubeWatchUrlFromDuckPlayer(uri) + + assertEquals("$https://$youTubeHost/$youTubeWatchPath?v=$videoID", result) + } + + @Test + fun createYoutubeWatchUrlFromDuckPlayer_returnsNull_whenNoVideoIdFound() = runTest { + val youTubeWatchPath = "watch" + val youTubeHost = "youtube.com" + val uri = Uri.parse("$https://$youTubeHost/$youTubeWatchPath") + + val result = testee.createYoutubeWatchUrlFromDuckPlayer(uri) + + assertNull(result) + } + + //endregion + + // region isDuckPlayerUrl + + @Test + fun whenUriIsDuckPlayerUri_isDuckPlayerUriReturnsTrue() = runTest { + val uri = "duck://player/12345" + + val result = testee.isDuckPlayerUri(uri) + + assertTrue(result) + } + + @Test + fun whenUriIsNotDuckPlayerUri_isDuckPlayerUriReturnsFalse() = runTest { + val uri = "https://youtube.com/watch?v=12345" + + val result = testee.isDuckPlayerUri(uri) + + assertFalse(result) + } + + @Test + fun whenUriIsEmpty_isDuckPlayerUriReturnsFalse() = runTest { + val uri = "" + + val result = testee.isDuckPlayerUri(uri) + + assertFalse(result) + } + + // endregion + + // region isSimulatedYoutubeNoCookie + + @Test + fun whenUriHostYouTube_isSimulatedYoutubeNoCookieReturnsTrue() = runTest { + val uri = "https://www.youtube.com".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + @Test + fun whenUriHostIsEmbedUrlAndPathContainsValidPath_isSimulatedYoutubeNoCookieReturnsTrue() = runTest { + val uri = "https://www.youtube-nocookie.com/?videoID=1234".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertTrue(result) + } + + @Test + fun whenUrHostIsEmbedAndFileIsAvailableLocally_isSimulatedYoutubeNoCookieReturnsTrue() = runTest { + whenever(mockDuckPlayerLocalFilesPath.assetsPath).thenReturn(listOf("js/duckplayer.js")) + val uri = "https://www.youtube-nocookie.com/js/duckplayer.js".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertTrue(result) + } + + @Test + fun whenUrHostIsEmbedAndFileIsNotAvailableLocally_isSimulatedYoutubeNoCookieReturnsFalse() = runTest { + whenever(mockDuckPlayerLocalFilesPath.assetsPath).thenReturn(listOf("css/duckplayer.css")) + val uri = "https://www.youtube-nocookie.com/js/duckplayer.js".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + @Test + fun whenUrHostIsEmbedAndPathContainsEmbed_isSimulatedYoutubeNoCookieReturnsFalse() = runTest { + whenever(mockDuckPlayerLocalFilesPath.assetsPath).thenReturn(listOf()) + val uri = "https://www.youtube-nocookie.com/embed/js/duckplayer.js".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + @Test + fun whenUriHostIsNotEmbedUrl_isSimulatedYoutubeNoCookieReturnsFalse() = runTest { + val uri = "https://www.notyoutube.com".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + @Test + fun whenUriHostIsEmbedUrlAndPathContainsEmbed_isSimulatedYoutubeNoCookieReturnsFalse() = runTest { + val uri = "https://www.youtube.com/embed/12345".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + @Test + fun whenUriHostIsEmbedUrlAndPathDoesNotContainValidPath_isSimulatedYoutubeNoCookieReturnsFalse() = runTest { + val uri = "https://www.youtube.com/invalidPath".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + @Test + fun whenUriHostIsEmbedUrlAndPathDoesNotHaveVideoIdQueryParam_isSimulatedYoutubeNoCookieReturnsFalse() = runTest { + val uri = "https://www.youtube.com/watch".toUri() + + val result = testee.isSimulatedYoutubeNoCookie(uri) + + assertFalse(result) + } + + // endregion + + // region isYouTubeWatchUrl + + @Test + fun whenUriHostIsYoutubeAndPathIsWatch_isYoutubeWatchUrlReturnsTrue() = runTest { + val uri = "https://www.youtube.com/watch?v=12345".toUri() + + val result = testee.isYoutubeWatchUrl(uri) + + assertTrue(result) + } + + @Test + fun whenUriHostIsMobileYoutubeAndPathIsWatch_isYoutubeWatchUrlReturnsTrue() = runTest { + val uri = "https://m.youtube.com/watch?v=12345".toUri() + + val result = testee.isYoutubeWatchUrl(uri) + + assertTrue(result) + } + + @Test + fun whenUriHostIsNotYoutube_isYoutubeWatchUrlReturnsFalse() = runTest { + val uri = "https://www.notyoutube.com/watch?v=12345".toUri() + + val result = testee.isYoutubeWatchUrl(uri) + + assertFalse(result) + } + + @Test + fun whenUriPathIsNotWatch_isYoutubeWatchUrlReturnsFalse() = runTest { + val uri = "https://www.youtube.com/notwatch?v=12345".toUri() + + val result = testee.isYoutubeWatchUrl(uri) + + assertFalse(result) + } + + // endregion + + // region createDuckPlayerUriFromYoutubeNoCookie + + @Test + fun whenUriHasVideoIdQueryParam_createDuckPlayerUriFromYoutubeNoCookieReturnsCorrectUri() = runTest { + val uri = "https://www.youtube-nocookie.com/?videoID=12345".toUri() + + val result = testee.createDuckPlayerUriFromYoutubeNoCookie(uri) + + assertEquals("$duck://player/12345", result) + } + + @Test + fun whenUriDoesNotHaveVideoIdQueryParam_createDuckPlayerUriFromYoutubeNoCookieReturnsNull() = runTest { + val uri = "https://www.youtube-nocookie.com/".toUri() + + val result = testee.createDuckPlayerUriFromYoutubeNoCookie(uri) + + assertNull(result) + } + + @Test + fun whenUriIsEmpty_createDuckPlayerUriFromYoutubeNoCookieReturnsNull() = runTest { + val uri = "".toUri() + + val result = testee.createDuckPlayerUriFromYoutubeNoCookie(uri) + + assertNull(result) + } + + // endregion + + // region intercept + + @Test + fun whenDuckPlayerStateIsNotEnabled_interceptReturnsNull() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = mock() + val webView: WebView = mock() + + mockFeatureToggle(false) + + val result = testee.intercept(request, url, webView) + + assertNull(result) + } + + @Test + fun whenUrlDoesNotMatchAnyCondition_interceptReturnsNull() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.notmatching.com") + val webView: WebView = mock() + + mockFeatureToggle(true) + + val result = testee.intercept(request, url, webView) + + assertNull(result) + } + + @Test + fun whenUriIsDuckPlayerUri_interceptProcessesDuckPlayerUri() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("duck://player/12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("https://www.youtube-nocookie.com?videoID=12345") + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_OTHER) + assertNotNull(result) + } + + @Test + fun whenUriIsDuckPlayerUriWithOpenInYouTube_interceptLoadsYouTubeUri() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("duck://player/openInYouTube?v=12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("https://youtube.com/watch?v=12345") + verify(mockPixel).fire(DUCK_PLAYER_WATCH_ON_YOUTUBE) + assertNotNull(result) + } + + @Test + fun whenUriIsDuckPlayerUriAndFeatureIsDisabled_InterceptLoadsYouTubeUri() = runTest { + mockFeatureToggle(false) + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("duck://player/12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("https://youtube.com/watch?v=12345") + verify(mockPixel, never()).fire(DUCK_PLAYER_WATCH_ON_YOUTUBE) + assertNotNull(result) + } + + @Test + fun whenUriIsDuckPlayerUriAndUserSettingsNever_InterceptLoadsYouTubeUri() = runTest { + mockFeatureToggle(true) + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("duck://player/12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Disabled)) + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("https://youtube.com/watch?v=12345") + verify(mockPixel, never()).fire(DUCK_PLAYER_WATCH_ON_YOUTUBE) + assertNotNull(result) + } + + @Test + fun whenUriIsYouTubeEmbed_interceptLoadsLocalFile() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube-nocookie.com?videoID=12345") + val webView: WebView = mock() + val context: Context = mock() + val assets: AssetManager = mock() + whenever(webView.context).thenReturn(context) + whenever(context.assets).thenReturn(assets) + whenever(assets.open(any())).thenReturn(mock()) + + val result = testee.intercept(request, url, webView) + + verify(assets).open("duckplayer/index.html") + verify(mockPixel).fire(DUCK_PLAYER_DAILY_UNIQUE_VIEW, type = DAILY) + assertEquals("text/html", result?.mimeType) + } + + @Test + fun whenUriIsYouTubeEmbedAndFeatureDisabled_doNothing() = runTest { + mockFeatureToggle(false) + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube-nocookie.com?videoID=12345") + val webView: WebView = mock() + + val result = testee.intercept(request, url, webView) + + assertNull(result) + } + + @Test + fun whenUriIsYoutubeWatchUrl_interceptProcessesYoutubeWatchUri() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube.com/watch?v=12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("duck://player/12345?origin=auto") + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC) + assertNotNull(result) + } + + @Test + fun whenUriIsYoutubeWatchUrlAndPreviousUrlIsDuckPlayer_interceptReturnsNull() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube.com/watch?v=12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + whenever(webView.url).thenReturn("https://www.youtube-nocookie.com?videoID=12345") + + val result = testee.intercept(request, url, webView) + + assertNull(result) + } + + @Test + fun whenUriIsYoutubeWatchUrlWithRefererAndPreviousUrlIsDuckPlayer_interceptReturnsNull() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube.com/watch?v=12345") + whenever(request.requestHeaders).thenReturn(mapOf("Referer" to "https://www.youtube-nocookie.com?videoID=12345")) + whenever(mockDuckPlayerFeatureRepository.getYouTubeReferrerHeaders()).thenReturn(listOf("Referer")) + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + whenever(webView.url).thenReturn("https://www.youtube-nocookie.com?videoID=12345") + + val result = testee.intercept(request, url, webView) + + assertNull(result) + } + + @Test + fun whenUriIsYoutubeWatchUrlAndPreviousUrlIsDuckPlayerWithDifferentId_interceptOpensDuckPlayer() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube.com/watch?v=123456") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + whenever(webView.url).thenReturn("https://www.youtube-nocookie.com?videoID=12345") + + val result = testee.intercept(request, url, webView) + + verify(webView).loadUrl("duck://player/123456?origin=auto") + verify(mockPixel).fire(DUCK_PLAYER_VIEW_FROM_YOUTUBE_AUTOMATIC) + assertNotNull(result) + } + + @Test + fun whenUriIsYoutubeWatchUrlAndSettingsAlwaysAsk_interceptProcessesYoutubeWatchUri() = runTest { + val request: WebResourceRequest = mock() + val url: Uri = Uri.parse("https://www.youtube.com/watch?v=12345") + val webView: WebView = mock() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, AlwaysAsk)) + + val result = testee.intercept(request, url, webView) + + verify(webView, never()).loadUrl(any()) + assertNull(result) + } + + // endregion + + // region willNavigateToDuckPlayer + + @Test + fun whenWillNavigateToDuckPlayerWithYouTubeWatchUrlSettingEnabledAndFeatureEnabled_returnTrue() = runTest { + val uri = "https://www.youtube.com/watch?v=12345".toUri() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.willNavigateToDuckPlayer(uri) + + assertTrue(result) + } + + @Test + fun whenWillNavigateToDuckPlayerWithYouTubeWatchUrlSettingDisabledAndFeatureEnabled_returnFalse() = runTest { + val uri = "https://www.youtube.com/watch?v=12345".toUri() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Disabled)) + + val result = testee.willNavigateToDuckPlayer(uri) + + assertFalse(result) + } + + @Test + fun whenWillNavigateToDuckPlayerWithYouTubeRootUrlSettingEnableddAndFeatureEnabled_returnFalse() = runTest { + val uri = "https://www.youtube.com".toUri() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.willNavigateToDuckPlayer(uri) + + assertFalse(result) + } + + @Test + fun whenWillNavigateToDuckPlayerWithNonYouTubeUrlSettingEnabledAndFeatureEnabled_returnFalse() = runTest { + val uri = "https://example.com".toUri() + whenever(mockDuckPlayerFeatureRepository.getUserPreferences()).thenReturn(UserPreferences(true, Enabled)) + + val result = testee.willNavigateToDuckPlayer(uri) + + assertFalse(result) + } + + // endregion + + private fun mockFeatureToggle(enabled: Boolean) { + whenever(mockDuckPlayerFeature.self()).thenReturn( + object : Toggle { + override fun isEnabled() = enabled + + override fun setEnabled(state: State) { + TODO("Not yet implemented") + } + + override fun getRawStoredState(): State? { + TODO("Not yet implemented") + } + }, + ) + + whenever(mockDuckPlayerFeature.enableDuckPlayer()).thenReturn( + object : Toggle { + override fun isEnabled() = enabled + + override fun setEnabled(state: State) { + TODO("Not yet implemented") + } + + override fun getRawStoredState(): State? { + TODO("Not yet implemented") + } + }, + ) + } +} diff --git a/duckplayer/readme.md b/duckplayer/readme.md new file mode 100644 index 000000000000..2225b8802a25 --- /dev/null +++ b/duckplayer/readme.md @@ -0,0 +1,9 @@ +# Feature Name + +Describe the feature here. + +## Who can help you better understand this feature? +- Cris Barreiro + +## More information +- [Duck Player project](https://app.asana.com/0/1203249713006009/1203249713006009) \ No newline at end of file diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/cog.svg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/cog.svg new file mode 100644 index 000000000000..c62b1b8e15c1 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/cog.svg @@ -0,0 +1,3 @@ + + + diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/dax.svg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/dax.svg new file mode 100644 index 000000000000..faa1f060ef7e --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/dax.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/eyeball.svg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/eyeball.svg new file mode 100644 index 000000000000..da9bd6ee74d5 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/eyeball.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/info-icon.svg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/info-icon.svg new file mode 100644 index 000000000000..dee6a9b256f0 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/info-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/open.svg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/open.svg new file mode 100644 index 000000000000..d5943118d7ae --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/player-bg.png b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/img/player-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..83a4b8c3394ef665fcdaa6a84a0688f7d79c417c GIT binary patch literal 205117 zcmd41cT|(x7X?TW!G;A<5fB9h1W_T>glYi`y(C10pp?*&7CHfWiiIK~ASDDrrG=Kz zyNynePUwPkLLd-&k_qqke9!k_X4b4VYu5ZlR_=GtJ!kKI&OPS~&vmsm4(&g^pNWa- z(Cu3{^qH9U$TKmq*l_G+d=sp#ca8Cf*-c;L8dDCQZ<>keEYs~9YKC5BbCoZ#Kjm{Q z6O7;i$*oknN@a(Pu$Ah#EA#pEXT7BYS06U%*cLl8hbj}x zkwB(BXPN%~Samh1`iOkhqgg%oT~-k+9DJu(et-V2t*|5}mbzHyYhwRc`o|~>yX-Ds z*Tjkh)+~-EyxPChi?e|>L5EaF`1ce!b5SYbf86H!ub)y3PkPz@zw+cCn6@-o`1aEP z{iu7_nOKHn7*Q8H-V@vVH-5jOUQ^g}L_R><#K2$&j5Wm<7=b$`#Jo`Z$L?Pk1aLC% z2}gHycOGXsvX-(Zi6BU8#=(xC{Py%8me$^Fo3{C4;JDMYry#>L>RwdYPD}$t8KwpL z77Tg+i{;q`6vx>|eretBFaJ*sK?2%=Zt#E-W?lb3k63HlKGA*2zCdmHHy@ z?Q){@_KAaAhG%x3sI+}z1mgXP|LE^5Q;>ibQ_z@(oJw@aKQaF$Qoq9t+J{s5Gj=jR zqQRv)vewq3Gk)%u*lK1u7UG6_^3PxR(`{ph-tzIGBEKm5P55h;i~yCP!Mc3^1QND8Mi7)MaGFKBj+>cjc_A7nCQMe;a571k8|$` znJFldiG@9ed52U2|L-&5e@6{z1s>5B&`Wn4t&E_ffMQCo=6(zQC}n zL%4K&{|>uMLEMZO#S2PK?y{tFo1r5E*#l1=@6eH|`4dL;`z7X|>|7c#IK~jto0eGO%y$7JS40FCBO#F~Jo4lFnUJ9eKI!(8ELOF8^V5 zKrnF1?Evp#e|`qDu(WK^i#y_EVqs(6!>P&oFVlYp9dt#9fnJl`IqqFBU(YogR;hmKI|L?f1)F}F7Pc95e zwPD$LF)Ke~gvlNx%)5&&hcpG1$i>M8!x#ZDoHds zsQq%CfzI2*HK8w7%B+MGF4xmSk&F#H!jr1Ld<^F7-y5F^e89zyOJZ+*qiQU|wTYy8y~{dQ5*7%*%neXjfR zJKt-9d%T`gZ=QbfkO77#qdE*+sB`=Mfxk%dJH0K4j~7Kb7=j3Dy&D)CfG&0v{kzgI zYML|S3FF7HVK7tN!_xH@N8d!E&TV-@gURPlHI~#S?5!USnpUD#YZ;Aiu)JP?RhM}g| z&fW5vi4mm$Q{1JUksR^5!VthJ`GUtT;!qCUE?la@y9ww2?eKz}?~I^fjMj#tHM_!{ z^`vHz+ayftZ{q$T_YtGK1gZU2RDOM27~j6$)`j2i1eKYAn>p)l`txQW1FPDX@tdIk z{>T`H|NgwaH{cR;74xD+Leh)tBfQ)F?9;G2(PtNIIYt2f@gI}V{%(J5oI?*Rjq ze{c;t!R)?%LfC1$bzZRDcDC>1JM-Un{l%+0W6k}xi@**|GO?hyr{gro)BFED9s32a zI5=%=Io{ZZ+|S?>-QM20W4b@P5Vn2hSnUTpMp}7Zs7XlqRnTgB1Z(`33;SNSUmMxn z9{FgFnum4@X?XjEM6vTjv9-uOUJ;9t1r7hHhBeiUsPp!6sE*uW`mi_a-!iQ_wOwku z&5!RkS1`&eCx(TEbhc*yvt9aN#-mGVzS)99_G{ z!jA1mOC2Ns=R#zZND{<== zn!oYmr4S@=`O(6Iv{;kl`~K5*F&?8a=9^r-#{ccV< zUVeRNziDTd7ZH@>tS^l2_Q=`t9IzhkC9f{ylo!l7HQf2-N_Z@GeYgR7=E-(4_$Hf1M zQcc!2p7Em7zhgFHy61bv(9Ib4b4)?bstir>N(&nOI^v&9-EwDetTM=wuJVsNe`Vna zFC+2R_J-Z!-pRiTiV>GiG1JV>A#rW*cFL&<>9g&is77b|bz6r-;Kt6Bt72xL==}Ye z;SUk6HJ{-Zo3pvFYT7zuwjB}mEaH!b6sTMY z%1t}qXBHFu+CwDfl6ku-t}%t{l4nxOV|tr&99ODZ65SUZ+k7sA?}OgeQnB;RGw-wq zM9&I5H#BYg5mz+VS#A<;^zN+18^YtIraAlW1~}f!Dmc|$V*u0){(7je7+Qr>UUE1q zd8{bY)wZfA+VC`+}FUy+R*nfLSjMNgr5K=d_pkWEC{(aE+fW_>6orOWo1O(C_+eD4aCF zq@yOyzF3I2VGg`R?8S?&rIQhp_cpz{>Yh^lO&TbkRWIy?h20gbnbqLU4Sn9V9>@LM zmzrx^;<7IzD9bCJ6r=!siawhyV^7b7g+C6|X`?5Ii_WhdwDWAHyCtO3(LDYy*6E9a zF*M}L(LT3nBy9;@%d+S_Il{TEsaE;iAE8E{$f90v+5EC3013KSrmF|4vNok|RtO!l ze5__v+!)kChGvj32j@kN*24lKfEjP4#{gwRw)E!bE3WSCJZ#)J`_$zua@bG~<~@W` z9i5D#(Mc=inWo?ks{)wFT77NJy!=x12Aw|us`8et#giowQwb!t^FWkS=CI%!jFhjF%%f5Z zA!vffoToM9-ilr+mL>}Vz^y?-PN{GZC5aL@b3eMPXu61cX%Gr(@d^d!W0vVo{?@BT z{sDz~`>A5SbJTK=d(KJX{1pi)uNps^}I&wZVk7uczO7B`i_J9Xz>WCf`9e&W- zYPOe_?o9u>Ufm%(xQ^&n#<>(4#Juf+P?3AWdHi!2se6%`p9!+}OEi;>Y$Sx)yeRA| zdpq7dgSi7Ukczctf_v~fK51YvM5jX@|Izpr?qN24T1Z3C*pdj%_qEO?XBx=Ff6ek~ z2@vRNa!l=yF@@`RYbaRv`(tTbL`vEQ-67%5xT4WGf0ziaN|i1Uu@}G#2Hod@xQDK| zhSObmghQNE1p6q>BC-y9;+EOvFe;4|vE-FV!ibeWP_C$6_zhBzjq ztZi!&X`!R&^ar`Drdxq;wJK&DTKPKSOqDHsjTQ<%yBrV9zEoN{CTcr0n4B}4?p+D= zPj_p@HJ5vb_PV#93iA}&JX7cm_t5MT0a=;l_qQ1X>{H{itIFxQoqz+Etw*QrHU_KY zPW!?&v!wVD8?z)BeQSd>U8G%FhM?uF)44He4rgEplQ{9R`_iOn1zdd36ur_&1{a`& zd(Lh0mSB^HE=75PR0ioSwdDb~d+@^ySSAajs-M+J0<^ZN5@JCGWyk;QdTzE4GY>oz z36u4{5f8{u0gT`u2XnT@b$K*K>X0jSIKqTGyY~4`IJi5c+Y7{l#}6W!#0R`89Zn`o!&a6+%i*fOxn+oo|{+)MF&ijLei*snhLHp9Mw`A8aEg^ z@oL^UicL*?9ltS%2}jA2@|tJbw0sMz(wJ^M&M}%mNx^Ncm;B%d=YRH4?$YMAhV2Qt zeRR@toSvzNrZug)509g&_!Sf{Gh}B5pS;W%ZRCO?JB|b!!BcYiP;nieY5Y>`);~K# z`4*t6G7qo1&0AafAfy(p>Tz3TLG>63K>75?L6`4YKJNUn&@<80=C65;7x9UC(M9!h z=0E+cWC_z6D8GWs;_J;06XB_4$K(~8e60aE$wy#%^m3LD+#glVIb9?lY3&J(-RJ`zlfr0pU0VO5v+eNHMl@eyXd*_uF(*muI2Bcsa~*EY8A+ zf`oazrqJR1G|nh(di88ccq#?j&p6_z@O-b&BP zVva4dK1H^m+-u3F58@g`xj2D%?-So!5(*Z%^fPFbPZ z?nhe~DZI4@VV8f@NW&+Xhw7r zA7(RDD`MuVeQS=zs^-F)CcTyK8{D|Xt0XL^yWC!fzpm&s9q8S>fs~_bn~78D{1!gk zn(loY+SCdroTP+}UQi zxWxbuAESV-L}&|LFE^S73mY!i5`5IG88mw&F!0zEaDxEg4=D$EtBiFH9U_y{ASKbYx8v zRc3W6%pPDuoRG}Ym`L_ui3Y8EZuW>0q5e{!X=MO5A7qvJIJt}LZtU4Uw;&K^fKr@q zeu+|0689}uOs(qN_ZYZE>P(;wU)Uu156Y78^sJ?^7bMvky13iW{trjd@J+f6Cuz;n zNag;6Hb=Tsc@>rpo6y=5&fPI?#o+t=FXv2(O|?%&DNAFcqW!E9N*09t$oW$qY1*C}P2xk#h*@b~x8Z5y z!;`P;$W$mD9gXZfX+5UDGM7mNQ;{pv6JMNO;tppnT3eEErEVXb1`E{Pnovav;|YXL zjM-onIg(1}Oo-HS&sC^~EYmL|$2Zd^=$RPq5W=ZNkqLSK}`BK`~o1JJUINo7a-C{d|(X_r{-JOx4QC<=f|(43m7eFiZFT&x%ex%}*r@Bv8eqH{5U zFrd_wHYSwQ*NPEmV&koL|s-2XMN!W@BVNCK8N?j8t3 z)d0t*2F6&cfMmT8d_q3{eEy70juZxeu|aq$$F1*TFvuu%1b{1UqdL_^8;oc_u+}m4 zox(QE6^|`=@HJdsZM5|-M@?r<>Ge-P7ik&#ay5=YE*wYBde9m<1ptc@4mipEnPcjcKNF@*~Y-v`^m` z&N5Rc>W@cK7Q465eDMldbu*MqKHi9kd3u(Gn%u1bPdb|N1e2Jm@FBshE2L}*8N1q? z)=;|v8nY`_BzR4Fm{9=0Drm7EX2h8PNr?c9N??tHxqyNzsv&3ACR;uf=hsy_&G|}5 z!*)eoJYm2(yvN4%E!#`ami7)}0Ts6e{G>Ku;No|ar-IKY-jh576!LV$EZwhzzGaio z@<3C*L@N(wSjbg2=0Ik9@&_|>!{_WLwuXsxK!Tae1}U$s<7w*pgX6CTcvxZ3v^WVL zvsB4ZZByVFG_4AyFw&_mR~;`odl!X8C}cds-m7*x<1&n*srYiyXK19QSsT8$1d13M zTJ9;Ivj#yeS{i-?A=aRibZwLDm56Y(WI_nIsVfe0L>_2Z%Fdz^swyKtj55UTvz{2p zmfzj8n+KxrQbVd&;jsO%TTJo2qizc>icqM3Vo> zay{5|%q;iHpkqYNAc8d8ou4pNKA#ZnhVYl3$bPe)M%lgz>Du(%t(UC>^ztGZ3+aSn zdgm6pCD^n)JqeXR^-;y*qaznRY2Di{SY-H(gxH|<4`mLIUdvR*DA|(XX zUY?OmX8TT-L3cRX$JBNlqR?n+EpC;?w}1%)an(1zc5*z>lKUmgO-JNbqNI_s=Tb$- znaK(oHF+()B0AMQzL~htl`}TYc~dw!)WN!xezK_r%4%Ur8`e(ui zpN!2ou44cLj!}`>L)opOq=-2>suT{cgVJ0394&jqDUkkjaz^d46MAv7eCfNL5>WXw zMq)nv@HRmy%bz+`KzCt@D0ptF5B%2J-^Ay-<1)025Lbb2 zADc>>KB1oJ#fhC6tN#w*mv_4-e0 zzK5=BP%Tf?WBTsMgnKmgR$dWbwb6dQ3G#ze-K}+$W2|L)#w?5`#KPcxseBf~;_YT# z!oXGhj1uo*Mz0RnB(2c9Ce;z`{NiwLd`W9kHODe*f2qA3Ze;fxHaDi;$7-Zajnz$-pexlEKqx9BZfxWt|`Mvn$iHC{W>x= zSFM)5MJ3@jHk%_G6Du6@a_9RSw~+MBVSn8f=6>W|_OFZ|K3@3vazzLWRYFQ_2fsY% z4Bi8ZDrHRBwmcwlr%YEvE|>Q_#Hn$t?%$TQ(I;WI|Zi??-Sq29%1^Lv?hbtuq~O3_b4lCQ5~6)Q>2bLZyZysOq0 zdNKi9ugNVA9_6cO7=0pIQfqd>D@&GF1=wL&r$!vWy@tqs^kh9>xK3D;!L|_!XUqxF zlJ+qgX_^b)Y}11 z42r)XP%ogvjSe3&t9yhFxE3uJ&>dGx(npv}(#EI|l4sC4mhSYBy@UPE0ZL;NRs%`T z>G!`r-?%6wQC*CN^(S?l1nV_O2X@XJ59f-mDf-Go);ZiiW#ddT919 zR5NI!uI8GaYH~0a$DdJDeyo~}4nGi!?W11)dK35{#P^=bk+n6Q;Clc2c{TdP)MK70 zrhv99jlP}G_EurDT+d!igax#8c_zBCFIgLz8AGy4on3z++9> z?ud=*qUfTa{^|%fx-88Z)ax+vmP^(b$V5Hi;!#!Kcdlfr?>bdr)s?S)sbH&X zr2pbl!3pCgO+9gLg=Kh?~0*^JmMx_R(u$(fW|SoQET$Y-sB+?X!n<)of6YXq+F z`Ac}3`Fa~7ZRHL2;w*lRRP=s4)X&ry*sjH5OIn4ZTfs_R^woSdben5Ni*M;Q%wqJ# z@)ifJjWxr}z85bq0b_0BDs*OzM<35z?Dgr*m{f>AiR2xw76(*>;S@d*VUSlhd^D)p z{5S>IiANuooE9$}yh36$%FR~FtM2p_so@6u$x9DyPw1L$eEr#c%CX!I-w@E+XAZ8z z4^(~rMu_N>6;FszK>@?&8ybf)A|gyuReL&2yjKVt-`1qbY+Yb$5436S2DtbPL4Gxp zHY{=d8^&bS9pj5xFg^N4tUQk3AOpd4BFBf|C721ERL&rOIdH47VC-PH_5>~{!vIR; z>!PMI-vVJ&V22F(Rqqvt2Q`QK7~Res zyNa;n8=xhrwyA#NjWtzj#{XWnpnucknL=V}*!j0z0de`RdY|jc(w(0@;!kJ>3zZnZ zn@+qDQpxl!Gc<%lKa8!*_#TDsU^YOkc_KsTn|S;xC$GNNOF-j#bHhe;AL)X?UBz-U zh2=O{4B-x#2C6O%E78hcJ3t?V*i&h_OZUeuTDC%)=L>VQ_jD1qq9oJFezok)4nSf@ z*isU<2)+JD(k%TdQBj?x=D`=)ryfbr{H|~bE22C$I5Nz-2U|3=5u93v@FQUOA!|x* z&au?NfU7f2cIdup^fcWQ>aNf{XBR`iqilpmOA=S2!&~4MUa|oK;__yW&44c^4Oi~k zzoFTrGz!0mLN%g#>-U+&Eo9#Iw0_&=;d!>;(%?pTd(wwV$7EZpa6kBCVv%KL`zEBSzu1NIRN{zqj2<(+0=trs~byXCS)gGxz-d6 z>g8=C28`#3TVet1MdlDl`V9e>qZ#)u=Es_TWz2j!#JFdGIGds#B?SYu&DT9Yn7jm|yQG|sF&Ca?(@nidoCwm(5}`O(zT$0)Q$=E$ua zKL<8-MC+{*vdg&Z{B2>&hVFF7t|#G=>>Zdx2lQMzIUk_Qs1s6E$NN87yWK=k!-JD`pZOrE1sLW@~6zZ=CWk0;od})DLpdpKdR9UQS+VI{EEl*|YL+7r!Q?qzr zXkW4F)GwC{Lo8|cGYq1gYj016LV7=^;{iFI-X8048Zr$pB(Tv{wUBkHLJS2dB#5G* zXK=HFWr&XHfu7RIpZ&I5Z~Sk5kkS^o4+(RT4h$Dg#lO$ARbhR<$4YIo@oSlWk^{@q z?GpQ%={>ROXaGIc%yy{Ex>c3h0LS%RQX+Y-(o0zf5(e_;1L&{6NxkW zTbhXrXFstf)@$5+JcTn-64HXXl7ez#Oj-ldhrk>1()YBEUJH!rFLrDe9*XQu^;c)5 zYSy^SJ4N&l6EcH4);Su;a}F|E05C*0q^q_aycy2RIiUyLI!e1NX}p zdz=hJ5{~X&lvQSFYqrJ)WTiaKJ=oW8_?Vn3kv-pa=N$2_$mXp*k1LaIA3-ilfL;Nz znm*8t{QSzF(gy2A{t&gw9((NEl)h2jZ!QzyWfIfMZE`|;@@sueRHaf29wgW=*?l79 zRni}A@j;`Csm=efSS1%tslqUMJNDesIP4X&HqZE8g9*?l`X;}hnQq|;H?~iv~ zw2w~uIE|l}(&`Q!PM;^JPI7(1XnP(kk9TO?XYIOX-;)fQh6fL&hoiJ5ElQ+DFr6OV zt=1kMo9S~>0iL`o#oziVUcQyoe%7$&#f3nX{xiCct-AfWj}=>S$M!vr-pju6v?ti? zP-s@Z-Ce-gJIbQbLHbfp(IjmZS@t>=m@4!Z4)e>HNfAGnSj|o_Yqb*un{@ijCfJ>P z`=+$-PSNKJypx?l2$N;G2_MeedusKpPc{`)mwm@6q*px|b=Pt2emoK7ZIQd4FKP2@k5_SpVq??M<9(K$E1fu|Gia-RU&HibYn7RC=k`@v&Fo zccHx2W3IJ7ztY>PrqzJitbn)e8_!0Hr8bz*rp z1q^9ovZa+dPW%frzx>0X!)ckm!dLm+9_%q|IV;9ba)^dp2rQSdbeyih6?d2lT*8Gp z;KNpyBt0hT46`naox8r&R+KsM>Zk@cEfZ0_w3&gA0_bBEY?e*BJO|Nby%S*)N;9+k z)Q(zl&r#)(FFKN)MezLeet!rthz{`zHQ2iI&djg{=?89%AZCj~h+m@q-Kc z-kh9~Ix`rnaJuU}p}|=;5-TJ9JVeE}EZpbrvGv7VgE%N8D3Gwsi|m=)^Y zSn)2`fn3x)9A5Ax4$CY{UMU`+`R1Y6ApQPlIE+jT?|Xe8xxdr^es2>v{rL-cMJ#!R z`z&FG`{cfgTz3({LiWNh&BFdSZ~Z*#k=6KKHh0?kQsu$E+SuS@AKHO>0@K21x;yXl zEo`9NfwfW0(>CU4J&c1@`=Vb(zR{%H=rgc|<2Z*kyTQCwRPlNmuum;U=#xlcbnl%- z;|s6YzQvu254#Bz&TzCpOO)-Krfb|w?Gi%R%w9YzTC~J>UjX(-VV{uJVjXKlvuuQK zX0{qx!Crv%PFl8C?Pm6Vo#;TK|HosK66glq&XL)_-=s zuJmfK&T2SIuwEscvy7ZgQ_74g4oQv9&|i(dw- z`xQ=zOIte{f8&D2LkS@9XX?cJ(In~1Q;BWR(uZsH6mG@I|aVnt#;VoY!b*!kY|M`nkMZ_Q4 z^j5r(%h6OI?tFIE^2@w$b4>cChh9@=&EGtK303kRSY9lfp_ki0Fe|s&WO+6Ze>$gt zn@HchUNQBoN+aN#_nEZTQRTCupln>kO=WAu1i(2qYH;Jk$MerRm1iA4E;&+24i55R ztVM5p-&hxfsN@pgNa_ou0YX~0Z27APVHsPoFM@Lm+BUHT;z)@l3BL-ZB5LoCfeh1z zpigm!{umxbZ{*y-c0daEHm$C1i1c%--v{_+I-JkCyIS0cob5G~i^#kj?F^cUGAb`c z%ZvA?JGUmGC`e@0oNrjR4tHu7O(oO7sH*Mli5}~5lNJb3lISTH@yK}Ep72)DzGE4X z&!30F|ClZ@khBZUJgUK^2_|s9(W^6lAPIO3wJ=mv9PD~7AvfWuMunvr)Ztd*J$l>B z<#|Iv4&on4Z<(ye8|FNho73mS5d%EE@Sx#O)uL)GYc zFl^0^tJr9G<|uVQWQItxyQ}d{EaBOhnbvF` zZH=lwhn6?`l)e#PFNrw5`2_A3=H#Z!{F$EZrY$SnQd>;q;TCYeRD$8i{~{-@rb@JI z8tz!GQMx;Me2c_Sudag!S3a?B4A{RS6_83ZB$$WTmQ`lC z^g5=64T{XZsoSJqI*~qM!(Rtt>A?u#=g#p$q4OOH`sa?B**i?7eU(z5T51-KOH``@%t^j>8UDA-*0z>JH-#)xK8a zM@B=79g9WCq5s|@Q-AMOm7I*xzz47f3^57Tm) z+3XI_=$*_uy|U*aPh+m>v#Dj|p*z?BDCH)J0W;d+{gLj>#cOrZQDN3uYiDeI(a*`2 z4cO7XhuT-Wn6z#**3V;0K{$U@*48j#I4;{p z;r`c|H4~^QKp^$u>v5HtCA;#j3WBY(GuL?6(LWNcxD~+ALW!S9C0i>S)S(CZ z)ALau$wKOm2KdS!S?6JMGkad6e7sGjhf^mjlcG%_TSiJWxuKyCN#wY&WIlXT0AI-W&h)h?pMksuhtBF?*YNE7LkQ@wCwQjf zsZuytd@4?WP@PpXVHh)d_asQb&1P_~G%9s20xs46h~2}UcM`=W1zZeCO+Zmy-oVMw z?*_@0!8CteJA`(4asLxFa&*)kzfc;99g&&$&Lh3(+SFSaySv2Ac+V}+oHTE+j>XG` z46;4-jNAnnNYC2LsrY&4{A)m{yLS8w!Pn`lU-W#aSF??5r@6t1w-zDnEDxx=&Tn@( z9@oYFpqz&26QPulMn@FEykCDr&nH_uzw|%YK^t?{1_afx#(&AzuIfwfDZ)B`vU#)| zFza#f=(+jog_z>Xei@bOYa$n5sFajjE78x1gc%7e06%v&0K|4K0I%xW-F~Y%;}1S= zlZO>8%8NevC}XWR{zDz$)BIF9h2^UWTdNgwxV*aclUSK=ts@-lGUG?QRoW@hLRJD7 zXX3(oM11`*$qTexi@3C-kL%7osyEwcuEGruOv;bHW6{ON3oj`>^y{I;yzoiWgjtJDf?3 z+s6;o!21DzwBDF8@hp)R6cL_I2&t5W!=7!ou|15LKf(cSa<2S!{FBrLT@0r_=jf59}}( z=c@yMWOxMkDLwPX$z_`(iAL!bs^W(dqSv+8=M@2&_Gw9|2FqGEtE(&fWRqGP;FbrB z1~Z?xqFg+gz#t=yz_zlm#FDw>Orwb^KRI4Q>y@iz^Nud^H4X1xrs{+{O|PQ1yghmj;6Zw5?O zyPk5t-&BQtRCd{UGHbZ&U1{$%SFdq1@KMl~LGitlhjJo{E>~&3qa1P))87!WH9h4f za*Jrv(WI=vZ*hwf&F?Hg6gp=e{dvrMM2pzey5^;ryK*n4j@({VLWcg(4DEfL697Y* zWSV;$=0;b*{jju&m6QNY1@$}MaEe(p71y5t6~TmH>FO7gwu+ZdH%wkceiVyX?Q!sH z*l?fFI9Qkb)WyTtM#*~R2h=+BjX_K`-VwK)xhxdnVAecmK=S;UDrM~!mNsrBx^$-q z0oHoE9ugg)9A90KqX9ZO=`>B5J9sDprWn59_>54&<@P=eEolU5sD1Ku9p>bHb3ks1 z2$=9}1o{Xs>7g5nc_S*y&ZJ3RDM~kFhIM)6Uh=C`9aw-L8jl)GPc;gD>G=^3s>3AJ zQ6`Fqa6#C9Te#Kq!OzMZ;LufVtZtf<33~73liWr3Q>FfpsOvJ%)W6Gt0wr)`cI^$q zL--M0|7dexFl(Fx3%QuC2Rl?%2y>uc4h|k&@`awcm&>79P%;1BCc!#OUr@CPoX0oa zXR;<5?7XiY>|@B4+x-P{!3$(Rl7omQq)eP7&OWY{d~hLXDH2k6tfdH-@hVw1kJ=B} zD5=a~Jw|<4FeB)D;EZ=tE#F|gAdF*?@e8ABzf)%#wMGv)pXWw+>h5~vHStiI>|MMU z4`n<`XnjraZo^fD?+AJA^DxP0i$Jc6M(%g`zmJNa8%ncKG{~0$9S}4|*NSJo3THQ) zz@Oq-O~2)`OecCBQ4q2`z6JT@7y2MoN5q$>04?!SE$R;Bp(c2*vz$P2<}CEp8;XU$ z^3uhD#T98Fv0Utud;2mCV}a;Jh?L|y)HZ$R1}m0;1Lil^3Neo2Y8L0+Lm4l9u|+L& z7cH_8BkNYk2=YvkL4kd@?7RVF>Fxw_1{cR~C7bK9j7;+uQFcASUP4zQKf$?1Jbc+! zXx{gur1;BECMROBIF-=!V>)yrD#8lN>(SW$qw{u}ScFOfq3IdS1G&;XWhNlfRN|jk z=w=w}&!*AEgIziRL?Jq%IQ0x5RQA;D4Udf8Ye!6FhA@&+V*)&VnLiBo>id^{7kLj| zP%cyPy`Dj#1g#R7ExXm=tVGPx#u8A$h_&^3tK~kFP9CmG(o&`OBepRE+g7hK)LAQX zID(Z=LbQqRgJ#7-V0-W%Gl?NK!49CuIho%#rQZ$_J)Ap-VYf~_FZgt0wg5ZxslNsS zG31P1df|@Uu-!}qOZGN05y#^3i{BzGFZy+E9#14>MsnPzBsb>$&M0CiY zFndP^SF~h%zTAe|#)rfGuibiGR&4cT$fE}7X(mO=u?{2!O0=4?2KqKQjt}7oBQJ6* z#>XBwY?rEGqBJL(^m2KWPFa1abjLbAb~53``G{x@z3@CV(OD_Bw&@+E!(LK3lze)S zM7>B`alzB^V2^2M(;w$~BcwhG;c-~-wayGuUy1&BTB}We2K9!wx~*6FVD@#ZRj0#o zS7nb)VS$F#j*edB3m7SsA69x)j{ZV-!Z(!hFSSL0MXB0BXrFiwKvGm(L$(gFS@$g# zN36n;eb6omQ~PhsL~ZiLM!(BMvwxNZ+i#}BPTRuBo$F?AL$~6Z?$j+9$jVvvA3J~L z%ICUH*C+mH@XfL-!Z4%uGH<1DSK(G)YJ}@_+Suw>dKDiT^=;t?eEMayJ1zFg@V6Ho zq1BtOPLCCBAuWE&=HW6pd5kPxHqvN$QzSZeL{D6gt&b3pcgy)iqA&YGAXfZMXnP!6 z22`Y(1#+cH{149YH;%RoOeARjgmzq35ZPw60irsvS4__wT-+!tRgRxS6j`0;cejii zY6io(?o9ghfN|!w(j#8TW<19;HRa7W54dgSrZ+}5IBK&3RmNT^)Xf~X^{$+2bLy)S z16zt4G-eAnd`i9CZJ1?n?NwWDa7pS|R4ORIMJXXPT=E`k*Icr;<6-ZmT&~!DpCZ9q zO#v<+H3QS}N%efk{CSaBaW0k5+WO}vC7;7$^6*t_dVCJ?3chjI-&Yf@biO`~w3zBi zwC}!xD^hS!CR}qXi{VJ@r)1KzM#i@)hjV~68@J7L#JQNI-hF1>vMJWoT^KG>#F
    |*EZa_`fO_~?vi=WGB(-o_$|-Ad+W8;mG_~%mgAW- zW?i^rJeeci3Sg^r2O%ys><0W=WRIh8k{7=<^P^&XbaIhcvgIY5&#Zh7^=4CKp|eIH z?>><@iI#w@4QQ>feQU>qi3mxb&dUM1p6{n6p(|1kWVJWnKJso8gkSqW0X(?Rx?o_Q z_-r4LzPbMz$wU$j7N;rN^!PZ_mDy$+nj)#RhOOKc$ol&>Vb2MA8(;kM^FIu%*%Qi1 z$+BEM_oZE#uaQ96qXjzmTerT1I(|H0$YR_>AMPg{=E(*&VWK>9vtAhb_mu1(KqTDN z_TaQ228a-v%1+m}7A@Iq2%QrJ-jhpL0e8%C$w8i&C2kvW5pCR{G&Nwo+>9>2N~&RIXNnmF??2&aMs>Mlw`9ac+Ty<4|} z_0|gybg0^gh^d1tKOs+eW!a248t>;_F$xp4xDam1=RtnNuJPp1mEgU$mw(?cd&`wE3rOgq$Yk@-f_?v))lIvkpVAwems%1(&)Hw7 zdVF&-bR@0{`25k|g#YKkvZ+ltB0*c09viR4qfx9=#Fsz}m2-tKB7ezaoFC@*Jh z`I2s~Lm^j%hO$vjwgnNRV@cHCC)YUC^r}UV`*4dvKTzCW*4ay;!c>DrHO4dTGE@Qn z`F8D%V5!L{w&lJPLV7xrU*qVm`%^Al%q9QZUzgjtt9^rF5>&ju+1@BT|0zEa6ltdRtiX_lMDJlJK6A?C> zSt@C6Armt3bm}_haC)x)6jNL>#33Vm@hXKbkoDk;SvaoIoh<|uC?ce}VwaZLbnS@3 z{yFZdFZGFwc1Pk-)bIOFb(puUAR%ZWimM02Omq|DgmR75d=Zr}M&rTVMeT)MUql#w zY2BMNZz)ygT(lv02!CS5%cDu2E5ZE{sVI$IrOPe0D)G|wrH1RQb<(dIt3nso?$sZ4 zH(X!qxA5jlW^NJ{ZC3*&sICsbZj~VsqcsCgx}xQFEEv132yTf~KSfoSz5TX-0W*`He}7KD6yLci@x;tNN*Mxfe83C6ZOv<6A^?@%7Tz zA2~B)GJJTaJf`vnnjro{eKBFa8lKwFa>Ma6g>C5rsAQjRTAwgL9W@&X>v`(Ov=Xjj zA!~6J7vY|l_$(N}8HaMNo<_V*hx|mIeq_~jQp^Ex*M|aHIS_x@b*jWr;mJKYE05+TM9F;wJt<<9{z*hz9IoQ_wlcz2 zH?0R4^la~QXidq7$yH0W_DjzaD|v>=JlUV?Z}7qhufkjiN~t5=5W~=3>rVw_GH_bn zz-84XZwVpU&QJJ`q_0=kJ)O!NaH`V_(1IbGdPBhpYt+;(akJV?{>15K{QW;%)ztej z3E-}rq03z*uL;fR!$v93`ssr-6}#2^Fh{-7^=gOv=??XqUfpO=wRch+b;B5sOn)xWbo0qr>;9rbzLV9e z#nj{Ie8_zK@c1x6B`!CZkf>Pb4_eVUm z*McwjuAK3x)v>w^Hf?^Jv^OGX=52Jj*h|hoX5QE{eI$Mm4_W!>&4(x!(+};%&w(VC zEUiK(ds@Eqx6JyLn)TYrwuK5eUNU~Z(ssdDSv}p$?VwG=r`Wz+_q@uV;>;Nn(OUlp z0E0k$zp&YLA7qa}adNOzlxd$_|NHA#DB3=D=zhdZNZFo02_Q+o(Eu~JTkA8ole_|g=cuU$$PIjO+K?i2eRVRPb?1Uo3dvj6B_`s)G9C4VkTsI+b0@dT~mV!$7-E zw2nvK*CKxnH1_`VICQfU^k?^XgVl^z7L3l`kVm~n0cUotD-8JD5Ovnyv-TJT;ng*- zOZ9hFrqOR+|1Pt*uWD1bomsgm+sXu~nQc`qjnrj6|u5gWB$_|Ih3;_{@Z!Y)+UxGs=5qoOhe` zKGmtC2jwwGtk_W1FMa+w=GDnz*Y1l~+fBFhE3qh>&oeQis{hG9C|*jO+A2F7G&vV8 zm}LI{P?RV>TfeuLZC{Qy9JOu9P0ryv`Es$6)%^66y(Fu9_4@CxsN56^I>tN_82tIs z)&gxLbx75b%^X$Dqf^gIne20ZojSkjp6o5ZHeMAdjPf6)>HM-)2zTnH+$#a;lK0>+ z(y9!VU~m#d2A@!z+K&ksI@opF;;yK13993afd`Dyc{aZMd^SP4NU;5V2pYQq{u{t% z1GsMk2!%LoHwJ0%$K?bs1Kcsd%Uk!;msOJfs*{|a4UrAFI>~J1ai!WP<4SNjjXR0m zgzflX9p{u}&iJt`@gYzgPR&D*C|8?FUs7S}GFcvj?7vGmPkESd;jC1^bdg$D>gXGT zcw69QKaErNR>B3equ4fPT*iq+C_CA;oPzF-QrLp>@UIW4{Z^B?+I}W3rO#!KOmc#F zlsR=+eS6v9dFOWY|3s9C@>WjrNMHz-k$yI=j{-Oq4L$$N5kK*HEb-064ljoX%>GQY zdpzgCMi$6!_qwRk6}1zqrk5dFe+Wqc>Yr}=nYgC?WVMfEWv=X9ZQlQV{N3pP-tTq%h&J;vHyZ~l zQD7tvbjPJV|4HyZTKP8;H%4Pbq*Z0 z`AjUT=KqtpF`1hq7MQ(AI`LYV|D8cr_eeYS|0QGh^|!Vf`Twac5B&dZLBWY-oWW5A zzERd$V3q~0D*>Z6#z?fdBT{5(d%d0w=4X7gBaX_WOUJ4yJ>Vql39LH{;aNg5^18@hW;k@RE@p+?{+xp>9?bj4?(Z->D0@FZY%kGE76HV;izU# zQ`WB9H>$D7)4KHk$AOla<1%w*G|$LK9mtsou{}Qu`m!zz|jDqUmz5TzpQ`Qz$y_L15rRlqEjM5*8T9sYT%HV;F+vkjh z+taKZ-SSi%X5v>S^kj9Km2)O`cs^Nwu52~4JxwoTI;;1r?`8JqV}vttA!F44tMn`T ziTh{cnwM$DbR>>sY3VrTeIc`vYOL=5u4#5f0}uYI=alKpzPIZd@W%bwT&Z*F=qoJQ zi-|}#3*NVD?x&_JL<-;hU_LmDY|kTm!J|G!;$Nimf3{*`^1|5{CyZ9vA8p{ik$q9o z;{z9gGNH$9ru>SE8%`E{b_3F?LN0*J(#wLGj)8Z?mld&~n`d;cFmQJe-@#b(3Zvi4 z@mM;ooo$uB*W)CB6x%aKg4Cs5o;-u1D|~E$?mz#WWzzHvl3}Ajbkf&-cUj+c zVId}Xt@F_rWJQFEBz{1o51!6XC&V<&AAZ19V|$X1h1F!1ZQF^u?qv{Gx^?K}WHh!W9sOi~P`2C* zR#Rx-8VL#2{4sLqDj{N)R%Wv!2kli(W!9eA`7qmkSwIoCsLyDcqc`ML(6&8a1znkV zFbcG*z^~&QRORsBv$QI~VirJY{kmoJ`!azk+Ak}6hjmq!8T(m%y28^ez@M?22}tpL zme0seEB3SUXnyhUN*KFSrr9$t7HD}Z;h^e!J~lkt|6N?kg8r~$WmkW|8cT8|$`F9b&Opw`7> zI^F(Ut#lstVcCAWKcDzgF*la;c;??!K3Q>;LiJ+wRCXz=e>Klz%k4Smt^RBki=3W$ z_~~0(E&uaoIRRqW~1S?@U*M&~L-fRxo>%TOhzoRrNX&yFn&`-yLTi8sS0E_L#m z*-}UR@I7+Trmm9f#HzvlA!^uZ%|5AXaOt0K+$JRrRK2gp*b+RAFy+0HX^SryyHCS#`9ftLwHJz$dM&ojk}5N6Cec;JOdp4`EZcrtH5z z&aLr$4u?X80{sGxZx^xe*=OUW+svu1+0w~@0C7A!ds}hxLgZ`=71@%!Q-Py`$^1vSc>0+BWaJf7X6gz#IL{=OF#v|Lr!H z*}C_aNJIO#=biDHwM$mEkxa*!sc497PaGhTEJhrr-&XKcEw7T!Jvv&VKw)t!YkQYDhzq&Bv@ptwD z(*32%o+B@}m#p1(fkSWfAg&XCBhg2#rEvwfpCN3XnVj~y z6b?)#NMvn1+T*;}*Lf9NSx%yo>;YFH@MsIGW2~HUz9(nV-qNTYm-~~Gd}d7-YY@;i1jf~2$}wxR zku9k0dV6K!R+iRGnDOyXZB^6rHu1bx1coZDN~F=TI+*y285c&f@>~Mu0=~}iamz(W zkojMeaWO{gnh7=Cv7kDL{51c|r)nz^N$b;^{~r%%KJEiLBmH~(x2~ej*gC)M@2UT9 zFY7A-$@Aa-E`^c7b{!S3_AM$^LRI3b^r$hl4Zv=k2?V2by8*1{;f{8dhX-SqudWoI zclBUDW9)gZ)Fo?!Rvrz}$qhCW7OJv#>kxIX#Gm8$MtNs;cdE||(yQ9-Rb{>-GNvM! zZW|hWpE~R&8+i7*%7LESpZ*yvRa7+y_Gdki(}8%qcOU3SCpX8zs(#i&A$IKf%e^y) zXAfi_&jgcgz}NpyB8Gh~)K_tACCU-{6_nY!RoeqziHT8jIEy}l7pfle9hRU0fx zygav+|55#{&3$6_3GtMkwOL#nZCh~GE=Zj0cCZ>_c=*^rEE)0IuRZ#gZrkzgT$Q7g z{oZw|+#w91DEsX1sBd@k3;WNU-O+DHpDqHjETGc7W&(!yugFgWirL&Sy50$_G|!d1 zx@l{gcb@Swjna5U`lI|-1L#Pr3j8YG+5MfmRYA7@u68#oTa|C8JY9S0lws6YI{D1R zkvsjU5)`tscI|k^qElD5yV)~$*kpo;k6%6(y4}U!UT2+`D=}jxPq{ zm#qBNeX};u>)DHonTW7IU-$x^_V%~NlfJcD*$(Ib;!7l#x*F5={6E-X;l7@4G4V2l zZ@2w^_=2)pM9+!Ko;_j<+rqy6zSpPNh8<~no1VB=2hM_+ROaey?s<*^R=sKFrcCg@jT^e}b6d5k<(zMYTvs2p-%9aTwGjVQJ@X739AAD?U%_!Y!Fwy=IgNmz!>Sabd^b9_Zg1m>+|^e$vZV zt0d>R==hV3DJRk5@%BOMbP_3S!kE?FztVCXuoz1bZ56;sR5%G+dZdj`$jRQ%t^O*| z>fa|ex;4hn`A9mo+N6tRAp&G4kGm6csj~=tBS$YCXM9hs_JzV3>Sl6@^|x%x>j}Trt@t1yZL#&r zkbd@FMe1rbg|<)R?RCmTn~vDhWuLvE%7hD@hqC$4{ku;7{_-ZC$prpRdWUaoU#81^ zqb$8txUv_K(LO3>RI8LLAuP(1#S|ysTYdE8C7S#DL;pYdipVSLNQ_&*( zjOOIPOD(eom>n_0>+4%D_VQO}am)@Ky(yG9Q*0k&y>iI@o7{ag;BDOLb{qqze|aU!sSZN+ zbryX-($&1PymTV;RR!8sqro&27qraTiUcpqNm+j!GIe~_Mh+s?RSBwq>%_Y`F>ZfR z#?!u0v4_icjO6XX;n-B?h$O3pw2!MGeX^}W3`re^?A|C(u_Y=M4orY>k$E3^H2?r0 z07*naR7B^2y&MmR#2uda`}9SQLM+N0%8sa^94=zcz^fWLFaB8O91fB$m&%U$Zva)9!OKj=e0Ki6*Dv2R2IQzX1}Y0uirlv1Bq_^T}%oa-5BY!x};QXb8mM!RO>Xk zOMR+-qxE*4oj#s96C>f{ByuE&!j8%?bq=+@4;#}uxJcvmn+3I1K(nP&rQH=qI;{1c zR|P_=`Tptqx7}y||3AOKko8gjy^Hl~x~uo!;q!_-Mrphv{j6Rq>8{*6V|Yh=c}4!K zX}I7~)oZowd@k3y_)Z?-AB@Io7e7YhVphJ&etiCS;ijrDBnXjQf z)$zr;WUljhZzRrmnTFeA4c=`2Kl$bIqS{}&ZON*y~;(k8zb@MTu%*fL&hQ}Fnz6HkpIc*d5Q!j^%MrG8=_Ci+y+bD4Dhzh4l+Gj`0Ye^u# z-}`va(8~7_$aD^roDI@yT-m+DV02KI%hr{_A)=b^to~IQeQQh@_F?S7_vR{!(TW7$ za-=qSoYCOH>EvrA%Fk`w2ZYSNX97~nu~?bn_hxCU4QJ&Y*`cq9IR%y$v>yYJzRBK! z$kvi#`@-q#O1=u<(H_Uy5Rbnj2=)HF{T&K$H30hra0=vg8-WC=7jfk<;NODEaqh+e z`5X+pU=_Dj6IWKotUu0#!;5%epVQpZOjobi@<4qPaT4cjm4%)^5{`kU-<_zv?_}z_ z+paot`$Sc3k1AF~=p;Q71YEqy>U1~@CX$w)icPke1cT~)k*faj`^YhK$FNgh%`Z!P zR33MZLjcmCcqV2<`tD43pX&_w4uVJUR8VMtCmb!($qWSmW?nlEc{}_B;7z5}$`Z?w`=z%Hx97 zc26pB_*?1#JgQXo)-mp1St-zY8||Oz>Dr=iA?OGiU4bBb6YJ%gZABTbG-$49sJ4LY zZD^uIRkvC2mGPblH~LI>MTI^y5*SY6%Ohy@x<&`M1YpV87SWNbavpD&A071f_jk^3 z<1gj4RW8Tjmy1G;_9;D(vlJceGp7;A#cndoN-~n->O_z4;7P|uG-jcQAaR+WU$4ka_}_W zNb}^3+81D=KaJ*LcfkG2L@JnM`FG~{R|VCbysCRT!GJ&837j%f3bQaPQE@qSGMiJ{L1)Oi4rSiPRDo=5nRmnv@@}LRIW_W zi8N;dd^L|=gm_um>O9rw$zFhs1PK*FBHi$fD!)PJ$ICpZD}BRG8@pW}-lu7*~ z^=mKwR`HcLZ3AyV-Fl-dB=m*z$>%J#qNwkfi8EdoE&Hjh9tYrG{C_f8D#>!pl0599Q@-4Td_X;)0z>ri}RSVgpKC5P6#T*c5_anccaMm##tXMLh0f_Xpa zobwTZUM#~umg7GLihV%K6!h4Vp<6>91)qr|my?;ZKzlb3`FhGD9}nD*;8$0Z*t5r= zF9)2a6Q-?JIpvjnmQ`1Iq;*bK52?_&{aeKu4Zu&6g&jZ{;?P#j_yUU`5|stY<%DKp zl8Q(=#+}BL*t(Pi%HgPFUzYJ8-^HlR0rNK($0RN7tY0ZnK}5%lL-2fvV0xh1s!Kl- zRdmAjb8d9?M)_r&M`2uOINDppo2cvVRQb2*?GRtBqdN$CZS>Y}MJ(t#` zCz~@eF(VTxvU%yk!?wL4Z5{mOMsU^sdOF>G^pRUAxNiPT#ywwoQ3{qxRO79_7}Zf!!T> z`F0HXZ0BiUkyXi;U_Lu%;gNygt1BFi3E^?ZLG0f~xd?U5!`7-dTiKYGIeB{D$f5Q0 zTrlm9v0v#v=P_Fm@s;ZK%CfEmXRpkcw0h|Jxxbvez- zqQP>obOFNa=JnnF*5EgLIW?Nkvh*X5Q!t;czzjduwsyE(ykq{a@;H2zM{vLMd93a@ zy?DvC^iEd4YM$YHp1wX(_RzJ|M>T#PkC97fL-^-$qEk*&NCrl#?;Go$v?^! zB8?q9dJ498WBB)64GkKzPcTQ_j@1Jq45_$SKKsZ|&$PI#F3=S#GA@K@uqAO?6=7e! zx!9a-L(<9bB%D0%Q$Z*C%5Ymzwemyuh8}Mgl23Te4zsP^miX$HqhA&KOJ{Fgsy7{; zZo2AcVzik!;BRiSw3 zapTVQSLFL_8qYp|r{1$?RIJH_o7weQ`dojW0D{`{3_nBPyHkelGnw$9bBC|eP@>hd zu_7{~uX9Le1<000wW7o4w$XjnwcY2n6$_=+6e~m`4S!DO{8!KaC!hDRHCDG6pI#Dr z!HO@Ooa&JX3;MEb_X~^9wd%_%^;P#`6rXV2F8LPWqs?@cNVe6tI+M-63b_|rR)c3- zRDx9*1ZQ8x5J4ex{FVP@>qhdWk}7yO1x_Zc9D%P09kbzY7R2woOpNfi=(9KYqZJ3O z)fUy)P#RxJIoi_C0Fb|To0PLTPRt&Gp{AqvE1$SW-{iaKl9eY*FIz6}&sKHI?(2#w zvz0S$4=1}WR&7-E8`USCxrjMMV4n#?@$BDwxz7Kk?QMa)9uBH)t6xt^`$v+O_Mu~S zg&j&d6kSAUIjt0tGG9ayTd{Ji9Lq!*JFjA^!t6^bdo_JU%K=Kh2%~!f{ou;tFyxI_ zcJ@c`9<}#r8&@>|J{Y7YVK-I=_}hbEbrBe*&orxs=#zV6`iEt`zs8A^fExJ=#y*Zl+81ArbWg0osv$uMVvYXbd30`r%8z4NB z*Q@h*WqPyVcy>MGa_5<6`0^|S-brA0JD$j(+h413)SthTUZ?oobR(^IjN_xZWF(sC z998jBp(LH-k}aPVw`z{``=6Qr)4auJ^Hna{yuRh{XRc|wzI9a@9os$RED&Agv88i5 zqwV3>rU%iRIH9Yny*%ZrVB4yDyeO8hJF0kc+H$^~!J!#-_V;J^>v=m zwJN`?ZBN31*;H!B5AiDotV|Gb+l+Ml42o?$ID$IK%9lTPA1WK4Ej@#UfRN!x7_c40sriOcfQN1a?S$m}_L19VLK zCdgDPMfCU5fhbOjKl+AlZ#f37#}|JFXKiF}kSgJ0x0~ei!urN-)#**gh$nx1R~c3u z^mmovNi_T8|L4gsoy^Km2^RYMyUQ>u<8~ckr9+8(lt8y%ckg>A-B}q%fxWK?jB@Ju z|16g*tz+&R4UzF*;wvj9gcIT*L|YC#C!sKgrL%OSYk_Z*GhrhOHn%jZ{3E~0URIuS;lKlZFGrSVX4jcW zG-?ySKl1I^iQen1{aTf7nvZ-PCAJQ@xN$jvbn>;pnV6%t5NUS>HEkO&=YGvrY8*@V zq|7Q{WY28rp7tNc-a-5HY?gnh#s0b3Nb^wJlTTe;7|4O1!A#3_h)A-s>SSYnUPtU> zd_X*{w2*zMmqS92L1+sdu3LOgLQJf7c{noZDhmthwg$bX6b$f zapjpZKJ)C1-FLp;87n&Xe0D~axUw?lm14`or^xci=1QFpE0JS+&V~QeZ1Is(P+r`uT+Re(DZF%CnsuU}}e8F`JkRx22? z09EZK6C|qZ+3#sEJK9e3NTeesW$jSqw^BB3@2qWR?dr_5UEB^wVoTQc-tMD`vl2YA zFR&cx*mjX42=>EG7iVl0EOcDBtV$@-b#Y~%%mxRkdK*+LQx2tE57(5wTb*xmVw-$D zCaMu)gia2+N+SK;7@R~F>mQdTeZ`ic{V`bz=v!cXQXZ=MgCPI3f=Az0BqB%TvA3lj z^l69D9^#X5@$zL3%znFVuTcj^1s>fCJA9i|ZPzJIeXrh%9aTxT%4AO6#nDO}HLaWT zSnRILx34()EB8i0?mV+xiR6KmGb&cf_^NV7xP zmFGJ5-euWI`xUl5OXHR0cx76%a{sC8qyE1#Hgw8giV5~v)lpinC~uX}5;h7j1c+UNFfEpNn`pl}!F69W{GdaNZvbw~4IQ6ew49U(2*Hxo=U!6(Ww6K$%%|73v8uZd{gvjDc6S0xH$D<5Wqs0YVcy>=rU zYy+9uR<<%C(sbJ$r8VL=5?CtnLhaqtu2zw#EvB}XtkS94pkjS;?jBz<@iel7Ju5+@ z5wF(J{>?*C6s&#CO&Pe2VWhZ1+~MAw2zj;g^6g&z*jCf*X{4qs+i>LkX7N& z`)1gP9!PrnDoHH&8+_hZ`=9n3U&VKtfaTM>`|olXR7^tK&nIEa&PiHbe!2GzJLpO$ zFE^9y4M~!A(xoDC(Ou-=&CsrNgS2&oJ6J2%d z)iPG;ydxNTN1C&7GP~~Oh*cB*dk7D^O(qFt_4%L2Sp$CmmWQ^buk=u%;IKi-v&(LF zUslJNC{v}G{XWV+6GQyBS@}Edx!qIcRkh_YK9`>n-u6eH2hdk>`N%%l?JsxQp_}$Q z?t9g^_l~mNO>?x`BIdGVzO+L`$}c%J5E5hQfZzRblUZd7F&)l;A8%>PF} zevG&AjK0XJR>tafmq;hu>RWxWEnTVcNGOB~KU?ES|8Ypo!SBIWw_fi_B|em|Oz{Iq zNA`DmxhY>w(QPt^$g-Wm;X&X?P#CR{*qumUC85iyCueKyL16M_gi18n*-gGck*&bk z%h_11Q3d$foAyXk%T%qf@N#Ew=STIbg5@mm?+71ZOn07-x{P>M##)I8cLvv)Akj^; zE6`|qnLW%zkXb(2^^V|_wfF8k6~cydncY{36jj+)x4TsNk8CBoroUabsK_TmTLM_J z3PrctWc^3Sm@N+tthZ|+1Z?H>&(t0hm5gntd-Urz`dKVHoYco6O*j3L%2AQuCsF%Y z9IHxn((=jcq{z!wpZ%G~%8Y8I!*JOH3S9~L3Vp;)F<+eG_taE>KkQJ;YHKdG)DY0-=LFr)c5FngLWL1 zWh6Ri9$6aI_MkhWP-Tmn)@(ac*0wV-O8L+1HfyU_K0izUGhX+@P-i0b?D{ixeDyfp zElYPr$T7d!* z_BFS7Oa+zTxclF7wx0UAm#xLSt%}&n z*14y;FAMJX`aa(99}tr-eN=(v383vm&sGlTZ;{_@l|U8Hdw^M%vDtHHfv~d5-u9ks zH(51N1&7gI%6}AK&+^X7Ib)r*eMc1O*pAzn+r>y=m<80{w4E|%Heaz{wc(Qk&=fk3 z0km=dOcYt+m7SrHm4B9Jr+eKMQ@n4C^78&4V#hvtHax%6#4y=X=Y1^euOarRz~QTB zqA&ZIADW&nquld-9Hts+Wx=_>h0wE|c82zeFBMsoag^gExI8Awbn>(VOfz9&dp!GH z-=-WBwk_vX=Vx^lMYep_?WuCEMsKV#2dmR3?%d-+NhQFz_@aMz^PL4)-E{W$ZU_sb z^g2QKF^)=JAqrG0EaDjz0J_4#Y)H{Se;&satN z&b#xR^QziRg_JBm7Z74h&^qeM8BL=jhy-gFiYoCVYoBc8PCP#oJ5;1nJIU&;WzE`9 z?a1vw`Db+-wehS^t_oFm`|rv)@)_^#3Ju-+W_9eOQ_WXC56o6a=!=G|T%Gx6?_2T$ zMg5h_stXZAD&b=E(k9NonAI!Bn`1mnUhruCewNqpg^6vsnYHIJZ*8ke{Gj+`8_CYn zJ`o#f__i1SdnjTj8h4+WwPhz=FV9Srn5F4r zRCa$iIDbd|X65M$C!Ws8zFr|5>;rW}7@4(?Ua!(y*;*6j@-q}dXwa1&-S!;yzfSo_ zV%4nASKDV?e9(#J7!!3>N_I|4bv}rHQdbT}JqZ-LdPAOZh2IZnjS1@bUEZi{6WVC` zLf4Qm#UQ#;RIFAkH5LeR!>xII7%~fmUi2~ zUv+O5)V(qY@^a1su(G|IVoDVdW_60XI|CO`Djpi}hs{+7j%u3ObDOQGSQ55c1=7LE zMTgz~3}RLvz0X$-=&F(}Enj7#W%Hmv((+)T5=FB*X92&KQR}SFd!3wJT+h#m=*I-6lIi$7oC#i8pRDZtt@`2`m$&DMn#_l0IYgO=rT?uA^;1QdUE`#U}V zLRS5rwzb%&Vn<(GJza|xAdeNR7eS*qM1vB(Ut{srqiX!Q%K|r z+O4u$dFI)By&iXhC=U)iIGVi`&+3o`d>)i~5PvGiuCRq3?2NgxK)gD=1dzoxYMI|g$>33IfxM<^b9*GwjpX`1YDzwg8S2yxZ z^f-(r5e+=8uCTBYJVze8A=)aLQC+$N+Uk9)WnWGIyWc+(14gSSc4z0|$#vUdl;23S z$l75ff{YFl-O}=BM*`zW2r2t$a-Psi|8Oy3%GXw1T*Q~;ybe8(_24`Bx{HoI+W&Rc zh@N)6-(P%5=WrP8Eyg}!>9WY;bFA*1=-+B9I%*)mEbaK zmP&}6J?H&f``}g0jCMEo zu(j$i-AHG)f@{=I@Am(_-52_Q$F9E0J!)$nY#(i#2mpVoe(K-2Ad;2W&j8ateCi9u zIaq#wmY&c5*+HRM|Bw8$7desd?3!*n*6}M=g!qA?Dv)T}vjb31V^5ru@pyr@UGsxT z{j80VXrb+(`M8F0TES6?3F;(wgo@eLl*-UE0U+B>vLbS5UbEn!5(2g~s%J!1@ z*pnJw?jyZeWufZ+UcBt{uf-Oan6&k+&t++&h#1-1+p(P4^Z)=L07*naRNVm)f#ovU z+NzYy(bKm~Cy~PhjoC8UN+5}5>#XXioUFU{7|(63z6W-X^A=|5z4H8wlarGK8>7_^ zE6;{K={W?a=j|K{Pdf`T_cW!<{#M&LYA~s*DKwB(d(c4E{~dWpxkjRfuGZN5gf11f zLRO-I=A-9wR02vWI$B<{K-}A>ljdR54_nTjiJ&_ZGJ?-#?kEJ$Osw?a@iK;u#%lf4 zq}-Rai}u&7O|#%4%evxm*rKfz@o_Q}*LIFcX5L3K`}Mx3EmfYg zaq=@=UlqGo?yv5hiS#ihk2rZ6vw2p>w=CULKY79Yf28;DGhEQnemm;J;Y<1WbLzwD zXW3`zM+`%Vh!2POfvBhJHs+iJ3_rBp{4c#%#g9mGwjIW|wP>CG{Y-4=&+_)}bfbKA zr24%&i(|V#6H2OcIkN2?kw#}Lh?Z|9cw7-AMsIr~|J49^_5ODbgq^ZveFUWK~?6F*&Y3 zmM(_KQ1X_gg?^64sqV6h<=7^&59a4E;nXINttBU+!|vam92Lx}d>$3^C6bi`a5x$d z$0OcUc(8GKUU@j|!RP7Rv8o)g5L{eL=s1blGU1s+*S4318&{m)D%sf^-FU8g!>WOj z3ND$*P;HISGH8$&fp_**kE2`}a)oBVM!e&dLx>SL<+@BNCCL5}x+{pKR$F-M`S)kF>)+ z!WLZ=saBP2Ed1I?*y@OSzUn0F$2xu}BXte5?~cwI+1vFSuk?7ucs{bJ)%3GDTE`Ua z^WFKcQwDz-5@{cG%jSPQ8!QoyF9L_Wa8YI_zb5(m+n!G2*|@Lt!_Hru{(V+Ryktrm z(N~h_A)@IgZzBymuVedJ1&qrIlgXgeK4i2yobxD`eC5PfdiYyIU#*dCbI8iw4H~LC zYrg5ZBM*`1*?c_zD!o_dGpqMAOh2luBYXV}W%$bF+b7(~DvB)lzc^BZG)BV6D^@d{ z9LA4$K-`ytwIhRA*c-CKHg+`=tICllt%j%C}>&da? z^5GR;R%YxOJ}xGoBReML$9?6Bj@Y`Q6S!|@vAHk&!yb}V6f4&}=+ElB zvfAP_Clo72xkhyV3E_R~_lKW2`IA0@1o>>11u^OuddO zthi88jRBd^s0ZF=Vr#Y9W3T^XyqTTrqxp8`fxGv7CGn***Nx_^5hH)8u>WnML^a>~ zINlkL_Oc{rfnB~9k_Z!7f6w|~^tTWbM*Z5wh@*e%izX2`E_Da&<)ovu_H>h_ry~);oeICVvr@xtU7x9fPj^;2WqhW;#Fk0CXsXo{p7(6q%ShPpuWjsh^2i`7hXzJ!z8>ggTRXxA!{*{|6)aBnXrBnIf_`nU z(V>vaZPv!wDxz$4L6wf%S{7_(LAE-knW&JZqdeUa&?>wjz}Hc;RDUX2Ts$8K0=rNWiG_pNUDnG9~($uL$vAQRj(jMTminzyKkehP zoi!Viv**=5^_+~6sF|%&Jyux^w&NW275{Aa9c{J8X^Zjpki0yqR@UyfZL35)t;h3` z&sN{jijcql_doyN??XoI34jT4oC5Zzx({V>M>ef(Fyo>=fvuk{T-9$tR6ESv$0V{f>>?w3ITOAX3old z)Y(55D=@lg#OjHCQg*?^&-sYe5iT<5)|JOLk(hf9@kPJzBBzC`gPXmToVD-C`6^aQ zq;H30ng1w{?Cnr?e-==7g4o$TEA@ZZy_sY04!W<5P;o-VhhzM*Z6et{8hB+ZDYmry zld68OV%XFu;M&I=JDfJk8G_33x7nE5N4%k~#Fagt!TA!(W*^_ofv;?AC8(}M3-5bX z06P*aqJCMqE886P|E@SVV$~IPva)Ao&gx%@D;9MsX#1mW1v1uCreZ2ipfg=+` z^ouI`LM?pD<1?`?P*mq>sB>Jcl*kTA-=BisOyoRe8FVnpnO%-xC8|LMkH|>f=Rl%#STNA1sv`qhjZ=r z(h(@KGdP}g$nNyj)YcnYJw|zr&dcy1;$ZsthrbQglU@8RtUl|GWcJ3^-`0i@5zef( zr@keQ!!&2Xwg+I!XwqS(Rnw_pCe>pFWy$h>)qL=5I^D6)?e1>AcjnPd zTo{d4;jih+lB)dGSXhm#ZlBee8zb@L_|n~D>u`4O>9>a{aoTn=Xn<@rM*QaK##SBm z2REVP^cmU5`5zbYL{@z4*Z%XL|NPVW4C+?)yI&LWdUu44JI1;aNZd)EXCUxh2zW4j z47L_jo(9jxnHMv$;MKxOq_+~>XW~GoU1zI1vNWo4c~I_UnsMAGyM>iBes48acJfVG z>9!KN;Ut`xFmY)+6Afwz{ZMoz=0rWyQ{<$}PUATSDAZwItVMeYay&+vGr?3pmKQkL0D3r}mc4g3rXAOB|Q>EyXP zr#BCm)#tJ?xSQ_bs6D*0N_;q_CtoHxCdS8>lyrr~mWhZLmkHjV)vYU;=waeS_GZN$ z>nxbv%bqw=ne(67;OxE?A!6m;QGe(-(cOD?hL+jvu)f`I@;pS_6)>^f|Flt8o7RhG zr#Q8RWimYxP?G6Dd-1+kn{Ks1P`WcYZX(li5$nX; z8+{gJ@|7(sKMWj0UfCF(L>Aqv7xyHr2h?1Q=D2(YpF%wNc~Z@5vk7x+MDrf!o3B3> zmd0s4ILT0`39hpom|^yXktALL$iK+-5uN68O5Qs4kxrcp6Um<(a&cM=gc@J4Lm+R+ zl7oW>IRX?aprIqhiU`szRZ{z3Sx9PZm?&d}*9{#@hdr1#Nn$XIAo8<5+_ zb@Fp|)pw{9g}=DoOwX2g%p=rE6PP9}kIuc_ky+*iB}t#I3t&ZlFiDbIR?OT$kY#5OUM#aJpU1_ugu7Q@$^nD=h~ntglf z{`wxMHp`FHL)_%LU0dx}o9qfi>9nP6L|gGe?|*muQrXuXs|}cp~7*St!eR34p_ai*R1Sk!>oBXL&h`q=-lf^`mQjArZZpS6_PpP+9s2 zF{-#IL9hnLU`yuq?nbFQ)9l76ZLdAK6Ma^%O&c{aVcfd?rLfuFsyC*R|1@8#d0L#PVmb`C`1gcdbeIg$_$yl!Xi&{R?-wq5t2WUWnJnuZ-KckLXPM6Z zRE=Y2{CSSCPdXcX<@?U_EAxK@94+q{5snlA8EVdt_%51L*%)PWc%t9Mr60#kN2)4??aJ;%YlW}nuebD z3{*_>hH{8i0HHH26yS+KSq3IsnScC3Kx74^L$Y=p1J=tqJ@`4XYKM-AW#E2kN38iU z>?RCR**jSwxf#|&YdwIwXByQYSJ|>+I6#7}%SG7v3IbYa^R(bXx~1aG`htwYRMdiewOAm%(N`U} z@*-CV(KUI&TFnjAJ1tg&uS#7#N6S@r$5vez=+}DdT^1g5`Hc|ghZX4 zFITvUhyaYtpV3CYngd(G0A)*OQ;qjPwE6w*-4?F(?CIF97UQV7qocMYme{Jl<4Xks zO!sZTI2q|jr~NJZx_T?Qs+?_8_n`YKlFdMtXkMw!8PB&0NbCX_N-x%p=0e98IB&D? z7d6+|2DHPUqw3gdWWJ#vy}klOrn2OD?&Y1FAJzM1WWMi}9i88kUscYEjUOJek13gL za&8g(LcNptIhox@OFvb}41h5!2FEGKnaYEPVJ+rI&$%&?8sOxU6~hC+OFQ*Wdy%XhG|LK9J2aXLPxQ0I!+hKWVbK;@EfG&Grdg*Nqsen+1tUa_H&5c+c zo9#&k;~F}n<`f8!&6Dm}b$}1|^_Kot0FGj6tk^tox{8{wxbV36LP*KHvPF61OSPbN z*)VrV(e4rV_tuSFIKN;KySks~3!nO;=Zk7wz#w0NKd{MjH3!zIAV^Wj%+g;*@*s$#;)UARd=L+(T1V3$?3>! zd9IAuUpF$938G}>SvInN#$f>c~y5W0HRLms^)c7-knpcXXQrFBYj7xQ{jlI z>d#@!dv@_21u#Ulcfq8FkPn|93^+K$WLoP1yAS~(HMcI3ho07Y4t!kHA;4w1SH$&6 zx1FL=4e*}wEbQbNlCsCM&$>?!)E=l94$}iM_>va^1*;e81`tEpG`exdxN%jt(ihnf z`J-y1DrcW%exDWs!mrge1tet8*e&2uJ>zl-WGqynOY8>(c%~Oy5c`VpsxF5a#Fqu* z0DGd`+);wB@~i$87C_m8bIZ6zVJWC9JK|8$opJH+fBV-<^D!ssQ;T{$sYHIw0uZFy zoK_nqUofiiRJkkMH&wftTvazK-&^MOv%XbxDcclg8_Vh*tJ-~(@@o99VdoL!b`87m z{$c$J08k)9$1VjFbn0a?YCifitE;=33ti9ypBKyf3r-ip=PExRBU^d%s1lP#xi#?B zsX`L@U6h3~?tO1v1JsD->M{-jY)~I+P#*Jub>~lX|6DLj2i6}MD|ML@&c-kU-~$tb z7t0z_Bqd#eAqIs7`OB6Q12K_2g{Z6&%%9rAHK5zYxk82kjwdKFGBuyFnFp#(=#k#L zWm`wuy+>E}Wj@+0PY={Vz=Oep4kJK?C+z7f*IjLfMSs{v?01*>fNgfbpw7L}ou?H* z(;X8vR^7fRIrn3w1A&fe(ICGlK-{PL>b_8vi;xucbtd|%7PWH`PD3wp^vTE^uNKE6 zy1KI27c^efMhWuVPnhu^*%_P@!)Br2@k#@*{nzbn!EXH=x&OOSRb@ z(+E(^fJS^);`7osk>0jFbMcwY$)NG17xi9od1RC@sqs>*%x-Qjl?-(B z{m-n2s*iHLB65GQ9tPKzb(V*Fh>(*-2PI(V_v_53@?X~j-Obsr;GtXlBHK_#$a41!x<8<&mtDfxKu`f6 z>I`O}KAa0(U}R+eFwjwLyp`^X>A?Moz@sP!NY(Gd0XopW2)OD3 zGCbFCafk7l(V{_p+2Ah+OB*5)sefhZE@;$zk&7k@F&J40V;fPu4%=eO;!!a)CjUWP z_Vio`z9#x27?XtZHD4SlFhcsUw6xsJY$%*b<;WtILe z;IXIOy>)M8W7W>l)Z$$ui)9?2>7{su&-@6uC)=%ASYBv+i|0 zP;GifTi^2KyK3WT8>{y2QQiR!_SzL~FfYE24>YuQV?Hup7^GpA$*8V~rGni*dhTWZ zVsOY`2Ga{I2v=blx1C>X(HLidl=1`HFJG?WBpd~>Ngx9AzyKf+=DF|}oEr<&MMV51 zsFeT?+2TWf-JugL25zBHv|0ci(Z;9_l_P_b)^Rrexebegoim9k5JD~5T$WigY%?z8 z`R|PG9&Lsg{1^c=vQ64@Zw9CL_=zWGGjC2KFu_hpQJay-ic zJKrliM(6ZXexC=bP5Y>J)7MRa8|m)Mr9F0c^zX5)dRCzB$d>H7*JltodiG_{nCv;~ z?*e9aSRD+9!TnqyQE* z7Ca`{rMi(<%@54u{mo57ID;Iz5!mo6zxeH$p?RpkU;^osA3XrKP&p?Z1 zo)q(dzhEjrV07bfjqz=7qzpR>GO2X|fv%kD=QY^_d+fT#m>i)q>(kHJ-w~gs;PQs& z`$*d;TLGY%?AA6%>WzZA_*{+Jsrt+(tW-=7XX;IDpBlw?!}Jb-ld!T7#fZs)pnJ!E zdy|-U+Bian!(P<~FmC4}R92kQcJc8Ga+%3CgO4`~Ae^p=BV&2Y7q+JlDgY{ey2`3= zn0WN%49xJ!EXwcC5S1fl;}_L2vMKwbU3{8N3;^D^t~M$s;H6?iC@@0-DG~EPu`i~V z-RXhX*<4RA&B2h3u08IP2uv86RNXc!AgT-aRG7qRGal3v8%x zmkOZqd9l=`FBBtV*I5YiSFhE=w-E<>ggv7=qU`k?6YC35U!Zb1QkyaC1Q$PeH0v_o zl8s8V$mY5kIHJa+3l?PKIqK(&1?Yw_rLNs0wzPNhNM>j7!6;l;1c}G1xtF!Q<(-S`LOk+Stq=Ix!kl%`0@n1a4`u44M95usvGzOV z=rG$XFULhZm;qB_U+~K$M@|Dpk}12(?%w< z*I(JDE^C*|_S(TtuJmOa`OXHN0gq{8M9Cf1z(@Y7+8pUm7kKIVtg@{;!MlKk>r=o~ z)wgI|dAyjRQH@Jxi?Vn1!X#iyfhMe<0TG{H(?V_FBos9^>Ptj-48NLO^tDUn?~wQF ziK!tMHG`l9FS2VFmbqxzfq2ImO&q2sr$@}s&tb2E}TV&0?f zqxJFgW_pj&@zT*dssbd~Zu|fJ)i#3qh-uZ&48-8)b<`J>%YbHo_EqDpKoE8OxVMXm zP3;gns2y3*G4a0X&;UdvVbKH+H74?6W};vzTQYLhtFArh? zyhNMiilLxl9aZ>_vU^z|0Qa&BdMJPb+sMkJG3WaB{{@|qLSdruRDE;*yWYy@74Xr$ zH>dWJm$!V;2gi(#ZkR80H;;zh`{gM++7GF|e2i>+Wg8RU{8e9*v;K~5AS>V^@`1NgZTz!MKIqcd)-Iw53_j$9H{^h^ik9X`z&g|J6}e|edO8Qk?qW*BcB;Z>Qc|S z(dWqWj!TZm?$}(t-|!hYz0V=BG`>5~egrb2&_$G23=r3?LL~Xbtt>x>0yp-)YdiDv zS~Asmb=!?XQ=-sKJc^aqbITmz@Dp5k1ndpZ>9OSV@D8|qQqS$cBW>;4lfluQ1z$vN zgekD4OVtJ*%Wacl8{-ahqtaX1>YJ@j|F>YI&;tMfAOJ~3K~%KGb?krN@T)TKGsGZ#v zlW3c=JE|W6Skd^no(?DwjR}sw>#IUUGUf+^Oc7wEz^Lq6^+jWO(KXo};TI6WNa(cP znG;)NqOcPD!o_;ClT{*p%UpsZ0C^!>EtactUI1r6NhUkZS)f`g5G7+}Y#E2D&K4k~ z(%CH^-8g4@BL)Jt;Tv-Y6;zqEfuKW&VNii6Zg@&(K8#8~p9`pgrvws=L zu<{6uz&^5{qyDo^neRuz9;M5#RpY>Y!G5!kI$%Zje%LBE^8DJ`M)X`eLZ9o%7#X~I z?esa?P$NKXZD(?rmt%TRCH|wMAM68GiG|2z}ww1wz3S7vBwE{O7 zuyA=Ei%1XJrhLNJrf0z^6i9uopI$(gUHAmh@c#%%DWAIkk@I?xrfUuWMH(*R`W7SJ@OX0W$p&@L_FL;72#i!gVQT zgZGJbaoeav{cYDGAVdKod@cJws?YsWzz~jE2F!H87xq;Iz-0Zc%GkH6k7{gGJ?wim z$E$KR_Yq90PW#!Szq;Q#<(=o2KZ~d5zsRK#F)gz8GhkK$7}z$KV>?)u_00haby|vI zenbgRIYdRh7ugScz=~^zz#R2KrCN8T^P-U#Z?#^9+#Az%v2OCv6J~nyNhwPh3L2>L zbnq!JCmE~u1UNcRoh!&>n>%h!ZEU;sM>hV;!{7vjL?DC9A%kF=OfTOT#zwXoSDSLz z$)E`8oZ2`s9qeypN#OsHeJ)dh2`o3lo`}E`WiR$Q^Yth=g#BOsK8+3giFNZvngI{x z!|3Zg&Zns!uzB21Honsf`}81<03g06viTnYE)j6i2{q~7Z_%S7vI*rCU=Xn+&}a5{ z2Hq%HetA}G4eponS?4iXy|kcvf7)?maq3`&`NMd+1_a0v9~7DQ=`Oq$N6;#X*bg!@?e)bn8ZIv`+nO zT{FFd{j`(fc`U>i(?t@&$lPQ(U~kt zrnBKena(D)BeT`ss1NFkeZ(4w+Exq*1V0pu!E_-&VSjVGE`y*(bj@u;Jt*ryiIU6O z<}pOOSNGm24+n!j4A?09l^>#hM6zrPw&S{x+2Je>yXFrWXIl6*4li;QvVXa4ZmR<@@F|$-%e%>}P<^a_9SY@CG0vbveF5ss51IN5Z)Md5l z&%Vw@H028)i~Jqa914<_c7cvaP5~LC?^M%mi9P@Q_)(rl;JQ1Ts;!&5DBQr>R-4g? z8NhysHro|Q5CIPirgZ$?VN-PUTtjArVR4Q2QKteFB5(=k#TL6dbBgDVwZq^`27W17 zyqB`M&3#h$Rdhdg?o;o}2n(dExBGnCGH$o36TK@s9~`pxgO~Z~D(EY2GXkmooyDP@ zd^Gx&Q(La--)fzt%CnC>mA-5(6s?~T%vi0jILRm_)?JMwF-Ev8x69cfcvS3)5poJ? zc~}NVcX5BsNp*zoBm1!h0Erj{ys5Xo^5)#G@kZNyQf3>J2<7<@JvY^G}pr(ssk}+m^8jDjld0*W5HI=lj0z2IH zZr+Tktsn_$wcy?2n=QbKoL9-IFr#DKm1m}g)x%9xwkh*ABKayjp$mFMec4;T>!Y7d z?os`g+0_NUa85?^PmN~q2xY+N1<1UYgJMeGsvA#hoLI&dKp=c5d>N20Z)Gfk#7&WRPW z02_AEKn(sNmPZTVs5T5oI`$}C>I?4?y0?N1{1x47$(Rqy=gf-8!Wz7B2TBh6MUjZS>U(#M}0| z+2*}sp-eAwIx&WU57mzdK&gP6E>q(gAVD;5Z`sxn?-tbI^jbx!jS_@%J`Hum9xmXr za1riLwqUKkmQ_H;GT#F>$C19>vf-#UPnFIJn82^^+20)ogxY*|mwLlr0{qkZ2$GymYcG*lTjlh4+>3z*T;%&GOa~Dp9X}~T&&T>k+cqkv;dAR6CI7%TK``4)W*IGY%qP_`p~WG{Z+sR zmUVy$lkc)GTsP_+^?kJYR(h(<@fPMqr4#)zqCfK^_N~)?$EREM_}K9rs@kl86lQ=_ z`ZD{yt>`z-Kg_-24@V54!TZV?Bsh<=2dcW4)aIWV78R=P&%EWOAdaNP2Y$vFnO?vF z5*`5`k$pU7*~^Lhpz2mKZu=G{Nj1Orlz(NwW0^O!Fw5eI5x`K#?=3ouWS^adQMQ2X zj@by{($zW5#X&a4)~~qMR%ZhgZFsy-__aFv+vp%XY6?~xk-^)ugIl9aHh0m+#8jqi1Y*b)KhY8ZLiTha1 zt!sk~Ba0+u3tsDN*dm!u-N|=w8jAm|lAT2f)21;J3$)cjM0~ND>T#y?CLl24V{4~c zxR2_q=Gbz-QOvKZS5eoo&WNn{8|yPmqr;R?8@p?LE${mEDfjfitvt{L@1pP)zW-Fn zOvLQ)x(qfV^8)YT==q}bD8^RjS$QkJU#p!Akf<1co%e(T`g~Fc>PB$G-nITN^mi$; z9lb0%mVNL0|5|>&rC!Ad#~8}E*cYQO6LwaZ8CzC|A*(4n%mD&OME4Di~b zud1^Z)Ns9BCJoPj1VtQZa~6lox(u>Atjz!C(|PbZZ~7H0OFe@alu-Ud-72(2g?gm4 z!wpEhvVmVd3J{K1F4;U81u{nEt2wG(u-9m#V+Vh&!9@|q5p!4yCk^!lnEl2tPMmwg z%GEKjzF1R>zb;i785`bCXW`lLQzX;1t!kqJTD*P*WMoVXMMLK=$7+-1ZFe_xRbM;h zT@bFL>nU>u4{(;{D>N~x@)q;c=H=fXKUSx`WOi0IbwEPYA2C59Fhemu6x*YEHkb_x z1XZCdXm9tq#y;FyZ*eoKC-P?AEf+Z?Mc8!wDz8kmGx6p{8YyO_y76t=T^tvyPg6i zwtyd5J_+oj0wYvt0A9;-V3=+ti`V%pfA=dsGAyFaw7NbTlAz|mmd*Q?I!DgGd$;d> z*<(jmcMBMi)xk19>J`}z{SkAOr z_JIQxxaji5k}uf24j;pc|6eR(noN#sk4XfaWDD~S_~3T)#svElZHi1rwGsIu+I)2= z_t*CCQ~%QgU)2K@sBuKUs(Tcl1zC7o76QZ1qI-c3_iDF0P8|@vBjfKy2RWidrg$As zZ}E36hNtNeiWwRHrVfsNzt81m5%O4bOf|O+pJVB6k*wk`uV+nl|NnX(+27-9`m_sNZ0$woo{z#@7z`P; zAp=@Qbv>ed_CDz{{S*svT5s90rsDW?*2|d=R7mNhF9<>9P}HuUqkBTFb&gv9C=dhf z<$QIxZteWdALtCUP@zf}%#R>!WeG{I7#v#|5#518IR+v+?M8VkTyH0@#R!8!u_w4K z+%S!>Au>ByCw?9802ZB%<(4sf^g4St{I}}gSF!U}{!r~?a~Q)?m^p$>rik6a^%3B} z+#SokeqxW{v9}n;;(wo+8%B~dp7H#X0T$DXRh)jsFNy4b#pdXY-BB;YPFr6^CiNuUH$)Tx_y5x&TF2j>mrskg=zZ3Kz*vGswb8 zzRFyF+IjZC-i`C#`dh~09&K*1^H;0W0Wy?Nu$}@stWFfFGV<)}Fx^J<-D7;WwGzGabO;xuM-WO`a744?N18x(ArHrOl`gf*lw4{>!(C#f5o2 zm~oPYie!uqHLMW>W%MT40$gmVvuv7+n9ouBJMts+&JkH`8;@?SY*N6*^unf!h-Go^ z4Fpk8jsXkTi+Mck8h-)d1)ut#ldx3@-{ib)f+m#@JLOrP46Gl47ttoduQRZ*Wizv- zPN(mCWUjqfzP5hu`z_kr+s0`9QTb>-RC+qG|CNqCbJ6NvTmN4Bj;P;BfXdITPTn3) zj_Nu(&qfATsyrWEKimT_Q6NS1j;nxpWOpzFGGi?I`(5SRFyC>0jqdxu^823kTBUoW ze8dg~PDCtbv>^>+fdr*iXFtHf3%0lQ@rEPLsbsS-lBqM4u+y(8y|^Lduo06-AS2p5 za-2TW<(1CL4lF-%!#bjCM7A2QXZh0ukKh3XW*|_(a~|(&PG0KI1{ELZ*Er{fJ&-XM z4gKT3wC z+)yptruG}BigffehG9r#WKq0DHrgzxWP%xEV-H}lr|yt5ue@p8a({K~KT2;`56b!9 z-MZW#)@9{e5(X-pR9miJ*}bBe#+nxmQPI(f%lrsmX9C9QQk8cSLP4 z$P$4gUf-XC41A0{e{VHjoQs!3O*%lu2vY=;mwMVZIm z+pB;G?u**!XGss0d<7O%<de>{>Wh@yG&&}ce6{FhfFWzQ z%86lhvjm`4Ud5q5!%mJG`Aa=V-J98F|5|NWZycEDZtgQ%PzQR-H$&K#w~hC|N_5Ef zSv_S##m-P)n%S=IEQxe+{m!^`WV&|kk-MfHRSuBO^QqFI`p(Z*l&Js@m#v=ldwf1p z_edbV0yp?%7Jm2QJC*?t^-&?a|F`Dn81da+*$KQk9p~!0y6Z;OcDCkKWek{1YbBz{ z9?IaT0FJ8MJIzcDyRLFnFnE#W++c?uu{o<~?%rk<)`GS@0fW7_-LwAF1GxvPd%rq~ zWqD3PE)LYm>@a+;Iarb*)(R-t^WxYtA8y^=78|c!hglek;elZxe2NKwy}^v_X1h8m zg+UPSW0XfD0xl-~0D>$F5C|VpG0I0S7_U7(roRG0A`qmrdANoBkKD#JH+Q4`t~TnU z`cOUsQ54{)HvcS#G6+<7n-MsHHmS`|rdKgV7@T3CC2B+YGV;IDseF5_@!L8_tN!en zPpWR0B^Ch|M>0f4!(NcZu?N`LQH>%EG_v8-DFxVXr}aROS@eYXkVs2CTRd`~v|_HDY%VH~`5hHWw$*0_i0f3K4fZ#y8grVpM)!`j+HwtJuz}4O{;%07nH{F!jR~c3Bybe8$%3hLT|CvO3R= zes`)p<(?k+xjhiEG_2Q(;SpU!FCj?5AuH;C&jZ2_`0DuCuRb#OTohJqXYs8OhSbw_}YXs$7vA@TtO1KH-in%8*G$VVZ3aS5Zztxu&UNrD2C*Hp<2;uS( z=nyTaxZbTmi){R|{1_R9S|z)?<~(^c-CGy9oqj|hKn7IsmwLY^)b)-HDAQSVXEF>< zbQuC}Lk1iuz`~y!*(Mypoer2X>N90~c5PID)bCL}>?<{IBLAp4QjInGfNkx}Gqi6@ zUyrEYwR;Ot;kvOdgBOZT$2O@Xrd$2>$n~msFta83U5}@&_`RSQ8#r$S>zy@3_AAZ2i(>Y)N3td7UNVuo@RW-`fI>?hRWMGOKL{zWFs zvEe#Z7>n!Xa1o}%Zl%>${u(Lo(vhQNcMkfpeU*=Z5ns3~ z-$m`HIt=Q#Oa>Nso7oq2*3;38n{Kp~uY3DZI7${aGcwmQHphsbQP~RUsLI(tRr{Gf z#rElL@;U$++Lq0y2nej^V%7I8<=vbgk?!hQnuV~qPCf+&!RhWhVoTpg>g)ykyWyLP z1>&Ftu3Z!`qE1Vxp5x0}ac?&*Q22c8}<4`d*QdRPq zb#_lk;joh^WaL*1Gug6n+OlYkHhv03usPEShsZXVYICOm6PHsg3oEPG4j3|_!Y8i% z%<69FGdm(MKha+!_uz=Ex^H-#w&=^ox&k$#x{9^Y z&7Gt|T#lNLx71U;+i~rHc40ld|8YH{<}P3J4=9Qmaw|V{!HTXu-Ef!){Ah=$M4=<< zWRq!8{^R>9+yt+!7U(CrZik;hM%PnMxu*x79=N3k3=pvgt%JQP#|IuayLt|u5|Z*Y z`=9I7z{KZN>u3GRVo80$r{Y>8e;_*}S{(3(EDB|**cobJb0o;%`W0x>SzvYNO!oCH z3NMMM&k7j%)$(L8xR8Ab?l3I6<7jd?x4oVMEKsMagXIyx5QVK|OpJ&bG1A9hP4)<| zrRy`**0q6}3jFAX58-@|?gN~gc)xhv&i%+by1u=)T$Y5iV;ev7txgo0Xk{unYNwrB z$Xw$+9levYb%KY-`kZ102|l|yXIS+7?fB1Dl(w?u9|jl9u75s$grOs_1bMK60htZ+#6UBgtSQ}$aBa`pvl=VK@ZhAQQ-}(K7pOSPyc4iEa3`95rU>NnCx4Gv<8GWgCTQc1Uf~X^4)yXEE+!q3%-%>>=iN8vhu>aaUj7}c zvQat%LZZae{$5c{K1#yU$Q^s%$7-FRbY{dEDEjxYCD;vP!KNk!R zD{xx%^!MAhMYx*)bT2?r)vwA+FFggG9(a1->46)0AWB%OfC_*9A|PDJDS$-f?r`1Q z?GEtKkukk2xAi3%ff~F?vo@K@;S0*;g$H3K(Q#@VcH&@zhp|lU1fePspi!|qSoe*l zW7tfT4}-s=p#lan?%jubwciCVDlmiRu3}_xAJyimvoVYGxDB@iD7M&pud#l%`}9EM z0X2{LTC@>$os~SwDLd4&DEdPG^77lux7Uv)nKtV?>oNeueku@zfs)@|K7Pz~s^h;i z_J(@TsQ1z%gB@Fe4#i!ou@3+MAOJ~3K~%m(#s;oU>~Bwaj;tYUI|Ae)W(Uh(Gwfut z|Hqt9Bl_W^VwZKEs3QRms}Qj?xY10`b9*RO$nz2T^uW^tPY?V`9_TPN)G@Q>BV>T6 zyc>K3EdvdmBWk-K#>G28gvz+g9)JVe!9`&AJ!;v`d*YU-t=t+U$=?5? zYXe_{@TqPJ;85T>f*Jk!aGSf^^J^&ZBLYPhfP+Dey(T|d7JEJTtpD`D(*wVP2N=BI za6SY$GB(Kc3_pPl{F}=nol&k2mq$JRHV*$6=!F3*GkySA-DgZ8;-wMcxzzy>a>WLT@ z3}T>9c@y{T?d|2SkMFDSl_SROXVqc;`p%i`e%8)z?APn-x0lb)?;C56Rl8+dW0v)# zzkj|@U_^CatFP|df_es3Y>xO@bIHVr*&8*8fuKv3n<+cJB?3b-mSJxsdQ!IrKO(Et z2N;v(b%}cRTV2KSU~mK9BmVB;d5HQI1ElBG{Zb5m`2Q<_V_LBOm>*1C*2Sp&{;qsI z4L|q6jbCp(o)%x%18*XJB^@Q;V|v!ZHEvra zK#lC8~Qo*pN4Pf(yL?`(<&>9HNDRs;ptJ&PnI$qm zQw^-KpDK){GnNRNTmmQxWH+tMI^;yyw*o>sJv{RLALZ%rwT!=4_~Q>JAnn*)$#mYY zm5w9Ir;`Tlm{?O*9o^auSvkV6r;euw_Ilu_-DD3s=9+%ErtZORep&Gjd&4^yfgGJ< zXVF0nY$#TUVsfw!`=4`tR0Gj{`LP^>A6bG_&p!gc#Xqw>!ql}E`& zm5m*m@lk#2&Y>gPi`vTQy%Dp5H#Zqoy|P_xl$8F8<)JpCoegSMuT#J3-@VJX0ytX>}N*QvKxW(4FIku|&227niNCZ}lQxCyB?BY?RSBVd)vIV zufK9V20WsKrPJc=6t=?ticVyS!caz%mqtlU)pb4@MQs-SMeSvn@52AK{zb_)w=NJm zbL^VBJME)PCvKS88e71QQM-Agw$MfQLlojJQlMm@V+Uj@GE=iWL5MSYx@^>cWMT{8e9 z0zSrA8Lp?w1?7E>jx3MDT&y03WUk+(W59^(x@Blg6|RD7BTlyN>n_Dm#P=ckgzJpV zS#^BvNc~DS1UQ~aLMzuj%OBMPBb#%V9jWuE{qESM`gzN~ACShP)6JhkCSqu0%#BX2 zjtbmx=T*ng;>M5@qhjbv_5~foN37k7QqM^{t56lwi7)Xe*T#{61l!n|j8XeX#_2|j zEzYlDa`$3pbbPf%CZgH8E>)f_@;L0N6CToGcWmj~5p}8!?{sex+u?{!FK*zMv4D0) zH}1;L%s&desDPetZ?7-ke|*ZEB~@LtZ?tbC;F9XI@-_Rb0w_iS7qt1~^KAheI=}$h z5`h?9MhDifm>Cg>fh-bclrW%zpo#fd+1asa^xp0Oo@c<7^L)$zhUfl3U{)O`n}vQN ztD}1VpTl3Qbw7^Q{$@}lVsYRRw&%Y)pu%Nx?KObm%nfF6+>6oCS*NHEJ^YaeKMHvn zzn^RboDHk$kIu~z`Rb_HuC7fCjwkrhb?;N|njYwG%)4@KtI5wBqd8ILXnoXkWU|dI zc6eXBKOG$%#s`lp>+Q62&AIV2>8?UiI(=0-qEMIjm+uoOpiXn)wj9W?MU02d;KIn+ z8NL`onPo#WB`Eu`0yjDfwP^FNK#Xc(P=%Wu0YFqXbjER1Ci)WPdid*>Uw=e^gBQxo zp16z~_Y`V#uexC-=;P6Ospf4pm#cCf^NdlDF#(*pz#H9)p-`1izCD2w83+=|EYE=i zK#3Ry-TrzKRc{ObXGRBa;;jzKc)4F!|Er{}9WaG;MZS$}#QLjwd`cpV{a5o!`G>DX z3>cGh5TgQ96v&|Zjy9^fW$nGc{#e>|puw)OoeU^&{~d(L7$GLJ7nrEPovM8F9k70$ zU4j~z%ygR5pYwB5hp${FKb`gewn~b6)-!_=^Rt(ekYbs&$>4@tzRuxs?vG}L%q&{7RwbfzdtW^?HPkdsEZ(ojkhNsUG8c}b|ybt`f48NhCoF9>eTPD5V}Cc2#X_+?TkD~|Yl0M=P?Bn{ zRSDIsesxS=)kdd$H19=6r^}4$R$m&`*D3{o_(Hv67G)qs=c_n^4{F0bax`hx-r`4* zf1(3aaQzH0iN-YoO{(jvZ(L^@GuY5~PKaSUu}%hPEE@s>5Kte3E6OLz-jRDk*^lEi zVh;mhS=|U6@-bi=qPd+h9(cp=^rrR@uQbs%lfo+cjg8jDix?Y_4n{nHyDG zKTEj-29fVN07OT|{bjQDVqr$cmJk>m0|Y+!1*g6ODT;n1XZmncsS;zYoxiCuoGu)%qOn+$twncILNVXZGAFJ#@w)a z9Ga5l`IzNrR))^`ars`H<(LuD@nr>+ROOvEj*#IzW(xSfcwd|tw|L}i#;&|IsDbuA z85~E^3jI8(?$lGbO4oV18uH18T-k?K`KUp(h=DgZQ3h8M$=@(kKSMI zYi2*Ui~aJlt)VodfJr2ec69vlfVbS;OyW=z1TM0C8`C1}bni^B|I_I#*b%%Ig|u*c zuGfJT{~yCyI7S!StnyAoU&}`Th7sA!w(g*BZLiXgAtnmo&|j$@E60Wc3wTkK<-0l+GFZpTH$@mp6;=>|9+Uqlac2fRs89pf!C(ho z_r62`zz8rfB8xt}b-n(ane&1i%>?PR-}Q+DAel}CNo*tU;V02=W>ly!mne5e#L__f z5HLV61l#5QWMwYPcH4EOU&*-+&I5vWjD)#t4Y^T_kIRrgWOK}6C=PZw$Z-jNutymT z(cp;A*i#lcJu(Q)XM%@76O=;(*Ed=v$q`qjMkKTZTU(dG0b5a*`z$ z<#%<}FZFIe3KV$)9F_Nf!t!hbnK2F`SMY{eZI(x5R6D+b_cCT9WFSYi`Nwi)C%29E zxgBU%WIx*60S2m#eg^)S4V8}~pJH9*?>%-YV5S31ZEf=>ynUB0RV$?ch;Q7IfjVezE+m+?au(ChjzfGIQh`qu3IzWWl;E#l$Sep@m5b2IWKO*}T(15a> zlOoz!S71c+wZCeA%luNdbT{W)+I!Z~9*Dw!SULhk@Ve-?chF0YUQR910 zhR4YI$!y0Q9{0+>5#1wCfTQ#DcyO6&^JnH)Oo)h;fL&+sqqD*G`c?Z;J9b?G0~zRm zx^VN%KnLnjKn0g$UsM}PUI9iv9^SXi$I;Y7e`bGk8>-LP_srK>(p9!0@(+(k1a7Fg z<~q9|N$0-EZtczT7|U{fD4@f@47M#}Z7gGJ39nw) zb(uyv!iG??TTVIISmQ-pFkc6P zAQ;Ya338yiOJ>I!z?fy%pu}8f9lC=5uj|-<&iqiJD|>*Adx09+dMbODBCrv?lYMkL zp#b_o1-~!XxSyT?$2GjVQRm18No|}v07SImc9R(d;I3eOFN;6~wdu`(1TM?mI3kO7 zvi)jvzj63vH3ke&C}MOdAf^MrRNw_0Sb;y0ZxHBU-`f9N&#tL?6@hBmeb52ovhf?a z$2#+%qt9gCU;mjH4EW=9G4un$4U|a`Vw-otN&l`mOYC z0a31DABLy!-zs4!f*PX$M<-WB)<<;`$u+`A&<3>-QwuMff zU|kh+19gw&xu`-0I)Da)Ba8ooM3oiW;~MwT&!%$(K*(gVOv%}`^LU;pR)I4sUGp5B z^M%~wCt`LOP~kr;gLXb5+am%&RQ?d_i#o9;JH^KP&pAR+*ZFyS8FmsgGv-=rwuYV+ zRh|yc>v7f+ ze9+kIgD1eT*Ml4DXKafRCWbe!zHD1r6@Fkcij}dkTf^0=P4uwBXNAokP-CG$jA%~r zT+Em#S!jySTb!HfZeXJjBrmuX>O zgJN|k1_!qng`cQ|qnvNU`;OPyAJX`-CZm7{yQ7njqe?QC zEp)s+1&UZ%HyLOZreeA}VI!S<85!7-g@~XHQD}<$BVu1L{&hiv+3$K&f7E9GC_R-wI_>Q3tJVMe<8{iJk^a2?@$&NdcPfJUH>$p2IkQ2k zd?4P=NE^47%>WOyB}!~sg|I|nFzTq-DAa_33?-+*jx{(jmal`4qwOX&)vOQqC9*cu zv9byj*~|1eiV@QJxDzD;jgp3U)(%JC6HkESXpde=U$iNUz!3Zr9_1`hz{V4BxY7XL zQ0=3P;mEu*_hg|?Ud{{;#qLn9X0Te#~e`t$w$<*$#+ z_*eyun5`VD;`U=AQBEp4ZMZ(q`4BR2qw|8@GlsXSn}xYVi9i)_5rG@3AA1-aBYqh< zv1HohohYjKd!)TC7^c7tPZo;pb@j~~)VGV1LQq##u5_q65ukz$i6{g_g`2SKnyq19 z=I5{TYEya*@WFLjdw+fZnEd((KYKUjQ6D;hzayOwZ60`=P^@==!!}r)tmH z9UVqS#15(Ij&fzHPf4mmnZxou6=j>oD2yDdpJTT#?4p~_P9}4KOmKlstVJ&=Z znukB;aGP$z((W^0kDTY@@RL{Ov04L^-ILJv*uiz50LMYz%;@VCyP}f|!OW~Sci5Hc zYy1%*3BEn3dxD)J-b+e00@Gpdo}`W{NBu1DOp{#EwW4X zSLOA{@^N@OihY4)$S6VE97M5p?Z@+Pu)d73kxg}P_uAmbwe@egomHYx*Q0W9%nP0C z<=3cK81Vx0ZMCpbAuC?DI>BVmNjh7`!0L2>3fJ8U17V#R<3c3}jlx-`1tcX0t-yjV zV8I*FZoREOKcZe|gWhSUqX$Da=5d(AEpQ{=(bKU9Wn5^g_5D7FTI7X0@#$4@}XdhGI06_t#%J6AYVCHf!rG3YVB&o6jbyMvR0b{H87>=*$+s;?^yRH&~r^Wt)SVoBwv zQTa^Y2spuds=A#}73&K#MY4KbwnmivGxJ52S3}j!fQ%#A0@a+z$2iX4P<`p|00X>v zjzA0Cd{6UUuqu|t`6)a_0Td<^F&kJf{*U!4o4p>}oL$%RV-1KP^Md`Pd=j;Pc`w2k z1K8nm-#>qU`E~-?BO6d>I(3`Lb45w@b(xMnY( zNEam=jn)PV*f=tj<(B|Bw!Tm!Dth&GS8Xt&kdX|$=xp$|_U&2k>48V_0JAyJE@n3| z7-6!Cc~LPmydELFadoQ zl`FqqJRcAsA^;--a6C`Pw^uvE>-RYy2eCP(Ac$-YzmVJmAn69R7m|Nx9DSY&d-sQKzHr1wejj zfFs&i@P=Z`1_UP^>RSdKP6&Ad8C!4BXT7cm-d^8detgDfkazXmQ!Zj!sQU};j6e|v z92h7u9hr=`p#TVV|Eus31aOdv!GAk~&d10ydm|bj&e36ECV0#&hNWbQMW>guNQ$b$ zRl0L!6wnxz@7i%Kxr(X542^m5at1C&K#C*#ke!HfUT~flGF#ag-IGm@>>KKe!az7d zC<7O)V_K|HQct^90Tm2NjDQ%GY}aQQ7!WZGRPKcEYdINGF(5Lw#q84=1c<mMtYh5~w) zXAmWDU5)bzESY={V8+EfrLY(VN>+VgumkjVj-6fQ={RjtqfnN6vOX}Zq?&t5RQ z?~RU~^;7f^0ND#-Wc~Q^07nHrbOy27pqP&6E2q-`3@Pb&{VDU6JYY-3^I>tfx4Z%Y zJ1h_dU|>%an?r$}QSTK1!?KFSfo*VJjR?TN=MjgV@QExOeuDi(S-k%H`{yd@C^JQ@ zUznJbkBQ}eBPdY;6jk}{XJ6f-0x{H)uBHjWiwi$pF|S6|3uk4h6HNH)3bQWM|F_7Y z(Mi;~S}30w9aTsQhnr*|NOr=BwH@Wkh?0f6y;;2~-$pgoS-D~*n0%LIp=24b$P$dI zFZK)+F#DhbJX9eSXTR6EDWZ*j1^}SX_tqa7->R*y%^mxdU0d{XTRx|ven)vw=KJrQ zBgKIZ3vn?vg`dK*%sCadaspc>Cdk`w=aX9!CdguEzz|u5xj4u{U*son;{>LRjQ69; zs?%8z@bGeEa#&xG#i9OY)<>1M;|Tw(L5e!eB?@7=eE-kP4*XGU4=az7m(FX9ee8}c zKH_kd<#`%}t*SKzCm@d!mAdXKoaM^{96S&i;8BH^xUOg;6w&3Q#D-6xW6N#wtn+9d zcqH~m1$4OYx^i7#M@d4XP?oM<2MH>6hwDQ*1Q^tFkDp`K2LwzQpfDf=ule6Bhc8de z4PtWs(shsm!HVdms{jp^VLYOBc7457E(;}boyM}jAI_t(Ua%aK9JG45tNv!mLscG* zEv$?!batAh4`&k*vvmBn}!r?CW>gQ#A7OddrsvI4br=7Q0?eZl=BCm)DW6x>Y;R@}~!$9=NUt znB8#%_`%Nu?g4|LUY>!ZCc{h#0}cN9cwHT#%WZLv3}m~yJ?6#YzmrY;qri@g zkulqO1~iTgH&Fn`2L4NaEhlkK)T$Y_YdFJ`+gj}e{wbOuT)I={=a+_^uOc_>uAh6>ae)mP1> zXZhL#qwJ-v{^} z)WC&O#^%_{;)uqZlXWS%kwqMbo3K6yLvF#`;I=bRBLYEO$2AjtZoL;T`j>>2WG9hu z2#5*^Q9wk+%*gbh?F@E!SvS;!!#b=!g9=&bMkb4PqMTh@pok2`GH?Wc91t0;f>DLtE;^8uFkuS8+B4-aruAOt$9qG>Cqa&8Ti*|VYP4BS)Z>48V^KnIxU$gJ)` zk<4^dhtju4$EdH5y8kQC#OzWbE^e=4i9`$#d~R_r4SUfTfWR`=HYOWIdlYzrAuA?} zYzqZsAjseVhF$yX<70WrIcvmO8~i_mC#XlgblrC<=GDs13$>Y9v0BuLxK(CnV4Vo` zaF&LW9i5Ane00)D7K-9JkI0eH`O>oGOLLW&)A|tQu;5cqyuZ8wk9ifp?}y zCA{r0p3nxRgLNZwqhmkYtk@nEID+jcz{0NC4~DR)YbG<&76Tm-V}t!L(gxOzfDmJS zEIC4^j?SRQMdDKjJx~UZsij;FCRXKhP#*?XK+5Oe3%KHd3Wm{OUI=S%EwAgj?+AYep)>%`S zP9z{`OII8dycJ@f_4XgXikI|W~UsL9Y&2X3ba70_T^705nV~b5&e6gj@ z7Tc~-XKUisD!j#P&6pql{EB3zg$10FqH+&;8_4M3<6}K^#laEOi+@{N5mSTxj&i)1 zs`74RFHc-daT~no;)R!5gdq?zzo6%yTpr;%hqK_%QDG<%Goy1|CF@dvhWhdmEgnYh zp(sp*byS#c=(BzD^Me1bXXfv zXo$Cub=NkHC(67x=rA(By0Tk=hfdhkJU%l}QUMO@xzq<&BG!iLe`P}i8gf56_gvO@ zbq_MQ7_m1@Z#OJueop;9fdLWwW4Zq+R3^&Tk>vz&T?VW$d&4%m%mz_ytm-*{2`9LC zpQg6x*jrAFI?2$nxPDFLQgHi`ospf=;=l$oKupg{#~l({M)$u0C``7>@4@=Iz{R~- z9Np)i$z_a>=pzy5aZwDOEKKDHV9Rwxc{!GM;OR)(FA<`WEyi(BD?lTf0G-Xn$mDs- zUe5#7=C>+8(z2~>ZLN2t?R#`}?dOmcYqJyT(y^^;Lr2%GWhxd&me80%SfvZ|ZFpV` zW~F&@Q7d=W1||np$w}7CdUs|?oETavY_eX`Cs10Dn z%s~HSCz*7QZ&lz&6kf6{%1-PHl{>>^x}hkM95Xtw&k-n6&A*Fz1-VW}>vrV#Fc>=e zJ2Dx7VfCwdUI8UhA2Y^CWfR*K0S5TkZ&Wjtiw9Wd(`DN{iLC;`Fl}L2YyvXV@TO$ieaXndH4fU0yO9|cD z$1R&c-jwkvCeuPD$jzZPSgDUG$)}QyjxL?;m=k|iOpJ&j(LKpz)c;2?GEB$JKIz3x z@m^Z?Cp_@00^_xK?VC8H{je}v}s?){_|t{Q|XI<3w0eq0`#QY7J&$R z84X!~N6L8gE4#YB;ePJ{Q8FL~?XTwCQF{3KQMFs$VGcN;99~o9>Y1kGmPN!VBxN;6 zU%oHkF9O9FiemD}{4kKjek@OhpWxylC z3RPj?WAUL#x^_+)T8E+>#pGb7hS|r!hQGi&$E6){LdxjoaI(4h!XItms{5%*0$SD2 z!cDktmM0?$N#P?^@r0DwFPA5wWT&8H{ZSwWx9{~kfQOGsN4D~FGzSy>_kimxYk*!!Bfg4rlf5!5Wjo65+^3_rG zc|#q63}yp@3T#tW=CUTkfCsk6Umy!@r|GWx$TriR@JM&>pstK^pI$ z0zt5D1@54Y7x(fh3F%fqWXrtE>U2Soqn;NXkb-6Ke`jehTf>0~1ReZuXHnSyehP{w z40Qk)LtVVD{*v)<76vJNv7-`y`a*|aRO)}XJxY}2FG^fosh4vkmo2)guU6-mH@3)N z$0hi2)O=BTD!^idkuf66wp0Lyv>jL%ULJ*lWFaCE$e}h<+Kw~G;5jrLh{(cARB}-z zf9~5k7e)q%L@W@@!NH)z9uQ*7*j3{neX-B(1!YIJ`8QqI4$dJg%jO=IMF58COD1bN zJIo5V5#`;Ut}V>C;J$VNzzWpJ^r*7xKJMO|ZkGZq6tLkwb$|rt4+0t{=U{>R%tKqE zkP{4%u{tU=hU?;ZDE+L*Wz=!D5oj~z`#2p>JN4gyl$YOMzPXj$>5_5pQoDaox;b}uQrC>5^t>00t5*RukHw^f!|AEw8c9R_gtkN>YiNDw43c?Lb$F4n^1yiNK>uulYCwa}@i;>tIx*a3IUL39HQ=Me-TZah+al9pPq5x@a!{`q<=NO~@8u~RTl8?93WPBEPH06(#@o(9I!s38 ze{egzTqNlZC@9wj9=5QWj+h%q=r91nfQKzV9sjs~2gP{4 z+W!m^xGfP#!N5rr*1}8=Z!6Mk?m12mbPMU>;XEezEx|W4!DuAHq?hH zAFl|Es2CerC<>m=Hwt$62cbg%4h3pd$Gmo!8r>5Js)7Gm$}3=_OZ$&DaoHx>eAr<% z9KG@XS;p>>`_W<9R6vOW!mQ6crxf^50UTQai;VH1023c01Wra6B@wG78v86<#p*DC z;d)f)iI*Wrak0>N{b$Oz;kh-Y@p<_^{ck_^a>H^y{#7k#xQ&aiJ#W8FxjGzV@pil( zzqr&_9>wZ#J!@h>lCUHmI>BoI!0A z6@}j%7Hx<-n@4uIZ-lyR4XhurG)x8o4X-nDMC-Qaza`iQb_JKNE8GYS%zs;~HHw_;dYd6f4`^|=B$5ZGXlBLh8{snKC>OmF+s ziC1iE1d4bamFGkC_nM$bbsyt+nXLLh3Sn99(I4}=M}4?rfK26LJKyU%M+dJFJ!BgD z=GSww z*cR0s%YY098gPF5-`V&mP_JUcpgz9GtSn+YRsF93n~a%Z?M4g_zU}}@#2D!^Nig3B zE>sY>Kz4_c>4c7CUl=-HAN&Pfy^xjMnXz$ZkgVHe@WMfk4E)F$9h^%;op2J(>7zqY zMxSk+jkE$Tx}hkQJr#iA^%;avAtf%$AVyTz{QK?hG>s&a8;Kt#pDuy!ieh{@n)+rbM18T{B-1U5Ji2+lpX$2Z@Y42mn#6izs)7%9#Px-49Bu`}ed)Qy@q+4hVR74vzJ+p??C)W`M&R%yMKfqr>VvPdZso zt18rF@$>1#3H*QZrvNgrg^_TA?u38JJw33;11gsXvLaZwg9f%(e}A1rC?dv%m#O>2 zt})or4MFj`5vYOo8wldZsM>Y>AN$q`W8srh_TWC!U=9F(>tm5`~&v#LQ6kW`M?? z6HnBbMB}4a7#vnI_v@U;ga77n#Q!gSd1K76q()^l=DL+BE_R;z1-_5 z#>x48xlU{vffG^4is|WusYK&eg~F(i4A*tB$p*jz%SJZi*~T`@!x4qP`bUdw-dbII zwqlY|WNR>!qk9iT;VYbQROR3p$;+|T*WG@M%5{N_EacbPRLl#`%@)bwT*(+55&HxC z95OgKM+Ro!k8*C9Vg+XKwPzU0JOqmN?$_1CtbmQt$^4Xmdf-+b*fMu3wuAyms<0Xk zAK|<*84$zw?DV_?uo3H`Umaw~!eqEE=QMGBDq*OXIj~~?JJ`|*XNf{$qU5BeU$HhA zxWJbnGDnbgGQH%8oe?dRs=1ogt6$tRpm7rog=RA&n?(WzrHe|~#UH-rkrP+)=kD)RT-x68z& zIG(d_W_FO$=Q^-FVogLq#L)o7$Q=3Vve8`N@v6o!8%L$9V&h;P1xi$)2iLd%8C-Cg ziV@NYW64+|8Mxs#plx^!ffENFq6DRCE>CbotyYcgbkD<4wazAvfR1k1imi`YAu5z)Z1ZbFjX~L_^10Q5k=wpX?P!V!Ey>Z-y#o@FEK_v368m3kDoI$GMI$H}(!g z_SL@gMPC6=5i29R?)})3&!Xc46&lm2cMX{gtQ#GV5fEUsFI`YEW8`F@*!kYnNo4V! z?y`ft4$3>jW4=evC)S`0_Q8P&>_3LbAW*@%HLMNA!0^6u*a-p=3`(#r)M0-sMhK5_ zb`ndM!O>w9OmDFtpG#8AEgN*y;kiKU+6a>)n&XOT$(!FO--hd~)(se{in^;@9B8xK zz&STirsLZT*y#GRD>wRF*()D?ltR5Y7JMCk=mdM{9e_sL|9Dbtm zXGA$QuoFKMOCzF{cVgttC!LLq$#pk$dnf0!{?h|r(F45M_dYO_BV&Ga!c~~n(e+it z;Hd6@Wv@RE6#FA$op`Pg`)xlC4k+NG3WHJQs1tpn0FewN;XE9Xu8!Y3UmQ9zowl^h z8uU;{yL!DU+=Lk%6{BOj&2i1>tA4hL<2t6j+9X$C2U@5Sda7eu6>wpCBH*L~I=1+? z14y{u&PJLyxC&I7`*=?L$s5{glUxB+Tl{d(IvHDK)UJx|n>Q6f_I+oOUR><466M9)(NTJVP{1zd104j=P~1roOZ`TNV~%U@#* z4phb=E2A%tpG}^_P1OHCTc4l(d3xZ{Jiu%Twom2pFc}0PRCtJO_&vu(1b|dbk}bdq z>%@eo^TP8o_Xpa^j1Co!f?!59o>5*7yv_^`#ptN0ja$M^I$ss${xK7S85r(`S?=4C z{apQjW?5VVkhq3UHOAGZaAcEQ&7sk96@oI)eLF_=lC|ObM-qRcKDIAnfOPu7evSUG z;j%fsztumpX>^{h;>Le%x@L>6RwM2SY-7T$Q{v9g#PvpPkD86i9mvQtE|=c9U8 zPd=ffqNpQdZ$#?_&d6bYWI0cMvRQ9e??+X*Ajh=ttS4LE4R0&x$+DC zyuQ4@e7<~qLCg1%2hV)DbGL)-*{FzHJHv{ZLobT?RR%fBDrXD-1SKX zI7E87V2}5a^;E!xs;7Vx_T}oHZ{#%Lb`(fh^)(v<&VjCl9){l zZbV>*x)-8w8ML=^FIM3c97=)A4b#tn2%o$X*`V@#jJ)5O1rUKB(bs*HSBXbEzS}C} z?OmL-a@sesPraRcusTM@#O!1+De{qG;NZtDKEZF&{W+hO60toZu!FDLnra$@|Ni%X z%WW#=-SEGw!TvfR$J~=n&J6yto6*Uyy=kia|0|S78*jYsf8%fSje#0m-~RV9^k25Y zS7na~2t<9eeva6yeESGr38q1wm19agULZs2vLLxrPsz6vN^>P0;Ia?W1HGaXX1Hn85B(P1TL28c=? z>UF<=zAtPL2ScJnq1Q?v8uh>0j7JG2JM*@Z;SdtV%+T|5J_TjuCrbd@Ne)`IRWV&w z^GIxxS%1dLP@7b&&-o3kOkGob@^){*)`-A`%&y2!^K*60WXY~A_a-D*MexbXBLL8K zyKfb<0%cGR$Af_k1s+skU)UE0!bZR^d_(_?p+v@^6eB1MB)afJH}U&;26(w}h!!NXo~H z0xMo$zrB2Z&iPO9$7QqYk<1bOsJ|=#i~Ab# zT#WisfdpOPVD-!yv>|Gv!vf(xGAP2VjgC*eU1olGU1VzT^D7%O)2mKP!B7|XrA$Hq z03ZNKL_t(^h%-5+wz7S++9*Jy0%UVeDmR zC=jUHfY1H7WD~4?bj}NrfgAHZJPu^+9mCH2$`}t^KLgV;e^p=kw}fZ7Pb0PiGbY@= z^?W;raa_~a(PrkFZSvS5TSNVg*QR&P%YUYj4)kxtz~Hf9U2Y45CoacK392sRSuwB{vKLa{cQc`DeAn2iz zhjv&T7>0t34F)#w=vwX@=Rk>0UitR++XS5W&pkp}w#=!{*df5-ZDfgK(T-}I76!)_ z;~%w-I`yOrYA{XxsIR(@>MOTvE3?7sD~5nw?}7rZ!|O1Z(FGwQA4gyZZrT;&BeDnE zselyjTl7EM(qULcK#3aD$j22B>c}=% zQP_#Hfms8V z21X(#NC!AUM==nhfR73+*)y(OS0yV&nZLfzxfG(Gt@C|!eV{@mn7P3qhqEypoQO_c zF(3T-?e~RQX!0n#gz=n`mYOe|-Qj=*Gd8*mjw8o%kFG4_SdA5*!ouUjj1CpjWY;iX zhuN?6y#9ax{XbtcRpjN6R5>-e%!{KpHG3w{J?my+8D>`oL@=8n0zvNCziYLffgYVU ztNNSD-RE2jNY9_=T72S$ zqViWhi^`F;fdB+21D$PJCk|zX2e*-x9|d+)j03hQ3MJw3Sl^HO1eE!{%UK)O{DNPx zRyb@!fg%e0@xJ3cdw+X-`Jf|ABi07jcRw*Ow{^~bwR+vJxd<*OpaJ_Gl_PM#G991- z=c{6TSU!(xPG{|?oFN|0Qq2!_Vhhi4lQBTU>~y`HT(n|ba5;h!Dh$TUt=)e={#lqM zHeN>o8tT|s&LdH=J5=Ze>WJE?a&|=b8*7Ns-+C*Es2!t@}!{1j1YH;7P92^Lq za1v7gKVobsFvRRsdNQD7E4VZ=79HS&{mZ};=jY(|9ptc+Q~rE^UyYZ^emQ`HL5wal z108eZ2IkRUhTTnL^~F8wXSE^6x)tN03PpKze?OvqBje-tXL&@pk34rWX2~AbiSliC ze(xQZEzgYZNh<2-R|FxpwB7Bm0zw>&z|a>3uFM;YZxb-Xe|Ot|963BXc5Ri)xq&%2 zOgcMB@1AcVEC%l)uh*x7W%GQVg4A$KNJ?6PJo=~(iw>kjEmM{~=@Q5~86@y?8 z;IxG$vEJzF5St9Rn8)jq*$@FB-rk;mcE)_-M1yiSy$Gy^EOF7|=zng0Lv+qakh`|tPZH!?#wbOo6p9ya6lVHpA}IES$> z7!G5;V8#Xm5el?GkR;0O;d-zQ^t}Q+_RjI6>PN{pO@9W+b?=W+JF8F$)5m!>48Y*r z5iDnQoPp>zG3z5@e_$UtRK?5G2{%|4`C*G6x75M*vyWIKC+lzKn|pZ}Vu^(VOLw<+E^+C=A2;vt<+f$bR_MZQC=RBMcEWcbL^tc}&@ z$nCi0xGTm*#KKS@MFebcAF{BP4y$9tCk|FvorujbqVFrpDh9{-)RW8b6b@fO*$&&| zC?93)f#_>EGXyf`2XD|h48{7PLQ^<*#^wB;PeO@8Ra{;rC-pW*03B?9B)nv{ab{G^ z?ViIl@S4kIVJxfzeOrCO&gP2(tJD~G=0hhm1#MAmlc+BhsI%vu==yK(g5GT$(NodS zJG!C`qGDa}H3TdW(1_&JICMad^ZY)Cq-5bKR%Sp2hJj$)5!-{?K{*FN5csgU@#nYy zENv;Ug@KPOZ%71u7_-9eF%EIz#G}g`Iv$dy&b8q>Fz1E}ePI9uK@4MVczKkMBV%#w z@zb7hR&}%dYYZG^C!=tZSZhli8O-btllxKthdP0P9ed5MokvfAt@@&T8PkBmv8SU6 zB(46Qjq%q0JnMa34|GG3R49vLb9C4r%3Er#KdM(VU?WQ&x}`r60OBXFpiX3s%rEvi zWTpaGP)8>eWlKM9QRjU8Yu;J}U}Q`U#k$~Y{=fa6p2gxsmU&z*H`bMZaI=lF6>VFA z7{2I>PBc-WBm9+%Pd>RdLn8{&LH|Yo1~U;NAVYx`oeh6yEEV|R^+s458H0t(BD)x@ za38LpEpjdFZc%#Mf=qUP2;K#=Gji0+5c5SG!<6%J1^Kl!E_-%Dy!jz`Zr zn}fqLJuip*KLR)$4Dk>alSiZTXrWP=E*aL?w2$a!#tsbw<3A z0e~ZV_Q+QGG)%Vw4pf_?phu@|ZXes_lT?15FqHm?BG{4TLVD$ug^JUT@v6JS4+qc>~{N8*ra1 zwvNe1ZLtryKMbBk;Wi!H8T|OqxA&L-nG(gmRl|5uZn~_#ZFo>`21b~Yt(5pE3>LtCkt0G8AWf8sO{$%{}{-a=fljrP@oO!V1PmaE~qa8TB2|o#THTEhXOaE zP?!iLQ9uQP8eVR5@9ph>O?f!{2Xk|9cnq^VFg#_NpTyp9K*QQ#HU=|1u&(C?VbDT> z8217;d@e^ff5ciCeOQbD=9q`W+EIBQR49tdFXcc!CL8S>F{}EA`8Y7o2j<)0<7el4 zZ9Z`&0w>ioan!#Z`Mp3BC#Yrc0*{8p931HHy8s;7DJPNRI_!;()4zgDmfSN+?D+&Q zzQV}RvDpS9w0JISN}CWSx^$(B_Dhak&XvGcYh$M&tt}qw;k83gIi$ zV*T^`bQCPJHmbZD4oY<8syV4Pw9{OF{kCFyWMAZcBRdj?63>%3A?V!BT+Tb!R|f-`>KTN|NjXtz*9X^-%feN>J!b#%X(}!k0Vj%Ka~FUE9T+7IHJL6uW6P%E z8iP2h6E|plT&FtC0`)%ujZtqu<-ei_IN7Q7K^<*7zcAicOpr(KZ-;3y!rs`z2$^5% z>qMdMw0Exg3)Z>3Mn?nH- z(>yul$M9Su4xBLiA_5@ry8Z1s{crr=+nVO~i6xT#UESkH10Pv_4!mFa9%Va6Ls+VQ za`{CLkI#Q!K0nvFIxy^Hp5x~PrSrWwgBtVvwfrpSTpOxCem@~Y!{zw-!Hf-lHoE`r z4S0+u8bu}z`bZ^ywQ^)`xcuD#4l^V>8D(@bKB|0_6p=xW=P4rFNAUIa{pIuXe1>_Z zX+(CrMZZu@%M*CufzuU$r$SmdJZ5>wQC^w|Ai39Qb$|}Htx7673UbUZbg|Hn=VV9u98UCf-31XLleVqV%yI%mIR6hU+#vO=kVvIOh!EItYYtAFYgm z4+kfrHhuAj^RFoGynXU?=O=>5CmBsUhdaWhYE$6=GDhPt0QPV zhliIrD1xWBMByjSGC@Ga!)WXj89&;V$0hbbkRjl0E3Ua^=Ia5K!S)M}@Yi zrkRPTP6(Ty)n`_t{f~?f6}rN?HjvrDW!66gGT2V+KZ7R`SnJPDeirT>pRM(yqk2uB zI&v1<`F#1$%j+p*?Cv2d>;OLMwaeVt>d0Hwi>UGn#6UX!d~?j%`hVpg1|hbB5>dUa z`bNg&7Hy2!^Qf}f$t_VC_hn0h(k#(w6f{8}m_V>5*0(E`?{qe%r_Rh7DjnQaZougls&Th^P2QN$qCks`{MOn^e za0C4D`L=)_uHQir2RKZR3Nx4&VB>iYvQtrTQI{nRtxiYjj9nFagE}fE2g~CBdp7!2 zKU{Z~ABA;rUzo{(<6YTav2`l@y*vwph&CD>up*POw)uI`VSaEu1{4$+k$qWKKI+O> z0EYq-ussYhVRi;3q z0Q5i$zs>6!$iM(YCj>@?zPQfFUcc_ZhJh9K!$61Iq!=W4GK-o+3`ig&0~4WoT?9ig zoW;t$%>HI(h=V2=zJg4U&NH9_GCp67onXhV&p-Ts)kbwcWx!(goT%FEma9`)FeF7W z8u8>6f2U%e4t3g!+k;6;)e*FwZ(|z!Q-}=%vhEj`$)26)hik6E7_@MP2PYm?z(>U5 zFxwvr;P{zxYg9+HMu`-wuhrcf?<>evphZ=_-1NlO@OoI^{$3ukpC2!gZkEgZ80p74 z#v-Xe6;-bK@Tg<@h``M#gTs8wrw;KwdT+Kz#>j|3i|q6h>vP5ev2qMS$!Lu#562_s z=IFf4nSR{?KvtCRSG(u)(@)ONuCpA19mwJ^Py|5^12@jkvhNdEVb@G9OCZW@jtb=H z9`pL=xAzrG;u_DX3JA!OU(WMxdaR$X&+nWX=s4eVXW&Hnmvu$;5V$bV!T<_pYk0^@ zblpJ~Y{OX}e$Dkd0Akm>2~AZ93+DhqumS-L4q;Kp$7UzAWMMBVD~h_*8w4@Z+wSjU`=%=$jusRpzKSi;@mD01ZNMtnB`-2%H4oJ8z1Sk*)VO>~H zwGlBq5bRjy{wY@nGdY;8;b4c=L$-$h&&f}b-GP9I$zz%Szj{W9P?d_!v3JZz>qp@$ z(R0VH?_nv7yyH};i`%7+lI0|(=jX!=c0~W%b_{q}1f!oo76zTP6g=VXHx>j#?Y82PYzy0r$20&5Z9|?C}bWy+}!{pkTCft03WyvH}XgIa|__0Y(_nuztR4# z4Ut~7Cu_U%S;h$Ij&0QjmS02XNc$|Cg}t0#4j=90q{#NL`ddzGiNag5qir3i;P%Z1 z4sRLF(P27N($I>X;pO(SwD1}RM(&-^iswSLq2f4I1U9@3kB)V~gHK6OjErfnz-cEK zzOsI9%&ZLzJMr)qW^?#;1%gDZgo?d^wqVGKUFXwKj?TA{0S6<1K?Uq607Qj-RL>pL z^XIp3@+elWn*keH+ZoU@%Dzzji5B(|*pYoT<*%pe%P9jAOs7_x6STYfva9;IrH<+G zdcVKFz5Merha`;tbit0*GXNMHQTLW(Q-pIg@=pD${@%^dhpeH27yc;g3PFnFYXK!#K zQch@Ef!B|=R#QHTPFO)%205l@%K!J@|8q!cKui?Me~-Qx?kTTg+~>FS+;f~Cz3s|xim4I-M$8C7MvC&ot$~Ws_DufA=RYYss$ym+uwY(f%_p&N zxLI_~>+mxIue*;;hXWE-Xv`nqeuNAS&f%eKwtN@{Lhz3|83pCA++Tdi&TwXjmp#g{ zWD$U&fQoF5}gu4kD`5SkK&p>#IE(SnP= zOm&WY9eJjzu$nF_!`k7njCm1xnOqZX$`Wpl*zRqqP?jhesOygajlEz-XRJGH3q?a# zzy|tDF)O^h0x?Xk0w1HX*$Q5aj&)Za!4L(EL?I)rKdSr3`}YZKiE?qw@0@>LynZJn zMfHvKyWaU7cMenGJRdHn>SH?**wOX#Ryh^A!XO9(3@V2R0~&4*f+QI3f{9Ai-}Ccq zKK;dh9PF^aG4BU5Ks;ndu|JU2!TC5)9zhOfcBm6tGMVW_ z2Ik|ie&YYT0gkU4YEn&VF8`T9jp@ZCP9RYs4bzPRCzUXJW7Mxt`P+M-V%Ye6aLcNk zB0MK<-=oVGv&5v)z08Sz*Nd_>@G&puF0ZLND)A?Wni!yAOb7+AI#|Ja94v`WJyBsM z?5WH)w9#1_2zoGU!*qK&CLQJQ6*WJu`65-Ih1-yA-tddE0VM`d@TMvPH0FC^rjc^O zQN`eJJsy5y_3&@@nU(oZXLDQKlPdSdS^qi)d0sqSUd!6x5R1!sczG-<$}o7L$`n8{ z&&eG4sC>h1MeVqp>Z{7h!Hk;GB%x~ItpEzMONFP5gsiv@`>lY5iot0~gkg0Sy_Tq00PPR4)r#@xHlCr0eewqp!~(hyS3hT;KjT;Kb{3JDhjJ%N0nW zuDNdi&GorX#NNo-ou0A41R1%X7ayJSZ(vbrH3RzKYcY%)x1VEk4hL{&<=cJ+j|7>3jVvGVn3XJeJBId}}M4!?4jW9Ygpn~VR z{U0$o{NAaU9wT$E+H7E(thZB_>qJ0?x~@Qis&8FT!}K7SU;qL#F#PWb2w^)AfUr8Q z4}UwO0?SPgvOdmz{?E%lAOABo<6ww~;{5(Tm$ChBQxwW#Odi0o7#Mij!52Qj8+%ljN|#Mkiu$j4DWkC9VYST=fwL?A6bANlY2S=|Tw)%&r^U*(JsY#-m> zDu0J3BlQzS@RSw@KN##-LUG@In}Cqf_1PY#OXS-sX9u%4OwS=nMlUMwhDHT?xD5zsWNek`8FpfLxJ}-sfebG%e^33G-!YfD zG^Tgii5*h^)}Y4hqyJ2R#~v_a?_B8CM<9e5ARNNtC$L~b(hMA##_x1IE`uuwP&hz> zpog&okPPBrt873+_!lki97@KDFoGPr>{DyHC8e{8A8N6604wAYvS*MFwtVe!X+ zj&_I&Gc`=ny@8Ao2Ss06M|9j0S?oHY>p`Wq_b$sLR2byqjC;Y zS-rGB-xk1QM1KZoTsz-}+o=+OR;Qj|9~ltH`p;lb27IVck@nJqA4- z@aVEN6#GMk#b8^U{1p8Vo!a7c@VC`dxjZtzc9|ZL9EaIWHbA&a^!!De{9PJ_3bOq! zk6|W}jOXa^a1@hcb_cULn4MWAEsa7~IDdy9Lwl3sn79001BWNklvlUSkDZ$kP^( z<2A-K>vIc>qg{(ffl_5$Xnz`V67?!Ty1aZH86D22mN_O!lmX08-Hzykg+=>*N5Z3x z>Uc!CmMM6_Ku5;f;PuErlPwQV_oI#b(0`e;I9$A#0Q6YBpdA??v**sW?e>PIRR034 zkHaae*49vyY_1h6V}`NeK#f_TV+3kA7DmS2=+{HnJ*hyAEi4)ZH5{{o)4J+8o}JU9 z^Uj_7XtRaVJ?dvqeZ|n2VQ*9+Dls0;h%y2;GEf747*OE3I(7!aMpy(i+Hg1tzz+Cl z_5^I>lfzwf3|hxP$LagW#=d|r9+L}yInAvDB>*padlyR5H9acup#m}#sBoZ&^R4Gw zW>bMIBLHUhoF0J@>_-_K54X=PzoBDxv~%OR|EZyR9S|q2vbD^P-`N51fH)}t2?vBk zdj&3*d(4p?;+P(2=lW;%Mg~$;VJ_kG=FrpFyG=pjDwMB{^SS?d>cUtvmT9&A8mgii z8)!SL+o62{+<=HF>MJo)utE^d!fV27V2{`B%?hBu7G9%e>3ck7&H0S+)O;NP`l@PWs{ z%ntn?3=Q=iZ57yHriXslj4_;A7Wy2ghi8w!bj*$nh)@t?FMDGq=95EJ@PKupBwLU3 za@>wtQ1<-Wyho2C3s2b#X7Idc=&hj0MmzpDh|80}f zp&=U4pSE@TVOMEpFd)J{bv|l``YhCh+pn1!r<(KfHHZ;_g7*Wv{>dOnXMLPl8SS}q z3{5%t0y|_KvT_0`)?f$xt;fP_kaN&Pfr@C$3=$q^R;NJBwSbH**ZFds4I{&awm9FJ zZb%l|!eB@{Clhc)g-bJ*2ak!_96DAP`r_Cfj=fQ3V`=v=$=2e)jwp+*0yK2Y8G|Ey zJwAV3g$`EC4)!}6ZrGnEc?b*Y;agM7W+LHWeMCD=ADe}{tQ3)s~y`Ph1F55I|CGK=a?1k zh4IMFa6UCPZP>OKI0;MPP#3o0?*K_q7J3{EZs1|3*-})0Blh8gMXFSkN*{ceq?gTv zOsjvevmbx%ABY(ZfoolB4$xqY4ee*CJKVNtyS=~%$NDOGp=s;f%hv4t9guN)DeM0{ zRt$_xmtwa6u-n26QNJS~2KF-|pw0&8S}-#J;yC7pzHB?27>l#pvQ`y%F#~L5KthZ` z!!LMTBkhL@)PNt=$k6iGAAuQc!{cK%1%}zbqS*dSS0%j#SfKt(~04Z}+{J2EYg@Lkm@ z=<)R_GRb{gKnxxmfDHf|u^;x>q8;iO^l)X>oJT^Xj_Cn^PRnaGhXJdBpT*ht@TVzd zqfEn5xL>EO#r1f9L+DBO7wLO@_7<=_90&wkW`gWtuxvRGXKfq6!EB8PKWQiBI}As0 zG4C3qK0EQZvU9o_Oxe%q`~T`-Mw-bd?q??^wLOfk?dTl8*7?BK&9!{~EBVpuq0;@h z218@Cwpk_>+>0uJB78f+P%cOR%*a5x*ecA$0Vd4!;82Yp}*f`BHRbeyB~B_O${!M(1Q!@3Nsd zR)zyK)-O9_RrpGXv!5>~1_bukeRTFiCLpT17+coFX=m`5DoqR*hN5k^grUr^Gs1ty z)Zlqn42`gJpx%~sQ`@ac-Stto`I*$?_CgLQnKqdeCAGSoAiI#Q}f>KkPH01E5E@AAuLGwVV%|McN zechhYi&vp8nyH1eroayio?anp65u21 zM!FruB zeK0nhPRGrE|1baR1)3V!=d}kk>n}@%*iX!e1?N3XE z9#+Xne}m`j0FtUK)7S`~0bYPXqS;r_XWT2J&e`j+fCdLNJn(P>Sb_&&urog0e{By& zmrX_48|ueBL@^fpi@^T~I9lXLk6;VnajokMAVvmeL^c#`%Hr6|)Hto%%^BtOl6g76 zLNzSdzXJhAdKz1RgMJRJL6plSoDO<-Bei^J#4GoRk4*Lw4PzHr->yGJ2 zM`MPWF*;Y2xsio#Y}LGQK*$W+gU9PaKpZO~V_F(T)KNo_7^yGP2{g z>PPo07DsgrDew`1#D}|^RT^4^qj0&?;>f^^5y*k@!7jTm^fF%5n01zI-HP3a& zY}w-ft$`T@IdI*@Cj%jF4l350WgptZ3;}R}@Dc655(8acj!d{prQ5-wD%vlP$7y^- zp9oP2_C;GqN+$~tgdcqPj`6Y>X%EJtbPW3#tYD=`@4lrF`5$t3!m;5cbRb8*3hX8#H)li6@ZAbyU-G4LsVHswtyWBXrS-- z%-9&b{wm7pL_e!AlFZlW{O2|Dwi$yX3;C#ebO1%zXXVS&WC;`LB>*S%JNL~tY&X;P z*o)|PIL!^6tz&Pr-V?xMROeQ+Vu;KDD{aLAUc?TuK~{A~KmiPoh4leIVie92^|5gn z47FLYHJs1m+#6^a6m}~Ud#X}S>}~0BTJ0Qg|`6YK==yS z8&S^TF9>a6jSc`M?eHGONHcv8XmV6nZWd-3ZQLI+qx;SX7*J-Wik4K)KB%Qm@$d2)@AnYRWp_N;Gv zCpd=JWm$(bG;AbR`uy_saC`G%@!MWxB%=w+zCJx1!d^pHWS%sN(F1jZ2skQAt~8>XDG1MT2TNosc z!4WT8%)ZdIaiLMOFSN?Us2CjF?^eJg+RlW0ppO|~!ui}AGQ!}Bj-di8uE((Gz0NW@ z_*i^sX;U?WNTpff!aG>YVJ-Hl;VJ&wbU=y%AJH=F+lQ$1zM$EGo-^=6>(ws5t5g7n zYc;ZWVxtPMtbmT^`xdgY%s|3rGJh`YrLD37WSZWHhQ=Jr77$|#_z};P3M4VijyAth zJ1l~b|MEN51I9bUo(S7ZK#OayjfQEl|C#w@K*k8T$>!n4qwJEb z?Wj)&;Eb3gv**gSkINo^S!fIUa;y#j3^PoQxL0K$2>Pi2i?EpmG}NEAZ7*5JFqQ0c zd6|=N7WV*y5Gk)0%2=dS)wuOoDXP!*qX#GZ(ti&CU|a%byatQI>25HCBVJG&h4SY4 z0LF%XE-$f3Y<8uZzLmj&H6DQ&VW(h#0turGCnL5*6$&#NUzT}g)MtNzJI{#X7#a@r zPzDCp(*YNb-mFZIJ@>j%8wDZ4-mx&E+_5LBEF;=R$1-AC%!qZ4=J(q59?)3s$YAA1aD!*&E0HT))~+rUlZT=NjUojE;=8!FDrYFbPOvH9zhiTKEgLGf?Bm6j`-B zwyA)P8Fq&|9{^^Y-)B15(eG`i@39ZI2QxQR%L8@_RBXw}g8RE>Uco({`-Cl?6Zf|t z&a$<{n6_wlRDWbtAc(R!Dyt#J**`*E?1lW?$9&zo!&Rr`_3lQFqzHX@?8Or zz9`>cM`I=*NA|Dqo*VLy(1t;c+3{A4hxaUAJ3GI(w9CL0EuXc+*hlsmt7Z$RG6G57 zavixZ1w351OE$JFyhSxJg2}<5FHx^$?M22gLAad9k%hNp8B^F^=L6t_%eBneI3PmH zI0K9JtG3y`V{fT_+VO@+E7U6S7Qv-Gic3?~jm{egV8R${@&O}4^tXrRp zV`1#|*6!Za_?(>){lyl{j~vW^i;ss6P@LPYfs$a6bg<*(pTUz;-3e5I@B7wAA5#xg zJcY7Uz{NID*4;dpve-JS$KGK*vZ=1lk>7cO1h+ zIW%Pz=0f&a_{te5K4OMAwg;XEr0EfUS>K~yJ00|JAcfXpzqPx4UQ*S%{Vm5^LsT^7 zEQ1^w(*t2EEp!(PaMj!}91@q*Bk3gpl+fvv&7 zgN7$F%Y&I4T+V)3e~q7+ClG0!-Nl_dZw_#PwV~{deqx-2L${ph*RmZ^*3nOv+2LB_ zcuhZ<4u&!@Fb36=$n+*gW1e9@jC}m8?`)ThnNhJ%+}c%alTk>GJCC9d1~+EKTVM0I zj#wN{Tr|crTOM{F?!GOlTQL^~Hu%0$g{))(q7^Hny?hZ^Vqd@#RtINm;dTl#FyH~^ z2A9n+G)4eNb`I!21j{}67=rR1Kn?{rGU_q2G-4j5kf^+zu3k27J}w}|Oz6oYfg1KD zK!6U|WMLm$84>H37B39SeP)?V!lufA0v{M(1%QN&0v|K?A*Yw&UcMC22;UjVv4=%8 z6M9k!c5Y>9jMA;nfDaD57-?jT>SvDGwQLkva3BSP7}0Ourro&(P*~fC1%ncO9x5E# zOTh|ZP(UbY8(R^W*C%l{IbULw5C#%ll{s=L7~i~tevFb}(UAzcJRk1vsD zVJQ5~)~&RkM(f)~5;S*uvbvm|RL%$Et7{SZ3$tzzn6nBJ>14 zl|GD@rmTKzn8_YcBb#rQf))0o6s?)ir?!uMwH+OUjFA!TuC1lPV_|ki_I+f(W_X+! z6@4An><_i=Ha$e-98~B%Gvnf9b7E{TfWr0M7IV;kwC}#&@ZH%O8Cc=!^f+tyOSOL< zDlgI$;-hG8)!TB_kE zx(F;_8g*4jivdKkY%Q&v!dWytC1Y<~%Ia|X8~uD=gBzoLZlj?rSVGq8XiG;aXk(w5 zAiC@fbetWM!^27o1iBuM?J;{!tDOr`2^(gFqRkfJQ8ykAQR(Z{^*DNeG1tDY;mW8- z8%;HfwtcNTgM&8(K&}pOD420NG2T`jv^oj+-&xrG6$C=7f7Q3>G3>p7ezyC$0=fcK zxXM<5VypeNjz%S3>N2r_)e)g7e1Bmu!?7}WycI|>18B_7UB`iT3|wRkl97H!RiDLf z^0@UOsddZRil8Vcme&*+)l{!6CJJFw4LfoA8ZqWg0;9(l%ya5r>PM>oVX|Z9)*@_(}B1;U&!EIIRVT zpG4`o&?p!gr#YO5qX7KqefIDa1y2xy!eGk`W22uF4yftnH#ff?US9qLBHul?3>-wkr!^iuNhcAz79g^zW*klg?!~h{N7KE$V{t{qg30(V!t&BU2<1XLDah04z6!~&yWcc_tykVgGP#uo>6AcG4S9A`iWgCOQR zgCwJSWv0imz7OTv8~`#{y8~@mh_o#Su{pMUcMJ|UFV$=cn~bply^hl}v+H-vZX*CA z`;$dQN%t~4T(*^L?3EB{)&@+Fs{(cVCalK#t6vJkP%*=Ec;2=sHTQy zI|&n-#j2O0xH}Er)?An*#d~Lt_K4bNqGf$lsX_s@FJ$eK!*c4 zR@qWm-{S~iFw?{eSt|g-VK7)v1x#pPLsXcZk!f*k5gXkCevIz7BSr_nkA6+{P!|1; zKB`PCTl72djA2nymy%Y6Nk?;a^$c=kEDmkA2hgZ=)%yEE7aPq!`PuCLY&aj|Nxbx` z00--9M5o^a+~CR1W;=PW;V2`$jIEjrdoF^XZT`9|Fq0+ZlG$g1s5644Rf^%QKnZ+R zfJSu>7_qxDoe$i9(7)>gz=MXZ^atrl+he59;lPR!fWhEJ27>VMp8-3zoG+Rc1#L3` zq+)bLThzxx7_166o(kwdU1@{tCq_VYrN!)yU9(*B}VMiJlI&W?`H{ zQP^h&VmKQGK1TcSkpm^`@R$tvU?4($@fi9Vbijex8VXQwKg#f65CrUv1@Iu2$Fe>J z^CJy?VUS`glf$(|2+NVR!3>ZEn2>&2`TczdW(I&q1zI3GjI)U~av?C*-~IjJ%j2Wh z@^Ba5tma3i>CqlE>r}%W!m0zbqkcv|H{&}_Ajljz-xmTGb>(Yeo@g{_{+dOu8;3d6J`cLnHe|R?75FP zKm}#^9EHDdxJ%f&%qcw1EF+2wIhkF9D)Y)r2A0`=X6&+5WTzL0uF0Wnj=k&+mk}lW zYsd=9@PZcMDcH_H4xGr&f6K0CleMLK6Pk}y!gea$d7MjtVp(=|_LB=PhGrWWd4RHSFU%2QV0H2pa$l3(#uo!ZA1+phC9I_w&ud>_A`oxnh81jE@m(W5oJcUP>tR#Q-H|V92o@ zmw);8#}fV$)eMwu1wxkflyDXs){=#>WNeN0ko|H7Vw{z|d}&!zAVLbzV-?zxg{+Y9 zXs|qx%_X{4fMm~g#GnUfWO0B;@6)q5G?ZoaJWj$|VB<7BxV>g%>ADrCa1;$y(U6u* ztaKz!o(Yg<>%0Z@$kNP0PXoFf*mof+Y7^sf!lhROINHsWLR0pfa9i7CKi+1U8(aIk zrhO#{8vSHH+@fxzr*X}3{`9q8f%gCy{FRQ0V?Af85fvu@H&~}*MI9#lhr3UQZ%;jap#wRXoq_L;vBAJbfC*18 zk*@g5!q#9tj=P(0D`rOT`;^_prDq+15xS1fA8axa(Mr@*{i|>&kI!W~;r1D;!=-P% z1jwk)@r>1>zyo|ZanG$xj4FI2tJ@NC;>0{PETmqOO1N{~GV2Rg4xHGciJ`#7=zcZB zqUiV5HPhq6?d{?BZ}9{0q-UXaGvEjS3XD^Mjwn|ULqQMjm-}?S*xzZ*PvIrbw~oPu zyC}nh!(pO5>u@ZgFUJrT$1dUU7G{blvtnUcRADcU4dR#^x5sCYYI&IbU{xqf17pbm zjkeympBf{>wMBUoE0ypT94iJn@H|t-g#|E<&%VXh0V4@m0tUzFd=XL72|W(S=5TC~ zEQ3pR9cN;t+GYj-;Wn9;Mw#Ucx$qSh4s~Eh zbO(V_6&dBBG(pn;{$Kvrt`WX9n85?D0E|8Vv|ez0rkhs(NNXxqIQ_}km|otMp=z)= z-4z#hz=Dyk=Plp{_qlHt|9Y($)XTo>syKE?d;TA_cKBMt_o%~<`}KoptjWQwjO<>+ zHpCy5brOgR-H7tz&VZeDwkd$0QiaLZQn`#mk2VoU|7TN$r~H zXQ-V6KODQlffzdfS>{HxbF7bk4A#I&)yFJ|qD&08uUZ)jRAj(~6AZglxHPnd86FI3obJ0P zMu$$@iI%EO7P``|zhhPv{%qINF~w|rkLO<0v$!ZTHWcV^ho&lXSywV$Fa|ni*c?uC z17l$(hO#pF{f-$QoTUY86VK~S0Edc)_65IdB>G(_$<`Go`|L;T1un1Jw9*i`YDa%@ zX9`ZJ-odOsLc3|a^jKOImvuhf`85NcIgq3h2#Q9H001BWNkl!+zQ8^h8$5;# z@Nng;5SORh!^gY+gKT!YL|Bv`YNP&`;W5(UU|+M^8vrf>AVKH}{Ma(9Ge4uGXUbgM;S9WcV-#K;&R2p2(njEMmf zF4HjpU+LYZM?oL{pi@IpgX`pZZMqn!2z%J&?fik&gT#;9y6(a=mu zU9+HIP8BO-&%Cuw#=3wV>udDuw1YwbG}zARYRu?m=$K~0QTBizvnf*CxW@6ihL4IVu^dkVHp2el=FbLna9ajH@ZA9wS$IkGmHB2+qpjcV?cv+YFAF=v z`3yT{Z|Hbnla0xN8~`bJ?JE`u12Nnm+AB~49S}XXj^)8&E@$Ay?WaRyegL!(k#{-)zVBWUB(EOQ?!4Ih+7#xK|bjPeaqua$O6DIfllZF*-O+t<&ac z2a$l012=H!Z8cjuD&VQWgi#e#NeY;c+lp?r{^LbI`@Qa5 z@bg-0Qu%SLiwsCuj->z}E*vCmXBZt7xS;^T41j`uF;)Nx{E0dRP%@T?vr_;gTVvP1 z>j&+TnI-m_eR9SmdHJmY8QSg){5T4kE~2K<;Q8}e*h>RG+K>DWjxb2F0!)tWVZWMp z1!zDhGznQ*`6E9K{D?N(@AKyc~x9KnoCywn`U`{ue+;55GB{w=`fCB0Y_39hQyv0}F6eERDBfZoKyp6fCS}J8?g* zMnAYNIpJQTZ3bLKUscwJJ!AP-1vU1pm9|kvOw?y!;0PpP-+O_KjFGceIH(2(#^Ki8g{rWfV{2%et&EP*18wH~ zV8njLUTv7oq2PqtDr+NSdMqNMSvpvx$>Xq;n~#SGNy#=963tS;y7mfugw2eg=gfL) zHWuEW(epUHh@8L=03MEJ$^b?dUJ^c4FjVKV7q|#NGi{x=hNf)gG0gxX{rWv+;X!-H zxY#N*I$}`tzP;FIrjbzr7o&44*Sa_X7Be=Zezd*{8_DV_z`=nBZ7mI$p`j}bh%jK` z+6M~+EDSeJWrTz++HhNCS!6ab24zbCU{FR%#pr;4_Lupsj->(~nLqeoV8i)c7#;v6 z5Yj@xMznJoTLK(712Q=5#bi=hWq-*+R-!}J`k2uDSg|;w{So9?01gxWk}^5K(x3$^ z!HlDw&CcQ$eIhdpvaBF;N+u)T~zXDGOW{8XU^lcnh{hM%(}1 zKY(5MNxOd>|Dn~BcIHRa=tO_3`s#;l7G8p}eedJ)R%`TE>t}{7qUXgP2FNVC=Njwl z`kJ{8M>d+m7Frp&{(0;flPkcC00?3Ou%Wh2!y^lWnH4a_*qjE3mbuUu2ZW5kk-f)K zAAa#*dOk#^lhX!)Q2bk<3=Zc5VJa*z8te-VUs2)F=rsdDR$#}?^WiKM1$rCp3ERPp z6}9u_!}6m%*cY+Qf*2WdBTEh2d?##*EypTL@!HSn^B>|-9xq#eK0ck*1GvyED4C`P ze2u_{OY5`saN#Fmn}wG|olE&TlLpp};TqXZ^0}>n47PJ1N1yk(2F7`+)Xq4pbT+n} zf7h_BzyMcQff))=WFaS2C`!x;;DgiW;6518Xvg3fa^`@Nr~_aE03!CeKdh(v#dy#M z0~YN%Jc29n-7zp`z=z6){Z`Bkj6Y+6xIUtdf*l$7;p~{9(R?vW1B?!&txeb*5It33 zWEHxSusGVB0d!;+#DvxdXI@$9j}WMFmYL-k)}q}y(8GZoD;^X{iZQh3@RKSvnfAx+s>~@2Xt-G6SThYxY3rB>tf}}G z_Oom#?U{S?v88s+LQ&jtQ$bM(i9%E~+KfPsQGJ#|)(MEt-qA)cQwn&5U(Jk?^)&)J zX2+;@8CxUlGVKf(hSIOwlMcs-1;XRlqMK2Lkn}P1Fk`2UG19_N@y;r=WOlu;8D_GF zp`mTC?{qTS`VqJXV4%qW;`fj&W%E;o*olKu&1bDD-1~F8p1AV4I2LT!`6y*%a0ITEZnCk&< ztjaI|)$E9J)$NF~S*?%EFS9))6lMuqxxZi1(yHjGW^UnQ7U3~2ZEdCXk%^dUn%V5W z!hlD=&v_68?MFHuv-dBx%YY9To-(7)F=BB5*kC3IvpbM+g~5$W867<5jK$%ENVE6C zwZCsoC=^@9=-~2p@4CHNbT}Ho@s^B@?BC%S*n0sr+CAf#X7Wb>F=j771vSv_J+1rf zTz=YppA}#YkUgO+d)i!MrnA?v0wK!cV4WS8>1BC`Fbpqdb7Xex(}5cy|i)w7lfEZ{qVsXqM@$Puep!-?sSOr1CKMP-3V1fq0^_lGgjv!qu10I?gr9I5f zLs0^Bh)tOr8R((q*azT{fg2gCV()dt{;F&z4k*#G3fRcfzw)?eKn@3Rpe)N|qHB`@ z5dB&^*TbmvG-g6e01#BRdygNt%R*F6FZ<_WpjimYEFj^)kmzr={oZ*i7|{AWvL76n z!TqopsD_z<8PU##Bgnzv1(!1`q5=ZKCpHH{WT7SuvPAn)A3Pq|qhAeEaUg}-MB5Ai z>G!%*`q;3$yZznwg5C@cVTe=$Vh+?wmn;WBR<+skaf0} zv-Gmi=^(*U1}o5ywrkht5%hq~wb&f(9&^l`qVsg+8p^^z#|ZSOvawWkV0EZo$E8_T zX2MirO{zbr+;zzS$tV*GG&=AuaRzX-^Xl;QY7CC|^kX^`0n{HcKML5PVLn+H$hbooZe|5Eg(=!NU6AAJOv?E9UZ97Y=ZFQ8-5bK!)AQE`(z+}xdJoZlSd5g@&vH16 z0v-;i=nwC;;1UBE3Q$zSqgbCRTxF!s5f7(-e!D+_5u$6JQ5jEtZ%aVVAc#}<27uu} zjS2`+`xxIMCJL>M09GO-<>f?dt)o?p&%G+1*ZY_jYq81JQwjjdTw`Zaa}IZiLqJzGtd^`yW2KBsM~(3Y9w zSEX&$aFIUFQ;14cuS^de%WDA@qx7oEpuo6h*c;HgXvc(7xH`asnH+4d<=HV(M#tIu zNmvQjJNCtL-Wz?545V{fEHUPn2Aho_4CvI=L+myfhyC>Yk(Qo7B?;58HAa^j1T_q*c$EKLfU6`%R9|W z#JqnyV_iU;lo=hG@)kfx*nYbIa`^oCu`xLM!=bY|^gH^^m>RA;9#HU&2N?I|)^p24 zO>MJ%rafw(s`Rb(D{v!=6Q)KBkU^jZ^fvGyZ+#MzLjep8MOnSn9K%%*YJya+9D34^ z-6;j^-s{yLpW7w0Wfa2V+RuU=PJ6>;NYOrKGNM#$jSNhHwuXWf(SPR8rGaH8#+I;> z>KKmvRO}1~W^k4h^y|P1)!nFUuXzq;zFg=?J3dE2PX|1>@Q`RT0w0#SO4!NS`FI2y zDnKIII0i+<-T>pl+3fL${=;v83)mEdi1LcT!S%2Uz#`ZgTbLl|f2NJvVjulh=iVsn zrxiS%yo1rm=S$c0;H>~djW8~He=&^GX|=EU1PB-8v4uu`<^!Mi4pTX*WqX1-){vn zHb;A2y*#ZlyF}S*fE=UQJ9d+6GYVzldIcx2&o&G!u-GWR>ljpr1N&j;?qeKs0w5z8 z6AW&I4SxJ*p(ZXhtP?23ct@csrw3+7=VxLr-SKeon_=fbj{e~5cs;k{zFCLE^*0J< z!3!Ad0@Mi5Ay^s#X0*PKERL4y?B&a%t&yFh9J(?at^!{QctrWr%flhGI4<2ZQ%Yao z`Lwd=Khx7tAj4%#2_Kg-G{Q#}a)NpWH&{dzZNEJ|AL8uFQnZc$h)a)0*4}ApIDt>J z2e8rSeF{lY|8JeWq>YENj_`P#4n_uKz|S(@L=%JkXCWqMYx4ZOq;J*mkan&ffevox z${1i!u)!VYDr`kT5VeK>C}-9LLPo%%sKQU2EsvY~RG@?RqfKl9gk*L+e>9!@Khy93 z$2YS%&5+ZEp;nO)COK1>4ibeNGsci%a!8JImgZEB<*b|!Gm;LNLnh{wFrqB)kci4D z$L~HreE$Kz?B;r1*Yo+f-ye{3Gxs4brH8}xpvzFSKAx0*t=li$z>x{;&H7c6~1LPa{t8*?qWl zTe>JpYv$|W1F_}DDN{TtscKDDRc=lOqGd*_z+mGCr;V$9TBuqZZENAsfVFsQ+vEhL z*fyaE@U!`4_6gkoF^T7|0H@;C64&4-ldY?msWfU)aqn5Bog1$jS~$#UHhI!v?=RN` zPXcbWd_m>6ogIUoMJ$c_O~|c6xiXCp#s(VL0Z@adD)JgCy1TK2x=vMvSb?}al_$^B zs$#|02h3of&`r40iEJaJLwclmD)qZtaLL(eeXv35IlKO4!!U?a?#t0H!%#peiHpVy z5C^JJr0FZVTKT8E(a-*z2WeLm8J)(P+bcdw#@)f>#i zRIPb3N`DDMyR4t6nXHwAci`^2t)#TkL93b~aRd371RTE7soX>rJ(T>OrLNAb0G*@i zi}n!#z*r%u0)yiOB_NXRwwxx;88JQgJozZ3i}AtNZ{_lN6I6 z;39nC*jJ%tYWUF>{ZB!Vpg##)Y%kS}D zVA}xSOz+OSFA9#~#|0>_?$Eu5S1US-&Z@lSKx0i8ycZ;%BI#mpp~Zc<$O9MutCBx= zZ_1*YZtKj@V^sfq|KXRIcK?X*3xqZ7m?;t#3o?8!Zxuf!GZdD=g27M zK<>+u*PiYM4X7&m@DoQTj@71M%LN@!xsb}(^vbc}k-&z7ya&f*`O}ZnpgWpjo?pWW zP<|3L%vKb(g6+i^c$m8=giWgByxa~m>r|%&3}8J>5ZkY2lqZox5@Tm5yRKrUC6qt8 z1-{#&qFZO#?LEzB)%aBrt=MyJ$^0|ZFoPMBYkW)-E zn?wzCb+wy%d3CNEO3r#XJLahB?FRhP)r}u$QPFITa`pdAu@+vrnjLv)?Q{ORPYcW%Q(ni8o>tZQpu;L&2`Tb1h8xw?oQE{{ zaxU7MMPY*qQkb;;q)-w>w8n_uh2=6JVW6;I0{TvUyaqc87G=XfbxnR>9$B`hTOX{0 z3Ofbxr$|*oKAT}-f->hX?ChOl1EeZhW|srS3=uDa+c1?p4F~e51Um0k~`S zx8>Bewdt4@a_zxoqu7h7lb2aPQ0hH7lz6cvsC1LLx=5P_juKL*kA_LCxX}*dud9Vwv$X1$B3k$6c$y zIODt+sdcn#fYs(2m^*Lu2NB_*>A)uw2G&S0q(k8`FtrwLeo~&4CF8`4j5ByqNPrjj zz*2(QriY#VFKyw8tYPja)#Y3Qr$FbiPuNfTDul zxxuHRrw*ECPy;4J4~!J2f6Ca6>$qgQ_+6l}A{`lK%P$FfV z*mPCe;=nM?=W#%85*&L=1uJ~~q@8^7*w2yqN0HbXAIj0^#GLLmXFnTqhM53ogghyF z{TM_=^CbAG%&0z}!G~rQ{H^59`|-PFz5-8h104e4irHMdV~g2Jr_?pm&&RLA8B2>( zuTLkcX-lyWDVtD92LM0BT=wR)v%GJGpWGW?U*vfy_Z#qjF$?2vQ&+Vn*HUxGjSaOa zbdlR$*`FUK$hdr~Hxkw|4%B=?qe)SgK1K%;zay8XBg_#L@Jy5|rA1VQwJf!y3Ub`| zTX!_TyT^{(tj;7eAG|K4fEB-NBFWbh2N?B5T(_&q?gal;jEyy}a|`aeXi+-U=#+ zK3>5BqPcSwG>>gE4s-1)>?HB`G})Ub%VMK}T|E|yp)9Nr^hPS&nbkNSSz6TeHx{W@ zB|j&$neP-2zFcN*WT~)~n0hAcD1ecoM}Td-;+wN*>2y%z*OW3jf4-e74AlkL?C4Rf z>2|0en|QK6y_I?~TA$4dfq){q*tgzk1qWc2$D7vUom`(Q^_oo8D5o5n>H>llv+MM~ zd~D~?ah2i?XE+8ZLopHVEM=5ZTD4Q$BiV3Nmnq;L8Ne?mo18AJC&o)K*7LZ+5){k1 z{{BT8_;1Y1^HPee|coMVqN5qkAXszTDh5*}1ay`YJfg?0$dAnS+^S z5~Q8(AzIpni`z=TS-6oB!lX9?N)n&7`kcGf^TTW1Kr3}LI;R}E0tZT1U!7D?Jhf{* z$PYH4XgJ(_?W&3Y78nS`tAoZ5|`Pzuh zKL#LqV*~4?hpDkXh?z>b#1iV+EwMY)_S-0M6Su>5s`z)dy2oCM4PjV6mW$?pHVa4;=Fis}J=bVS zoM#)9r*@D=^N(M*Z?}k&GE$tP+}Ldy#xgJ!i-q3vld2$f1wq`kBkNYic_Sx0T*aaH zAiG65`~~kh7v!^Qr3IR7A35Yqle+EF6C+CzW*|YxmyZT0Es?V4DDH`Hg)!70sx7(p z++PX+c>WORPRtHEwF`pJ(?t2be+X0*#!UPmc=kO^cwv}+H+wL{Qe=GXMNC=QU%RLt zojoX%+hKgMPpyKz`t>3A=0wZ%&k*fyDtyf{lS~|$PJIgB$-f@f>S4a3 zGe)i4y&sgCC*;n3*3u%j`}()NAC5m2)8s+jv5S@SDcW%_TGJojjTlLsCVqu#Tal7+ zk!}&*ntY#Mzp%QWGJXaQ$j6j_f5Ptw6ywC!v2RzxqVD7aL`VpE z9C3bZmJVqq!Uwols;zgZq&lLTU18H@V1c^2GEv(?e)Y=jQk6K>$2#?^)itVeg5<>EI~| zqQSeZ`Xleg3-vtI6}t*c?P>PduewlZ02JXB#-&cYLt(yLZ7D7Ok=Tk(l7kV1a�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 JPDHLkV1huxGX?+v literal 0 HcmV?d00001 diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/player.css b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/player.css new file mode 100644 index 000000000000..b516c95565c3 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/assets/player.css @@ -0,0 +1,263 @@ +:root { + --aspect-ratio: calc(9 / 16); + --toolbar-height: 56px; + + /* Set video to take up 80vw width */ + --video-width: 80vw; + + /* Calculate video height based on aspect ratio, but never exceed 80vh + * for the video height, for example when using short, wide screens. + */ + --video-height: min(calc(var(--video-width) * var(--aspect-ratio)), 80vh); +} + +@media screen and (max-width: 1080px) { + :root { + --video-width: 85vw; + } +} + +@media screen and (max-width: 740px) { + :root { + --video-width: 90vw; + } +} + +.bg { + background: url('img/player-bg.png'); +} + +.bg { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-size: cover; + opacity: 0.6; +} + +.bg::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: -50%; + height: 100%; + background: linear-gradient(0deg, rgba(0, 0, 0, 0), #101010 70%); + transition: all 1s ease-out; +} + +body.faded .bg::after { + top: 30%; +} + +body { + color: rgba(255, 255, 255, 0.85); + font-family: system-ui; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.08px; + margin: 0; + background: #101010; + + /* Make it feel more like something native */ + -webkit-user-select: none; + cursor: default; +} + +.player-container { + overflow: hidden; + z-index: 10; + position: relative; + background: black; +} + +.player-container, +#player { + width: var(--video-width); + height: var(--video-height); + max-width: 3840px; +} + +.player-error { + text-align: center; + line-height: var(--video-height); + background: #2f2f2f; +} + +.content-hover { + --content-padding: 1px; + padding: var(--content-padding); + width: var(--video-width); + + /* Set margin left to be half of the remaining vw - video width */ + margin-left: calc(((100vw - var(--video-width)) / 2) - var(--content-padding)); + + /* Set margin-top to be half of the remaaining vh - video and toolbar height, but never less than 0px. */ + margin-top: max(0px, calc((100vh - (var(--video-height) + var(--toolbar-height))) / 2)); + position: absolute; +} + +.toolbar { + background: rgba(0, 0, 0, 0.3); + border-radius: 0px 0px 12px 12px; + transition: all 0.5s linear; + opacity: 1; + margin-top: -12px; + padding: 12px; + padding-top: 24px; + height: 32px; + display: flex; +} + +@media (prefers-reduced-motion) { + .toolbar { + transition: none; + } +} + +body.faded .toolbar { + opacity: 0; + margin-top: -80px; +} + +.logo { + font-style: normal; + font-weight: 600; + color: #ffffff; + display: flex; + align-items: center; +} + +.dax-icon { + margin-right: 5px; +} + +.info-icon-container { + position: relative; + display: inline-block; + margin-left: 4px; +} + +.info-icon { + margin-bottom: -4px; +} + +.info-icon:hover path { + fill: rgba(255, 255, 255, 0.8); +} + +.info-icon-tooltip { + position: absolute; + background: linear-gradient(0deg, rgba(48, 48, 48, 0.35), rgba(48, 48, 48, 0.35)), rgba(33, 33, 33, 0.55); + background-blend-mode: normal, luminosity; + box-shadow: inset 0px 0px 1px #ffffff; + filter: drop-shadow(0px 0px 1px #000000) drop-shadow(0px 0px 1px #000000) drop-shadow(0px 0px 1px #000000) + drop-shadow(0px 8px 16px rgba(0, 0, 0, 0.2)); + backdrop-filter: blur(76px); + border-radius: 10px; + width: 300px; + font-weight: normal; + padding: 12px; + left: -162px; + top: 32px; + display: none; +} + +.info-icon-tooltip::after { + content: ''; + width: 15px; + height: 15px; + border: 1px solid #5f5f5f; + display: block; + position: absolute; + top: -8px; + border-right: none; + border-bottom: none; + transform: rotate(45deg); + background: #1d1d1d; + left: 162px; +} + +.info-icon-tooltip.above { + top: -105px; + z-index: 50; +} + +.info-icon-tooltip.above::after { + top: 80px; + transform: rotate(225deg); +} + +.info-icon-tooltip.visible { + display: block; +} + +.options { + margin-left: auto; + display: flex; + align-items: center; +} + +.setting-container { + overflow: hidden; + white-space: nowrap; + margin-right: 0; + width: 299px; +} + +.setting-container.animatable { + transition: 0.3s linear all; +} + +@media (prefers-reduced-motion) { + .setting-container.animatable { + transition: none + } +} + +.setting-container.invisible { + width: 0px; +} + +@media screen and (max-width: 760px) { + .setting-container { + width: calc(var(--video-width) - 370px); + margin-right: 8px; + text-overflow: ellipsis; + } +} + +.settings-label { + cursor: pointer; + display: flex; + align-items: center; +} +.settings-checkbox { + margin-right: 4px; + width: 14px; + height: 14px; +} + +.options-button { + height: 16px; + padding: 8px; + background: rgba(255, 255, 255, 0.18); + border-radius: 8px; + float: left; + color: white; + text-decoration: none; + margin-left: 8px; + font-weight: bold; + text-align: center; +} + +.options-button:hover, +.options-button.active { + background: rgba(255, 255, 255, 0.28); +} + +.play-on-youtube img { + margin-bottom: -3px; +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html new file mode 100644 index 000000000000..f48bb6eef269 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html @@ -0,0 +1,14 @@ + + + + Duck Player + + + + + + +

    + + + diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.css b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.css new file mode 100644 index 000000000000..de2906b8f108 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.css @@ -0,0 +1,954 @@ +/* pages/duckplayer/app/base.css */ +*, +*:after, +*:before { + box-sizing: border-box; +} +html[data-reduced-motion=true] *:not([data-allow-animation]) { + animation: none !important; + transition: none !important; +} +body { + font-family: system-ui, sans-serif; + font-size: 13px; + margin: 0; + height: 100vh; + width: 100%; + overflow-x: hidden; + user-select: none; + -webkit-user-select: none; + cursor: default; +} +button { + font-family: system-ui, sans-serif; + font-size: 13px; +} +ul { + margin: 0; + padding: 0; +} +li { + list-style: none; + margin: 0; + padding: 0; +} + +/* pages/duckplayer/app/index.css */ +html:has(body[data-display=app]) { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} +body[data-display=app] { + color: rgba(242, 242, 242, 1); + background: #101010; + height: 100vh; + overflow: hidden; + padding: 16px; +} + +/* pages/duckplayer/app/components/Fallback.module.css */ +.Fallback_fallback { + height: 100%; + width: 100%; +} + +/* pages/duckplayer/app/components/Components.module.css */ +.Components_main { + color: white; + max-width: 3840px; + margin: 0 auto; + padding: 2rem 8px; + position: relative; + z-index: 1; +} +.Components_tube { + max-width: 750px; + margin: 0 auto; +} + +/* pages/duckplayer/app/components/PlayerContainer.module.css */ +.PlayerContainer_container { + border-radius: 14px; +} +.PlayerContainer_inset { + padding: 8px; + border-radius: var(--outer-radius); + background: rgba(0, 0, 0, 0.3); + transition: background 1s; +} +[data-focus-mode=on] .PlayerContainer_inset { + background: none; +} +.PlayerContainer_internals { + display: grid; + overflow: hidden; +} +.PlayerContainer_insetInternals { + gap: 8px; +} + +/* pages/duckplayer/app/components/Button.module.css */ +.Button_button { + border: none; + outline: none; + display: flex; + height: 44px; + line-height: 44px; + font-size: 16px; + padding: 0 20px; + flex-shrink: 0; + box-shadow: none; + background: rgba(255, 255, 255, 0.12); + border-radius: var(--inner-radius); + font-weight: bold; + color: rgba(255, 255, 255, 1); + text-decoration: none; +} +.Button_button:hover, +.Button_button:focus-visible { + cursor: pointer; + background: rgba(255, 255, 255, 0.2); +} +.Button_fill { + flex: 1; + text-align: center; + justify-content: center; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.Button_desktop { + height: 32px; + line-height: 32px; + font-size: 13px; + font-weight: 600; + .Button_icon { + width: 16px; + height: 16px; + } +} +.Button_iconOnly { + width: 44px; + padding: 0 8px; + position: relative; +} +.Button_iconOnly .Button_icon { + position: absolute; + top: 50%; + left: 50%; + transform: translateY(-50%) translateX(-50%); +} +.Button_icon { +} +.Button_icon img { + display: block; + width: 100%; +} +.Button_desktop.Button_iconOnly { + width: 32px; +} +.Button_highlight { + transition: all .3s ease-in-out; + transition-delay: 2s; + background: rgba(255, 255, 255, 0.2); +} +.Button_highlight .Button_icon img { + transition: all .3s ease-in-out; + transition-delay: 2s; + transform: scale(1.1); +} + +/* pages/duckplayer/app/components/FloatingBar.module.css */ +.FloatingBar_floatingBar { + display: flex; + gap: 8px; +} +.FloatingBar_inset { + border-radius: var(--outer-radius); + padding: 8px; + background: rgba(0, 0, 0, 0.3); +} +.FloatingBar_topBar { + display: grid; + justify-content: center; + width: 100%; +} + +/* pages/duckplayer/app/components/SwitchBarMobile.module.css */ +.SwitchBarMobile_switchBar { + display: grid; + border-radius: 16px; + background: rgba(255, 255, 255, 0.03); + padding-inline: 16px; + height: 100%; + line-height: 1.1; +} +@media screen and (min-width: 900px) { + .SwitchBarMobile_switchBar { + border-radius: 8px; + } +} +@media screen and (min-width: 667px) and (max-height: 450px) { + .SwitchBarMobile_switchBar { + border-radius: 8px; + } +} +.SwitchBarMobile_stateExiting { + transition: all .3s ease-in-out; + transition-delay: 2s; + opacity: 0; + visibility: hidden; + transform: translateY(-50%); +} +.SwitchBarMobile_stateHidden { + display: none; +} +.SwitchBarMobile_label { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + gap: 16px; +} +.SwitchBarMobile_checkbox { +} +.SwitchBarMobile_text { + font-weight: bold; + font-size: 14px; +} +.SwitchBarMobile_placeholder { + height: 44px; + position: relative; + width: 100%; + > * { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +} + +/* pages/duckplayer/app/components/Switch.module.css */ +.Switch_switch { + margin: 0; + padding: 0; + width: 52px; + height: 32px; + border: 0; + box-shadow: none; + background: rgba(136, 136, 136, 0.5); + border-radius: 32px; + position: relative; + transition: all .3s; +} +.Switch_switch:active .Switch_thumb { + scale: 1.15; +} +.Switch_thumb { + width: 24px; + height: 24px; + border-radius: 100%; + background: white; + position: absolute; + top: 4px; + left: 4px; + pointer-events: none; + transition: .2s left ease-in-out; +} +.Switch_switch[aria-checked=true] .Switch_thumb { + left: calc(100% - 32px + 4px); +} +.Switch_switch[aria-checked=true] { + background: rgba(57, 105, 239, 1); +} +.Switch_ios { + width: 51px; + height: 31px; +} +.Switch_ios .Switch_thumb { + top: 2px; + left: 2px; + width: 27px; + height: 27px; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.25); +} +.Switch_ios:active .Switch_thumb { + scale: 1; +} +.Switch_ios[aria-checked=true] .Switch_thumb { + left: calc(100% - 32px + 3px); +} + +/* pages/duckplayer/app/components/InfoBar.module.css */ +.InfoBar_infoBar { + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} +.InfoBar_container { + padding: 12px; + background: rgba(0, 0, 0, 0.3); + position: relative; + z-index: 1; + border-bottom-left-radius: 14px; + border-bottom-right-radius: 14px; +} +.InfoBar_dax { + padding: 2px; +} +.InfoBar_img { + display: block; + width: 20px; + height: 20px; +} +.InfoBar_text { + margin-left: 5px; + margin-right: 8px; + font-size: 14px; + font-weight: bold; + white-space: nowrap; +} +.InfoBar_info { + width: 16px; + height: 16px; + appearance: none; + -webkit-appearance: none; + background: none; + border: 0; + padding: 0; + margin: 0; + position: relative; + &:focus-visible { + outline: none; + } + &:focus-visible[aria-expanded=true] { + outline: 1px solid white; + outline-offset: 2px; + border-radius: 50%; + } + img { + display: block; + width: 100%; + } +} +.InfoBar_lhs { + display: flex; + align-items: center; +} +.InfoBar_rhs { + margin-left: auto; + display: flex; + gap: 16px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.InfoBar_switch { + display: flex; + overflow: hidden; + white-space: nowrap; +} +.InfoBar_controls { + display: flex; + gap: 8px; +} + +/* pages/duckplayer/app/components/SwitchBarDesktop.module.css */ +.SwitchBarDesktop_switchBarDesktop { + display: flex; + align-items: center; +} +.SwitchBarDesktop_stateCompleted { + display: none; +} +.SwitchBarDesktop_stateExiting { + transition: all .5s ease-in-out; + transition-delay: 2s; + opacity: 0; + visibility: hidden; +} +.SwitchBarDesktop_label { + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + overflow: hidden; +} +.SwitchBarDesktop_stateExiting .SwitchBarDesktop_label { + animation: SwitchBarDesktop_slide-out .5s forwards; + animation-delay: 2s; +} +@keyframes SwitchBarDesktop_slide-out { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(100%); + } +} +.SwitchBarDesktop_checkbox { + display: block; +} +.SwitchBarDesktop_stateExiting .SwitchBarDesktop_input { + pointer-events: none; +} +.SwitchBarDesktop_input { + display: block; + &:focus-visible { + outline: 1px solid white; + outline-offset: 2px; + } +} +.SwitchBarDesktop_input[disabled] { +} +.SwitchBarDesktop_text { + line-height: 1; + &:hover { + cursor: pointer; + } +} + +/* pages/duckplayer/app/components/Tooltip.module.css */ +.Tooltip_tooltip { + position: absolute; + background: linear-gradient(0deg, rgba(48, 48, 48, 0.35), rgba(48, 48, 48, 0.35)), rgba(33, 33, 33, 0.55); + background-blend-mode: normal, luminosity; + box-shadow: inset 0px 0px 1px #ffffff; + filter: drop-shadow(0px 0px 1px #000000) drop-shadow(0px 0px 1px #000000) drop-shadow(0px 0px 1px #000000) drop-shadow(0px 8px 16px rgba(0, 0, 0, 0.2)); + backdrop-filter: blur(76px); + border-radius: 10px; + width: 300px; + font-weight: normal; + padding: 12px; + left: -162px; + top: 32px; + color: white; + display: none; + z-index: 1; +} +.Tooltip_tooltip[aria-hidden=false] { + display: block; +} +.Tooltip_tooltip::after { + content: ""; + width: 15px; + height: 15px; + border: 1px solid #5f5f5f; + display: block; + position: absolute; + top: -8px; + border-right: none; + border-bottom: none; + transform: rotate(45deg); + background: #1d1d1d; + left: 162px; +} +.Tooltip_top { + top: -100px; + z-index: 50; +} +.Tooltip_top::after { + top: calc(100% - 7px); + transform: rotate(225deg); +} +.Tooltip_bottom { +} +.Tooltip_tooltip.Tooltip_visible { + display: block; +} + +/* pages/duckplayer/app/components/FocusMode.module.css */ +[data-focus-mode=on] .FocusMode_fade { + opacity: 0; + visibility: hidden; +} +[data-focus-mode=on] .FocusMode_slide { + opacity: 0; + visibility: hidden; + transform: translateY(-100%); +} +.FocusMode_hideInFocus { + opacity: 1; + visibility: visible; + top: 0; + transition: all .3s; +} + +/* pages/duckplayer/app/components/Wordmark.module.css */ +.Wordmark_wordmark { + display: flex; + white-space: nowrap; + align-items: center; + gap: 8px; +} +.Wordmark_logo { + width: 32px; + height: 32px; +} +.Wordmark_img { + display: block; + width: 100%; +} +.Wordmark_text { + font-size: 19px; + font-weight: bold; +} + +/* pages/duckplayer/app/components/Wordmark-mobile.module.css */ +.Wordmark_mobile_logo { + height: 44px; + display: grid; + width: 100%; + align-items: center; + grid-column-gap: 8px; + grid-template-columns: max-content max-content; +} +@media screen and (max-width: 500px) { + .Wordmark_mobile_logo { + height: 100px; + } +} +.Wordmark_mobile_logoSvg { + img { + display: block; + width: 32px; + height: 32px; + } +} +.Wordmark_mobile_text { + font-size: 17px; + font-weight: 600; +} + +/* pages/duckplayer/app/components/Background.module.css */ +.Background_bg { + background: url("./player-bg-F7QLKTXS.jpg"); + background-size: cover; +} +[data-layout=mobile] .Background_bg { + background: url("./mobile-bg-GCRU67TC.jpg"); + background-size: cover; +} +.Background_bg { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; +} +.Background_bg::before { + content: ""; + position: absolute; + inset: 0; + height: 100%; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.00) 0%, rgba(0, 0, 0, 0.48) 32.23%, #000 93.87%); + transition: all .3s ease-in-out; +} +.Background_bg::after { + content: ""; + position: absolute; + inset: 0; + height: 100%; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.48) 0%, rgba(0, 0, 0, 0.90) 34.34%, #000 100%); + opacity: 0; + visibility: hidden; + transition: all .3s ease-in-out; +} +[data-focus-mode=on] .Background_bg::before { + transition-delay: .1s; + opacity: 0; +} +[data-focus-mode=off] .Background_bg::after { + transition-delay: .1s; +} +[data-focus-mode=on] .Background_bg::after { + opacity: 1; + visibility: visible; +} + +/* pages/duckplayer/app/components/Player.module.css */ +.Player_root.Player_desktop { + z-index: 1; + position: relative; + overflow: hidden; + height: var(--frame-height); +} +.Player_root.Player_mobile { + aspect-ratio: 16/9; + border-radius: var(--inner-radius); + height: auto; +} +.Player_root.Player_desktop { + height: var(--frame-height); +} +.Player_player { + font-size: 0; +} +.Player_iframe.Player_mobile { + height: 100%; + width: 100%; +} +.Player_iframe.Player_desktop { + height: var(--frame-height); + width: 100%; + z-index: 1; + position: relative; +} +.Player_desktop { + border-top-left-radius: var(--outer-radius); + border-top-right-radius: var(--outer-radius); + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.Player_error { + height: 100%; + display: grid; + align-items: center; + justify-items: center; + background: #2f2f2f; +} + +/* pages/duckplayer/app/components/MobileApp.module.css */ +html[data-focus-mode=on]:root .MobileApp_main { + --bg-color: transparent; +} +.MobileApp_hideInFocus { + opacity: 1; + visibility: visible; + transition: opacity .3s, visibility .3s; +} +html[data-focus-mode=on] .MobileApp_hideInFocus { + opacity: 0; + visibility: hidden; +} +@keyframes MobileApp_fadeout { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + visibility: visible; + } +} +.MobileApp_filler { + display: none; +} +.MobileApp_main { + --bg-color: rgba(0, 0, 0, 0.3); + --logo-spacing: 185px; + --ui-control-height: calc(44px + 12px + 12px); + --additional-ui: calc(44px * 3); + --gutter-width: 8px; + --gutter-combined: calc(var(--gutter-width) * 2); + --outer-radius: 16px; + --inner-radius: 8px; + --logo-width: 157px; + --inner-padding: 12px; + position: relative; + max-width: 100vh; + margin: 0 auto; + height: 100%; + display: grid; + grid-template-columns: auto; + --row-1: 0; + --row-2: auto; + --row-3: max-content; + --row-4: max-content; + --row-5: 12px; + --row-6: max-content; + --row-7: auto; + grid-template-rows: var(--row-1) var(--row-2) var(--row-3) var(--row-4) var(--row-5) var(--row-6) var(--row-7); + grid-template-areas: "logo" "gap1" "embed" "buttons" "button-gap" "switch" "gap2"; +} +body:has([data-state=completed] [aria-checked=true]) .MobileApp_main { + --row-1: 0; + --row-2: auto; + --row-3: max-content; + --row-4: max-content; + --row-5: 0; + --row-6: 0; + --row-7: auto; +} +body:has([data-state=completed] [aria-checked=true]) .MobileApp_switch { + background: transparent; +} +.MobileApp_embed { + background: var(--bg-color); + grid-area: embed; + padding: var(--inner-padding); + padding-bottom: 0; + border-top-left-radius: var(--outer-radius); + border-top-right-radius: var(--outer-radius); +} +.MobileApp_logo { + justify-self: center; + grid-area: logo; +} +.MobileApp_buttons { + grid-area: buttons; + padding: var(--inner-padding); + background: var(--bg-color); + border-bottom-left-radius: var(--outer-radius); + border-bottom-right-radius: var(--outer-radius); +} +.MobileApp_switch { + grid-area: switch; + height: 50px; + background: rgba(255, 255, 255, 0.03); + border-radius: 16px; +} +@media screen and (min-width: 425px) and (max-height: 600px) { + .MobileApp_main { + grid-template-rows: max-content auto max-content max-content 12px max-content auto; + } +} +@media screen and (min-width: 768px) and (min-height: 600px) { + .MobileApp_logo { + justify-self: unset; + background: var(--bg-color); + border-bottom-left-radius: var(--outer-radius); + display: grid; + align-items: center; + padding-left: var(--inner-padding); + } + .MobileApp_buttons { + border-bottom-left-radius: unset; + } + .MobileApp_main { + grid-template-columns: auto minmax(384px, max-content); + grid-template-rows: max-content max-content 12px max-content; + grid-template-areas: "embed embed" "logo buttons" "button-gap button-gap" "switch switch"; + align-content: center; + max-width: calc(100vh * 1.3); + } + body:has([data-state=completed] [aria-checked=true]) .MobileApp_main { + grid-template-rows: max-content max-content 0 0; + } +} +@media screen and (min-width: 900px) and (min-height: 660px) { + .MobileApp_logo { + justify-self: unset; + padding-right: 34px; + } + .MobileApp_switch { + background: var(--bg-color); + border-radius: unset; + display: grid; + padding-top: 12px; + padding-bottom: 12px; + height: 100%; + } + .MobileApp_buttons { + padding-left: 8px; + } + .MobileApp_main { + grid-template-columns: max-content auto minmax(384px, max-content); + grid-template-rows: max-content max-content; + grid-template-areas: "embed embed embed" "logo switch buttons"; + align-content: center; + max-width: calc(100vh * 1.3); + } + body:has([data-state=completed] [aria-checked=true]) .MobileApp_switch { + background: var(--bg-color); + } +} +@media screen and (min-width: 600px) and (max-height: 450px) { + .MobileApp_main { + grid-template-columns: 1fr 1fr; + grid-template-rows: calc(44px + 24px) 44px auto calc(44px + 24px); + grid-template-areas: "embed logo" "embed buttons" "embed filler" "embed switch"; + align-content: center; + max-width: 100%; + max-height: 90vh; + } + body:has([data-state=completed] [aria-checked=true]) .MobileApp_main { + grid-template-rows: max-content max-content 0 0; + } + body:has([data-state=completed] [aria-checked=true]) .MobileApp_logo { + padding-top: 0; + align-items: end; + } + body:has([data-state=completed] [aria-checked=true]) .MobileApp_buttons { + border-bottom-right-radius: var(--outer-radius); + padding-bottom: 12px; + } + body:has([data-state=completed] [aria-checked=true]) .MobileApp_switch { + display: none; + } + .MobileApp_filler { + display: block; + height: 100%; + grid-area: filler; + background: var(--bg-color); + } + .MobileApp_embed { + padding: var(--inner-padding); + border-bottom-left-radius: var(--outer-radius); + border-top-right-radius: 0; + } + .MobileApp_logo { + display: grid; + width: 100%; + background: var(--bg-color); + justify-content: center; + border-top-right-radius: var(--outer-radius); + padding: var(--inner-padding); + padding-left: 0; + } + .MobileApp_buttons { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + padding: 0; + padding-right: var(--inner-padding); + } + .MobileApp_switch { + background: var(--bg-color); + border-top-right-radius: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: var(--outer-radius); + align-self: end; + padding: var(--inner-padding); + padding-left: 0; + height: 100%; + } +} +@media screen and (min-width: 1100px) { + .MobileApp_switch { + justify-content: end; + } + .MobileApp_switch > * { + min-width: 400px; + } +} + +/* pages/duckplayer/app/components/MobileButtons.module.css */ +.MobileButtons_buttons { + display: grid; + grid-template-columns: max-content max-content auto; + grid-column-gap: 8px; +} + +/* pages/duckplayer/app/components/DesktopApp.module.css */ +:root { + --video-width: 80vw; + --outer-radius: 16px; + --inner-radius: 8px; +} +@media screen and (max-width: 1080px) { + :root { + --video-width: 85vw; + } +} +@media screen and (max-width: 740px) { + :root { + --video-width: 90vw; + } +} +:root [data-layout=desktop] { + --frame-height: min( calc(var(--video-width) * calc(9 / 16)), 80vh ) ; +} +:root [data-layout=mobile][data-orientation=portrait] { + --video-width: calc(100vw - 32px) ; +} +:root [data-layout=mobile][data-orientation=landscape] { + --video-width: calc(calc(100vw - 32px) * 0.6) ; +} +@media screen and (max-width: 700px) { + :root [data-layout=mobile][data-orientation=landscape] { + --video-width: calc(calc(100vw - 32px) * 0.5) ; + } +} +:root [data-layout=mobile] { + --frame-height: min( calc(var(--video-width) * calc(9 / 16)), calc(100vh - 32px) ) ; +} +.DesktopApp_app { + margin: 0 auto; + position: relative; + z-index: 1; + height: 100%; + width: 100%; + max-width: 3840px; + color: rgba(255, 255, 255, 0.85); +} +.DesktopApp_portrait { + height: 100%; + display: grid; + align-self: center; + grid-template-areas: "header" "main"; + grid-template-rows: max-content 1fr; +} +.DesktopApp_landscape { + height: 100%; + display: grid; + align-self: center; + align-items: center; + align-content: center; +} +.DesktopApp_wrapper { +} +.DesktopApp_portrait .DesktopApp_wrapper { + grid-area: main; + display: grid; + grid-template-areas: "main" "controls"; + grid-template-rows: auto max-content; +} +.DesktopApp_landscape .DesktopApp_wrapper { + display: grid; + grid-template-columns: 60% 1fr; + grid-column-gap: 8px; + background: rgba(0, 0, 0, 0.3); + border-radius: var(--outer-radius); + padding: 8px; + @media screen and (max-width: 700px) { + grid-template-columns: 50% 1fr; + } +} +.DesktopApp_desktop { + height: 100%; + width: var(--video-width); + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; +} +.DesktopApp_rhs { + display: grid; + height: 100%; + grid-template-areas: "header" "controls" "switch"; + grid-template-rows: max-content max-content auto; + grid-template-columns: 1fr; + grid-row-gap: 12px; +} +.DesktopApp_rhs:has([data-state=completed] [aria-checked=true]) { + align-content: center; +} +.DesktopApp_header { + grid-area: header; + padding-top: 48px; + @media screen and (max-height: 500px) { + padding-top: 32px; + } +} +.DesktopApp_main { + align-self: center; +} +.DesktopApp_controls { + grid-area: controls; +} +.DesktopApp_switch { + grid-area: switch; +} +.DesktopApp_landscape .DesktopApp_header { + padding-top: 8px; +} +.DesktopApp_landscape .DesktopApp_switch { + align-self: end; +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js new file mode 100644 index 000000000000..59dac3b2756d --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js @@ -0,0 +1,3603 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod + )); + + // ../../node_modules/classnames/index.js + var require_classnames = __commonJS({ + "../../node_modules/classnames/index.js"(exports, module) { + (function() { + "use strict"; + var hasOwn = {}.hasOwnProperty; + var nativeCodeString = "[native code]"; + function classNames() { + var classes = []; + for (var i3 = 0; i3 < arguments.length; i3++) { + var arg = arguments[i3]; + if (!arg) + continue; + var argType = typeof arg; + if (argType === "string" || argType === "number") { + classes.push(arg); + } else if (Array.isArray(arg)) { + if (arg.length) { + var inner = classNames.apply(null, arg); + if (inner) { + classes.push(inner); + } + } + } else if (argType === "object") { + if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) { + classes.push(arg.toString()); + continue; + } + for (var key in arg) { + if (hasOwn.call(arg, key) && arg[key]) { + classes.push(key); + } + } + } + } + return classes.join(" "); + } + if (typeof module !== "undefined" && module.exports) { + classNames.default = classNames; + module.exports = classNames; + } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) { + define("classnames", [], function() { + return classNames; + }); + } else { + window.classNames = classNames; + } + })(); + } + }); + + // ../messaging/lib/windows.js + var WindowsMessagingTransport = class { + /** + * @param {WindowsMessagingConfig} config + * @param {import('../index.js').MessagingContext} messagingContext + * @internal + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + this.globals = { + window, + JSONparse: window.JSON.parse, + JSONstringify: window.JSON.stringify, + Promise: window.Promise, + Error: window.Error, + String: window.String + }; + for (const [methodName, fn] of Object.entries(this.config.methods)) { + if (typeof fn !== "function") { + throw new Error("cannot create WindowsMessagingTransport, missing the method: " + methodName); + } + } + } + /** + * @param {import('../index.js').NotificationMessage} msg + */ + notify(msg) { + const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {})); + const notification = WindowsNotification.fromNotification(msg, data); + this.config.methods.postMessage(notification); + } + /** + * @param {import('../index.js').RequestMessage} msg + * @param {{signal?: AbortSignal}} opts + * @return {Promise} + */ + request(msg, opts = {}) { + const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {})); + const outgoing = WindowsRequestMessage.fromRequest(msg, data); + this.config.methods.postMessage(outgoing); + const comparator = (eventData) => { + return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.id === msg.id; + }; + function isMessageResponse(data2) { + if ("result" in data2) + return true; + if ("error" in data2) + return true; + return false; + } + return new this.globals.Promise((resolve, reject) => { + try { + this._subscribe(comparator, opts, (value, unsubscribe) => { + unsubscribe(); + if (!isMessageResponse(value)) { + console.warn("unknown response type", value); + return reject(new this.globals.Error("unknown response")); + } + if (value.result) { + return resolve(value.result); + } + const message = this.globals.String(value.error?.message || "unknown error"); + reject(new this.globals.Error(message)); + }); + } catch (e3) { + reject(e3); + } + }); + } + /** + * @param {import('../index.js').Subscription} msg + * @param {(value: unknown | undefined) => void} callback + */ + subscribe(msg, callback) { + const comparator = (eventData) => { + return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.subscriptionName === msg.subscriptionName; + }; + const cb = (eventData) => { + return callback(eventData.params); + }; + return this._subscribe(comparator, {}, cb); + } + /** + * @typedef {import('../index.js').MessageResponse | import('../index.js').SubscriptionEvent} Incoming + */ + /** + * @param {(eventData: any) => boolean} comparator + * @param {{signal?: AbortSignal}} options + * @param {(value: Incoming, unsubscribe: (()=>void)) => void} callback + * @internal + */ + _subscribe(comparator, options, callback) { + if (options?.signal?.aborted) { + throw new DOMException("Aborted", "AbortError"); + } + let teardown; + const idHandler = (event) => { + if (this.messagingContext.env === "production") { + if (event.origin !== null && event.origin !== void 0) { + console.warn("ignoring because evt.origin is not `null` or `undefined`"); + return; + } + } + if (!event.data) { + console.warn("data absent from message"); + return; + } + if (comparator(event.data)) { + if (!teardown) + throw new Error("unreachable"); + callback(event.data, teardown); + } + }; + const abortHandler = () => { + teardown?.(); + throw new DOMException("Aborted", "AbortError"); + }; + this.config.methods.addEventListener("message", idHandler); + options?.signal?.addEventListener("abort", abortHandler); + teardown = () => { + this.config.methods.removeEventListener("message", idHandler); + options?.signal?.removeEventListener("abort", abortHandler); + }; + return () => { + teardown?.(); + }; + } + }; + var WindowsMessagingConfig = class { + /** + * @param {object} params + * @param {WindowsInteropMethods} params.methods + * @internal + */ + constructor(params) { + this.methods = params.methods; + this.platform = "windows"; + } + }; + var WindowsNotification = class { + /** + * @param {object} params + * @param {string} params.Feature + * @param {string} params.SubFeatureName + * @param {string} params.Name + * @param {Record} [params.Data] + * @internal + */ + constructor(params) { + this.Feature = params.Feature; + this.SubFeatureName = params.SubFeatureName; + this.Name = params.Name; + this.Data = params.Data; + } + /** + * Helper to convert a {@link NotificationMessage} to a format that Windows can support + * @param {NotificationMessage} notification + * @returns {WindowsNotification} + */ + static fromNotification(notification, data) { + const output = { + Data: data, + Feature: notification.context, + SubFeatureName: notification.featureName, + Name: notification.method + }; + return output; + } + }; + var WindowsRequestMessage = class { + /** + * @param {object} params + * @param {string} params.Feature + * @param {string} params.SubFeatureName + * @param {string} params.Name + * @param {Record} [params.Data] + * @param {string} [params.Id] + * @internal + */ + constructor(params) { + this.Feature = params.Feature; + this.SubFeatureName = params.SubFeatureName; + this.Name = params.Name; + this.Data = params.Data; + this.Id = params.Id; + } + /** + * Helper to convert a {@link RequestMessage} to a format that Windows can support + * @param {RequestMessage} msg + * @param {Record} data + * @returns {WindowsRequestMessage} + */ + static fromRequest(msg, data) { + const output = { + Data: data, + Feature: msg.context, + SubFeatureName: msg.featureName, + Name: msg.method, + Id: msg.id + }; + return output; + } + }; + + // ../messaging/schema.js + var RequestMessage = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.method + * @param {string} params.id + * @param {Record} [params.params] + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.method = params.method; + this.id = params.id; + this.params = params.params; + } + }; + var NotificationMessage = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.method + * @param {Record} [params.params] + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.method = params.method; + this.params = params.params; + } + }; + var Subscription = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.subscriptionName + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.subscriptionName = params.subscriptionName; + } + }; + function isResponseFor(request, data) { + if ("result" in data) { + return data.featureName === request.featureName && data.context === request.context && data.id === request.id; + } + if ("error" in data) { + if ("message" in data.error) { + return true; + } + } + return false; + } + function isSubscriptionEventFor(sub, data) { + if ("subscriptionName" in data) { + return data.featureName === sub.featureName && data.context === sub.context && data.subscriptionName === sub.subscriptionName; + } + return false; + } + + // ../messaging/lib/webkit.js + var WebkitMessagingTransport = class { + /** + * @param {WebkitMessagingConfig} config + * @param {import('../index.js').MessagingContext} messagingContext + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + this.globals = captureGlobals(); + if (!this.config.hasModernWebkitAPI) { + this.captureWebkitHandlers(this.config.webkitMessageHandlerNames); + } + } + /** + * Sends message to the webkit layer (fire and forget) + * @param {String} handler + * @param {*} data + * @internal + */ + wkSend(handler, data = {}) { + if (!(handler in this.globals.window.webkit.messageHandlers)) { + throw new MissingHandler(`Missing webkit handler: '${handler}'`, handler); + } + if (!this.config.hasModernWebkitAPI) { + const outgoing = { + ...data, + messageHandling: { + ...data.messageHandling, + secret: this.config.secret + } + }; + if (!(handler in this.globals.capturedWebkitHandlers)) { + throw new MissingHandler(`cannot continue, method ${handler} not captured on macos < 11`, handler); + } else { + return this.globals.capturedWebkitHandlers[handler](outgoing); + } + } + return this.globals.window.webkit.messageHandlers[handler].postMessage?.(data); + } + /** + * Sends message to the webkit layer and waits for the specified response + * @param {String} handler + * @param {import('../index.js').RequestMessage} data + * @returns {Promise<*>} + * @internal + */ + async wkSendAndWait(handler, data) { + if (this.config.hasModernWebkitAPI) { + const response = await this.wkSend(handler, data); + return this.globals.JSONparse(response || "{}"); + } + try { + const randMethodName = this.createRandMethodName(); + const key = await this.createRandKey(); + const iv = this.createRandIv(); + const { + ciphertext, + tag + } = await new this.globals.Promise((resolve) => { + this.generateRandomMethod(randMethodName, resolve); + data.messageHandling = new SecureMessagingParams({ + methodName: randMethodName, + secret: this.config.secret, + key: this.globals.Arrayfrom(key), + iv: this.globals.Arrayfrom(iv) + }); + this.wkSend(handler, data); + }); + const cipher = new this.globals.Uint8Array([...ciphertext, ...tag]); + const decrypted = await this.decrypt(cipher, key, iv); + return this.globals.JSONparse(decrypted || "{}"); + } catch (e3) { + if (e3 instanceof MissingHandler) { + throw e3; + } else { + console.error("decryption failed", e3); + console.error(e3); + return { error: e3 }; + } + } + } + /** + * @param {import('../index.js').NotificationMessage} msg + */ + notify(msg) { + this.wkSend(msg.context, msg); + } + /** + * @param {import('../index.js').RequestMessage} msg + */ + async request(msg) { + const data = await this.wkSendAndWait(msg.context, msg); + if (isResponseFor(msg, data)) { + if (data.result) { + return data.result || {}; + } + if (data.error) { + throw new Error(data.error.message); + } + } + throw new Error("an unknown error occurred"); + } + /** + * Generate a random method name and adds it to the global scope + * The native layer will use this method to send the response + * @param {string | number} randomMethodName + * @param {Function} callback + * @internal + */ + generateRandomMethod(randomMethodName, callback) { + this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, { + enumerable: false, + // configurable, To allow for deletion later + configurable: true, + writable: false, + /** + * @param {any[]} args + */ + value: (...args) => { + callback(...args); + delete this.globals.window[randomMethodName]; + } + }); + } + /** + * @internal + * @return {string} + */ + randomString() { + return "" + this.globals.getRandomValues(new this.globals.Uint32Array(1))[0]; + } + /** + * @internal + * @return {string} + */ + createRandMethodName() { + return "_" + this.randomString(); + } + /** + * @type {{name: string, length: number}} + * @internal + */ + algoObj = { + name: "AES-GCM", + length: 256 + }; + /** + * @returns {Promise} + * @internal + */ + async createRandKey() { + const key = await this.globals.generateKey(this.algoObj, true, ["encrypt", "decrypt"]); + const exportedKey = await this.globals.exportKey("raw", key); + return new this.globals.Uint8Array(exportedKey); + } + /** + * @returns {Uint8Array} + * @internal + */ + createRandIv() { + return this.globals.getRandomValues(new this.globals.Uint8Array(12)); + } + /** + * @param {BufferSource} ciphertext + * @param {BufferSource} key + * @param {Uint8Array} iv + * @returns {Promise} + * @internal + */ + async decrypt(ciphertext, key, iv) { + const cryptoKey = await this.globals.importKey("raw", key, "AES-GCM", false, ["decrypt"]); + const algo = { + name: "AES-GCM", + iv + }; + const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); + const dec = new this.globals.TextDecoder(); + return dec.decode(decrypted); + } + /** + * When required (such as on macos 10.x), capture the `postMessage` method on + * each webkit messageHandler + * + * @param {string[]} handlerNames + */ + captureWebkitHandlers(handlerNames) { + const handlers = window.webkit.messageHandlers; + if (!handlers) + throw new MissingHandler("window.webkit.messageHandlers was absent", "all"); + for (const webkitMessageHandlerName of handlerNames) { + if (typeof handlers[webkitMessageHandlerName]?.postMessage === "function") { + const original = handlers[webkitMessageHandlerName]; + const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original); + this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound; + delete handlers[webkitMessageHandlerName].postMessage; + } + } + } + /** + * @param {import('../index.js').Subscription} msg + * @param {(value: unknown) => void} callback + */ + subscribe(msg, callback) { + if (msg.subscriptionName in this.globals.window) { + throw new this.globals.Error(`A subscription with the name ${msg.subscriptionName} already exists`); + } + this.globals.ObjectDefineProperty(this.globals.window, msg.subscriptionName, { + enumerable: false, + configurable: true, + writable: false, + value: (data) => { + if (data && isSubscriptionEventFor(msg, data)) { + callback(data.params); + } else { + console.warn("Received a message that did not match the subscription", data); + } + } + }); + return () => { + this.globals.ReflectDeleteProperty(this.globals.window, msg.subscriptionName); + }; + } + }; + var WebkitMessagingConfig = class { + /** + * @param {object} params + * @param {boolean} params.hasModernWebkitAPI + * @param {string[]} params.webkitMessageHandlerNames + * @param {string} params.secret + * @internal + */ + constructor(params) { + this.hasModernWebkitAPI = params.hasModernWebkitAPI; + this.webkitMessageHandlerNames = params.webkitMessageHandlerNames; + this.secret = params.secret; + } + }; + var SecureMessagingParams = class { + /** + * @param {object} params + * @param {string} params.methodName + * @param {string} params.secret + * @param {number[]} params.key + * @param {number[]} params.iv + */ + constructor(params) { + this.methodName = params.methodName; + this.secret = params.secret; + this.key = params.key; + this.iv = params.iv; + } + }; + function captureGlobals() { + const globals = { + window, + getRandomValues: window.crypto.getRandomValues.bind(window.crypto), + TextEncoder, + TextDecoder, + Uint8Array, + Uint16Array, + Uint32Array, + JSONstringify: window.JSON.stringify, + JSONparse: window.JSON.parse, + Arrayfrom: window.Array.from, + Promise: window.Promise, + Error: window.Error, + ReflectDeleteProperty: window.Reflect.deleteProperty.bind(window.Reflect), + ObjectDefineProperty: window.Object.defineProperty, + addEventListener: window.addEventListener.bind(window), + /** @type {Record} */ + capturedWebkitHandlers: {} + }; + if (isSecureContext) { + globals.generateKey = window.crypto.subtle.generateKey.bind(window.crypto.subtle); + globals.exportKey = window.crypto.subtle.exportKey.bind(window.crypto.subtle); + globals.importKey = window.crypto.subtle.importKey.bind(window.crypto.subtle); + globals.encrypt = window.crypto.subtle.encrypt.bind(window.crypto.subtle); + globals.decrypt = window.crypto.subtle.decrypt.bind(window.crypto.subtle); + } + return globals; + } + + // ../messaging/lib/android.js + var AndroidMessagingTransport = class { + /** + * @param {AndroidMessagingConfig} config + * @param {MessagingContext} messagingContext + * @internal + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + } + /** + * @param {NotificationMessage} msg + */ + notify(msg) { + try { + this.config.sendMessageThrows?.(JSON.stringify(msg)); + } catch (e3) { + console.error(".notify failed", e3); + } + } + /** + * @param {RequestMessage} msg + * @return {Promise} + */ + request(msg) { + return new Promise((resolve, reject) => { + const unsub = this.config.subscribe(msg.id, handler); + try { + this.config.sendMessageThrows?.(JSON.stringify(msg)); + } catch (e3) { + unsub(); + reject(new Error("request failed to send: " + e3.message || "unknown error")); + } + function handler(data) { + if (isResponseFor(msg, data)) { + if (data.result) { + resolve(data.result || {}); + return unsub(); + } + if (data.error) { + reject(new Error(data.error.message)); + return unsub(); + } + unsub(); + throw new Error("unreachable: must have `result` or `error` key by this point"); + } + } + }); + } + /** + * @param {Subscription} msg + * @param {(value: unknown | undefined) => void} callback + */ + subscribe(msg, callback) { + const unsub = this.config.subscribe(msg.subscriptionName, (data) => { + if (isSubscriptionEventFor(msg, data)) { + callback(data.params || {}); + } + }); + return () => { + unsub(); + }; + } + }; + var AndroidMessagingConfig = class { + /** @type {(json: string, secret: string) => void} */ + _capturedHandler; + /** + * @param {object} params + * @param {Record} params.target + * @param {boolean} params.debug + * @param {string} params.messageSecret - a secret to ensure that messages are only + * processed by the correct handler + * @param {string} params.javascriptInterface - the name of the javascript interface + * registered on the native side + * @param {string} params.messageCallback - the name of the callback that the native + * side will use to send messages back to the javascript side + */ + constructor(params) { + this.target = params.target; + this.debug = params.debug; + this.javascriptInterface = params.javascriptInterface; + this.messageSecret = params.messageSecret; + this.messageCallback = params.messageCallback; + this.listeners = new globalThis.Map(); + this._captureGlobalHandler(); + this._assignHandlerMethod(); + } + /** + * The transport can call this to transmit a JSON payload along with a secret + * to the native Android handler. + * + * Note: This can throw - it's up to the transport to handle the error. + * + * @type {(json: string) => void} + * @throws + * @internal + */ + sendMessageThrows(json) { + this._capturedHandler(json, this.messageSecret); + } + /** + * A subscription on Android is just a named listener. All messages from + * android -> are delivered through a single function, and this mapping is used + * to route the messages to the correct listener. + * + * Note: Use this to implement request->response by unsubscribing after the first + * response. + * + * @param {string} id + * @param {(msg: MessageResponse | SubscriptionEvent) => void} callback + * @returns {() => void} + * @internal + */ + subscribe(id, callback) { + this.listeners.set(id, callback); + return () => { + this.listeners.delete(id); + }; + } + /** + * Accept incoming messages and try to deliver it to a registered listener. + * + * This code is defensive to prevent any single handler from affecting another if + * it throws (producer interference). + * + * @param {MessageResponse | SubscriptionEvent} payload + * @internal + */ + _dispatch(payload) { + if (!payload) + return this._log("no response"); + if ("id" in payload) { + if (this.listeners.has(payload.id)) { + this._tryCatch(() => this.listeners.get(payload.id)?.(payload)); + } else { + this._log("no listeners for ", payload); + } + } + if ("subscriptionName" in payload) { + if (this.listeners.has(payload.subscriptionName)) { + this._tryCatch(() => this.listeners.get(payload.subscriptionName)?.(payload)); + } else { + this._log("no subscription listeners for ", payload); + } + } + } + /** + * + * @param {(...args: any[]) => any} fn + * @param {string} [context] + */ + _tryCatch(fn, context = "none") { + try { + return fn(); + } catch (e3) { + if (this.debug) { + console.error("AndroidMessagingConfig error:", context); + console.error(e3); + } + } + } + /** + * @param {...any} args + */ + _log(...args) { + if (this.debug) { + console.log("AndroidMessagingConfig", ...args); + } + } + /** + * Capture the global handler and remove it from the global object. + */ + _captureGlobalHandler() { + const { target, javascriptInterface } = this; + if (Object.prototype.hasOwnProperty.call(target, javascriptInterface)) { + this._capturedHandler = target[javascriptInterface].process.bind(target[javascriptInterface]); + delete target[javascriptInterface]; + } else { + this._capturedHandler = () => { + this._log("Android messaging interface not available", javascriptInterface); + }; + } + } + /** + * Assign the incoming handler method to the global object. + * This is the method that Android will call to deliver messages. + */ + _assignHandlerMethod() { + const responseHandler = (providedSecret, response) => { + if (providedSecret === this.messageSecret) { + this._dispatch(response); + } + }; + Object.defineProperty(this.target, this.messageCallback, { + value: responseHandler + }); + } + }; + + // ../messaging/lib/typed-messages.js + function createTypedMessages(base, messaging2) { + const asAny = ( + /** @type {any} */ + messaging2 + ); + return ( + /** @type {BaseClass} */ + asAny + ); + } + + // ../messaging/index.js + var MessagingContext = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {"production" | "development"} params.env + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.env = params.env; + } + }; + var Messaging = class { + /** + * @param {MessagingContext} messagingContext + * @param {MessagingConfig} config + */ + constructor(messagingContext, config) { + this.messagingContext = messagingContext; + this.transport = getTransport(config, this.messagingContext); + } + /** + * Send a 'fire-and-forget' message. + * @throws {MissingHandler} + * + * @example + * + * ```ts + * const messaging = new Messaging(config) + * messaging.notify("foo", {bar: "baz"}) + * ``` + * @param {string} name + * @param {Record} [data] + */ + notify(name, data = {}) { + const message = new NotificationMessage({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + method: name, + params: data + }); + this.transport.notify(message); + } + /** + * Send a request, and wait for a response + * @throws {MissingHandler} + * + * @example + * ``` + * const messaging = new Messaging(config) + * const response = await messaging.request("foo", {bar: "baz"}) + * ``` + * + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ + request(name, data = {}) { + const id = globalThis?.crypto?.randomUUID?.() || name + ".response"; + const message = new RequestMessage({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + method: name, + params: data, + id + }); + return this.transport.request(message); + } + /** + * @param {string} name + * @param {(value: unknown) => void} callback + * @return {() => void} + */ + subscribe(name, callback) { + const msg = new Subscription({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + subscriptionName: name + }); + return this.transport.subscribe(msg, callback); + } + }; + var TestTransportConfig = class { + /** + * @param {MessagingTransport} impl + */ + constructor(impl) { + this.impl = impl; + } + }; + var TestTransport = class { + /** + * @param {TestTransportConfig} config + * @param {MessagingContext} messagingContext + */ + constructor(config, messagingContext) { + this.config = config; + this.messagingContext = messagingContext; + } + notify(msg) { + return this.config.impl.notify(msg); + } + request(msg) { + return this.config.impl.request(msg); + } + subscribe(msg, callback) { + return this.config.impl.subscribe(msg, callback); + } + }; + function getTransport(config, messagingContext) { + if (config instanceof WebkitMessagingConfig) { + return new WebkitMessagingTransport(config, messagingContext); + } + if (config instanceof WindowsMessagingConfig) { + return new WindowsMessagingTransport(config, messagingContext); + } + if (config instanceof AndroidMessagingConfig) { + return new AndroidMessagingTransport(config, messagingContext); + } + if (config instanceof TestTransportConfig) { + return new TestTransport(config, messagingContext); + } + throw new Error("unreachable"); + } + var MissingHandler = class extends Error { + /** + * @param {string} message + * @param {string} handlerName + */ + constructor(message, handlerName) { + super(message); + this.handlerName = handlerName; + } + }; + + // shared/environment.js + var Environment = class _Environment { + /** + * @param {object} params + * @param {'app' | 'components'} [params.display] - whether to show the application or component list + * @param {'production' | 'development'} [params.env] - application environment + * @param {URLSearchParams} [params.urlParams] - URL params passed into the page + * @param {ImportMeta['injectName']} [params.injectName] - application platform + * @param {boolean} [params.willThrow] - whether the application will simulate an error + * @param {boolean} [params.debugState] - whether to show debugging UI + * @param {string} [params.locale] - for applications strings + * @param {number} [params.textLength] - what ratio of text should be used. Set a number higher than 1 to have longer strings for testing + */ + constructor({ + env = "production", + urlParams = new URLSearchParams(location.search), + injectName = "windows", + willThrow = urlParams.get("willThrow") === "true", + debugState = urlParams.has("debugState"), + display = "app", + locale = "en", + textLength = 1 + } = {}) { + this.display = display; + this.urlParams = urlParams; + this.injectName = injectName; + this.willThrow = willThrow; + this.debugState = debugState; + this.env = env; + this.locale = locale; + this.textLength = textLength; + } + /** + * @param {string|null|undefined} injectName + * @returns {Environment} + */ + withInjectName(injectName) { + if (!injectName) + return this; + if (!isInjectName(injectName)) + return this; + return new _Environment({ + ...this, + injectName + }); + } + /** + * @param {string|null|undefined} env + * @returns {Environment} + */ + withEnv(env) { + if (!env) + return this; + if (env !== "production" && env !== "development") + return this; + return new _Environment({ + ...this, + env + }); + } + /** + * @param {string|null|undefined} display + * @returns {Environment} + */ + withDisplay(display) { + if (!display) + return this; + if (display !== "app" && display !== "components") + return this; + return new _Environment({ + ...this, + display + }); + } + /** + * @param {string|null|undefined} locale + * @returns {Environment} + */ + withLocale(locale) { + if (!locale) + return this; + if (typeof locale !== "string") + return this; + if (locale.length !== 2) + return this; + return new _Environment({ + ...this, + locale + }); + } + /** + * @param {string|number|null|undefined} length + * @returns {Environment} + */ + withTextLength(length) { + if (!length) + return this; + const num = Number(length); + if (num >= 1 && num <= 2) { + return new _Environment({ + ...this, + textLength: num + }); + } + return this; + } + }; + function isInjectName(input) { + const allowed = ["windows", "apple", "integration", "android"]; + return allowed.includes(input); + } + + // shared/create-special-page-messaging.js + function createSpecialPageMessaging(opts) { + const messageContext = new MessagingContext({ + context: "specialPages", + featureName: opts.pageName, + env: opts.env + }); + try { + if (opts.injectName === "windows") { + const opts2 = new WindowsMessagingConfig({ + methods: { + // @ts-expect-error - not in @types/chrome + postMessage: window.chrome.webview.postMessage, + // @ts-expect-error - not in @types/chrome + addEventListener: window.chrome.webview.addEventListener, + // @ts-expect-error - not in @types/chrome + removeEventListener: window.chrome.webview.removeEventListener + } + }); + return new Messaging(messageContext, opts2); + } else if (opts.injectName === "apple") { + const opts2 = new WebkitMessagingConfig({ + hasModernWebkitAPI: true, + secret: "", + webkitMessageHandlerNames: ["specialPages"] + }); + return new Messaging(messageContext, opts2); + } else if (opts.injectName === "android") { + const opts2 = new AndroidMessagingConfig({ + messageSecret: "duckduckgo-android-messaging-secret", + messageCallback: "messageCallback", + javascriptInterface: messageContext.context, + target: globalThis, + debug: true + }); + return new Messaging(messageContext, opts2); + } + } catch (e3) { + console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); + } + const fallback = new TestTransportConfig({ + /** + * @param {import('@duckduckgo/messaging').NotificationMessage} msg + */ + notify(msg) { + console.log(msg); + }, + /** + * @param {import('@duckduckgo/messaging').RequestMessage} msg + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + request: (msg) => { + console.log(msg); + if (msg.method === "initialSetup") { + return Promise.resolve({ + locale: "en", + env: opts.env + }); + } + return Promise.resolve(null); + }, + /** + * @param {import('@duckduckgo/messaging').SubscriptionEvent} msg + */ + subscribe(msg) { + console.log(msg); + return () => { + console.log("teardown"); + }; + } + }); + return new Messaging(messageContext, fallback); + } + + // shared/call-with-retry.js + async function callWithRetry(fn, params = {}) { + const { maxAttempts = 10, intervalMs = 300 } = params; + let attempt = 1; + while (attempt <= maxAttempts) { + try { + return { value: await fn(), attempt }; + } catch (error) { + if (attempt === maxAttempts) { + return { error: `Max attempts reached: ${error}` }; + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + attempt++; + } + } + return { error: "Unreachable: value not retrieved" }; + } + + // ../../node_modules/preact/dist/preact.module.js + var n; + var l; + var u; + var t; + var i; + var o; + var r; + var f; + var e; + var c = {}; + var s = []; + var a = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; + var h = Array.isArray; + function v(n2, l3) { + for (var u3 in l3) + n2[u3] = l3[u3]; + return n2; + } + function p(n2) { + var l3 = n2.parentNode; + l3 && l3.removeChild(n2); + } + function y(l3, u3, t3) { + var i3, o3, r3, f3 = {}; + for (r3 in u3) + "key" == r3 ? i3 = u3[r3] : "ref" == r3 ? o3 = u3[r3] : f3[r3] = u3[r3]; + if (arguments.length > 2 && (f3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), "function" == typeof l3 && null != l3.defaultProps) + for (r3 in l3.defaultProps) + void 0 === f3[r3] && (f3[r3] = l3.defaultProps[r3]); + return d(l3, f3, i3, o3, null); + } + function d(n2, t3, i3, o3, r3) { + var f3 = { type: n2, props: t3, key: i3, ref: o3, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, constructor: void 0, __v: null == r3 ? ++u : r3, __i: -1, __u: 0 }; + return null == r3 && null != l.vnode && l.vnode(f3), f3; + } + function g(n2) { + return n2.children; + } + function b(n2, l3) { + this.props = n2, this.context = l3; + } + function m(n2, l3) { + if (null == l3) + return n2.__ ? m(n2.__, n2.__i + 1) : null; + for (var u3; l3 < n2.__k.length; l3++) + if (null != (u3 = n2.__k[l3]) && null != u3.__e) + return u3.__e; + return "function" == typeof n2.type ? m(n2) : null; + } + function k(n2) { + var l3, u3; + if (null != (n2 = n2.__) && null != n2.__c) { + for (n2.__e = n2.__c.base = null, l3 = 0; l3 < n2.__k.length; l3++) + if (null != (u3 = n2.__k[l3]) && null != u3.__e) { + n2.__e = n2.__c.base = u3.__e; + break; + } + return k(n2); + } + } + function w(n2) { + (!n2.__d && (n2.__d = true) && i.push(n2) && !x.__r++ || o !== l.debounceRendering) && ((o = l.debounceRendering) || r)(x); + } + function x() { + var n2, u3, t3, o3, r3, e3, c3, s3, a3; + for (i.sort(f); n2 = i.shift(); ) + n2.__d && (u3 = i.length, o3 = void 0, e3 = (r3 = (t3 = n2).__v).__e, s3 = [], a3 = [], (c3 = t3.__P) && ((o3 = v({}, r3)).__v = r3.__v + 1, l.vnode && l.vnode(o3), L(c3, o3, r3, t3.__n, void 0 !== c3.ownerSVGElement, 32 & r3.__u ? [e3] : null, s3, null == e3 ? m(r3) : e3, !!(32 & r3.__u), a3), o3.__.__k[o3.__i] = o3, M(s3, o3, a3), o3.__e != e3 && k(o3)), i.length > u3 && i.sort(f)); + x.__r = 0; + } + function C(n2, l3, u3, t3, i3, o3, r3, f3, e3, a3, h3) { + var v3, p3, y3, d3, _2, g3 = t3 && t3.__k || s, b3 = l3.length; + for (u3.__d = e3, P(u3, l3, g3), e3 = u3.__d, v3 = 0; v3 < b3; v3++) + null != (y3 = u3.__k[v3]) && "boolean" != typeof y3 && "function" != typeof y3 && (p3 = -1 === y3.__i ? c : g3[y3.__i] || c, y3.__i = v3, L(n2, y3, p3, i3, o3, r3, f3, e3, a3, h3), d3 = y3.__e, y3.ref && p3.ref != y3.ref && (p3.ref && z(p3.ref, null, y3), h3.push(y3.ref, y3.__c || d3, y3)), null == _2 && null != d3 && (_2 = d3), 65536 & y3.__u || p3.__k === y3.__k ? e3 = S(y3, e3, n2) : "function" == typeof y3.type && void 0 !== y3.__d ? e3 = y3.__d : d3 && (e3 = d3.nextSibling), y3.__d = void 0, y3.__u &= -196609); + u3.__d = e3, u3.__e = _2; + } + function P(n2, l3, u3) { + var t3, i3, o3, r3, f3, e3 = l3.length, c3 = u3.length, s3 = c3, a3 = 0; + for (n2.__k = [], t3 = 0; t3 < e3; t3++) + null != (i3 = n2.__k[t3] = null == (i3 = l3[t3]) || "boolean" == typeof i3 || "function" == typeof i3 ? null : "string" == typeof i3 || "number" == typeof i3 || "bigint" == typeof i3 || i3.constructor == String ? d(null, i3, null, null, i3) : h(i3) ? d(g, { children: i3 }, null, null, null) : void 0 === i3.constructor && i3.__b > 0 ? d(i3.type, i3.props, i3.key, i3.ref ? i3.ref : null, i3.__v) : i3) ? (i3.__ = n2, i3.__b = n2.__b + 1, f3 = H(i3, u3, r3 = t3 + a3, s3), i3.__i = f3, o3 = null, -1 !== f3 && (s3--, (o3 = u3[f3]) && (o3.__u |= 131072)), null == o3 || null === o3.__v ? (-1 == f3 && a3--, "function" != typeof i3.type && (i3.__u |= 65536)) : f3 !== r3 && (f3 === r3 + 1 ? a3++ : f3 > r3 ? s3 > e3 - r3 ? a3 += f3 - r3 : a3-- : a3 = f3 < r3 && f3 == r3 - 1 ? f3 - r3 : 0, f3 !== t3 + a3 && (i3.__u |= 65536))) : (o3 = u3[t3]) && null == o3.key && o3.__e && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3, false), u3[t3] = null, s3--); + if (s3) + for (t3 = 0; t3 < c3; t3++) + null != (o3 = u3[t3]) && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3)); + } + function S(n2, l3, u3) { + var t3, i3; + if ("function" == typeof n2.type) { + for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++) + t3[i3] && (t3[i3].__ = n2, l3 = S(t3[i3], l3, u3)); + return l3; + } + return n2.__e != l3 && (u3.insertBefore(n2.__e, l3 || null), l3 = n2.__e), l3 && l3.nextSibling; + } + function H(n2, l3, u3, t3) { + var i3 = n2.key, o3 = n2.type, r3 = u3 - 1, f3 = u3 + 1, e3 = l3[u3]; + if (null === e3 || e3 && i3 == e3.key && o3 === e3.type) + return u3; + if (t3 > (null != e3 && 0 == (131072 & e3.__u) ? 1 : 0)) + for (; r3 >= 0 || f3 < l3.length; ) { + if (r3 >= 0) { + if ((e3 = l3[r3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type) + return r3; + r3--; + } + if (f3 < l3.length) { + if ((e3 = l3[f3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type) + return f3; + f3++; + } + } + return -1; + } + function I(n2, l3, u3) { + "-" === l3[0] ? n2.setProperty(l3, null == u3 ? "" : u3) : n2[l3] = null == u3 ? "" : "number" != typeof u3 || a.test(l3) ? u3 : u3 + "px"; + } + function T(n2, l3, u3, t3, i3) { + var o3; + n: + if ("style" === l3) + if ("string" == typeof u3) + n2.style.cssText = u3; + else { + if ("string" == typeof t3 && (n2.style.cssText = t3 = ""), t3) + for (l3 in t3) + u3 && l3 in u3 || I(n2.style, l3, ""); + if (u3) + for (l3 in u3) + t3 && u3[l3] === t3[l3] || I(n2.style, l3, u3[l3]); + } + else if ("o" === l3[0] && "n" === l3[1]) + o3 = l3 !== (l3 = l3.replace(/(PointerCapture)$|Capture$/, "$1")), l3 = l3.toLowerCase() in n2 ? l3.toLowerCase().slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + o3] = u3, u3 ? t3 ? u3.u = t3.u : (u3.u = Date.now(), n2.addEventListener(l3, o3 ? D : A, o3)) : n2.removeEventListener(l3, o3 ? D : A, o3); + else { + if (i3) + l3 = l3.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s"); + else if ("width" !== l3 && "height" !== l3 && "href" !== l3 && "list" !== l3 && "form" !== l3 && "tabIndex" !== l3 && "download" !== l3 && "rowSpan" !== l3 && "colSpan" !== l3 && "role" !== l3 && l3 in n2) + try { + n2[l3] = null == u3 ? "" : u3; + break n; + } catch (n3) { + } + "function" == typeof u3 || (null == u3 || false === u3 && "-" !== l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, u3)); + } + } + function A(n2) { + var u3 = this.l[n2.type + false]; + if (n2.t) { + if (n2.t <= u3.u) + return; + } else + n2.t = Date.now(); + return u3(l.event ? l.event(n2) : n2); + } + function D(n2) { + return this.l[n2.type + true](l.event ? l.event(n2) : n2); + } + function L(n2, u3, t3, i3, o3, r3, f3, e3, c3, s3) { + var a3, p3, y3, d3, _2, m3, k3, w3, x2, P2, S2, $, H2, I2, T3, A2 = u3.type; + if (void 0 !== u3.constructor) + return null; + 128 & t3.__u && (c3 = !!(32 & t3.__u), r3 = [e3 = u3.__e = t3.__e]), (a3 = l.__b) && a3(u3); + n: + if ("function" == typeof A2) + try { + if (w3 = u3.props, x2 = (a3 = A2.contextType) && i3[a3.__c], P2 = a3 ? x2 ? x2.props.value : a3.__ : i3, t3.__c ? k3 = (p3 = u3.__c = t3.__c).__ = p3.__E : ("prototype" in A2 && A2.prototype.render ? u3.__c = p3 = new A2(w3, P2) : (u3.__c = p3 = new b(w3, P2), p3.constructor = A2, p3.render = O), x2 && x2.sub(p3), p3.props = w3, p3.state || (p3.state = {}), p3.context = P2, p3.__n = i3, y3 = p3.__d = true, p3.__h = [], p3._sb = []), null == p3.__s && (p3.__s = p3.state), null != A2.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = v({}, p3.__s)), v(p3.__s, A2.getDerivedStateFromProps(w3, p3.__s))), d3 = p3.props, _2 = p3.state, p3.__v = u3, y3) + null == A2.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), null != p3.componentDidMount && p3.__h.push(p3.componentDidMount); + else { + if (null == A2.getDerivedStateFromProps && w3 !== d3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(w3, P2), !p3.__e && (null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(w3, p3.__s, P2) || u3.__v === t3.__v)) { + for (u3.__v !== t3.__v && (p3.props = w3, p3.state = p3.__s, p3.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.forEach(function(n3) { + n3 && (n3.__ = u3); + }), S2 = 0; S2 < p3._sb.length; S2++) + p3.__h.push(p3._sb[S2]); + p3._sb = [], p3.__h.length && f3.push(p3); + break n; + } + null != p3.componentWillUpdate && p3.componentWillUpdate(w3, p3.__s, P2), null != p3.componentDidUpdate && p3.__h.push(function() { + p3.componentDidUpdate(d3, _2, m3); + }); + } + if (p3.context = P2, p3.props = w3, p3.__P = n2, p3.__e = false, $ = l.__r, H2 = 0, "prototype" in A2 && A2.prototype.render) { + for (p3.state = p3.__s, p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), I2 = 0; I2 < p3._sb.length; I2++) + p3.__h.push(p3._sb[I2]); + p3._sb = []; + } else + do { + p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s; + } while (p3.__d && ++H2 < 25); + p3.state = p3.__s, null != p3.getChildContext && (i3 = v(v({}, i3), p3.getChildContext())), y3 || null == p3.getSnapshotBeforeUpdate || (m3 = p3.getSnapshotBeforeUpdate(d3, _2)), C(n2, h(T3 = null != a3 && a3.type === g && null == a3.key ? a3.props.children : a3) ? T3 : [T3], u3, t3, i3, o3, r3, f3, e3, c3, s3), p3.base = u3.__e, u3.__u &= -161, p3.__h.length && f3.push(p3), k3 && (p3.__E = p3.__ = null); + } catch (n3) { + u3.__v = null, c3 || null != r3 ? (u3.__e = e3, u3.__u |= c3 ? 160 : 32, r3[r3.indexOf(e3)] = null) : (u3.__e = t3.__e, u3.__k = t3.__k), l.__e(n3, u3, t3); + } + else + null == r3 && u3.__v === t3.__v ? (u3.__k = t3.__k, u3.__e = t3.__e) : u3.__e = j(t3.__e, u3, t3, i3, o3, r3, f3, c3, s3); + (a3 = l.diffed) && a3(u3); + } + function M(n2, u3, t3) { + u3.__d = void 0; + for (var i3 = 0; i3 < t3.length; i3++) + z(t3[i3], t3[++i3], t3[++i3]); + l.__c && l.__c(u3, n2), n2.some(function(u4) { + try { + n2 = u4.__h, u4.__h = [], n2.some(function(n3) { + n3.call(u4); + }); + } catch (n3) { + l.__e(n3, u4.__v); + } + }); + } + function j(l3, u3, t3, i3, o3, r3, f3, e3, s3) { + var a3, v3, y3, d3, _2, g3, b3, k3 = t3.props, w3 = u3.props, x2 = u3.type; + if ("svg" === x2 && (o3 = true), null != r3) { + for (a3 = 0; a3 < r3.length; a3++) + if ((_2 = r3[a3]) && "setAttribute" in _2 == !!x2 && (x2 ? _2.localName === x2 : 3 === _2.nodeType)) { + l3 = _2, r3[a3] = null; + break; + } + } + if (null == l3) { + if (null === x2) + return document.createTextNode(w3); + l3 = o3 ? document.createElementNS("http://www.w3.org/2000/svg", x2) : document.createElement(x2, w3.is && w3), r3 = null, e3 = false; + } + if (null === x2) + k3 === w3 || e3 && l3.data === w3 || (l3.data = w3); + else { + if (r3 = r3 && n.call(l3.childNodes), k3 = t3.props || c, !e3 && null != r3) + for (k3 = {}, a3 = 0; a3 < l3.attributes.length; a3++) + k3[(_2 = l3.attributes[a3]).name] = _2.value; + for (a3 in k3) + _2 = k3[a3], "children" == a3 || ("dangerouslySetInnerHTML" == a3 ? y3 = _2 : "key" === a3 || a3 in w3 || T(l3, a3, null, _2, o3)); + for (a3 in w3) + _2 = w3[a3], "children" == a3 ? d3 = _2 : "dangerouslySetInnerHTML" == a3 ? v3 = _2 : "value" == a3 ? g3 = _2 : "checked" == a3 ? b3 = _2 : "key" === a3 || e3 && "function" != typeof _2 || k3[a3] === _2 || T(l3, a3, _2, k3[a3], o3); + if (v3) + e3 || y3 && (v3.__html === y3.__html || v3.__html === l3.innerHTML) || (l3.innerHTML = v3.__html), u3.__k = []; + else if (y3 && (l3.innerHTML = ""), C(l3, h(d3) ? d3 : [d3], u3, t3, i3, o3 && "foreignObject" !== x2, r3, f3, r3 ? r3[0] : t3.__k && m(t3, 0), e3, s3), null != r3) + for (a3 = r3.length; a3--; ) + null != r3[a3] && p(r3[a3]); + e3 || (a3 = "value", void 0 !== g3 && (g3 !== l3[a3] || "progress" === x2 && !g3 || "option" === x2 && g3 !== k3[a3]) && T(l3, a3, g3, k3[a3], false), a3 = "checked", void 0 !== b3 && b3 !== l3[a3] && T(l3, a3, b3, k3[a3], false)); + } + return l3; + } + function z(n2, u3, t3) { + try { + "function" == typeof n2 ? n2(u3) : n2.current = u3; + } catch (n3) { + l.__e(n3, t3); + } + } + function N(n2, u3, t3) { + var i3, o3; + if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current !== n2.__e || z(i3, null, u3)), null != (i3 = n2.__c)) { + if (i3.componentWillUnmount) + try { + i3.componentWillUnmount(); + } catch (n3) { + l.__e(n3, u3); + } + i3.base = i3.__P = null, n2.__c = void 0; + } + if (i3 = n2.__k) + for (o3 = 0; o3 < i3.length; o3++) + i3[o3] && N(i3[o3], u3, t3 || "function" != typeof n2.type); + t3 || null == n2.__e || p(n2.__e), n2.__ = n2.__e = n2.__d = void 0; + } + function O(n2, l3, u3) { + return this.constructor(n2, u3); + } + function q(u3, t3, i3) { + var o3, r3, f3, e3; + l.__ && l.__(u3, t3), r3 = (o3 = "function" == typeof i3) ? null : i3 && i3.__k || t3.__k, f3 = [], e3 = [], L(t3, u3 = (!o3 && i3 || t3).__k = y(g, null, [u3]), r3 || c, c, void 0 !== t3.ownerSVGElement, !o3 && i3 ? [i3] : r3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, f3, !o3 && i3 ? i3 : r3 ? r3.__e : t3.firstChild, o3, e3), M(f3, u3, e3); + } + function F(n2, l3) { + var u3 = { __c: l3 = "__cC" + e++, __: n2, Consumer: function(n3, l4) { + return n3.children(l4); + }, Provider: function(n3) { + var u4, t3; + return this.getChildContext || (u4 = [], (t3 = {})[l3] = this, this.getChildContext = function() { + return t3; + }, this.shouldComponentUpdate = function(n4) { + this.props.value !== n4.value && u4.some(function(n5) { + n5.__e = true, w(n5); + }); + }, this.sub = function(n4) { + u4.push(n4); + var l4 = n4.componentWillUnmount; + n4.componentWillUnmount = function() { + u4.splice(u4.indexOf(n4), 1), l4 && l4.call(n4); + }; + }), n3.children; + } }; + return u3.Provider.__ = u3.Consumer.contextType = u3; + } + n = s.slice, l = { __e: function(n2, l3, u3, t3) { + for (var i3, o3, r3; l3 = l3.__; ) + if ((i3 = l3.__c) && !i3.__) + try { + if ((o3 = i3.constructor) && null != o3.getDerivedStateFromError && (i3.setState(o3.getDerivedStateFromError(n2)), r3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), r3 = i3.__d), r3) + return i3.__E = i3; + } catch (l4) { + n2 = l4; + } + throw n2; + } }, u = 0, t = function(n2) { + return null != n2 && null == n2.constructor; + }, b.prototype.setState = function(n2, l3) { + var u3; + u3 = null != this.__s && this.__s !== this.state ? this.__s : this.__s = v({}, this.state), "function" == typeof n2 && (n2 = n2(v({}, u3), this.props)), n2 && v(u3, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), w(this)); + }, b.prototype.forceUpdate = function(n2) { + this.__v && (this.__e = true, n2 && this.__h.push(n2), w(this)); + }, b.prototype.render = g, i = [], r = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, f = function(n2, l3) { + return n2.__v.__b - l3.__v.__b; + }, x.__r = 0, e = 0; + + // ../../node_modules/preact/hooks/dist/hooks.module.js + var t2; + var r2; + var u2; + var i2; + var o2 = 0; + var f2 = []; + var c2 = []; + var e2 = l.__b; + var a2 = l.__r; + var v2 = l.diffed; + var l2 = l.__c; + var m2 = l.unmount; + function d2(t3, u3) { + l.__h && l.__h(r2, t3, o2 || u3), o2 = 0; + var i3 = r2.__H || (r2.__H = { __: [], __h: [] }); + return t3 >= i3.__.length && i3.__.push({ __V: c2 }), i3.__[t3]; + } + function h2(n2) { + return o2 = 1, s2(B, n2); + } + function s2(n2, u3, i3) { + var o3 = d2(t2++, 2); + if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u3) : B(void 0, u3), function(n3) { + var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3); + t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({})); + }], o3.__c = r2, !r2.u)) { + var f3 = function(n3, t3, r3) { + if (!o3.__c.__H) + return true; + var u4 = o3.__c.__H.__.filter(function(n4) { + return n4.__c; + }); + if (u4.every(function(n4) { + return !n4.__N; + })) + return !c3 || c3.call(this, n3, t3, r3); + var i4 = false; + return u4.forEach(function(n4) { + if (n4.__N) { + var t4 = n4.__[0]; + n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true); + } + }), !(!i4 && o3.__c.props === n3) && (!c3 || c3.call(this, n3, t3, r3)); + }; + r2.u = true; + var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate; + r2.componentWillUpdate = function(n3, t3, r3) { + if (this.__e) { + var u4 = c3; + c3 = void 0, f3(n3, t3, r3), c3 = u4; + } + e3 && e3.call(this, n3, t3, r3); + }, r2.shouldComponentUpdate = f3; + } + return o3.__N || o3.__; + } + function p2(u3, i3) { + var o3 = d2(t2++, 3); + !l.__s && z2(o3.__H, i3) && (o3.__ = u3, o3.i = i3, r2.__H.__h.push(o3)); + } + function y2(u3, i3) { + var o3 = d2(t2++, 4); + !l.__s && z2(o3.__H, i3) && (o3.__ = u3, o3.i = i3, r2.__h.push(o3)); + } + function _(n2) { + return o2 = 5, F2(function() { + return { current: n2 }; + }, []); + } + function F2(n2, r3) { + var u3 = d2(t2++, 7); + return z2(u3.__H, r3) ? (u3.__V = n2(), u3.i = r3, u3.__h = n2, u3.__V) : u3.__; + } + function T2(n2, t3) { + return o2 = 8, F2(function() { + return n2; + }, t3); + } + function q2(n2) { + var u3 = r2.context[n2.__c], i3 = d2(t2++, 9); + return i3.c = n2, u3 ? (null == i3.__ && (i3.__ = true, u3.sub(r2)), u3.props.value) : n2.__; + } + function b2() { + for (var t3; t3 = f2.shift(); ) + if (t3.__P && t3.__H) + try { + t3.__H.__h.forEach(k2), t3.__H.__h.forEach(w2), t3.__H.__h = []; + } catch (r3) { + t3.__H.__h = [], l.__e(r3, t3.__v); + } + } + l.__b = function(n2) { + r2 = null, e2 && e2(n2); + }, l.__r = function(n2) { + a2 && a2(n2), t2 = 0; + var i3 = (r2 = n2.__c).__H; + i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.forEach(function(n3) { + n3.__N && (n3.__ = n3.__N), n3.__V = c2, n3.__N = n3.i = void 0; + })) : (i3.__h.forEach(k2), i3.__h.forEach(w2), i3.__h = [], t2 = 0)), u2 = r2; + }, l.diffed = function(t3) { + v2 && v2(t3); + var o3 = t3.__c; + o3 && o3.__H && (o3.__H.__h.length && (1 !== f2.push(o3) && i2 === l.requestAnimationFrame || ((i2 = l.requestAnimationFrame) || j2)(b2)), o3.__H.__.forEach(function(n2) { + n2.i && (n2.__H = n2.i), n2.__V !== c2 && (n2.__ = n2.__V), n2.i = void 0, n2.__V = c2; + })), u2 = r2 = null; + }, l.__c = function(t3, r3) { + r3.some(function(t4) { + try { + t4.__h.forEach(k2), t4.__h = t4.__h.filter(function(n2) { + return !n2.__ || w2(n2); + }); + } catch (u3) { + r3.some(function(n2) { + n2.__h && (n2.__h = []); + }), r3 = [], l.__e(u3, t4.__v); + } + }), l2 && l2(t3, r3); + }, l.unmount = function(t3) { + m2 && m2(t3); + var r3, u3 = t3.__c; + u3 && u3.__H && (u3.__H.__.forEach(function(n2) { + try { + k2(n2); + } catch (n3) { + r3 = n3; + } + }), u3.__H = void 0, r3 && l.__e(r3, u3.__v)); + }; + var g2 = "function" == typeof requestAnimationFrame; + function j2(n2) { + var t3, r3 = function() { + clearTimeout(u3), g2 && cancelAnimationFrame(t3), setTimeout(n2); + }, u3 = setTimeout(r3, 100); + g2 && (t3 = requestAnimationFrame(r3)); + } + function k2(n2) { + var t3 = r2, u3 = n2.__c; + "function" == typeof u3 && (n2.__c = void 0, u3()), r2 = t3; + } + function w2(n2) { + var t3 = r2; + n2.__c = n2.__(), r2 = t3; + } + function z2(n2, t3) { + return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) { + return t4 !== n2[r3]; + }); + } + function B(n2, t3) { + return "function" == typeof t3 ? t3(n2) : t3; + } + + // shared/components/EnvironmentProvider.js + var EnvironmentContext = F({ + isReducedMotion: false, + isDarkMode: false, + debugState: false, + injectName: ( + /** @type {import('../environment').Environment['injectName']} */ + "windows" + ), + willThrow: false + }); + var THEME_QUERY = "(prefers-color-scheme: dark)"; + var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)"; + function EnvironmentProvider({ children, debugState, willThrow = false, injectName = "windows" }) { + const [theme, setTheme] = h2(window.matchMedia(THEME_QUERY).matches ? "dark" : "light"); + const [isReducedMotion, setReducedMotion] = h2(window.matchMedia(REDUCED_MOTION_QUERY).matches); + p2(() => { + const mediaQueryList = window.matchMedia(THEME_QUERY); + const listener = (e3) => setTheme(e3.matches ? "dark" : "light"); + mediaQueryList.addEventListener("change", listener); + return () => mediaQueryList.removeEventListener("change", listener); + }, []); + p2(() => { + const mediaQueryList = window.matchMedia(REDUCED_MOTION_QUERY); + const listener = (e3) => setter(e3.matches); + mediaQueryList.addEventListener("change", listener); + setter(mediaQueryList.matches); + function setter(value) { + document.documentElement.dataset.reducedMotion = String(value); + setReducedMotion(value); + } + window.addEventListener("toggle-reduced-motion", () => { + setter(true); + }); + return () => mediaQueryList.removeEventListener("change", listener); + }, []); + return /* @__PURE__ */ y(EnvironmentContext.Provider, { value: { + isReducedMotion, + debugState, + isDarkMode: theme === "dark", + injectName, + willThrow + } }, children); + } + function UpdateEnvironment({ search }) { + p2(() => { + const params = new URLSearchParams(search); + if (params.has("reduced-motion")) { + setTimeout(() => { + window.dispatchEvent(new CustomEvent("toggle-reduced-motion")); + }, 0); + } + }, [search]); + return null; + } + function useEnv() { + return q2(EnvironmentContext); + } + function WillThrow() { + const env = useEnv(); + if (env.willThrow) { + throw new Error("Simulated Exception"); + } + return null; + } + + // shared/translations.js + function apply(subject, replacements, textLength = 1) { + if (typeof subject !== "string" || subject.length === 0) + return ""; + let out = subject; + if (replacements) { + for (let [name, value] of Object.entries(replacements)) { + if (typeof value !== "string") + value = ""; + out = out.replaceAll(`{${name}}`, value); + } + } + if (textLength !== 1 && textLength > 0 && textLength <= 2) { + const targetLen = Math.ceil(out.length * textLength); + const target = Math.ceil(textLength); + const combined = out.repeat(target); + return combined.slice(0, targetLen); + } + return out; + } + + // shared/components/TranslationsProvider.js + var TranslationContext = F({ + /** @type {LocalTranslationFn} */ + t: () => { + throw new Error("must implement"); + } + }); + function TranslationProvider({ children, translationObject, fallback, textLength = 1 }) { + function t3(inputKey, replacements) { + const subject = translationObject?.[inputKey]?.title || fallback?.[inputKey]?.title; + return apply(subject, replacements, textLength); + } + return /* @__PURE__ */ y(TranslationContext.Provider, { value: { t: t3 } }, children); + } + + // shared/components/ErrorBoundary.js + var ErrorBoundary = class extends b { + /** + * @param {{didCatch: (params: {error: Error; info: any}) => void}} props + */ + constructor(props) { + super(props); + this.state = { hasError: false }; + } + static getDerivedStateFromError() { + return { hasError: true }; + } + componentDidCatch(error, info) { + console.error(error); + console.log(info); + this.props.didCatch({ error, info }); + } + render() { + if (this.state.hasError) { + return this.props.fallback; + } + return this.props.children; + } + }; + + // pages/duckplayer/app/embed-settings.js + var EmbedSettings = class _EmbedSettings { + /** + * @param {object} params + * @param {VideoId} params.videoId - videoID is required + * @param {Timestamp|null|undefined} params.timestamp - optional timestamp + * @param {boolean} [params.autoplay] - optional timestamp + * @param {boolean} [params.muted] - optionally start muted + */ + constructor({ + videoId, + timestamp, + autoplay = true, + muted = false + }) { + this.videoId = videoId; + this.timestamp = timestamp; + this.autoplay = autoplay; + this.muted = muted; + } + /** + * @param {boolean|null|undefined} autoplay + * @return {EmbedSettings} + */ + withAutoplay(autoplay) { + if (typeof autoplay !== "boolean") + return this; + return new _EmbedSettings({ + ...this, + autoplay + }); + } + /** + * @param {boolean|null|undefined} muted + * @return {EmbedSettings} + */ + withMuted(muted) { + if (typeof muted !== "boolean") + return this; + return new _EmbedSettings({ + ...this, + muted + }); + } + /** + * @param {string|null|undefined} href + * @returns {EmbedSettings|null} + */ + static fromHref(href) { + try { + return new _EmbedSettings({ + videoId: VideoId.fromHref(href), + timestamp: Timestamp.fromHref(href) + }); + } catch (e3) { + console.error(e3); + return null; + } + } + /** + * @return {string} + */ + toEmbedUrl() { + const url = new URL(`/embed/${this.videoId.id}`, "https://www.youtube-nocookie.com"); + url.searchParams.set("iv_load_policy", "1"); + if (this.autoplay) { + url.searchParams.set("autoplay", "1"); + if (this.muted) { + url.searchParams.set("muted", "1"); + } + } + url.searchParams.set("rel", "0"); + url.searchParams.set("modestbranding", "1"); + if (this.timestamp && this.timestamp.seconds > 0) { + url.searchParams.set("start", String(this.timestamp.seconds)); + } + return url.href; + } + /** + * @param {URL} base + * @return {string} + */ + intoYoutubeUrl(base) { + const url = new URL(base); + url.searchParams.set("v", this.videoId.id); + if (this.timestamp && this.timestamp.seconds > 0) { + url.searchParams.set("t", `${this.timestamp.seconds}s`); + } + return url.toString(); + } + }; + var VideoId = class _VideoId { + /** + * @param {string|null|undefined} input + * @throws {Error} + */ + constructor(input) { + if (typeof input !== "string") + throw new Error("string required, got: " + input); + const sanitized = sanitizeYoutubeId(input); + if (sanitized === null) + throw new Error("invalid ID from: " + input); + this.id = sanitized; + } + /** + * @param {string|null|undefined} href + */ + static fromHref(href) { + return new _VideoId(idFromHref(href)); + } + }; + var Timestamp = class _Timestamp { + /** + * @param {string|null|undefined} input + * @throws {Error} + */ + constructor(input) { + if (typeof input !== "string") + throw new Error("string required for timestamp"); + const seconds = timestampInSeconds(input); + if (seconds === null) + throw new Error("invalid input for timestamp: " + input); + this.seconds = seconds; + } + /** + * @param {string|null|undefined} href + * @return {Timestamp|null} + */ + static fromHref(href) { + if (typeof href !== "string") + return null; + const param = timestampFromHref(href); + if (param) { + try { + return new _Timestamp(param); + } catch (e3) { + return null; + } + } + return null; + } + }; + function idFromHref(href) { + if (typeof href !== "string") + return null; + let url; + try { + url = new URL(href); + } catch (e3) { + return null; + } + const fromParam = url.searchParams.get("videoID"); + if (fromParam) + return fromParam; + if (url.protocol === "duck:") { + return url.pathname.slice(1); + } + if (url.pathname.includes("/embed/")) { + return url.pathname.replace("/embed/", ""); + } + return null; + } + function timestampFromHref(href) { + if (typeof href !== "string") + return null; + let url; + try { + url = new URL(href); + } catch (e3) { + console.error(e3); + return null; + } + const timeParameter = url.searchParams.get("t"); + if (timeParameter) { + return timeParameter; + } + return null; + } + function timestampInSeconds(timestamp) { + const units = { + h: 3600, + m: 60, + s: 1 + }; + const parts = timestamp.split(/(\d+[hms]?)/); + const totalSeconds = parts.reduce((total, part) => { + if (!part) + return total; + for (const unit in units) { + if (part.includes(unit)) { + return total + parseInt(part) * units[unit]; + } + } + return total; + }, 0); + if (totalSeconds > 0) { + return totalSeconds; + } + return null; + } + function sanitizeYoutubeId(input) { + const subject = input.slice(0, 11); + if (/^[a-zA-Z0-9-_]+$/.test(subject)) { + return subject; + } + return null; + } + + // pages/duckplayer/src/locales/en/duckplayer.json + var duckplayer_default = { + smartling: { + string_format: "icu", + translate_paths: [ + { + path: "*/title", + key: "{*}/title", + instruction: "*/note" + } + ] + }, + alwaysWatchHere: { + title: "Always open YouTube videos here", + note: "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + keepEnabled: { + title: "Keep Duck Player turned on", + note: "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + openInfoButton: { + title: "Open Info", + note: "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + openSettingsButton: { + title: "Open Settings", + note: "aria label text on a button, opens a screen where the user can change settings" + }, + watchOnYoutube: { + title: "Watch on YouTube", + note: "text on a link that takes the user from the current page back onto YouTube.com" + }, + invalidIdError: { + title: "ERROR: Invalid video id", + note: "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + tooltipInfo: { + title: "Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations." + } + }; + + // pages/duckplayer/app/settings.js + var Settings = class _Settings { + /** + * @param {object} params + * @param {{name: ImportMeta['platform']}} [params.platform] + * @param {{state: 'enabled' | 'disabled'}} [params.pip] + * @param {{state: 'enabled' | 'disabled'}} [params.autoplay] + * @param {{state: 'enabled' | 'disabled'}} [params.focusMode] + */ + constructor({ + platform = { name: "macos" }, + pip = { state: "disabled" }, + autoplay = { state: "enabled" }, + focusMode = { state: "enabled" } + }) { + this.platform = platform; + this.pip = pip; + this.autoplay = autoplay; + this.focusMode = focusMode; + } + /** + * @param {keyof import("../../../types/duckplayer").DuckPlayerPageSettings} named + * @param {{state: 'enabled' | 'disabled'} | null | undefined} settings + * @return {Settings} + */ + withFeatureState(named, settings) { + if (!settings) + return this; + const valid = ["pip", "autoplay", "focusMode"]; + if (!valid.includes(named)) { + console.warn(`Excluding invalid feature key ${named}`); + return this; + } + if (settings.state === "enabled" || settings.state === "disabled") { + return new _Settings({ + ...this, + [named]: settings + }); + } + return this; + } + withPlatformName(name) { + const valid = ["windows", "macos", "ios", "android"]; + if (valid.includes( + /** @type {any} */ + name + )) { + return new _Settings({ + ...this, + platform: { name } + }); + } + return this; + } + /** + * @param {string|null|undefined} newState + * @return {Settings} + */ + withDisabledFocusMode(newState) { + if (newState === "disabled" || newState === "enabled") { + return new _Settings({ + ...this, + focusMode: { state: newState } + }); + } + return this; + } + /** + * @return {string} + */ + get youtubeBase() { + switch (this.platform.name) { + case "windows": + case "ios": + case "android": { + return "duck://player/openInYoutube"; + } + case "macos": { + return "https://www.youtube.com/watch"; + } + default: + throw new Error("unreachable"); + } + } + /** + * @return {'desktop' | 'mobile'} + */ + get layout() { + switch (this.platform.name) { + case "windows": + case "macos": { + return "desktop"; + } + case "ios": + case "android": { + return "mobile"; + } + default: + return "desktop"; + } + } + }; + + // pages/duckplayer/app/types.js + function useTypedTranslation() { + return { + t: q2(TranslationContext).t + }; + } + var MessagingContext2 = F( + /** @type {import("../src/js/index.js").DuckplayerPage} */ + {} + ); + var useMessaging = () => q2(MessagingContext2); + + // pages/duckplayer/app/providers/SettingsProvider.jsx + var SettingsContext = F( + /** @type {{settings: Settings}} */ + {} + ); + function SettingsProvider({ settings, children }) { + return /* @__PURE__ */ y(SettingsContext.Provider, { value: { settings } }, children); + } + function usePlatformName() { + return q2(SettingsContext).settings.platform.name; + } + function useOpenSettingsHandler() { + const settings = q2(SettingsContext).settings; + const messaging2 = useMessaging(); + return () => { + switch (settings.platform.name) { + case "ios": + case "android": { + messaging2.openSettings(); + break; + } + default: { + console.warn("unreachable!"); + } + } + }; + } + function useSettingsUrl() { + return "duck://settings/duckplayer"; + } + function useSettings() { + return q2(SettingsContext).settings; + } + function useOpenInfoHandler() { + const settings = q2(SettingsContext).settings; + const messaging2 = useMessaging(); + return () => { + switch (settings.platform.name) { + case "android": + case "ios": { + messaging2.openInfo(); + break; + } + default: { + console.warn("unreachable!"); + } + } + }; + } + function useOpenOnYoutubeHandler() { + const settings = q2(SettingsContext).settings; + return (embed) => { + if (!embed) + return console.warn("unreachable, settings.embed must be present"); + try { + const base = new URL(settings.youtubeBase); + window.location.href = embed.intoYoutubeUrl(base); + } catch (e3) { + console.error("could not form a URL to open in Youtube", e3); + } + }; + } + + // pages/duckplayer/app/providers/UserValuesProvider.jsx + var UserValuesContext = F({ + /** @type {UserValues} */ + value: { + privatePlayerMode: { alwaysAsk: {} }, + overlayInteracted: false + }, + /** + * @type {() => void} + */ + setEnabled: () => { + } + }); + function UserValuesProvider({ initial, children }) { + const [value, setValue] = h2(initial); + const messaging2 = useMessaging(); + p2(() => { + window.addEventListener("toggle-user-values-enabled", () => { + setValue({ privatePlayerMode: { enabled: {} }, overlayInteracted: false }); + }); + window.addEventListener("toggle-user-values-ask", () => { + setValue({ privatePlayerMode: { alwaysAsk: {} }, overlayInteracted: false }); + }); + const unsubscribe = messaging2.onUserValuesChanged((userValues) => { + setValue(userValues); + }); + return () => unsubscribe(); + }, [messaging2]); + function setEnabled() { + const values = { + privatePlayerMode: { enabled: {} }, + overlayInteracted: false + }; + messaging2.setUserValues(values).then((next) => { + console.log("response after setUserValues...", next); + console.log("will set", values); + setValue(values); + }).catch((err) => { + console.error("could not set the enabled flag", err); + messaging2.reportPageException({ message: "could not set the enabled flag: " + err.toString() }); + }); + } + return /* @__PURE__ */ y(UserValuesContext.Provider, { value: { value, setEnabled } }, children); + } + function useUserValues() { + return q2(UserValuesContext).value; + } + function useSetEnabled() { + return q2(UserValuesContext).setEnabled; + } + + // pages/duckplayer/app/components/Fallback.module.css + var Fallback_default = { + fallback: "Fallback_fallback" + }; + + // pages/duckplayer/app/components/Fallback.jsx + function Fallback({ showDetails }) { + return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); + } + + // pages/duckplayer/app/components/Components.module.css + var Components_default = { + main: "Components_main", + tube: "Components_tube" + }; + + // pages/duckplayer/app/components/PlayerContainer.jsx + var import_classnames = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/PlayerContainer.module.css + var PlayerContainer_default = { + container: "PlayerContainer_container", + inset: "PlayerContainer_inset", + internals: "PlayerContainer_internals", + insetInternals: "PlayerContainer_insetInternals" + }; + + // pages/duckplayer/app/components/PlayerContainer.jsx + function PlayerContainer({ children, inset }) { + return /* @__PURE__ */ y("div", { class: (0, import_classnames.default)(PlayerContainer_default.container, { + [PlayerContainer_default.inset]: inset + }) }, children); + } + function PlayerInternal({ children, inset }) { + return /* @__PURE__ */ y("div", { class: (0, import_classnames.default)(PlayerContainer_default.internals, { [PlayerContainer_default.insetInternals]: inset }) }, children); + } + + // pages/duckplayer/app/img/info.data.svg + var info_data_default = 'data:image/svg+xml,%0A %0A %0A %0A%0A'; + + // pages/duckplayer/app/img/cog.data.svg + var cog_data_default = 'data:image/svg+xml,%0A %0A %0A%0A'; + + // pages/duckplayer/app/components/Button.jsx + var import_classnames2 = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/Button.module.css + var Button_default = { + button: "Button_button", + fill: "Button_fill", + desktop: "Button_desktop", + icon: "Button_icon", + iconOnly: "Button_iconOnly", + highlight: "Button_highlight" + }; + + // pages/duckplayer/app/components/Button.jsx + function Button({ + children, + formfactor = "mobile", + icon = false, + fill = false, + highlight = false, + buttonProps = {} + }) { + const classes = (0, import_classnames2.default)({ + [Button_default.button]: true, + [Button_default.desktop]: formfactor === "desktop", + [Button_default.highlight]: highlight === true, + [Button_default.fill]: fill === true, + [Button_default.iconOnly]: icon === true + }); + return /* @__PURE__ */ y( + "button", + { + class: classes, + type: "button", + ...buttonProps + }, + children + ); + } + function ButtonLink({ + children, + formfactor = "mobile", + icon = false, + fill = false, + highlight = false, + anchorProps = {} + }) { + const classes = (0, import_classnames2.default)({ + [Button_default.button]: true, + [Button_default.desktop]: formfactor === "desktop", + [Button_default.highlight]: highlight === true, + [Button_default.fill]: fill === true, + [Button_default.iconOnly]: icon === true + }); + return /* @__PURE__ */ y( + "a", + { + class: classes, + type: "button", + ...anchorProps + }, + children + ); + } + function Icon({ src }) { + return /* @__PURE__ */ y("span", { class: Button_default.icon }, /* @__PURE__ */ y("img", { src, alt: "" })); + } + + // pages/duckplayer/app/components/FloatingBar.module.css + var FloatingBar_default = { + floatingBar: "FloatingBar_floatingBar", + inset: "FloatingBar_inset", + topBar: "FloatingBar_topBar" + }; + + // pages/duckplayer/app/components/FloatingBar.jsx + var import_classnames3 = __toESM(require_classnames(), 1); + function FloatingBar({ children, inset = false }) { + return /* @__PURE__ */ y("div", { class: (0, import_classnames3.default)(FloatingBar_default.floatingBar, { [FloatingBar_default.inset]: inset }) }, children); + } + + // pages/duckplayer/app/components/SwitchBarMobile.jsx + var import_classnames5 = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/SwitchBarMobile.module.css + var SwitchBarMobile_default = { + switchBar: "SwitchBarMobile_switchBar", + stateExiting: "SwitchBarMobile_stateExiting", + stateHidden: "SwitchBarMobile_stateHidden", + label: "SwitchBarMobile_label", + checkbox: "SwitchBarMobile_checkbox", + text: "SwitchBarMobile_text", + placeholder: "SwitchBarMobile_placeholder" + }; + + // pages/duckplayer/app/providers/SwitchProvider.jsx + var SwitchContext = F({ + /** @type {SwitchState} */ + state: "showing", + /** @type {() => void} */ + onChange: () => { + throw new Error("must implement"); + }, + /** @type {() => void} */ + onDone: () => { + throw new Error("must implement"); + } + }); + function SwitchProvider({ children }) { + const userValues = useUserValues(); + const setEnabled = useSetEnabled(); + const initialState = "enabled" in userValues.privatePlayerMode ? "completed" : "showing"; + const [state, dispatch] = s2((state2, event) => { + console.log("\u{1F4E9}", { state: state2, event }); + switch (state2) { + case "showing": { + if (event === "change") { + return "exiting"; + } + if (event === "enabled") { + return "completed"; + } + if (event === "done") { + return "completed"; + } + break; + } + case "exiting": { + if (event === "done") { + return "completed"; + } + break; + } + case "completed": { + if (event === "ask") { + return "showing"; + } + } + } + return state2; + }, initialState); + function onChange() { + dispatch("change"); + setEnabled(); + } + p2(() => { + const evt = "enabled" in userValues.privatePlayerMode ? "enabled" : "ask"; + dispatch(evt); + }, [initialState]); + function onDone() { + dispatch("done"); + } + return /* @__PURE__ */ y(SwitchContext.Provider, { value: { state, onChange, onDone } }, children); + } + + // pages/duckplayer/app/components/Switch.module.css + var Switch_default = { + switch: "Switch_switch", + thumb: "Switch_thumb", + ios: "Switch_ios" + }; + + // pages/duckplayer/app/components/Switch.jsx + var import_classnames4 = __toESM(require_classnames(), 1); + function Switch({ checked, onChange, platformName = "ios" }) { + return /* @__PURE__ */ y( + "button", + { + role: "switch", + "aria-checked": checked, + onClick: onChange, + className: (0, import_classnames4.default)(Switch_default.switch, { + [Switch_default.ios]: platformName === "ios", + [Switch_default.android]: platformName === "android" + }) + }, + /* @__PURE__ */ y("span", { className: Switch_default.thumb }) + ); + } + + // pages/duckplayer/app/components/SwitchBarMobile.jsx + function SwitchBarMobile({ platformName }) { + const { onChange, onDone, state } = q2(SwitchContext); + const { t: t3 } = useTypedTranslation(); + function blockClick(e3) { + if (state === "exiting") { + return e3.preventDefault(); + } + } + function onTransitionEnd(e3) { + if (e3.target?.dataset?.state === "exiting") { + onDone(); + } + } + const classes = (0, import_classnames5.default)({ + [SwitchBarMobile_default.switchBar]: true, + [SwitchBarMobile_default.stateExiting]: state === "exiting", + [SwitchBarMobile_default.stateHidden]: state === "completed" + }); + return /* @__PURE__ */ y("div", { class: classes, "data-state": state, onTransitionEnd }, /* @__PURE__ */ y("label", { onClick: blockClick, class: SwitchBarMobile_default.label }, /* @__PURE__ */ y("span", { className: SwitchBarMobile_default.text }, t3("keepEnabled")), /* @__PURE__ */ y( + Switch, + { + checked: state !== "showing", + onChange, + platformName + } + ))); + } + + // pages/duckplayer/app/components/InfoBar.module.css + var InfoBar_default = { + infoBar: "InfoBar_infoBar", + container: "InfoBar_container", + dax: "InfoBar_dax", + img: "InfoBar_img", + text: "InfoBar_text", + info: "InfoBar_info", + lhs: "InfoBar_lhs", + rhs: "InfoBar_rhs", + switch: "InfoBar_switch", + controls: "InfoBar_controls" + }; + + // pages/duckplayer/app/img/dax.data.svg + var dax_data_default = 'data:image/svg+xml,%0A %0A %0A %0A %0A %0A %0A %0A %0A %0A %0A%0A'; + + // pages/duckplayer/app/components/SwitchBarDesktop.module.css + var SwitchBarDesktop_default = { + switchBarDesktop: "SwitchBarDesktop_switchBarDesktop", + stateCompleted: "SwitchBarDesktop_stateCompleted", + stateExiting: "SwitchBarDesktop_stateExiting", + label: "SwitchBarDesktop_label", + "slide-out": "SwitchBarDesktop_slide-out", + checkbox: "SwitchBarDesktop_checkbox", + input: "SwitchBarDesktop_input", + text: "SwitchBarDesktop_text" + }; + + // pages/duckplayer/app/components/SwitchBarDesktop.jsx + var import_classnames6 = __toESM(require_classnames(), 1); + function SwitchBarDesktop() { + const { onChange, onDone, state } = q2(SwitchContext); + const { t: t3 } = useTypedTranslation(); + function blockClick(e3) { + if (state === "exiting") { + return e3.preventDefault(); + } + } + const classes = (0, import_classnames6.default)({ + [SwitchBarDesktop_default.switchBarDesktop]: true, + [SwitchBarDesktop_default.stateExiting]: state === "exiting", + [SwitchBarDesktop_default.stateCompleted]: state === "completed" + }); + return /* @__PURE__ */ y( + "div", + { + class: classes, + "data-state": state, + "data-allow-animation": true, + onTransitionEnd: onDone + }, + /* @__PURE__ */ y("label", { class: SwitchBarDesktop_default.label, onClick: blockClick }, /* @__PURE__ */ y("span", { class: SwitchBarDesktop_default.checkbox }, /* @__PURE__ */ y( + "input", + { + class: SwitchBarDesktop_default.input, + onChange, + name: "enabled", + type: "checkbox", + checked: state !== "showing" + } + )), /* @__PURE__ */ y("span", { class: SwitchBarDesktop_default.text }, t3("alwaysWatchHere"))) + ); + } + + // pages/duckplayer/app/components/Tooltip.jsx + var import_classnames7 = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/Tooltip.module.css + var Tooltip_default = { + tooltip: "Tooltip_tooltip", + top: "Tooltip_top", + bottom: "Tooltip_bottom", + visible: "Tooltip_visible" + }; + + // pages/duckplayer/app/components/Tooltip.jsx + function Tooltip({ id, isVisible, position }) { + const { t: t3 } = useTypedTranslation(); + return /* @__PURE__ */ y( + "div", + { + class: (0, import_classnames7.default)(Tooltip_default.tooltip, { + [Tooltip_default.top]: position === "top", + [Tooltip_default.bottom]: position === "bottom" + }), + role: "tooltip", + "aria-hidden": !isVisible, + id + }, + t3("tooltipInfo") + ); + } + + // pages/duckplayer/app/components/FocusMode.jsx + var import_classnames8 = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/FocusMode.module.css + var FocusMode_default = { + fade: "FocusMode_fade", + slide: "FocusMode_slide", + hideInFocus: "FocusMode_hideInFocus" + }; + + // pages/duckplayer/app/components/FocusMode.jsx + var EVENT_ON = "ddg-duckplayer-focusmode-on"; + var EVENT_OFF = "ddg-duckplayer-focusmode-off"; + function FocusMode() { + p2(() => { + let enabled = true; + let timerId; + const on = () => { + if (document.documentElement.dataset.focusModeState === "paused") { + wait(); + } else { + if (!enabled) { + return console.warn("ignoring focusMode because it was disabled"); + } + document.documentElement.dataset.focusMode = "on"; + } + }; + const off = () => document.documentElement.dataset.focusMode = "off"; + const cancel = () => { + clearTimeout(timerId); + off(); + wait(); + }; + const wait = () => { + clearTimeout(timerId); + timerId = setTimeout(on, 2e3); + }; + wait(); + document.addEventListener("mousemove", cancel); + document.addEventListener("pointerdown", cancel); + window.addEventListener("frame-mousemove", cancel); + window.addEventListener(EVENT_OFF, () => { + enabled = false; + off(); + }); + window.addEventListener(EVENT_ON, () => { + if (enabled === true) + return; + enabled = true; + on(); + }); + return () => { + clearTimeout(timerId); + }; + }, []); + return null; + } + FocusMode.disable = () => setTimeout(() => window.dispatchEvent(new Event(EVENT_OFF)), 0); + FocusMode.enable = () => setTimeout(() => window.dispatchEvent(new Event(EVENT_ON)), 0); + function HideInFocusMode({ children, style = "fade" }) { + const classes = (0, import_classnames8.default)({ + [FocusMode_default.hideInFocus]: true, + [FocusMode_default.fade]: style === "fade", + [FocusMode_default.slide]: style === "slide" + }); + return /* @__PURE__ */ y("div", { class: classes, "data-style": style }, children); + } + function useSetFocusMode() { + return T2((action) => { + document.documentElement.dataset.focusModeState = action; + }, []); + } + + // pages/duckplayer/app/components/InfoBar.jsx + function InfoBar({ embed }) { + return /* @__PURE__ */ y("div", { class: InfoBar_default.infoBar }, /* @__PURE__ */ y("div", { class: InfoBar_default.lhs }, /* @__PURE__ */ y("div", { class: InfoBar_default.dax }, /* @__PURE__ */ y("img", { src: dax_data_default, class: InfoBar_default.img })), /* @__PURE__ */ y("div", { class: InfoBar_default.text }, "Duck Player"), /* @__PURE__ */ y(InfoIcon, null)), /* @__PURE__ */ y("div", { class: InfoBar_default.rhs }, /* @__PURE__ */ y(SwitchProvider, null, /* @__PURE__ */ y("div", { class: InfoBar_default.switch }, /* @__PURE__ */ y(SwitchBarDesktop, null)), /* @__PURE__ */ y(ControlBarDesktop, { embed })))); + } + function InfoIcon({ debugStyles = false }) { + const setFocusMode = useSetFocusMode(); + const [isVisible, setIsVisible] = h2(debugStyles); + const [isBottom, setIsBottom] = h2(false); + const tooltipRef = _(null); + function show() { + setIsVisible(true); + setFocusMode("paused"); + } + function hide() { + setIsVisible(false); + setFocusMode("enabled"); + } + y2(() => { + if (!tooltipRef.current) + return; + const icon = tooltipRef.current; + const rect = icon.getBoundingClientRect(); + const iconTop = rect.top + window.scrollY; + const spaceBelowIcon = window.innerHeight - iconTop; + if (spaceBelowIcon < 125) { + return setIsBottom(false); + } + return setIsBottom(true); + }, [isVisible]); + return /* @__PURE__ */ y( + "button", + { + className: InfoBar_default.info, + "aria-describedby": "tooltip1", + "aria-expanded": isVisible, + "aria-label": "Info", + onMouseEnter: show, + onMouseLeave: hide, + onFocus: show, + onBlur: hide, + ref: tooltipRef + }, + /* @__PURE__ */ y(Icon, { src: info_data_default }), + /* @__PURE__ */ y( + Tooltip, + { + id: "tooltip1", + isVisible, + position: isBottom ? "bottom" : "top" + } + ) + ); + } + function ControlBarDesktop({ embed }) { + const settingsUrl = useSettingsUrl(); + const openOnYoutube = useOpenOnYoutubeHandler(); + const { t: t3 } = useTypedTranslation(); + const { state } = q2(SwitchContext); + return /* @__PURE__ */ y("div", { className: InfoBar_default.controls }, /* @__PURE__ */ y( + ButtonLink, + { + formfactor: "desktop", + icon: true, + highlight: state === "exiting", + anchorProps: { + "href": settingsUrl, + target: "_blank", + "aria-label": t3("openSettingsButton") + } + }, + /* @__PURE__ */ y(Icon, { src: cog_data_default }) + ), /* @__PURE__ */ y( + Button, + { + formfactor: "desktop", + buttonProps: { + onClick: () => { + if (embed) + openOnYoutube(embed); + } + } + }, + t3("watchOnYoutube") + )); + } + function InfoBarContainer({ children }) { + return /* @__PURE__ */ y("div", { class: InfoBar_default.container }, children); + } + + // pages/duckplayer/app/components/Wordmark.module.css + var Wordmark_default = { + wordmark: "Wordmark_wordmark", + logo: "Wordmark_logo", + img: "Wordmark_img", + text: "Wordmark_text" + }; + + // pages/duckplayer/app/components/Wordmark-mobile.module.css + var Wordmark_mobile_default = { + logo: "Wordmark_mobile_logo", + logoSvg: "Wordmark_mobile_logoSvg", + text: "Wordmark_mobile_text" + }; + + // pages/duckplayer/app/components/Wordmark.jsx + function Wordmark() { + return /* @__PURE__ */ y("div", { class: Wordmark_default.wordmark }, /* @__PURE__ */ y("div", { className: Wordmark_default.logo }, /* @__PURE__ */ y("img", { src: dax_data_default, className: Wordmark_default.img, alt: "DuckDuckGo logo" })), /* @__PURE__ */ y("div", { className: Wordmark_default.text }, "Duck Player")); + } + function MobileWordmark() { + return /* @__PURE__ */ y("div", { class: Wordmark_mobile_default.logo }, /* @__PURE__ */ y("span", { class: Wordmark_mobile_default.logoSvg }, /* @__PURE__ */ y("img", { src: dax_data_default, className: Wordmark_mobile_default.img, alt: "DuckDuckGo logo" })), /* @__PURE__ */ y("span", { class: Wordmark_mobile_default.text }, "Duck Player")); + } + + // pages/duckplayer/app/components/Background.module.css + var Background_default = { + bg: "Background_bg" + }; + + // pages/duckplayer/app/components/Background.jsx + function Background() { + return /* @__PURE__ */ y("div", { class: Background_default.bg }); + } + + // pages/duckplayer/app/components/Player.jsx + var import_classnames9 = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/Player.module.css + var Player_default = { + root: "Player_root", + desktop: "Player_desktop", + mobile: "Player_mobile", + player: "Player_player", + iframe: "Player_iframe", + error: "Player_error" + }; + + // pages/duckplayer/app/features/pip.js + var PIP = class { + /** + * @param {HTMLIFrameElement} iframe + */ + iframeDidLoad(iframe) { + try { + const iframeDocument = iframe.contentDocument; + const iframeWindow = iframe.contentWindow; + if (iframeDocument && iframeWindow) { + const CSSStyleSheet = ( + /** @type {any} */ + iframeWindow.CSSStyleSheet + ); + const styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync("button.ytp-pip-button { display: inline-block !important; }"); + iframeDocument.adoptedStyleSheets = [...iframeDocument.adoptedStyleSheets, styleSheet]; + } + } catch (e3) { + console.warn(e3); + } + return null; + } + }; + + // pages/duckplayer/app/features/autofocus.js + var AutoFocus = class { + /** + * @param {HTMLIFrameElement} iframe + */ + iframeDidLoad(iframe) { + const maxAttempts = 1e3; + let attempt = 0; + let id; + function check() { + if (!iframe.contentDocument) + return; + if (attempt > maxAttempts) + return; + attempt += 1; + const selector = "#player video"; + const video = ( + /** @type {HTMLIFrameElement | null} */ + iframe.contentDocument?.body.querySelector(selector) + ); + if (!video) { + id = requestAnimationFrame(check); + return; + } + video.focus(); + document.body.dataset.videoState = "loaded+focussed"; + } + id = requestAnimationFrame(check); + return () => { + cancelAnimationFrame(id); + }; + } + }; + + // ../../src/features/duckplayer/util.js + var VideoParams = class _VideoParams { + /** + * @param {string} id - the YouTube video ID + * @param {string|null|undefined} time - an optional time + */ + constructor(id, time) { + this.id = id; + this.time = time; + } + static validVideoId = /^[a-zA-Z0-9-_]+$/; + static validTimestamp = /^[0-9hms]+$/; + /** + * @returns {string} + */ + toPrivatePlayerUrl() { + const duckUrl = new URL(`duck://player/${this.id}`); + if (this.time) { + duckUrl.searchParams.set("t", this.time); + } + return duckUrl.href; + } + /** + * Create a VideoParams instance from a href, only if it's on the watch page + * + * @param {string} href + * @returns {VideoParams|null} + */ + static forWatchPage(href) { + let url; + try { + url = new URL(href); + } catch (e3) { + return null; + } + if (!url.pathname.startsWith("/watch")) { + return null; + } + return _VideoParams.fromHref(url.href); + } + /** + * Convert a relative pathname into VideoParams + * + * @param pathname + * @returns {VideoParams|null} + */ + static fromPathname(pathname) { + let url; + try { + url = new URL(pathname, window.location.origin); + } catch (e3) { + return null; + } + return _VideoParams.fromHref(url.href); + } + /** + * Convert a href into valid video params. Those can then be converted into a private player + * link when needed + * + * @param href + * @returns {VideoParams|null} + */ + static fromHref(href) { + let url; + try { + url = new URL(href); + } catch (e3) { + return null; + } + let id = null; + const vParam = url.searchParams.get("v"); + const tParam = url.searchParams.get("t"); + if (url.searchParams.has("list") && !url.searchParams.has("index")) { + return null; + } + let time = null; + if (vParam && _VideoParams.validVideoId.test(vParam)) { + id = vParam; + } else { + return null; + } + if (tParam && _VideoParams.validTimestamp.test(tParam)) { + time = tParam; + } + return new _VideoParams(id, time); + } + }; + + // pages/duckplayer/src/js/utils.js + function createYoutubeURLForError(href, urlBase) { + const valid = VideoParams.forWatchPage(href); + if (!valid) + return null; + const original = new URL(href); + if (original.searchParams.get("feature") !== "emb_err_woyt") + return null; + const url = new URL(urlBase); + url.searchParams.set("v", valid.id); + if (typeof valid.time === "string") { + url.searchParams.set("t", valid.time); + } + return url.toString(); + } + function getValidVideoTitle(iframeTitle) { + if (typeof iframeTitle !== "string") + return null; + if (iframeTitle === "YouTube") + return null; + return iframeTitle.replace(/ - YouTube$/g, ""); + } + + // pages/duckplayer/app/features/click-capture.js + var ClickCapture = class { + /** + * @param {object} params + * @param {string} params.baseUrl + */ + constructor({ baseUrl }) { + this.baseUrl = baseUrl; + } + /** + * @param {HTMLIFrameElement} iframe + */ + iframeDidLoad(iframe) { + const handler = (e3) => { + if (!e3.target) + return; + const target = ( + /** @type {Element} */ + e3.target + ); + if (!("href" in target) || typeof target.href !== "string") + return; + const next = createYoutubeURLForError(target.href, this.baseUrl); + if (!next) + return; + e3.preventDefault(); + e3.stopImmediatePropagation(); + window.location.href = next; + }; + iframe.contentDocument?.addEventListener("click", handler); + return () => { + iframe.contentDocument?.removeEventListener("click", handler); + }; + } + }; + + // pages/duckplayer/app/features/title-capture.js + var TitleCapture = class { + /** + * @param {HTMLIFrameElement} iframe + */ + iframeDidLoad(iframe) { + const setter = (title) => { + const validTitle = getValidVideoTitle(title); + if (validTitle) { + document.title = "Duck Player - " + validTitle; + } + }; + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + if (!doc) { + console.log("could not access contentDocument"); + return () => { + }; + } + if (doc.title) { + setter(doc.title); + } + if (win && doc) { + const titleElem = doc.querySelector("title"); + if (titleElem) { + const observer = new win.MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + setter(mutation.target.textContent); + }); + }); + observer.observe(titleElem, { childList: true }); + } else { + console.warn("could not access title in iframe"); + } + } else { + console.warn("could not access iframe?.contentWindow && iframe?.contentDocument"); + } + return null; + } + }; + + // pages/duckplayer/app/features/mouse-capture.js + var MouseCapture = class { + /** + * @param {HTMLIFrameElement} iframe + */ + iframeDidLoad(iframe) { + iframe.contentDocument?.addEventListener("mousemove", () => { + window.dispatchEvent(new Event("iframe-mousemove")); + }); + return null; + } + }; + + // pages/duckplayer/app/features/iframe.js + var IframeFeature = class { + /** + * @param {HTMLIFrameElement} iframe + * @returns {(() => void) | null} + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + iframeDidLoad(iframe) { + return () => { + console.log("teardown"); + }; + } + static noop() { + return { + iframeDidLoad: () => { + return () => { + }; + } + }; + } + }; + function createIframeFeatures(settings) { + return { + /** + * @return {IframeFeature} + */ + pip: () => { + if (settings.pip.state === "enabled") { + return new PIP(); + } + return IframeFeature.noop(); + }, + /** + * @return {IframeFeature} + */ + autofocus: () => { + return new AutoFocus(); + }, + /** + * @return {IframeFeature} + */ + clickCapture: () => { + return new ClickCapture({ + baseUrl: settings.youtubeBase + }); + }, + /** + * @return {IframeFeature} + */ + titleCapture: () => { + return new TitleCapture(); + }, + /** + * @return {IframeFeature} + */ + mouseCapture: () => { + return new MouseCapture(); + } + }; + } + + // pages/duckplayer/app/components/Player.jsx + function Player({ src, layout }) { + const { ref, didLoad } = useIframeEffects(src); + const wrapperClasses = (0, import_classnames9.default)({ + [Player_default.root]: true, + [Player_default.player]: true, + [Player_default.desktop]: layout === "desktop", + [Player_default.mobile]: layout === "mobile" + }); + const iframeClasses = (0, import_classnames9.default)({ + [Player_default.iframe]: true, + [Player_default.desktop]: layout === "desktop", + [Player_default.mobile]: layout === "mobile" + }); + return /* @__PURE__ */ y("div", { class: wrapperClasses }, /* @__PURE__ */ y( + "iframe", + { + class: iframeClasses, + frameBorder: "0", + id: "player", + allow: "autoplay; encrypted-media; fullscreen", + sandbox: "allow-popups allow-scripts allow-same-origin allow-popups-to-escape-sandbox", + src, + ref, + onLoad: didLoad + } + )); + } + function PlayerError({ kind, layout }) { + const { t: t3 } = useTypedTranslation(); + const errors = { + ["invalid-id"]: /* @__PURE__ */ y("span", { dangerouslySetInnerHTML: { __html: t3("invalidIdError") } }) + }; + const text = errors[kind] || errors["invalid-id"]; + return /* @__PURE__ */ y("div", { class: (0, import_classnames9.default)(Player_default.root, { + [Player_default.desktop]: layout === "desktop", + [Player_default.mobile]: layout === "mobile" + }) }, /* @__PURE__ */ y("div", { className: Player_default.error }, /* @__PURE__ */ y("p", null, text))); + } + function useIframeEffects(src) { + const ref = _( + /** @type {HTMLIFrameElement|null} */ + null + ); + const didLoad = _( + /** @type {boolean} */ + false + ); + const settings = useSettings(); + p2(() => { + if (!ref.current) + return; + const iframe = ref.current; + const features = createIframeFeatures(settings); + const iframeFeatures = [ + features.autofocus(), + features.pip(), + features.clickCapture(), + features.titleCapture(), + features.mouseCapture() + ]; + const cleanups = []; + const loadHandler = () => { + for (let feature of iframeFeatures) { + try { + cleanups.push(feature.iframeDidLoad(iframe)); + } catch (e3) { + console.error(e3); + } + } + }; + if (didLoad.current === true) { + loadHandler(); + } else { + iframe.addEventListener("load", loadHandler); + } + return () => { + for (let cleanup of cleanups) { + cleanup?.(); + } + iframe.removeEventListener("load", loadHandler); + }; + }, [src, settings]); + return { ref, didLoad: () => didLoad.current = true }; + } + + // pages/duckplayer/app/components/Components.jsx + function Components() { + const settings = new Settings({ + platform: { name: "macos" } + }); + let embed = EmbedSettings.fromHref("https://localhost?videoID=123"); + let url = embed?.toEmbedUrl(); + if (!url) + throw new Error("unreachable"); + return /* @__PURE__ */ y(g, null, /* @__PURE__ */ y("div", { "data-layout": "mobile" }, /* @__PURE__ */ y(Background, null)), /* @__PURE__ */ y("main", { class: Components_default.main }, /* @__PURE__ */ y("div", { class: Components_default.tube }, /* @__PURE__ */ y(Wordmark, null), /* @__PURE__ */ y("h2", null, "Floating Bar"), /* @__PURE__ */ y("div", { style: "position: relative; padding-left: 10em; min-height: 150px;" }, /* @__PURE__ */ y(InfoIcon, { debugStyles: true })), /* @__PURE__ */ y("h2", null, "Info Tooltip"), /* @__PURE__ */ y(FloatingBar, null, /* @__PURE__ */ y(Button, { icon: true }, /* @__PURE__ */ y(Icon, { src: info_data_default })), /* @__PURE__ */ y(Button, { icon: true }, /* @__PURE__ */ y(Icon, { src: cog_data_default })), /* @__PURE__ */ y(Button, { fill: true }, "Open in YouTube")), /* @__PURE__ */ y("h2", null, "Info Bar"), /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(SwitchProvider, null, /* @__PURE__ */ y(InfoBar, { embed }))), /* @__PURE__ */ y("br", null), /* @__PURE__ */ y("h2", null, "Mobile Switch Bar (ios)"), /* @__PURE__ */ y(SwitchProvider, null, /* @__PURE__ */ y(SwitchBarMobile, { platformName: "ios" })), /* @__PURE__ */ y("h2", null, "Mobile Switch Bar (android)"), /* @__PURE__ */ y(SwitchProvider, null, /* @__PURE__ */ y(SwitchBarMobile, { platformName: "android" })), /* @__PURE__ */ y("h2", null, "Desktop Switch bar"), /* @__PURE__ */ y("h3", null, "idle"), /* @__PURE__ */ y(SwitchProvider, null, /* @__PURE__ */ y(SwitchBarDesktop, null))), /* @__PURE__ */ y("h2", null, /* @__PURE__ */ y("code", null, "inset=false (desktop)")), /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(PlayerContainer, null, /* @__PURE__ */ y(Player, { src: url, layout: "desktop" }), /* @__PURE__ */ y(InfoBarContainer, null, /* @__PURE__ */ y(InfoBar, { embed })))), /* @__PURE__ */ y("br", null), /* @__PURE__ */ y("h2", null, /* @__PURE__ */ y("code", null, "inset=true (mobile)")), /* @__PURE__ */ y(PlayerContainer, { inset: true }, /* @__PURE__ */ y(PlayerInternal, { inset: true }, /* @__PURE__ */ y(PlayerError, { layout: "mobile", kind: "invalid-id" }), /* @__PURE__ */ y(SwitchBarMobile, { platformName: "ios" }))), /* @__PURE__ */ y("br", null))); + } + + // pages/duckplayer/app/components/MobileApp.jsx + var import_classnames10 = __toESM(require_classnames(), 1); + + // pages/duckplayer/app/components/MobileApp.module.css + var MobileApp_default = { + main: "MobileApp_main", + hideInFocus: "MobileApp_hideInFocus", + fadeout: "MobileApp_fadeout", + filler: "MobileApp_filler", + switch: "MobileApp_switch", + embed: "MobileApp_embed", + logo: "MobileApp_logo", + buttons: "MobileApp_buttons" + }; + + // pages/duckplayer/app/features/app.js + function createAppFeaturesFrom(settings) { + return { + focusMode: () => { + if (settings.focusMode.state === "enabled") { + return /* @__PURE__ */ y(FocusMode, null); + } else { + return null; + } + } + }; + } + + // pages/duckplayer/app/components/MobileButtons.module.css + var MobileButtons_default = { + buttons: "MobileButtons_buttons" + }; + + // pages/duckplayer/app/components/MobileButtons.jsx + function MobileButtons({ embed }) { + const openSettings = useOpenSettingsHandler(); + const openInfo = useOpenInfoHandler(); + const openOnYoutube = useOpenOnYoutubeHandler(); + const { t: t3 } = useTypedTranslation(); + return /* @__PURE__ */ y("div", { class: MobileButtons_default.buttons }, /* @__PURE__ */ y( + Button, + { + icon: true, + buttonProps: { + "aria-label": t3("openInfoButton"), + onClick: openInfo + } + }, + /* @__PURE__ */ y(Icon, { src: info_data_default }) + ), /* @__PURE__ */ y( + Button, + { + icon: true, + buttonProps: { + "aria-label": t3("openSettingsButton"), + onClick: openSettings + } + }, + /* @__PURE__ */ y(Icon, { src: cog_data_default }) + ), /* @__PURE__ */ y( + Button, + { + fill: true, + buttonProps: { + onClick: () => { + if (embed) + openOnYoutube(embed); + } + } + }, + t3("watchOnYoutube") + )); + } + + // pages/duckplayer/app/providers/OrientationProvider.jsx + function OrientationProvider({ onChange }) { + p2(() => { + if (!screen.orientation?.type) + return; + onChange(getOrientationFromScreen()); + const handleOrientationChange = () => { + onChange(getOrientationFromScreen()); + }; + screen.orientation.addEventListener("change", handleOrientationChange); + return () => screen.orientation.removeEventListener("change", handleOrientationChange); + }, []); + p2(() => { + let timer; + const listener = () => { + clearTimeout(timer); + timer = setTimeout(() => onChange(getOrientationFromWidth()), 300); + }; + window.addEventListener("resize", listener); + return () => window.removeEventListener("resize", listener); + }, []); + return null; + } + function getOrientationFromWidth() { + return window.innerWidth > window.innerHeight ? "landscape" : "portrait"; + } + function getOrientationFromScreen() { + return screen.orientation.type.includes("landscape") ? "landscape" : "portrait"; + } + + // pages/duckplayer/app/components/MobileApp.jsx + var DISABLED_HEIGHT = 450; + function MobileApp({ embed }) { + const settings = useSettings(); + const features = createAppFeaturesFrom(settings); + return /* @__PURE__ */ y(g, null, /* @__PURE__ */ y(Background, null), features.focusMode(), /* @__PURE__ */ y(OrientationProvider, { onChange: (orientation) => { + if (orientation === "portrait") { + return FocusMode.enable(); + } + if (window.innerHeight < DISABLED_HEIGHT) { + return FocusMode.disable(); + } + return FocusMode.enable(); + } }), /* @__PURE__ */ y(MobileLayout, { embed })); + } + function MobileLayout({ embed }) { + const platformName = usePlatformName(); + return /* @__PURE__ */ y("main", { class: MobileApp_default.main }, /* @__PURE__ */ y("div", { class: (0, import_classnames10.default)(MobileApp_default.filler, MobileApp_default.hideInFocus) }), /* @__PURE__ */ y("div", { class: MobileApp_default.embed }, embed === null && /* @__PURE__ */ y(PlayerError, { layout: "mobile", kind: "invalid-id" }), embed !== null && /* @__PURE__ */ y(Player, { src: embed.toEmbedUrl(), layout: "mobile" })), /* @__PURE__ */ y("div", { class: (0, import_classnames10.default)(MobileApp_default.logo, MobileApp_default.hideInFocus) }, /* @__PURE__ */ y(MobileWordmark, null)), /* @__PURE__ */ y("div", { class: (0, import_classnames10.default)(MobileApp_default.switch, MobileApp_default.hideInFocus) }, /* @__PURE__ */ y(SwitchProvider, null, /* @__PURE__ */ y(SwitchBarMobile, { platformName }))), /* @__PURE__ */ y("div", { class: (0, import_classnames10.default)(MobileApp_default.buttons, MobileApp_default.hideInFocus) }, /* @__PURE__ */ y(MobileButtons, { embed }))); + } + + // pages/duckplayer/app/components/DesktopApp.module.css + var DesktopApp_default = { + app: "DesktopApp_app", + portrait: "DesktopApp_portrait", + landscape: "DesktopApp_landscape", + wrapper: "DesktopApp_wrapper", + desktop: "DesktopApp_desktop", + rhs: "DesktopApp_rhs", + header: "DesktopApp_header", + main: "DesktopApp_main", + controls: "DesktopApp_controls", + switch: "DesktopApp_switch" + }; + + // pages/duckplayer/app/components/DesktopApp.jsx + function DesktopApp({ embed }) { + const settings = useSettings(); + const features = createAppFeaturesFrom(settings); + return /* @__PURE__ */ y(g, null, /* @__PURE__ */ y(Background, null), features.focusMode(), /* @__PURE__ */ y("main", { class: DesktopApp_default.app }, /* @__PURE__ */ y(DesktopLayout, { embed }))); + } + function DesktopLayout({ embed }) { + return /* @__PURE__ */ y("div", { class: DesktopApp_default.desktop }, /* @__PURE__ */ y(PlayerContainer, null, embed === null && /* @__PURE__ */ y(PlayerError, { layout: "desktop", kind: "invalid-id" }), embed !== null && /* @__PURE__ */ y(Player, { src: embed.toEmbedUrl(), layout: "desktop" }), /* @__PURE__ */ y(HideInFocusMode, { style: "slide" }, /* @__PURE__ */ y(InfoBarContainer, null, /* @__PURE__ */ y(InfoBar, { embed }))))); + } + + // pages/duckplayer/app/index.js + async function init(messaging2, baseEnvironment2) { + const result = await callWithRetry(() => messaging2.initialSetup()); + if ("error" in result) { + throw new Error(result.error); + } + const init2 = result.value; + console.log("INITIAL DATA", init2); + const environment = baseEnvironment2.withEnv(init2.env).withLocale(init2.locale).withLocale(baseEnvironment2.urlParams.get("locale")).withTextLength(baseEnvironment2.urlParams.get("textLength")).withDisplay(baseEnvironment2.urlParams.get("display")); + console.log("environment:", environment); + console.log("locale:", environment.locale); + document.body.dataset.display = environment.display; + const strings = environment.locale === "en" ? duckplayer_default : await getTranslationsFromStringOrLoadDynamically(init2.localeStrings, environment.locale) || duckplayer_default; + const settings = new Settings({}).withPlatformName(baseEnvironment2.injectName).withPlatformName(init2.platform?.name).withPlatformName(baseEnvironment2.urlParams.get("platform")).withFeatureState("pip", init2.settings.pip).withFeatureState("autoplay", init2.settings.autoplay).withFeatureState("focusMode", init2.settings.focusMode).withDisabledFocusMode(baseEnvironment2.urlParams.get("focusMode")); + console.log(settings); + const embed = createEmbedSettings(window.location.href, settings); + const didCatch = (error) => { + const message = error?.message || "unknown"; + messaging2.reportPageException({ message }); + }; + document.body.dataset.layout = settings.layout; + const root = document.querySelector("body"); + if (!root) + throw new Error("could not render, root element missing"); + if (environment.display === "app") { + q( + /* @__PURE__ */ y( + EnvironmentProvider, + { + debugState: environment.debugState, + injectName: environment.injectName, + willThrow: environment.willThrow + }, + /* @__PURE__ */ y(ErrorBoundary, { didCatch, fallback: /* @__PURE__ */ y(Fallback, { showDetails: environment.env === "development" }) }, /* @__PURE__ */ y(UpdateEnvironment, { search: window.location.search }), /* @__PURE__ */ y(MessagingContext2.Provider, { value: messaging2 }, /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(UserValuesProvider, { initial: init2.userValues }, settings.layout === "desktop" && /* @__PURE__ */ y(TranslationProvider, { translationObject: duckplayer_default, fallback: duckplayer_default, textLength: environment.textLength }, /* @__PURE__ */ y(DesktopApp, { embed })), settings.layout === "mobile" && /* @__PURE__ */ y(TranslationProvider, { translationObject: strings, fallback: duckplayer_default, textLength: environment.textLength }, /* @__PURE__ */ y(MobileApp, { embed })), /* @__PURE__ */ y(WillThrow, null))))) + ), + root + ); + } else if (environment.display === "components") { + q( + /* @__PURE__ */ y(EnvironmentProvider, { debugState: false, injectName: environment.injectName }, /* @__PURE__ */ y(MessagingContext2.Provider, { value: messaging2 }, /* @__PURE__ */ y(TranslationProvider, { translationObject: duckplayer_default, fallback: duckplayer_default, textLength: environment.textLength }, /* @__PURE__ */ y(Components, null)))), + root + ); + } + } + function createEmbedSettings(href, settings) { + const embed = EmbedSettings.fromHref(href); + if (!embed) + return null; + return embed.withAutoplay(settings.autoplay.state === "enabled").withMuted(settings.platform.name === "ios"); + } + async function getTranslationsFromStringOrLoadDynamically(stringInput, locale) { + if (stringInput) { + try { + return JSON.parse(stringInput); + } catch (e3) { + console.warn("String could not be parsed. Falling back to fetch..."); + } + } + try { + const response = await fetch(`./locales/${locale}/duckplayer.json`); + if (!response.ok) { + console.error("Network response was not ok"); + return null; + } + return await response.json(); + } catch (e3) { + console.error("Failed to fetch or parse JSON from the network:", e3); + return null; + } + } + + // pages/duckplayer/src/js/storage.js + function deleteStorage(subject) { + Object.keys(subject).forEach((key) => { + if (key.indexOf("yt-player") === 0) { + return; + } + subject.removeItem(key); + }); + } + function deleteAllCookies() { + const cookies = document.cookie.split(";"); + for (let i3 = 0; i3 < cookies.length; i3++) { + const cookie = cookies[i3]; + const eqPos = cookie.indexOf("="); + const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=youtube-nocookie.com;path=/;"; + } + } + function initStorage() { + window.addEventListener("unload", () => { + deleteStorage(localStorage); + deleteStorage(sessionStorage); + deleteAllCookies(); + }); + window.addEventListener("load", () => { + deleteStorage(localStorage); + deleteStorage(sessionStorage); + deleteAllCookies(); + }); + } + + // pages/duckplayer/src/js/index.js + var DuckplayerPage = class { + /** + * @param {import("@duckduckgo/messaging").Messaging} messaging + */ + constructor(messaging2, injectName) { + this.messaging = createTypedMessages(this, messaging2); + this.injectName = injectName; + } + /** + * This will be sent if the application has loaded, but a client-side error + * has occurred that cannot be recovered from + * @returns {Promise} + */ + initialSetup() { + if (this.injectName === "integration") { + return Promise.resolve({ + platform: { name: "ios" }, + env: "development", + userValues: { privatePlayerMode: { alwaysAsk: {} }, overlayInteracted: false }, + settings: { + pip: { + state: "enabled" + }, + autoplay: { + state: "enabled" + } + }, + locale: "en" + }); + } + return this.messaging.request("initialSetup"); + } + /** + * This is sent when the user wants to set Duck Player as the default. + * + * @param {import("../../../../types/duckplayer").UserValues} userValues + */ + setUserValues(userValues) { + return this.messaging.request("setUserValues", userValues); + } + /** + * For platforms that require a message to open settings + */ + openSettings() { + return this.messaging.notify("openSettings"); + } + /** + * For platforms that require a message to open info modal + */ + openInfo() { + return this.messaging.notify("openInfo"); + } + /** + * This is a subscription that we set up when the page loads. + * We use this value to show/hide the checkboxes. + * + * **Integration NOTE**: Native platforms should always send this at least once on initial page load. + * + * - See {@link Messaging.SubscriptionEvent} for details on each value of this message + * + * ```json + * // the payload that we receive should look like this + * { + * "context": "specialPages", + * "featureName": "duckPlayerPage", + * "subscriptionName": "onUserValuesChanged", + * "params": { + * "overlayInteracted": false, + * "privatePlayerMode": { + * "enabled": {} + * } + * } + * } + * ``` + * + * @param {(value: import("../../../../types/duckplayer").UserValues) => void} cb + */ + onUserValuesChanged(cb) { + return this.messaging.subscribe("onUserValuesChanged", cb); + } + /** + * This will be sent if the application has loaded, but a client-side error + * has occurred that cannot be recovered from + * @param {{message: string}} params + */ + reportPageException(params) { + this.messaging.notify("reportPageException", params); + } + /** + * This will be sent if the application fails to load. + * @param {{message: string}} params + */ + reportInitException(params) { + this.messaging.notify("reportInitException", params); + } + }; + var baseEnvironment = new Environment().withInjectName(document.documentElement.dataset.platform).withEnv("production"); + var messaging = createSpecialPageMessaging({ + injectName: baseEnvironment.injectName, + env: baseEnvironment.env, + pageName: "duckPlayerPage" + }); + var example = new DuckplayerPage(messaging, "android"); + init(example, baseEnvironment).catch((e3) => { + console.error(e3); + const msg = typeof e3?.message === "string" ? e3.message : "unknown init error"; + example.reportInitException({ message: msg }); + }); + initStorage(); +})(); +/*! Bundled license information: + +classnames/index.js: + (*! + Copyright (c) 2018 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames + *) +*/ diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js new file mode 100644 index 000000000000..6f18bb2bfd2e --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js @@ -0,0 +1,14 @@ +"use strict"; +(() => { + // pages/duckplayer/src/js/inline.js + var param = new URLSearchParams(window.location.search).get("platform"); + if (isAllowed(param)) { + document.documentElement.dataset.platform = String(param); + } else { + document.documentElement.dataset.platform = "android"; + } + function isAllowed(input) { + const allowed = ["windows", "apple", "integration"]; + return allowed.includes(input); + } +})(); diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.example.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.example.js new file mode 100644 index 000000000000..1ce11d984b14 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.example.js @@ -0,0 +1,43 @@ +/** + * The user wishes to enable DuckPlayer + * @satisfies {import("./messages").UserValues} + */ +const enabled = { + privatePlayerMode: { enabled: {} }, + overlayInteracted: false +} + +console.log(enabled) + +/** + * The user wishes to disable DuckPlayer + * @satisfies {import("./messages").UserValues} + */ +const disabled = { + privatePlayerMode: { disabled: {} }, + overlayInteracted: false +} + +console.log(disabled) + +/** + * The user wishes for overlays to always show + * @satisfies {import("./messages").UserValues} + */ +const alwaysAsk = { + privatePlayerMode: { alwaysAsk: {} }, + overlayInteracted: false +} + +console.log(alwaysAsk) + +/** + * The user wishes only for small overlays to show, not the blocking video ones + * @satisfies {import("./messages").UserValues} + */ +const alwaysAskRemembered = { + privatePlayerMode: { alwaysAsk: {} }, + overlayInteracted: true +} + +console.log(alwaysAskRemembered) diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.js new file mode 100644 index 000000000000..60b35fcbd077 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/messages.js @@ -0,0 +1,150 @@ +/** + * @typedef {object} InitialSetup - The initial payload used to communicate render-blocking information + * @property {UserValues} userValues - The state of the user values + * @property {DuckPlayerPageSettings} settings - Additional settings + */ + +/** + * Notifications or requests that the Duck Player Page will + * send to the native side + */ +export class DuckPlayerPageMessages { + /** + * @param {import("@duckduckgo/messaging").Messaging} messaging + * @param {ImportMeta["injectName"]} injectName + * @internal + */ + constructor (messaging, injectName) { + /** + * @internal + */ + this.messaging = messaging + this.injectName = injectName + } + + /** + * This is sent when the user wants to set Duck Player as the default. + * + * @returns {Promise} params + */ + initialSetup () { + if (this.injectName === 'integration') { + return Promise.resolve({ + settings: { + pip: { + state: 'enabled' + } + }, + userValues: new UserValues({ + overlayInteracted: false, + privatePlayerMode: { alwaysAsk: {} } + }) + }) + } + return this.messaging.request('initialSetup') + } + + /** + * This is sent when the user wants to set Duck Player as the default. + * + * @param {UserValues} userValues + */ + setUserValues (userValues) { + return this.messaging.request('setUserValues', userValues) + } + + /** + * This is sent when the user wants to set Duck Player as the default. + * @return {Promise} + */ + getUserValues () { + if (this.injectName === 'integration') { + return Promise.resolve(new UserValues({ + overlayInteracted: false, + privatePlayerMode: { alwaysAsk: {} } + })) + } + return this.messaging.request('getUserValues') + } + + /** + * This is a subscription that we set up when the page loads. + * We use this value to show/hide the checkboxes. + * + * **Integration NOTE**: Native platforms should always send this at least once on initial page load. + * + * - See {@link Messaging.SubscriptionEvent} for details on each value of this message + * - See {@link UserValues} for details on the `params` + * + * ```json + * // the payload that we receive should look like this + * { + * "context": "specialPages", + * "featureName": "duckPlayerPage", + * "subscriptionName": "onUserValuesChanged", + * "params": { + * "overlayInteracted": false, + * "privatePlayerMode": { + * "enabled": {} + * } + * } + * } + * ``` + * + * @param {(value: UserValues) => void} cb + */ + onUserValuesChanged (cb) { + return this.messaging.subscribe('onUserValuesChanged', cb) + } +} + +/** + * This data structure is sent to enable user settings to be updated + * + * ```js + * [[include:packages/special-pages/pages/duckplayer/src/js/messages.example.js]]``` + */ +export class UserValues { + /** + * @param {object} params + * @param {{enabled: {}} | {disabled: {}} | {alwaysAsk: {}}} params.privatePlayerMode + * @param {boolean} params.overlayInteracted + */ + constructor (params) { + /** + * 'enabled' means 'always play in duck player' + * 'disabled' means 'never play in duck player' + * 'alwaysAsk' means 'show overlay prompts for using duck player' + * @type {{enabled: {}}|{disabled: {}}|{alwaysAsk: {}}} + */ + this.privatePlayerMode = params.privatePlayerMode + /** + * `true` when the user has asked to remember a previous choice + * + * `false` if they have never used the checkbox + * @type {boolean} + */ + this.overlayInteracted = params.overlayInteracted + } +} + +/** + * Sent in the initial page load request. Used to provide features toggles + * and other none-user-specific settings. + * + * Note: This will be improved soon with better remote config integration. + */ +export class DuckPlayerPageSettings { + /** + * @param {object} params + * @param {object} params.pip + * @param {"enabled" | "disabled"} params.pip.state + */ + constructor (params) { + /** + * 'enabled' means that the FE should show the PIP button + * 'disabled' means that the FE should never show it + */ + this.pip = params.pip + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/mobile-bg-GCRU67TC.jpg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/mobile-bg-GCRU67TC.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aefa076350907a94554d7ddbcd95e9ca5031a88f GIT binary patch literal 7700 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 Qd71tn3m79WTfkla4cj{$&Cc1~q8q%&em+Pa$^s%0Dz*-&n`+T0Zi-JB7{K`}MvE)@>e zB#elTj#6w*h3QtwK{%&ds?+Iw`d!mG-#>nj$M>(_w3MO&)4g^ z-Zx+Fe|ZKlTQ{*b0R#d71pI+7k3bsG`zreSdMMO96w1&5jW#eeHkuEA#(0bg1|Imu z-{2PEzFBN*@$I)3wkvI{Y*x~!RN7|O%|Stj5&y3feEAg^qjYv7*XSY$K*t!NYmE4E z9Z+DHUj_Ut2pwHLeI)At?uKCC0P@R2V5ExxIv8CHH29ClzgGYK|6d0F%fSC%22B4~ zcZ`2V4E|S*e+S_HfBVK{dI7&=HV5egunI{qImZA8!? z19&Z3QElK0P+B4|0IQ?pCc*#+VYgNRNS(kS<85YR9ZrFly^- zq2_X(yC{G`xS)~#Ak`u_1;N;+<@T#!T%c}4>O(|iQw8ucJb~tn6l-5fxN8~~Dea&rwgZFm<PPTSuO`Eob-eqQZ$q?aax4bQx-c&2J#ZgDwOtl4p zK$#H3bYL66q4n)c6)LZ}45b2ijWY?t&+#&2l?;!GL#7 zq~UfwJGXRJu$7d9riOBe><1*4bwq}kLnUDfJ3?Rh8ZvzKfENZJFc_pgH_U}|mQx|a zxAuEKvafxz0#0^;tH7_2^xFCA;jD0*^1?YpHy%X-8DT#-mG4MopW~d8nsqen@pbWK zRKipubdU_BIa&4-#GbJB=KV(7bsn)@5v7yvTMl!U zOJZOeq}alM95QxF>BGG&R|>g}eo7gxPO(asVCN*m$xRh*csI$mYEfQ|G!Cy0-IBkW zUC|pZ{goA$ecqhiz_~FK+T^pVuboah&|z30F;cR*>~xrW(Pf35yPWr^IKer=UQrrgKkcWKJIU;&ZT2!hiQHE| zi&ecXRA4106=jG8yub-(TBd`lm%locRic_$&8J0B|9tg8xwTg2SHP%+5soHnq-reX^Uxn7zJ8k1?tZ7Yo9Y1!6Xoh^E?ZR0F}wH7Jt0fx z6zx=@sk5Igjq%xkTNuaPq;RU$C}5R&aBG_feVYV0d|s%)`W(l)u`qu%D~?jw8@8J7 zOr`LRCeS54*^TLaW54XeH#gNb%WDm4mz$yh*2H3ql~8weBije}_)*x7wJrETwIVKA z_)S}}W^Ay)PZ%Kd3la$Zq8~&_rsVe?t|B|lcdZ}r;D1MI;PsTU%vF42>QCL~g{|sv zHK#kg+q`GvqQ4UTPVazO*zT*{&Qh=pYuBnxe$SFOg+2Y7PfzUYBc$>rX@!(dT_FI znxkpfv^dEp4)_FU6679F6bblyk1Crrr$cJus$*=cCEuuudd<}px788qeNyF`+l5M+ za?QAOT)o%Yk$QAP#LST(t}JSxMS#2a(C%C{ib%w;&_tvDGKp{J4dLk)dJF!_MDr#2 z^jgiOy5jEHnyWSa|+Yjgp(d9cOpVfBDBWf zs8VV%?fC>vsy4q#qm7h2=j$JmC+{LYik3|IDtw~`?SC>gb;0Bwn_0qo*Eu>~6y!8)?O7Ug&PL zJ}0CE_VB`}c&WOvWSnPBYs`K;N}ed=Z-^W<8ljG@Unzd#w7*ui!-E@ScTNOQsqBhO zi=I%2etfo|^$9ItuRJg)Fy=tnY!^MMwX`#@s zHmCP-&bS8zTBzYg`}oi_oK|V8+Q%4zRkKIRl3KfYN$*rQ*Pe*%BYrxwv~H}aS)uT4 z^E+i{08p7|G}bjdw9Y-tepSiKi*^B7^3W%7Po8cl(l~d#9sa~W_;|7-?BMW$j@z&N zH6QF-oNI>_GP$oJ`gBAzKC``88lTtBsGw!6&~xH6fpV%JUEe!5&`n-$nJ zWapG6->10qKOj$-17AT|1t&_)SMVzW1=Uf9vX6 zGJLAaBv8a#Zrv!rwZ6EPG?^qAK5#&ys5|U*A%6JJn4dilPPQiuzv-Ci^ML7Y7s7N` zeQ1^MNsuK(^$X*jzKb0WtFlbWPf}Ked3*3YcAEG!zvyYECZop!qEEYD-s2Jswpip@8=JRo@_r}l+q9@pcyq@gnC+l<5cFh+7WZU- z;HQMi6Gf9{#nU-4amfS2ADsJrlKZ)XZHnlz=zxkV*)u);SfOn{Ert?95m+}`H(IGO zDN+^OWo=8?qu4OB!ZB)J@f!XHz1gdV`v)JyH$S}Bf|Gfj%R&$cXjG;{zNOVf#f^c9 zsIl`9_YUsFm8(Jaf4-{vGk)^wVS4;T|6%G-)tjrOj`FGa;qedcuaaXDlI;fjcWm3= zxUa>sDty?aDtt0yn0LUElh64|0IYJUV;bCoApx{e^NF6a$cYKX^ym3evUA$3w`H9@ z8{&o}y1M2Othc3x*4e5ruKyOM{FPnwJ57sHKG}D0Qd#vuS@-GP)1c9do`srEp2Y)` zSEY~uxkrqLkSmzvN?yeG+}rfFlvL_y!=|=Zy@T)@ic|#@e-+D29ERl^jZ)&S7fxi4 zmc`wOd)GF5=r6h5Lq$TuVqv`8z{$YYn208A5mT}+`RLu)xt})e$=$hwJ1V<+aB!^u zFfIP6XOQ#NAbP^Y(9i7n$^Ny&@A?mx(qq<6R?W61G%IS`?#9czVkf2a2>KX3)U!~T zl<)2F++*DRRD)|;FSW2c&hb>YdAFluAGM5ng0(?-!i<>o-(VTu8RzYA-zwMBDjCm;)Zsn?CRe^1IMz3qWW65}!aW*x3Jgm%S zLsB29@3wI0U362+xhJb0KAAt??#_TM$Rm>2w|HBWwmzo|^d_Q{9wkL3PIJfPF(+p0 zf(}6Z@uQH8naQCu(N`wwI;cZIHlD@m4|h9Wt<@AwDw>~g|Drw1dB9L6g5hQUKd7K@}4x^e~pXf7@o-7!;I4%qb64VVs8t|tpcYEbhjVd;U zMo6jXqPR<0PCa~jdsF)X`I}*dGI?le@uc#i$6Lt7hSKDHe+*SQ4{@gZtCC|X<`zT1 zqkwx)0xARGee9l1;Lj|ZNY)TGhE}>45&<{C(7;AiK4pta#W&}hOS}2&Lc`9`ZuKdJ z_utOS!q=Mfj#X#{f-xU z+Ha`*Gt$I7wyL6=qc-dQ_D#v?w-cl@cE9j@gX)^-b`P6YPZl{5vnb^V>o7;_@ciXU zs@_;lY@Ki2+5$<-)o%tzJL1QN4$~5zdaj4-ap>aCpyV02ENymPftynC@c5@F`QWg8 zuxbAHRVK01g_ES|Z1rV+RrqwKvx#;|ZfUh8eNW->SW-uaZrbBGRX0t|o`4?*2^jbd zQ%6_(B@pPsr)ll`6MQSiSU51ef@4LU@LM4KObFirz!%WDGvo^h?bJQL@S{W5--yp$ ze=~xi!Msm*bl(B9AJ6K(gf9!nJ9R%_KEwbUF<|M*x0_p!ErW0Cpv!2X^}LYJumyA* z-yP4TuXO;r;{b67B4CHE6jSO|Y zzzbd7`k4sj-*-rF%@883HWVCKf`4#q3F7_2%_AE^j^}pOzXxV!@$jQTQPG@^nLmh- zEM1uQc0u-nlbh>-?t?TD76gN@_W1h%2y0S;^+X}>H`;z$kZFw#InIFZy!zz;5mJp$ zNxUB%j1we;+!OXsY@@G4g9V}iK!(5isjk-mj@@4(T6&TapFlXq zc5txK(gvIWc>n;irn4S^bxXTCk68W|a=iRS&*MVmF)YFYE4=U=00DjqQP(zISB?mY zkZ?jG(vRUd$8-NYb9uqhbBs46sRhDTUpu3R7x)>I4h+lo?x1v^(GBA)A|r%24= z_a71guz{h!WXZf>GZ;UXRpUU?4N4do3Sz)xV7^D>fYVR_XpgLKk2EwSUpBYlcWOzG zU#k9DIv_R+7qdtyB<*QU(Ud>{po{6cmJ?NNYP>*ily_EMkfB$Q8Idl*`Lo`F&zt7u zQashE2W)}`2$C3?HfkXNz>_{**EWS0;fsc?oBcV7!vtQr|KgnV^y8fi3lTHAy4UXP zPavgJ=g#;KKtPBx8r6zIMLhrerWO0tfUu$yLCU+wvF*cp0(Ny7PaBE+n_cMidR-De3ljhzXkE|QqZLX*&>T(O4-%c&$` zJ|8nMpv?g+0$_MEpj%09c+;RXlyGb|E$yU3Q!rx|AHS5;LlVObK>Ed4Em{@{`oqIu zp+HB$i*rU3C7g(YoY4r0XXo#ve6T?T5ck>Z^-V}p3X9aEb;;10v%viDPA33e0$ne1 zJEMN(H!Hs(Hz>*Nk&WdJoq%q%cI`qi@2Khxy_Dquy-Fe8Ey(=rA`y0r)|~+0B{(R0 z>5_fW8z(}N3_3eEq#Wn-JKrY2K&9GzYh8I#uv=Jb(vDS?Ywdu)KMvA~+c%mx>`ZP@ z`<&|h-Qfitd|c|OKDtn!mG&|L1}cX1%!R3KLslHLTSa}RPN$9%a?={2^%$%V5EVgrqbm>6HERL`L9sB7N5{S^vU~Dtw7}Sq(XUAtW08fxq#UdaM*KF_NpAS{ zt3Q==E!;ZbP8;%io$Bhi=Qy{F7-4JxDDt_Qz^ zh3#+IrSPYXQBCorgn!M1*0I^LCWjYFsISE*VOb;(1tp_5guE|fODT!s1WLFXYC|_- zY`B>9;P@*CBZ|@ zif*qp;Y6H)H##!>BBrB<)P?7HkYOkYV@E)6V1!^Syn=OHw#8!eD0@XImpZTgbI;S= z2=945p1U;C$<3)7i}mt`*Gfmu)Mss;=G-N39yuTM35;FZx4R?zQ;2LW@}=9F&7W}x zR$7++akHp9yX~I$jc*03{JYD`(@*|clkL5BZ_m{+f9up(|N4NT=N;B^%ZoD8k-cUE zpEuR*4?X(pn@-7<-Q5G#2f|zje!sEmxJ0%8n){P=-Z$NMBBQzon{Tdan*Z#a@gpDD(N->^(3GV{$Quey5c4K&}s%dtB2RjjS&uy?|;;LUeqp9#wbE_jQt z|9W`aU(YdQBiCo2AC|Snz{HY_wNEfm5PFDz5(_ZK77jWL7cYY2w&1;a^>Yc;K_IjV z&6oUADj1ac$+-$)<@=VHfx)`JlmB|J+^L+U|7TuZ4p%WZdRQ&rFZ?4xct^7{rXu;; z2kzuT-{*hhT92Qt!z)OyWB=l>Po^twD~})j@ap`PL;6oCL+%e`ewPJs z+VJKYdcqLwTf=WW{LOeQeJ6JYJCIMkF?*6dHW$F1K7C$d`-kYy!TVwz>;B$5rk@oWMWKX(U3mX4i+dVSg`Z&17%pq38 zws9`IBnzC);dUM4@p!Rg%InV#xw^WTS>k&mN`4vG#GbdaJ|7P8ja2WAwes-eOG+H1 z@N%QIxl}4GX-&kvb`8$RmR!lm{#M6OOYIG7I2A9(R@!wZJR>7}$x4c)zRrT=e6sqR zUO2!vdh6h-6@K93R?CzntYa)`d(~SwSr;wQIF(2wt`VN?iY8Jvb3HjyX)(0nmuDLr zn-Pga?xo9`QQ5{vYaE7ss8njwP9G{QsgOvtL}=8gYsTjE2XGo_5<6oK=OGg5(wh_3 z3qBaBK5KEScYdF$?X{R{0jBCFfd%Wk#e~vW& zapo8IV_N$d+xDvDTUp&uF=HxL^%T}D(HOu-ZuqBt^!}%4jpv@Zb+!c)Vntso7OH3~ zw(BaUc;oqBf9zBKOTuUlaKz)_oUIo_wm! ztw8k%1tVRPHkF*4HoKTgKo1a5OiYR5&7IaX8;3Lky8b8!t^6~#gt-^R48O$;w~2Wi zSjWedFioz{UKC%lOASD)rVdpQLYX6WX@u1n>1vZPf&h1WOPY-d&y#CoQt*lpiXCW1 zN5YBcoA7-yPmT~GjgbxLa2rh8t#xUyH>O@P#1WtnwJO5##AR)1uz{-}k*uy0(2WGt zD$nveZ$@YDv@tR1nJ8uevH??ebmXrWObiS#HV^fKWssBdJ*E|2lL!QX>&&v03rBi! zXCaa_s~U71iW&d;cCr$q_@^V%`Uk_e8$$$hSknju2hOkB%C4r%qfE5_3jP3VsDfn4eteI)1Qkx+ zI(aLN5dSgE?9J%w^}>h#S~zfGXyo{5;R9_`du{*JC~VnT`pJISwtP!imIr6~;h5v7 h+2_W~KboVyfRhxcxh(%J)PB9e?B>N~ITK$V{2#E-xmy4L literal 0 HcmV?d00001 diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/storage.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/storage.js new file mode 100644 index 000000000000..e32740f51d9b --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/storage.js @@ -0,0 +1,32 @@ +function deleteStorage (subject) { + Object.keys(subject).forEach((key) => { + if (key.indexOf('yt-player') === 0) { + return + } + subject.removeItem(key) + }) +} + +function deleteAllCookies () { + const cookies = document.cookie.split(';') + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i] + const eqPos = cookie.indexOf('=') + const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=youtube-nocookie.com;path=/;' + } +} + +export function initStorage () { + window.addEventListener('unload', () => { + deleteStorage(localStorage) + deleteStorage(sessionStorage) + deleteAllCookies() + }) + + window.addEventListener('load', () => { + deleteStorage(localStorage) + deleteStorage(sessionStorage) + deleteAllCookies() + }) +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/utils.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/utils.js new file mode 100644 index 000000000000..4a88871a9234 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/utils.js @@ -0,0 +1,40 @@ +import { VideoParams } from '../../../../../../src/features/duckplayer/util' + +/** + * @param {string} href + * @param {string} urlBase + * @return {null | string} + */ +export function createYoutubeURLForError (href, urlBase) { + const valid = VideoParams.forWatchPage(href) + if (!valid) return null + + // this will not throw, since it was guarded above + const original = new URL(href) + + // for now, we're only intercepting clicks when `emb_err_woyt` is present + // this may not be enough to cover all situations, but it solves our immediate + // problems whilst keeping the blast radius low + if (original.searchParams.get('feature') !== 'emb_err_woyt') return null + + // if we get this far, we think a click is occurring that would cause a navigation loop + // construct the 'next' url + const url = new URL(urlBase) + url.searchParams.set('v', valid.id) + + if (typeof valid.time === 'string') { + url.searchParams.set('t', valid.time) + } + + return url.toString() +} + +/** + * @param {string|null|undefined} iframeTitle + * @return {string | null} + */ +export function getValidVideoTitle (iframeTitle) { + if (typeof iframeTitle !== 'string') return null + if (iframeTitle === 'YouTube') return null + return iframeTitle.replace(/ - YouTube$/g, '') +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/bg/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/bg/duckplayer.json new file mode 100644 index 000000000000..d7f3d20ef74c --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/bg/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Видеоклиповете в YouTube винаги да се отварят тук", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Duck Player да остане включен", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Отваряне на информацията", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Отваряне на настройки", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Гледане в YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ГРЕШКА: невалиден идентификатор на видеоклипа", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player осигурява чисто изживяване без персонализирани реклами в YouTube и предотвратява влиянието на вече гледаните видеоклипове върху препоръките на YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/cs/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/cs/duckplayer.json new file mode 100644 index 000000000000..8d24c5726549 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/cs/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Vždy otevírat videa YouTube tady", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Nechat zapnutý přehrávač Duck Player", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Otevřít informace", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Otevřené nastavení", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Sledovat na YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "CHYBA: Neplatné ID videa", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Přehrávač Duck Player nabízí sledování v minimalistickém prostředí bez personalizovaných reklam a brání tomu, aby sledovaná videa ovlivňovala tvoje doporučení na YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/da/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/da/duckplayer.json new file mode 100644 index 000000000000..f99ab298e425 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/da/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Åbn altid YouTube-videoer her", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Hold Duck Player slået til", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Åbn info", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Åbn Indstillinger", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Se på YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "FEJL: Ugyldigt video-ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player giver en ren seeroplevelse uden målrettede annoncer og forhindrer, at visningsaktivitet påvirker dine YouTube-anbefalinger." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/de/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/de/duckplayer.json new file mode 100644 index 000000000000..0ddca103a2f9 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/de/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "YouTube-Videos immer hier öffnen", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Duck Player aktiviert lassen", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Info öffnen", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Einstellungen öffnen", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Auf YouTube ansehen", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "FEHLER: Ungültige Video-ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Mit Duck Player kannst du dir ungestört und ohne personalisierte Werbung Inhalte ansehen. Er verhindert, dass das, was du dir ansiehst, deine YouTube-Empfehlungen beeinflussen." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/el/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/el/duckplayer.json new file mode 100644 index 000000000000..d00195f5e350 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/el/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Ανοίγετε πάντα τα βίντεο του YouTube εδώ", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Διατηρήστε το Duck Player ενεργοποιημένο", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Ανοίξτε για πληροφορίες", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Άνοιγμα ρυθμίσεων", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Παρακολούθηση στο YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ΣΦΑΛΜΑ: Μη έγκυρο αναγνωριστικό βίντεο", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Το Duck Player παρέχει μια καθαρή εμπειρία προβολής χωρίς εξατομικευμένες διαφημίσεις, ενώ εμποδίζει τη δραστηριότητα προβολής να επηρεάσει τις συστάσεις που θα λαμβάνετε στο YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/en/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/en/duckplayer.json new file mode 100644 index 000000000000..c2b5683b9f0e --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/en/duckplayer.json @@ -0,0 +1,39 @@ +{ + "smartling": { + "string_format": "icu", + "translate_paths": [ + { + "path": "*/title", + "key": "{*}/title", + "instruction": "*/note" + } + ] + }, + "alwaysWatchHere": { + "title": "Always open YouTube videos here", + "note": "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled": { + "title": "Keep Duck Player turned on", + "note": "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton": { + "title": "Open Info", + "note": "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton": { + "title": "Open Settings", + "note": "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube": { + "title": "Watch on YouTube", + "note": "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError": { + "title": "ERROR: Invalid video id", + "note": "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo": { + "title": "Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/es/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/es/duckplayer.json new file mode 100644 index 000000000000..1b5d8b9585e9 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/es/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Abrir siempre los vídeos de YouTube aquí", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Mantener Duck Player activado", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Abrir información", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Abrir ajustes", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Ver en YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ERROR: ID de vídeo no válida", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player ofrece una experiencia de visualización limpia sin anuncios personalizados e impide que la actividad de visualización influya en tus recomendaciones de YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/et/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/et/duckplayer.json new file mode 100644 index 000000000000..c9863f4f50c4 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/et/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Ava YouTube'i videod alati siin", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Hoia Duck Player sisse lülitatud", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Ava teave", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Ava seaded", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Vaata YouTube'is", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "VIGA: vale video ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player pakub isikupärastatud reklaamidest vaba vaatamiskogemust ja takistab, et vaatamisaktiivsus mõjutaks sinu YouTube'i soovitusi." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fi/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fi/duckplayer.json new file mode 100644 index 000000000000..e73022b8fa9a --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fi/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Avaa YouTube-videot aina täällä", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Pidä Duck Player käytössä", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Avaa tiedot", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Avaa asetukset", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Katso YouTubessa", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "VIRHE: virheellinen videotunnus", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player tarjoaa puhtaan katselukokemuksen ilman kohdennettuja mainoksia ja estää katseluhistoriaa vaikuttamasta YouTube-suosituksiisi." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fr/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fr/duckplayer.json new file mode 100644 index 000000000000..716f0c071011 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/fr/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Toujours ouvrir les vidéos YouTube ici", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Laisser Duck Player activé", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Ouvrir les infos", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Ouvrez les Paramètres", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Regarder sur YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ERREUR : identifiant vidéo non valide", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player offre une expérience de visionnage épurée, sans publicités personnalisées, et empêche l'activité de visionnage d'influencer vos recommandations YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hr/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hr/duckplayer.json new file mode 100644 index 000000000000..3f0e8aeaee03 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hr/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "YouTube videozapise uvijek otvaraj ovdje", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Drži Duck Player uključen", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Više informacija", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Otvori Postavke", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Pogledaj na YouTubeu", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "POGREŠKA: Nevažeći ID videozapisa", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player pruža čisti doživljaj gledanja bez personaliziranih oglasa i sprječava da aktivnosti gledanja utječu na tvoje preporuke na YouTubeu." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hu/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hu/duckplayer.json new file mode 100644 index 000000000000..3bbe06210f10 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/hu/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Mindig itt nyissa meg a YouTube-videókat", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Maradjon bekapcsolva a Duck Player", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Információk megtekintése", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Beállítások megnyitása", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Lejátszás YouTube-on", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "HIBA: Érvénytelen videoazonosító", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "A Duck Player személyre szabott hirdetések nélküli, letisztult megtekintési élményt nyújt, és megakadályozza, hogy a megtekintési tevékenységed befolyásolja a neked szóló YouTube-ajánlásokat." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/it/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/it/duckplayer.json new file mode 100644 index 000000000000..9ce21667708e --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/it/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Apri sempre i video di YouTube qui", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Tieni Duck Player attivo", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Apri le informazioni", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Apri Impostazioni", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Guarda su YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ERRORE: ID video non valido", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player offre un'esperienza di visualizzazione pulita, senza annunci personalizzati, e impedisce che l'attività di visualizzazione incida sulle raccomandazioni di YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lt/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lt/duckplayer.json new file mode 100644 index 000000000000..1b282dd2ccaa --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lt/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Visada atidaryti „YouTube“ vaizdo įrašus čia", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Laikyti „Duck Player“ įjungtą", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Atidaryti informaciją", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Atidaryti Nustatymus", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Žiūrėti „YouTube“", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "KLAIDA: netinkamas vaizdo įrašo ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "„Duck Player“ užtikrina nepriekaištingą žiūrėjimo patirtį be suasmenintų reklamų ir neleidžia žiūrėjimo veiklai daryti įtakos „YouTube“ rekomendacijoms." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lv/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lv/duckplayer.json new file mode 100644 index 000000000000..46d0bee8e660 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/lv/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Vienmēr atvērt YouTube videoklipus šeit", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Paturēt Duck Player ieslēgtu", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Atvērt informāciju", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Atvērt iestatījumus", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Skatīties pie YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "KĻŪDA: Nederīgs video ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player nodrošina netraucētu skatīšanās pieredzi bez personalizētām reklāmām un neļauj skatīšanās darbībām ietekmēt tavus YouTube ieteikumus." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nb/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nb/duckplayer.json new file mode 100644 index 000000000000..4c1d826f666f --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nb/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Åpne alltid YouTube-videoer her", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "La Duck Player være på", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Åpne informasjon", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Åpne innstillingene", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Se på YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "FEIL: Ugyldig video-ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player tilbyr en ren seeropplevelse uten tilpassede annonser og forhindrer at seeraktiviteten din påvirker YouTube-anbefalingene dine." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nl/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nl/duckplayer.json new file mode 100644 index 000000000000..a1be8669e254 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/nl/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "YouTube-video's altijd hier openen", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Houd Duck Player ingeschakeld", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Meer info", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Open Instellingen", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Kijken op YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "FOUT: ongeldige video-id", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player biedt puur kijkplezier zonder gepersonaliseerde advertenties en voorkomt dat de dingen die je bekijkt je YouTube-aanbevelingen beïnvloeden." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pl/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pl/duckplayer.json new file mode 100644 index 000000000000..accd3fde5d50 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pl/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Zawsze otwieraj filmy z YouTube tutaj", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Nie wyłączaj odtwarzacza Duck Player", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Otwórz informacje", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Otwórz ustawienia", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Obejrzyj na YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "BŁĄD: nieprawidłowy identyfikator filmu", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player zapewnia czyste środowisko oglądania bez spersonalizowanych reklam i sprawia, że aktywność związana z oglądaniem filmów nie wpływa na rekomendacje YouTube'a." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pt/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pt/duckplayer.json new file mode 100644 index 000000000000..a5bfca188192 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/pt/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Abrir sempre os vídeos do YouTube aqui", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Manter o Duck Player ligado", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Abrir Informações", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Abre Definições", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Ver no YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ERRO: ID de vídeo inválido", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "O Duck Player oferece uma experiência de visualização limpa sem anúncios personalizados e evita que as atividades de visualização influenciem as recomendações do YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ro/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ro/duckplayer.json new file mode 100644 index 000000000000..bfafec70e054 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ro/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Deschide întotdeauna videoclipurile YouTube aici", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Menține Duck Player activat", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Deschide Informațiile", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Deschide Setări", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Vizionează pe YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "EROARE: ID video incorect", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player oferă o experiență de vizionare fără perturbări, fără reclame personalizate și împiedică activitatea de vizionare să îți influențeze recomandările YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ru/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ru/duckplayer.json new file mode 100644 index 000000000000..4bf5cc0c1f99 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/ru/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Всегда открывайте видео на YouTube здесь", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Держите Duck Player включенным", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Открыть информацию", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Открыть Настройки", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Смотреть на YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "ОШИБКА: Неверный идентификатор видео", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Проигрыватель Duck Player обеспечивает беспрепятственный просмотр без персонализированной рекламы и влияния просмотренных роликов на рекомендации в YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sk/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sk/duckplayer.json new file mode 100644 index 000000000000..88a2cc3a57d8 --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sk/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Vždy otvárajte videá YouTube tu", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Nechajte zapnutý prehrávač Duck Player", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Otvorené informácie", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Otvoriť nastavenia", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Pozrieť na YouTube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "CHYBA: Neplatný identifikátor videa", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player poskytuje čisté zobrazenie bez personalizovaných reklám a zabraňuje tomu, aby aktivita pri sledovaní ovplyvňovala vaše odporúčania v službe YouTube." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sl/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sl/duckplayer.json new file mode 100644 index 000000000000..7d4a89155afd --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sl/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Videoposnetke iz YouTuba vedno odpri tukaj", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Predvajalnik Duck Player naj ostane vklopljen", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Prikaži informacije", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Odpri nastavitve", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Glej na YouTubu", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "NAPAKA: Neveljaven ID videoposnetka", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Predvajalnik Duck Player zagotavlja čisto izkušnjo gledanja brez prilagojenih oglasov in preprečuje, da bi dejavnost gledanja vplivala na vaša priporočila v YouTubu." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sv/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sv/duckplayer.json new file mode 100644 index 000000000000..cb5b75e4caef --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/sv/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "Öppna alltid YouTube-videor här", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Låt Duck Player vara aktiverat", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Öppna info", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Öppna inställningar", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Se på Youtube", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "FEL: Ogiltigt video-ID", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player ger en störningsfri visningsupplevelse utan personliga annonser och förhindrar att din tittaraktivitet påverkar YouTube-rekommendationer." + } +} diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/tr/duckplayer.json b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/tr/duckplayer.json new file mode 100644 index 000000000000..c23cab2bbd0a --- /dev/null +++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/locales/tr/duckplayer.json @@ -0,0 +1,38 @@ +{ + "smartling" : { + "string_format" : "icu", + "translate_paths" : [ + { + "path" : "*/title", + "key" : "{*}/title", + "instruction" : "*/note" + }] + }, + "alwaysWatchHere" : { + "title" : "YouTube videolarını her zaman burada aç", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "keepEnabled" : { + "title" : "Duck Player'ı açık tut", + "note" : "label text for a checkbox that enables this feature for all videos, not just the current one" + }, + "openInfoButton" : { + "title" : "Bilgileri Aç", + "note" : "aria label text on a button, to indicate there's more information to be shown if clicked" + }, + "openSettingsButton" : { + "title" : "Ayarları Aç", + "note" : "aria label text on a button, opens a screen where the user can change settings" + }, + "watchOnYoutube" : { + "title" : "Youtube'da İzle", + "note" : "text on a link that takes the user from the current page back onto YouTube.com" + }, + "invalidIdError" : { + "title" : "HATA: Geçersiz video kimliği", + "note" : "Shown when the page URL doesn't match a known video ID. Note for translators: The tag makes the word 'ERROR:' bold. Depending on the grammar of the target language, you might need to move it so that the correct word is emphasized." + }, + "tooltipInfo" : { + "title" : "Duck Player, kişiselleştirilmiş reklamlar olmadan temiz bir görüntüleme deneyimi sağlar ve görüntüleme etkinliğinin YouTube önerilerinizi etkilemesini önler." + } +} diff --git a/settings/settings-api/src/main/java/com/duckduckgo/settings/api/ProSettingsPlugin.kt b/settings/settings-api/src/main/java/com/duckduckgo/settings/api/ProSettingsPlugin.kt index 485c124f5adc..bfb6db7462ba 100644 --- a/settings/settings-api/src/main/java/com/duckduckgo/settings/api/ProSettingsPlugin.kt +++ b/settings/settings-api/src/main/java/com/duckduckgo/settings/api/ProSettingsPlugin.kt @@ -34,3 +34,8 @@ interface SettingsPlugin { * This is the plugin for the subs settings */ interface ProSettingsPlugin : SettingsPlugin + +/** + * This is the plugin for Duck Player settings + */ +interface DuckPlayerSettingsPlugin : SettingsPlugin diff --git a/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/DuckPlayerSettingModule.kt b/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/DuckPlayerSettingModule.kt new file mode 100644 index 000000000000..97266b059c35 --- /dev/null +++ b/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/DuckPlayerSettingModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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. + */ + +package com.duckduckgo.settings.impl + +import com.duckduckgo.anvil.annotations.ContributesPluginPoint +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.settings.api.DuckPlayerSettingsPlugin + +@ContributesPluginPoint( + scope = ActivityScope::class, + boundType = DuckPlayerSettingsPlugin::class, +) +private interface DuckPlayerSettingsPluginTrigger diff --git a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt index fe34602cc5f4..9b13ea9144a3 100644 --- a/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt +++ b/subscriptions/subscriptions-api/src/main/java/com/duckduckgo/subscriptions/api/Subscriptions.kt @@ -56,9 +56,9 @@ interface Subscriptions { fun launchPrivacyPro(context: Context, uri: Uri?) /** - * @return `true` if the given URL leads to the Privacy Pro page, or `false` otherwise + * @return `true` if the given Uri leads to the Privacy Pro page, or `false` otherwise */ - fun isPrivacyProUrl(url: String): Boolean + fun isPrivacyProUrl(uri: Uri): Boolean } enum class Product(val value: String) { diff --git a/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt b/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt index 97fb40123fb0..ce28bcf8b04a 100644 --- a/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt +++ b/subscriptions/subscriptions-dummy-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsDummy.kt @@ -47,5 +47,5 @@ class SubscriptionsDummy @Inject constructor() : Subscriptions { // no-op } - override fun isPrivacyProUrl(url: String): Boolean = false + override fun isPrivacyProUrl(uri: Uri): Boolean = false } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt index 68921212ac7e..086a0a4657fd 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt @@ -112,7 +112,7 @@ class RealSubscriptions @Inject constructor( } override fun shouldLaunchPrivacyProForUrl(url: String): Boolean { - return if (isPrivacyProUrl(url)) { + return if (isPrivacyProUrl(url.toUri())) { runBlocking { isEligible() } @@ -121,8 +121,7 @@ class RealSubscriptions @Inject constructor( } } - override fun isPrivacyProUrl(url: String): Boolean { - val uri = url.toUri() + override fun isPrivacyProUrl(uri: Uri): Boolean { val eTld = uri.host?.toTldPlusOne() ?: return false val size = uri.pathSegments.size val path = uri.pathSegments.firstOrNull()