Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

190 lines
6.7 KiB
Swift

import CoreAudio
import Foundation
import OSLog
import SwiftUI
import MacToolsPluginKit
public final class SystemMutePluginFactory: NSObject, MacToolsPluginBundleFactory {
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
SystemMutePluginProvider(context: context)
}
}
@MainActor
private struct SystemMutePluginProvider: PluginProvider {
let context: PluginRuntimeContext
func makePlugins() -> [any MacToolsPlugin] {
[SystemMutePlugin(localization: PluginLocalization(bundle: context.resourceBundle))]
}
}
protocol SystemAudioControlling {
func readMuteState() -> Bool
func setMuteState(_ muted: Bool) -> Bool
}
struct CoreAudioSystemOutputController: SystemAudioControlling {
func readMuteState() -> Bool {
guard let deviceID = Self.defaultOutputDeviceID() else { return false }
return Self.getMuteState(deviceID: deviceID) ?? false
}
func setMuteState(_ muted: Bool) -> Bool {
guard let deviceID = Self.defaultOutputDeviceID() else { return false }
return Self.applyMuteState(deviceID: deviceID, muted: muted)
}
private static func defaultOutputDeviceID() -> AudioDeviceID? {
var deviceID = AudioDeviceID(kAudioObjectUnknown)
var size = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&address, 0, nil, &size, &deviceID
)
guard status == noErr, deviceID != kAudioObjectUnknown else { return nil }
return deviceID
}
private static func getMuteState(deviceID: AudioDeviceID) -> Bool? {
var mute: UInt32 = 0
var size = UInt32(MemoryLayout<UInt32>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyMute,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain
)
guard AudioObjectHasProperty(deviceID, &address) else { return nil }
let status = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &mute)
guard status == noErr else { return nil }
return mute != 0
}
private static func applyMuteState(deviceID: AudioDeviceID, muted: Bool) -> Bool {
var mute: UInt32 = muted ? 1 : 0
let size = UInt32(MemoryLayout<UInt32>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyMute,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain
)
var settable: DarwinBoolean = false
guard AudioObjectIsPropertySettable(deviceID, &address, &settable) == noErr,
settable.boolValue else {
return false
}
let status = AudioObjectSetPropertyData(deviceID, &address, 0, nil, size, &mute)
return status == noErr
}
}
@MainActor
final class SystemMutePlugin: MacToolsPlugin, PluginPrimaryPanel {
let metadata: PluginMetadata
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .switch,
menuActionBehavior: .keepPresented
)
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
private let logger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "cc.ggbond.mactools",
category: "SystemMutePlugin"
)
private let localization: PluginLocalization
private let controller: any SystemAudioControlling
private var isMuted: Bool = false
private var lastErrorMessage: String?
init(
controller: any SystemAudioControlling = CoreAudioSystemOutputController(),
localization: PluginLocalization = PluginLocalization(bundle: .main)
) {
self.localization = localization
self.controller = controller
self.metadata = PluginMetadata(
id: "system-mute",
title: localization.string("metadata.title", defaultValue: "系统静音"),
iconName: "speaker.slash",
iconTint: Color(nsColor: .systemOrange),
order: 48,
defaultDescription: localization.string(
"metadata.description",
defaultValue: "快速静音或恢复系统音频输出"
)
)
self.isMuted = controller.readMuteState()
}
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: isMuted
? localization.string("panel.subtitle.muted", defaultValue: "已静音")
: localization.string("panel.subtitle.unmuted", defaultValue: "未静音"),
isOn: isMuted,
isExpanded: false,
isEnabled: true,
isVisible: true,
detail: nil,
errorMessage: lastErrorMessage
)
}
var permissionRequirements: [PluginPermissionRequirement] { [] }
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
func refresh() {
let current = controller.readMuteState()
if current != isMuted {
isMuted = current
onStateChange?()
}
}
func deactivate(reason: PluginDeactivationReason) {
guard reason.requiresStateCleanup, isMuted else { return }
_ = controller.setMuteState(false)
isMuted = false
onStateChange?()
}
func handleAction(_ action: PluginPanelAction) {
guard case let .setSwitch(enable) = action else { return }
applyMute(enable)
}
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
func handlePermissionAction(id: String) {}
func handleSettingsAction(id: String) {}
func handleShortcutAction(id: String) {}
// MARK: - Private
private func applyMute(_ muted: Bool) {
let success = controller.setMuteState(muted)
if success {
isMuted = muted
lastErrorMessage = nil
} else {
logger.error("Failed to set system mute to \(muted, privacy: .public)")
lastErrorMessage = muted
? localization.string("error.muteFailed", defaultValue: "静音操作失败")
: localization.string("error.unmuteFailed", defaultValue: "取消静音失败")
}
onStateChange?()
}
}