From 9dd64ef9e7f5ad98cd228cc621bc6dcf814b39ab Mon Sep 17 00:00:00 2001 From: Stas Geft Date: Sun, 19 Sep 2021 02:51:13 +0300 Subject: [PATCH] Changing default device in-app or via hotkeys. New settings layout. --- Volumey.Package/Package.appxmanifest | 2 +- Volumey/App.xaml | 102 +++- .../CoreAudio/Interfaces/IPolicyConfig.cs | 77 +++ Volumey/CoreAudioWrapper/Wrapper/GuidValue.cs | 2 + .../CoreAudioWrapper/Wrapper/PolicyClient.cs | 20 + Volumey/DataProvider/AppSettings.cs | 44 ++ Volumey/DataProvider/DeviceProvider.cs | 24 + Volumey/DataProvider/IDeviceProvider.cs | 2 + Volumey/DataProvider/SettingsProvider.cs | 2 +- Volumey/Model/AudioSessionModel.cs | 4 +- Volumey/Model/MasterSessionModel.cs | 4 +- Volumey/Model/OutputDeviceModel.cs | 1 - Volumey/Resources/Resources.de.resx | 18 + Volumey/Resources/Resources.en.resx | 18 + Volumey/Resources/Resources.es.resx | 18 + Volumey/Resources/Resources.fr.resx | 18 + Volumey/Resources/Resources.hi.resx | 18 + Volumey/Resources/Resources.it.resx | 18 + Volumey/Resources/Resources.ja.resx | 18 + Volumey/Resources/Resources.pt.resx | 18 + Volumey/Resources/Resources.resx | 18 + Volumey/Resources/Resources.ru.resx | 18 + Volumey/Resources/Resources.tr.resx | 18 + Volumey/Resources/Resources.zh.resx | 18 + .../View/Converters/BrushToColorConverter.cs | 22 + .../DeviceIsDefaultToEnabledConverter.cs | 22 + .../DeviceIsDefaultToIconConverter.cs | 35 ++ .../Converters/LocalizationKeyConverter.cs | 22 + Volumey/View/MainView.xaml | 67 +- Volumey/View/MasterView.xaml | 90 ++- Volumey/View/MixerView.xaml | 3 +- Volumey/View/SessionsListView.xaml | 4 +- .../View/SettingsPage/AppsHotkeysPage.xaml | 105 ++++ .../View/SettingsPage/AppsHotkeysPage.xaml.cs | 10 + .../DefaultDeviceHotkeysPage.xaml | 93 +++ .../DefaultDeviceHotkeysPage.xaml.cs | 10 + .../SettingsPage/DeviceVolumeHotkeysPage.xaml | 59 ++ .../DeviceVolumeHotkeysPage.xaml.cs | 10 + Volumey/View/SettingsPage/MiscPage.xaml | 79 +++ Volumey/View/SettingsPage/MiscPage.xaml.cs | 19 + .../SettingsPage/OpenMixerHotkeyPage.xaml | 39 ++ .../SettingsPage/OpenMixerHotkeyPage.xaml.cs | 10 + Volumey/View/SettingsView.xaml | 571 ++++++------------ Volumey/View/SettingsView.xaml.cs | 44 +- Volumey/ViewModel/DeviceProviderViewModel.cs | 29 + .../Settings/AppsHotkeysViewModel.cs | 6 +- .../Settings/DefaultDeviceHotkeysViewModel.cs | 194 ++++++ ...del.cs => DeviceVolumeHotkeysViewModel.cs} | 8 +- Volumey/ViewModel/Settings/HotkeysControl.cs | 60 +- Volumey/ViewModel/SettingsViewModel.cs | 6 +- 50 files changed, 1630 insertions(+), 487 deletions(-) create mode 100644 Volumey/CoreAudioWrapper/CoreAudio/Interfaces/IPolicyConfig.cs create mode 100644 Volumey/CoreAudioWrapper/Wrapper/PolicyClient.cs create mode 100644 Volumey/View/Converters/BrushToColorConverter.cs create mode 100644 Volumey/View/Converters/DeviceIsDefaultToEnabledConverter.cs create mode 100644 Volumey/View/Converters/DeviceIsDefaultToIconConverter.cs create mode 100644 Volumey/View/Converters/LocalizationKeyConverter.cs create mode 100644 Volumey/View/SettingsPage/AppsHotkeysPage.xaml create mode 100644 Volumey/View/SettingsPage/AppsHotkeysPage.xaml.cs create mode 100644 Volumey/View/SettingsPage/DefaultDeviceHotkeysPage.xaml create mode 100644 Volumey/View/SettingsPage/DefaultDeviceHotkeysPage.xaml.cs create mode 100644 Volumey/View/SettingsPage/DeviceVolumeHotkeysPage.xaml create mode 100644 Volumey/View/SettingsPage/DeviceVolumeHotkeysPage.xaml.cs create mode 100644 Volumey/View/SettingsPage/MiscPage.xaml create mode 100644 Volumey/View/SettingsPage/MiscPage.xaml.cs create mode 100644 Volumey/View/SettingsPage/OpenMixerHotkeyPage.xaml create mode 100644 Volumey/View/SettingsPage/OpenMixerHotkeyPage.xaml.cs create mode 100644 Volumey/ViewModel/Settings/DefaultDeviceHotkeysViewModel.cs rename Volumey/ViewModel/Settings/{DeviceHotkeysViewModel.cs => DeviceVolumeHotkeysViewModel.cs} (93%) diff --git a/Volumey.Package/Package.appxmanifest b/Volumey.Package/Package.appxmanifest index 505ced7..39247fa 100644 --- a/Volumey.Package/Package.appxmanifest +++ b/Volumey.Package/Package.appxmanifest @@ -10,7 +10,7 @@ + Version="1.3.0.0"/> Volumey diff --git a/Volumey/App.xaml b/Volumey/App.xaml index 537cde8..d319a43 100644 --- a/Volumey/App.xaml +++ b/Volumey/App.xaml @@ -66,16 +66,110 @@ 0 6 - + + + + + + + + + \ No newline at end of file diff --git a/Volumey/CoreAudioWrapper/CoreAudio/Interfaces/IPolicyConfig.cs b/Volumey/CoreAudioWrapper/CoreAudio/Interfaces/IPolicyConfig.cs new file mode 100644 index 0000000..1b2d573 --- /dev/null +++ b/Volumey/CoreAudioWrapper/CoreAudio/Interfaces/IPolicyConfig.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.InteropServices; +using Volumey.CoreAudioWrapper.CoreAudio.Enums; +using Volumey.CoreAudioWrapper.Wrapper; + +namespace Volumey.CoreAudioWrapper.CoreAudio.Interfaces +{ + [Guid(GuidValue.External.IPolicyConfig), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPolicyConfig + { + [PreserveSig] + int GetMixFormat( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In] IntPtr ppFormat); + + [PreserveSig] + int GetDeviceFormat( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In][MarshalAs(UnmanagedType.Bool)] bool bDefault, + [In] IntPtr ppFormat); + + [PreserveSig] + int ResetDeviceFormat([In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName); + + [PreserveSig] + int SetDeviceFormat( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In] IntPtr pEndpointFormat, + [In] IntPtr mixFormat); + + [PreserveSig] + int GetProcessingPeriod( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In][MarshalAs(UnmanagedType.Bool)] bool bDefault, + [In] IntPtr pmftDefaultPeriod, + [In] IntPtr pmftMinimumPeriod); + + [PreserveSig] + int SetProcessingPeriod( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In] IntPtr pmftPeriod); + + [PreserveSig] + int GetShareMode( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In] IntPtr pMode); + + [PreserveSig] + int SetShareMode( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In] IntPtr mode); + + [PreserveSig] + int GetPropertyValue( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In][MarshalAs(UnmanagedType.Bool)] bool bFxStore, + [In] IntPtr key, + [In] IntPtr pv); + + [PreserveSig] + int SetPropertyValue( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In][MarshalAs(UnmanagedType.Bool)] bool bFxStore, + [In] IntPtr key, + [In] IntPtr pv); + + [PreserveSig] + int SetDefaultEndpoint( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In][MarshalAs(UnmanagedType.U4)] ERole role); + + [PreserveSig] + int SetEndpointVisibility( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszDeviceName, + [In][MarshalAs(UnmanagedType.Bool)] bool bVisible); + } +} \ No newline at end of file diff --git a/Volumey/CoreAudioWrapper/Wrapper/GuidValue.cs b/Volumey/CoreAudioWrapper/Wrapper/GuidValue.cs index cebd572..8a91928 100644 --- a/Volumey/CoreAudioWrapper/Wrapper/GuidValue.cs +++ b/Volumey/CoreAudioWrapper/Wrapper/GuidValue.cs @@ -26,6 +26,8 @@ public static class External public const string IMMNotificationClient = "7991EEC9-7E89-4D85-8390-6C703CEC60C0"; public const string IPropertyStore = "886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"; public const string IMMEndpoint = "1BE09788-6894-4089-8586-9A2A6C265AC5"; + public const string IPolicyConfig = "F8679F50-850A-41CF-9C72-430F290290C8"; + public const string PolicyConfigCOM = "870AF99C-171D-4F9E-AF0D-E63DF40C2BC9"; } /// diff --git a/Volumey/CoreAudioWrapper/Wrapper/PolicyClient.cs b/Volumey/CoreAudioWrapper/Wrapper/PolicyClient.cs new file mode 100644 index 0000000..c9fdfae --- /dev/null +++ b/Volumey/CoreAudioWrapper/Wrapper/PolicyClient.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; +using Volumey.CoreAudioWrapper.CoreAudio.Enums; +using Volumey.CoreAudioWrapper.CoreAudio.Interfaces; + +namespace Volumey.CoreAudioWrapper.Wrapper +{ + public class PolicyClient + { + [ComImport, Guid(GuidValue.External.PolicyConfigCOM)] + private class PolicyClientCOM { } + + private IPolicyConfig instance = new PolicyClientCOM() as IPolicyConfig; + + public void SetDefaultEndpointDevice(string deviceId) + { + this.instance.SetDefaultEndpoint(deviceId, ERole.Console); + this.instance.SetDefaultEndpoint(deviceId, ERole.Multimedia); + } + } +} \ No newline at end of file diff --git a/Volumey/DataProvider/AppSettings.cs b/Volumey/DataProvider/AppSettings.cs index 2f27ffd..f80006c 100644 --- a/Volumey/DataProvider/AppSettings.cs +++ b/Volumey/DataProvider/AppSettings.cs @@ -217,6 +217,12 @@ internal bool HotkeyExists(HotKey key) if(session.Value.up.ToHotKey().Equals(key) || session.Value.down.ToHotKey().Equals(key)) return true; } + foreach(var value in this.serializableRegisteredDevices) + { + var hotkey = value.Key.ToHotKey(); + if(hotkey.Equals(key)) + return true; + } return false; } @@ -235,6 +241,13 @@ internal bool HotkeysExist(HotKey key, HotKey key2) if(up.Equals(key) || up.Equals(key2) || down.Equals(key) || down.Equals(key2)) return true; } + + foreach(var value in this.serializableRegisteredDevices) + { + var hotkey = value.Key.ToHotKey(); + if(hotkey.Equals(key) || hotkey.Equals(key2)) + return true; + } return false; } @@ -252,6 +265,37 @@ internal void RemoveRegisteredSession(string sessionName) this.serializableRegisteredSessions.Remove(sessionName); } + [OptionalField] + private Dictionary serializableRegisteredDevices; + + internal ObservableConcurrentDictionary> GetRegisteredDevices() + { + var dict = new ObservableConcurrentDictionary>(); + if(this.serializableRegisteredDevices == null) + { + this.serializableRegisteredDevices = new Dictionary(); + return dict; + } + + if(this.serializableRegisteredDevices.Count == 0) + return dict; + + foreach(var tuple in this.serializableRegisteredDevices) + dict.Add(tuple.Key.ToHotKey(), new Tuple(tuple.Value.id, tuple.Value.name)); + + return dict; + } + + internal void AddRegisteredDevice(HotKey hotkey, string deviceId, string name) + { + this.serializableRegisteredDevices.Add(hotkey.ToSerializableHotkey(), (deviceId, name)); + } + + internal void RemoveRegisteredDevice(HotKey hotkey) + { + this.serializableRegisteredDevices.Remove(hotkey.ToSerializableHotkey()); + } + [OnDeserialized] private void OnDeserialized(StreamingContext context) { diff --git a/Volumey/DataProvider/DeviceProvider.cs b/Volumey/DataProvider/DeviceProvider.cs index 197bb0c..0869e6b 100644 --- a/Volumey/DataProvider/DeviceProvider.cs +++ b/Volumey/DataProvider/DeviceProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Windows.Threading; @@ -30,6 +31,8 @@ public OutputDeviceModel DefaultDevice OnPropertyChanged(); } } + + private readonly PolicyClient PolicyClient = new PolicyClient(); private bool noOutputDevices; public bool NoOutputDevices @@ -102,6 +105,27 @@ public static IDeviceProvider GetInstance() return instance; } + public void SetDefaultDevice(string id) + { + try + { + //Check if the device to set is enabled + if(this.ActiveDevices.Any(device => device.CompareId(id))) + { + //Prevent if it's already the default device + if(DefaultDevice != null && !DefaultDevice.CompareId(id)) + { + // Debug.WriteLine($"Activating device {id}"); + this.PolicyClient.SetDefaultEndpointDevice(id); + } + } + } + catch(Exception e) + { + Logger.Error("Failed to change default device", e); + } + } + private void OnDeviceDisabled(OutputDeviceModel device) { dispatcher.Invoke(() => diff --git a/Volumey/DataProvider/IDeviceProvider.cs b/Volumey/DataProvider/IDeviceProvider.cs index 45f0be1..f6ad22e 100644 --- a/Volumey/DataProvider/IDeviceProvider.cs +++ b/Volumey/DataProvider/IDeviceProvider.cs @@ -17,5 +17,7 @@ public interface IDeviceProvider : IDisposable event Action DeviceFormatChanged; public bool NoOutputDevices { get; set; } + + public void SetDefaultDevice(string deviceId); } } \ No newline at end of file diff --git a/Volumey/DataProvider/SettingsProvider.cs b/Volumey/DataProvider/SettingsProvider.cs index 0cd7395..fcc09a8 100644 --- a/Volumey/DataProvider/SettingsProvider.cs +++ b/Volumey/DataProvider/SettingsProvider.cs @@ -82,7 +82,7 @@ await using(var fileStream = new FileStream(SettingsFullPath, FileMode.OpenOrCre } } - private static AppSettings.SerializableHotkey ToSerializableHotkey(this HotKey hotkey) + internal static AppSettings.SerializableHotkey ToSerializableHotkey(this HotKey hotkey) { if(hotkey == null) return new AppSettings.SerializableHotkey(Key.None, ModifierKeys.None); diff --git a/Volumey/Model/AudioSessionModel.cs b/Volumey/Model/AudioSessionModel.cs index 6a3eb44..6e8f725 100644 --- a/Volumey/Model/AudioSessionModel.cs +++ b/Volumey/Model/AudioSessionModel.cs @@ -115,7 +115,7 @@ public bool SetHotkeys(HotKey volUp, HotKey volDown) { try { - if(volUp != null && volDown != null && HotkeysControl.RegisterVolumeHotkeys(volUp, volDown)) + if(volUp != null && volDown != null && HotkeysControl.RegisterHotkeysPair(volUp, volDown)) { this.volumeUp = volUp; this.volumeDown = volDown; @@ -138,7 +138,7 @@ private void UnregisterHotkeys() { try { - HotkeysControl.UnregisterVolumeHotkeys(this.volumeUp, this.volumeDown); + HotkeysControl.UnregisterHotkeysPair(this.volumeUp, this.volumeDown); } catch { } } diff --git a/Volumey/Model/MasterSessionModel.cs b/Volumey/Model/MasterSessionModel.cs index 089b82f..9e1d771 100644 --- a/Volumey/Model/MasterSessionModel.cs +++ b/Volumey/Model/MasterSessionModel.cs @@ -115,7 +115,7 @@ public bool SetHotkeys(HotKey volUp, HotKey volDown) { try { - if(volUp != null && volDown != null && HotkeysControl.RegisterVolumeHotkeys(volUp, volDown)) + if(volUp != null && volDown != null && HotkeysControl.RegisterHotkeysPair(volUp, volDown)) { this.volumeUp = volUp; this.volumeDown = volDown; @@ -136,7 +136,7 @@ internal void ResetHotkeys() private void UnregisterHotkeys() { if(this.volumeUp != null && this.volumeDown != null) - HotkeysControl.UnregisterVolumeHotkeys(this.volumeUp, this.volumeDown); + HotkeysControl.UnregisterHotkeysPair(this.volumeUp, this.volumeDown); HotkeysControl.HotkeyPressed -= OnHotkeyPressed; } diff --git a/Volumey/Model/OutputDeviceModel.cs b/Volumey/Model/OutputDeviceModel.cs index 2299f00..983eb43 100644 --- a/Volumey/Model/OutputDeviceModel.cs +++ b/Volumey/Model/OutputDeviceModel.cs @@ -2,7 +2,6 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; -using System.Threading; using System.Windows.Threading; using Volumey.Controls; using Volumey.CoreAudioWrapper.CoreAudio; diff --git a/Volumey/Resources/Resources.de.resx b/Volumey/Resources/Resources.de.resx index 0ec067d..2393bde 100644 --- a/Volumey/Resources/Resources.de.resx +++ b/Volumey/Resources/Resources.de.resx @@ -157,4 +157,22 @@ Kauf mir einen Kaffee :) + + Gerät hinzufügen + + + Standardgerät per Hotkey ändern + + + Ausgabegerät + + + Gerät entfernen + + + Ändern Sie das Standardgerät + + + Hotkey + \ No newline at end of file diff --git a/Volumey/Resources/Resources.en.resx b/Volumey/Resources/Resources.en.resx index bc55329..bc388fa 100644 --- a/Volumey/Resources/Resources.en.resx +++ b/Volumey/Resources/Resources.en.resx @@ -157,4 +157,22 @@ Buy me a coffee :) + + Add device + + + Change default device by hotkey + + + Remove device + + + Change the default device + + + Output device + + + Hotkey + \ No newline at end of file diff --git a/Volumey/Resources/Resources.es.resx b/Volumey/Resources/Resources.es.resx index babf00e..f77ec6c 100644 --- a/Volumey/Resources/Resources.es.resx +++ b/Volumey/Resources/Resources.es.resx @@ -157,4 +157,22 @@ Cómprame un café :) + + Añadir dispositivo + + + Cambiar el dispositivo predeterminado mediante una tecla de acceso rápido + + + Dispositivo de salida + + + Retire el dispositivo + + + Cambiar el dispositivo predeterminado + + + Tecla de acceso rápido + \ No newline at end of file diff --git a/Volumey/Resources/Resources.fr.resx b/Volumey/Resources/Resources.fr.resx index 6e9e4e4..0acc7d0 100644 --- a/Volumey/Resources/Resources.fr.resx +++ b/Volumey/Resources/Resources.fr.resx @@ -157,4 +157,22 @@ Achetez-moi un café :) + + Ajouter un appareil + + + Définir l'appareil par défaut par raccourci clavier + + + Dispositif + + + Enlevez l'appareil + + + Changer l'appareil par défaut + + + Raccourci clavier + \ No newline at end of file diff --git a/Volumey/Resources/Resources.hi.resx b/Volumey/Resources/Resources.hi.resx index 787c045..302b2a3 100644 --- a/Volumey/Resources/Resources.hi.resx +++ b/Volumey/Resources/Resources.hi.resx @@ -157,4 +157,22 @@ मुझे एक कॉफी खरीदो :) + + डिवाइस जोडे + + + हॉटकी द्वारा डिफ़ॉल्ट डिवाइस बदलें + + + आउटपुट डिवाइस + + + डिवाइस हटाएं + + + डिफ़ॉल्ट डिवाइस बदलें + + + हॉटकी + \ No newline at end of file diff --git a/Volumey/Resources/Resources.it.resx b/Volumey/Resources/Resources.it.resx index 9bbaed4..f4575fa 100644 --- a/Volumey/Resources/Resources.it.resx +++ b/Volumey/Resources/Resources.it.resx @@ -157,4 +157,22 @@ Offrimi un caffè :) + + Aggiungi dispositivo + + + Cambia dispositivo predefinito tramite tasto di scelta rapida + + + Dispositivo di uscita + + + Rimuovi dispositivo + + + Cambia il dispositivo predefinito + + + Tasto di scelta rapida + \ No newline at end of file diff --git a/Volumey/Resources/Resources.ja.resx b/Volumey/Resources/Resources.ja.resx index 2db9725..c22c3e1 100644 --- a/Volumey/Resources/Resources.ja.resx +++ b/Volumey/Resources/Resources.ja.resx @@ -157,4 +157,22 @@ 私にコーヒーを買ってください:) + + デバイスを追加 + + + ホットキーでデフォルトのデバイスを変更する + + + 出力機器 + + + デバイスを削除 + + + デフォルトのデバイスを変更する + + + ホットキー + \ No newline at end of file diff --git a/Volumey/Resources/Resources.pt.resx b/Volumey/Resources/Resources.pt.resx index 17194e1..0d26b37 100644 --- a/Volumey/Resources/Resources.pt.resx +++ b/Volumey/Resources/Resources.pt.resx @@ -157,4 +157,22 @@ Compre-me um café :) + + Adicionar Dispositivo + + + Alterar dispositivo padrão por tecla de atalho + + + Dispositivo de saída + + + Remover dispositivo + + + Alterar o dispositivo padrão + + + Tecla de atalho + \ No newline at end of file diff --git a/Volumey/Resources/Resources.resx b/Volumey/Resources/Resources.resx index aa5a3fe..57a5c3f 100644 --- a/Volumey/Resources/Resources.resx +++ b/Volumey/Resources/Resources.resx @@ -114,4 +114,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Volumey/Resources/Resources.ru.resx b/Volumey/Resources/Resources.ru.resx index 3229a30..d9f9563 100644 --- a/Volumey/Resources/Resources.ru.resx +++ b/Volumey/Resources/Resources.ru.resx @@ -157,4 +157,22 @@ Купите мне кофе :) + + Добавить устройство + + + Изменить устройство вывода по умолчанию по хоткею + + + Удалить устройство + + + Сменить устройство вывода по умолчанию + + + Устройство вывода + + + Хоткей + \ No newline at end of file diff --git a/Volumey/Resources/Resources.tr.resx b/Volumey/Resources/Resources.tr.resx index 067924b..2cba123 100644 --- a/Volumey/Resources/Resources.tr.resx +++ b/Volumey/Resources/Resources.tr.resx @@ -154,4 +154,22 @@ Bana bir kahve ısmarla :) + + Kısayol tuşuyla varsayılan cihazı değiştir + + + Çıkış aygıtı + + + Aygıtı kaldır + + + Varsayılan cihazı değiştir + + + Cihaz ekle + + + Kısayol tuşu + \ No newline at end of file diff --git a/Volumey/Resources/Resources.zh.resx b/Volumey/Resources/Resources.zh.resx index 01d6c8b..fa0aa28 100644 --- a/Volumey/Resources/Resources.zh.resx +++ b/Volumey/Resources/Resources.zh.resx @@ -157,4 +157,22 @@ 给我买杯咖啡:) + + 添加设备 + + + 通过热键设置默认设备 + + + 输出设备 + + + 删除设备 + + + 更改默认设备 + + + 热键 + \ No newline at end of file diff --git a/Volumey/View/Converters/BrushToColorConverter.cs b/Volumey/View/Converters/BrushToColorConverter.cs new file mode 100644 index 0000000..77ab85f --- /dev/null +++ b/Volumey/View/Converters/BrushToColorConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace Volumey.View.Converters +{ + public class BrushToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(value is SolidColorBrush brush) + return brush.Color; + return Binding.DoNothing; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} \ No newline at end of file diff --git a/Volumey/View/Converters/DeviceIsDefaultToEnabledConverter.cs b/Volumey/View/Converters/DeviceIsDefaultToEnabledConverter.cs new file mode 100644 index 0000000..fb44264 --- /dev/null +++ b/Volumey/View/Converters/DeviceIsDefaultToEnabledConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Volumey.Model; + +namespace Volumey.View.Converters +{ + public class DeviceIsDefaultToEnabledConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if(values != null && values[0] is OutputDeviceModel selectedDevice && values[1] is OutputDeviceModel currentDefaultDevice) + return selectedDevice != currentDefaultDevice; + return false; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + return new object[] { }; + } + } +} \ No newline at end of file diff --git a/Volumey/View/Converters/DeviceIsDefaultToIconConverter.cs b/Volumey/View/Converters/DeviceIsDefaultToIconConverter.cs new file mode 100644 index 0000000..429e326 --- /dev/null +++ b/Volumey/View/Converters/DeviceIsDefaultToIconConverter.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; +using ModernWpf.Controls; +using Volumey.Model; + +namespace Volumey.View.Converters +{ + public class DeviceIsDefaultToIconConverter : IMultiValueConverter + { + private static readonly FontIcon CurrentDefaultDeviceIcon; + + static DeviceIsDefaultToIconConverter() + { + CurrentDefaultDeviceIcon = new FontIcon { Glyph = "\xE73E", FontFamily = new FontFamily("Segoe MDL2 Assets") }; + + } + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if(values != null && values[0] is OutputDeviceModel model && values[1] is OutputDeviceModel defaultDevice) + { + if(model == defaultDevice) + return CurrentDefaultDeviceIcon; + } + return null; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + return new object[] { }; + } + } +} \ No newline at end of file diff --git a/Volumey/View/Converters/LocalizationKeyConverter.cs b/Volumey/View/Converters/LocalizationKeyConverter.cs new file mode 100644 index 0000000..b2dc1d6 --- /dev/null +++ b/Volumey/View/Converters/LocalizationKeyConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Volumey.Localization; + +namespace Volumey.View.Converters +{ + public class LocalizationKeyConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if(values != null && values[0] is string key) + return TranslationSource.Instance[key]; + return null; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + return new object[] { }; + } + } +} \ No newline at end of file diff --git a/Volumey/View/MainView.xaml b/Volumey/View/MainView.xaml index 9766128..00ebe9e 100644 --- a/Volumey/View/MainView.xaml +++ b/Volumey/View/MainView.xaml @@ -47,17 +47,35 @@ + + + - + ui:ControlHelper.CornerRadius="6" + Opacity="0.95"> + + + + + 0 + + + + + + + + + + + + + + + + Margin="4 3 0 3"/> diff --git a/Volumey/View/MasterView.xaml b/Volumey/View/MasterView.xaml index b0b0af4..77dd8a0 100644 --- a/Volumey/View/MasterView.xaml +++ b/Volumey/View/MasterView.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:converters="clr-namespace:Volumey.View.Converters" xmlns:lc="clr-namespace:Volumey.Localization" - xmlns:modern="http://schemas.modernwpf.com/2019" + xmlns:ui="http://schemas.modernwpf.com/2019" xmlns:controls="clr-namespace:Volumey.Controls" mc:Ignorable="d"> @@ -15,6 +15,13 @@ + + + + + + + @@ -28,17 +35,60 @@ - + Width="Auto" + MaxWidth="300" + Margin="42 0 0 0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -75,22 +125,22 @@ VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Margin="0 -2 0 0" /> - - + Margin="-4 -8 0 0"> + - - - + + - - + + diff --git a/Volumey/View/MixerView.xaml b/Volumey/View/MixerView.xaml index 74fd155..9ac218d 100644 --- a/Volumey/View/MixerView.xaml +++ b/Volumey/View/MixerView.xaml @@ -6,7 +6,7 @@ xmlns:ui="http://schemas.modernwpf.com/2019" xmlns:view="clr-namespace:Volumey.View" mc:Ignorable="d"> - + @@ -25,6 +25,5 @@ Grid.Row="2"> - \ No newline at end of file diff --git a/Volumey/View/SessionsListView.xaml b/Volumey/View/SessionsListView.xaml index 1641beb..ff5eead 100644 --- a/Volumey/View/SessionsListView.xaml +++ b/Volumey/View/SessionsListView.xaml @@ -75,9 +75,9 @@ Grid.Row="1" Command="{Binding MuteCommand}" Background="Transparent" - Width="35" + Width="30" Height="40" - Margin="-7 -8 0 0"> + Margin="-5 -8 0 0"> diff --git a/Volumey/View/SettingsPage/AppsHotkeysPage.xaml b/Volumey/View/SettingsPage/AppsHotkeysPage.xaml new file mode 100644 index 0000000..2e11518 --- /dev/null +++ b/Volumey/View/SettingsPage/AppsHotkeysPage.xaml @@ -0,0 +1,105 @@ + + + + + - - - - - - - - - - - - - - - + + + + + Name="ScrollViewer" + Margin="4 0 0 0"> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/Volumey/View/SettingsView.xaml.cs b/Volumey/View/SettingsView.xaml.cs index 62817b5..0da7c7b 100644 --- a/Volumey/View/SettingsView.xaml.cs +++ b/Volumey/View/SettingsView.xaml.cs @@ -1,19 +1,51 @@ -using ModernWpf.Controls; +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using ModernWpf.Controls; +using ModernWpf.Media.Animation; +using Volumey.View.SettingsPage; namespace Volumey.View { - public partial class SettingsView + public partial class SettingsView : INotifyPropertyChanged { + public class NavLink + { + public string LocalizationKey { get; } + public Type PageType { get; } + + public NavLink(string localizationKey, Type pageType) + { + this.LocalizationKey = localizationKey; + this.PageType = pageType; + } + } + + public ObservableCollection NavLinks { get; } = new ObservableCollection(); + public NavLink SelectedNavLink { get; set; } + public SettingsView() { + NavLinks.Add(new NavLink("Settings_AppsHotkeys", typeof(AppsHotkeysPage))); + NavLinks.Add(new NavLink("Settings_DeviceHotkeys", typeof(DeviceVolumeHotkeysPage))); + NavLinks.Add(new NavLink("Settings_DefaultDeviceHotkey", typeof(DefaultDeviceHotkeysPage))); + NavLinks.Add(new NavLink("Settings_OpenMixer", typeof(OpenMixerHotkeyPage))); + NavLinks.Add(new NavLink("Settings_HeaderMisc", typeof(MiscPage))); InitializeComponent(); + this.SelectedNavLink = NavLinks[0]; + this.PageContentFrame.Navigate(this.SelectedNavLink.PageType); } - private void NumberBox_OnValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) + private NavigationTransitionInfo transition = new DrillInNavigationTransitionInfo(); + + private void OnNavLinkItemClick(object sender, ItemClickEventArgs e) { - //prevent leaving number boxes empty - if (double.IsNaN(args.NewValue)) - sender.Value = args.OldValue; + if(this.SelectedNavLink == null) + return; + if(this.PageContentFrame.CurrentSourcePageType != this.SelectedNavLink.PageType) + this.PageContentFrame.Navigate(this.SelectedNavLink.PageType, null, transition); } + + public event PropertyChangedEventHandler PropertyChanged; } } \ No newline at end of file diff --git a/Volumey/ViewModel/DeviceProviderViewModel.cs b/Volumey/ViewModel/DeviceProviderViewModel.cs index 6c4c2d6..ffd5e42 100644 --- a/Volumey/ViewModel/DeviceProviderViewModel.cs +++ b/Volumey/ViewModel/DeviceProviderViewModel.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Windows.Input; using System.Windows.Threading; +using Microsoft.Xaml.Behaviors.Core; using Volumey.DataProvider; using Volumey.Model; @@ -45,6 +47,19 @@ public DeviceProviderViewModel() if(App.Current != null) App.Current.Exit += (s, a) => this.DeviceProvider.Dispose(); + + this.SetDefaultDeviceCommand = new ActionCommand(() => + { + if(this.SelectedDevice == null) + return; + this.DeviceProvider.SetDefaultDevice(this.SelectedDevice.Id); + }); + + this.SetDefaultDeviceTrayCommand = new ActionCommand((param) => + { + if(param is OutputDeviceModel model) + SetAsDefaultDevice(model.Id); + }); } private void OnDefaultDeviceChanged(OutputDeviceModel newDevice) @@ -79,6 +94,20 @@ private void OnDeviceDisabled(OutputDeviceModel device) }); } + public ICommand SetDefaultDeviceCommand { get; } + public ICommand SetDefaultDeviceTrayCommand { get; } + + private void SetAsDefaultDevice(string deviceId) + { + if(string.IsNullOrEmpty(deviceId)) + return; + + if(DeviceProvider.DefaultDevice != null && DeviceProvider.DefaultDevice.CompareId(deviceId)) + return; + + this.DeviceProvider.SetDefaultDevice(deviceId); + } + public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string prop = null) diff --git a/Volumey/ViewModel/Settings/AppsHotkeysViewModel.cs b/Volumey/ViewModel/Settings/AppsHotkeysViewModel.cs index 3f45beb..82e024f 100644 --- a/Volumey/ViewModel/Settings/AppsHotkeysViewModel.cs +++ b/Volumey/ViewModel/Settings/AppsHotkeysViewModel.cs @@ -149,13 +149,9 @@ private void OnDefaultDeviceChanged(OutputDeviceModel newDevice) } this.DefaultDevice = newDevice; - if(this.DefaultDevice == null) - HotkeysControl.SetHotkeysState(HotkeysState.Disabled); - else if(this.RegisteredSessions.Keys.Count != 0) + if(this.RegisteredSessions.Keys.Count != 0) { - if(HotkeysControl.VolumeHotkeysState == HotkeysState.Disabled) - HotkeysControl.SetHotkeysState(HotkeysState.Enabled); this.DefaultDevice.SessionCreated += OnSessionCreated; this.FindAndSetupRegisteredHotkeys(); } diff --git a/Volumey/ViewModel/Settings/DefaultDeviceHotkeysViewModel.cs b/Volumey/ViewModel/Settings/DefaultDeviceHotkeysViewModel.cs new file mode 100644 index 0000000..d36848b --- /dev/null +++ b/Volumey/ViewModel/Settings/DefaultDeviceHotkeysViewModel.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using log4net; +using Microsoft.Xaml.Behaviors.Core; +using Volumey.Controls; +using Volumey.DataProvider; +using Volumey.Model; + +namespace Volumey.ViewModel.Settings +{ + public class DefaultDeviceHotkeysViewModel : HotkeyViewModel + { + private OutputDeviceModel _selected; + public OutputDeviceModel SelectedDevice + { + get => this._selected; + set + { + this._selected = value; + OnPropertyChanged(); + } + } + + private KeyValuePair>? _selectedRegDev; + public KeyValuePair>? SelectedRegDev + { + get => this._selectedRegDev; + set + { + this._selectedRegDev = value; + OnPropertyChanged(); + } + } + + public ObservableConcurrentDictionary> RegisteredDevices { get; } + + private HotKey _hotkey; + public HotKey HotKey + { + get => this._hotkey; + set + { + this._hotkey = value; + OnPropertyChanged(); + } + } + + public ICommand AddDeviceCommand { get; } + public ICommand RemoveDeviceCommand { get; } + + public IDeviceProvider DeviceProvider { get; } + + private ILog _logger; + private ILog Logger => this._logger ??= LogManager.GetLogger(typeof(DefaultDeviceHotkeysViewModel)); + + public DefaultDeviceHotkeysViewModel() + { + ErrorDictionary.LanguageChanged += () => { this.SetErrorMessage(this.CurrentErrorType);}; + + this.DeviceProvider = DataProvider.DeviceProvider.GetInstance(); + + this.AddDeviceCommand = new ActionCommand(AddDevice); + this.RemoveDeviceCommand = new ActionCommand(RemoveDevice); + + this.RegisteredDevices = SettingsProvider.HotkeysSettings.GetRegisteredDevices(); + if (this.RegisteredDevices.Keys.Count > 0) + { + if (HotkeysControl.IsActive) + this.RegisterLoadedHotkeys(); + else + HotkeysControl.Activated += this.RegisterLoadedHotkeys; + + //Update registered devices names once on startup + UpdateNames(); + } + } + + private void RegisterLoadedHotkeys() + { + HotkeysControl.HotkeyPressed += OnHotkeyPressed; + foreach (var tuple in this.RegisteredDevices) + HotkeysControl.RegisterHotkey(tuple.Key); + } + + /// + /// Check if registered devices have changed their names and update them + /// + private void UpdateNames() + { + try + { + for (int i = 0; i < this.DeviceProvider.ActiveDevices.Count; i++) + { + var activeDevice = this.DeviceProvider.ActiveDevices[i]; + foreach (var device in this.RegisteredDevices) + { + var id = device.Value.Item1; + if (activeDevice.CompareId(id)) + { + var actualFriendlyName = activeDevice.Master.DeviceFriendlyName; + var friendlyName = device.Value.Item2; + if (!actualFriendlyName.Equals(friendlyName)) + this.RegisteredDevices[device.Key] = new Tuple(id, actualFriendlyName); + } + } + } + } + catch { } + } + + private void AddDevice() + { + if(this.RegisteredDevices.Values.Any(val => val.Item1.Equals(this.SelectedDevice.Id))) + return; + + if (this.RegisteredDevices.ContainsKey(this.HotKey)) + { + this.SetErrorMessage(ErrorMessageType.HotkeyExists); + return; + } + + if (HotkeysControl.HotkeyIsValid(this.HotKey) is var error && error != ErrorMessageType.None) + { + this.SetErrorMessage(error); + return; + } + + this.SetErrorMessage(ErrorMessageType.None); + + if (RegisteredDevices.Keys.Count == 0) + HotkeysControl.HotkeyPressed += OnHotkeyPressed; + + if (!HotkeysControl.RegisterHotkey(this.HotKey)) + { + this.SetErrorMessage(ErrorMessageType.OpenReg); + return; + } + var id = this.SelectedDevice.Id; + var name = this.SelectedDevice.Master.DeviceFriendlyName; + var hotkey = this.HotKey; + this.RegisteredDevices.Add(hotkey, new Tuple(id, name)); + this.SelectedDevice = null; + this.HotKey = null; + Task.Run(() => + { + try + { + this.Logger.Info($"Registered default device hotkey: {hotkey.ToString()}, count: {this.RegisteredDevices.Keys.Count.ToString()}"); + SettingsProvider.HotkeysSettings.AddRegisteredDevice(hotkey, id, name); + _ = SettingsProvider.SaveSettings(); + } + catch { } + }); + + } + + private void RemoveDevice() + { + if (this.SelectedRegDev == null || !this.SelectedRegDev.HasValue) + return; + + var hotkey = this.SelectedRegDev.Value.Key; + + HotkeysControl.UnregisterHotkey(hotkey); + this.RegisteredDevices.Remove(hotkey); + + if (this.RegisteredDevices.Keys.Count == 0) + HotkeysControl.HotkeyPressed -= OnHotkeyPressed; + + this.SelectedRegDev = null; + this.SetErrorMessage(ErrorMessageType.None); + + try + { + SettingsProvider.HotkeysSettings.RemoveRegisteredDevice(hotkey); + _ = SettingsProvider.SaveSettings(); + } + catch { } + } + + private void OnHotkeyPressed(HotKey pressedHotkey) + { + if (RegisteredDevices.TryGetValue(pressedHotkey, out var value)) + { + var id = value.Item1; + this.DeviceProvider.SetDefaultDevice(id); + } + } + } +} \ No newline at end of file diff --git a/Volumey/ViewModel/Settings/DeviceHotkeysViewModel.cs b/Volumey/ViewModel/Settings/DeviceVolumeHotkeysViewModel.cs similarity index 93% rename from Volumey/ViewModel/Settings/DeviceHotkeysViewModel.cs rename to Volumey/ViewModel/Settings/DeviceVolumeHotkeysViewModel.cs index cfd7356..9f57a6f 100644 --- a/Volumey/ViewModel/Settings/DeviceHotkeysViewModel.cs +++ b/Volumey/ViewModel/Settings/DeviceVolumeHotkeysViewModel.cs @@ -6,7 +6,7 @@ namespace Volumey.ViewModel.Settings { - public sealed class DeviceHotkeysViewModel : HotkeyViewModel + public sealed class DeviceVolumeHotkeysViewModel : HotkeyViewModel { private OutputDeviceModel defaultDevice; @@ -56,9 +56,9 @@ public bool IsOn private bool hotkeysRegistered; private ILog _logger; - private ILog Logger => _logger ??= LogManager.GetLogger(typeof(DeviceHotkeysViewModel)); + private ILog Logger => _logger ??= LogManager.GetLogger(typeof(DeviceVolumeHotkeysViewModel)); - public DeviceHotkeysViewModel() + public DeviceVolumeHotkeysViewModel() { var deviceProvider = DeviceProvider.GetInstance(); deviceProvider.DefaultDeviceChanged += OnDefaultDeviceChanged; @@ -151,8 +151,6 @@ private void OnDefaultDeviceChanged(OutputDeviceModel newDevice) } } this.defaultDevice = newDevice; - if(HotkeysControl.VolumeHotkeysState == HotkeysState.Disabled) - HotkeysControl.SetHotkeysState(HotkeysState.Enabled); } } } \ No newline at end of file diff --git a/Volumey/ViewModel/Settings/HotkeysControl.cs b/Volumey/ViewModel/Settings/HotkeysControl.cs index 766462e..62c80aa 100644 --- a/Volumey/ViewModel/Settings/HotkeysControl.cs +++ b/Volumey/ViewModel/Settings/HotkeysControl.cs @@ -6,13 +6,6 @@ namespace Volumey.ViewModel.Settings { - public enum HotkeysState - { - NotRegistered, - Enabled, - Disabled - } - public static class HotkeysControl { /// @@ -23,8 +16,6 @@ public static class HotkeysControl internal static event Action OpenHotkeyPressed; internal static bool IsActive { get; private set; } - internal static HotkeysState VolumeHotkeysState { get; private set; } = HotkeysState.NotRegistered; - private static int volumeStep = 1; public static int VolumeStep { @@ -35,36 +26,52 @@ public static int VolumeStep private static HotKey openMixer; private static IHotkeyManager hotkeyManager; - public static bool RegisterVolumeHotkeys(HotKey volUp, HotKey volDown) + public static bool RegisterHotkeysPair(HotKey hotkey1, HotKey hotkey2) { if(hotkeyManager == null) throw new NullReferenceException("Hotkey manager is not set"); try { - hotkeyManager.RegisterHotkey(volUp); - hotkeyManager.RegisterHotkey(volDown); - if(VolumeHotkeysState == HotkeysState.NotRegistered) - VolumeHotkeysState = HotkeysState.Enabled; + hotkeyManager.RegisterHotkey(hotkey1); + hotkeyManager.RegisterHotkey(hotkey2); return true; } catch { - hotkeyManager.UnregisterHotkey(volUp); - hotkeyManager.UnregisterHotkey(volDown); + hotkeyManager.UnregisterHotkey(hotkey1); + hotkeyManager.UnregisterHotkey(hotkey2); throw; } } - public static void UnregisterVolumeHotkeys(HotKey volUp, HotKey volDown) + public static void UnregisterHotkeysPair(HotKey hotkey1, HotKey hotkey2) { if(hotkeyManager == null) throw new NullReferenceException("Hotkey manager is not set"); try { - hotkeyManager.UnregisterHotkey(volUp); - hotkeyManager.UnregisterHotkey(volDown); - if(hotkeyManager.RegisteredHotkeysCount == 0) - VolumeHotkeysState = HotkeysState.NotRegistered; + hotkeyManager.UnregisterHotkey(hotkey1); + hotkeyManager.UnregisterHotkey(hotkey2); + } + catch { } + } + + public static bool RegisterHotkey(HotKey hotkey) + { + try + { + hotkeyManager.RegisterHotkey(hotkey); + return true; + } + catch {} + return false; + } + + public static void UnregisterHotkey(HotKey hotkey) + { + try + { + hotkeyManager.UnregisterHotkey(hotkey); } catch { } } @@ -96,7 +103,7 @@ public static void UnregisterOpenMixerHotkey(HotKey hotkey) openMixer = null; hotkeyManager.UnregisterHotkey(hotkey); } - catch {} + catch { } } public static ErrorMessageType HotkeyIsValid(HotKey hotkey) @@ -124,11 +131,6 @@ public static ErrorMessageType HotkeysAreValid(HotKey up, HotKey down) return ErrorMessageType.None; } - internal static void SetHotkeysState(HotkeysState newState) - { - VolumeHotkeysState = newState; - } - internal static void SetVolumeStep(int newValue) { VolumeStep = newValue; @@ -144,10 +146,10 @@ public static void SetHotkeyManager(IHotkeyManager hm) private static void OnHotkeyPressed(HotKey hotkey) { - if(VolumeHotkeysState == HotkeysState.Enabled) - HotkeyPressed?.Invoke(hotkey); if(openMixer != null && hotkey.Equals(openMixer)) OpenHotkeyPressed?.Invoke(); + else + HotkeyPressed?.Invoke(hotkey); } public static void Dispose() diff --git a/Volumey/ViewModel/SettingsViewModel.cs b/Volumey/ViewModel/SettingsViewModel.cs index 668044f..fcc4da8 100644 --- a/Volumey/ViewModel/SettingsViewModel.cs +++ b/Volumey/ViewModel/SettingsViewModel.cs @@ -8,10 +8,11 @@ namespace Volumey.ViewModel { sealed class SettingsViewModel { - public DeviceHotkeysViewModel DeviceHotkeysViewModel { get; } + public DeviceVolumeHotkeysViewModel DeviceVolumeHotkeysViewModel { get; } public AppsHotkeysViewModel HotkeysViewModel { get; } public OpenHotkeyViewModel OpenHotkeyViewModel { get; } public VolumeLimitViewModel VolumeLimitViewModel { get; } + public DefaultDeviceHotkeysViewModel DefaultDeviceHotkeysViewModel { get; } public LangSettings LangSettings { get; } public ICommand GitHubCommand { get; } public ICommand TipCommand { get; } @@ -20,9 +21,10 @@ public SettingsViewModel() { this.LangSettings = new LangSettings(); this.HotkeysViewModel = new AppsHotkeysViewModel(); - this.DeviceHotkeysViewModel = new DeviceHotkeysViewModel(); + this.DeviceVolumeHotkeysViewModel = new DeviceVolumeHotkeysViewModel(); this.OpenHotkeyViewModel = new OpenHotkeyViewModel(); this.VolumeLimitViewModel = new VolumeLimitViewModel(); + this.DefaultDeviceHotkeysViewModel = new DefaultDeviceHotkeysViewModel(); this.GitHubCommand = new ActionCommand(async () => { await OpenWebPage("https://github.com/G-Stas/Volumey"); }); this.TipCommand = new ActionCommand(async () => { await OpenWebPage("https://ko-fi.com/stasg"); }); }