From 0d785167b0ea821b0f2d855236b6ba016eced99b Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 2 Jan 2024 16:00:48 -0300 Subject: [PATCH 01/19] Add mute/unmute tab feature --- .../Extensions/WKWebViewExtension.swift | 46 +++++++++++++++++++ DuckDuckGo/Common/Localizables/UserText.swift | 2 + DuckDuckGo/Localizable.xcstrings | 24 ++++++++++ .../Model/PinnedTabsViewModel.swift | 26 +++++++++++ .../PinnedTabs/View/PinnedTabView.swift | 12 ++++- DuckDuckGo/Tab/Model/Tab.swift | 8 ++++ .../TabBar/View/TabBarViewController.swift | 23 ++++++++++ DuckDuckGo/TabBar/View/TabBarViewItem.swift | 40 ++++++++++++++++ DuckDuckGo/TabBar/View/TabBarViewItem.xib | 26 ++++++++--- 9 files changed, 200 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index a2eb4706e4..03aeb92225 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -29,6 +29,12 @@ extension WKWebView { return false } + enum AudioState { + case muted + case unmuted + case notSupported + } + enum CaptureState { case none case active @@ -129,6 +135,46 @@ extension WKWebView { } #endif +#if !APPSTORE + func muteOrUnmute() { + guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { + assertionFailure("WKWebView does not respond to selector _stopMediaCapture") + return + } + let mutedState: _WKMediaMutedState = { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } + return self._mediaMutedState() + }() + var newState = mutedState + + if newState == .audioMuted { + newState.remove(.audioMuted) + } else { + newState.insert(.audioMuted) + } + guard newState != mutedState else { return } + self._setPageMuted(newState) + + return + } + + /// Returns the audio state of the WKWebView. + /// + /// - Returns: `muted` if the web view is muted + /// `unmuted` if the web view is unmuted + /// `notSupported` if the web view does not support fetching the current audio state + func audioState() -> AudioState { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return .notSupported + } + + let mutedState = self._mediaMutedState() + + return mutedState.contains(.audioMuted) ? .muted : .unmuted + } +#endif + func stopMediaCapture() { guard #available(macOS 12.0, *) else { #if !APPSTORE diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c3f8ff0f68..0d802a6d99 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -199,6 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index faef1b6820..ad2e305fb9 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -5092,6 +5092,18 @@ } } }, + "mute.tab" : { + "comment" : "Menu item. Mute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mute" + } + } + } + }, "n.more.tabs" : { "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", @@ -9100,6 +9112,18 @@ } } }, + "unmute.tab" : { + "comment" : "Menu item. Unmute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unmute" + } + } + } + }, "unpin.tab" : { "comment" : "Menu item. Unpin as a verb", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2c714304df..2f1f8a2611 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let selectedItem = selectedItem { selectedItemIndex = items.firstIndex(of: selectedItem) + updateTabAudioState(tab: selectedItem) } else { selectedItemIndex = nil } @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let hoveredItem = hoveredItem { hoveredItemIndex = items.firstIndex(of: hoveredItem) + updateTabAudioState(tab: hoveredItem) } else { hoveredItemIndex = nil } @@ -72,6 +74,7 @@ final class PinnedTabsViewModel: ObservableObject { @Published private(set) var selectedItemIndex: Int? @Published private(set) var hoveredItemIndex: Int? @Published private(set) var dragMovesWindow: Bool = true + @Published private(set) var audioStateView: AudioStateView = .notSupported @Published private(set) var itemsWithoutSeparator: Set = [] @@ -111,6 +114,18 @@ final class PinnedTabsViewModel: ObservableObject { } itemsWithoutSeparator = items } + + private func updateTabAudioState(tab: Tab) { + let audioState = tab.audioState() + switch audioState { + case .muted: + audioStateView = .muted + case .unmuted: + audioStateView = .unmuted + case .notSupported: + audioStateView = .notSupported + } + } } // MARK: - Context Menu @@ -124,6 +139,13 @@ extension PinnedTabsViewModel { case fireproof(Tab) case removeFireproofing(Tab) case close(Int) + case muteOrUnmute(Tab) + } + + enum AudioStateView { + case muted + case unmuted + case notSupported } func isFireproof(_ tab: Tab) -> Bool { @@ -168,4 +190,8 @@ extension PinnedTabsViewModel { func removeFireproofing(_ tab: Tab) { contextMenuActionSubject.send(.removeFireproofing(tab)) } + + func muteOrUmute(_ tab: Tab) { + contextMenuActionSubject.send(.muteOrUnmute(tab)) + } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 148b0d5d82..6d84a3d518 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -96,7 +96,17 @@ struct PinnedTabView: View { fireproofAction Divider() - + switch collectionModel.audioStateView { + case .muted, .unmuted: + let audioStateText = collectionModel.audioStateView == .muted ? UserText.unmuteTab : UserText.muteTab + Button(audioStateText) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.muteOrUmute(model) + } + Divider() + case .notSupported: + EmptyView() + } Button(UserText.closeTab) { [weak collectionModel, weak model] in guard let model = model else { return } collectionModel?.close(model) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index aba8f89859..226ae10e42 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -934,6 +934,14 @@ protocol NewWindowPolicyDecisionMaker { } } + func muteUnmuteTab() { + webView.muteOrUnmute() + } + + func audioState() -> WKWebView.AudioState { + webView.audioState() + } + @MainActor(unsafe) @discardableResult private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? { diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 919cd75eff..e7462b8d9a 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -314,6 +314,8 @@ final class TabBarViewController: NSViewController { removeFireproofing(from: tab) case let .close(index): tabCollectionViewModel.remove(at: .pinned(index)) + case let .muteOrUnmute(tab): + tab.muteUnmuteTab() } } @@ -1101,6 +1103,17 @@ extension TabBarViewController: TabBarViewItemDelegate { fireproof(tab) } + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + guard let indexPath = collectionView.indexPath(for: tabBarViewItem), + let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] + else { + assertionFailure("TabBarViewController: Failed to get tab from tab bar view item") + return + } + + tab.muteUnmuteTab() + } + func tabBarViewItemRemoveFireproofing(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] @@ -1112,6 +1125,16 @@ extension TabBarViewController: TabBarViewItemDelegate { removeFireproofing(from: tab) } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + guard let indexPath = collectionView.indexPath(for: tabBarViewItem), + let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] + else { + return .notSupported + } + + return tab.audioState() + } + func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 50937c231a..9735be826f 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -42,7 +42,9 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewBurnerWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemFireproofSite(_ tabBarViewItem: TabBarViewItem) + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemRemoveFireproofing(_ tabBarViewItem: TabBarViewItem) + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState @@ -53,6 +55,8 @@ final class TabBarViewItem: NSCollectionViewItem { enum Constants { static let textFieldPadding: CGFloat = 32 static let textFieldPaddingNoFavicon: CGFloat = 12 + static let textFieldTrailingNoMuteIcon: CGFloat = 8 + static let textFieldTrailingMuteIconPresent: CGFloat = 32 } var widthStage: WidthStage { @@ -94,6 +98,7 @@ final class TabBarViewItem: NSCollectionViewItem { @IBOutlet weak var titleTextField: NSTextField! @IBOutlet weak var titleTextFieldLeadingConstraint: NSLayoutConstraint! + @IBOutlet weak var titleTextFieldTrailingConstraint: NSLayoutConstraint! @IBOutlet weak var closeButton: MouseOverButton! @IBOutlet weak var rightSeparatorView: ColorView! @IBOutlet weak var mouseOverView: MouseOverView! @@ -104,6 +109,7 @@ final class TabBarViewItem: NSCollectionViewItem { @IBOutlet var tabLoadingPermissionLeadingConstraint: NSLayoutConstraint! @IBOutlet var closeButtonTrailingConstraint: NSLayoutConstraint! + @IBOutlet weak var mutedTabIcon: NSImageView! private let titleTextFieldMaskLayer = CAGradientLayer() private var currentURL: URL? @@ -121,6 +127,7 @@ final class TabBarViewItem: NSCollectionViewItem { setupMenu() updateTitleTextFieldMask() closeButton.isHidden = true + setupMuteOrUnmutedIcon() } override func viewDidLayout() { @@ -128,6 +135,7 @@ final class TabBarViewItem: NSCollectionViewItem { updateSubviews() updateTitleTextFieldMask() + setupMuteOrUnmutedIcon() } override func viewWillDisappear() { @@ -178,6 +186,11 @@ final class TabBarViewItem: NSCollectionViewItem { delegate?.tabBarViewItemFireproofSite(self) } + @objc func muteUnmuteSiteAction(_ sender: NSButton) { + delegate?.tabBarViewItemMuteUnmuteSite(self) + setupMuteOrUnmutedIcon() + } + @objc func removeFireproofingAction(_ sender: NSButton) { delegate?.tabBarViewItemRemoveFireproofing(self) } @@ -418,6 +431,17 @@ final class TabBarViewItem: NSCollectionViewItem { faviconImageView.image = favicon } + private func setupMuteOrUnmutedIcon() { + switch delegate?.tabBarViewItemAudioState(self) { + case .muted: + mutedTabIcon.isHidden = false + default: + mutedTabIcon.isHidden = true + } + + titleTextFieldTrailingConstraint.constant = mutedTabIcon.isHidden ? Constants.textFieldTrailingNoMuteIcon : Constants.textFieldTrailingMuteIconPresent + } + } extension TabBarViewItem: NSMenuDelegate { @@ -440,6 +464,9 @@ extension TabBarViewItem: NSMenuDelegate { addFireproofMenuItem(to: menu) menu.addItem(NSMenuItem.separator()) + addMuteUnmuteMenuItem(to: menu) + menu.addItem(NSMenuItem.separator()) + // Section 3 addCloseMenuItem(to: menu) addCloseOtherMenuItem(to: menu, areThereOtherTabs: areThereOtherTabs) @@ -482,6 +509,19 @@ extension TabBarViewItem: NSMenuDelegate { menu.addItem(menuItem) } + #if !APPSTORE + private func addMuteUnmuteMenuItem(to menu: NSMenu) { + let audioState = delegate?.tabBarViewItemAudioState(self) ?? .notSupported + + if audioState != .notSupported { + let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab + var muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") + muteUnmuteMenuItem.target = self + menu.addItem(muteUnmuteMenuItem) + } + } + #endif + private func addCloseMenuItem(to menu: NSMenu) { let closeMenuItem = NSMenuItem(title: UserText.closeTab, action: #selector(closeButtonAction(_:)), keyEquivalent: "") closeMenuItem.target = self diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.xib b/DuckDuckGo/TabBar/View/TabBarViewItem.xib index deb089ac2a..1e9d4995ac 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.xib +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.xib @@ -1,7 +1,7 @@ - + - + @@ -10,18 +10,26 @@ - + + - + - + @@ -116,7 +124,7 @@ - + @@ -125,6 +133,7 @@ + @@ -133,10 +142,12 @@ + + @@ -158,12 +169,14 @@ + + @@ -171,6 +184,7 @@ + From a52138ac8909c99bf87dfae0547faa347e81fb22 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 4 Jan 2024 11:39:46 -0300 Subject: [PATCH 02/19] Show mute icon on pinned tabs --- .../Model/PinnedTabsViewModel.swift | 3 +- .../PinnedTabs/View/PinnedTabView.swift | 32 ++++++++++++++++--- DuckDuckGo/Tab/Model/Tab.swift | 7 ++-- .../TabBar/View/TabBarViewController.swift | 2 +- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2f1f8a2611..ee5c9f0c91 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -116,7 +116,7 @@ final class PinnedTabsViewModel: ObservableObject { } private func updateTabAudioState(tab: Tab) { - let audioState = tab.audioState() + let audioState = tab.audioState switch audioState { case .muted: audioStateView = .muted @@ -193,5 +193,6 @@ extension PinnedTabsViewModel { func muteOrUmute(_ tab: Tab) { contextMenuActionSubject.send(.muteOrUnmute(tab)) + updateTabAudioState(tab: tab) } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 6d84a3d518..e61cadc7ca 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -197,11 +197,31 @@ struct PinnedTabInnerView: View { .frame(width: PinnedTabView.Const.dimension) } + @ViewBuilder + var mutedTabIndicator: some View { + switch model.audioState { + case .muted: + ZStack { + Circle() + .foregroundColor(.black) + .frame(width: 12, height: 12) + Image(systemName: "speaker.slash.fill") + .resizable() + .foregroundColor(.white) + .frame(width: 10, height: 10) + }.offset(x: 6, y: -2) + default: EmptyView() + } + } + @ViewBuilder var favicon: some View { if let favicon = model.favicon { - Image(nsImage: favicon) - .resizable() + ZStack(alignment: .topTrailing) { + Image(nsImage: favicon) + .resizable() + mutedTabIndicator + } } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() @@ -209,11 +229,15 @@ struct PinnedTabInnerView: View { Text(firstLetter) .font(.caption) .foregroundColor(.white) + mutedTabIndicator } .cornerRadius(4.0) } else { - Image(nsImage: #imageLiteral(resourceName: "Web")) - .resizable() + ZStack { + Image(nsImage: #imageLiteral(resourceName: "Web")) + .resizable() + mutedTabIndicator + } } } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 226ae10e42..5c1c11d1db 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -488,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker { } #endif + self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -934,12 +935,12 @@ protocol NewWindowPolicyDecisionMaker { } } + @Published private(set) var audioState: WKWebView.AudioState = .notSupported + func muteUnmuteTab() { webView.muteOrUnmute() - } - func audioState() -> WKWebView.AudioState { - webView.audioState() + audioState = webView.audioState() } @MainActor(unsafe) diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index e7462b8d9a..c00b5854a0 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1132,7 +1132,7 @@ extension TabBarViewController: TabBarViewItemDelegate { return .notSupported } - return tab.audioState() + return tab.audioState } func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState { From 1df409c9c36c6374db7501ee52ba268b916bfc36 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 4 Jan 2024 16:24:07 -0300 Subject: [PATCH 03/19] Add not AppStore flag --- DuckDuckGo/Common/Extensions/WKWebViewExtension.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 03aeb92225..6ad8fa4070 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -135,8 +135,8 @@ extension WKWebView { } #endif -#if !APPSTORE func muteOrUnmute() { +#if !APPSTORE guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { assertionFailure("WKWebView does not respond to selector _stopMediaCapture") return @@ -154,7 +154,7 @@ extension WKWebView { } guard newState != mutedState else { return } self._setPageMuted(newState) - +#endif return } @@ -164,6 +164,7 @@ extension WKWebView { /// `unmuted` if the web view is unmuted /// `notSupported` if the web view does not support fetching the current audio state func audioState() -> AudioState { +#if !APPSTORE guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { assertionFailure("WKWebView does not respond to selector _mediaMutedState") return .notSupported @@ -172,8 +173,10 @@ extension WKWebView { let mutedState = self._mediaMutedState() return mutedState.contains(.audioMuted) ? .muted : .unmuted - } +#else + return .notSupported #endif + } func stopMediaCapture() { guard #available(macOS 12.0, *) else { From 97c61c5d08adce22ca933645d065762b9273ab93 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 4 Jan 2024 16:36:46 -0300 Subject: [PATCH 04/19] Fix tests --- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 4 +--- UnitTests/TabBar/View/MockTabViewItemDelegate.swift | 8 ++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 9735be826f..ce60ab458c 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -462,7 +462,6 @@ extension TabBarViewItem: NSMenuDelegate { // Section 2 addBookmarkMenuItem(to: menu) addFireproofMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) addMuteUnmuteMenuItem(to: menu) menu.addItem(NSMenuItem.separator()) @@ -509,18 +508,17 @@ extension TabBarViewItem: NSMenuDelegate { menu.addItem(menuItem) } - #if !APPSTORE private func addMuteUnmuteMenuItem(to menu: NSMenu) { let audioState = delegate?.tabBarViewItemAudioState(self) ?? .notSupported if audioState != .notSupported { + menu.addItem(NSMenuItem.separator()) let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab var muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") muteUnmuteMenuItem.target = self menu.addItem(muteUnmuteMenuItem) } } - #endif private func addCloseMenuItem(to menu: NSMenu) { let closeMenuItem = NSMenuItem(title: UserText.closeTab, action: #selector(closeButtonAction(_:)), keyEquivalent: "") diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9d576f6550..1307e7d229 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -75,6 +75,14 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + return .notSupported + } + + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + + } + func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } From e58526941cf58d397446a50a5c8bb6a7f1ff64e4 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 4 Jan 2024 17:37:33 -0300 Subject: [PATCH 05/19] Add unit tests --- .../PinnedTabs/PinnedTabsViewModelTests.swift | 6 +++-- .../TabBar/View/MockTabViewItemDelegate.swift | 7 +++++- .../TabBar/View/TabBarViewItemTests.swift | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index 6e34010422..c9f063d54f 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -149,17 +149,19 @@ class PinnedTabsViewModelTests: XCTestCase { model.fireproof(tabA) model.removeFireproofing(tabB) model.close(tabA) + model.muteOrUmute(tabB) cancellable.cancel() - XCTAssertEqual(events.count, 6) + XCTAssertEqual(events.count, 7) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], case .duplicate(0) = events[2], case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], - case .close(0) = events[5] + case .close(0) = events[5], + case .muteOrUnmute(tabB) = events[6] else { XCTFail("Incorrect context menu action") return diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 1307e7d229..ab577650a2 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -22,6 +22,7 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheRight = false + var audioState: WKWebView.AudioState = .notSupported func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -76,7 +77,7 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { - return .notSupported + return audioState } func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { @@ -87,4 +88,8 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } + func clear() { + self.audioState = .notSupported + } + } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index d855470109..c4e8f4418b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -33,6 +33,10 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.delegate = delegate } + override func tearDown() { + delegate.clear() + } + func testThatAllExpectedItemsAreShown() { tabBarViewItem.menuNeedsUpdate(menu) @@ -48,6 +52,24 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) } + func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { + delegate.audioState = .unmuted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + + func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { + delegate.audioState = .muted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) From 3c7c9a1508d4c6df45352c39708ea95d3b892e85 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 20 Feb 2024 15:52:40 -0300 Subject: [PATCH 06/19] Change text to Mute/Unmute Tab --- DuckDuckGo/Common/Localizables/UserText.swift | 4 ++-- DuckDuckGo/Localizable.xcstrings | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 0d802a6d99..eee5b3f4e5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -199,8 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") - static let muteTab = NSLocalizedString("mute.tab", value: "Mute", comment: "Menu item. Mute tab") - static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute", comment: "Menu item. Unmute tab") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index ad2e305fb9..ddf6e62331 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -5099,7 +5099,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Mute" + "value" : "Mute Tab" } } } @@ -9119,7 +9119,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Unmute" + "value" : "Unmute Tab" } } } From 461bec0a01accb0c317f4a65892615f0ac8f8698 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 6 Feb 2024 10:59:18 -0300 Subject: [PATCH 07/19] Fix SwiftLint --- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index ce60ab458c..62cb475990 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -514,7 +514,7 @@ extension TabBarViewItem: NSMenuDelegate { if audioState != .notSupported { menu.addItem(NSMenuItem.separator()) let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab - var muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") + let muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") muteUnmuteMenuItem.target = self menu.addItem(muteUnmuteMenuItem) } From 49262bb2ac1859a6327a01a8557a3df50d15ec09 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 12 Feb 2024 14:13:20 -0300 Subject: [PATCH 08/19] Add audio icons --- .../Images/Audio-Mute.imageset/Audio-Mute-12.pdf | Bin 0 -> 4374 bytes .../Images/Audio-Mute.imageset/Contents.json | 12 ++++++++++++ .../Images/Audio.imageset/Audio-12.pdf | Bin 0 -> 4307 bytes .../Images/Audio.imageset/Contents.json | 12 ++++++++++++ 4 files changed, 24 insertions(+) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de1f4718ed0a91177028ae95b498623cae76a1cb GIT binary patch literal 4374 zcmeH~TaOb*5QX38SM&=KJZ!t~7YQM8SCk;gZXS?O9*lQ@CA&7UO$7P%`Kmph@m>-Z zo-;!1_)J%I^{G?U-FIF+d3I!V?mDZCTD|?)DRuw8n*H|mQ?F*PU%vg=FScMjvtRn< z`|Sti7e44NO<>Kq5?++dfu1-F`-L^@et=1O< zcHD1PpVy0iBU6kcA3a)qQE!ydrajY(b$e)f`yE$UZno=we$maI^naZ#`s3%1)Z>%G zUyDumXJ_~3qwAyb?fSQ%LpI3-wYccUVb@RnRsX=Zw1OtkEH);;i!8 z)*4e*=K?0i*{GaODdtoaP>#m)#`0?QIS!EZPFWpY4z8$$nC4JRB$2v=l1r9x1ZblN zvbmVjOuch5inRq_qcuBQ85f;G3?^pFvZ_wr+MESSCObY>ZEf|bs3Ss&t{RVR(Ir+B zl7KGPn1cfHHnSk0l!H$eQxi+@&9r*f4R2l55(yC;2?f!ZB(vsPDyCjL9zw3VhQ=A* zgeNPrvb0!Iwr$$rT`EaIvo+C_wu{Ib^PDPPux%;}%;&T(!3HCy#bz=~DxJ5XHg6%hkS!@Uux46{2qsuxjDQi2 zTFk-LY-Jp*2j|7xRkbX-58b8G(0#vY@A{#`+ zxEwuFYk^cI6;mk(*J3ltmufLZ^DrT^dI|zmwCpHi!UQ7i7V!WDYLf{Zgc}yoOaY6C z%t07p$TH5?6r(_a5`vu@X#eqaI~cSj-li#~eYeZ#zK|-iJz`N)p}8iK9i;t88cCSH zie;qAVr3kqj((ATWR20og<$>%tB(MGWmquN|DxDTnW-+ z9nPeL=<_LhLkBk6dpfMcm%&$}RZ83N)Uq^NiHhi@A7!L{hzv1^uVGZ6<+WJF*MLP$ zcMABYsXK&PRMMXHu%eNh5s@`kV|}dCD9Ib7me!54p|42c22HU@pQ^-PE&Ilpae8*i zkpdo(Q5QchN9xYA6VW!mxkrxEQn!RCIk}j$ca*5$1|A3!Fts+{R+a)g12vg+OlVS; z>opC4?x&8VcX;}28#(X-SlKAk0Wnm0(TQ=5${`m(HWmI&)L`mZ7Oi42)yx$5`46{i zbj5Z>Q~Wkzvbh&ebCeB(mR8VNJ5^LiOJ=7>(w_a&wl{XtbOl4TRpdQdqa)eKXo0AV zLqxP}=kk5RLL|DjYwfL(KieDiA}VBe6PL)iBzAGwrUYu1sb!;$*)R~###v9BJ%FV4 zAbqry`ta6Q+O5=@HFWd?BvX>a#vx#D$5tZ@5XB9=ObO}w<>_!3tQ zt>7xeetH@5o?FXRC_tG!qc5MX8|LeyIJ;jptR4<8`3jtNMrga9pPl#XPU!vV3FO}0 wtSh9U;y&c7~J1!o9%qPZR`Y-eRt>1)0fZw0h>RF!2kdN literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json new file mode 100644 index 0000000000..b609317961 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-Mute-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..635e45f8743e3360c3aa3f3fe264526a6d890242 GIT binary patch literal 4307 zcmeH~O>Y!O5QgvbEBb;2k(ll84+&WU8>}cn5IYAXl*6(fj1zm;*ysCcO`Dp=D6LlSes7d|@f`O6LH)abS8aO3bNsF6%gyb4-7OcN@4P=c8eE-!e7CKezF4hq z2JEcctUj(6-A1+;S3Y~T`lQ|}-RkX z{l6BQ=Fi6L%tzOc$ESPW1>>X1YH`zy>kc0_i)G_mTXardNnlYYW7G{`v1E`1jK%rj zRoq1YWPK{e?m?z?Aw*l$qM6pUF}diHx}GrIrfgiWYSH7|+Y)mK0biyq z+PJ8dYh5xaMSPhy+CXgk7;bF}E>GB)t8aBFj$rqYQesNtf5dXDo%O}we-A6R6fz&Z zmo|nZtHu~cWEX2FYQ(PZ@k(LmjVNGt_dhht=9lkQQEa^r24=r5qSRXJs2)5(q^RkpNtnkH1JPS+3%W{e_Qp{Hkl;%Y z<_$%!b=A&z=Shs3MQ1gFq^o0ykyQyf2dR&fKFA=&7wa9piBORUcBM5wX*8%?-sK$V zM0%P|klweb$Mi^XPBA@XUqTQ=s%aq*uo#!?I^(m2X9loC>r4{13`W~1P}VV8h6}MG zyHv7zOiGUAWSx(6GFwnJBC8!|bc!(vR3Nm@2_b_x*}@Sx@fP^;K}&T7GT25S3kUeX zH2X|%v1?)!*(6!mHsp{~Vx4y02BM?Kit)OdmLw`8U>Z00R84~+ew81*G9pTO0$;2&fHj3%TOoNDc^@IfjH-#cyOdlXW#MB?%|GHKEcZ z-rz|FprP9AAe(v-C?k>HFufuCnItu3^b{{W6bw(YDl>z#AeQAsLs$tK`&enGl4Mdj zDQ#v}4{@PO<}0E)?Ae8A-XFfjknUcaaJsGc(?#)4dLW(x{n6 z5yUZKP*&mVWKoaUnM{4)HMg0MNvO&SIYI_Vv{pN^Lq$1R>?n}KH~G?yStQ1O&L!F= zwuB&+WD?V}Gy)KOd1?y8H^E~Nm}xffj%i3Cbg=jh)ZWw8b%@ip^%LftD<0+24ph)t zK1eYHhnTA=13MEfD!?#0S%*|ikcog83(Zw`wX%vrWjGOO1IjX#AvM;irWKuG(9#FN z!i<+h4Ly}XWWFbdA#^~eM;6j}hV+FYI!eTdrH{VO2_3RWyFf@g(=K%?5QV84&@mZ? z6ttmrnp_k>p718s)DT`x`Kr=aHC(EZOx>2`GhU2g)CHa8L+q$9bO9kZ@xnVM7ox$I z=|Y3!M@Ff3a05i|O1UiMN^&&l4^caUeae-kdd6Z%@DU1zu}E#iTQZEfzzGHY)<96Z zM;j64l~fyob&M!_L3Dzpd<&tc)UPxH0`95EVe3wcX}aabaFXLmPOFWaM*CwcV>rly z3M1WbJI!+P`2RG^{HCAl?;D28xU_E&%$P3I^Yp|SW;~IPMtg_9`>@HP9xCdfyr=9^ zwxR6Kj#cL)yjS(%_EN7WZ;-lFdb&kC|Fk_l-_EyPegE9l|IS~|H-FTp{sJU_-np(! zx0<_8vr{E4H{6TvzB~A9#)atV_3elG*In-}JqJDn7;b4}CjaK{=%`N{XFGkwJ4_8X zJLX)^&^O(dW^i`7X=YG+vf|`-PnE=BUnL*Mt(l#BMS8kiuC^QXR_-w&o!I4alA+#`C@_$#667IAwl3-_EaBmv?PHt-JS)Ox30rsr;+tn->)L@RJSX zr)qW|P7ZWmLWvaRH~Mnn&@djZiUq>%8p`qD3y7282yNH%tLtvv2)#d^ zK<>lM>ZW_79$sBM>M5R`tyb_W^woOJ&aeLNgbT>sZ0GB3WyfnBn+Fe`zk2Zx7O#DG literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json new file mode 100644 index 0000000000..35d4dda319 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From b2bb9fea17ef67c269b4f759165365b6b1ea00e9 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 12 Feb 2024 14:43:47 -0300 Subject: [PATCH 09/19] Apply new icons and designs for pinned tabs --- DuckDuckGo/PinnedTabs/View/PinnedTabView.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index e61cadc7ca..69f9fb947b 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -173,6 +173,7 @@ struct PinnedTabInnerView: View { var foregroundColor: Color var drawSeparator: Bool = true + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Tab @Environment(\.controlActiveState) private var controlActiveState @@ -203,12 +204,14 @@ struct PinnedTabInnerView: View { case .muted: ZStack { Circle() - .foregroundColor(.black) - .frame(width: 12, height: 12) - Image(systemName: "speaker.slash.fill") + .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) + .background(Circle().foregroundColor(colorScheme == .dark ? Color.black : Color.white)) + .frame(width: 14, height: 14) + Image("Audio-Mute") .resizable() - .foregroundColor(.white) - .frame(width: 10, height: 10) + .renderingMode(.template) + .foregroundColor(.gray) + .frame(width: 9, height: 9) }.offset(x: 6, y: -2) default: EmptyView() } From 954de00670c00a646fd448e56883bdc346fc0bbe Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 12 Feb 2024 16:17:42 -0300 Subject: [PATCH 10/19] Move mute icon to the left of the title WIP: When tabs are getting smaller, there is a constraint that is failing. --- .../MutedTabIconColor.colorset/Contents.json | 38 +++++++++++++++++++ .../Contents.json | 38 +++++++++++++++++++ .../PinnedTabs/View/PinnedTabView.swift | 2 +- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 25 +++++++++--- DuckDuckGo/TabBar/View/TabBarViewItem.xib | 12 +++--- 5 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json new file mode 100644 index 0000000000..3fe9b59242 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json new file mode 100644 index 0000000000..802fa68a4c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 69f9fb947b..51a866ee11 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -205,7 +205,7 @@ struct PinnedTabInnerView: View { ZStack { Circle() .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) - .background(Circle().foregroundColor(colorScheme == .dark ? Color.black : Color.white)) + .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) .frame(width: 14, height: 14) Image("Audio-Mute") .resizable() diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 62cb475990..2db24aaf81 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -55,8 +55,7 @@ final class TabBarViewItem: NSCollectionViewItem { enum Constants { static let textFieldPadding: CGFloat = 32 static let textFieldPaddingNoFavicon: CGFloat = 12 - static let textFieldTrailingNoMuteIcon: CGFloat = 8 - static let textFieldTrailingMuteIconPresent: CGFloat = 32 + static let textFieldPaddingMuteIconPresent: CGFloat = 56 } var widthStage: WidthStage { @@ -98,7 +97,6 @@ final class TabBarViewItem: NSCollectionViewItem { @IBOutlet weak var titleTextField: NSTextField! @IBOutlet weak var titleTextFieldLeadingConstraint: NSLayoutConstraint! - @IBOutlet weak var titleTextFieldTrailingConstraint: NSLayoutConstraint! @IBOutlet weak var closeButton: MouseOverButton! @IBOutlet weak var rightSeparatorView: ColorView! @IBOutlet weak var mouseOverView: MouseOverView! @@ -127,7 +125,6 @@ final class TabBarViewItem: NSCollectionViewItem { setupMenu() updateTitleTextFieldMask() closeButton.isHidden = true - setupMuteOrUnmutedIcon() } override func viewDidLayout() { @@ -135,7 +132,6 @@ final class TabBarViewItem: NSCollectionViewItem { updateSubviews() updateTitleTextFieldMask() - setupMuteOrUnmutedIcon() } override func viewWillDisappear() { @@ -349,6 +345,7 @@ final class TabBarViewItem: NSCollectionViewItem { mouseOverView.mouseOverColor = isSelected || isDragged ? NSColor.clear : NSColor.tabMouseOverColor } + setupMuteOrUnmutedIcon() let showCloseButton = (isMouseOver && !widthStage.isCloseButtonHidden) || isSelected closeButton.isHidden = !showCloseButton updateSeparatorView() @@ -432,16 +429,32 @@ final class TabBarViewItem: NSCollectionViewItem { } private func setupMuteOrUnmutedIcon() { + setupMutedTabIconVisibility() + setupMutedTabIconColor() + setupMutedTabIconPosition() + } + + private func setupMutedTabIconVisibility() { switch delegate?.tabBarViewItemAudioState(self) { case .muted: mutedTabIcon.isHidden = false default: mutedTabIcon.isHidden = true } + } - titleTextFieldTrailingConstraint.constant = mutedTabIcon.isHidden ? Constants.textFieldTrailingNoMuteIcon : Constants.textFieldTrailingMuteIconPresent + private func setupMutedTabIconColor() { + mutedTabIcon.image?.isTemplate = true + mutedTabIcon.contentTintColor = NSColor(named: "MutedTabIconColor") } + private func setupMutedTabIconPosition() { + if !mutedTabIcon.isHidden { + titleTextFieldLeadingConstraint.constant = Constants.textFieldPaddingMuteIconPresent + } else { + titleTextFieldLeadingConstraint.constant = faviconWrapperView.isHidden ? Constants.textFieldPaddingNoFavicon : Constants.textFieldPadding + } + } } extension TabBarViewItem: NSMenuDelegate { diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.xib b/DuckDuckGo/TabBar/View/TabBarViewItem.xib index 1e9d4995ac..97b8979631 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.xib +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.xib @@ -14,12 +14,12 @@ @@ -29,7 +29,7 @@ - + @@ -133,7 +133,6 @@ - @@ -147,9 +146,9 @@ - + @@ -176,15 +175,14 @@ - + - From 8ba8c4e2fe9660635f9ad1dd83a95fd541d6cb9f Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 20 Feb 2024 15:41:31 -0300 Subject: [PATCH 11/19] Further design improvements --- .../PinnedTabs/View/PinnedTabView.swift | 11 ++++--- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 30 ++++++++++++++----- DuckDuckGo/TabBar/View/TabBarViewItem.xib | 15 +++++----- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 51a866ee11..5775673c9a 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -21,8 +21,8 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { - static let dimension: CGFloat = 32 - static let cornerRadius: CGFloat = 6 + static let dimension: CGFloat = 34 + static let cornerRadius: CGFloat = 8.5 } @ObservedObject var model: Tab @@ -206,13 +206,12 @@ struct PinnedTabInnerView: View { Circle() .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) - .frame(width: 14, height: 14) + .frame(width: 16, height: 16) Image("Audio-Mute") .resizable() .renderingMode(.template) - .foregroundColor(.gray) - .frame(width: 9, height: 9) - }.offset(x: 6, y: -2) + .frame(width: 12, height: 12) + }.offset(x: 8, y: -8) default: EmptyView() } } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 2db24aaf81..3c3fdb7d8d 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -53,9 +53,11 @@ protocol TabBarViewItemDelegate: AnyObject { final class TabBarViewItem: NSCollectionViewItem { enum Constants { - static let textFieldPadding: CGFloat = 32 + static let textFieldPadding: CGFloat = 28 static let textFieldPaddingNoFavicon: CGFloat = 12 - static let textFieldPaddingMuteIconPresent: CGFloat = 56 + static let textFieldPaddingMuteIconPresent: CGFloat = 48 + static let faviconLeadingPadding: CGFloat = 4 + static let faviconLeadingPaddingMuteIconPresent: CGFloat = 6 } var widthStage: WidthStage { @@ -276,7 +278,7 @@ final class TabBarViewItem: NSCollectionViewItem { layer.borderWidth = TabShadowConfig.dividerSize layer.opacity = TabShadowConfig.alpha layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner] - layer.cornerRadius = 7 + layer.cornerRadius = 11 layer.mask = layerMask return layer }() @@ -328,7 +330,7 @@ final class TabBarViewItem: NSCollectionViewItem { private func setupView() { view.wantsLayer = true - view.layer?.cornerRadius = 7 + view.layer?.cornerRadius = 11 view.layer?.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] view.layer?.masksToBounds = true view.layer?.addSublayer(borderLayer) @@ -352,8 +354,20 @@ final class TabBarViewItem: NSCollectionViewItem { permissionCloseButtonTrailingConstraint.isActive = !closeButton.isHidden titleTextField.isHidden = widthStage.isTitleHidden && faviconImageView.image != nil - faviconWrapperViewCenterConstraint.priority = titleTextField.isHidden ? .defaultHigh : .defaultLow - faviconWrapperViewLeadingConstraint.priority = titleTextField.isHidden ? .defaultLow : .defaultHigh + if mutedTabIcon.isHidden { + faviconWrapperViewCenterConstraint.priority = titleTextField.isHidden ? .defaultHigh : .defaultLow + faviconWrapperViewLeadingConstraint.priority = titleTextField.isHidden ? .defaultLow : .defaultHigh + } else { + // When the mute icon is visible and the tab is compressed we need to center both + faviconWrapperViewCenterConstraint.priority = .defaultLow + faviconWrapperViewLeadingConstraint.priority = .defaultHigh + + if titleTextField.isHidden { // If the title text is hidden it means the tab is compressed + faviconWrapperViewLeadingConstraint.constant = Constants.faviconLeadingPaddingMuteIconPresent + } else { + faviconWrapperViewLeadingConstraint.constant = Constants.faviconLeadingPadding + } + } updateBorderLayerColor() @@ -598,11 +612,11 @@ extension TabBarViewItem: MouseClickViewDelegate { extension TabBarViewItem { enum Height: CGFloat { - case standard = 32 + case standard = 34 } enum Width: CGFloat { - case minimum = 50 + case minimum = 56 case minimumSelected = 120 case maximum = 240 } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.xib b/DuckDuckGo/TabBar/View/TabBarViewItem.xib index 97b8979631..1be0472238 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.xib +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.xib @@ -14,12 +14,13 @@ @@ -41,10 +42,10 @@ - + - + @@ -55,8 +56,8 @@ - + - + @@ -125,7 +125,7 @@ - + @@ -133,6 +133,7 @@ + @@ -144,7 +145,6 @@ - @@ -158,7 +158,7 @@ - + From 5c96aab4bdd389a3bd3f8cee12bb95c2a0d4a399 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 21 Feb 2024 09:46:53 -0300 Subject: [PATCH 14/19] Revert back bouncing normal tab --- .../PinnedTabs/View/PinnedTabView.swift | 2 +- .../TabBar/View/TabBarViewController.swift | 7 +++-- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 28 ++++++++++--------- DuckDuckGo/TabBar/View/TabBarViewItem.xib | 18 ++++++------ 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 5775673c9a..09da70e6ad 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -22,7 +22,7 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { static let dimension: CGFloat = 34 - static let cornerRadius: CGFloat = 8.5 + static let cornerRadius: CGFloat = 8 } @ObservedObject var model: Tab diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index c00b5854a0..4b8b49cda0 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -473,14 +473,14 @@ final class TabBarViewController: NSViewController { return numberOfItems } - private func currentTabWidth(selected: Bool = false, removedIndex: Int? = nil) -> CGFloat { + private func currentTabWidth(selected: Bool = false, isMuted: Bool = false, removedIndex: Int? = nil) -> CGFloat { let numberOfItems = CGFloat(self.layoutNumberOfItems(removedIndex: removedIndex)) guard numberOfItems > 0 else { return 0 } let tabsWidth = scrollView.bounds.width - footerCurrentWidthDimension - let minimumWidth = selected ? TabBarViewItem.Width.minimumSelected.rawValue : TabBarViewItem.Width.minimum.rawValue + let minimumWidth = selected ? TabBarViewItem.Width.minimumSelected.rawValue : (isMuted ? TabBarViewItem.Width.minumumMuted.rawValue : TabBarViewItem.Width.minimum.rawValue) if tabMode == .divided { var dividedWidth = tabsWidth / numberOfItems @@ -795,7 +795,8 @@ extension TabBarViewController: NSCollectionViewDelegateFlowLayout { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { let isItemSelected = tabCollectionViewModel.selectionIndex == .unpinned(indexPath.item) - return NSSize(width: self.currentTabWidth(selected: isItemSelected), height: TabBarViewItem.Height.standard.rawValue) + let isMuted = tabCollectionViewModel.tabCollection.tabs[indexPath.item].audioState == .muted + return NSSize(width: self.currentTabWidth(selected: isItemSelected, isMuted: isMuted), height: TabBarViewItem.Height.standard.rawValue) } } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 4f60c8c42f..b31300da36 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -56,8 +56,8 @@ final class TabBarViewItem: NSCollectionViewItem { static let textFieldPadding: CGFloat = 28 static let textFieldPaddingNoFavicon: CGFloat = 12 static let textFieldPaddingMuteIconPresent: CGFloat = 48 -// static let faviconLeadingPadding: CGFloat = 7 -// static let faviconLeadingPaddingMuteIconPresent: CGFloat = 7 + static let faviconLeadingPadding: CGFloat = 5 + static let faviconLeadingPaddingMuteIconPresent: CGFloat = 7 } var widthStage: WidthStage { @@ -278,7 +278,7 @@ final class TabBarViewItem: NSCollectionViewItem { layer.borderWidth = TabShadowConfig.dividerSize layer.opacity = TabShadowConfig.alpha layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner] - layer.cornerRadius = 11 + layer.cornerRadius = 7 layer.mask = layerMask return layer }() @@ -330,7 +330,7 @@ final class TabBarViewItem: NSCollectionViewItem { private func setupView() { view.wantsLayer = true - view.layer?.cornerRadius = 11 + view.layer?.cornerRadius = 7 view.layer?.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] view.layer?.masksToBounds = true view.layer?.addSublayer(borderLayer) @@ -353,19 +353,20 @@ final class TabBarViewItem: NSCollectionViewItem { updateSeparatorView() permissionCloseButtonTrailingConstraint.isActive = !closeButton.isHidden titleTextField.isHidden = widthStage.isTitleHidden && faviconImageView.image != nil -// faviconWrapperViewLeadingConstraint.constant = Constants.faviconLeadingPadding if mutedTabIcon.isHidden { faviconWrapperViewCenterConstraint.priority = titleTextField.isHidden ? .defaultHigh : .defaultLow faviconWrapperViewLeadingConstraint.priority = titleTextField.isHidden ? .defaultLow : .defaultHigh } else { // When the mute icon is visible and the tab is compressed we need to center both -// faviconWrapperViewCenterConstraint.priority = .defaultLow -// faviconWrapperViewLeadingConstraint.priority = .defaultHigh -// -// if titleTextField.isHidden { // If the title text is hidden it means the tab is compressed -// faviconWrapperViewLeadingConstraint.constant = Constants.faviconLeadingPaddingMuteIconPresent -// } + faviconWrapperViewCenterConstraint.priority = .defaultLow + faviconWrapperViewLeadingConstraint.priority = .defaultHigh + + if titleTextField.isHidden { // If the title text is hidden it means the tab is compressed + faviconWrapperViewLeadingConstraint.constant = Constants.faviconLeadingPaddingMuteIconPresent + } else { + faviconWrapperViewLeadingConstraint.constant = Constants.faviconLeadingPadding + } } updateBorderLayerColor() @@ -378,7 +379,7 @@ final class TabBarViewItem: NSCollectionViewItem { // Adjust colors for burner window if isBurner && faviconImageView.image === TabViewModel.Favicon.burnerHome { - faviconImageView.contentTintColor = .textColor + faviconImageView.contentTintColor = .textColor } else { faviconImageView.contentTintColor = nil } @@ -612,11 +613,12 @@ extension TabBarViewItem: MouseClickViewDelegate { extension TabBarViewItem { enum Height: CGFloat { - case standard = 34 + case standard = 32 } enum Width: CGFloat { case minimum = 50 + case minumumMuted = 56 case minimumSelected = 120 case maximum = 240 } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.xib b/DuckDuckGo/TabBar/View/TabBarViewItem.xib index 3902770062..a3c41007b8 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.xib +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.xib @@ -10,11 +10,11 @@ - + - + @@ -31,7 +31,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -61,7 +61,7 @@ - + @@ -125,7 +125,7 @@ - + From 1d522232e9a8b279997ee5ddc11a209d53a61299 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 21 Feb 2024 10:34:25 -0300 Subject: [PATCH 15/19] Fix compressed tabs bouncing --- .../TabBar/View/Base.lproj/TabBar.storyboard | 21 +++++++++---------- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard index 2bf37ba9b2..ff559dfdfe 100644 --- a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard +++ b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -62,7 +62,7 @@ - +