diff --git a/MultiSoundChanger.xcodeproj/project.pbxproj b/MultiSoundChanger.xcodeproj/project.pbxproj index 75c9711..8fa21bf 100644 --- a/MultiSoundChanger.xcodeproj/project.pbxproj +++ b/MultiSoundChanger.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4743EFAA1E91493B0032F5AA /* AppDelegate.swift */; }; - 6985C6FE251951F8003C2FDB /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6985C6FD251951F8003C2FDB /* OSD.framework */; }; E4FFDC0757FD125F92CC0F62 /* Pods_MultiSoundChanger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */; }; F312C54E25B3741C00205846 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F373D8C02561D24600642274 /* Main.storyboard */; }; F312C55025B3742200205846 /* Volume.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F373D8BC2561D22000642274 /* Volume.storyboard */; }; @@ -16,6 +15,7 @@ F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B02561D1A600642274 /* StatusBarController.swift */; }; F373D8B42561D1A600642274 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B12561D1A600642274 /* AudioManager.swift */; }; F373D8B52561D1A600642274 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B22561D1A600642274 /* MediaManager.swift */; }; + AAA1B0C0D0E0F0A0B0C0D001 /* VolumeHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */; }; F373D8BB2561D21900642274 /* Stories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8BA2561D21900642274 /* Stories.swift */; }; F373D8BF2561D22000642274 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8BD2561D22000642274 /* VolumeViewController.swift */; }; F373D8C62561D2A600642274 /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8C52561D2A600642274 /* Audio.swift */; }; @@ -33,13 +33,13 @@ 4743EFA71E91493B0032F5AA /* MultiSoundChanger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MultiSoundChanger.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4743EFAA1E91493B0032F5AA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MultiSoundChanger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6985C6FD251951F8003C2FDB /* OSD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OSD.framework; sourceTree = ""; }; 6FD0ED04AFD1CC1242C9B3B3 /* Pods-MultiSoundChanger.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultiSoundChanger.debug.xcconfig"; path = "Target Support Files/Pods-MultiSoundChanger/Pods-MultiSoundChanger.debug.xcconfig"; sourceTree = ""; }; D184B2CD842B856AFFE7DF7E /* Pods-MultiSoundChanger.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultiSoundChanger.release.xcconfig"; path = "Target Support Files/Pods-MultiSoundChanger/Pods-MultiSoundChanger.release.xcconfig"; sourceTree = ""; }; F3433FCA25B36E16009AAE86 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; F373D8B02561D1A600642274 /* StatusBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarController.swift; sourceTree = ""; }; F373D8B12561D1A600642274 /* AudioManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; F373D8B22561D1A600642274 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = ""; }; + AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeHUD.swift; sourceTree = ""; }; F373D8BA2561D21900642274 /* Stories.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stories.swift; sourceTree = ""; }; F373D8BC2561D22000642274 /* Volume.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Volume.storyboard; sourceTree = ""; }; F373D8BD2561D22000642274 /* VolumeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeViewController.swift; sourceTree = ""; }; @@ -62,7 +62,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6985C6FE251951F8003C2FDB /* OSD.framework in Frameworks */, E4FFDC0757FD125F92CC0F62 /* Pods_MultiSoundChanger.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -127,7 +126,6 @@ 83889335DD9089B748A33010 /* Frameworks */ = { isa = PBXGroup; children = ( - 6985C6FD251951F8003C2FDB /* OSD.framework */, 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */, ); name = Frameworks; @@ -161,6 +159,7 @@ F373D8B02561D1A600642274 /* StatusBarController.swift */, F373D8B12561D1A600642274 /* AudioManager.swift */, F373D8B22561D1A600642274 /* MediaManager.swift */, + AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */, ); path = Classes; sourceTree = ""; @@ -367,6 +366,7 @@ F373D8C62561D2A600642274 /* Audio.swift in Sources */, F37C2ED1256AAA4C001C3D36 /* Strings.swift in Sources */, F373D8B52561D1A600642274 /* MediaManager.swift in Sources */, + AAA1B0C0D0E0F0A0B0C0D001 /* VolumeHUD.swift in Sources */, F373D8B42561D1A600642274 /* AudioManager.swift in Sources */, F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */, 4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */, @@ -442,7 +442,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -497,7 +497,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -514,14 +514,14 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", ); INFOPLIST_FILE = MultiSoundChanger/Other/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.rlxone.multisoundchanger; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -540,14 +540,14 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", ); INFOPLIST_FILE = MultiSoundChanger/Other/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.rlxone.multisoundchanger; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h b/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h index 6b74749..a31bfe4 100644 --- a/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h +++ b/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h @@ -8,6 +8,5 @@ #define MultiSoundChanger_Bridging_Header_h #import -#import #endif /* MultiSoundChanger_Bridging_Header_h */ diff --git a/MultiSoundChanger/Sources/Classes/MediaManager.swift b/MultiSoundChanger/Sources/Classes/MediaManager.swift index b3a4799..cee9c30 100644 --- a/MultiSoundChanger/Sources/Classes/MediaManager.swift +++ b/MultiSoundChanger/Sources/Classes/MediaManager.swift @@ -43,31 +43,7 @@ final class MediaManagerImpl: MediaManager { } func showOSD(volume: Float, chicletsCount: Int = 16) { - guard let manager = OSDManager.sharedManager() as? OSDManager else { - return - } - - let mouseloc: NSPoint = NSEvent.mouseLocation - var displayForPoint: CGDirectDisplayID = 0 - var count: UInt32 = 0 - - if CGGetDisplaysWithPoint(mouseloc, 1, &displayForPoint, &count) != .success { - Logger.warning(Constants.InnerMessages.getDisplayError) - displayForPoint = CGMainDisplayID() - } - - let image = (volume == 0) ? OSDGraphicSpeakerMuted.rawValue : OSDGraphicSpeaker.rawValue - let volumeStep: Float = 100 / Float(chicletsCount) - - manager.showImage( - Int64(image), - onDisplayID: displayForPoint, - priority: 0x1F4, - msecUntilFade: 1_000, - filledChiclets: UInt32(volume / volumeStep), - totalChiclets: UInt32(100.0 / volumeStep), - locked: false - ) + VolumeHUD.shared.show(volume: volume, muted: volume == 0) } // MARK: Private diff --git a/MultiSoundChanger/Sources/Classes/VolumeHUD.swift b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift new file mode 100644 index 0000000..8bedb26 --- /dev/null +++ b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift @@ -0,0 +1,171 @@ +// +// VolumeHUD.swift +// MultiSoundChanger +// + +import Cocoa + +final class VolumeHUD { + static let shared = VolumeHUD() + + private var window: NSWindow? + private var hideWorkItem: DispatchWorkItem? + private let chicletCount = 16 + + private init() {} + + func show(volume: Float, muted: Bool) { + DispatchQueue.main.async { [weak self] in + self?.present(volume: volume, muted: muted) + } + } + + private func present(volume: Float, muted: Bool) { + let win = window ?? makeWindow() + window = win + + let view = win.contentView as? HUDView + view?.update(volume: volume, muted: muted, chicletCount: chicletCount) + + positionWindow(win) + win.orderFrontRegardless() + + hideWorkItem?.cancel() + let item = DispatchWorkItem { [weak self] in + self?.fadeOut() + } + hideWorkItem = item + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2, execute: item) + + win.alphaValue = 1.0 + } + + private func fadeOut() { + guard let win = window else { return } + NSAnimationContext.runAnimationGroup({ ctx in + ctx.duration = 0.3 + win.animator().alphaValue = 0.0 + }, completionHandler: { [weak self] in + self?.window?.orderOut(nil) + }) + } + + private func makeWindow() -> NSWindow { + let size = NSSize(width: 220, height: 220) + let w = NSWindow( + contentRect: NSRect(origin: .zero, size: size), + styleMask: .borderless, + backing: .buffered, + defer: false + ) + w.isOpaque = false + w.backgroundColor = .clear + w.level = .statusBar + w.ignoresMouseEvents = true + w.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary] + w.hasShadow = false + w.contentView = HUDView(frame: NSRect(origin: .zero, size: size)) + return w + } + + private func positionWindow(_ win: NSWindow) { + let mouseLoc = NSEvent.mouseLocation + let screen = NSScreen.screens.first { NSMouseInRect(mouseLoc, $0.frame, false) } ?? NSScreen.main + guard let screen = screen else { return } + let frame = screen.frame + let size = win.frame.size + let origin = NSPoint( + x: frame.midX - size.width / 2, + y: frame.minY + 140 + ) + win.setFrameOrigin(origin) + } +} + +// MARK: - HUD View + +final class HUDView: NSView { + private var volume: Float = 0 + private var muted: Bool = false + private var chicletCount: Int = 16 + + override init(frame: NSRect) { + super.init(frame: frame) + wantsLayer = true + } + + required init?(coder: NSCoder) { super.init(coder: coder) } + + func update(volume: Float, muted: Bool, chicletCount: Int) { + self.volume = volume + self.muted = muted + self.chicletCount = chicletCount + needsDisplay = true + } + + override var isFlipped: Bool { false } + + override func draw(_ dirtyRect: NSRect) { + // Rounded background panel + let panelRect = bounds.insetBy(dx: 0, dy: 0) + let panelPath = NSBezierPath(roundedRect: panelRect, xRadius: 22, yRadius: 22) + NSColor(white: 0.10, alpha: 0.86).setFill() + panelPath.fill() + + // Speaker icon + let iconRect = NSRect(x: bounds.midX - 32, y: bounds.maxY - 90, width: 64, height: 64) + let speakerName: String = muted ? "speaker.slash.fill" : (volume < 1 ? "speaker.fill" : (volume < 50 ? "speaker.wave.1.fill" : (volume < 80 ? "speaker.wave.2.fill" : "speaker.wave.3.fill"))) + if let img = NSImage(systemSymbolName: speakerName, accessibilityDescription: nil) { + let conf = NSImage.SymbolConfiguration(pointSize: 44, weight: .regular) + let configured = img.withSymbolConfiguration(conf) ?? img + configured.isTemplate = true + let tinted = NSImage(size: configured.size, flipped: false) { rect in + configured.draw(in: rect) + NSColor.white.set() + rect.fill(using: .sourceAtop) + return true + } + tinted.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0, + respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high.rawValue]) + } + + // Chiclets + let chicletAreaWidth: CGFloat = 180 + let chicletAreaHeight: CGFloat = 14 + let originX = bounds.midX - chicletAreaWidth / 2 + let originY: CGFloat = 50 + let gap: CGFloat = 2 + let totalGap = gap * CGFloat(chicletCount - 1) + let chicletWidth = (chicletAreaWidth - totalGap) / CGFloat(chicletCount) + let step = 100.0 / Float(chicletCount) + let filled = Int((volume / step).rounded()) + + for i in 0.. - + + + + + + + + + - + + + + + diff --git a/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift b/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift index e1861c7..fcf980d 100644 --- a/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift +++ b/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift @@ -12,6 +12,7 @@ import MediaKeyTap final class VolumeViewController: NSViewController { @IBOutlet weak var volumeSlider: NSSlider! + @IBOutlet weak var volumeLabel: NSTextField! private var muted: Bool = false weak var statusBarController: StatusBarController? @@ -22,11 +23,14 @@ final class VolumeViewController: NSViewController { } func updateSliderVolume(volume: Float) { - volumeSlider.floatValue = volume.clamped(to: 0...100) + let clamped = volume.clamped(to: 0...100) + volumeSlider.floatValue = clamped + volumeLabel?.stringValue = "\(Int(clamped))%" } @IBAction func volumeSliderAction(_ sender: Any) { changeDeviceVolume(value: volumeSlider.floatValue / 100) statusBarController?.changeStatusItemImage(value: volumeSlider.floatValue) + volumeLabel?.stringValue = "\(Int(volumeSlider.floatValue))%" } }