Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mute/unmute tab #2019

Merged
merged 19 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that this has passed Design Review, but was this change expected?

The pinned tab item view now appears noticeably taller than a regular tab item view.

Screenshot 2024-02-19 at 13 06 02

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh actually it's still pending Design Review, so maybe this comment is legit after all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This was fixed in the latest version I’ve just pushed. I’m going to ask Bryan for a final review.

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
Loading