From 1671ba0b00fadc68cd440a0fe777634d2e1395ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 30 Jul 2024 18:04:24 +0200 Subject: [PATCH] New Tab Page Settings (#3140) Task/Issue URL: https://app.asana.com/0/1206226850447395/1207896254384739/f Tech Design URL: CC: **Description**: See https://github.com/duckduckgo/iOS/pull/3159 first. New Tab Page Settings View, allowing to rearrange and toggle visibility of sections and shortcuts. Implementation caveats: Using Grid with drag/drop functionality inside a List allowing native UI for rearranging items is breaking drag/drop in Grid. It was necessary to include way to measure List content height and adjust it's size in order to place Grid below, outside the List content. --- DuckDuckGo.xcodeproj/project.pbxproj | 34 +++- .../16px/Check-16-alt.imageset/Check-16.svg | 3 + .../16px/Check-16-alt.imageset/Contents.json | 16 ++ .../24px/Shortcut-24.imageset/Contents.json | 15 ++ .../24px/Shortcut-24.imageset/Shortcut-24.svg | 3 + DuckDuckGo/EditableShortcutsView.swift | 75 ++++++++ DuckDuckGo/MainViewController.swift | 11 ++ DuckDuckGo/NewTabPageDaxLogoView.swift | 33 ++++ .../NewTabPageSectionsSettingsModel.swift | 4 +- DuckDuckGo/NewTabPageSettingsModel.swift | 74 ++++++++ .../NewTabPageSettingsSectionItemView.swift | 47 +++++ DuckDuckGo/NewTabPageSettingsView.swift | 165 ++++++++++++++++++ .../NewTabPageShortcutsSettingsModel.swift | 4 +- DuckDuckGo/NewTabPageView.swift | 148 +++++++++++----- DuckDuckGo/NewTabPageViewController.swift | 16 +- DuckDuckGo/ReorderableForEach.swift | 164 +++++++++++++++++ DuckDuckGo/ShortcutAccessoryView.swift | 22 ++- DuckDuckGo/ShortcutItemView.swift | 62 ++++--- DuckDuckGo/ShortcutsModel.swift | 17 -- DuckDuckGo/ShortcutsView.swift | 8 +- DuckDuckGo/UserText.swift | 7 + DuckDuckGo/ViewExtension.swift | 37 ++++ DuckDuckGo/en.lproj/Localizable.strings | 12 ++ 23 files changed, 859 insertions(+), 118 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Check-16.svg create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Shortcut-24.svg create mode 100644 DuckDuckGo/EditableShortcutsView.swift create mode 100644 DuckDuckGo/NewTabPageDaxLogoView.swift create mode 100644 DuckDuckGo/NewTabPageSettingsModel.swift create mode 100644 DuckDuckGo/NewTabPageSettingsSectionItemView.swift create mode 100644 DuckDuckGo/NewTabPageSettingsView.swift create mode 100644 DuckDuckGo/ReorderableForEach.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ff88f67afa..6e74ea87f0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -259,7 +259,11 @@ 6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB062C32F173004179A8 /* PixelFiring.swift */; }; 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */; }; 6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */; }; - 6F3537A22C4AB97A009F8717 /* NewTabPagePreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A12C4AB97A009F8717 /* NewTabPagePreferencesModel.swift */; }; + 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */; }; + 6F35379E2C4AAF2E009F8717 /* NewTabPageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379D2C4AAF2E009F8717 /* NewTabPageSettingsView.swift */; }; + 6F3537A02C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */; }; + 6F3537A22C4AB97A009F8717 /* NewTabPageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */; }; + 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */; }; 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */; }; @@ -276,6 +280,7 @@ 6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; }; 6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */; }; 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */; }; + 6F9FFE2A2C57ADB100A238BE /* EditableShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */; }; 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE2C2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift */; }; 6F9FFE302C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE2F2C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift */; }; 6FA3438F2C3D3BC300470677 /* Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA3438E2C3D3BC300470677 /* Favorite.swift */; }; @@ -1421,7 +1426,11 @@ 6F03CB062C32F173004179A8 /* PixelFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFiring.swift; sourceTree = ""; }; 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFiringAsync.swift; sourceTree = ""; }; 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsStorage.swift; sourceTree = ""; }; - 6F3537A12C4AB97A009F8717 /* NewTabPagePreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPagePreferencesModel.swift; sourceTree = ""; }; + 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; + 6F35379D2C4AAF2E009F8717 /* NewTabPageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsView.swift; sourceTree = ""; }; + 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsSectionItemView.swift; sourceTree = ""; }; + 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsModel.swift; sourceTree = ""; }; + 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageDaxLogoView.swift; sourceTree = ""; }; 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorage.swift; sourceTree = ""; }; @@ -1438,6 +1447,7 @@ 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = ""; }; 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsStorage.swift; sourceTree = ""; }; 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsStorage.swift; sourceTree = ""; }; + 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableShortcutsView.swift; sourceTree = ""; }; 6F9FFE2C2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsModel.swift; sourceTree = ""; }; 6F9FFE2F2C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsModel.swift; sourceTree = ""; }; 6FA3438E2C3D3BC300470677 /* Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favorite.swift; sourceTree = ""; }; @@ -3515,13 +3525,15 @@ name = NewTabPage; sourceTree = ""; }; - 6F35379C2C4AAF1C009F8717 /* Preferences */ = { + 6F35379C2C4AAF1C009F8717 /* Settings */ = { isa = PBXGroup; children = ( 6F9FFE2E2C57B14100A238BE /* Model */, 6F9FFE2B2C57AE4200A238BE /* Storage */, + 6F35379D2C4AAF2E009F8717 /* NewTabPageSettingsView.swift */, + 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */, ); - name = Preferences; + name = Settings; sourceTree = ""; }; 6F691CC82C4979DD002E9553 /* Tooltip */ = { @@ -3548,7 +3560,7 @@ children = ( 6F9FFE2C2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift */, 6F9FFE2F2C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift */, - 6F3537A12C4AB97A009F8717 /* NewTabPagePreferencesModel.swift */, + 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */, ); name = Model; sourceTree = ""; @@ -3619,12 +3631,14 @@ 6FE1273B2C204C0D00EB5724 /* Subviews */ = { isa = PBXGroup; children = ( - 6F35379C2C4AAF1C009F8717 /* Preferences */, + 6F35379C2C4AAF1C009F8717 /* Settings */, 6FE127472C20941A00EB5724 /* Shortcuts */, 6FE127412C204DE900EB5724 /* Favorites */, 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */, 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */, 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */, + 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */, + 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */, ); name = Subviews; sourceTree = ""; @@ -3645,6 +3659,7 @@ isa = PBXGroup; children = ( 6FE1273F2C204D9B00EB5724 /* ShortcutsView.swift */, + 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */, 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */, 6F64AA5C2C4920D200CF4489 /* ShortcutAccessoryView.swift */, 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */, @@ -6787,6 +6802,7 @@ 31DE43C42C2C60E800F8C51F /* DuckPlayerModalPresenter.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */, + 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */, F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, @@ -6876,6 +6892,7 @@ 8C4838B5221C8F7F008A6739 /* GestureToolbarButton.swift in Sources */, EE276BEA2A77F823009167B6 /* NetworkProtectionRootViewController.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, + 6F9FFE2A2C57ADB100A238BE /* EditableShortcutsView.swift in Sources */, 1E908BF329827C480008C8F3 /* AutoconsentManagement.swift in Sources */, D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */, C185ED612BD4329700BAE9DC /* ImportPasswordsStatusHandler.swift in Sources */, @@ -6943,7 +6960,7 @@ 83BE9BC3215D69C1009844D9 /* AppConfigurationFetch.swift in Sources */, 37CF91622BB474AA00BADCAE /* CrashCollectionOnboardingView.swift in Sources */, 1EEC460627A9499600E75FCB /* DownloadsList.swift in Sources */, - 6F3537A22C4AB97A009F8717 /* NewTabPagePreferencesModel.swift in Sources */, + 6F3537A22C4AB97A009F8717 /* NewTabPageSettingsModel.swift in Sources */, 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */, C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, @@ -7098,6 +7115,7 @@ 311BD1AD2836BB3900AEF6C1 /* AutofillItemsEmptyView.swift in Sources */, C1F341C52A6924000032057B /* EmailAddressPromptView.swift in Sources */, 316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */, + 6F35379E2C4AAF2E009F8717 /* NewTabPageSettingsView.swift in Sources */, 31C70B5B2804C61000FB6AD1 /* SaveAutofillLoginManager.swift in Sources */, 982123502B6D233E00F08C57 /* UserSession.swift in Sources */, 85449EFD23FDA71F00512AAF /* KeyboardSettings.swift in Sources */, @@ -7195,6 +7213,7 @@ 8505836A219F424500ED4EDB /* UIAlertControllerExtension.swift in Sources */, C12726F22A5FF8CB00215B02 /* EmailSignupPromptViewController.swift in Sources */, 983EABB8236198F6003948D1 /* DatabaseMigration.swift in Sources */, + 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */, 314C92B827C3DD660042EC96 /* QuickLookPreviewView.swift in Sources */, 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */, F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */, @@ -7273,6 +7292,7 @@ CBD4F13F279EBFAF00B20FD7 /* HomeMessageViewModel.swift in Sources */, 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */, 1DEAADFB2BA71E9A00E25A97 /* SettingsPrivacyProtectionDescriptionView.swift in Sources */, + 6F3537A02C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift in Sources */, 2DC3FC65C6D9DA634426672D /* AutofillNoAuthAvailableView.swift in Sources */, 6F03CAFC2C32C6F6004179A8 /* NewTabPageMessagesModel.swift in Sources */, ); diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Check-16.svg b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Check-16.svg new file mode 100644 index 0000000000..db9ba12868 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Check-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Contents.json new file mode 100644 index 0000000000..67060d7400 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/16px/Check-16-alt.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Check-16.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json new file mode 100644 index 0000000000..74079951c3 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Shortcut-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Shortcut-24.svg b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Shortcut-24.svg new file mode 100644 index 0000000000..350fb215fc --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Shortcut-24.svg @@ -0,0 +1,3 @@ + + + diff --git a/DuckDuckGo/EditableShortcutsView.swift b/DuckDuckGo/EditableShortcutsView.swift new file mode 100644 index 0000000000..f907a06d5b --- /dev/null +++ b/DuckDuckGo/EditableShortcutsView.swift @@ -0,0 +1,75 @@ +// +// EditableShortcutsView.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 UniformTypeIdentifiers + +struct EditableShortcutsView: View { + + @ObservedObject private(set) var model: NewTabPageShortcutsSettingsModel + + var body: some View { + NewTabPageGridView { _ in + ReorderableForEach(model.itemsSettings, id: \.item.id, isReorderingEnabled: true) { setting in + let isEnabled = model.enabledItems.contains(setting.item) + Button { + setting.isEnabled.wrappedValue.toggle() + } label: { + ShortcutItemView(shortcut: setting.item, accessoryType: isEnabled ? .selected : .add) + } + } preview: { setting in + ShortcutIconView(shortcut: setting.item).previewShape() + } onMove: { indices, newOffset in + withAnimation { + model.moveItems(from: indices, to: newOffset) + } + } + } + } +} + +private extension View { + func previewShape() -> some View { + contentShape(.dragPreview, RoundedRectangle(cornerRadius: 8)) + } +} + +extension NewTabPageSettingsModel.NTPSetting: Reorderable, Hashable, Equatable { + + var dropItemProvider: NSItemProvider { + NSItemProvider(object: item.id as NSString) + } + + var dropType: UTType { .text } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.item == rhs.item + } + + func hash(into hasher: inout Hasher) { + hasher.combine(item.hashValue) + } +} + +#Preview { + ScrollView { + EditableShortcutsView(model: NewTabPageShortcutsSettingsModel()) + } + .background(Color(designSystemColor: .background)) +} diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 417f274b23..225ceeb6c8 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -530,6 +530,16 @@ class MainViewController: UIViewController { } } + adjustNewTabPageSafeAreaInsets(for: position) + } + + private func adjustNewTabPageSafeAreaInsets(for addressBarPosition: AddressBarPosition) { + switch addressBarPosition { + case .top: + newTabPageViewController?.additionalSafeAreaInsets = .zero + case .bottom: + newTabPageViewController?.additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: 52, right: 0) + } } @objc func onShowFullSiteAddressChanged() { @@ -742,6 +752,7 @@ class MainViewController: UIViewController { newTabPageViewController = controller addToContentContainer(controller: controller) viewCoordinator.logoContainer.isHidden = true + adjustNewTabPageSafeAreaInsets(for: appSettings.currentAddressBarPosition) } else { let controller = HomeViewController.loadFromStoryboard(homePageConfiguration: homePageConfiguration, model: tabModel, diff --git a/DuckDuckGo/NewTabPageDaxLogoView.swift b/DuckDuckGo/NewTabPageDaxLogoView.swift new file mode 100644 index 0000000000..42aa6ee291 --- /dev/null +++ b/DuckDuckGo/NewTabPageDaxLogoView.swift @@ -0,0 +1,33 @@ +// +// NewTabPageDaxLogoView.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 NewTabPageDaxLogoView: View { + var body: some View { + VStack { + Image(.home) + Image(.textDuckDuckGo) + } + } +} + +#Preview { + NewTabPageDaxLogoView() +} diff --git a/DuckDuckGo/NewTabPageSectionsSettingsModel.swift b/DuckDuckGo/NewTabPageSectionsSettingsModel.swift index 680c8afb05..37f27339ef 100644 --- a/DuckDuckGo/NewTabPageSectionsSettingsModel.swift +++ b/DuckDuckGo/NewTabPageSectionsSettingsModel.swift @@ -19,10 +19,10 @@ import Foundation -typealias NewTabPageSectionsSettingsModel = NewTabPagePreferencesModel +typealias NewTabPageSectionsSettingsModel = NewTabPageSettingsModel extension NewTabPageSectionsSettingsModel { convenience init(storage: NewTabPageSectionsSettingsStorage = NewTabPageSectionsSettingsStorage()) { - self.init(preferencesStorage: storage) + self.init(settingsStorage: storage) } } diff --git a/DuckDuckGo/NewTabPageSettingsModel.swift b/DuckDuckGo/NewTabPageSettingsModel.swift new file mode 100644 index 0000000000..4e359bc883 --- /dev/null +++ b/DuckDuckGo/NewTabPageSettingsModel.swift @@ -0,0 +1,74 @@ +// +// NewTabPageSettingsModel.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 SwiftUI + +final class NewTabPageSettingsModel: ObservableObject where Storage.SettingItem == SettingItem { + + /// Settings page settings collection with bindings + @Published private(set) var itemsSettings: [NTPSetting] = [] + + /// Enabled items, ordered. + @Published private(set) var enabledItems: [SettingItem] = [] + + private let settingsStorage: Storage + + init(settingsStorage: Storage) { + self.settingsStorage = settingsStorage + + updatePublishedValues() + } + + func moveItems(from: IndexSet, to: Int) { + settingsStorage.moveItems(from, toOffset: to) + updatePublishedValues() + } + + func save() { + settingsStorage.save() + } + + private func updatePublishedValues() { + populateSettings() + populateEnabledItems() + } + + private func populateEnabledItems() { + enabledItems = settingsStorage.enabledItems + } + + private func populateSettings() { + itemsSettings = settingsStorage.itemsOrder.map { item in + NTPSetting(item: item, isEnabled: Binding(get: { + self.settingsStorage.isEnabled(item) + }, set: { newValue in + self.settingsStorage.setItem(item, enabled: newValue) + self.updatePublishedValues() + })) + } + } +} + +extension NewTabPageSettingsModel { + struct NTPSetting { + let item: Item + let isEnabled: Binding + } +} diff --git a/DuckDuckGo/NewTabPageSettingsSectionItemView.swift b/DuckDuckGo/NewTabPageSettingsSectionItemView.swift new file mode 100644 index 0000000000..4f0bce3515 --- /dev/null +++ b/DuckDuckGo/NewTabPageSettingsSectionItemView.swift @@ -0,0 +1,47 @@ +// +// NewTabPageSettingsSectionItemView.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 NewTabPageSettingsSectionItemView: View { + + let title: String + let iconResource: ImageResource + @Binding var isEnabled: Bool + + var body: some View { + HStack(spacing: 8) { + Image(iconResource) + .foregroundColor(Color(designSystemColor: .icons)) + + Toggle(isOn: $isEnabled, label: { + Text(title) + .foregroundStyle(Color(designSystemColor: .textPrimary)) + .daxBodyRegular() + }) + + Divider() + } + } +} + +#Preview { + @State var isEnabled: Bool = false + return NewTabPageSettingsSectionItemView(title: "Foo", iconResource: .favorite24, isEnabled: $isEnabled).fixedSize(horizontal: false, vertical: true) +} diff --git a/DuckDuckGo/NewTabPageSettingsView.swift b/DuckDuckGo/NewTabPageSettingsView.swift new file mode 100644 index 0000000000..ea7ab8538d --- /dev/null +++ b/DuckDuckGo/NewTabPageSettingsView.swift @@ -0,0 +1,165 @@ +// +// NewTabPageSettingsView.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 Common + +struct NewTabPageSettingsView: View { + @Environment(\.dismiss) var dismiss + + @ObservedObject var shortcutsSettingsModel: NewTabPageShortcutsSettingsModel + @ObservedObject var sectionsSettingsModel: NewTabPageSectionsSettingsModel + + // Arbitrary high value is required to acomodate for the content size + @State var listHeight: CGFloat = 5000 + + @State var firstSectionFrame: CGRect = .zero + @State var lastSectionFrame: CGRect = .zero + + var body: some View { + mainView + .applyBackground() + .tintIfAvailable(Color(designSystemColor: .accent)) + .navigationTitle(UserText.newTabPageSettingsTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(UserText.navigationTitleDone) { + dismiss() + } + .tint(Color(designSystemColor: .textPrimary)) + } + } + } + + // MARK: Views + + @ViewBuilder + private var mainView: some View { + if sectionsSettingsModel.enabledItems.contains(.shortcuts) { + ScrollView { + VStack { + sectionsList(withFrameUpdates: true) + .withoutScroll() + .frame(height: listHeight) + + EditableShortcutsView(model: shortcutsSettingsModel) + .padding(.horizontal, Metrics.horizontalPadding) + } + } + .coordinateSpace(name: Constant.scrollCoordinateSpace) + } else { + sectionsList(withFrameUpdates: false) + } + } + + @ViewBuilder + private func sectionsList(withFrameUpdates: Bool) -> some View { + List { + Section { + sectionsSettingsContentView + } header: { + Text(UserText.newTabPageSettingsSectionsHeaderTitle) + .if(withFrameUpdates) { + $0.onFrameUpdate(in: Constant.scrollCoordinateSpace, using: FirstSectionFrameKey.self) { frame in + self.firstSectionFrame = frame + updateListHeight() + } + } + } footer: { + Text(UserText.newTabPageSettingsSectionsDescription) + } + + if sectionsSettingsModel.enabledItems.contains(.shortcuts) { + Section { + } header: { + Text(UserText.newTabPageSettingsShortcutsHeaderTitle) + .if(withFrameUpdates) { + $0.onFrameUpdate(in: Constant.scrollCoordinateSpace, using: LastSectionFrameKey.self) { frame in + self.lastSectionFrame = frame + updateListHeight() + } + } + } + } + } + .applyInsetGroupedListStyle() + .environment(\.editMode, .constant(.active)) + } + + @ViewBuilder + private var sectionsSettingsContentView: some View { + ForEach(sectionsSettingsModel.itemsSettings, id: \.item) { setting in + switch setting.item { + case .favorites: + NewTabPageSettingsSectionItemView(title: "Favorites", + iconResource: .favorite24, + isEnabled: setting.isEnabled) + case .shortcuts: + NewTabPageSettingsSectionItemView(title: "Shortcuts", + iconResource: .shortcut24, + isEnabled: setting.isEnabled) + } + }.onMove(perform: { indices, newOffset in + sectionsSettingsModel.moveItems(from: indices, to: newOffset) + }) + } + + // MARK: - + + private func updateListHeight() { + guard firstSectionFrame != .zero, lastSectionFrame != .zero else { return } + + let newHeight = lastSectionFrame.maxY - firstSectionFrame.minY + Metrics.defaultListTopPadding + self.listHeight = max(0, newHeight) + } +} + +private struct Constant { + static let scrollCoordinateSpaceName = "Scroll" + static let scrollCoordinateSpace = CoordinateSpace.named(scrollCoordinateSpaceName) +} + +private struct Metrics { + static let defaultListTopPadding = 24.0 + static let horizontalPadding = 16.0 +} + +#Preview { + NavigationView { + NewTabPageSettingsView( + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), + sectionsSettingsModel: NewTabPageSectionsSettingsModel() + ) + } +} + +private struct FirstSectionFrameKey: PreferenceKey { + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } + static var defaultValue: CGRect = .zero +} + +private struct LastSectionFrameKey: PreferenceKey { + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } + static var defaultValue: CGRect = .zero +} diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift b/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift index 1ad779fa68..fd960f9009 100644 --- a/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift +++ b/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift @@ -19,10 +19,10 @@ import Foundation -typealias NewTabPageShortcutsSettingsModel = NewTabPagePreferencesModel +typealias NewTabPageShortcutsSettingsModel = NewTabPageSettingsModel extension NewTabPageShortcutsSettingsModel { convenience init(storage: NewTabPageShortcutsSettingsStorage = NewTabPageShortcutsSettingsStorage()) { - self.init(preferencesStorage: storage) + self.init(settingsStorage: storage) } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index 3285ce22a7..0b77289d7f 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -27,71 +27,129 @@ struct NewTabPageView: View { @ObservedObject private var messagesModel: NewTabPageMessagesModel @ObservedObject private var favoritesModel: FavoritesModelType @ObservedObject private var shortcutsModel: ShortcutsModel - + @ObservedObject private var shortcutsSettingsModel: NewTabPageShortcutsSettingsModel + @ObservedObject private var sectionsSettingsModel: NewTabPageSectionsSettingsModel + @State var isShowingTooltip: Bool = false + @State private var isShowingSettings: Bool = false - init(messagesModel: NewTabPageMessagesModel, favoritesModel: FavoritesModelType, shortcutsModel: ShortcutsModel) { + init(messagesModel: NewTabPageMessagesModel, + favoritesModel: FavoritesModelType, + shortcutsModel: ShortcutsModel, + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel, + sectionsSettingsModel: NewTabPageSectionsSettingsModel) { self.messagesModel = messagesModel self.favoritesModel = favoritesModel self.shortcutsModel = shortcutsModel + self.shortcutsSettingsModel = shortcutsSettingsModel + self.sectionsSettingsModel = sectionsSettingsModel self.messagesModel.load() } + private var messagesSectionView: some View { + ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in + HomeMessageView(viewModel: messageModel) + .frame(maxWidth: horizontalSizeClass == .regular ? Constant.messageMaximumWidthPad : Constant.messageMaximumWidth) + .padding(16) + } + } + + private var favoritesSectionView: some View { + Group { + if favoritesModel.isEmpty { + FavoritesEmptyStateView(isShowingTooltip: $isShowingTooltip) + } else { + FavoritesView(model: favoritesModel) + } + } + .sectionPadding() + } + + @ViewBuilder + private var shortcutsSectionView: some View { + if isShortcutsSectionVisible { + ShortcutsView(model: shortcutsModel, shortcuts: shortcutsSettingsModel.enabledItems) + .sectionPadding() + } + } + + private var customizeButtonView: some View { + HStack { + Spacer() + + Button(action: { + isShowingSettings = true + }, label: { + NewTabPageCustomizeButtonView() + // Needed to reduce default button margins + .padding(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) + }).buttonStyle(SecondaryFillButtonStyle(compact: true, fullWidth: false)) + .padding(.top, 40) + }.sectionPadding() + } + + private var isAnySectionEnabled: Bool { + !sectionsSettingsModel.enabledItems.isEmpty + } + + private var isShortcutsSectionVisible: Bool { + !shortcutsSettingsModel.enabledItems.isEmpty + } + var body: some View { GeometryReader { proxy in ScrollView { VStack { - // MARK: Messages - ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in - HomeMessageView(viewModel: messageModel) - .frame(maxWidth: horizontalSizeClass == .regular ? Constant.messageMaximumWidthPad : Constant.messageMaximumWidth) - .padding(16) - } - - // MARK: Favorites - if favoritesModel.isEmpty { - FavoritesEmptyStateView(isShowingTooltip: $isShowingTooltip) - .padding(Constant.sectionPadding) + messagesSectionView + + if isAnySectionEnabled { + ForEach(sectionsSettingsModel.enabledItems, id: \.rawValue) { section in + switch section { + case .favorites: + favoritesSectionView + case .shortcuts: + shortcutsSectionView + } + } } else { - FavoritesView(model: favoritesModel) - .padding(Constant.sectionPadding) - } - - // MARK: Shortcuts - if !shortcutsModel.enabledShortcuts.isEmpty { - ShortcutsView(model: shortcutsModel) - .padding(Constant.sectionPadding) + // MARK: Dax Logo + Spacer() + NewTabPageDaxLogoView() } Spacer() // MARK: Customize button - HStack { - Spacer() - - Button(action: { - }, label: { - NewTabPageCustomizeButtonView() - // Needed to reduce default button margins - .padding(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) - }).buttonStyle(SecondaryFillButtonStyle(compact: true, fullWidth: false)) - .padding(Constant.sectionPadding) - .padding(.top, 40) - } + customizeButtonView } .frame(minHeight: proxy.frame(in: .local).size.height) } - .background(Color(designSystemColor: .background)) - .if(isShowingTooltip, transform: { - $0.highPriorityGesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { _ in - isShowingTooltip = false - }) + } + .background(Color(designSystemColor: .background)) + .if(isShowingTooltip) { + $0.highPriorityGesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { _ in + isShowingTooltip = false }) } + .sheet(isPresented: $isShowingSettings, onDismiss: { + shortcutsSettingsModel.save() + sectionsSettingsModel.save() + }, content: { + NavigationView { + NewTabPageSettingsView(shortcutsSettingsModel: shortcutsSettingsModel, + sectionsSettingsModel: sectionsSettingsModel) + } + }) } } +private extension View { + func sectionPadding() -> some View { + self.padding(Constant.sectionPadding) + } + } + private struct Constant { static let sectionPadding = EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) @@ -109,7 +167,9 @@ private struct Constant { ) ), favoritesModel: FavoritesPreviewModel(), - shortcutsModel: ShortcutsModel() + shortcutsModel: ShortcutsModel(), + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), + sectionsSettingsModel: NewTabPageSectionsSettingsModel() ) } @@ -131,7 +191,9 @@ private struct Constant { ) ), favoritesModel: FavoritesPreviewModel(), - shortcutsModel: ShortcutsModel() + shortcutsModel: ShortcutsModel(), + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), + sectionsSettingsModel: NewTabPageSectionsSettingsModel() ) } @@ -154,9 +216,3 @@ private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration homeMessages = homeMessages.dropLast() } } - -private extension ShortcutsModel { - convenience init() { - self.init(shortcutsPreferencesStorage: InMemoryShortcutsPreferencesStorage()) - } -} diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index 4a97e673a2..81bd1d0164 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -29,8 +29,11 @@ final class NewTabPageViewController: UIHostingController: View { + + private let data: [Data] + private let isReorderingEnabled: Bool + private let id: KeyPath + + private let content: (Data) -> Content + private let preview: ((Data) -> Preview)? + private let onMove: (_ from: IndexSet, _ to: Int) -> Void + + @State private var movedItem: Data? + @State private var isItemLocationChanged: Bool = false + + init(_ data: [Data], + id: KeyPath, + isReorderingEnabled: Bool = true, + @ViewBuilder content: @escaping (Data) -> Content, + onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { + self.data = data + self.id = id + self.isReorderingEnabled = isReorderingEnabled + self.content = content + self.preview = nil + self.onMove = onMove + } + + init(_ data: [Data], + id: KeyPath, + isReorderingEnabled: Bool = true, + @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder preview: @escaping (Data) -> Preview, + onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { + self.data = data + self.id = id + self.isReorderingEnabled = isReorderingEnabled + self.content = content + self.preview = preview + self.onMove = onMove + } + + var body: some View { + ForEach(data, id: id) { item in + if isReorderingEnabled { + if let preview { + droppableContent(for: item) + .onDrag { + movedItem = item + return item.dropItemProvider + } preview: { + preview(item) + } + } else { + droppableContent(for: item) + .onDrag { + movedItem = item + return item.dropItemProvider + } + } + } else { + content(item) + } + } + } + + private func droppableContent(for item: Data) -> some View { + content(item) + .onDrop(of: [item.dropType], delegate: ReorderDropDelegate( + data: data, + item: item, + onMove: onMove, + movedItem: $movedItem, + isItemLocationChanged: $isItemLocationChanged)) + } +} + +private struct ReorderDropDelegate: DropDelegate { + + let data: [Data] + let item: Data + let onMove: (_ from: IndexSet, _ to: Int) -> Void + + @Binding var movedItem: Data? + @Binding var isItemLocationChanged: Bool + + func dropEntered(info: DropInfo) { + guard item != movedItem, + let current = movedItem, + let from = data.firstIndex(of: current), + let to = data.firstIndex(of: item) + else { return } + + isItemLocationChanged = true + + if data[to] != current { + let fromIndices = IndexSet(integer: from) + let toIndex = to > from ? to + 1 : to + onMove(fromIndices, toIndex) + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + isItemLocationChanged = false + movedItem = nil + return true + } +} + +extension ReorderableForEach where Data: Identifiable, ID == Data.ID { + init(_ data: [Data], + isReorderingEnabled: Bool = true, + @ViewBuilder content: @escaping (Data) -> Content, + onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { + self.data = data + self.id = \Data.id + self.isReorderingEnabled = isReorderingEnabled + self.content = content + self.preview = nil + self.onMove = onMove + } + + init(_ data: [Data], + isReorderingEnabled: Bool = true, + @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder preview: @escaping (Data) -> Preview, + onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { + self.data = data + self.id = \Data.id + self.isReorderingEnabled = isReorderingEnabled + self.content = content + self.preview = preview + self.onMove = onMove + } +} diff --git a/DuckDuckGo/ShortcutAccessoryView.swift b/DuckDuckGo/ShortcutAccessoryView.swift index 2de2cfc368..873b95274d 100644 --- a/DuckDuckGo/ShortcutAccessoryView.swift +++ b/DuckDuckGo/ShortcutAccessoryView.swift @@ -24,16 +24,14 @@ struct ShortcutAccessoryView: View { let accessoryType: ShortcutAccessoryType var body: some View { - ZStack { - Circle() - .foregroundStyle(accessoryType.backgroundColor) - Image(accessoryType.iconResource) - .resizable() - .padding(4) - .foregroundColor(accessoryType.foregroundColor) - .aspectRatio(contentMode: .fit) - } - .shadow(color: .shade(0.15), radius: 1, y: 1) + Circle() + .foregroundStyle(accessoryType.backgroundColor) + .overlay { + Image(accessoryType.iconResource) + .foregroundColor(accessoryType.foregroundColor) + .aspectRatio(contentMode: .fit) + } + .shadow(color: .shade(0.15), radius: 1, y: 1) } } @@ -46,9 +44,9 @@ private extension ShortcutAccessoryType { var iconResource: ImageResource { switch self { case .selected: - return .check24 + return .check16Alt case .add: - return .add24 + return .add16 } } diff --git a/DuckDuckGo/ShortcutItemView.swift b/DuckDuckGo/ShortcutItemView.swift index 2372bee3fa..43cb5d1d34 100644 --- a/DuckDuckGo/ShortcutItemView.swift +++ b/DuckDuckGo/ShortcutItemView.swift @@ -26,17 +26,7 @@ struct ShortcutItemView: View { var body: some View { VStack(spacing: 6) { - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(Color(designSystemColor: .surface)) - .shadow(color: .shade(0.12), radius: 0.5, y: 1) - .aspectRatio(1, contentMode: .fit) - .frame(width: NewTabPageGrid.Item.edgeSize) - Image(shortcut.imageResource) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: NewTabPageGrid.Item.edgeSize * 0.5) - } + ShortcutIconView(shortcut: shortcut) .overlay(alignment: .topTrailing) { if let accessoryType { ShortcutAccessoryView(accessoryType: accessoryType) @@ -59,6 +49,24 @@ struct ShortcutItemView: View { } } +struct ShortcutIconView: View { + let shortcut: NewTabPageShortcut + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(Color(designSystemColor: .surface)) + .shadow(color: .shade(0.12), radius: 0.5, y: 1) + .aspectRatio(1, contentMode: .fit) + .frame(width: NewTabPageGrid.Item.edgeSize) + Image(shortcut.imageResource) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: NewTabPageGrid.Item.edgeSize * 0.5) + } + } +} + private extension NewTabPageShortcut { var name: String { switch self { @@ -92,20 +100,24 @@ private extension NewTabPageShortcut { } #Preview { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 86))], content: { - let accessoryTypes: [ShortcutAccessoryType?] = [.none, .add, .selected] - - ForEach(accessoryTypes, id: \.?.hashValue) { type in - Section { - ForEach(NewTabPageShortcut.allCases) { shortcut in - ShortcutItemView(shortcut: shortcut, accessoryType: type) + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 86))], content: { + let accessoryTypes: [ShortcutAccessoryType?] = [.none, .add, .selected] + + ForEach(accessoryTypes, id: \.?.hashValue) { type in + Section { + ForEach(NewTabPageShortcut.allCases) { shortcut in + ShortcutItemView(shortcut: shortcut, accessoryType: type) + } + + } footer: { + Spacer(minLength: 12) + Divider() + Spacer(minLength: 12) } - - } footer: { - Spacer(minLength: 12) - Divider() - Spacer(minLength: 12) } - } - }).padding(8) + }) + .padding(8) + } + .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/ShortcutsModel.swift b/DuckDuckGo/ShortcutsModel.swift index 21ef76d50a..5492ff39ed 100644 --- a/DuckDuckGo/ShortcutsModel.swift +++ b/DuckDuckGo/ShortcutsModel.swift @@ -19,28 +19,11 @@ import Foundation -protocol ShortcutsPreferencesStorage { - var enabledShortcuts: [NewTabPageShortcut] { get } -} - final class ShortcutsModel: ObservableObject { - @Published private(set) var enabledShortcuts: [NewTabPageShortcut] = [] - - private let shortcutsPreferencesStorage: ShortcutsPreferencesStorage var onShortcutOpened: ((NewTabPageShortcut) -> Void)? - init(shortcutsPreferencesStorage: ShortcutsPreferencesStorage) { - self.shortcutsPreferencesStorage = shortcutsPreferencesStorage - - enabledShortcuts = shortcutsPreferencesStorage.enabledShortcuts - } - func openShortcut(_ shortcut: NewTabPageShortcut) { onShortcutOpened?(shortcut) } } - -final class InMemoryShortcutsPreferencesStorage: ShortcutsPreferencesStorage { - private(set) var enabledShortcuts: [NewTabPageShortcut] = NewTabPageShortcut.allCases -} diff --git a/DuckDuckGo/ShortcutsView.swift b/DuckDuckGo/ShortcutsView.swift index 3ee93cf3c2..9ff0c6a2b2 100644 --- a/DuckDuckGo/ShortcutsView.swift +++ b/DuckDuckGo/ShortcutsView.swift @@ -18,13 +18,15 @@ // import SwiftUI +import UniformTypeIdentifiers struct ShortcutsView: View { - @ObservedObject private(set) var model: ShortcutsModel + private(set) var model: ShortcutsModel + let shortcuts: [NewTabPageShortcut] var body: some View { NewTabPageGridView { _ in - ForEach(model.enabledShortcuts) { shortcut in + ForEach(shortcuts) { shortcut in Button { model.openShortcut(shortcut) } label: { @@ -37,7 +39,7 @@ struct ShortcutsView: View { #Preview { ScrollView { - ShortcutsView(model: ShortcutsModel(shortcutsPreferencesStorage: InMemoryShortcutsPreferencesStorage())) + ShortcutsView(model: ShortcutsModel(), shortcuts: NewTabPageShortcut.allCases) } .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index e06f59a4b3..6d3fba3ab3 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1200,6 +1200,13 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: Tooltip public static let newTabPageTooltipBody = NSLocalizedString("new.tab.page.tooltip.body", value: "On any site, open the ••• menu and select **Add Favorite** to add it to your new tab page.", comment: "Text shown on the favorites info tooltip") + // MARK: Settings + + public static let newTabPageSettingsTitle = NSLocalizedString("new.tab.page.settings.title", value: "Customize New Tab", comment: "Title of New Tab Page preferences page.") + public static let newTabPageSettingsSectionsHeaderTitle = NSLocalizedString("new.tab.page.settings.sections.header.title", value: "SECTIONS", comment: "Header title of the group allowing for setting up new tab page sections") + public static let newTabPageSettingsSectionsDescription = NSLocalizedString("new.tab.page.settings.sections.description", value: "Show, hide, and reorder sections on the new tab page", comment: "Footer of the group allowing for setting up new tab page sections") + public static let newTabPageSettingsShortcutsHeaderTitle = NSLocalizedString("new.tab.page.settings.shortcuts.header.title", value: "SHORTCUTS", comment: "Header title of the shortcuts in New Tab Page preferences.") + // MARK: - Dax Onboarding Experiment public enum DaxOnboardingExperiment { enum Intro { diff --git a/DuckDuckGo/ViewExtension.swift b/DuckDuckGo/ViewExtension.swift index 287be0c110..fa575b6228 100644 --- a/DuckDuckGo/ViewExtension.swift +++ b/DuckDuckGo/ViewExtension.swift @@ -34,6 +34,43 @@ extension View { } } +extension View { + /// Disables scroll in a backwards-compatible way. + /// + /// Keep in mind fallback version may have unforeseen consequences. + /// Verify if it does not break anything else for you. + @ViewBuilder + func withoutScroll() -> some View { + if #available(iOS 16, *) { + scrollDisabled(true) + } else { + gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local), including: .gesture) + } + } +} + +extension View { + /// Adds a preference key observer for views' frame in a given coordinate space. + /// + /// - Parameters: + /// - space: `CoordinateSpace` used to convert the frame to. + /// - key: `PreferenceKey` used to observe the value. + /// - perform: Closure to call on value change. + func onFrameUpdate( + in space: CoordinateSpace, + using key: K.Type, + perform: @escaping (CGRect) -> Void) -> some View where K.Value == CGRect { + + self.background { + GeometryReader(content: { geometry in + Color.clear + .preference(key: key, value: geometry.frame(in: space)) + }) + } + .onPreferenceChange(key, perform: perform) + } +} + extension View { @ViewBuilder func applyInsetGroupedListStyle() -> some View { diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 3635ff4bf4..88ca572a13 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1585,6 +1585,18 @@ https://duckduckgo.com/mac"; /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Settings"; +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Show, hide, and reorder sections on the new tab page"; + +/* Header title of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.header.title" = "SECTIONS"; + +/* Header title of the shortcuts in New Tab Page preferences. */ +"new.tab.page.settings.shortcuts.header.title" = "SHORTCUTS"; + +/* Title of New Tab Page preferences page. */ +"new.tab.page.settings.title" = "Customize New Tab"; + /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat";