From c40417771879348634a45d01b89d18219c725f01 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 14 Nov 2024 13:18:05 +0000 Subject: [PATCH] text zoom improvements (#3509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/392891325557410/1206268893832417/f Tech Design URL: CC: **Description**: Makes it easier to access text zoom and allows per page state. **Steps to test this PR**: See test scenarios in Ship Review: https://app.asana.com/0/0/1208685664909495/f **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/FeatureFlag.swift | 3 + Core/Pixel.swift | 5 +- Core/PixelEvent.swift | 21 +- Core/TextSizeUserScript.swift | 58 ----- Core/UserDefaultsPropertyWrapper.swift | 7 +- Core/textsize.js | 154 ------------ DuckDuckGo.xcodeproj/project.pbxproj | 68 ++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/AppDelegate.swift | 13 +- DuckDuckGo/AppSettings.swift | 2 +- DuckDuckGo/AppUserDefaults.swift | 19 +- .../TextSizeLarger.imageset/Contents.json | 15 -- .../Font-Larger-24.pdf | Bin 1058 -> 0 bytes .../TextSizeSmaller.imageset/Contents.json | 15 -- .../Font-Smaller-24.pdf | Bin 1054 -> 0 bytes DuckDuckGo/Base.lproj/Settings.storyboard | 165 +------------ DuckDuckGo/ContentBlockingUpdating.swift | 1 - DuckDuckGo/IntervalSlider.swift | 62 ++++- DuckDuckGo/MainViewController+Segues.swift | 3 +- DuckDuckGo/MainViewController.swift | 21 +- DuckDuckGo/SettingsAccessibilityView.swift | 44 ++-- DuckDuckGo/SettingsGeneralView.swift | 30 +-- DuckDuckGo/SettingsLegacyViewProvider.swift | 2 - DuckDuckGo/SettingsState.swift | 8 +- DuckDuckGo/SettingsViewModel.swift | 38 ++- DuckDuckGo/TabManager.swift | 23 +- DuckDuckGo/TabViewController.swift | 32 ++- ...bViewControllerBrowsingMenuExtension.swift | 6 +- ...ViewControllerLongPressMenuExtension.swift | 25 +- .../TextSizeSettingsViewController.swift | 221 ------------------ DuckDuckGo/TextZoomController.swift | 38 +++ DuckDuckGo/TextZoomCoordinator.swift | 181 ++++++++++++++ DuckDuckGo/TextZoomEditorModel.swift | 73 ++++++ DuckDuckGo/TextZoomEditorView.swift | 95 ++++++++ DuckDuckGo/TextZoomLevel.swift | 39 ++++ DuckDuckGo/TextZoomStorage.swift | 60 +++++ DuckDuckGo/UserScripts.swift | 5 +- DuckDuckGo/UserText.swift | 20 +- DuckDuckGo/en.lproj/Localizable.strings | 13 +- DuckDuckGoTests/AppSettingsMock.swift | 1 + .../ContentBlockingUpdatingTests.swift | 25 -- DuckDuckGoTests/MockFeatureFlagger.swift | 4 + DuckDuckGoTests/MockTabDelegate.swift | 3 +- DuckDuckGoTests/MockTextZoomCoordinator.swift | 55 +++++ .../OnboardingDaxFavouritesTests.swift | 4 +- .../OnboardingNavigationDelegateTests.swift | 4 +- DuckDuckGoTests/TextZoomTests.swift | 192 +++++++++++++++ 47 files changed, 1092 insertions(+), 785 deletions(-) delete mode 100644 Core/TextSizeUserScript.swift delete mode 100644 Core/textsize.js delete mode 100644 DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Font-Larger-24.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Font-Smaller-24.pdf delete mode 100644 DuckDuckGo/TextSizeSettingsViewController.swift create mode 100644 DuckDuckGo/TextZoomController.swift create mode 100644 DuckDuckGo/TextZoomCoordinator.swift create mode 100644 DuckDuckGo/TextZoomEditorModel.swift create mode 100644 DuckDuckGo/TextZoomEditorView.swift create mode 100644 DuckDuckGo/TextZoomLevel.swift create mode 100644 DuckDuckGo/TextZoomStorage.swift create mode 100644 DuckDuckGoTests/MockTextZoomCoordinator.swift create mode 100644 DuckDuckGoTests/TextZoomTests.swift diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index eff7ca49c4..81e9b34dd4 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -45,6 +45,7 @@ public enum FeatureFlag: String { case onboardingAddToDock case autofillSurveys case autcompleteTabs + case textZoom case adAttributionReporting /// https://app.asana.com/0/72649045549333/1208231259093710/f @@ -111,6 +112,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.feature(.autocompleteTabs)) case .networkProtectionUserTips: return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.userTips)) + case .textZoom: + return .remoteReleasable(.feature(.textZoom)) case .networkProtectionEnforceRoutes: return .remoteDevelopment(.subfeature(NetworkProtectionSubfeature.enforceRoutes)) case .adAttributionReporting: diff --git a/Core/Pixel.swift b/Core/Pixel.swift index b0796afe64..fc2e7f3945 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -86,8 +86,9 @@ public struct PixelParameters { public static let count = "count" public static let source = "source" - public static let textSizeInitial = "text_size_initial" - public static let textSizeUpdated = "text_size_updated" + // Text size is the legacy name + public static let textZoomInitial = "text_size_initial" + public static let textZoomUpdated = "text_size_updated" public static let canAutoPreviewMIMEType = "can_auto_preview_mime_type" public static let mimeType = "mime_type" diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 969bf6f637..85416a001c 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -80,6 +80,7 @@ extension Pixel { case browsingMenuCopy case browsingMenuPrint case browsingMenuFindInPage + case browsingMenuZoom case browsingMenuDisableProtection case browsingMenuEnableProtection case browsingMenuReportBrokenSite @@ -220,8 +221,10 @@ extension Pixel { case bookmarkExportSuccess case bookmarkExportFailure - case textSizeSettingsChanged - + case textZoomSettingsChanged + case textZoomChangedOnPage + case textZoomChangedOnPageDaily + case downloadStarted case downloadStartedDueToUnhandledMIMEType case downloadTriedToPresentPreviewWithoutTab @@ -748,7 +751,7 @@ extension Pixel { case settingsRecentlyVisitedOff case settingsAddressBarSelectorPressed case settingsAccessibilityOpen - case settingsAccessiblityTextSize + case settingsAccessiblityTextZoom // Web pixels case privacyProOfferMonthlyPriceClick @@ -927,6 +930,7 @@ extension Pixel.Event { case .browsingMenuCopy: return "mb_cp" case .browsingMenuPrint: return "mb_pr" case .browsingMenuFindInPage: return "mb_fp" + case .browsingMenuZoom: return "m_menu_page_zoom_taps" case .browsingMenuDisableProtection: return "mb_wla" case .browsingMenuEnableProtection: return "mb_wlr" case .browsingMenuReportBrokenSite: return "mb_rb" @@ -1063,8 +1067,11 @@ extension Pixel.Event { case .bookmarkExportSuccess: return "m_be_a" case .bookmarkExportFailure: return "m_be_e" - case .textSizeSettingsChanged: return "m_text_size_settings_changed" - + // Text size is the legacy name + case .textZoomSettingsChanged: return "m_text_size_settings_changed" + case .textZoomChangedOnPageDaily: return "m_menu_page_zoom_changed_daily" + case .textZoomChangedOnPage: return "m_menu_page_zoom_changed" + case .downloadStarted: return "m_download_started" case .downloadStartedDueToUnhandledMIMEType: return "m_download_started_due_to_unhandled_mime_type" case .downloadTriedToPresentPreviewWithoutTab: return "m_download_tried_to_present_preview_without_tab" @@ -1590,7 +1597,9 @@ extension Pixel.Event { case .settingsRecentlyVisitedOff: return "m_settings_autocomplete_recently-visited_off" case .settingsAddressBarSelectorPressed: return "m_settings_address_bar_selector_pressed" case .settingsAccessibilityOpen: return "m_settings_accessibility_open" - case .settingsAccessiblityTextSize: return "m_settings_accessiblity_text_size" + + // legacy name is text size + case .settingsAccessiblityTextZoom: return "m_settings_accessiblity_text_size" // Web case .privacyProOfferMonthlyPriceClick: return "m_privacy-pro_offer_monthly-price_click" diff --git a/Core/TextSizeUserScript.swift b/Core/TextSizeUserScript.swift deleted file mode 100644 index a246d48e63..0000000000 --- a/Core/TextSizeUserScript.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// TextSizeUserScript.swift -// DuckDuckGo -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import WebKit -import UserScript - -public class TextSizeUserScript: NSObject, UserScript { - - public static let knownDynamicTypeExceptions: [String] = ["wikipedia.org"] - public var textSizeAdjustmentInPercents: Int = 100 - - public var source: String { TextSizeUserScript.makeSource(for: textSizeAdjustmentInPercents) } - - public var injectionTime: WKUserScriptInjectionTime = .atDocumentStart - public var forMainFrameOnly: Bool = false - public var messageNames: [String] = [] - - public init(textSizeAdjustmentInPercents: Int) { - self.textSizeAdjustmentInPercents = textSizeAdjustmentInPercents - } - - public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { } - - fileprivate static func makeSource(for textSizeAdjustmentInPercents: Int) -> String { - let dynamicTypeScalePercentage = UIFontMetrics.default.scaledValue(for: 100) - - return loadJS("textsize", from: Bundle.core, withReplacements: [ - "$KNOWN_DYNAMIC_TYPE_EXCEPTIONS$": knownDynamicTypeExceptions.joined(separator: "\n"), - "$TEXT_SIZE_ADJUSTMENT_IN_PERCENTS$": "\(textSizeAdjustmentInPercents)", - "$DYNAMIC_TYPE_SCALE_PERCENTAGE$": "\(dynamicTypeScalePercentage)" - ]) - } -} - -public extension WKWebView { - - func adjustTextSize(_ percentage: Int) { - let jsString = TextSizeUserScript.makeSource(for: percentage) - evaluateJavaScript(jsString, completionHandler: nil) - } -} diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index eae3ee9842..ac6a5a6bdf 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -75,7 +75,9 @@ public struct UserDefaultsWrapper { case downloadedSurrogatesCount = "com.duckduckgo.app.downloadedSurrogatesCount" case downloadedTrackerDataSetCount = "com.duckduckgo.app.downloadedTrackerDataSetCount" case downloadedPrivacyConfigurationCount = "com.duckduckgo.app.downloadedPrivacyConfigurationCount" - case textSize = "com.duckduckgo.ios.textSize" + + // Text size is the legacy name and this key is still in use + case textZoom = "com.duckduckgo.ios.textSize" case emailWaitlistShouldReceiveNotifications = "com.duckduckgo.ios.showWaitlistNotification" case unseenDownloadsAvailable = "com.duckduckgo.app.unseenDownloadsAvailable" @@ -179,6 +181,9 @@ public struct UserDefaultsWrapper { case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered.v2" case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override.v2" + // Domain specific text zoom + case domainTextZoomStorage = "com.duckduckgo.ios.domainTextZoomStorage" + // TipKit case resetTipKitOnNextLaunch = "com.duckduckgo.ios.tipKit.resetOnNextLaunch" } diff --git a/Core/textsize.js b/Core/textsize.js deleted file mode 100644 index 98a434ad82..0000000000 --- a/Core/textsize.js +++ /dev/null @@ -1,154 +0,0 @@ -// -// textsize.js -// DuckDuckGo -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -(function() { - let hostname = getTopLevelURL().hostname; - - let knownDynamicTypeExceptions = `$KNOWN_DYNAMIC_TYPE_EXCEPTIONS$`.split("\n"); - - let shouldAdjustForDynamicType = isURLMatchingAnyOfDomains(hostname, knownDynamicTypeExceptions); - let isDDG = isURLMatchingDomain(hostname, "duckduckgo.com"); - - let currentTextSizeAdjustment = $TEXT_SIZE_ADJUSTMENT_IN_PERCENTS$; - - if (document.readyState === "complete" - || document.readyState === "loaded" - || document.readyState === "interactive") { - // DOM should have been parsed - adjustTextSize(currentTextSizeAdjustment); - } else { - // DOM not yet ready, add a listener instead - if ((shouldAdjustForDynamicType) || (isDDG) || (currentTextSizeAdjustment != 100)) { - document.addEventListener("DOMContentLoaded", function(event) { - adjustTextSize(currentTextSizeAdjustment); - }, false) - } - } - - function getTopLevelURL() { - try { - // FROM: https://stackoverflow.com/a/7739035/73479 - // FIX: Better capturing of top level URL so that trackers in embedded documents are not considered first party - return new URL(window.location != window.parent.location ? document.referrer : document.location.href) - } catch(error) { - return new URL(location.href) - } - } - - function isURLMatchingDomain(url, domain) { - var urlParts = url.split('.'); - - while (urlParts.length > 1) { - if (domain === urlParts.join('.')) { - return true; - } - - urlParts.shift(); - } - - return false; - } - - function isURLMatchingAnyOfDomains(url, domains) { - for (const domain of domains) { - if (isURLMatchingDomain(url, domain)) { - return true - } - } - - return false - } - - function adjustTextSize(percentage) { - if (shouldAdjustForDynamicType) { - adjustTextSizeForDynamicType(percentage); - } else if (isDDG && (typeof DDG !== 'undefined')) { - adjustTextSizeForDDG(percentage); - } else { - document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust=percentage+"%"; - } - } - - function adjustTextSizeForDynamicType(percentage) { - let dynamicTypeAdjustment = $DYNAMIC_TYPE_SCALE_PERCENTAGE$; - var adjustedPercentage = percentage * 100/dynamicTypeAdjustment; - - document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust=adjustedPercentage+"%"; - } - - function adjustTextSizeForDDG(percentage) { - var adjustedPercentage = 100; - - // Fix for side menu sliding in when growing due to increased text - let menu = document.getElementsByClassName('nav-menu--slideout')[0]; - let previousLeft = menu.style.left; - menu.style.left="-100%"; - - // Force re-painting of the menu: https://stackoverflow.com/a/3485654 - menu.style.display='none'; - menu.offsetHeight; // no need to store this anywhere, the reference is enough - menu.style.display='block'; - - switch(percentage) { - case 80: - DDG.settings.set('ks', 's'); - break; - case 90: - DDG.settings.set('ks', 'm'); - break; - case 100: - DDG.settings.set('ks', 'n'); - break; - case 110: - DDG.settings.set('ks', 'n'); - adjustedPercentage = 105; - break; - case 120: - DDG.settings.set('ks', 'l'); - break; - case 130: - DDG.settings.set('ks', 'l'); - adjustedPercentage = 105; - break; - case 140: - DDG.settings.set('ks', 'l'); - adjustedPercentage = 110; - break; - case 150: - DDG.settings.set('ks', 't'); - break; - case 160: - DDG.settings.set('ks', 't'); - adjustedPercentage = 105; - break; - case 170: - DDG.settings.set('ks', 't'); - adjustedPercentage = 110; - break; - default: - DDG.settings.set('ks', 'n'); - break; - } - - document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust=adjustedPercentage+"%"; - - menu.style.left = previousLeft; - } - -}) (); diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e43e2b1e62..310c116fad 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -78,7 +78,6 @@ 1E60989B290009C700A508F9 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 1E7060BD28F88EE200E4CCDB /* Common */; }; 1E60989D290011E600A508F9 /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 1E60989C290011E600A508F9 /* ContentBlocking */; }; 1E6098A1290011E600A508F9 /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E6098A0290011E600A508F9 /* UserScript */; }; - 1E61BC2A27074BED00B2854D /* TextSizeUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E61BC2927074BED00B2854D /* TextSizeUserScript.swift */; }; 1E6A4D692984208800A371D3 /* LocaleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6A4D682984208800A371D3 /* LocaleExtension.swift */; }; 1E722729292EB24D003B5F53 /* AppSettingsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AD9E3C28D46FD50019CDE9 /* AppSettingsMock.swift */; }; 1E7A71172934EB6400B7EA19 /* OmniBarNotificationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7A71162934EB6400B7EA19 /* OmniBarNotificationAnimator.swift */; }; @@ -86,7 +85,6 @@ 1E7A711C2934EEBC00B7EA19 /* OmniBarNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7A711B2934EEBC00B7EA19 /* OmniBarNotification.swift */; }; 1E8146AD28C8ABF000D1AF63 /* TrackerAnimationLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8146A728C8AB3F00D1AF63 /* TrackerAnimationLogicTests.swift */; }; 1E8146AE28C8ABF400D1AF63 /* PrivacyIconLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8146A928C8AB8200D1AF63 /* PrivacyIconLogicTests.swift */; }; - 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E865AEF272042DB001C74F3 /* TextSizeSettingsViewController.swift */; }; 1E87615928A1517200C7C5CE /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E87615828A1517200C7C5CE /* PrivacyDashboardViewController.swift */; }; 1E8AD1C727BE9B2900ABA377 /* DownloadsListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8AD1C627BE9B2900ABA377 /* DownloadsListDataSource.swift */; }; 1E8AD1C927BFAD1500ABA377 /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8AD1C827BFAD1500ABA377 /* DirectoryMonitor.swift */; }; @@ -126,7 +124,6 @@ 1EEF12532851D32B003DDE57 /* trackers-2.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EEF12512851D32A003DDE57 /* trackers-2.json */; }; 1EEF12542851D32B003DDE57 /* trackers-1.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EEF12522851D32A003DDE57 /* trackers-1.json */; }; 1EEF387D285B1A1100383393 /* TrackerImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEF387C285B1A1100383393 /* TrackerImageCache.swift */; }; - 1EEFD2D52758E31600B1393B /* textsize.js in Resources */ = {isa = PBXBuildFile; fileRef = 1EEFD2D42758E31600B1393B /* textsize.js */; }; 1EF24235273BB9D200DE3D02 /* IntervalSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF24234273BB9D200DE3D02 /* IntervalSlider.swift */; }; 1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFDCBC027D2393C00916BC5 /* DownloadsDeleteHelper.swift */; }; 22CB1ED8203DDD2C00D2C724 /* AppDeepLinksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22CB1ED7203DDD2C00D2C724 /* AppDeepLinksTests.swift */; }; @@ -525,6 +522,14 @@ 8596C30D2B7EB1800058EF90 /* DataStoreWarmup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8596C30C2B7EB1800058EF90 /* DataStoreWarmup.swift */; }; 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8598F6792405EB8600FBC70C /* KeyboardSettingsTests.swift */; }; 8599690F29D2F1C100DBF9FA /* DDGSync in Frameworks */ = {isa = PBXBuildFile; productRef = 8599690E29D2F1C100DBF9FA /* DDGSync */; }; + 859DB8132CE6263C001F7210 /* TextZoomStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB80D2CE6263B001F7210 /* TextZoomStorage.swift */; }; + 859DB8142CE6263C001F7210 /* TextZoomController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB80E2CE6263B001F7210 /* TextZoomController.swift */; }; + 859DB8152CE6263C001F7210 /* TextZoomEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB80F2CE6263C001F7210 /* TextZoomEditorModel.swift */; }; + 859DB8162CE6263C001F7210 /* TextZoomEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB8102CE6263C001F7210 /* TextZoomEditorView.swift */; }; + 859DB8172CE6263C001F7210 /* TextZoomLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB8112CE6263C001F7210 /* TextZoomLevel.swift */; }; + 859DB8182CE6263C001F7210 /* TextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB8122CE6263C001F7210 /* TextZoomCoordinator.swift */; }; + 859DB81C2CE6268C001F7210 /* MockTextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB81A2CE6268C001F7210 /* MockTextZoomCoordinator.swift */; }; + 859DB81E2CE62766001F7210 /* TextZoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB81B2CE6268C001F7210 /* TextZoomTests.swift */; }; 85A1B3B220C6CD9900C18F15 /* CookieStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A1B3B120C6CD9900C18F15 /* CookieStorage.swift */; }; 85A313972028E78A00327D00 /* release_notes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 85A313962028E78A00327D00 /* release_notes.txt */; }; 85A9C37920E0E00C00073340 /* HomeRow.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85A9C37820E0E00C00073340 /* HomeRow.xcassets */; }; @@ -1396,7 +1401,6 @@ 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDownloadRowViewModel.swift; sourceTree = ""; }; 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteDownloadRowViewModel.swift; sourceTree = ""; }; 1E53508E2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift"; sourceTree = ""; }; - 1E61BC2927074BED00B2854D /* TextSizeUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextSizeUserScript.swift; sourceTree = ""; }; 1E6A4D682984208800A371D3 /* LocaleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtension.swift; sourceTree = ""; }; 1E7A71162934EB6400B7EA19 /* OmniBarNotificationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarNotificationAnimator.swift; sourceTree = ""; }; 1E7A71182934EC6100B7EA19 /* OmniBarNotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarNotificationContainerView.swift; sourceTree = ""; }; @@ -1443,7 +1447,6 @@ 1EEF12512851D32A003DDE57 /* trackers-2.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trackers-2.json"; sourceTree = ""; }; 1EEF12522851D32A003DDE57 /* trackers-1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trackers-1.json"; sourceTree = ""; }; 1EEF387C285B1A1100383393 /* TrackerImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerImageCache.swift; sourceTree = ""; }; - 1EEFD2D42758E31600B1393B /* textsize.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = textsize.js; sourceTree = ""; }; 1EF24234273BB9D200DE3D02 /* IntervalSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalSlider.swift; sourceTree = ""; }; 1EFDCBC027D2393C00916BC5 /* DownloadsDeleteHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsDeleteHelper.swift; sourceTree = ""; }; 22CB1ED7203DDD2C00D2C724 /* AppDeepLinksTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDeepLinksTests.swift; sourceTree = ""; }; @@ -1832,6 +1835,14 @@ 8590CB68268A4E190089F6BF /* DebugEtagStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugEtagStorage.swift; sourceTree = ""; }; 8596C30C2B7EB1800058EF90 /* DataStoreWarmup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreWarmup.swift; sourceTree = ""; }; 8598F6792405EB8600FBC70C /* KeyboardSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardSettingsTests.swift; sourceTree = ""; }; + 859DB80D2CE6263B001F7210 /* TextZoomStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomStorage.swift; sourceTree = ""; }; + 859DB80E2CE6263B001F7210 /* TextZoomController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomController.swift; sourceTree = ""; }; + 859DB80F2CE6263C001F7210 /* TextZoomEditorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomEditorModel.swift; sourceTree = ""; }; + 859DB8102CE6263C001F7210 /* TextZoomEditorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomEditorView.swift; sourceTree = ""; }; + 859DB8112CE6263C001F7210 /* TextZoomLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomLevel.swift; sourceTree = ""; }; + 859DB8122CE6263C001F7210 /* TextZoomCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomCoordinator.swift; sourceTree = ""; }; + 859DB81A2CE6268C001F7210 /* MockTextZoomCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTextZoomCoordinator.swift; sourceTree = ""; }; + 859DB81B2CE6268C001F7210 /* TextZoomTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomTests.swift; sourceTree = ""; }; 85A1B3B120C6CD9900C18F15 /* CookieStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieStorage.swift; sourceTree = ""; }; 85A313962028E78A00327D00 /* release_notes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = release_notes.txt; path = fastlane/metadata/default/release_notes.txt; sourceTree = ""; }; 85A53EC9200D1FA20010D13F /* FileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStore.swift; sourceTree = ""; }; @@ -4230,21 +4241,22 @@ 37CF915E2BB4735F00BADCAE /* Crashes */, D6E0C1812B7A2B0700D5E1E9 /* DesktopDownloads */, 310D09192799EF5C00DC0060 /* Downloads */, - D63FF8922C1B67D1006DE24D /* DuckPlayer */, F143C2C51E4A08F300CFDE3A /* DuckDuckGo.entitlements */, EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */, + D63FF8922C1B67D1006DE24D /* DuckPlayer */, C159DF052A430B36007834BB /* EmailProtection */, 3157B43627F4C8380042D3D7 /* Favicons */, 839F119520DBC489007CD8C2 /* Feedback */, 85F2FFFE2215C163006BB258 /* FindInPage */, - F13B4BF31F18C73A00814661 /* NewTabPage */, 84E341A11E2F7EFB00BDBA6F /* Info.plist */, 98B001B1251EABB40090EC07 /* InfoPlist.strings */, 85DFEDEB24C7CC7600973FE7 /* iPad */, F1C5ECFA1E37B15B00C599A4 /* Main */, EECD94B22A28B8580085C66E /* NetworkProtection */, + F13B4BF31F18C73A00814661 /* NewTabPage */, 85AE668C20971FCA0014CF04 /* Notifications */, F1C4A70C1E5771F800A6CA1B /* OmniBar */, + CB48D32F2B90CE8500631D8B /* PageRefreshMonitor */, F1AE54DB1F0425BB00D9A700 /* Privacy */, F1DF09502B039E6E008CC908 /* PrivacyDashboard */, 02ECEC602A965074009F0654 /* PrivacyInfo.xcprivacy */, @@ -4255,10 +4267,10 @@ 85F98F8C296F0ED100742F4A /* Sync */, F13B4BF41F18C74500814661 /* Tabs */, F1386BA21E6846320062FC3C /* TabSwitcher */, + 859DB80C2CE6262A001F7210 /* TextZoom */, 98F3A1D6217B36EE0011A0D4 /* Themes */, 7BF78E002CA2CC100026A1FC /* TipKit */, F11CEF581EBB66C80088E4D7 /* Tutorials */, - CB48D32F2B90CE8500631D8B /* PageRefreshMonitor */, F1D796ED1E7AE4090019D451 /* UserInterface */, 84E341E31E2FC0E400BDBA6F /* UserInterfaceResources */, 3151F0E827357F8F00226F58 /* VoiceSearch */, @@ -4466,7 +4478,6 @@ isa = PBXGroup; children = ( 4B60AC96252EC07B00E8D219 /* fullscreenvideo.js */, - 1EEFD2D42758E31600B1393B /* textsize.js */, ); name = "ios-js-support"; sourceTree = ""; @@ -4479,6 +4490,28 @@ name = FireAnimation; sourceTree = ""; }; + 859DB80C2CE6262A001F7210 /* TextZoom */ = { + isa = PBXGroup; + children = ( + 859DB80E2CE6263B001F7210 /* TextZoomController.swift */, + 859DB8122CE6263C001F7210 /* TextZoomCoordinator.swift */, + 859DB80F2CE6263C001F7210 /* TextZoomEditorModel.swift */, + 859DB8102CE6263C001F7210 /* TextZoomEditorView.swift */, + 859DB8112CE6263C001F7210 /* TextZoomLevel.swift */, + 859DB80D2CE6263B001F7210 /* TextZoomStorage.swift */, + ); + name = TextZoom; + sourceTree = ""; + }; + 859DB8192CE6265F001F7210 /* TextZoom */ = { + isa = PBXGroup; + children = ( + 859DB81A2CE6268C001F7210 /* MockTextZoomCoordinator.swift */, + 859DB81B2CE6268C001F7210 /* TextZoomTests.swift */, + ); + name = TextZoom; + sourceTree = ""; + }; 85AE668C20971FCA0014CF04 /* Notifications */ = { isa = PBXGroup; children = ( @@ -5764,10 +5797,10 @@ F12D98401F266B30003C2EE3 /* DuckDuckGo */ = { isa = PBXGroup; children = ( - 851952692CE2569600578553 /* Autocomplete */, 6FF9157F2B88E04F0042AC87 /* AdAttribution */, F17669A21E411D63003D3222 /* Application */, 981FED7222045FFA008488D7 /* AutoClear */, + 851952692CE2569600578553 /* Autocomplete */, 1E1D8B5B2994FF7800C96994 /* Autoconsent */, F40F843228C92B1C0081AE75 /* Autofill */, 98559FD0267099F400A83094 /* ContentBlocker */, @@ -5784,6 +5817,7 @@ F1BDDBFC2C340D9C00459306 /* Subscription */, 569437222BDD402600C0881B /* Sync */, F13B4BF71F18C9E800814661 /* Tabs */, + 859DB8192CE6265F001F7210 /* TextZoom */, 98EA2C3A218B9A880023E1DC /* Themes */, F12790DD1EBBDDF3001D3AEC /* Tutorials */, F194FAF91F14E605009B4DF8 /* UserInterface */, @@ -5979,7 +6013,6 @@ 850559CF23CF647C0055C0D5 /* PreserveLogins.swift */, 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */, 988F3DCE237D5C0F00AEE34C /* SchemeHandler.swift */, - 1E61BC2927074BED00B2854D /* TextSizeUserScript.swift */, 836A941C247F23C600BF8EF5 /* UserAgentManager.swift */, F1A886771F29394E0096251E /* WebCacheManager.swift */, 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */, @@ -7177,7 +7210,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1EEFD2D52758E31600B1393B /* textsize.js in Resources */, 83E2D2B4253CC16B005605F5 /* httpsMobileV2BloomSpec.json in Resources */, 98B001B0251EABB40090EC07 /* InfoPlist.strings in Resources */, 02BA15B126A89ECA00472DD7 /* ios-config.json in Resources */, @@ -7471,6 +7503,7 @@ 853C5F6121C277C7001F7A05 /* global.swift in Sources */, EE9D68D82AE15AD600B55EF4 /* UIApplicationExtension.swift in Sources */, 566B73702BECD46800FF1959 /* MainViewController+SyncAlerts.swift in Sources */, + 859DB8182CE6263C001F7210 /* TextZoomCoordinator.swift in Sources */, 6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */, F13B4BD31F1822C700814661 /* Tab.swift in Sources */, BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */, @@ -7513,9 +7546,11 @@ 1E1626072968413B0004127F /* ViewExtension.swift in Sources */, 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */, 6F691CCA2C4979EC002E9553 /* FavoritesTooltip.swift in Sources */, + 859DB8132CE6263C001F7210 /* TextZoomStorage.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, 8C4838B5221C8F7F008A6739 /* GestureToolbarButton.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, + 859DB8172CE6263C001F7210 /* TextZoomLevel.swift in Sources */, BDE91CD62C6294020005CB74 /* FeedbackCategoryProviding.swift in Sources */, 6F9FFE2A2C57ADB100A238BE /* EditableShortcutsView.swift in Sources */, 1E908BF329827C480008C8F3 /* AutoconsentManagement.swift in Sources */, @@ -7538,6 +7573,7 @@ CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */, 85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */, 310D09212799FD1A00DC0060 /* MIMEType.swift in Sources */, + 859DB8152CE6263C001F7210 /* TextZoomEditorModel.swift in Sources */, D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, D64648AD2B59936B0033090B /* SubscriptionEmailView.swift in Sources */, BD862E032B30DA170073E2EE /* VPNFeedbackFormViewModel.swift in Sources */, @@ -7596,6 +7632,7 @@ C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, 85B9CB8921AEBDD5009001F1 /* FavoriteHomeCell.swift in Sources */, + 859DB81C2CE6268C001F7210 /* MockTextZoomCoordinator.swift in Sources */, 1E39BEB02CC9477200496FBA /* SubscriptionCookieManageEventPixelMapping.swift in Sources */, C1935A102C88D131001AD72D /* AutofillSurveyManager.swift in Sources */, 7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, @@ -7641,6 +7678,7 @@ D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */, 851B128822200575004781BC /* Onboarding.swift in Sources */, 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */, + 859DB8142CE6263C001F7210 /* TextZoomController.swift in Sources */, F1EFB0062C5B8B8E009AB44B /* StatusIndicatorView.swift in Sources */, EE5929622C5A8AF40029380B /* AutofillUsageMonitor.swift in Sources */, 3151F0EE2735800800226F58 /* VoiceSearchFeedbackView.swift in Sources */, @@ -7843,6 +7881,7 @@ 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */, + 859DB8162CE6263C001F7210 /* TextZoomEditorView.swift in Sources */, 858566FB252E55D6007501B8 /* ImageCacheDebugViewController.swift in Sources */, D6E0C1832B7A2B1E00D5E1E9 /* DesktopDownloadView.swift in Sources */, 1E7A71172934EB6400B7EA19 /* OmniBarNotificationAnimator.swift in Sources */, @@ -7934,7 +7973,6 @@ 1E8AD1D727C2E24E00ABA377 /* DownloadsListRowViewModel.swift in Sources */, 9FEA222E2C324ECD006B03BF /* ViewVisibility.swift in Sources */, BD10B8AA2C7629740033115D /* Logger+Subscription.swift in Sources */, - 1E865AF0272042DB001C74F3 /* TextSizeSettingsViewController.swift in Sources */, D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, @@ -8006,6 +8044,7 @@ 1E722729292EB24D003B5F53 /* AppSettingsMock.swift in Sources */, 8536A1C8209AF2410050739E /* MockVariantManager.swift in Sources */, C1B7B53428944EFA0098FD6A /* CoreDataTestUtilities.swift in Sources */, + 859DB81E2CE62766001F7210 /* TextZoomTests.swift in Sources */, 1DE384E42BC41E2500871AF6 /* PixelExperimentTests.swift in Sources */, CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, @@ -8348,7 +8387,6 @@ 85CA53A824BB343700A6288C /* Favicons.swift in Sources */, F143C3181E4A99D200CFDE3A /* Link.swift in Sources */, 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */, - 1E61BC2A27074BED00B2854D /* TextSizeUserScript.swift in Sources */, 37CEFCAC2A673B90001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */, CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */, 85D2187624BF6164004373D2 /* FaviconSourcesProvider.swift in Sources */, @@ -10996,7 +11034,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 210.0.0; + version = 210.0.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ea6d72ec5a..3234f8630b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "cfb178099738bc6cd0c3a3d73df717420ef4a59f", - "version" : "210.0.0" + "revision" : "183b9c111176fd7821cd17d01c01ddb38486c9ac", + "version" : "210.0.1" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 52bc2e1ac9..4305c5b418 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -355,7 +355,9 @@ import os.log contextualOnboardingPixelReporter: onboardingPixelReporter, subscriptionFeatureAvailability: subscriptionFeatureAvailability, voiceSearchHelper: voiceSearchHelper, - subscriptionCookieManager: subscriptionCookieManager) + featureFlagger: AppDependencyProvider.shared.featureFlagger, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: makeTextZoomCoordinator()) main.loadViewIfNeeded() syncErrorHandler.alertPresenter = main @@ -408,6 +410,15 @@ import os.log return true } + private func makeTextZoomCoordinator() -> TextZoomCoordinator { + let provider = AppDependencyProvider.shared + let storage = TextZoomStorage() + + return TextZoomCoordinator(appSettings: provider.appSettings, + storage: storage, + featureFlagger: provider.featureFlagger) + } + private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, currentCookieStore: { [weak self] in diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index af3f91b5a4..4c7c9d8991 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -60,7 +60,7 @@ protocol AppSettings: AnyObject, AppDebugSettings { var currentAddressBarPosition: AddressBarPosition { get set } var showFullSiteAddress: Bool { get set } - var textSize: Int { get set } + var defaultTextZoomLevel: TextZoomLevel { get set } var favoritesDisplayMode: FavoritesDisplayMode { get set } diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 4cd79c3e0b..559a521900 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -27,7 +27,7 @@ public class AppUserDefaults: AppSettings { public struct Notifications { public static let doNotSellStatusChange = Notification.Name("com.duckduckgo.app.DoNotSellStatusChange") public static let currentFireButtonAnimationChange = Notification.Name("com.duckduckgo.app.CurrentFireButtonAnimationChange") - public static let textSizeChange = Notification.Name("com.duckduckgo.app.TextSizeChange") + public static let textZoomChange = Notification.Name("com.duckduckgo.app.TextZoomChange") public static let favoritesDisplayModeChange = Notification.Name("com.duckduckgo.app.FavoritesDisplayModeChange") public static let syncPausedStateChanged = SyncBookmarksAdapter.syncBookmarksPausedStateChanged public static let syncCredentialsPausedStateChanged = SyncCredentialsAdapter.syncCredentialsPausedStateChanged @@ -235,8 +235,21 @@ public class AppUserDefaults: AppSettings { } } - @UserDefaultsWrapper(key: .textSize, defaultValue: 100) - var textSize: Int + @UserDefaultsWrapper(key: .textZoom, defaultValue: 100) + private var textZoom: Int { + didSet { + NotificationCenter.default.post(name: Notifications.textZoomChange, object: textZoom) + } + } + + var defaultTextZoomLevel: TextZoomLevel { + get { + return TextZoomLevel(rawValue: textZoom) ?? .percent100 + } + set { + textZoom = newValue.rawValue + } + } public var favoritesDisplayMode: FavoritesDisplayMode { get { diff --git a/DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Contents.json deleted file mode 100644 index 172e1f8799..0000000000 --- a/DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Font-Larger-24.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Font-Larger-24.pdf b/DuckDuckGo/Assets.xcassets/TextSizeLarger.imageset/Font-Larger-24.pdf deleted file mode 100644 index 4f9ffc74208d7c97791bdcfd51839e5570d438ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1058 zcmZvb&2H2%5P zjX%%-Ojg^?-4!DWfFZ^9`wu|8zQ&syu+7e2fiHRT(Kh?x2~!7iuhkEoEe?=u?C+|y z`MXjf?2_F2HTkIfimZi|zC%Zf)|R+PPHGSCC_fvu>ZA zEj!Ds%iI>hk0!0PWn)@$iQl=6eo)4oqO4vk{x;*_k zB1z2|N?mtOwG=%wR7yv!p)0D|cHpP?e&gD4(YFVCi5J!Ga%xJFx9xzw;IYBUef8J6 U?8o1^=!UTzohn$Z-hRIO2NeG5(*OVf diff --git a/DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Contents.json deleted file mode 100644 index aee39a9607..0000000000 --- a/DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Font-Smaller-24.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Font-Smaller-24.pdf b/DuckDuckGo/Assets.xcassets/TextSizeSmaller.imageset/Font-Smaller-24.pdf deleted file mode 100644 index 4f76f020d0defb17e1762c56aa79ecf9859b3116..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1054 zcmZuw$!^;)5WUY=%*8-*2*sT=5CmxKrYPE?uF_l3gDNwQ3N5Xq6luRcq%G2llQ{^F zkDQq|ix!*p%>^R{fFZ^1=Py9Kyu_<3aP2l6fzSEuvuk(b0}4;(;ngtqu4o`zyT4WG z@(x-JN6%9*lr2~yjpf=)k3J2sgcGPi5yP~y z8X1o-YmkInA(?zZCZ_~yuU~Oy#PXP?22QkHa>h^)DWvkC215j+#xCdFYl$^H_|!|G zlrDnwUm6$y%8}ax`!6NK^W#VsW{d! z!Na&EQ)$>Tv3?1%cWv8^1K$1cOW`g1{_nTLY*XyqM0|1Es<`f+@b1`})Hg0;tB)u~ z%H7aC_N5yztX)2ZTXpT|TAwlOTqHL|?>STxHQUTUrH#!#Ra-9=Lj^g;Jge@}`?6R1 z2hYqR=@*d>Wv+^`sJq?d8j2p?67kf#d!T6Gu;Ss1S}CE>(~~iZdxUMGgx4ADNGa;; zIia{YrHpqud1cHo*4P(S?Rp5)`?&V)I2*dgo#R=xJwG%h%e!vGQ1HIN*=_aTdG-5W Rx#-7zIaUZ*EZ%>;c?S0O>8t<% diff --git a/DuckDuckGo/Base.lproj/Settings.storyboard b/DuckDuckGo/Base.lproj/Settings.storyboard index b0decd8ad5..39927df63a 100644 --- a/DuckDuckGo/Base.lproj/Settings.storyboard +++ b/DuckDuckGo/Base.lproj/Settings.storyboard @@ -1,162 +1,14 @@ - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -174,7 +26,7 @@ - + @@ -962,7 +814,7 @@ - + @@ -1066,16 +918,7 @@ - - - - - - - - - diff --git a/DuckDuckGo/ContentBlockingUpdating.swift b/DuckDuckGo/ContentBlockingUpdating.swift index 95e70dbcf6..95002bca9e 100644 --- a/DuckDuckGo/ContentBlockingUpdating.swift +++ b/DuckDuckGo/ContentBlockingUpdating.swift @@ -87,7 +87,6 @@ public final class ContentBlockingUpdating { .combineLatest(onNotificationWithInitial(PreserveLogins.Notifications.loginDetectionStateChanged), combine) .combineLatest(onNotificationWithInitial(AppUserDefaults.Notifications.doNotSellStatusChange), combine) .combineLatest(onNotificationWithInitial(AppUserDefaults.Notifications.autofillEnabledChange), combine) - .combineLatest(onNotificationWithInitial(AppUserDefaults.Notifications.textSizeChange), combine) .combineLatest(onNotificationWithInitial(AppUserDefaults.Notifications.didVerifyInternalUser), combine) .combineLatest(onNotificationWithInitial(ConfigurationManager.didUpdateTrackerDependencies) .receive(on: DispatchQueue.main), combine) diff --git a/DuckDuckGo/IntervalSlider.swift b/DuckDuckGo/IntervalSlider.swift index 37ced4bea3..e338daae7f 100644 --- a/DuckDuckGo/IntervalSlider.swift +++ b/DuckDuckGo/IntervalSlider.swift @@ -18,6 +18,7 @@ // import UIKit +import SwiftUI class IntervalSlider: UISlider { @@ -40,14 +41,8 @@ class IntervalSlider: UISlider { let thumbRect = thumbRect(forBounds: rect, trackRect: trackRect, value: 1.0) let thumbOffset = Darwin.round(thumbRect.width/2) - 3 - - let newTrackRect = trackRect.inset(by: UIEdgeInsets(top: 0.0, left: thumbOffset, bottom: 0.0, right: thumbOffset)) - - let color: UIColor = UIColor.cornflowerBlue - let bpath: UIBezierPath = UIBezierPath(rect: newTrackRect) - color.set() - bpath.fill() + let newTrackRect = trackRect.inset(by: UIEdgeInsets(top: 0.0, left: thumbOffset, bottom: 0.0, right: thumbOffset)) guard steps.count > 1 else { return } for i in 0...steps.count-1 { @@ -58,8 +53,13 @@ class IntervalSlider: UISlider { width: Constants.markWidth, height: Constants.markHeight) let markPath: UIBezierPath = UIBezierPath(roundedRect: markRect, cornerRadius: Constants.markCornerRadius) - color.set() - + + if Int(self.value) >= i { + minimumTrackTintColor?.set() + } else { + maximumTrackTintColor?.set() + } + markPath.fill() } } @@ -74,3 +74,47 @@ class IntervalSlider: UISlider { } } + +struct IntervalSliderRepresentable: UIViewRepresentable { + + @Binding var value: Int + + let steps: [Int] + + func makeUIView(context: Context) -> IntervalSlider { + let slider = IntervalSlider(frame: .zero) + slider.minimumTrackTintColor = UIColor(designSystemColor: .accent) + slider.maximumTrackTintColor = UIColor.systemGray3 + slider.steps = steps + slider.minimumValue = Float(0) + slider.maximumValue = Float(steps.count - 1) + slider.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .valueChanged) + return slider + } + + func updateUIView(_ uiView: IntervalSlider, context: Context) { + uiView.value = Float(value) + uiView.setNeedsDisplay() + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + var parent: IntervalSliderRepresentable + + init(_ parent: IntervalSliderRepresentable) { + self.parent = parent + } + + @objc func valueChanged(_ sender: IntervalSlider) { + let roundedValue = round(sender.value) + sender.value = roundedValue + if Int(roundedValue) != parent.value { + parent.value = Int(roundedValue) + sender.setNeedsDisplay() + } + } + } +} diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 8d91186009..3ab8722e7f 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -301,7 +301,8 @@ extension MainViewController { deepLink: deepLinkTarget, historyManager: historyManager, syncPausedStateManager: syncPausedStateManager, - privacyProDataReporter: privacyProDataReporter) + privacyProDataReporter: privacyProDataReporter, + textZoomCoordinator: textZoomCoordinator) Pixel.fire(pixel: .settingsPresented) if let navigationController = self.presentedViewController as? UINavigationController, diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 75c5777782..29364843b7 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -177,6 +177,9 @@ class MainViewController: UIViewController { fatalError("Use init?(code:") } + let preserveLogins: PreserveLogins + let textZoomCoordinator: TextZoomCoordinating + var historyManager: HistoryManaging var viewCoordinator: MainViewCoordinator! @@ -200,7 +203,10 @@ class MainViewController: UIViewController { statisticsStore: StatisticsStore = StatisticsUserDefaults(), subscriptionFeatureAvailability: SubscriptionFeatureAvailability, voiceSearchHelper: VoiceSearchHelperProtocol, - subscriptionCookieManager: SubscriptionCookieManaging + featureFlagger: FeatureFlagger, + preserveLogins: PreserveLogins = .shared, + subscriptionCookieManager: SubscriptionCookieManaging, + textZoomCoordinator: TextZoomCoordinating ) { self.bookmarksDatabase = bookmarksDatabase self.bookmarksDatabaseCleaner = bookmarksDatabaseCleaner @@ -223,7 +229,10 @@ class MainViewController: UIViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: contextualOnboardingPixelReporter, - subscriptionCookieManager: subscriptionCookieManager) + featureFlagger: featureFlagger, + subscriptionCookieManager: subscriptionCookieManager, + appSettings: appSettings, + textZoomCoordinator: textZoomCoordinator) self.syncPausedStateManager = syncPausedStateManager self.privacyProDataReporter = privacyProDataReporter self.homeTabManager = NewTabPageManager() @@ -234,7 +243,9 @@ class MainViewController: UIViewController { self.statisticsStore = statisticsStore self.subscriptionFeatureAvailability = subscriptionFeatureAvailability self.voiceSearchHelper = voiceSearchHelper + self.preserveLogins = preserveLogins self.subscriptionCookieManager = subscriptionCookieManager + self.textZoomCoordinator = textZoomCoordinator super.init(nibName: nil, bundle: nil) @@ -2659,6 +2670,7 @@ extension MainViewController: AutoClearWorker { self.bookmarksDatabaseCleaner?.cleanUpDatabaseNow() } + self.forgetTextZoom() await historyManager.removeAllHistory() self.clearInProgress = false @@ -2736,6 +2748,11 @@ extension MainViewController: AutoClearWorker { viewCoordinator.omniBar.dismissOnboardingPrivacyIconAnimation() } + private func forgetTextZoom() { + let allowedDomains = preserveLogins.allowedDomains + textZoomCoordinator.resetTextZoomLevels(excludingDomains: allowedDomains) + } + } extension MainViewController { diff --git a/DuckDuckGo/SettingsAccessibilityView.swift b/DuckDuckGo/SettingsAccessibilityView.swift index ad0141d69f..f801ae83d0 100644 --- a/DuckDuckGo/SettingsAccessibilityView.swift +++ b/DuckDuckGo/SettingsAccessibilityView.swift @@ -28,35 +28,33 @@ struct SettingsAccessibilityView: View { var body: some View { List { - Section { - // Text Size - if viewModel.state.textSize.enabled { - SettingsCellView(label: UserText.settingsText, - action: { viewModel.presentLegacyView(.textSize) }, - accessory: .rightDetail("\(viewModel.state.textSize.size)%"), - disclosureIndicator: true, - isButton: true) + if viewModel.state.textZoom.enabled { + Section(footer: Text(UserText.textZoomDescription)) { + // Text Size + SettingsPickerCellView(label: UserText.settingsText, + options: TextZoomLevel.allCases, + selectedOption: viewModel.textZoomLevelBinding) } } - Section(footer: Text(UserText.voiceSearchFooter)) { - // Private Voice Search - if viewModel.state.speechRecognitionAvailable { + if viewModel.state.speechRecognitionAvailable { + Section(footer: Text(UserText.voiceSearchFooter)) { + // Private Voice Search SettingsCellView(label: UserText.settingsVoiceSearch, accessory: .toggle(isOn: viewModel.voiceSearchEnabledBinding)) } - } - .alert(isPresented: $shouldShowNoMicrophonePermissionAlert) { - Alert(title: Text(UserText.noVoicePermissionAlertTitle), - message: Text(UserText.noVoicePermissionAlertMessage), - dismissButton: .default(Text(UserText.noVoicePermissionAlertOKbutton), - action: { - viewModel.shouldShowNoMicrophonePermissionAlert = false - }) - ) - } - .onChange(of: viewModel.shouldShowNoMicrophonePermissionAlert) { value in - shouldShowNoMicrophonePermissionAlert = value + .alert(isPresented: $shouldShowNoMicrophonePermissionAlert) { + Alert(title: Text(UserText.noVoicePermissionAlertTitle), + message: Text(UserText.noVoicePermissionAlertMessage), + dismissButton: .default(Text(UserText.noVoicePermissionAlertOKbutton), + action: { + viewModel.shouldShowNoMicrophonePermissionAlert = false + }) + ) + } + .onChange(of: viewModel.shouldShowNoMicrophonePermissionAlert) { value in + shouldShowNoMicrophonePermissionAlert = value + } } } .applySettingsListModifiers(title: UserText.accessibility, diff --git a/DuckDuckGo/SettingsGeneralView.swift b/DuckDuckGo/SettingsGeneralView.swift index 2307c092f7..427ec3bfc3 100644 --- a/DuckDuckGo/SettingsGeneralView.swift +++ b/DuckDuckGo/SettingsGeneralView.swift @@ -49,24 +49,24 @@ struct SettingsGeneralView: View { } } - Section(footer: Text(UserText.voiceSearchFooter)) { - // Private Voice Search - if viewModel.state.speechRecognitionAvailable { + if viewModel.state.speechRecognitionAvailable { + Section(footer: Text(UserText.voiceSearchFooter)) { + // Private Voice Search SettingsCellView(label: UserText.settingsVoiceSearch, accessory: .toggle(isOn: viewModel.voiceSearchEnabledBinding)) } - } - .alert(isPresented: $shouldShowNoMicrophonePermissionAlert) { - Alert(title: Text(UserText.noVoicePermissionAlertTitle), - message: Text(UserText.noVoicePermissionAlertMessage), - dismissButton: .default(Text(UserText.noVoicePermissionAlertOKbutton), - action: { - viewModel.shouldShowNoMicrophonePermissionAlert = false - }) - ) - } - .onChange(of: viewModel.shouldShowNoMicrophonePermissionAlert) { value in - shouldShowNoMicrophonePermissionAlert = value + .alert(isPresented: $shouldShowNoMicrophonePermissionAlert) { + Alert(title: Text(UserText.noVoicePermissionAlertTitle), + message: Text(UserText.noVoicePermissionAlertMessage), + dismissButton: .default(Text(UserText.noVoicePermissionAlertOKbutton), + action: { + viewModel.shouldShowNoMicrophonePermissionAlert = false + }) + ) + } + .onChange(of: viewModel.shouldShowNoMicrophonePermissionAlert) { value in + shouldShowNoMicrophonePermissionAlert = value + } } Section(header: Text(UserText.settingsCustomizeSection), diff --git a/DuckDuckGo/SettingsLegacyViewProvider.swift b/DuckDuckGo/SettingsLegacyViewProvider.swift index 736e81f8e5..dee451e380 100644 --- a/DuckDuckGo/SettingsLegacyViewProvider.swift +++ b/DuckDuckGo/SettingsLegacyViewProvider.swift @@ -53,7 +53,6 @@ class SettingsLegacyViewProvider: ObservableObject { case addToDock, sync, logins, - textSize, appIcon, gpc, autoconsent, @@ -72,7 +71,6 @@ class SettingsLegacyViewProvider: ObservableObject { // Legacy UIKit Views (Pushed unmodified) var addToDock: UIViewController { instantiate( "instructions", fromStoryboard: "HomeRow") } - var textSettings: UIViewController { return instantiate("TextSize", fromStoryboard: "Settings") } var appIcon: UIViewController { instantiate("AppIcon", fromStoryboard: "Settings") } var gpc: UIViewController { instantiate("DoNotSell", fromStoryboard: "Settings") } var autoConsent: UIViewController { instantiate("AutoconsentSettingsViewController", fromStoryboard: "Settings") } diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 299cfeba21..665077e29d 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -31,9 +31,9 @@ struct SettingsState { var position: AddressBarPosition } - struct TextSize { + struct TextZoom { var enabled: Bool - var size: Int + var level: TextZoomLevel } struct Subscription: Codable { @@ -57,7 +57,7 @@ struct SettingsState { var appTheme: ThemeName var appIcon: AppIcon var fireButtonAnimation: FireButtonAnimationType - var textSize: TextSize + var textZoom: TextZoom var addressBar: AddressBar var showsFullURL: Bool @@ -107,7 +107,7 @@ struct SettingsState { appTheme: .systemDefault, appIcon: AppIconManager.shared.appIcon, fireButtonAnimation: .fireRising, - textSize: TextSize(enabled: false, size: 100), + textZoom: TextZoom(enabled: false, level: .percent100), addressBar: AddressBar(enabled: false, position: .top), showsFullURL: false, sendDoNotSell: true, diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index e008a34ab6..5f96ce0215 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -43,6 +43,8 @@ final class SettingsViewModel: ObservableObject { var emailManager: EmailManager { EmailManager() } private let historyManager: HistoryManaging let privacyProDataReporter: PrivacyProDataReporting? + let textZoomCoordinator: TextZoomCoordinating + // Subscription Dependencies private let subscriptionManager: SubscriptionManager let subscriptionFeatureAvailability: SubscriptionFeatureAvailability @@ -63,7 +65,8 @@ final class SettingsViewModel: ObservableObject { // App Data State Notification Observer private var appDataClearingObserver: Any? - + private var textZoomObserver: Any? + // Closures to interact with legacy view controllers through the container var onRequestPushLegacyView: ((UIViewController) -> Void)? var onRequestPresentLegacyView: ((UIViewController, _ modal: Bool) -> Void)? @@ -77,7 +80,7 @@ final class SettingsViewModel: ObservableObject { enum Features { case sync case autofillAccessCredentialManagement - case textSize + case zoomLevel case voiceSearch case addressbarPosition case speechRecognition @@ -256,6 +259,20 @@ final class SettingsViewModel: ObservableObject { ) } + var textZoomLevelBinding: Binding { + Binding( + get: { self.state.textZoom.level }, + set: { newValue in + Pixel.fire(.settingsAccessiblityTextZoom, withAdditionalParameters: [ + PixelParameters.textZoomInitial: String(self.appSettings.defaultTextZoomLevel.rawValue), + PixelParameters.textZoomUpdated: String(newValue.rawValue), + ]) + self.appSettings.defaultTextZoomLevel = newValue + self.state.textZoom.level = newValue + } + ) + } + var duckPlayerModeBinding: Binding { Binding( get: { @@ -368,7 +385,8 @@ final class SettingsViewModel: ObservableObject { deepLink: SettingsDeepLinkSection? = nil, historyManager: HistoryManaging, syncPausedStateManager: any SyncPausedStateManaging, - privacyProDataReporter: PrivacyProDataReporting) { + privacyProDataReporter: PrivacyProDataReporting, + textZoomCoordinator: TextZoomCoordinating) { self.state = SettingsState.defaults self.legacyViewProvider = legacyViewProvider @@ -379,6 +397,7 @@ final class SettingsViewModel: ObservableObject { self.historyManager = historyManager self.syncPausedStateManager = syncPausedStateManager self.privacyProDataReporter = privacyProDataReporter + self.textZoomCoordinator = textZoomCoordinator setupNotificationObservers() updateRecentlyVisitedSitesVisibility() @@ -387,6 +406,7 @@ final class SettingsViewModel: ObservableObject { deinit { subscriptionSignOutObserver = nil appDataClearingObserver = nil + textZoomObserver = nil } } @@ -402,7 +422,7 @@ extension SettingsViewModel { appTheme: appSettings.currentThemeName, appIcon: AppIconManager.shared.appIcon, fireButtonAnimation: appSettings.currentFireButtonAnimation, - textSize: SettingsState.TextSize(enabled: !isPad, size: appSettings.textSize), + textZoom: SettingsState.TextZoom(enabled: textZoomCoordinator.isEnabled, level: appSettings.defaultTextZoomLevel), addressBar: SettingsState.AddressBar(enabled: !isPad, position: appSettings.currentAddressBarPosition), showsFullURL: appSettings.showFullSiteAddress, sendDoNotSell: appSettings.sendDoNotSell, @@ -605,10 +625,6 @@ extension SettingsViewModel { pushViewController(legacyViewProvider.loginSettings(delegate: self, selectedAccount: state.activeWebsiteAccount)) - case .textSize: - firePixel(.settingsAccessiblityTextSize) - pushViewController(legacyViewProvider.textSettings) - case .gpc: firePixel(.settingsDoNotSellShown) pushViewController(legacyViewProvider.gpc) @@ -771,6 +787,12 @@ extension SettingsViewModel { self?.state.autoclearDataEnabled = (AutoClearSettingsModel(settings: settings) != nil) } + textZoomObserver = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.textZoomChange, + object: nil, + queue: .main, using: { [weak self] _ in + guard let self = self else { return } + self.state.textZoom = SettingsState.TextZoom(enabled: true, level: self.appSettings.defaultTextZoomLevel) + }) } func restoreAccountPurchase() async { diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index 46efdc1369..9f9786fecb 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -42,7 +42,10 @@ class TabManager { private let contextualOnboardingPresenter: ContextualOnboardingPresenting private let contextualOnboardingLogic: ContextualOnboardingLogic private let onboardingPixelReporter: OnboardingPixelReporting + private let featureFlagger: FeatureFlagger + private let textZoomCoordinator: TextZoomCoordinating private let subscriptionCookieManager: SubscriptionCookieManaging + private let appSettings: AppSettings weak var delegate: TabDelegate? @@ -60,7 +63,10 @@ class TabManager { contextualOnboardingPresenter: ContextualOnboardingPresenting, contextualOnboardingLogic: ContextualOnboardingLogic, onboardingPixelReporter: OnboardingPixelReporting, - subscriptionCookieManager: SubscriptionCookieManaging) { + featureFlagger: FeatureFlagger, + subscriptionCookieManager: SubscriptionCookieManaging, + appSettings: AppSettings, + textZoomCoordinator: TextZoomCoordinating) { self.model = model self.previewsSource = previewsSource self.bookmarksDatabase = bookmarksDatabase @@ -71,7 +77,10 @@ class TabManager { self.contextualOnboardingPresenter = contextualOnboardingPresenter self.contextualOnboardingLogic = contextualOnboardingLogic self.onboardingPixelReporter = onboardingPixelReporter + self.featureFlagger = featureFlagger self.subscriptionCookieManager = subscriptionCookieManager + self.appSettings = appSettings + self.textZoomCoordinator = textZoomCoordinator registerForNotifications() } @@ -84,6 +93,7 @@ class TabManager { @MainActor private func buildController(forTab tab: Tab, url: URL?, inheritedAttribution: AdClickAttributionLogic.State?) -> TabViewController { let configuration = WKWebViewConfiguration.persistent() + let controller = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, @@ -93,8 +103,9 @@ class TabManager { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - subscriptionCookieManager: subscriptionCookieManager) + featureFlagger: featureFlagger, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: textZoomCoordinator) controller.applyInheritedAttribution(inheritedAttribution) controller.attachWebView(configuration: configuration, andLoadRequest: url == nil ? nil : URLRequest.userInitiated(url!), @@ -172,8 +183,9 @@ class TabManager { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - subscriptionCookieManager: subscriptionCookieManager) + featureFlagger: featureFlagger, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: textZoomCoordinator) controller.attachWebView(configuration: configCopy, andLoadRequest: request, consumeCookies: !model.hasActiveTabs, @@ -356,4 +368,5 @@ extension TabManager { TabPreviewsCleanup.shared.startCleanup(with: model, source: previewsSource) } } + } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 5609aec797..7e4bb51b8c 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -197,7 +197,6 @@ class TabViewController: UIViewController { var failedURL: URL? var storedSpecialErrorPageUserScript: SpecialErrorPageUserScript? var isSpecialErrorPageVisible: Bool = false - let syncService: DDGSyncing private let daxDialogsDebouncer = Debouncer(mode: .common) @@ -332,7 +331,9 @@ class TabViewController: UIViewController { onboardingPixelReporter: OnboardingCustomInteractionPixelReporting, urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), featureFlagger: FeatureFlagger, - subscriptionCookieManager: SubscriptionCookieManaging) -> TabViewController { + subscriptionCookieManager: SubscriptionCookieManaging, + textZoomCoordinator: TextZoomCoordinating) -> TabViewController { + let storyboard = UIStoryboard(name: "Tab", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "TabViewController", creator: { coder in TabViewController(coder: coder, @@ -348,7 +349,8 @@ class TabViewController: UIViewController { onboardingPixelReporter: onboardingPixelReporter, urlCredentialCreator: urlCredentialCreator, featureFlagger: featureFlagger, - subscriptionCookieManager: subscriptionCookieManager + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: textZoomCoordinator ) }) return controller @@ -367,6 +369,7 @@ class TabViewController: UIViewController { let contextualOnboardingPresenter: ContextualOnboardingPresenting let contextualOnboardingLogic: ContextualOnboardingLogic let onboardingPixelReporter: OnboardingCustomInteractionPixelReporting + let textZoomCoordinator: TextZoomCoordinating required init?(coder aDecoder: NSCoder, tabModel: Tab, @@ -382,7 +385,8 @@ class TabViewController: UIViewController { onboardingPixelReporter: OnboardingCustomInteractionPixelReporting, urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), featureFlagger: FeatureFlagger, - subscriptionCookieManager: SubscriptionCookieManaging) { + subscriptionCookieManager: SubscriptionCookieManaging, + textZoomCoordinator: TextZoomCoordinating) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase @@ -402,6 +406,8 @@ class TabViewController: UIViewController { self.urlCredentialCreator = urlCredentialCreator self.featureFlagger = featureFlagger self.subscriptionCookieManager = subscriptionCookieManager + self.textZoomCoordinator = textZoomCoordinator + super.init(coder: aDecoder) // Assign itself as tabNavigationHandler for DuckPlayer @@ -418,7 +424,7 @@ class TabViewController: UIViewController { preserveLoginsWorker = PreserveLoginsWorker(controller: self) initAttributionLogic() decorate() - addTextSizeObserver() + addTextZoomObserver() subscribeToEmailProtectionSignOutNotification() registerForDownloadsNotifications() registerForAddressBarLocationNotifications() @@ -542,6 +548,7 @@ class TabViewController: UIViewController { } else { webView = WKWebView(frame: view.bounds, configuration: configuration) } + textZoomCoordinator.onWebViewCreated(applyToWebView: webView) webView.allowsLinkPreview = true webView.allowsBackForwardNavigationGestures = true @@ -958,10 +965,10 @@ class TabViewController: UIViewController { breakageAdditionalInfo: makeBreakageAdditionalInfo()) } - private func addTextSizeObserver() { + private func addTextZoomObserver() { NotificationCenter.default.addObserver(self, - selector: #selector(onTextSizeChange), - name: AppUserDefaults.Notifications.textSizeChange, + selector: #selector(onTextZoomChange), + name: AppUserDefaults.Notifications.textZoomChange, object: nil) } @@ -974,8 +981,8 @@ class TabViewController: UIViewController { } } - @objc func onTextSizeChange() { - webView.adjustTextSize(appSettings.textSize) + @objc func onTextZoomChange() { + textZoomCoordinator.onTextZoomChange(applyToWebView: webView) } @objc func onDuckDuckGoEmailSignOut(_ notification: Notification) { @@ -1310,6 +1317,7 @@ extension TabViewController: WKNavigationDelegate { let tld = storageCache.tld let httpsForced = tld.domain(lastUpgradedURL?.host) == tld.domain(webView.url?.host) onWebpageDidStartLoading(httpsForced: httpsForced) + textZoomCoordinator.onNavigationCommitted(applyToWebView: webView) } private func onWebpageDidStartLoading(httpsForced: Bool) { @@ -1439,6 +1447,7 @@ extension TabViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { self.currentlyLoadedURL = webView.url + onTextZoomChange() adClickAttributionDetection.onDidFinishNavigation(url: webView.url) adClickAttributionLogic.onDidFinishNavigation(host: webView.url?.host) hideProgressIndicator() @@ -2167,7 +2176,7 @@ extension TabViewController { */ private func setupOrClearTemporaryDownload(for response: URLResponse) -> WKNavigationResponsePolicy? { let downloadManager = AppDependencyProvider.shared.downloadManager - guard let url = response.url, + guard response.url != nil, let downloadMetaData = downloadManager.downloadMetaData(for: response), !downloadMetaData.mimeType.isHTML else { @@ -2544,7 +2553,6 @@ extension TabViewController: UserContentControllerDelegate { userScripts.autofillUserScript.vaultDelegate = vaultManager userScripts.faviconScript.delegate = faviconUpdater userScripts.printingUserScript.delegate = self - userScripts.textSizeUserScript.textSizeAdjustmentInPercents = appSettings.textSize userScripts.loginFormDetectionScript?.delegate = self userScripts.autoconsentUserScript.delegate = self userScripts.specialErrorPageUserScript?.delegate = self diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 8e8d715052..fe193e42a0 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -156,6 +156,10 @@ extension TabViewController { entries.append(entry) } + if let entry = textZoomCoordinator.makeBrowsingMenuEntry(forLink: link, inController: self, forWebView: self.webView) { + entries.append(entry) + } + let title = self.tabModel.isDesktop ? UserText.actionRequestMobileSite : UserText.actionRequestDesktopSite let image = self.tabModel.isDesktop ? UIImage(named: "Device-Mobile-16")! : UIImage(named: "Device-Desktop-16")! entries.append(BrowsingMenuEntry.regular(name: title, image: image, action: { [weak self] in @@ -166,7 +170,7 @@ extension TabViewController { return entries } - + private func buildKeepSignInEntry(forLink link: Link) -> BrowsingMenuEntry? { guard let domain = link.url.host, !link.url.isDuckDuckGo else { return nil } let isFireproofed = PreserveLogins.shared.isAllowed(cookieDomain: domain) diff --git a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index 8de6d3de6c..974fbbf895 100644 --- a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -102,17 +102,20 @@ extension TabViewController { fileprivate func buildOpenLinkPreview(for url: URL) -> UIViewController? { let tab = Tab(link: Link(title: nil, url: url)) - let tabController = TabViewController.loadFromStoryboard(model: tab, - bookmarksDatabase: bookmarksDatabase, - historyManager: historyManager, - syncService: syncService, - duckPlayer: duckPlayer, - privacyProDataReporter: privacyProDataReporter, - contextualOnboardingPresenter: contextualOnboardingPresenter, - contextualOnboardingLogic: contextualOnboardingLogic, - onboardingPixelReporter: onboardingPixelReporter, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - subscriptionCookieManager: subscriptionCookieManager) + let tabController = TabViewController.loadFromStoryboard( + model: tab, + bookmarksDatabase: bookmarksDatabase, + historyManager: historyManager, + syncService: syncService, + duckPlayer: duckPlayer, + privacyProDataReporter: privacyProDataReporter, + contextualOnboardingPresenter: contextualOnboardingPresenter, + contextualOnboardingLogic: contextualOnboardingLogic, + onboardingPixelReporter: onboardingPixelReporter, + featureFlagger: featureFlagger, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: textZoomCoordinator) + tabController.isLinkPreview = true let configuration = WKWebViewConfiguration.nonPersistent() tabController.attachWebView(configuration: configuration, andLoadRequest: URLRequest.userInitiated(url), consumeCookies: false) diff --git a/DuckDuckGo/TextSizeSettingsViewController.swift b/DuckDuckGo/TextSizeSettingsViewController.swift deleted file mode 100644 index c28cfd61be..0000000000 --- a/DuckDuckGo/TextSizeSettingsViewController.swift +++ /dev/null @@ -1,221 +0,0 @@ -// -// TextSizeSettingsViewController.swift -// DuckDuckGo -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UIKit -import Core - -class TextSizeSettingsViewController: UITableViewController { - - @IBOutlet var customBackBarButtonItem: UIBarButtonItem! - @IBOutlet weak var customBackInnerButton: UIButton! - @IBOutlet weak var descriptionLabel: UILabel! - @IBOutlet weak var textSizeSlider: IntervalSlider! - @IBOutlet weak var smallerTextIcon: UIImageView! - @IBOutlet weak var largerTextIcon: UIImageView! - @IBOutlet weak var currentSelectedValueLabel: UILabel! - - private let predefinedPercentages = [80, 90, 100, 110, 120, 130, 140, 150, 160, 170] - - private let initialTextSizePercentage: Int = AppDependencyProvider.shared.appSettings.textSize - private var currentTextSizePercentage: Int = AppDependencyProvider.shared.appSettings.textSize - - private var hasAdjustedDetent: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.leftBarButtonItem = nil - - configureCustomBackButtonTitle() - configureDescriptionLabel() - configureSlider() - updateTextSizeFooterLabel() - - decorate() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - adjustDetentOnPresentation() - } - - private func adjustDetentOnPresentation() { - if !hasAdjustedDetent, let sheetController = navigationController?.presentationController as? UISheetPresentationController { - sheetController.detents = [.medium(), .large()] - sheetController.delegate = self - sheetController.preferredCornerRadius = 16 - - sheetController.animateChanges { - sheetController.selectedDetentIdentifier = .medium - } - - navigationItem.leftBarButtonItem = customBackBarButtonItem - - hasAdjustedDetent = true - } - } - - override func willMove(toParent parent: UIViewController?) { - super.willMove(toParent: parent) - - if parent == nil { - firePixelForTextSizeChange() - } - } - - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - let theme = ThemeManager.shared.currentTheme - cell.decorate(with: theme) - } - - private func configureCustomBackButtonTitle() { - let topViewController = navigationController?.topViewController - let previousViewController = navigationController?.viewControllers.last(where: { $0 != topViewController }) - let backTitle = previousViewController?.navigationItem.title ?? "" - - customBackInnerButton.setTitle(backTitle, for: .normal) - } - - private func configureDescriptionLabel() { - descriptionLabel.text = UserText.textSizeDescription - adjustDescriptionLabelHeight() - } - - private func adjustDescriptionLabelHeight() { - guard let headerView = tableView.tableHeaderView else { return } - - let adjustedSize = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) - - if headerView.frame.size.height != adjustedSize.height { - headerView.frame.size.height = adjustedSize.height - } - } - - private func configureSlider() { - textSizeSlider.minimumValue = 0 - textSizeSlider.maximumValue = Float(predefinedPercentages.count - 1) - - textSizeSlider.steps = predefinedPercentages - - configureSliderCurrentSelection() - } - - private func configureSliderCurrentSelection() { - let currentSelectionIndex = predefinedPercentages.firstIndex(of: currentTextSizePercentage) ?? 0 - textSizeSlider.value = Float(currentSelectionIndex) - } - - private func updateTextSizeFooterLabel() { - let percentageString = "\(currentTextSizePercentage)%" - currentSelectedValueLabel.text = UserText.textSizeFooter(for: percentageString) - - } - - @IBAction func customBackButtonTapped(_ sender: AnyObject) { - var shouldPopViewController: Bool = true - - if let sheetController = navigationController?.presentationController as? UISheetPresentationController { - sheetController.detents = [.large()] - - // We recreate two-step detent animation, like on push but in reverse - if sheetController.selectedDetentIdentifier != .large { - shouldPopViewController = false - - // First step is to animate detent to large - sheetController.animateChanges { - sheetController.selectedDetentIdentifier = .large - } - - // Second step is to actually pop the view controller - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - self.navigationItem.leftBarButtonItem = nil - self.navigationController?.popViewController(animated: true) - } - } - } - - if shouldPopViewController { - navigationItem.leftBarButtonItem = nil - navigationController?.popViewController(animated: true) - } - } - - @IBAction func onSliderValueChanged(_ sender: Any) { - let roundedValue = round(textSizeSlider.value) - - // make the slider snap - textSizeSlider.value = roundedValue - - let index = Int(roundedValue) - let newTextSizePercentage = predefinedPercentages[index] - - if newTextSizePercentage != currentTextSizePercentage { - currentTextSizePercentage = newTextSizePercentage - - updateTextSizeFooterLabel() - storeTextSizeInAppSettings(currentTextSizePercentage) - } - } - - private func storeTextSizeInAppSettings(_ percentage: Int) { - let appSettings = AppDependencyProvider.shared.appSettings - appSettings.textSize = percentage - - NotificationCenter.default.post(name: AppUserDefaults.Notifications.textSizeChange, object: self) - } - - private func firePixelForTextSizeChange() { - guard initialTextSizePercentage != currentTextSizePercentage else { return } - - Pixel.fire(pixel: .textSizeSettingsChanged, withAdditionalParameters: [PixelParameters.textSizeInitial: "\(initialTextSizePercentage)", - PixelParameters.textSizeUpdated: "\(currentTextSizePercentage)"]) - } -} - -extension TextSizeSettingsViewController: UISheetPresentationControllerDelegate { - - func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { - navigationItem.leftBarButtonItem = sheetPresentationController.selectedDetentIdentifier == .medium ? customBackBarButtonItem : nil - } -} - -extension TextSizeSettingsViewController: UIAdaptivePresentationControllerDelegate { - - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - firePixelForTextSizeChange() - } -} - -extension TextSizeSettingsViewController { - - private func decorate() { - let theme = ThemeManager.shared.currentTheme - descriptionLabel.textColor = theme.tableCellTextColor - smallerTextIcon.tintColor = theme.tableCellTextColor - largerTextIcon.tintColor = theme.tableCellTextColor - currentSelectedValueLabel.textColor = theme.tableHeaderTextColor - - tableView.backgroundColor = theme.backgroundColor - tableView.separatorColor = theme.tableCellSeparatorColor - - tableView.reloadData() - } -} diff --git a/DuckDuckGo/TextZoomController.swift b/DuckDuckGo/TextZoomController.swift new file mode 100644 index 0000000000..e175c81d2a --- /dev/null +++ b/DuckDuckGo/TextZoomController.swift @@ -0,0 +1,38 @@ +// +// TextZoomController.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import SwiftUI + +class TextZoomController: UIHostingController { + + let coordinator: TextZoomCoordinating + let model: TextZoomEditorModel + + @MainActor init(domain: String, coordinator: TextZoomCoordinating, defaultTextZoom: TextZoomLevel) { + self.coordinator = coordinator + self.model = TextZoomEditorModel(domain: domain, coordinator: coordinator, defaultTextZoom: defaultTextZoom) + super.init(rootView: TextZoomEditorView(model: model)) + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/DuckDuckGo/TextZoomCoordinator.swift b/DuckDuckGo/TextZoomCoordinator.swift new file mode 100644 index 0000000000..3aec4d1ae2 --- /dev/null +++ b/DuckDuckGo/TextZoomCoordinator.swift @@ -0,0 +1,181 @@ +// +// TextZoomCoordinator.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit +import Common +import BrowserServicesKit +import Core + +/// Central point for coordinating text zoom activities. +/// * Host is used to represent unaltered host from a URL. Domain is a normalised host. +protocol TextZoomCoordinating { + + /// Based on .textZoom feature flag + var isEnabled: Bool { get } + + /// @return The zoom level for a host or the current default if there isn't one. Uses eTLDplus1 to determine the domain. + func textZoomLevel(forHost host: String?) -> TextZoomLevel + + /// Sets the text zoom level for a host. Uses eLTDplus1 to determine the domain. + /// If the level matches the global default then this specific level for the host is forgotten. + func set(textZoomLevel level: TextZoomLevel, forHost host: String?) + + /// Reset, ie 'forget', the saved zoom levels for all domains except the ones specified. + func resetTextZoomLevels(excludingDomains: [String]) + + /// Applies appropriate text zoom to webview on creation,. Does nothing if feature is disabled. + func onWebViewCreated(applyToWebView webView: WKWebView) + + /// Applies appropriate text zoom when navigation is committed. Does nothing if feature is disabled. + func onNavigationCommitted(applyToWebView webView: WKWebView) + + /// Applies appropriate text zoom to webview when the text zoom has changed (e.g. in settings or for the current tab). + /// Does nothing if feature is disabled. + func onTextZoomChange(applyToWebView webView: WKWebView) + + /// Shows a text zoom editor for the current webview. Does nothing if the feature is disabled. + func showTextZoomEditor(inController controller: UIViewController, forWebView webView: WKWebView) + + /// Creates a browsing menu entry for the given link. Returns nil if the feature is disabled. + func makeBrowsingMenuEntry(forLink: Link, inController controller: UIViewController, forWebView webView: WKWebView) -> BrowsingMenuEntry? + +} + +final class TextZoomCoordinator: TextZoomCoordinating { + + let appSettings: AppSettings + let storage: TextZoomStoring + let featureFlagger: FeatureFlagger + + var isEnabled: Bool { + featureFlagger.isFeatureOn(.textZoom) + } + + init(appSettings: AppSettings, storage: TextZoomStoring, featureFlagger: FeatureFlagger) { + self.appSettings = appSettings + self.storage = storage + self.featureFlagger = featureFlagger + } + + func textZoomLevel(forHost host: String?) -> TextZoomLevel { + let domain = TLD().eTLDplus1(host) ?? "" + // If the webview returns no host then there won't be a setting for a blank string anyway. + return storage.textZoomLevelForDomain(domain) + // And if there's no setting for whatever domain is passed in, use the app default + ?? appSettings.defaultTextZoomLevel + } + + func set(textZoomLevel level: TextZoomLevel, forHost host: String?) { + guard let domain = TLD().eTLDplus1(host) else { return } + if level == appSettings.defaultTextZoomLevel { + storage.removeTextZoomLevel(forDomain: domain) + } else { + storage.set(textZoomLevel: level, forDomain: domain) + } + } + + func resetTextZoomLevels(excludingDomains domains: [String]) { + storage.resetTextZoomLevels(excludingDomains: domains) + } + + func onWebViewCreated(applyToWebView webView: WKWebView) { + applyTextZoom(webView) + } + + func onNavigationCommitted(applyToWebView webView: WKWebView) { + applyTextZoom(webView) + } + + func onTextZoomChange(applyToWebView webView: WKWebView) { + applyTextZoom(webView) + } + + private func applyTextZoom(_ webView: WKWebView) { + guard isEnabled else { return } + let level = textZoomLevel(forHost: webView.url?.host) + let dynamicTypeScalePercentage = UIFontMetrics.default.scaledValue(for: 1.0) + let viewScale = CGFloat(level.rawValue) / 100 * dynamicTypeScalePercentage + webView.applyViewScale(viewScale) + } + + @MainActor + func showTextZoomEditor(inController controller: UIViewController, forWebView webView: WKWebView) { + guard isEnabled else { return } + + guard let domain = TLD().eTLDplus1(webView.url?.host) else { return } + let zoomController = TextZoomController( + domain: domain, + coordinator: self, + defaultTextZoom: appSettings.defaultTextZoomLevel + ) + + zoomController.modalPresentationStyle = .formSheet + if #available(iOS 16.0, *) { + zoomController.sheetPresentationController?.detents = [.custom(resolver: { _ in + return 152 + })] + + zoomController.sheetPresentationController?.prefersScrollingExpandsWhenScrolledToEdge = false + zoomController.sheetPresentationController?.prefersEdgeAttachedInCompactHeight = true + zoomController.sheetPresentationController?.widthFollowsPreferredContentSizeWhenEdgeAttached = true + } else { + zoomController.sheetPresentationController?.detents = [.medium()] + } + + controller.present(zoomController, animated: true) + } + + func makeBrowsingMenuEntry(forLink link: Link, + inController controller: UIViewController, + forWebView webView: WKWebView) -> BrowsingMenuEntry? { + guard isEnabled else { return nil } + + let label: String + if let domain = TLD().eTLDplus1(link.url.host), + let level = storage.textZoomLevelForDomain(domain) { + label = UserText.textZoomWithPercentForMenuItem(level.rawValue) + } else { + label = UserText.textZoomMenuItem + } + + return BrowsingMenuEntry.regular(name: label, + image: UIImage(named: "Type-Size-16")!, + showNotificationDot: false) { [weak self, weak controller, weak webView] in + guard let self = self, let controller = controller, let webView = webView else { return } + Task { @MainActor in + self.showTextZoomEditor(inController: controller, forWebView: webView) + Pixel.fire(pixel: .browsingMenuZoom) + } + } + } +} + +extension WKWebView { + + func applyViewScale(_ scale: CGFloat) { + let key = "viewScale" + guard responds(to: NSSelectorFromString("_\(key)")) else { + assertionFailure("viewScale API has changed") + return + } + setValue(scale, forKey: key) + } + +} diff --git a/DuckDuckGo/TextZoomEditorModel.swift b/DuckDuckGo/TextZoomEditorModel.swift new file mode 100644 index 0000000000..44e187d762 --- /dev/null +++ b/DuckDuckGo/TextZoomEditorModel.swift @@ -0,0 +1,73 @@ +// +// TextZoomEditorModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Core + +class TextZoomEditorModel: ObservableObject { + + let domain: String + let coordinator: TextZoomCoordinating + let initialValue: TextZoomLevel + + var valueAsPercent: Int { + TextZoomLevel.allCases[value].rawValue + } + + @Published var value: Int = 0 { + didSet { + valueWasSet() + } + } + + @Published var title: String = "" + + init(domain: String, coordinator: TextZoomCoordinating, defaultTextZoom: TextZoomLevel) { + self.domain = domain + self.coordinator = coordinator + self.initialValue = coordinator.textZoomLevel(forHost: domain) + value = TextZoomLevel.allCases.firstIndex(of: initialValue) ?? 0 + } + + func increment() { + value = min(TextZoomLevel.allCases.count - 1, value + 1) + } + + func decrement() { + value = max(0, value - 1) + } + + private func valueWasSet() { + title = UserText.textZoomWithPercentSheetTitle(TextZoomLevel.allCases[value].rawValue) + coordinator.set(textZoomLevel: TextZoomLevel.allCases[value], forHost: domain) + NotificationCenter.default.post( + name: AppUserDefaults.Notifications.textZoomChange, + object: nil) + DailyPixel.fire(pixel: .textZoomChangedOnPageDaily) + } + + func onDismiss() { + guard initialValue.rawValue != TextZoomLevel.allCases[value].rawValue else { return } + Pixel.fire(.textZoomChangedOnPage, withAdditionalParameters: [ + PixelParameters.textZoomInitial: String(initialValue.rawValue), + PixelParameters.textZoomUpdated: String(TextZoomLevel.allCases[value].rawValue), + ]) + } + +} diff --git a/DuckDuckGo/TextZoomEditorView.swift b/DuckDuckGo/TextZoomEditorView.swift new file mode 100644 index 0000000000..acc28235b8 --- /dev/null +++ b/DuckDuckGo/TextZoomEditorView.swift @@ -0,0 +1,95 @@ +// +// TextZoomEditorView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct TextZoomEditorView: View { + + @ObservedObject var model: TextZoomEditorModel + + @Environment(\.dismiss) var dismiss + + @ViewBuilder + func header() -> some View { + ZStack(alignment: .center) { + Text(model.title) + .font(Font(uiFont: .daxHeadline())) + .frame(alignment: .center) + .foregroundStyle(Color(designSystemColor: .textPrimary)) + + Button { + model.onDismiss() + dismiss() + } label: { + Text(UserText.navigationTitleDone) + .font(Font(uiFont: .daxHeadline())) + } + .buttonStyle(.plain) + .padding(0) + .frame(maxWidth: .infinity, alignment: .trailing) + .foregroundStyle(Color(designSystemColor: .textPrimary)) + } + .padding(.horizontal, 16) + .frame(height: 56) + } + + func slider() -> some View { + HStack(spacing: 6) { + Button { + model.decrement() + } label: { + Image("Font-Smaller-24") + } + .foregroundColor(Color(designSystemColor: .textPrimary)) + .padding(12) + .padding(.leading, 8) + + IntervalSliderRepresentable( + value: $model.value, + steps: TextZoomLevel.allCases.map { $0.rawValue }) + .padding(.vertical) + + Button { + model.increment() + } label: { + Image("Font-Larger-24") + } + .foregroundColor(Color(designSystemColor: .textPrimary)) + .padding(12) + .padding(.trailing, 8) + } + .background(RoundedRectangle(cornerRadius: 8) + .foregroundColor(Color(designSystemColor: .surface))) + .frame(height: 64) + .padding(.horizontal, 16) + + } + + var body: some View { + VStack { + header() + Spacer() + slider() + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(designSystemColor: .background)) + } + +} diff --git a/DuckDuckGo/TextZoomLevel.swift b/DuckDuckGo/TextZoomLevel.swift new file mode 100644 index 0000000000..ef0ba39812 --- /dev/null +++ b/DuckDuckGo/TextZoomLevel.swift @@ -0,0 +1,39 @@ +// +// TextZoomLevel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum TextZoomLevel: Int, CaseIterable, CustomStringConvertible { + + var description: String { + return "\(self.rawValue)%" + } + + case percent80 = 80 + case percent90 = 90 + case percent100 = 100 + case percent110 = 110 + case percent120 = 120 + case percent130 = 130 + case percent140 = 140 + case percent150 = 150 + case percent160 = 160 + case percent170 = 170 + +} diff --git a/DuckDuckGo/TextZoomStorage.swift b/DuckDuckGo/TextZoomStorage.swift new file mode 100644 index 0000000000..80920b380c --- /dev/null +++ b/DuckDuckGo/TextZoomStorage.swift @@ -0,0 +1,60 @@ +// +// TextZoomStorage.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Core +import Common + +protocol TextZoomStoring { + func textZoomLevelForDomain(_ domain: String) -> TextZoomLevel? + func set(textZoomLevel: TextZoomLevel, forDomain domain: String) + func removeTextZoomLevel(forDomain domain: String) + func resetTextZoomLevels(excludingDomains: [String]) +} + +class TextZoomStorage: TextZoomStoring { + + @UserDefaultsWrapper(key: .domainTextZoomStorage, defaultValue: [:]) + var textZoomLevels: [String: Int] + + func textZoomLevelForDomain(_ domain: String) -> TextZoomLevel? { + guard let zoomLevel = textZoomLevels[domain] else { + return nil + } + return TextZoomLevel(rawValue: zoomLevel) + } + + func set(textZoomLevel: TextZoomLevel, forDomain domain: String) { + textZoomLevels[domain] = textZoomLevel.rawValue + } + + func removeTextZoomLevel(forDomain domain: String) { + textZoomLevels.removeValue(forKey: domain) + } + + func resetTextZoomLevels(excludingDomains: [String]) { + let tld = TLD() + textZoomLevels = textZoomLevels.filter { level in + excludingDomains.contains(where: { + tld.eTLDplus1($0) == level.key + }) + } + } + +} diff --git a/DuckDuckGo/UserScripts.swift b/DuckDuckGo/UserScripts.swift index 144686acb9..eb69b13057 100644 --- a/DuckDuckGo/UserScripts.swift +++ b/DuckDuckGo/UserScripts.swift @@ -50,10 +50,10 @@ final class UserScripts: UserScriptsProvider { private(set) var findInPageScript = FindInPageUserScript() private(set) var fullScreenVideoScript = FullScreenVideoUserScript() private(set) var printingUserScript = PrintingUserScript() - private(set) var textSizeUserScript = TextSizeUserScript(textSizeAdjustmentInPercents: AppDependencyProvider.shared.appSettings.textSize) private(set) var debugScript = DebugUserScript() - init(with sourceProvider: ScriptSourceProviding) { + init(with sourceProvider: ScriptSourceProviding, appSettings: AppSettings = AppDependencyProvider.shared.appSettings) { + contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig) autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider) @@ -79,7 +79,6 @@ final class UserScripts: UserScriptsProvider { lazy var userScripts: [UserScript] = [ debugScript, - textSizeUserScript, autoconsentUserScript, findInPageScript, navigatorPatchScript, diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index c0d006206f..43e48a8762 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -326,8 +326,10 @@ public struct UserText { public static let voiceSearchCancelButton = NSLocalizedString("voiceSearch.cancel", value: "Cancel", comment: "Cancel button for voice search") public static let voiceSearchFooterOld = NSLocalizedString("voiceSearch.footer.note.old", value: "Audio is processed on-device. It's not stored or shared with anyone, including DuckDuckGo.", comment: "Voice-search footer note with on-device privacy warning") public static let voiceSearchFooter = NSLocalizedString("voiceSearch.footer.note", value: "Add Private Voice Search option to the address bar. Audio is not stored or shared with anyone, including DuckDuckGo.", comment: "Voice-search footer note with on-device privacy warning") - public static let textSizeDescription = NSLocalizedString("textSize.description", value: "Choose your preferred text size. Websites you view in DuckDuckGo will adjust to it.", comment: "Description text for the text size adjustment setting") - public static func textSizeFooter(for percentage: String) -> String { + + // Legacy name is text size - don't want to mess up translations by changing it. + public static let textZoomDescription = NSLocalizedString("textSize.description", value: "Increase or decrease text size across all sites.", comment: "Description text for the text size adjustment setting") + public static func textZoomFooter(for percentage: String) -> String { let message = NSLocalizedString("textSize.footer", value: "Text Size - %@", comment: "Replacement string is a current percent value e.g. '120%'") return message.format(arguments: percentage) } @@ -354,6 +356,18 @@ public struct UserText { } public static let messageAllFilesDeleted = NSLocalizedString("downloads.message.all-files-deleted", value: "All files deleted", comment: "Message confirming that all files on the downloads list have been deleted") + public static let textZoomMenuItem = NSLocalizedString("action.text-zoom-sheet-menu-item", value: "Zoom", comment: "Text zoom menu item") + + public static func textZoomWithPercentSheetTitle(_ percent: Int) -> String { + let message = NSLocalizedString("action.text-zoom-sheet-title", value: "Text Zoom (%d%%)", comment: "Title for text zoom sheet view. %d%% is replaced with percent, e.g. 56% so do not change that please.") + return message.format(arguments: percent) + } + + public static func textZoomWithPercentForMenuItem(_ percent: Int) -> String { + let message = NSLocalizedString("action.text-zoom-menu-item", value: "Zoom (%d%%)", comment: "Title for text zoom menu item. %d%% is replaced with percent, e.g. 56% so do not change that please.") + return message.format(arguments: percent) + } + public static let actionGenericShow = NSLocalizedString("action.generic.show", value: "Show", comment: "Button label for a generic show action") public static let actionDownloads = NSLocalizedString("action.title.downloads", value: "Downloads", comment: "Downloads menu item opening the downlods list") public static let downloadsScreenTitle = NSLocalizedString("downloads.downloads-list.title", value: "Downloads", comment: "Downloads list screen title") @@ -1051,7 +1065,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsTheme = NSLocalizedString("settings.theme", value: "Theme", comment: "Settings screen cell text for theme") public static let settingsIcon = NSLocalizedString("settings.icon", value: "App Icon", comment: "Settings screen cell text for app icon selection") public static let settingsFirebutton = NSLocalizedString("settings.firebutton", value: "Fire Button Animation", comment: "Settings screen cell text for fire button animation") - public static let settingsText = NSLocalizedString("settings.text.size", value: "Text Size", comment: "Settings screen cell text for text size") + public static let settingsText = NSLocalizedString("settings.text.size", value: "Default Text Zoom", comment: "Settings screen cell text for text size") public static let settingsAddressBar = NSLocalizedString("settings.address.bar", value: "Address Bar Position", comment: "Settings screen cell text for addess bar position") public static let settingsFullURL = NSLocalizedString("settings.address.full.url", value: "Show Full Site Address", comment: "Settings screen cell title for toggling full URL visibility in address bar") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 214e4328a4..fc7d856f4a 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -13,6 +13,15 @@ /* Button label for OK action */ "action.ok" = "OK"; +/* Title for text zoom menu item. %d%% is replaced with percent, e.g. 56% so do not change that please. */ +"action.text-zoom-menu-item" = "Zoom (%d%%)"; + +/* Text zoom menu item */ +"action.text-zoom-sheet-menu-item" = "Zoom"; + +/* Title for text zoom sheet view. %d%% is replaced with percent, e.g. 56% so do not change that please. */ +"action.text-zoom-sheet-title" = "Text Zoom (%d%%)"; + /* Add action - button shown in alert */ "action.title.add" = "Add"; @@ -2374,7 +2383,7 @@ But if you *do* want a peek under the hood, you can find more information about "settings.sync" = "Sync & Backup"; /* Settings screen cell text for text size */ -"settings.text.size" = "Text Size"; +"settings.text.size" = "Default Text Zoom"; /* Settings screen cell text for theme */ "settings.theme" = "Theme"; @@ -2728,7 +2737,7 @@ But if you *do* want a peek under the hood, you can find more information about "tab.switcher.accessibility.label" = "Tab Switcher"; /* Description text for the text size adjustment setting */ -"textSize.description" = "Choose your preferred text size. Websites you view in DuckDuckGo will adjust to it."; +"textSize.description" = "Increase or decrease text size across all sites."; /* Replacement string is a current percent value e.g. '120%' */ "textSize.footer" = "Text Size - %@"; diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index 585d936cd9..4d9b09321e 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -22,6 +22,7 @@ import Foundation @testable import DuckDuckGo class AppSettingsMock: AppSettings { + var defaultTextZoomLevel: DuckDuckGo.TextZoomLevel = .percent100 var recentlyVisitedSites: Bool = false diff --git a/DuckDuckGoTests/ContentBlockingUpdatingTests.swift b/DuckDuckGoTests/ContentBlockingUpdatingTests.swift index b57c10b1b3..dc68adcd9e 100644 --- a/DuckDuckGoTests/ContentBlockingUpdatingTests.swift +++ b/DuckDuckGoTests/ContentBlockingUpdatingTests.swift @@ -171,31 +171,6 @@ final class ContentBlockingUpdatingTests: XCTestCase { } } - func testWhenTextSizeChangeNotificationSentThenUserScriptsAreRebuild() { - let e1 = expectation(description: "should post initial update") - var e2: XCTestExpectation! - var ruleList: WKContentRuleList! - let c = updating.userContentBlockingAssets.sink { assets in - if ruleList == nil { - ruleList = assets.rules(withName: "test") - e1.fulfill() - } else { - // ruleList should not be recompiled - XCTAssertTrue(assets.rules(withName: "test") === ruleList) - XCTAssertTrue(assets.isValid) - e2.fulfill() - } - } - - rulesManager.updatesSubject.send(Self.testUpdate()) - withExtendedLifetime(c) { - waitForExpectations(timeout: 1, handler: nil) - e2 = expectation(description: "should rebuild user scripts") - NotificationCenter.default.post(name: AppUserDefaults.Notifications.textSizeChange, object: nil) - waitForExpectations(timeout: 1, handler: nil) - } - } - func testWhenDidVerifyInternalUserNotificationSentThenUserScriptsAreRebuild() { let e1 = expectation(description: "should post initial update") var e2: XCTestExpectation! diff --git a/DuckDuckGoTests/MockFeatureFlagger.swift b/DuckDuckGoTests/MockFeatureFlagger.swift index 9125dc769f..7a8f568486 100644 --- a/DuckDuckGoTests/MockFeatureFlagger.swift +++ b/DuckDuckGoTests/MockFeatureFlagger.swift @@ -26,6 +26,10 @@ final class MockFeatureFlagger: FeatureFlagger { var enabledFeatureFlags: [FeatureFlag] = [] + init(enabledFeatureFlags: [FeatureFlag] = []) { + self.enabledFeatureFlags = enabledFeatureFlags + } + func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { guard let flag = featureFlag as? FeatureFlag else { return false diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index b37a63a332..fefb80faa7 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -137,7 +137,8 @@ extension TabViewController { onboardingPixelReporter: contextualOnboardingPixelReporter, urlCredentialCreator: MockCredentialCreator(), featureFlagger: featureFlagger, - subscriptionCookieManager: SubscriptionCookieManagerMock() + subscriptionCookieManager: SubscriptionCookieManagerMock(), + textZoomCoordinator: MockTextZoomCoordinator() ) tab.attachWebView(configuration: .nonPersistent(), andLoadRequest: nil, consumeCookies: false, customWebView: customWebView) return tab diff --git a/DuckDuckGoTests/MockTextZoomCoordinator.swift b/DuckDuckGoTests/MockTextZoomCoordinator.swift new file mode 100644 index 0000000000..1fcaed95c3 --- /dev/null +++ b/DuckDuckGoTests/MockTextZoomCoordinator.swift @@ -0,0 +1,55 @@ +// +// MockTextZoomCoordinator.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo +import Core +import WebKit + +class MockTextZoomCoordinator: TextZoomCoordinating { + + let isEnabled: Bool = true + + func textZoomLevel(forHost host: String?) -> TextZoomLevel { + return .percent100 + } + + func set(textZoomLevel level: DuckDuckGo.TextZoomLevel, forHost host: String?) { + } + + func onWebViewCreated(applyToWebView webView: WKWebView) { + } + + func onNavigationCommitted(applyToWebView webView: WKWebView) { + } + + func onTextZoomChange(applyToWebView webView: WKWebView) { + } + + func showTextZoomEditor(inController controller: UIViewController, forWebView webView: WKWebView) { + } + + func makeBrowsingMenuEntry(forLink: Link, inController controller: UIViewController, forWebView webView: WKWebView) -> BrowsingMenuEntry? { + return nil + } + + func resetTextZoomLevels(excludingDomains: [String]) { + } + +} diff --git a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift index ff93a30198..99ad0e1b60 100644 --- a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift +++ b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift @@ -80,7 +80,9 @@ final class OnboardingDaxFavouritesTests: XCTestCase { tutorialSettings: tutorialSettingsMock, subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock.enabled, voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true), - subscriptionCookieManager: SubscriptionCookieManagerMock() + featureFlagger: MockFeatureFlagger(), + subscriptionCookieManager: SubscriptionCookieManagerMock(), + textZoomCoordinator: MockTextZoomCoordinator() ) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() diff --git a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift index 7ccd6d49b9..3f1762975b 100644 --- a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift +++ b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift @@ -78,7 +78,9 @@ final class OnboardingNavigationDelegateTests: XCTestCase { contextualOnboardingPixelReporter: onboardingPixelReporter, subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock.enabled, voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true), - subscriptionCookieManager: SubscriptionCookieManagerMock()) + featureFlagger: MockFeatureFlagger(), + subscriptionCookieManager: SubscriptionCookieManagerMock(), + textZoomCoordinator: MockTextZoomCoordinator()) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() window.makeKeyAndVisible() diff --git a/DuckDuckGoTests/TextZoomTests.swift b/DuckDuckGoTests/TextZoomTests.swift new file mode 100644 index 0000000000..ca4974ee10 --- /dev/null +++ b/DuckDuckGoTests/TextZoomTests.swift @@ -0,0 +1,192 @@ +// +// TextZoomTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo +import BrowserServicesKit +import Core +import XCTest +import WebKit + +final class TextZoomTests: XCTestCase { + + let viewScaleKey = "viewScale" + + func testZoomLevelAppliedToWebView() { + let storage = TextZoomStorage() + storage.textZoomLevels = [:] + + let coordinator: TextZoomCoordinating = makeTextZoomCoordinator(storage: storage) + let webView = URLFixedWebView(frame: .zero, configuration: .nonPersistent()) + + webView.setValue(0.1, forKey: viewScaleKey) + coordinator.onNavigationCommitted(applyToWebView: webView) + XCTAssertEqual(1.0, webView.value(forKey: viewScaleKey) as? Double) + + webView.setValue(0.1, forKey: viewScaleKey) + coordinator.onTextZoomChange(applyToWebView: webView) + XCTAssertEqual(1.0, webView.value(forKey: viewScaleKey) as? Double) + + webView.setValue(0.1, forKey: viewScaleKey) + coordinator.onWebViewCreated(applyToWebView: webView) + XCTAssertEqual(1.0, webView.value(forKey: viewScaleKey) as? Double) + + let host = "example.com" + webView.fixed = URL(string: "https://\(host)") + coordinator.set(textZoomLevel: .percent120, forHost: host) + + webView.setValue(0.1, forKey: viewScaleKey) + coordinator.onNavigationCommitted(applyToWebView: webView) + XCTAssertEqual(1.2, webView.value(forKey: viewScaleKey) as? Double) + + webView.setValue(0.1, forKey: viewScaleKey) + coordinator.onTextZoomChange(applyToWebView: webView) + XCTAssertEqual(1.2, webView.value(forKey: viewScaleKey) as? Double) + + webView.setValue(0.1, forKey: viewScaleKey) + coordinator.onWebViewCreated(applyToWebView: webView) + XCTAssertEqual(1.2, webView.value(forKey: viewScaleKey) as? Double) + + // When reset to the default then "forget" + coordinator.set(textZoomLevel: .percent100, forHost: host) + XCTAssertEqual(storage.textZoomLevels, [:]) + } + + func testMenuItemCreation() { + let host = "example.com" + + let storage = TextZoomStorage() + storage.textZoomLevels = [:] + + let coordinator: TextZoomCoordinating = makeTextZoomCoordinator(storage: storage) + coordinator.set(textZoomLevel: .percent120, forHost: host) + + let controller = UIViewController() + let webView = WKWebView(frame: .zero, configuration: .nonPersistent()) + + let item1 = coordinator.makeBrowsingMenuEntry( + forLink: makeLink(url: URL(string: "https://other.org")!), + inController: controller, + forWebView: webView) + + // Expecting the 'default' value + if case .regular(let name, _, _, _, _) = item1 { + XCTAssertEqual(UserText.textZoomMenuItem, name) + } else { + XCTFail("Unexpected menu item type") + } + + let item2 = coordinator.makeBrowsingMenuEntry( + forLink: makeLink(url: URL(string: "https://\(host)")!), + inController: controller, + forWebView: webView) + + // Expecting the menu item to include the percent + if case .regular(let name, _, _, _, _) = item2 { + XCTAssertEqual(UserText.textZoomWithPercentForMenuItem(120), name) + } else { + XCTFail("Unexpected menu item type") + } + + } + + func testSettingAndResetingDomainTextZoomLevels() { + let host1 = "example.com" + let host2 = "another.org" + + let storage = TextZoomStorage() + storage.textZoomLevels = [:] + + let coordinator: TextZoomCoordinating = makeTextZoomCoordinator(storage: storage) + coordinator.set(textZoomLevel: .percent120, forHost: host1) + XCTAssertEqual(coordinator.textZoomLevel(forHost: host1), .percent120) + + coordinator.set(textZoomLevel: .percent140, forHost: host2) + XCTAssertEqual(coordinator.textZoomLevel(forHost: host2), .percent140) + + coordinator.resetTextZoomLevels(excludingDomains: [host1]) + XCTAssertEqual(coordinator.textZoomLevel(forHost: host1), .percent120) + XCTAssertEqual(coordinator.textZoomLevel(forHost: host2), AppSettingsMock().defaultTextZoomLevel) + } + + func testWhenFeatureFlagEnabled_ThenCoordinatorIsEnabled() { + let controller = UIViewController() + let webView = WKWebView(frame: .zero, configuration: .nonPersistent()) + webView.setValue(0.1, forKey: viewScaleKey) + XCTAssertEqual(0.1, webView.value(forKey: viewScaleKey) as? Double) + + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.textZoom] + let coordinator: TextZoomCoordinating = makeTextZoomCoordinator(featureFlagger: featureFlagger) + XCTAssertTrue(coordinator.isEnabled) + + featureFlagger.enabledFeatureFlags = [] + XCTAssertFalse(coordinator.isEnabled) + + coordinator.onNavigationCommitted(applyToWebView: webView) + coordinator.onTextZoomChange(applyToWebView: webView) + coordinator.onWebViewCreated(applyToWebView: webView) + XCTAssertNil(coordinator.makeBrowsingMenuEntry(forLink: makeLink(), inController: controller, forWebView: webView)) + + XCTAssertEqual(0.1, webView.value(forKey: viewScaleKey) as? Double) + } + + private func makeTextZoomCoordinator( + appSettings: AppSettings = AppSettingsMock(), + storage: TextZoomStoring = MockTextZoomStorage(), + featureFlagger: FeatureFlagger = MockFeatureFlagger(enabledFeatureFlags: [.textZoom]) + ) -> TextZoomCoordinating { + return TextZoomCoordinator(appSettings: appSettings, + storage: storage, + featureFlagger: featureFlagger) + } + + private func makeLink(title: String? = "title", url: URL = .ddg, localPath: URL? = nil) -> Link { + return Link(title: title, url: url, localPath: localPath) + } + +} + +/// Nothing else should be using storage directly so just keeping it here out of the way. +private class MockTextZoomStorage: TextZoomStoring { + + func textZoomLevelForDomain(_ domain: String) -> DuckDuckGo.TextZoomLevel? { + return nil + } + + func set(textZoomLevel: DuckDuckGo.TextZoomLevel, forDomain domain: String) { + } + + func removeTextZoomLevel(forDomain domain: String) { + } + + func resetTextZoomLevels(excludingDomains: [String]) { + } + +} + +private class URLFixedWebView: WKWebView { + + var fixed: URL? + + override var url: URL? { + fixed + } + +}