Compare commits
3 Commits
0f7f1fffcd
...
d1a5e37067
Author | SHA1 | Date | |
---|---|---|---|
d1a5e37067 | |||
fdfa32909f | |||
7abbc92080 |
67
Overrides.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
## Overrides
|
||||||
|
|
||||||
|
Some games use particularly agressive forms of anti-cheat that interfere with the ability to determine information about the focused application, and then pair it to the appropriate audio process.
|
||||||
|
|
||||||
|
Unfortunately, there is nothing I can do about that.
|
||||||
|
|
||||||
|
In order to work around this, the overrides mechanism is there for you to set up manual mappings.
|
||||||
|
|
||||||
|
I chose to base it off of a process's Main Window Title.
|
||||||
|
|
||||||
|
These can change throughout the usage of an application. Once again, there's nothing I can do about that.
|
||||||
|
|
||||||
|
The reason I chose to use the Main Window Title despite this problem, is because in the case of some games, it was one of the only pieces of information that I could get about the running process due to its anti-cheat.
|
||||||
|
|
||||||
|
There is no way of knowing if this will work in all cases, but it seems to be reliable for the time being. And if I'm ever unable to get the Main Window's Title, I don't know if there will be a different data point to use for matching. Its kind of just the only thing available.
|
||||||
|
|
||||||
|
In order to make it so that I don't have to update the plugin for each game using agressive anti-cheat, and to make it so that you don't have to wait for me to fix something, I have created a way to put overrides into the plugin directly.
|
||||||
|
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```
|
||||||
|
<Match type>: Window title string
|
||||||
|
audio process string
|
||||||
|
|
||||||
|
//lines starting in // are comments, and are ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Match Type
|
||||||
|
eq: equals - case insensitive
|
||||||
|
start: starts with - case insensitive
|
||||||
|
end: ends with - case insensitive
|
||||||
|
regex: regular expression - case sensitive
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
//helldivers 2 has a trademark symbol in it, and those are hard to type.
|
||||||
|
//so we just find a window that starts with helldivers
|
||||||
|
start: Helldivers
|
||||||
|
helldivers
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
//you can actually map anything you want. it doesn't have to be only things with anti-cheat problems
|
||||||
|
eq: task manager
|
||||||
|
Google Chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
Getting window titles can be a little hard sometimes. You can mouse over the icon on the start bar, and get it from there.
|
||||||
|
|
||||||
|
Another great way to get it is to run this powershell
|
||||||
|
|
||||||
|
```
|
||||||
|
Get-Process | Where-Object ($_.mainWindowTitle} | Format-Table mainWindowTitle
|
||||||
|
```
|
||||||
|
|
||||||
|
For audio processes right click on the volume in the tray of the start bar, and open the Volume Mixer. When you look through the list of apps, you can just type the name from that.
|
||||||
|
|
||||||
|
Alternatively you can put in the name of the actual executable. The easiest way to get to those is to run the SoundBrowser published in the releases in github.
|
||||||
|
|
@ -12,6 +12,11 @@ The screen updates to show the name/icon of the app so that you can always know
|
|||||||
|
|
||||||
![Focus volume control plugin preview](previews/1-preview.png?raw=true)
|
![Focus volume control plugin preview](previews/1-preview.png?raw=true)
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
If you're having trouble with a specific applicaiton, there is an override that you can put in.
|
||||||
|
[More Details Here](Overrides.md)
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
build the solution with visual studio
|
build the solution with visual studio
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net48</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
168
src/FocusVolumeControl.UnitTests/OverrideParserTests.cs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
using FocusVolumeControl.Overrides;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.UnitTests
|
||||||
|
{
|
||||||
|
public class OverrideParserTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
[InlineData("\n")]
|
||||||
|
public void BlankReturnsEmpty(string str)
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Empty(overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelldiversParses()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var str =
|
||||||
|
"""
|
||||||
|
eq: HELLDIVERS™ 2
|
||||||
|
helldivers2
|
||||||
|
""";
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Single(overrides);
|
||||||
|
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
|
||||||
|
Assert.Equal("HELLDIVERS™ 2", overrides[0].WindowQuery);
|
||||||
|
Assert.Equal("helldivers2", overrides[0].AudioProcessName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleOverridesParse()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var str =
|
||||||
|
"""
|
||||||
|
eq: HELLDIVERS™ 2
|
||||||
|
helldivers2
|
||||||
|
|
||||||
|
start: Task
|
||||||
|
Steam
|
||||||
|
""";
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
|
||||||
|
Assert.Equal("HELLDIVERS™ 2", overrides[0].WindowQuery);
|
||||||
|
Assert.Equal("helldivers2", overrides[0].AudioProcessName);
|
||||||
|
Assert.Equal(MatchType.StartsWith, overrides[1].MatchType);
|
||||||
|
Assert.Equal("Task", overrides[1].WindowQuery);
|
||||||
|
Assert.Equal("Steam", overrides[1].AudioProcessName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncompleteMatchesAreSkipped()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var str =
|
||||||
|
"""
|
||||||
|
eq: HELLDIVERS™ 2
|
||||||
|
|
||||||
|
start: Task
|
||||||
|
Steam
|
||||||
|
""";
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Single(overrides);
|
||||||
|
Assert.Equal(MatchType.StartsWith, overrides[0].MatchType);
|
||||||
|
Assert.Equal("Task", overrides[0].WindowQuery);
|
||||||
|
Assert.Equal("Steam", overrides[0].AudioProcessName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidMatchesAreSkipped()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var str =
|
||||||
|
"""
|
||||||
|
equal: Chrome
|
||||||
|
chrome
|
||||||
|
|
||||||
|
end: Task
|
||||||
|
Steam
|
||||||
|
""";
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Single(overrides);
|
||||||
|
Assert.Equal(MatchType.EndsWith, overrides[0].MatchType);
|
||||||
|
Assert.Equal("Task", overrides[0].WindowQuery);
|
||||||
|
Assert.Equal("Steam", overrides[0].AudioProcessName);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MatchesAreCaseInsensitive()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var str =
|
||||||
|
"""
|
||||||
|
Eq: 0
|
||||||
|
0
|
||||||
|
|
||||||
|
eNd: 1
|
||||||
|
1
|
||||||
|
|
||||||
|
StArT: 2
|
||||||
|
2
|
||||||
|
|
||||||
|
Regex: 3
|
||||||
|
3
|
||||||
|
""";
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
|
||||||
|
Assert.Equal(MatchType.EndsWith, overrides[1].MatchType);
|
||||||
|
Assert.Equal(MatchType.StartsWith, overrides[2].MatchType);
|
||||||
|
Assert.Equal(MatchType.Regex, overrides[3].MatchType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CommentsAreSkipped()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var str =
|
||||||
|
"""
|
||||||
|
//Eq: 0
|
||||||
|
//0
|
||||||
|
|
||||||
|
end: 1
|
||||||
|
1
|
||||||
|
""";
|
||||||
|
|
||||||
|
//act
|
||||||
|
var overrides = OverrideParser.Parse(str);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Single(overrides);
|
||||||
|
Assert.Equal(MatchType.EndsWith, overrides[0].MatchType);
|
||||||
|
Assert.Equal("1", overrides[0].WindowQuery);
|
||||||
|
Assert.Equal("1", overrides[0].AudioProcessName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
1
src/FocusVolumeControl.UnitTests/Usings.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl", "Focus
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundBrowser", "SoundBrowser\SoundBrowser.csproj", "{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundBrowser", "SoundBrowser\SoundBrowser.csproj", "{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl.UnitTests", "FocusVolumeControl.UnitTests\FocusVolumeControl.UnitTests.csproj", "{322E16C9-C96E-45DF-912F-DB6366170645}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -21,6 +23,10 @@ Global
|
|||||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{322E16C9-C96E-45DF-912F-DB6366170645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{322E16C9-C96E-45DF-912F-DB6366170645}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{322E16C9-C96E-45DF-912F-DB6366170645}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{322E16C9-C96E-45DF-912F-DB6366170645}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using FocusVolumeControl.AudioSessions;
|
using FocusVolumeControl.AudioSessions;
|
||||||
|
using FocusVolumeControl.Overrides;
|
||||||
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.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioHelpers;
|
namespace FocusVolumeControl.AudioHelpers;
|
||||||
|
|
||||||
@ -14,6 +16,8 @@ public class AudioHelper
|
|||||||
int[] _currentProcesses;
|
int[] _currentProcesses;
|
||||||
int _retryFallbackCount = 0;
|
int _retryFallbackCount = 0;
|
||||||
|
|
||||||
|
public List<Override> Overrides { get; set; }
|
||||||
|
|
||||||
public IAudioSession Current { get; private set; }
|
public IAudioSession Current { get; private set; }
|
||||||
|
|
||||||
public void ResetCache()
|
public void ResetCache()
|
||||||
@ -113,7 +117,12 @@ public class AudioHelper
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var processes = GetPossibleProcesses();
|
var processes = TryGetProcessFromOverrides();
|
||||||
|
|
||||||
|
if(processes == null)
|
||||||
|
{
|
||||||
|
processes = GetPossibleProcesses();
|
||||||
|
}
|
||||||
var processIds = processes?.Select(x => x.Id).ToArray();
|
var processIds = processes?.Select(x => x.Id).ToArray();
|
||||||
|
|
||||||
//_currentProcesses null - first time getting sessions
|
//_currentProcesses null - first time getting sessions
|
||||||
@ -162,9 +171,9 @@ public class AudioHelper
|
|||||||
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
|
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public List<Process> GetPossibleProcesses()
|
public List<Process> GetPossibleProcesses(IntPtr? handleOverride = null)
|
||||||
{
|
{
|
||||||
var handle = Native.GetForegroundWindow();
|
var handle = handleOverride ?? Native.GetForegroundWindow();
|
||||||
|
|
||||||
if (handle == IntPtr.Zero)
|
if (handle == IntPtr.Zero)
|
||||||
{
|
{
|
||||||
@ -174,10 +183,19 @@ public class AudioHelper
|
|||||||
var ids = Native.GetProcessesOfChildWindows(handle);
|
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||||
|
|
||||||
Native.GetWindowThreadProcessId(handle, out var pid);
|
Native.GetWindowThreadProcessId(handle, out var pid);
|
||||||
ids.Insert(0, pid);
|
|
||||||
|
if(pid != 0)
|
||||||
|
{
|
||||||
|
ids.Insert(0, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ids.Count == 0)
|
||||||
|
{
|
||||||
|
return new List<Process>();
|
||||||
|
}
|
||||||
|
|
||||||
var processes = ids.Distinct()
|
var processes = ids.Distinct()
|
||||||
.Select(x => Process.GetProcessById(x))
|
.Select(Process.GetProcessById)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if(processes.FirstOrDefault()?.ProcessName == "explorer")
|
if(processes.FirstOrDefault()?.ProcessName == "explorer")
|
||||||
@ -287,4 +305,123 @@ public class AudioHelper
|
|||||||
return new SystemVolumeAudioSession(endpointVolume);
|
return new SystemVolumeAudioSession(endpointVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static Dictionary<string, List<int>> _audioSessionNameCache = new Dictionary<string, List<int>>();
|
||||||
|
private List<int> GetFromNameCacheIfPossible(string processName)
|
||||||
|
{
|
||||||
|
if(_audioSessionNameCache.TryGetValue(processName, out var result))
|
||||||
|
{
|
||||||
|
if(result.Count!= 0)
|
||||||
|
{
|
||||||
|
foreach(var pid in result)
|
||||||
|
{
|
||||||
|
var p = GetProcessById(pid);
|
||||||
|
if(p == null || p.ProcessName != processName)
|
||||||
|
{
|
||||||
|
_audioSessionNameCache.Remove(processName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Process> TryGetProcessFromOverrides(IntPtr? handleOverride = null)
|
||||||
|
{
|
||||||
|
var handle = handleOverride ?? Native.GetForegroundWindow();
|
||||||
|
|
||||||
|
if (Overrides?.Any() == true)
|
||||||
|
{
|
||||||
|
Process tmp = null;
|
||||||
|
foreach (var p in Process.GetProcesses())
|
||||||
|
{
|
||||||
|
if (p.MainWindowHandle == handle)
|
||||||
|
{
|
||||||
|
tmp = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmp != null)
|
||||||
|
{
|
||||||
|
foreach (var o in Overrides)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
(o.MatchType == MatchType.Equal && tmp.MainWindowTitle.Equals(o.WindowQuery, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
|| (o.MatchType == MatchType.StartsWith && tmp.MainWindowTitle.StartsWith(o.WindowQuery, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (o.MatchType == MatchType.EndsWith && tmp.MainWindowTitle.EndsWith(o.WindowQuery, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (o.MatchType == MatchType.Regex && Regex.IsMatch(tmp.MainWindowTitle, o.WindowQuery))
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var ids = FindAudioSessionByProcessName(o.AudioProcessName);
|
||||||
|
if (ids?.Count > 0)
|
||||||
|
{
|
||||||
|
return ids.Distinct()
|
||||||
|
.Select(Process.GetProcessById)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<int> FindAudioSessionByProcessName(string processName)
|
||||||
|
{
|
||||||
|
var cached = GetFromNameCacheIfPossible(processName);
|
||||||
|
if(cached != null)
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = new List<int>();
|
||||||
|
|
||||||
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
|
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||||
|
deviceCollection.GetCount(out var numDevices);
|
||||||
|
for (int d = 0; d < numDevices; d++)
|
||||||
|
{
|
||||||
|
deviceCollection.Item(d, out var device);
|
||||||
|
|
||||||
|
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||||
|
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
|
||||||
|
var manager = (IAudioSessionManager2)m;
|
||||||
|
|
||||||
|
device.GetId(out var currentDeviceId);
|
||||||
|
|
||||||
|
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||||
|
|
||||||
|
sessionEnumerator.GetCount(out var count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sessionEnumerator.GetSession(i, out var session);
|
||||||
|
session.GetDisplayName(out var displayName);
|
||||||
|
session.GetProcessId(out var sessionProcessId);
|
||||||
|
|
||||||
|
var audioProcess = GetProcessById(sessionProcessId);
|
||||||
|
|
||||||
|
if(audioProcess == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var audioProcessName = _nameAndIconHelper.TryGetProcessNameWithoutIcon(audioProcess);
|
||||||
|
if(audioProcess.ProcessName == processName || displayName == processName || processName == audioProcessName)
|
||||||
|
{
|
||||||
|
results.Add(sessionProcessId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = results.Distinct().ToList();
|
||||||
|
|
||||||
|
_audioSessionNameCache[processName] = results;
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,41 @@ public class NameAndIconHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string TryGetProcessNameWithoutIcon(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//appx packages are installed from the windows store. eg, itunes
|
||||||
|
var appx = AppxPackage.FromProcess(process);
|
||||||
|
if (appx == null)
|
||||||
|
{
|
||||||
|
|
||||||
|
//if the display name is already set, then it came from the display name of the audio session
|
||||||
|
if(!string.IsNullOrEmpty(process.MainWindowTitle))
|
||||||
|
{
|
||||||
|
return process.MainWindowTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
//using process.MainModule.FileVersionInfo sometimes throws permission exceptions
|
||||||
|
//we get the file version info with a limited query flag to avoid that
|
||||||
|
var fileVersionInfo = GetFileVersionInfo(process);
|
||||||
|
if(!string.IsNullOrEmpty(fileVersionInfo?.FileDescription))
|
||||||
|
{
|
||||||
|
return fileVersionInfo.FileDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.ProcessName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return appx.DisplayName ?? process.ProcessName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return process.ProcessName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
FileVersionInfo GetFileVersionInfo(Process process)
|
FileVersionInfo GetFileVersionInfo(Process process)
|
||||||
{
|
{
|
||||||
|
@ -10,13 +10,19 @@ using FocusVolumeControl.AudioSession;
|
|||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||||
{
|
{
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; } = null!;
|
||||||
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
||||||
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
||||||
|
|
||||||
public IconWrapper? IconWrapper { get; set; }
|
public IconWrapper? IconWrapper { get; set; }
|
||||||
|
public IEnumerable<int> Pids => Sessions.Select(x =>
|
||||||
|
{
|
||||||
|
x.GetProcessId(out var pid); return pid;
|
||||||
|
});
|
||||||
|
|
||||||
public string GetIcon()
|
public string GetIcon()
|
||||||
{
|
{
|
||||||
@ -98,3 +104,4 @@ public sealed class ActiveAudioSessionWrapper : IAudioSession
|
|||||||
return VolumeHelpers.GetVolumePercentage(level);
|
return VolumeHelpers.GetVolumePercentage(level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#nullable restore
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
@ -91,6 +92,91 @@ public interface IMMDeviceEnumerator
|
|||||||
int GetDefaultAudioEndpoint(DataFlow dataFlow, Role role, out IMMDevice device);
|
int GetDefaultAudioEndpoint(DataFlow dataFlow, Role role, out IMMDevice device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum EStgmAccess
|
||||||
|
{
|
||||||
|
STGM_READ = 0x0
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"),
|
||||||
|
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
public interface IPropertyStore
|
||||||
|
{
|
||||||
|
[PreserveSig]
|
||||||
|
int GetCount(out int count);
|
||||||
|
[PreserveSig]
|
||||||
|
int GetAt(int iProp, out PropertyKey pkey);
|
||||||
|
[PreserveSig]
|
||||||
|
int GetValue(ref PropertyKey key, out PropVariant pv);
|
||||||
|
[PreserveSig]
|
||||||
|
int SetValue(ref PropertyKey key, ref PropVariant propvar);
|
||||||
|
[PreserveSig]
|
||||||
|
int Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PropertyKey
|
||||||
|
{
|
||||||
|
public Guid fmtId;
|
||||||
|
public int PId;
|
||||||
|
|
||||||
|
public PropertyKey(Guid fmtId, int pId)
|
||||||
|
{
|
||||||
|
this.fmtId = fmtId;
|
||||||
|
this.PId = pId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public struct Blob
|
||||||
|
{
|
||||||
|
public int Length;
|
||||||
|
public IntPtr Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
public struct PropVariant
|
||||||
|
{
|
||||||
|
[FieldOffset(0)] short vt;
|
||||||
|
[FieldOffset(2)] short wReserved1;
|
||||||
|
[FieldOffset(4)] short wReserved2;
|
||||||
|
[FieldOffset(6)] short wReserved3;
|
||||||
|
[FieldOffset(8)] sbyte cVal;
|
||||||
|
[FieldOffset(8)] byte bVal;
|
||||||
|
[FieldOffset(8)] short iVal;
|
||||||
|
[FieldOffset(8)] ushort uiVal;
|
||||||
|
[FieldOffset(8)] int lVal;
|
||||||
|
[FieldOffset(8)] uint ulVal;
|
||||||
|
[FieldOffset(8)] long hVal;
|
||||||
|
[FieldOffset(8)] ulong uhVal;
|
||||||
|
[FieldOffset(8)] float fltVal;
|
||||||
|
[FieldOffset(8)] double dblVal;
|
||||||
|
[FieldOffset(8)] Blob blobVal;
|
||||||
|
[FieldOffset(8)] DateTime date;
|
||||||
|
[FieldOffset(8)] bool boolVal;
|
||||||
|
[FieldOffset(8)] int scode;
|
||||||
|
[FieldOffset(8)] FILETIME filetime;
|
||||||
|
[FieldOffset(8)] IntPtr everything_else;
|
||||||
|
|
||||||
|
public object Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var ve = (VarEnum)vt;
|
||||||
|
|
||||||
|
if((VarEnum)vt == VarEnum.VT_LPWSTR)
|
||||||
|
{
|
||||||
|
return Marshal.PtrToStringUni(everything_else);
|
||||||
|
}
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PKey
|
||||||
|
{
|
||||||
|
public static readonly PropertyKey DeviceFriendlyName = new(new(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14);
|
||||||
|
public static readonly PropertyKey DeviceFriendlyNameAttributes = new(new(0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b), 3);
|
||||||
|
public static readonly PropertyKey DeviceInterfaceFriendlyName = new(new(0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22), 2);
|
||||||
|
}
|
||||||
|
|
||||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
public interface IMMDevice
|
public interface IMMDevice
|
||||||
{
|
{
|
||||||
@ -98,7 +184,7 @@ public interface IMMDevice
|
|||||||
int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||||
|
|
||||||
[PreserveSig]
|
[PreserveSig]
|
||||||
int NotImpl1();
|
int OpenPropertyStore(EStgmAccess stgmAccess, out IPropertyStore propertyStore);
|
||||||
[PreserveSig]
|
[PreserveSig]
|
||||||
int GetId([Out, MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
|
int GetId([Out, MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
@ -15,4 +17,6 @@ public interface IAudioSession
|
|||||||
public void IncrementVolumeLevel(int step, int ticks);
|
public void IncrementVolumeLevel(int step, int ticks);
|
||||||
|
|
||||||
public int GetVolumeLevel();
|
public int GetVolumeLevel();
|
||||||
|
|
||||||
|
public IEnumerable<int> Pids { get; }
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ using FocusVolumeControl.UI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSession
|
namespace FocusVolumeControl.AudioSession
|
||||||
{
|
{
|
||||||
public abstract class IconWrapper
|
public abstract class IconWrapper
|
||||||
@ -101,3 +103,4 @@ namespace FocusVolumeControl.AudioSession
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
#nullable restore
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
@ -15,6 +16,9 @@ internal sealed class SystemSoundsAudioSession : IAudioSession
|
|||||||
ISimpleAudioVolume _volumeControl;
|
ISimpleAudioVolume _volumeControl;
|
||||||
|
|
||||||
public string DisplayName => "System sounds";
|
public string DisplayName => "System sounds";
|
||||||
|
|
||||||
|
public IEnumerable<int> Pids => new int[0];
|
||||||
|
|
||||||
public string GetIcon() => "Images/systemSounds";
|
public string GetIcon() => "Images/systemSounds";
|
||||||
|
|
||||||
public void ToggleMute()
|
public void ToggleMute()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
@ -15,6 +16,8 @@ internal sealed class SystemVolumeAudioSession : IAudioSession
|
|||||||
public string DisplayName => "System Volume";
|
public string DisplayName => "System Volume";
|
||||||
public string GetIcon() => "Images/encoderIcon";
|
public string GetIcon() => "Images/encoderIcon";
|
||||||
|
|
||||||
|
public IEnumerable<int> Pids => new int[0];
|
||||||
|
|
||||||
public void ToggleMute()
|
public void ToggleMute()
|
||||||
{
|
{
|
||||||
_volumeControl.SetMute(!IsMuted(), Guid.Empty);
|
_volumeControl.SetMute(!IsMuted(), Guid.Empty);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using BarRaider.SdTools.Payloads;
|
using BarRaider.SdTools.Payloads;
|
||||||
using FocusVolumeControl.AudioHelpers;
|
using FocusVolumeControl.AudioHelpers;
|
||||||
using FocusVolumeControl.AudioSessions;
|
using FocusVolumeControl.AudioSessions;
|
||||||
|
using FocusVolumeControl.Overrides;
|
||||||
using FocusVolumeControl.UI;
|
using FocusVolumeControl.UI;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@ -15,6 +16,12 @@ namespace FocusVolumeControl;
|
|||||||
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
||||||
public class DialAction : EncoderBase
|
public class DialAction : EncoderBase
|
||||||
{
|
{
|
||||||
|
private const string DefaultOverrides =
|
||||||
|
"""
|
||||||
|
//eq: HELLDIVERS™ 2
|
||||||
|
//helldivers2
|
||||||
|
""";
|
||||||
|
|
||||||
private class PluginSettings
|
private class PluginSettings
|
||||||
{
|
{
|
||||||
[JsonProperty("fallbackBehavior")]
|
[JsonProperty("fallbackBehavior")]
|
||||||
@ -23,11 +30,15 @@ public class DialAction : EncoderBase
|
|||||||
[JsonProperty("stepSize")]
|
[JsonProperty("stepSize")]
|
||||||
public int StepSize { get; set; }
|
public int StepSize { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("overrides")]
|
||||||
|
public string Overrides { get; set; }
|
||||||
|
|
||||||
public static PluginSettings CreateDefaultSettings()
|
public static PluginSettings CreateDefaultSettings()
|
||||||
{
|
{
|
||||||
PluginSettings instance = new PluginSettings();
|
PluginSettings instance = new PluginSettings();
|
||||||
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||||
instance.StepSize = 1;
|
instance.StepSize = 1;
|
||||||
|
instance.Overrides = DefaultOverrides;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,12 +57,18 @@ public class DialAction : EncoderBase
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
settings = payload.Settings.ToObject<PluginSettings>();
|
settings = payload.Settings.ToObject<PluginSettings>();
|
||||||
|
if(string.IsNullOrEmpty(settings.Overrides))
|
||||||
|
{
|
||||||
|
settings.Overrides = DefaultOverrides;
|
||||||
|
_ = SaveSettings();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowChangedEventLoop.Instance.WindowChanged += WindowChanged;
|
WindowChangedEventLoop.Instance.WindowChanged += WindowChanged;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
|
||||||
//just in case we fail to get the active session, don't prevent the plugin from launching
|
//just in case we fail to get the active session, don't prevent the plugin from launching
|
||||||
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||||
_ = UpdateStateIfNeeded(session);
|
_ = UpdateStateIfNeeded(session);
|
||||||
@ -220,7 +237,8 @@ public class DialAction : EncoderBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||||
_ = SaveSettings();
|
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
|
||||||
|
//_ = SaveSettings();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -66,6 +66,10 @@
|
|||||||
<Compile Include="AudioSessions\IAudioSession.cs" />
|
<Compile Include="AudioSessions\IAudioSession.cs" />
|
||||||
<Compile Include="FallbackBehavior.cs" />
|
<Compile Include="FallbackBehavior.cs" />
|
||||||
<Compile Include="AudioHelpers\NameAndIconHelper.cs" />
|
<Compile Include="AudioHelpers\NameAndIconHelper.cs" />
|
||||||
|
<Compile Include="InternalsVisibleTo.cs" />
|
||||||
|
<Compile Include="Overrides\Override.cs" />
|
||||||
|
<Compile Include="Overrides\MatchType.cs" />
|
||||||
|
<Compile Include="Overrides\OverrideParser.cs" />
|
||||||
<Compile Include="UI\IconExtraction.cs" />
|
<Compile Include="UI\IconExtraction.cs" />
|
||||||
<Compile Include="UI\ISDConnectionExtensions.cs" />
|
<Compile Include="UI\ISDConnectionExtensions.cs" />
|
||||||
<Compile Include="Native.cs" />
|
<Compile Include="Native.cs" />
|
||||||
@ -91,7 +95,7 @@
|
|||||||
<Content Include="Images\**\*.png">
|
<Content Include="Images\**\*.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
|
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css;PropertyInspector\assets\*.*">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="PropertyInspector\PluginActionPI.html">
|
<Content Include="PropertyInspector\PluginActionPI.html">
|
||||||
|
4
src/FocusVolumeControl/InternalsVisibleTo.cs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("FocusVolumeControl.UnitTests")]
|
||||||
|
[assembly: InternalsVisibleTo("SoundBrowser")]
|
10
src/FocusVolumeControl/Overrides/MatchType.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace FocusVolumeControl.Overrides
|
||||||
|
{
|
||||||
|
public enum MatchType
|
||||||
|
{
|
||||||
|
Equal,
|
||||||
|
StartsWith,
|
||||||
|
EndsWith,
|
||||||
|
Regex,
|
||||||
|
}
|
||||||
|
}
|
15
src/FocusVolumeControl/Overrides/Override.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.Overrides
|
||||||
|
{
|
||||||
|
public class Override
|
||||||
|
{
|
||||||
|
public MatchType MatchType { get; set; }
|
||||||
|
public string WindowQuery { get; set; }
|
||||||
|
public string AudioProcessName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
78
src/FocusVolumeControl/Overrides/OverrideParser.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.Overrides
|
||||||
|
{
|
||||||
|
internal class OverrideParser
|
||||||
|
{
|
||||||
|
public static List<Override> Parse(string raw)
|
||||||
|
{
|
||||||
|
var overrides = new List<Override>();
|
||||||
|
|
||||||
|
if (raw == null)
|
||||||
|
{
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = raw.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
|
||||||
|
Override tmp = null;
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(line) || line.StartsWith("//"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var split = line.Split(':');
|
||||||
|
if (split.Length > 1)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tmp?.WindowQuery) && !string.IsNullOrEmpty(tmp?.AudioProcessName))
|
||||||
|
{
|
||||||
|
overrides.Add(tmp);
|
||||||
|
}
|
||||||
|
tmp = new Override();
|
||||||
|
|
||||||
|
if (string.Equals(split[0], "eq", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
tmp.MatchType = MatchType.Equal;
|
||||||
|
}
|
||||||
|
else if (string.Equals(split[0], "start", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
tmp.MatchType = MatchType.StartsWith;
|
||||||
|
}
|
||||||
|
else if (string.Equals(split[0], "end", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
tmp.MatchType = MatchType.EndsWith;
|
||||||
|
}
|
||||||
|
else if (string.Equals(split[0], "regex", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
tmp.MatchType = MatchType.Regex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tmp.WindowQuery = split[1].Trim();
|
||||||
|
}
|
||||||
|
else if (tmp != null)
|
||||||
|
{
|
||||||
|
tmp.AudioProcessName = split[0].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(tmp?.WindowQuery) && !string.IsNullOrEmpty(tmp?.AudioProcessName))
|
||||||
|
{
|
||||||
|
overrides.Add(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@
|
|||||||
<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="./lib/sdpi.css">
|
<link rel="stylesheet" href="./css/sdpi.css">
|
||||||
<link rel="sytlesheet" href="./lib/rangeTooltip.css">
|
<link rel="sytlesheet" href="./css/rangeTooltip.css">
|
||||||
<script src="lib/sdtools.common.js"></script>
|
<script src="lib/sdtools.common.js"></script>
|
||||||
<script src="lib/rangeTooltip.js"></script>
|
<script src="lib/rangeTooltip.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@ -16,13 +16,20 @@
|
|||||||
|
|
||||||
<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 select sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||||
<option value="0">System Sounds</option>
|
<option value="0">System Sounds</option>
|
||||||
<option value="1">Previous App</option>
|
<option value="1">Previous App</option>
|
||||||
<option value="2">Default Output Device Volume</option>
|
<option value="2">Default Output Device Volume</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Fallback Details</summary>
|
||||||
|
<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>* 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>* Default Output Device Volume - Switch to the main volume control for the default output device. This will change the volume of the default output device. This is usually volume for all applications, unless you override the output device for specific applications.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div type="range" class="sdpi-item sdShowTooltip">
|
<div type="range" class="sdpi-item sdShowTooltip">
|
||||||
<div class="sdpi-item-label">Step Size</div>
|
<div class="sdpi-item-label">Step Size</div>
|
||||||
@ -32,15 +39,29 @@
|
|||||||
<span class="clickable" value="1">10</span>
|
<span class="clickable" value="1">10</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div>
|
<div class="sdpi-info-label hidden" style="z-index: 999;" value="">Tooltip</div>
|
||||||
|
|
||||||
|
<div type="textarea" class="sdpi-item">
|
||||||
|
<div class="sdpi-item-label">Overrides</div>
|
||||||
|
<span class="sdpi-item-value" textarea>
|
||||||
|
<textarea type="textarea" class="sdProperty" id="overrides" oninput="setSettings()"></textarea>
|
||||||
|
</span>
|
||||||
|
</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>
|
<summary>Override Details</summary>
|
||||||
<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>Some games use anti-cheat software that interferes with the ability to know what application is running, and pair it to the appropriate audio process.</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>There is nothing I can do about this.</p>
|
||||||
<p>* Default Output Device Volume - Switch to the main volume control for the default output device. This will change the volume of the default output device. This is usually volume for all applications, unless you override the output device for specific applications.</p>
|
<p>In order to work around this, the overrides mechanism is there for you to manually map an application window to an audio process.</p>
|
||||||
|
<p>They synax for this is</p>
|
||||||
|
<p>[matching]: Window Title <br />Audio Process</p>
|
||||||
|
<p>Blank lines can be used for spacing<br/>Lines starting with // are ignored</p>
|
||||||
|
<p>[matching] can be eq, start, end, or regex. Which will perform an exact match, starts with, ends with, or regular expressions respectively.</p>
|
||||||
|
<p>Example:<br />eq: Task Manager<br />Discord</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6">
|
||||||
|
<polygon fill="#8E8E92" fill-rule="evenodd" points="5 4 9 0 10 1 5 6 0 1 1 0"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 173 B |
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12">
|
||||||
|
<g fill="#8E8E92" transform="translate(4, 5) rotate(-90) translate(-4, -5) translate(-2, 2)">
|
||||||
|
<polygon id="Path" points="5 4 9 0 10 1 5 6 0 1 1 0"></polygon>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 268 B |
BIN
src/FocusVolumeControl/PropertyInspector/assets/check.png
Normal file
After Width: | Height: | Size: 234 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="10" viewBox="0 0 12 10">
|
||||||
|
<polygon fill="#FFF" points="7.2 7.5 7.2 -1.3 8.7 -1.3 8.6 9.1 2.7 8.7 2.7 7.2" transform="rotate(37 5.718 3.896)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 212 B |
@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path fill="#9C9C9C" fill-rule="nonzero"
|
||||||
|
d="M1,5 L1,14 L14,14 L14,5 L1,5 Z M0,1 L15,1 L15,15 L0,15 L0,1 Z M14,4 L14,2 L1,2 L1,4 L14,4 Z"/>
|
||||||
|
<rect width="1" height="1" x="2" fill="#9C9C9C" fill-rule="nonzero"/>
|
||||||
|
<rect width="1" height="1" x="12" fill="#9C9C9C" fill-rule="nonzero"/>
|
||||||
|
<g transform="translate(3 7)">
|
||||||
|
<rect width="1" height="1" x="2" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" fill="#666"/>
|
||||||
|
<rect width="1" height="1" x="4" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="6" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="8" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" y="2" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="2" y="2" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="4" y="2" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="6" y="2" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="8" y="2" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" y="4" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="2" y="4" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="4" y="4" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="6" y="4" fill="#9C9C9C"/>
|
||||||
|
<rect width="1" height="1" x="8" y="4" fill="#666"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<g fill="#9C9C9C">
|
||||||
|
<path d="M15,15 L1.77635684e-15,15 L1.77635684e-15,1 L15,1 L15,15 Z M5,7 L5,8 L6,8 L6,7 L5,7 Z M3,7 L3,8 L4,8 L4,7 L3,7 Z M7,7 L7,8 L8,8 L8,7 L7,7 Z M9,7 L9,8 L10,8 L10,7 L9,7 Z M11,7 L11,8 L12,8 L12,7 L11,7 Z M3,9 L3,10 L4,10 L4,9 L3,9 Z M5,9 L5,10 L6,10 L6,9 L5,9 Z M7,9 L7,10 L8,10 L8,9 L7,9 Z M9,9 L9,10 L10,10 L10,9 L9,9 Z M11,9 L11,10 L12,10 L12,9 L11,9 Z M3,11 L3,12 L4,12 L4,11 L3,11 Z M5,11 L5,12 L6,12 L6,11 L5,11 Z M7,11 L7,12 L8,12 L8,11 L7,11 Z M9,11 L9,12 L10,12 L10,11 L9,11 Z M11,11 L11,12 L12,12 L12,11 L11,11 Z M14,4 L14,2 L1,2 L1,4 L14,4 Z"/>
|
||||||
|
<rect width="1" height="1" x="2"/>
|
||||||
|
<rect width="1" height="1" x="12"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 780 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="6" viewBox="0 0 6 6">
|
||||||
|
<circle cx="3" cy="3" r="3" fill="#FFF"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 133 B |
3
src/FocusVolumeControl/PropertyInspector/assets/tick.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="8" viewBox="0 0 1 8">
|
||||||
|
<rect id="Rectangle" width="1" height="6" x="0" y="1" fill="#555"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 159 B |
@ -1,65 +1,3 @@
|
|||||||
.linkspan {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #7397d2;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleAlignedSmall {
|
|
||||||
font-size: 9pt;
|
|
||||||
padding-left: 33px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small {
|
|
||||||
font-size: 9pt !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftMargin10 {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftMargin0 {
|
|
||||||
margin-left: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftPadding3 {
|
|
||||||
padding-left: 3px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftPadding0 {
|
|
||||||
padding-left: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bright {
|
|
||||||
color: #d8d8d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconLeft {
|
|
||||||
background-position: 0px 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summaryIconPadding {
|
|
||||||
padding-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subMenu {
|
|
||||||
border-left: 1px dotted gray;
|
|
||||||
padding-left: 15px;
|
|
||||||
max-width: 96%;
|
|
||||||
background-color: #323232;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--sdpi-bgcolor: #2D2D2D;
|
|
||||||
--sdpi-background: #3D3D3D;
|
|
||||||
--sdpi-color: #d8d8d8;
|
|
||||||
--sdpi-bordercolor: #3a3a3a;
|
|
||||||
--sdpi-buttonbordercolor: #969696;
|
|
||||||
--sdpi-borderradius: 0px;
|
|
||||||
--sdpi-width: 224px;
|
|
||||||
--sdpi-fontweight: 600;
|
|
||||||
--sdpi-letterspacing: -0.25pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
--sdpi-bgcolor: #2D2D2D;
|
--sdpi-bgcolor: #2D2D2D;
|
||||||
--sdpi-background: #3D3D3D;
|
--sdpi-background: #3D3D3D;
|
||||||
@ -70,6 +8,16 @@ html {
|
|||||||
--sdpi-width: 224px;
|
--sdpi-width: 224px;
|
||||||
--sdpi-fontweight: 600;
|
--sdpi-fontweight: 600;
|
||||||
--sdpi-letterspacing: -0.25pt;
|
--sdpi-letterspacing: -0.25pt;
|
||||||
|
--sdpi-tab-color: #969696;
|
||||||
|
--sdpi-tab-left-margin: 1px;
|
||||||
|
--sdpi-tab-top-offset: 1px;
|
||||||
|
--sdpi-tab-selected-color: #333333;
|
||||||
|
--sdpi-tab-selected-top-offset: 0px;
|
||||||
|
--sdpi-tab-font-size: 9pt;
|
||||||
|
--sdpi-tab-container-left-offset: 5px;
|
||||||
|
--sdpi-tab-padding-horizontal: 12px;
|
||||||
|
--sdpi-tab-padding-vertical: 5px;
|
||||||
|
--sdpi-linecolor: #454545;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -119,16 +67,24 @@ hr2,
|
|||||||
margin: 8px 0px;
|
margin: 8px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sdpi-heading::before,
|
|
||||||
.sdpi-heading::after {
|
h1 {
|
||||||
content: "";
|
font-size: 1.3em;
|
||||||
flex-grow: 1;
|
font-weight: 500;
|
||||||
background: var(--sdpi-background);
|
text-align: center;
|
||||||
height: 1px;
|
margin-bottom: 12px;
|
||||||
font-size: 0px;
|
}
|
||||||
line-height: 0px;
|
|
||||||
margin: 0px 16px;
|
.sdpi-heading::before,
|
||||||
}
|
.sdpi-heading::after {
|
||||||
|
content: "";
|
||||||
|
flex-grow: 1;
|
||||||
|
background: var(--sdpi-background);
|
||||||
|
height: 1px;
|
||||||
|
font-size: 0px;
|
||||||
|
line-height: 0px;
|
||||||
|
margin: 0px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
hr2 {
|
hr2 {
|
||||||
height: 2px;
|
height: 2px;
|
||||||
@ -225,6 +181,9 @@ progress {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TABS */
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
/**
|
/**
|
||||||
* Setting display to flex makes this container lay
|
* Setting display to flex makes this container lay
|
||||||
@ -232,21 +191,93 @@ progress {
|
|||||||
* as in the above "Stepper input" example.
|
* as in the above "Stepper input" example.
|
||||||
*/
|
*/
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid #D7DBDD;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.0);
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
text-transform: capitalize;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-left: var(--sdpi-tab-container-left-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar-track {
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #444;
|
||||||
|
outline: 1px solid #444;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-separator {
|
||||||
|
margin-left: 100px;
|
||||||
|
max-width: 234px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 5px 30px;
|
padding: var(--sdpi-tab-padding-vertical) var(--sdpi-tab-padding-horizontal);
|
||||||
color: #16a2d7;
|
color: var(--sdpi-tab-color);
|
||||||
font-size: 9pt;
|
font-size: var(--sdpi-tab-font-size);
|
||||||
border-bottom: 2px solid transparent;
|
font-weight: var(--title-font-weight);
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 0px;
|
||||||
|
margin-top: var(--sdpi-tab-top-offset);
|
||||||
|
margin-left: var(--sdpi-tab-left-margin);
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-bottom: 1px solid var(--sdpi-linecolor);
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.is-tab-selected {
|
.tab:first-child {
|
||||||
border-bottom-color: #4ebbe4;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
margin-top: -14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container > hr {
|
||||||
|
margin-left: 100px;
|
||||||
|
max-width: 234px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs + hr {
|
||||||
|
margin-left: 0px;
|
||||||
|
max-width: 234px;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.selected {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--sdpi-tab-selected-color);
|
||||||
|
border-bottom: 2px solid var(--sdpi-tab-selected-color);
|
||||||
|
margin-top: var(--sdpi-tab-selected-top-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdpi-item.tabgroup {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.istab {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
@ -306,18 +337,30 @@ option {
|
|||||||
.sdpi-wrapper {
|
.sdpi-wrapper {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin-right: 1px; /* ensure scroller thumb is not clipped */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sdpi-item {
|
.sdpi-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
min-height: 32px;
|
min-height: 30px;
|
||||||
align-items: center;
|
align-items: first baseline;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
max-width: 344px;
|
max-width: 344px;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sdpi-item[type="textarea"],
|
||||||
|
.sdpi-item[type="color"],
|
||||||
|
.sdpi-item[type="canvas"],
|
||||||
|
.sdpi-item .aligncenter {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdpi-item[type="color"] > .sdpi-item-label {
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.sdpi-item:first-child {
|
.sdpi-item:first-child {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
@ -412,7 +455,7 @@ table > caption {
|
|||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
line-height: 24px;
|
line-height: normal;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,8 +529,8 @@ ol.sdpi-item-value,
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
padding: 4px !important;
|
padding: 4px !important;
|
||||||
display: flex;
|
/* display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.two-items li {
|
.two-items li {
|
||||||
@ -632,6 +675,16 @@ summary {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sdpi-item.details {
|
||||||
|
align-items: first baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* needs Chromium update 2023
|
||||||
|
.sdpi-item:has(>details) {
|
||||||
|
align-items: first baseline;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
details * {
|
details * {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@ -654,21 +707,21 @@ details.message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
details.message > summary:first-of-type {
|
details.message > summary:first-of-type {
|
||||||
/*line-height: 48px;*/
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.message h1 {
|
details.message h1 {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
details:not(.pointer) > summary {
|
/* details:not(.pointer)>summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary::-webkit-details-marker
|
details > summary::-webkit-details-marker,
|
||||||
.message > summary::-webkit-details-marker {
|
.message > summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.info20,
|
.info20,
|
||||||
.question,
|
.question,
|
||||||
@ -702,7 +755,6 @@ details > summary::-webkit-details-marker
|
|||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.sdpi-more-info {
|
.sdpi-more-info {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
@ -731,7 +783,6 @@ details > summary::-webkit-details-marker
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.sdpi-bottom-bar {
|
.sdpi-bottom-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: right;
|
align-self: right;
|
||||||
@ -759,7 +810,6 @@ details a {
|
|||||||
padding-right: 28px;
|
padding-right: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input:not([type="range"]),
|
input:not([type="range"]),
|
||||||
textarea {
|
textarea {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
@ -827,7 +877,6 @@ input[type="checkbox"] {
|
|||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
min-width: 8px;
|
min-width: 8px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -840,6 +889,10 @@ input[type="checkbox"] {
|
|||||||
|
|
||||||
span + input[type="range"] {
|
span + input[type="range"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
span + .range-container > input[type="range"],
|
||||||
|
span + input[type="range"] {
|
||||||
max-width: 168px;
|
max-width: 168px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1028,7 +1081,6 @@ textarea {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.card-carousel-wrapper {
|
.card-carousel-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1091,7 +1143,8 @@ textarea {
|
|||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */
|
/* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */
|
||||||
background-color: #fff;
|
/* background-color: #fff; */
|
||||||
|
text-align: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
@ -1114,6 +1167,7 @@ textarea {
|
|||||||
|
|
||||||
.card-carousel-cards .card-carousel--card img:hover {
|
.card-carousel-cards .card-carousel--card img:hover {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-carousel-cards .card-carousel--card--footer {
|
.card-carousel-cards .card-carousel--card--footer {
|
||||||
@ -1141,44 +1195,8 @@ textarea {
|
|||||||
color: #666a73;
|
color: #666a73;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-datetime-edit {
|
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
background: url(../assets/elg_calendar_inv.svg) no-repeat left center;
|
|
||||||
padding-right: 1em;
|
|
||||||
padding-left: 25px;
|
|
||||||
background-position: 4px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-datetime-edit-text {
|
|
||||||
padding: 0 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-datetime-edit-month-field {
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-datetime-edit-day-field {
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-datetime-edit-year-field {
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-inner-spin-button {
|
|
||||||
/* display: none; */
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-calendar-picker-indicator {
|
::-webkit-calendar-picker-indicator {
|
||||||
background: transparent;
|
background: url(../assets/elg_calendar_inv.svg) no-repeat center;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1186,53 +1204,30 @@ h1 {
|
|||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="text"]::-webkit-calendar-picker-indicator {
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="date"] {
|
input[type="date"] {
|
||||||
-webkit-align-items: center;
|
-webkit-align-items: center;
|
||||||
|
align-items: center;
|
||||||
display: -webkit-inline-flex;
|
display: -webkit-inline-flex;
|
||||||
font-family: monospace;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
|
||||||
-webkit-padding-start: 1px;
|
-webkit-padding-start: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-datetime-edit {
|
input::-webkit-datetime-edit {
|
||||||
-webkit-flex: 1;
|
flex: 1;
|
||||||
-webkit-user-modify: read-only !important;
|
-webkit-user-modify: read-only !important;
|
||||||
|
user-modify: read-only !important;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 4px;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
input::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
-webkit-user-modify: read-only !important;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1px 0;
|
|
||||||
white-space: pre;
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
input[type="date"] {
|
|
||||||
background-color: red;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="date"]::-webkit-clear-button {
|
|
||||||
font-size: 18px;
|
|
||||||
height: 30px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="date"]::-webkit-inner-spin-button {
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
|
||||||
font-size: 15px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: none;
|
display: none;
|
||||||
@ -1308,7 +1303,6 @@ input:required:valid {
|
|||||||
background-color: var(--sdpi-background);
|
background-color: var(--sdpi-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
@ -1330,23 +1324,72 @@ a {
|
|||||||
color: #7397d2;
|
color: #7397d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testcontainer {
|
input[type="week"] {
|
||||||
display: flex;
|
-webkit-appearance: auto !important;
|
||||||
background-color: #0000ff20;
|
appearance: auto !important;
|
||||||
max-width: 400px;
|
|
||||||
height: 200px;
|
|
||||||
align-content: space-evenly;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range] {
|
input[type="month"] + datalist,
|
||||||
-webkit-appearance: none;
|
input[type="day"] + datalist,
|
||||||
/* background-color: green; */
|
input[type="week"] + datalist,
|
||||||
|
input[type=text] + datalist {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
-webkit-appearance: auto;
|
||||||
|
appearance: auto;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
border: 0px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdpi-item[type="range"] .sdpi-item-value.datalist {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
datalist {
|
||||||
|
--sdpi-datalist-margin: 7px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: var(--sdpi-datalist-margin);
|
||||||
|
width: calc(100% - calc(var(--sdpi-datalist-margin) * 2.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
datalist > option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: end;
|
||||||
|
/* background-image: url(../assets/tick.svg); */
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='8' viewBox='0 0 1 8'%3E%3Crect width='1' height='6' x='0' y='1' fill='%23555'/%3E%3C/svg%3E%0A");
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9A9A99;
|
||||||
|
width: 1px;
|
||||||
|
height: 30px;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: -6px;
|
||||||
|
background-position: top 6px right 5px;
|
||||||
|
background-repeat: repeat no-repeat; /* fallback */
|
||||||
|
background-repeat-y: no-repeat;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="spinbutton"] {
|
||||||
|
-webkit-appearance: auto;
|
||||||
|
appearance: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
input[type="range"]::-webkit-slider-thumb {
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
@ -1595,7 +1638,8 @@ input[type="range"].colortemperature::-webkit-slider-runnable-track {
|
|||||||
background-image: linear-gradient(to right, #94d0ec, #ffb165);
|
background-image: linear-gradient(to right, #94d0ec, #ffb165);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="range"].colorbrightness::-webkit-slider-runnable-track {
|
input[type="range"].colorbrightness.greyscale::-webkit-slider-runnable-track,
|
||||||
|
input[type="range"].colorbrightness.grayscale::-webkit-slider-runnable-track {
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0));
|
background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
@ -1648,3 +1692,54 @@ select {
|
|||||||
-webkit-appearance: media-slider;
|
-webkit-appearance: media-slider;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*--------- context menu ----------*/
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 12px 0;
|
||||||
|
width: 120px;
|
||||||
|
background-color: #3D3D3D;
|
||||||
|
border: solid 1px #dfdfdf;
|
||||||
|
box-shadow: 1px 1px 2px #cfcfcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu--active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__items {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__item {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background-color: #3D3D3D !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__link {
|
||||||
|
display: block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
color: #ffff;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__link:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0066aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu_message {
|
||||||
|
cursor: default;
|
||||||
|
}
|
@ -29,6 +29,7 @@ function calcRangeLabel(elem) {
|
|||||||
return tooltipValue + outputType;
|
return tooltipValue + outputType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
function setElementLabel(elem, str) {
|
function setElementLabel(elem, str) {
|
||||||
// Try to set this for the rangeLabel class, if it exists
|
// Try to set this for the rangeLabel class, if it exists
|
||||||
let label = elem.querySelector('.rangeLabel');
|
let label = elem.querySelector('.rangeLabel');
|
||||||
@ -39,6 +40,7 @@ function setElementLabel(elem, str) {
|
|||||||
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
|
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
function setRangeTooltips() {
|
function setRangeTooltips() {
|
||||||
console.log("Loading setRangeTooltips");
|
console.log("Loading setRangeTooltips");
|
||||||
@ -68,7 +70,7 @@ function setRangeTooltips() {
|
|||||||
tooltip.style.top = (rangeRect.top - 32) + 'px';
|
tooltip.style.top = (rangeRect.top - 32) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
setElementLabel(elem, labelStr)
|
//setElementLabel(elem, labelStr)
|
||||||
};
|
};
|
||||||
|
|
||||||
rangeSelector.addEventListener(
|
rangeSelector.addEventListener(
|
||||||
@ -100,7 +102,7 @@ function setRangeTooltips() {
|
|||||||
console.log('rangeTooltip settingsUpdated called');
|
console.log('rangeTooltip settingsUpdated called');
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
let str = calcRangeLabel(rangeSelector);
|
let str = calcRangeLabel(rangeSelector);
|
||||||
setElementLabel(elem, str);
|
//setElementLabel(elem, str);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
@ -112,7 +114,7 @@ function setRangeTooltips() {
|
|||||||
console.log('rangeTooltip websocketCreate called');
|
console.log('rangeTooltip websocketCreate called');
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
let str = calcRangeLabel(rangeSelector);
|
let str = calcRangeLabel(rangeSelector);
|
||||||
setElementLabel(elem, str);
|
//setElementLabel(elem, str);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
|
@ -7,6 +7,8 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace FocusVolumeControl.UI
|
namespace FocusVolumeControl.UI
|
||||||
{
|
{
|
||||||
internal class JavaIconExtractor
|
internal class JavaIconExtractor
|
||||||
@ -94,3 +96,5 @@ namespace FocusVolumeControl.UI
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nullable restore
|
||||||
|
@ -8,6 +8,8 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
namespace FocusVolumeControl
|
||||||
{
|
{
|
||||||
internal class WindowChangedEventLoop
|
internal class WindowChangedEventLoop
|
||||||
@ -16,10 +18,10 @@ namespace FocusVolumeControl
|
|||||||
public static WindowChangedEventLoop Instance => _lazy.Value;
|
public static WindowChangedEventLoop Instance => _lazy.Value;
|
||||||
|
|
||||||
readonly Thread _thread;
|
readonly Thread _thread;
|
||||||
Dispatcher _dispatcher;
|
Dispatcher? _dispatcher;
|
||||||
|
|
||||||
IntPtr _foregroundWindowChangedEvent;
|
IntPtr _foregroundWindowChangedEvent;
|
||||||
Native.WinEventDelegate _delegate;
|
Native.WinEventDelegate? _delegate;
|
||||||
|
|
||||||
private WindowChangedEventLoop()
|
private WindowChangedEventLoop()
|
||||||
{
|
{
|
||||||
@ -37,7 +39,7 @@ namespace FocusVolumeControl
|
|||||||
_thread.Start();
|
_thread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action WindowChanged;
|
public event Action? WindowChanged;
|
||||||
|
|
||||||
CancellationTokenSource? _cancellationTokenSource = null;
|
CancellationTokenSource? _cancellationTokenSource = null;
|
||||||
|
|
||||||
@ -64,3 +66,5 @@ namespace FocusVolumeControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nullable restore
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
"Name": "Focused Application Volume",
|
"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.2.0",
|
"Version": "1.3.0",
|
||||||
"CodePath": "FocusVolumeControl",
|
"CodePath": "FocusVolumeControl",
|
||||||
"Category": "Volume Control [dlprows]",
|
"Category": "Volume Control [dlprows]",
|
||||||
"Icon": "Images/pluginIcon",
|
"Icon": "Images/pluginIcon",
|
||||||
|
@ -12,9 +12,9 @@
|
|||||||
<RowDefinition Height="auto"/>
|
<RowDefinition Height="auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
<Button x:Name="_pauseButton" Grid.Row="0" Click="PauseClicked"></Button>
|
||||||
|
|
||||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
<TextBox x:Name="_tf" Grid.Row="1" IsReadOnly="True">current</TextBox>
|
||||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
using FocusVolumeControl;
|
using FocusVolumeControl;
|
||||||
using FocusVolumeControl.AudioHelpers;
|
using FocusVolumeControl.AudioHelpers;
|
||||||
using FocusVolumeControl.AudioSessions;
|
using FocusVolumeControl.AudioSessions;
|
||||||
|
using NHotkey;
|
||||||
|
using NHotkey.Wpf;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace SoundBrowser;
|
namespace SoundBrowser;
|
||||||
|
|
||||||
@ -16,6 +19,8 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
AudioHelper _audioHelper;
|
AudioHelper _audioHelper;
|
||||||
Native.WinEventDelegate _delegate;
|
Native.WinEventDelegate _delegate;
|
||||||
|
bool _paused = false;
|
||||||
|
bool _doOnce = false;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@ -25,20 +30,93 @@ public partial class MainWindow : Window
|
|||||||
//normally you can just pass a lambda, but for some reason, that seems to get garbage collected
|
//normally you can just pass a lambda, but for some reason, that seems to get garbage collected
|
||||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||||
Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||||
|
|
||||||
|
HotkeyManager.Current.AddOrReplace("Pause", Key.P, ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift, OnPauseShortcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPauseShortcut(object? sender, HotkeyEventArgs e)
|
||||||
|
{
|
||||||
|
if( _paused )
|
||||||
|
{
|
||||||
|
_paused = false;
|
||||||
|
_pauseButton.Content = "Running - click to pause on next app";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_paused = true;
|
||||||
|
_doOnce = true;
|
||||||
|
_pauseButton.Content = "Paused - click to resume";
|
||||||
|
_handle = Native.GetForegroundWindow();
|
||||||
|
DoThing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PauseClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_paused = !_paused;
|
||||||
|
if(_paused)
|
||||||
|
{
|
||||||
|
_pauseButton.Content = "Pausing on next app";
|
||||||
|
_doOnce = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pauseButton.Content = "Running - click to pause on next app";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
SetupCurrentAppFields();
|
_handle = Native.GetForegroundWindow();
|
||||||
SetupAllSessionFields();
|
DoThing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupCurrentAppFields()
|
private void DoThing()
|
||||||
{
|
{
|
||||||
var handle = Native.GetForegroundWindow();
|
if(_paused)
|
||||||
var sb = new StringBuilder();
|
{
|
||||||
|
if(!_doOnce)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_doOnce = false;
|
||||||
|
_pauseButton.Content = "Paused - click to resume";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pauseButton.Content = "Running - click to pause on next app";
|
||||||
|
}
|
||||||
|
|
||||||
if (handle != IntPtr.Zero)
|
var sb = new StringBuilder();
|
||||||
|
SetupCurrentAppFields(sb);
|
||||||
|
sb.AppendLine("");
|
||||||
|
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||||
|
sb.AppendLine("");
|
||||||
|
DetermineDefaultDevice(sb);
|
||||||
|
sb.AppendLine("");
|
||||||
|
SetupAllSessionFields(sb);
|
||||||
|
|
||||||
|
_tf.Text = sb.ToString();
|
||||||
|
Trace.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetermineDefaultDevice(StringBuilder sb)
|
||||||
|
{
|
||||||
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
|
deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia, out var device);
|
||||||
|
|
||||||
|
var name = GetDeviceName(device);
|
||||||
|
|
||||||
|
sb.AppendLine($"Default Audio Device: {name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr _handle;
|
||||||
|
|
||||||
|
private void SetupCurrentAppFields(StringBuilder sb)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (_handle != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
//use this in debug to help there be less events
|
//use this in debug to help there be less events
|
||||||
|
|
||||||
@ -52,14 +130,15 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var processes = _audioHelper.GetPossibleProcesses();
|
var processes = _audioHelper.GetPossibleProcesses(_handle);
|
||||||
var session = _audioHelper.FindSession(processes);
|
var session = _audioHelper.FindSession(processes);
|
||||||
|
|
||||||
|
sb.AppendLine("Possible Current Processes");
|
||||||
foreach (var p in processes)
|
foreach (var p in processes)
|
||||||
{
|
{
|
||||||
var displayName = (new NameAndIconHelper()).GetProcessInfo(p);
|
var displayName = (new NameAndIconHelper()).GetProcessInfo(p);
|
||||||
|
|
||||||
sb.AppendLine($"pid: {p.Id}");
|
sb.AppendLine($"\tpid: {p.Id}");
|
||||||
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
||||||
sb.AppendLine($"\tDisplayName: {displayName}");
|
sb.AppendLine($"\tDisplayName: {displayName}");
|
||||||
|
|
||||||
@ -69,6 +148,7 @@ public partial class MainWindow : Window
|
|||||||
if (session != null)
|
if (session != null)
|
||||||
{
|
{
|
||||||
sb.AppendLine("picked the following best match");
|
sb.AppendLine("picked the following best match");
|
||||||
|
sb.AppendLine($"\tpid: {string.Join(", ", session.Pids)}");
|
||||||
sb.AppendLine($"\tsession: {session.DisplayName}");
|
sb.AppendLine($"\tsession: {session.DisplayName}");
|
||||||
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
|
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
|
||||||
}
|
}
|
||||||
@ -78,15 +158,10 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_tf.Text = sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupAllSessionFields()
|
private void SetupAllSessionFields(StringBuilder sb)
|
||||||
{
|
{
|
||||||
_tf2.Text = "";
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
|
||||||
|
|
||||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||||
@ -97,7 +172,8 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
deviceCollection.Item(i, out var device);
|
deviceCollection.Item(i, out var device);
|
||||||
//todo: put the device name in the output
|
//todo: put the device name in the output
|
||||||
sb.AppendLine($"----");
|
var name = GetDeviceName(device);
|
||||||
|
sb.AppendLine($"----{name}----");
|
||||||
|
|
||||||
|
|
||||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||||
@ -116,13 +192,36 @@ public partial class MainWindow : Window
|
|||||||
session.GetProcessId(out var processId);
|
session.GetProcessId(out var processId);
|
||||||
session.GetIconPath(out var path);
|
session.GetIconPath(out var path);
|
||||||
var audioProcess = Process.GetProcessById(processId);
|
var audioProcess = Process.GetProcessById(processId);
|
||||||
|
if(audioProcess.Id == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var displayName = (new NameAndIconHelper()).GetProcessInfo(audioProcess);
|
var displayName = (new NameAndIconHelper()).GetProcessInfo(audioProcess);
|
||||||
sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}");
|
sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
_tf2.Text = sb.ToString();
|
sb.Append("\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetDeviceName(IMMDevice device)
|
||||||
|
{
|
||||||
|
var fnkey = PKey.DeviceFriendlyName;
|
||||||
|
device.OpenPropertyStore(EStgmAccess.STGM_READ, out var propertyStore);
|
||||||
|
propertyStore.GetCount(out var count);
|
||||||
|
for(int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
propertyStore.GetAt(i, out var pkey);
|
||||||
|
if(pkey.fmtId == fnkey.fmtId && pkey.PId == fnkey.PId)
|
||||||
|
{
|
||||||
|
propertyStore.GetValue(ref pkey, out var pvalue);
|
||||||
|
|
||||||
|
return (string)pvalue.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|