Compare commits

...

3 Commits

11 changed files with 2218 additions and 52 deletions

View File

@ -65,18 +65,14 @@ public class ActiveAudioSessionWrapper : IAudioSession
{
//if you have more than one volume. they will all get set based on the first volume control
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
level += (0.01f * step) * ticks;
level = Math.Max(level, 0);
level = Math.Min(level, 1);
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
Volume.ForEach(x => x.MasterVolume = level);
}
public int GetVolumeLevel()
{
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
return (int)(level * 100);
return VolumeHelpers.GetVolumePercentage(level);
}
}

View File

@ -24,15 +24,10 @@ internal class SystemSoundsAudioSession : IAudioSession
public void IncrementVolumeLevel(int step, int ticks)
{
var level = _volumeControl.MasterVolume;
level += (0.01f * step) * ticks;
level = Math.Max(level, 0);
level = Math.Min(level, 1);
var level = VolumeHelpers.GetAdjustedVolume(_volumeControl.MasterVolume, step, ticks);
_volumeControl.MasterVolume = level;
}
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolume * 100);
public int GetVolumeLevel() => VolumeHelpers.GetVolumePercentage(_volumeControl.MasterVolume);
}

View File

@ -24,15 +24,10 @@ internal class SystemVolumeAudioSession : IAudioSession
public void IncrementVolumeLevel(int step, int ticks)
{
var level = _volumeControl.MasterVolumeLevelScalar;
level += (0.01f * step) * ticks;
level = Math.Max(level, 0);
level = Math.Min(level, 1);
var level = VolumeHelpers.GetAdjustedVolume(_volumeControl.MasterVolumeLevelScalar, step, ticks);
_volumeControl.MasterVolumeLevelScalar = level;
}
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolumeLevelScalar * 100);
public int GetVolumeLevel() => VolumeHelpers.GetVolumePercentage(_volumeControl.MasterVolumeLevelScalar);
}

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")]
public FallbackBehavior FallbackBehavior { get; set; }
[JsonProperty("stepSize")]
public int StepSize { get; set; }
public static PluginSettings CreateDefaultSettings()
{
PluginSettings instance = new PluginSettings();
@ -119,7 +122,7 @@ public class DialAction : EncoderBase
{
if (_currentAudioSession != null)
{
_currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
_currentAudioSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
await UpdateStateIfNeeded();
}
else
@ -137,13 +140,12 @@ public class DialAction : EncoderBase
public override void DialUp(DialPayload payload)
{
//dial unpressed
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Up");
}
public override void Dispose()
{
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
if(_foregroundWindowChangedEvent != IntPtr.Zero)
if (_foregroundWindowChangedEvent != IntPtr.Zero)
{
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
}
@ -151,17 +153,24 @@ public class DialAction : EncoderBase
}
public override async void OnTick()
{
try
{
//called once every 1000ms and can be used for updating the title/image of the key
var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
if(activeSession != null)
if (activeSession != null)
{
_currentAudioSession = activeSession;
}
await UpdateStateIfNeeded();
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on WinEventProc\n {ex}");
}
}
private async Task UpdateStateIfNeeded()
{
@ -170,7 +179,7 @@ public class DialAction : EncoderBase
var uiState = new UIState(_currentAudioSession);
if ( _previousState != null && uiState != null &&
if (_previousState != null && uiState != null &&
uiState.Title == _previousState.Title &&
uiState.Value.Value == _previousState.Value.Value &&
uiState.Value.Opacity == _previousState.Value.Opacity &&
@ -207,16 +216,8 @@ public class DialAction : EncoderBase
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
try
{
OnTick();
Thread.Sleep(TimeSpan.FromSeconds(1));
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on WinEventProc\n {ex}");
}
}
}

View File

@ -56,6 +56,7 @@
<ItemGroup>
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
<Compile Include="AudioHelper.cs" />
<Compile Include="AudioSessions\VolumeHelpers.cs" />
<Compile Include="AudioSessions\SystemSoundsAudioSession.cs" />
<Compile Include="AudioSessions\SystemVolumeAudioSession.cs" />
<Compile Include="DialAction.cs" />
@ -83,6 +84,9 @@
<Content Include="Images\**\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="PropertyInspector\PluginActionPI.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@ -6,11 +6,14 @@
<meta name=apple-mobile-web-app-capable content=yes>
<meta name=apple-mobile-web-app-status-bar-style content=black>
<title>FocusVolumeControl Settings</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdpi.css">
<script src="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdtools.common.js"></script>
<link rel="stylesheet" href="./lib/sdpi.css">
<link rel="sytlesheet" href="./lib/rangeTooltip.css">
<script src="lib/sdtools.common.js"></script>
<script src="lib/rangeTooltip.js"></script>
</head>
<body>
<div class="sdpi-wrapper">
<div class="sdpi-item">
<div class="sdpi-item-label">Fallback</div>
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
@ -19,12 +22,25 @@
<option value="2">Main System Volume</option>
</select>
</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>
<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>* Main System Volume - Switch to the main volume control for the system. This will change the volume of all applications</p>
</details>
</div>
</body>
</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);
}