Skip to content

Commit

Permalink
Mute/unmute tab (#2019)
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira authored Feb 22, 2024
1 parent 7c2e3e4 commit 3dad84b
Show file tree
Hide file tree
Showing 19 changed files with 427 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Audio-Mute-12.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
12 changes: 12 additions & 0 deletions DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Audio-12.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
48 changes: 48 additions & 0 deletions DuckDuckGo/Common/Extensions/WKWebViewExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ extension WKWebView {
return false
}

enum AudioState {
case muted
case unmuted
case notSupported
}

enum CaptureState {
case none
case active
Expand Down Expand Up @@ -129,6 +135,48 @@ extension WKWebView {
}
#endif

func muteOrUnmute() {
#if !APPSTORE
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)
#endif
}

/// 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 {
#if APPSTORE
return .notSupported
#else
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
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 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")
Expand Down
24 changes: 24 additions & 0 deletions DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -5092,6 +5092,18 @@
}
}
},
"mute.tab" : {
"comment" : "Menu item. Mute tab",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Mute Tab"
}
}
}
},
"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",
Expand Down Expand Up @@ -9100,6 +9112,18 @@
}
}
},
"unmute.tab" : {
"comment" : "Menu item. Unmute tab",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Unmute Tab"
}
}
}
},
"unpin.tab" : {
"comment" : "Menu item. Unpin as a verb",
"extractionState" : "extracted_with_value",
Expand Down
27 changes: 27 additions & 0 deletions DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject {
didSet {
if let selectedItem = selectedItem {
selectedItemIndex = items.firstIndex(of: selectedItem)
updateTabAudioState(tab: selectedItem)
} else {
selectedItemIndex = nil
}
Expand All @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject {
didSet {
if let hoveredItem = hoveredItem {
hoveredItemIndex = items.firstIndex(of: hoveredItem)
updateTabAudioState(tab: hoveredItem)
} else {
hoveredItemIndex = nil
}
Expand All @@ -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<Tab> = []

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -168,4 +190,9 @@ extension PinnedTabsViewModel {
func removeFireproofing(_ tab: Tab) {
contextMenuActionSubject.send(.removeFireproofing(tab))
}

func muteOrUmute(_ tab: Tab) {
contextMenuActionSubject.send(.muteOrUnmute(tab))
updateTabAudioState(tab: tab)
}
}
50 changes: 43 additions & 7 deletions DuckDuckGo/PinnedTabs/View/PinnedTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 10
}

@ObservedObject var model: Tab
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -163,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

Expand All @@ -187,23 +198,48 @@ struct PinnedTabInnerView: View {
.frame(width: PinnedTabView.Const.dimension)
}

@ViewBuilder
var mutedTabIndicator: some View {
switch model.audioState {
case .muted:
ZStack {
Circle()
.stroke(Color.gray.opacity(0.5), lineWidth: 0.5)
.background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor")))
.frame(width: 16, height: 16)
Image("Audio-Mute")
.resizable()
.renderingMode(.template)
.frame(width: 12, height: 12)
}.offset(x: 8, y: -8)
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()
.foregroundColor(.forString(eTLDplus1))
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
}
}
}
}
9 changes: 9 additions & 0 deletions DuckDuckGo/Tab/Model/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker {
}
#endif

self.audioState = webView.audioState()
addDeallocationChecks(for: webView)
}

Expand Down Expand Up @@ -934,6 +935,14 @@ protocol NewWindowPolicyDecisionMaker {
}
}

@Published private(set) var audioState: WKWebView.AudioState = .notSupported

func muteUnmuteTab() {
webView.muteOrUnmute()

audioState = webView.audioState()
}

@MainActor(unsafe)
@discardableResult
private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? {
Expand Down
Loading

0 comments on commit 3dad84b

Please sign in to comment.