10 Commits

15 changed files with 2812 additions and 202 deletions

View File

@ -1,33 +1,51 @@
using CoreAudio; using FocusVolumeControl.AudioSessions;
using FocusVolumeControl.AudioSessions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
namespace FocusVolumeControl; namespace FocusVolumeControl;
public class AudioHelper public class AudioHelper
{ {
IAudioSession _current; static object _lock = new object();
List<Process> _currentProcesses; List<Process> _currentProcesses;
public IAudioSession Current { get; private set; }
public void ResetCache()
{
lock (_lock)
{
Current = null;
}
}
public IAudioSession FindSession(List<Process> processes) public IAudioSession FindSession(List<Process> processes)
{ {
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
using var manager = device.AudioSessionManager2;
var sessions = manager.Sessions; Guid iid = typeof(IAudioSessionManager2).GUID;
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
var manager = (IAudioSessionManager2)m;
var matchingSession = new ActiveAudioSessionWrapper();
foreach (var session in sessions) manager.GetSessionEnumerator(out var sessionEnumerator);
var results = new ActiveAudioSessionWrapper();
sessionEnumerator.GetCount(out var count);
for (int i = 0; i < count; i++)
{ {
var audioProcess = Process.GetProcessById((int)session.ProcessID); sessionEnumerator.GetSession(i, out var session);
if (processes.Any(x => x.Id == session.ProcessID || x.ProcessName == audioProcess?.ProcessName)) session.GetProcessId(out var sessionProcessId);
var audioProcess = Process.GetProcessById(sessionProcessId);
if (processes.Any(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName))
{ {
try try
{ {
@ -36,23 +54,24 @@ public class AudioHelper
{ {
displayName = audioProcess.ProcessName; displayName = audioProcess.ProcessName;
} }
matchingSession.DisplayName = displayName; results.DisplayName = displayName;
} }
catch catch
{ {
matchingSession.DisplayName ??= audioProcess.ProcessName; results.DisplayName ??= audioProcess.ProcessName;
} }
matchingSession.ExecutablePath ??= audioProcess.MainModule.FileName; results.ExecutablePath ??= audioProcess.MainModule.FileName;
//some apps like discord have multiple volume processes. //some apps like discord have multiple volume processes.
matchingSession.AddVolume(session.SimpleAudioVolume); results.AddSession(session);
} }
} }
return matchingSession.Any() ? matchingSession : null;
}
static object _lock = new object(); return results.Any() ? results : null;
}
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior) public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
{ {
@ -62,23 +81,23 @@ public class AudioHelper
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes)) if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
{ {
_current = FindSession(processes); Current = FindSession(processes);
} }
if(_current == null) if (Current == null)
{ {
if(fallbackBehavior == FallbackBehavior.SystemSounds) if (fallbackBehavior == FallbackBehavior.SystemSounds && Current is not SystemSoundsAudioSession)
{ {
_current = GetSystemSounds(); Current = GetSystemSounds();
} }
else if(fallbackBehavior == FallbackBehavior.SystemVolume) else if (fallbackBehavior == FallbackBehavior.SystemVolume && Current is not SystemVolumeAudioSession)
{ {
_current = GetSystemVolume(); Current = GetSystemVolume();
} }
} }
_currentProcesses = processes; _currentProcesses = processes;
return _current; return Current;
} }
} }
@ -134,47 +153,65 @@ public class AudioHelper
public void ResetAll() public void ResetAll()
{ {
try var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
{
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
using var manager = device.AudioSessionManager2;
foreach (var session in manager.Sessions) Guid iid = typeof(IAudioSessionManager2).GUID;
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
var manager = (IAudioSessionManager2)m;
manager.GetSessionEnumerator(out var sessionEnumerator);
sessionEnumerator.GetCount(out var count);
for (int i = 0; i < count; i++)
{ {
session.SimpleAudioVolume.MasterVolume = 1; sessionEnumerator.GetSession(i, out var session);
session.SimpleAudioVolume.Mute = false;
var volume = (ISimpleAudioVolume)session;
var guid = Guid.Empty;
volume.SetMasterVolume(1, ref guid);
volume.SetMute(false, ref guid);
} }
} }
catch { }
}
public IAudioSession GetSystemSounds() public IAudioSession GetSystemSounds()
{ {
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
using var manager = device.AudioSessionManager2;
var sessions = manager.Sessions; Guid iid = typeof(IAudioSessionManager2).GUID;
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
var manager = (IAudioSessionManager2)m;
foreach (var session in sessions)
manager.GetSessionEnumerator(out var sessionEnumerator);
sessionEnumerator.GetCount(out var count);
for (int i = 0; i < count; i++)
{ {
if (session.IsSystemSoundsSession) sessionEnumerator.GetSession(i, out var session);
if (session.IsSystemSoundsSession() == 0)
{ {
return new SystemSoundsAudioSession(session.SimpleAudioVolume); return new SystemSoundsAudioSession(session);
} }
} }
return null; return null;
} }
public IAudioSession GetSystemVolume() public IAudioSession GetSystemVolume()
{ {
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
return new SystemVolumeAudioSession(device.AudioEndpointVolume);
Guid iid = typeof(IAudioEndpointVolume).GUID;
device.Activate(ref iid, 0, IntPtr.Zero, out var o);
var endpointVolume = (IAudioEndpointVolume)o;
return new SystemVolumeAudioSession(endpointVolume);
} }
} }

View File

@ -1,17 +1,18 @@
using CoreAudio; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BarRaider.SdTools; using BarRaider.SdTools;
using System.Drawing; using System.Drawing;
using System.Runtime.InteropServices;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
public class ActiveAudioSessionWrapper : IAudioSession public sealed class ActiveAudioSessionWrapper : IAudioSession
{ {
public string DisplayName { get; set; } public string DisplayName { get; set; }
public string ExecutablePath { get; set; } public string ExecutablePath { get; set; }
private List<SimpleAudioVolume> Volume { get; } = new List<SimpleAudioVolume>(); private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
string _icon; string _icon;
@ -36,11 +37,11 @@ public class ActiveAudioSessionWrapper : IAudioSession
{ {
return Volume.Any(); return Volume.Any();
} }
public int Count => Volume.Count; public int Count => Sessions.Count;
public void AddVolume(SimpleAudioVolume volume) public void AddSession(IAudioSessionControl2 session)
{ {
Volume.Add(volume); Sessions.Add(session);
} }
public void ToggleMute() public void ToggleMute()
@ -51,32 +52,52 @@ public class ActiveAudioSessionWrapper : IAudioSession
//when any volumes are unmuted, Volume.All will return false //when any volumes are unmuted, Volume.All will return false
//so we set muted to true (opposite of Volume.All) //so we set muted to true (opposite of Volume.All)
var muted = Volume.All(x => x.Mute); var muted = IsMuted();
Volume.ForEach(x => x.Mute = !muted); foreach(var v in Volume)
{
var guid = Guid.Empty;
v.SetMute(!muted, ref guid);
}
} }
public bool IsMuted() public bool IsMuted()
{ {
return Volume.All(x => x.Mute); return Volume.All(x =>
{
x.GetMute(out var mute);
return mute;
});
} }
public void IncrementVolumeLevel(int step, int ticks) public void IncrementVolumeLevel(int step, int ticks)
{ {
//if you have more than one volume. they will all get set based on the first volume control //if you have more than one volume. they will all get set based on the first volume control
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0; var volume = Volume.FirstOrDefault();
var level = 0f;
if (volume != null)
{
volume.GetMasterVolume(out level);
}
level += (0.01f * step) * ticks; level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
level = Math.Max(level, 0);
level = Math.Min(level, 1);
Volume.ForEach(x => x.MasterVolume = level); foreach(var v in Volume)
{
var guid = Guid.Empty;
v.SetMasterVolume(level, ref guid);
}
} }
public int GetVolumeLevel() public int GetVolumeLevel()
{ {
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0; var volume = Volume.FirstOrDefault();
return (int)(level * 100); var level = 0f;
if(volume != null)
{
volume.GetMasterVolume(out level);
} }
return VolumeHelpers.GetVolumePercentage(level);
}
} }

View File

@ -0,0 +1,306 @@
using System;
using System.Runtime.InteropServices;
namespace FocusVolumeControl.AudioSessions;
[ComImport]
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
internal class MMDeviceEnumerator
{
}
internal enum EDataFlow
{
eRender,
eCapture,
eAll,
EDataFlow_enum_count
}
internal enum ERole
{
eConsole,
eMultimedia,
eCommunications,
ERole_enum_count
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface CoreAudio
{
int NotImpl1();
[PreserveSig]
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMMDevice
{
[PreserveSig]
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
}
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IAudioSessionManager2
{
int NotImpl1();
int NotImpl2();
[PreserveSig]
int GetSessionEnumerator(out IAudioSessionEnumerator SessionEnum);
}
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IAudioSessionEnumerator
{
[PreserveSig]
int GetCount(out int SessionCount);
[PreserveSig]
int GetSession(int SessionCount, out IAudioSessionControl2 Session);
}
[Guid("87CE5498-68D6-44E5-9215-6DA47EF883D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface ISimpleAudioVolume
{
[PreserveSig]
int SetMasterVolume(float fLevel, ref Guid EventContext);
[PreserveSig]
int GetMasterVolume(out float pfLevel);
[PreserveSig]
int SetMute(bool bMute, ref Guid EventContext);
[PreserveSig]
int GetMute(out bool pbMute);
}
[Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAudioSessionControl2
{
// IAudioSessionControl
[PreserveSig]
int NotImpl0();
[PreserveSig]
int GetDisplayName([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
[PreserveSig]
int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
[PreserveSig]
int GetIconPath([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
[PreserveSig]
int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
[PreserveSig]
int GetGroupingParam(out Guid pRetVal);
[PreserveSig]
int SetGroupingParam([MarshalAs(UnmanagedType.LPStruct)] Guid Override, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
[PreserveSig]
int NotImpl1();
[PreserveSig]
int NotImpl2();
// IAudioSessionControl2
[PreserveSig]
int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
[PreserveSig]
int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
[PreserveSig]
int GetProcessId(out int pRetVal);
[PreserveSig]
int IsSystemSoundsSession();
[PreserveSig]
int SetDuckingPreference(bool optOut);
}
// http://netcoreaudio.codeplex.com/SourceControl/latest#trunk/Code/CoreAudio/Interfaces/IAudioEndpointVolume.cs
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAudioEndpointVolume
{
[PreserveSig]
int NotImpl1();
[PreserveSig]
int NotImpl2();
/// <summary>
/// Gets a count of the channels in the audio stream.
/// </summary>
/// <param name="channelCount">The number of channels.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetChannelCount(
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 channelCount);
/// <summary>
/// Sets the master volume level of the audio stream, in decibels.
/// </summary>
/// <param name="level">The new master volume level in decibels.</param>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int SetMasterVolumeLevel(
[In][MarshalAs(UnmanagedType.R4)] float level,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Sets the master volume level, expressed as a normalized, audio-tapered value.
/// </summary>
/// <param name="level">The new master volume level expressed as a normalized value between 0.0 and 1.0.</param>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int SetMasterVolumeLevelScalar(
[In][MarshalAs(UnmanagedType.R4)] float level,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Gets the master volume level of the audio stream, in decibels.
/// </summary>
/// <param name="level">The volume level in decibels.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetMasterVolumeLevel(
[Out][MarshalAs(UnmanagedType.R4)] out float level);
/// <summary>
/// Gets the master volume level, expressed as a normalized, audio-tapered value.
/// </summary>
/// <param name="level">The volume level expressed as a normalized value between 0.0 and 1.0.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetMasterVolumeLevelScalar(
[Out][MarshalAs(UnmanagedType.R4)] out float level);
/// <summary>
/// Sets the volume level, in decibels, of the specified channel of the audio stream.
/// </summary>
/// <param name="channelNumber">The channel number.</param>
/// <param name="level">The new volume level in decibels.</param>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int SetChannelVolumeLevel(
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
[In][MarshalAs(UnmanagedType.R4)] float level,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Sets the normalized, audio-tapered volume level of the specified channel in the audio stream.
/// </summary>
/// <param name="channelNumber">The channel number.</param>
/// <param name="level">The new master volume level expressed as a normalized value between 0.0 and 1.0.</param>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int SetChannelVolumeLevelScalar(
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
[In][MarshalAs(UnmanagedType.R4)] float level,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Gets the volume level, in decibels, of the specified channel in the audio stream.
/// </summary>
/// <param name="channelNumber">The zero-based channel number.</param>
/// <param name="level">The volume level in decibels.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetChannelVolumeLevel(
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
[Out][MarshalAs(UnmanagedType.R4)] out float level);
/// <summary>
/// Gets the normalized, audio-tapered volume level of the specified channel of the audio stream.
/// </summary>
/// <param name="channelNumber">The zero-based channel number.</param>
/// <param name="level">The volume level expressed as a normalized value between 0.0 and 1.0.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetChannelVolumeLevelScalar(
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
[Out][MarshalAs(UnmanagedType.R4)] out float level);
/// <summary>
/// Sets the muting state of the audio stream.
/// </summary>
/// <param name="isMuted">True to mute the stream, or false to unmute the stream.</param>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int SetMute(
[In][MarshalAs(UnmanagedType.Bool)] Boolean isMuted,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Gets the muting state of the audio stream.
/// </summary>
/// <param name="isMuted">The muting state. True if the stream is muted, false otherwise.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetMute(
[Out][MarshalAs(UnmanagedType.Bool)] out Boolean isMuted);
/// <summary>
/// Gets information about the current step in the volume range.
/// </summary>
/// <param name="step">The current zero-based step index.</param>
/// <param name="stepCount">The total number of steps in the volume range.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetVolumeStepInfo(
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 step,
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 stepCount);
/// <summary>
/// Increases the volume level by one step.
/// </summary>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int VolumeStepUp(
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Decreases the volume level by one step.
/// </summary>
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int VolumeStepDown(
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
/// <summary>
/// Queries the audio endpoint device for its hardware-supported functions.
/// </summary>
/// <param name="hardwareSupportMask">A hardware support mask that indicates the capabilities of the endpoint.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int QueryHardwareSupport(
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 hardwareSupportMask);
/// <summary>
/// Gets the volume range of the audio stream, in decibels.
/// </summary>
/// <param name="volumeMin">The minimum volume level in decibels.</param>
/// <param name="volumeMax">The maximum volume level in decibels.</param>
/// <param name="volumeStep">The volume increment level in decibels.</param>
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
[PreserveSig]
int GetVolumeRange(
[Out][MarshalAs(UnmanagedType.R4)] out float volumeMin,
[Out][MarshalAs(UnmanagedType.R4)] out float volumeMax,
[Out][MarshalAs(UnmanagedType.R4)] out float volumeStep);
}

View File

@ -1,4 +1,6 @@
namespace FocusVolumeControl.AudioSessions; using System;
namespace FocusVolumeControl.AudioSessions;
public interface IAudioSession public interface IAudioSession
{ {

View File

@ -1,38 +1,46 @@
using CoreAudio; using System;
using System; using System.Runtime.InteropServices;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
internal class SystemSoundsAudioSession : IAudioSession internal sealed class SystemSoundsAudioSession : IAudioSession
{ {
public SystemSoundsAudioSession(SimpleAudioVolume volumeControl) public SystemSoundsAudioSession(IAudioSessionControl2 sessionControl)
{ {
_volumeControl = volumeControl; _sessionControl = sessionControl;
_volumeControl = (ISimpleAudioVolume)sessionControl;
} }
SimpleAudioVolume _volumeControl; IAudioSessionControl2 _sessionControl;
ISimpleAudioVolume _volumeControl;
public string DisplayName => "System sounds"; public string DisplayName => "System sounds";
public string GetIcon() => "Images/systemSounds"; public string GetIcon() => "Images/systemSounds";
public void ToggleMute() public void ToggleMute()
{ {
_volumeControl.Mute = !_volumeControl.Mute; var guid = Guid.Empty;
_volumeControl.SetMute(!IsMuted(), ref guid);
} }
public bool IsMuted() => _volumeControl.Mute; public bool IsMuted()
{
_volumeControl.GetMute(out var mute);
return mute;
}
public void IncrementVolumeLevel(int step, int ticks) public void IncrementVolumeLevel(int step, int ticks)
{ {
var level = _volumeControl.MasterVolume; _volumeControl.GetMasterVolume(out var level);
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
level += (0.01f * step) * ticks; var guid = Guid.Empty;
level = Math.Max(level, 0); _volumeControl.SetMasterVolume(level, ref guid);
level = Math.Min(level, 1);
_volumeControl.MasterVolume = level;
} }
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolume * 100); public int GetVolumeLevel()
{
_volumeControl.GetMasterVolume(out var level);
return VolumeHelpers.GetVolumePercentage(level);
}
} }

View File

@ -1,38 +1,41 @@
using CoreAudio; using System;
using System; using System.Runtime.InteropServices;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
internal class SystemVolumeAudioSession : IAudioSession internal sealed class SystemVolumeAudioSession : IAudioSession
{ {
public SystemVolumeAudioSession(AudioEndpointVolume volumeControl) public SystemVolumeAudioSession(IAudioEndpointVolume volumeControl)
{ {
_volumeControl = volumeControl; _volumeControl = volumeControl;
} }
AudioEndpointVolume _volumeControl; IAudioEndpointVolume _volumeControl;
public string DisplayName => "System Volume"; public string DisplayName => "System Volume";
public string GetIcon() => "Images/encoderIcon"; public string GetIcon() => "Images/encoderIcon";
public void ToggleMute() public void ToggleMute()
{ {
_volumeControl.Mute = !_volumeControl.Mute; _volumeControl.SetMute(!IsMuted(), Guid.Empty);
} }
public bool IsMuted() => _volumeControl.Mute; public bool IsMuted()
{
_volumeControl.GetMute(out var mute);
return mute;
}
public void IncrementVolumeLevel(int step, int ticks) public void IncrementVolumeLevel(int step, int ticks)
{ {
var level = _volumeControl.MasterVolumeLevelScalar; _volumeControl.GetMasterVolumeLevelScalar(out var level);
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
level += (0.01f * step) * ticks; _volumeControl.SetMasterVolumeLevelScalar(level, Guid.Empty);
level = Math.Max(level, 0);
level = Math.Min(level, 1);
_volumeControl.MasterVolumeLevelScalar = level;
} }
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolumeLevelScalar * 100); public int GetVolumeLevel()
{
_volumeControl.GetMasterVolumeLevelScalar(out var level);
return VolumeHelpers.GetVolumePercentage(level);
}
} }

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl.AudioSessions
{
internal class VolumeHelpers
{
public static float GetAdjustedVolume(float startingVolume, int step, int ticks)
{
var level = startingVolume;
level += 0.01f * step * ticks;
level = Math.Max(level, 0);
level = Math.Min(level, 1);
return level;
}
public static int GetVolumePercentage(float volume) => (int)Math.Round(volume * 100);
}
}

View File

@ -19,6 +19,9 @@ public class DialAction : EncoderBase
[JsonProperty("fallbackBehavior")] [JsonProperty("fallbackBehavior")]
public FallbackBehavior FallbackBehavior { get; set; } public FallbackBehavior FallbackBehavior { get; set; }
[JsonProperty("stepSize")]
public int StepSize { get; set; }
public static PluginSettings CreateDefaultSettings() public static PluginSettings CreateDefaultSettings()
{ {
PluginSettings instance = new PluginSettings(); PluginSettings instance = new PluginSettings();
@ -32,7 +35,6 @@ public class DialAction : EncoderBase
IntPtr _foregroundWindowChangedEvent; IntPtr _foregroundWindowChangedEvent;
Native.WinEventDelegate _delegate; Native.WinEventDelegate _delegate;
IAudioSession _currentAudioSession;
AudioHelper _audioHelper = new AudioHelper(); AudioHelper _audioHelper = new AudioHelper();
Thread _thread; Thread _thread;
@ -45,7 +47,7 @@ public class DialAction : EncoderBase
if (payload.Settings == null || payload.Settings.Count == 0) if (payload.Settings == null || payload.Settings.Count == 0)
{ {
settings = PluginSettings.CreateDefaultSettings(); settings = PluginSettings.CreateDefaultSettings();
SaveSettings(); _ = SaveSettings();
} }
else else
{ {
@ -66,79 +68,8 @@ public class DialAction : EncoderBase
_thread.SetApartmentState(ApartmentState.STA); _thread.SetApartmentState(ApartmentState.STA);
_thread.Start(); _thread.Start();
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
_currentAudioSession = settings.FallbackBehavior == FallbackBehavior.SystemSounds ? _audioHelper.GetSystemSounds() : _audioHelper.GetSystemVolume(); _ = UpdateStateIfNeeded(session);
}
public override async void DialDown(DialPayload payload)
{
//dial pressed down
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
await ToggleMuteAsync();
}
public override async void TouchPress(TouchpadPressPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
if (payload.IsLongPress)
{
_audioHelper.ResetAll();
}
else
{
await ToggleMuteAsync();
}
}
async Task ToggleMuteAsync()
{
try
{
if (_currentAudioSession != null)
{
_currentAudioSession.ToggleMute();
await UpdateStateIfNeeded();
}
else
{
await Connection.ShowAlert();
}
}
catch (Exception ex)
{
await Connection.ShowAlert();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
}
}
public override async void DialRotate(DialRotatePayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
//dial rotated. ticks positive for right, negative for left
try
{
if (_currentAudioSession != null)
{
_currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
await UpdateStateIfNeeded();
}
else
{
await Connection.ShowAlert();
}
}
catch (Exception ex)
{
await Connection.ShowAlert();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
}
}
public override void DialUp(DialPayload payload)
{
//dial unpressed
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Up");
} }
public override void Dispose() public override void Dispose()
@ -151,25 +82,126 @@ public class DialAction : EncoderBase
_dispatcher.InvokeShutdown(); _dispatcher.InvokeShutdown();
} }
public override async void DialDown(DialPayload payload)
{
try
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
await ToggleMuteAsync();
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
}
}
public override void DialUp(DialPayload payload) { }
public override async void TouchPress(TouchpadPressPayload payload)
{
try
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
if (payload.IsLongPress)
{
await ResetAllAsync();
}
else
{
await ToggleMuteAsync();
}
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in TouchPress:\n {ex}");
}
}
public override async void DialRotate(DialRotatePayload payload)
{
try
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
//dial rotated. ticks positive for right, negative for left
var activeSession = _audioHelper.Current;
if (activeSession != null)
{
activeSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
await UpdateStateIfNeeded(activeSession);
}
else
{
await Connection.ShowAlert();
}
}
catch (Exception ex)
{
_audioHelper.ResetCache();
await Connection.ShowAlert();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to increment volume:\n {ex}");
}
}
async Task ResetAllAsync()
{
try
{
_audioHelper.ResetAll();
}
catch
{
_audioHelper.ResetCache();
await Connection.ShowAlert();
throw;
}
}
async Task ToggleMuteAsync()
{
try
{
var activeSession = _audioHelper.Current;
if (activeSession != null)
{
activeSession.ToggleMute();
await UpdateStateIfNeeded(activeSession);
}
else
{
await Connection.ShowAlert();
}
}
catch
{
_audioHelper.ResetCache();
await Connection.ShowAlert();
throw;
}
}
public override async void OnTick() public override async void OnTick()
{
try
{ {
//called once every 1000ms and can be used for updating the title/image of the key //called once every 1000ms and can be used for updating the title/image of the key
var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior); var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
if(activeSession != null) await UpdateStateIfNeeded(activeSession);
}
catch (Exception ex)
{ {
_currentAudioSession = activeSession; _audioHelper.ResetCache();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on Tick:\n {ex}");
}
} }
await UpdateStateIfNeeded(); private async Task UpdateStateIfNeeded(IAudioSession audioSession)
}
private async Task UpdateStateIfNeeded()
{ {
if (_currentAudioSession != null) try
{
if (audioSession != null)
{ {
var uiState = new UIState(_currentAudioSession); var uiState = new UIState(audioSession);
if (_previousState != null && uiState != null && if (_previousState != null && uiState != null &&
uiState.Title == _previousState.Title && uiState.Title == _previousState.Title &&
@ -188,6 +220,11 @@ public class DialAction : EncoderBase
_previousState = uiState; _previousState = uiState;
} }
} }
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Failed to update screen\n {ex}");
}
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
@ -196,21 +233,40 @@ public class DialAction : EncoderBase
public override void ReceivedSettings(ReceivedSettingsPayload payload) public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
try
{ {
Tools.AutoPopulateSettings(settings, payload.Settings); Tools.AutoPopulateSettings(settings, payload.Settings);
SaveSettings(); _ = SaveSettings();
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
}
} }
private Task SaveSettings() private async Task SaveSettings()
{ {
return Connection.SetSettingsAsync(JObject.FromObject(settings)); try
{
await Connection.SetSettingsAsync(JObject.FromObject(settings));
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
}
} }
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
try
{ {
OnTick(); OnTick();
} }
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
}
}
} }

View File

@ -56,6 +56,8 @@
<ItemGroup> <ItemGroup>
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" /> <Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
<Compile Include="AudioHelper.cs" /> <Compile Include="AudioHelper.cs" />
<Compile Include="AudioSessions\CoreAudio.cs" />
<Compile Include="AudioSessions\VolumeHelpers.cs" />
<Compile Include="AudioSessions\SystemSoundsAudioSession.cs" /> <Compile Include="AudioSessions\SystemSoundsAudioSession.cs" />
<Compile Include="AudioSessions\SystemVolumeAudioSession.cs" /> <Compile Include="AudioSessions\SystemVolumeAudioSession.cs" />
<Compile Include="DialAction.cs" /> <Compile Include="DialAction.cs" />
@ -83,14 +85,14 @@
<Content Include="Images\**\*.png"> <Content Include="Images\**\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="PropertyInspector\PluginActionPI.html"> <Content Include="PropertyInspector\PluginActionPI.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CoreAudio">
<Version>1.27.0</Version>
</PackageReference>
<PackageReference Include="IsExternalInit"> <PackageReference Include="IsExternalInit">
<Version>1.0.3</Version> <Version>1.0.3</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -6,11 +6,14 @@
<meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-capable content=yes>
<meta name=apple-mobile-web-app-status-bar-style content=black> <meta name=apple-mobile-web-app-status-bar-style content=black>
<title>FocusVolumeControl Settings</title> <title>FocusVolumeControl Settings</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdpi.css"> <link rel="stylesheet" href="./lib/sdpi.css">
<script src="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdtools.common.js"></script> <link rel="sytlesheet" href="./lib/rangeTooltip.css">
<script src="lib/sdtools.common.js"></script>
<script src="lib/rangeTooltip.js"></script>
</head> </head>
<body> <body>
<div class="sdpi-wrapper"> <div class="sdpi-wrapper">
<div class="sdpi-item"> <div class="sdpi-item">
<div class="sdpi-item-label">Fallback</div> <div class="sdpi-item-label">Fallback</div>
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()"> <select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
@ -19,12 +22,25 @@
<option value="2">Main System Volume</option> <option value="2">Main System Volume</option>
</select> </select>
</div> </div>
<div type="range" class="sdpi-item sdShowTooltip">
<div class="sdpi-item-label">Step Size</div>
<div class="sdpi-item-value">
<span class="clickable" value="1">1</span>
<input type="range" min="1" max="10" value="1" class="sdProperty" data-suffix=" %" id="stepSize" oninput="setSettings()" />
<span class="clickable" value="1">10</span>
</div>
</div>
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div>
<details> <details>
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p> <p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p> <p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p> <p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
<p>* Main System Volume - Switch to the main volume control for the system. This will change the volume of all applications</p> <p>* Main System Volume - Switch to the main volume control for the system. This will change the volume of all applications</p>
</details> </details>
</div> </div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,41 @@
.sdpi-info-label {
display: inline-block;
user-select: none;
position: absolute;
height: 15px;
width: auto;
text-align: center;
border-radius: 4px;
min-width: 44px;
max-width: 80px;
background: white;
font-size: 11px;
color: black;
z-index: 1000;
box-shadow: 0px 0px 12px rgba(0,0,0,.8);
padding: 2px;
}
.sdpi-info-label.hidden {
opacity: 0;
transition: opacity 0.25s linear;
}
.sdpi-info-label.shown {
position: absolute;
opacity: 1;
transition: opacity 0.25s ease-out;
}
.rangeLabel {
position: relative;
font-weight: normal;
margin-top: 22px;
left: -200px;
min-width: 200px;
text-align: center;
}
.percent::after {
content: "%";
}

View File

@ -0,0 +1,122 @@
// ****************************************************************
// * EasyPI v1.3
// * Author: BarRaider
// *
// * rangeTooltip.js adds a tooltip showing the value of a range slider.
// * Requires rangeTooltip.css to be referenced in the HTML file.
// *
// * Project page: https://github.com/BarRaider/streamdeck-easypi
// * Support: http://discord.barraider.com
// ****************************************************************
var tooltip = document.querySelector('.sdpi-info-label');
var tw;
document.addEventListener("DOMContentLoaded", function () {
// Handler when the DOM is fully loaded
setRangeTooltips();
});
function calcRangeLabel(elem) {
const value = elem.value;
const percent = (elem.value - elem.min) / (elem.max - elem.min);
let tooltipValue = value;
let outputType = elem.dataset.suffix;
if (outputType && outputType == '%') {
tooltipValue = Math.round(100 * percent);
}
return tooltipValue + outputType;
}
function setElementLabel(elem, str) {
// Try to set this for the rangeLabel class, if it exists
let label = elem.querySelector('.rangeLabel');
if (label) {
label.innerHTML = str;
}
else {
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
}
}
function setRangeTooltips() {
console.log("Loading setRangeTooltips");
if (!tooltip) {
tooltip = document.querySelector('.sdpi-info-label');
}
if (!tw) {
tw = tooltip.getBoundingClientRect().width;
}
const rangeToolTips = document.querySelectorAll('div[type=range].sdShowTooltip');
rangeToolTips.forEach(elem => {
let rangeSelector = elem.querySelector('input[type=range]');
let fn = () => {
const rangeRect = rangeSelector.getBoundingClientRect();
const w = rangeRect.width - tw / 2;
const labelStr = calcRangeLabel(rangeSelector);
// Set the tooltip
if (tooltip.classList.contains('hidden')) {
tooltip.style.top = '-1000px';
} else {
const percent = (rangeSelector.value - rangeSelector.min) / (rangeSelector.max - rangeSelector.min);
tooltip.style.left = (rangeRect.left + Math.round(w * percent) - tw / 4) + 'px';
tooltip.textContent = labelStr;
tooltip.style.top = (rangeRect.top - 32) + 'px';
}
setElementLabel(elem, labelStr)
};
rangeSelector.addEventListener(
'mouseenter',
function () {
tooltip.classList.remove('hidden');
tooltip.classList.add('shown');
fn();
},
false
);
rangeSelector.addEventListener(
'mouseout',
function () {
tooltip.classList.remove('shown');
tooltip.classList.add('hidden');
fn();
},
false
);
rangeSelector.addEventListener('input', fn, false);
rangeSelector.addEventListener("change", fn, false);
document.addEventListener(
'settingsUpdated',
function () {
console.log('rangeTooltip settingsUpdated called');
window.setTimeout(function () {
let str = calcRangeLabel(rangeSelector);
setElementLabel(elem, str);
}, 500);
},
false
);
document.addEventListener(
'websocketCreate',
function () {
console.log('rangeTooltip websocketCreate called');
window.setTimeout(function () {
let str = calcRangeLabel(rangeSelector);
setElementLabel(elem, str);
}, 500);
},
false
);
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,321 @@
// ****************************************************************
// * EasyPI v1.4
// * Author: BarRaider
// *
// * JS library to simplify the communication between the
// * Stream Deck's Property Inspector and the plugin.
// *
// * Project page: https://github.com/BarRaider/streamdeck-easypi
// * Support: http://discord.barraider.com
// *
// * Initially forked from Elgato's common.js file
// ****************************************************************
var websocket = null,
uuid = null,
registerEventName = null,
actionInfo = {},
inInfo = {},
runningApps = [],
isQT = navigator.appVersion.includes('QtWebEngine');
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
uuid = inUUID;
registerEventName = inRegisterEvent;
console.log(inUUID, inActionInfo);
actionInfo = JSON.parse(inActionInfo); // cache the info
inInfo = JSON.parse(inInfo);
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
addDynamicStyles(inInfo.colors);
websocket.onopen = websocketOnOpen;
websocket.onmessage = websocketOnMessage;
// Allow others to get notified that the websocket is created
var event = new Event('websocketCreate');
document.dispatchEvent(event);
loadConfiguration(actionInfo.payload.settings);
initPropertyInspector();
}
function websocketOnOpen() {
var json = {
event: registerEventName,
uuid: uuid
};
websocket.send(JSON.stringify(json));
// Notify the plugin that we are connected
sendValueToPlugin('propertyInspectorConnected', 'property_inspector');
}
function websocketOnMessage(evt) {
// Received message from Stream Deck
var jsonObj = JSON.parse(evt.data);
if (jsonObj.event === 'didReceiveSettings') {
var payload = jsonObj.payload;
loadConfiguration(payload.settings);
}
else {
console.log("Ignored websocketOnMessage: " + jsonObj.event);
}
}
function loadConfiguration(payload) {
console.log('loadConfiguration');
console.log(payload);
for (var key in payload) {
try {
var elem = document.getElementById(key);
if (elem.classList.contains("sdCheckbox")) { // Checkbox
elem.checked = payload[key];
}
else if (elem.classList.contains("sdFile")) { // File
var elemFile = document.getElementById(elem.id + "Filename");
elemFile.innerText = payload[key];
if (!elemFile.innerText) {
elemFile.innerText = "No file...";
}
}
else if (elem.classList.contains("sdList")) { // Dynamic dropdown
var textProperty = elem.getAttribute("sdListTextProperty");
var valueProperty = elem.getAttribute("sdListValueProperty");
var valueField = elem.getAttribute("sdValueField");
var items = payload[key];
elem.options.length = 0;
for (var idx = 0; idx < items.length; idx++) {
var opt = document.createElement('option');
opt.value = items[idx][valueProperty];
opt.text = items[idx][textProperty];
elem.appendChild(opt);
}
elem.value = payload[valueField];
}
else if (elem.classList.contains("sdHTML")) { // HTML element
elem.innerHTML = payload[key];
}
else { // Normal value
elem.value = payload[key];
}
console.log("Load: " + key + "=" + payload[key]);
}
catch (err) {
console.log("loadConfiguration failed for key: " + key + " - " + err);
}
}
}
function setSettings() {
var payload = {};
var elements = document.getElementsByClassName("sdProperty");
Array.prototype.forEach.call(elements, function (elem) {
var key = elem.id;
if (elem.classList.contains("sdCheckbox")) { // Checkbox
payload[key] = elem.checked;
}
else if (elem.classList.contains("sdFile")) { // File
var elemFile = document.getElementById(elem.id + "Filename");
payload[key] = elem.value;
if (!elem.value) {
// Fetch innerText if file is empty (happens when we lose and regain focus to this key)
payload[key] = elemFile.innerText;
}
else {
// Set value on initial file selection
elemFile.innerText = elem.value;
}
}
else if (elem.classList.contains("sdList")) { // Dynamic dropdown
var valueField = elem.getAttribute("sdValueField");
payload[valueField] = elem.value;
}
else if (elem.classList.contains("sdHTML")) { // HTML element
var valueField = elem.getAttribute("sdValueField");
payload[valueField] = elem.innerHTML;
}
else { // Normal value
payload[key] = elem.value;
}
console.log("Save: " + key + "<=" + payload[key]);
});
setSettingsToPlugin(payload);
}
function setSettingsToPlugin(payload) {
if (websocket && (websocket.readyState === 1)) {
const json = {
'event': 'setSettings',
'context': uuid,
'payload': payload
};
websocket.send(JSON.stringify(json));
var event = new Event('settingsUpdated');
document.dispatchEvent(event);
}
}
// Sends an entire payload to the sendToPlugin method
function sendPayloadToPlugin(payload) {
if (websocket && (websocket.readyState === 1)) {
const json = {
'action': actionInfo['action'],
'event': 'sendToPlugin',
'context': uuid,
'payload': payload
};
websocket.send(JSON.stringify(json));
}
}
// Sends one value to the sendToPlugin method
function sendValueToPlugin(value, param) {
if (websocket && (websocket.readyState === 1)) {
const json = {
'action': actionInfo['action'],
'event': 'sendToPlugin',
'context': uuid,
'payload': {
[param]: value
}
};
websocket.send(JSON.stringify(json));
}
}
function openWebsite() {
if (websocket && (websocket.readyState === 1)) {
const json = {
'event': 'openUrl',
'payload': {
'url': 'https://BarRaider.com'
}
};
websocket.send(JSON.stringify(json));
}
}
if (!isQT) {
document.addEventListener('DOMContentLoaded', function () {
initPropertyInspector();
});
}
window.addEventListener('beforeunload', function (e) {
e.preventDefault();
// Notify the plugin we are about to leave
sendValueToPlugin('propertyInspectorWillDisappear', 'property_inspector');
// Don't set a returnValue to the event, otherwise Chromium with throw an error.
});
function prepareDOMElements(baseElement) {
baseElement = baseElement || document;
/**
* You could add a 'label' to a textares, e.g. to show the number of charactes already typed
* or contained in the textarea. This helper updates this label for you.
*/
baseElement.querySelectorAll('textarea').forEach((e) => {
const maxl = e.getAttribute('maxlength');
e.targets = baseElement.querySelectorAll(`[for='${e.id}']`);
if (e.targets.length) {
let fn = () => {
for (let x of e.targets) {
x.textContent = maxl ? `${e.value.length}/${maxl}` : `${e.value.length}`;
}
};
fn();
e.onkeyup = fn;
}
});
}
function initPropertyInspector() {
// Place to add functions
prepareDOMElements(document);
}
function addDynamicStyles(clrs) {
const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style');
if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100);
const clr = clrs.highlightColor.slice(0, 7);
const clr1 = fadeColor(clr, 100);
const clr2 = fadeColor(clr, 60);
const metersActiveColor = fadeColor(clr, -60);
node.setAttribute('id', 'sdpi-dynamic-styles');
node.innerHTML = `
input[type="radio"]:checked + label span,
input[type="checkbox"]:checked + label span {
background-color: ${clrs.highlightColor};
}
input[type="radio"]:active:checked + label span,
input[type="radio"]:active + label span,
input[type="checkbox"]:active:checked + label span,
input[type="checkbox"]:active + label span {
background-color: ${clrs.mouseDownColor};
}
input[type="radio"]:active + label span,
input[type="checkbox"]:active + label span {
background-color: ${clrs.buttonPressedBorderColor};
}
td.selected,
td.selected:hover,
li.selected:hover,
li.selected {
color: white;
background-color: ${clrs.highlightColor};
}
.sdpi-file-label > label:active,
.sdpi-file-label.file:active,
label.sdpi-file-label:active,
label.sdpi-file-info:active,
input[type="file"]::-webkit-file-upload-button:active,
button:active {
background-color: ${clrs.buttonPressedBackgroundColor};
color: ${clrs.buttonPressedTextColor};
border-color: ${clrs.buttonPressedBorderColor};
}
::-webkit-progress-value,
meter::-webkit-meter-optimum-value {
background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2})
}
::-webkit-progress-value:active,
meter::-webkit-meter-optimum-value:active {
background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr})
}
`;
document.body.appendChild(node);
};
/** UTILITIES */
/*
Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account)
Usage:
fadeColor('#061261', 100); // will lighten the color
fadeColor('#200867'), -100); // will darken the color
*/
function fadeColor(col, amt) {
const min = Math.min, max = Math.max;
const num = parseInt(col.replace(/#/g, ''), 16);
const r = min(255, max((num >> 16) + amt, 0));
const g = min(255, max((num & 0x0000FF) + amt, 0));
const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0));
return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0);
}

View File

@ -30,10 +30,10 @@
} }
], ],
"Author": "dlprows", "Author": "dlprows",
"Name": "FocusVolumeControl", "Name": "Focused Application Volume",
"Description": "Control the volume of the focused application", "Description": "Control the volume of the focused application",
"URL": "https://github.com/dlprows/FocusVolumeControl", "URL": "https://github.com/dlprows/FocusVolumeControl",
"Version": "1.0.0", "Version": "1.1.1",
"CodePath": "FocusVolumeControl", "CodePath": "FocusVolumeControl",
"Category": "Volume Control [dlprows]", "Category": "Volume Control [dlprows]",
"Icon": "Images/pluginIcon", "Icon": "Images/pluginIcon",