Compare commits
89 Commits
86a652c170
...
main
Author | SHA1 | Date | |
---|---|---|---|
0d84818df5 | |||
65f60d2c35 | |||
c828c0b90b | |||
a9391325c2 | |||
309f42bad1 | |||
8b0d2153b0 | |||
2082072532 | |||
f7df25372d | |||
ff9f92424e | |||
0c0809c111 | |||
b32e75f15d | |||
46b96c0c5d | |||
0a04e3c6ce | |||
b5cb6e4c3a | |||
d7a0079c86 | |||
e76cf3cd2a | |||
c785965041 | |||
b95edde7df | |||
5a8a5baa33 | |||
a3e09c2407 | |||
38a7856a0f | |||
e80f06d8bf | |||
18143d0311 | |||
049b861689 | |||
7d71d90538 | |||
8c9e1cd91c | |||
cef9f8d68c | |||
60f3442d38 | |||
b559ea01ab | |||
2f80af70ba | |||
aeb1b1d573 | |||
8451e684db | |||
9c8027428c | |||
8e04161a60 | |||
ccbb4525f3 | |||
529754934d | |||
0471ba88c1 | |||
b33ae3b9be | |||
67667c74da | |||
8033aebb23 | |||
0898d5cf11 | |||
cb741349fa | |||
cabfb165da | |||
b074a4e975 | |||
53d7668fd6 | |||
124b263499 | |||
3f74b1a4e7 | |||
ded5908ca2 | |||
519fe4968e | |||
f95b884d16 | |||
4136c13d5b | |||
0ab1afc2f8 | |||
3b59e51368 | |||
c7a93c2d82 | |||
43350aed36 | |||
301309737b | |||
ab46442108 | |||
3c1aad7d27 | |||
d1b6d97fe8 | |||
15db118dd6 | |||
67315f90b0 | |||
6d75f88927 | |||
7fdf349c28 | |||
9a5f686dde | |||
b209cb2556 | |||
6e1f43bed0 | |||
9bd088fac8 | |||
0be28ab205 | |||
62a3220f7f | |||
6e4ed82211 | |||
7b2a7bae21 | |||
72d751db71 | |||
e4b63073d8 | |||
d49f3ab030 | |||
62835065c0 | |||
196b93c7f3 | |||
f9a14ee3c0 | |||
7aafa935e1 | |||
2960afd9bd | |||
23098e0ebb | |||
32d3610b07 | |||
fbb65a039a | |||
4d180871ca | |||
b87bff54c4 | |||
be8cef35d3 | |||
c568a957f7 | |||
2c8696155a | |||
48b5bc5d28 | |||
984d2056de |
102
App.config
Normal file
@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="unison.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<unison.Properties.Settings>
|
||||
<setting name="mpd_host" serializeAs="String">
|
||||
<value>192.168.0.1</value>
|
||||
</setting>
|
||||
<setting name="mpd_port" serializeAs="String">
|
||||
<value>6600</value>
|
||||
</setting>
|
||||
<setting name="mpd_password" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="snapcast_startup" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="snapcast_window" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="snapcast_path" serializeAs="String">
|
||||
<value>snapcast_0.31</value>
|
||||
</setting>
|
||||
<setting name="snapcast_port" serializeAs="String">
|
||||
<value>1704</value>
|
||||
</setting>
|
||||
<setting name="volume_offset" serializeAs="String">
|
||||
<value>5</value>
|
||||
</setting>
|
||||
<setting name="nextTrack_mod" serializeAs="String">
|
||||
<value>2</value>
|
||||
</setting>
|
||||
<setting name="nextTrack_vk" serializeAs="String">
|
||||
<value>176</value>
|
||||
</setting>
|
||||
<setting name="previousTrack_mod" serializeAs="String">
|
||||
<value>2</value>
|
||||
</setting>
|
||||
<setting name="previousTrack_vk" serializeAs="String">
|
||||
<value>177</value>
|
||||
</setting>
|
||||
<setting name="playPause_mod" serializeAs="String">
|
||||
<value>2</value>
|
||||
</setting>
|
||||
<setting name="playPause_vk" serializeAs="String">
|
||||
<value>179</value>
|
||||
</setting>
|
||||
<setting name="volumeUp_mod" serializeAs="String">
|
||||
<value>2</value>
|
||||
</setting>
|
||||
<setting name="volumeUp_vk" serializeAs="String">
|
||||
<value>175</value>
|
||||
</setting>
|
||||
<setting name="volumeDown_mod" serializeAs="String">
|
||||
<value>2</value>
|
||||
</setting>
|
||||
<setting name="volumeDown_vk" serializeAs="String">
|
||||
<value>174</value>
|
||||
</setting>
|
||||
<setting name="volumeMute_mod" serializeAs="String">
|
||||
<value>2</value>
|
||||
</setting>
|
||||
<setting name="volumeMute_vk" serializeAs="String">
|
||||
<value>173</value>
|
||||
</setting>
|
||||
<setting name="showWindow_mod" serializeAs="String">
|
||||
<value>3</value>
|
||||
</setting>
|
||||
<setting name="showWindow_vk" serializeAs="String">
|
||||
<value>13</value>
|
||||
</setting>
|
||||
<setting name="MainWindowTop" serializeAs="String">
|
||||
<value>10</value>
|
||||
</setting>
|
||||
<setting name="MainWindowLeft" serializeAs="String">
|
||||
<value>10</value>
|
||||
</setting>
|
||||
<setting name="RadiosWindowTop" serializeAs="String">
|
||||
<value>10</value>
|
||||
</setting>
|
||||
<setting name="RadiosWindowLeft" serializeAs="String">
|
||||
<value>1000</value>
|
||||
</setting>
|
||||
<setting name="ShuffleWindowTop" serializeAs="String">
|
||||
<value>10</value>
|
||||
</setting>
|
||||
<setting name="ShuffleWindowLeft" serializeAs="String">
|
||||
<value>330</value>
|
||||
</setting>
|
||||
<setting name="SettingsWindowTop" serializeAs="String">
|
||||
<value>330</value>
|
||||
</setting>
|
||||
<setting name="SettingsWindowLeft" serializeAs="String">
|
||||
<value>600</value>
|
||||
</setting>
|
||||
</unison.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
1
App.xaml
@ -8,6 +8,7 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Views/Systray.xaml" />
|
||||
<ResourceDictionary Source="Resources/Resources.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}">
|
||||
|
45
App.xaml.cs
@ -1,39 +1,54 @@
|
||||
using System.Windows;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using unison.Handlers;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
private TaskbarIcon Systray;
|
||||
private HotkeyHandler Hotkeys;
|
||||
private SnapcastHandler Snapcast;
|
||||
private MPDHandler MPD;
|
||||
private TaskbarIcon _systray;
|
||||
private HotkeyHandler _hotkeys;
|
||||
private SnapcastHandler _snapcast;
|
||||
private ShuffleHandler _shuffle;
|
||||
private MPDHandler _mpd;
|
||||
private UpdateHandler _updater;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
unison.Resources.Resources.Culture = System.Globalization.CultureInfo.CurrentCulture;
|
||||
|
||||
//debug language
|
||||
//unison.Resources.Resources.Culture = System.Globalization.CultureInfo.GetCultureInfo("fr-FR");
|
||||
//unison.Resources.Resources.Culture = System.Globalization.CultureInfo.GetCultureInfo("es-ES");
|
||||
|
||||
base.OnStartup(e);
|
||||
|
||||
MPD = new MPDHandler();
|
||||
Current.Properties["mpd"] = MPD;
|
||||
_mpd = new MPDHandler();
|
||||
Current.Properties["mpd"] = _mpd;
|
||||
|
||||
Hotkeys = new HotkeyHandler();
|
||||
Current.Properties["hotkeys"] = Hotkeys;
|
||||
_hotkeys = new HotkeyHandler();
|
||||
Current.Properties["hotkeys"] = _hotkeys;
|
||||
|
||||
Snapcast = new SnapcastHandler();
|
||||
Current.Properties["snapcast"] = Snapcast;
|
||||
_snapcast = new SnapcastHandler();
|
||||
Current.Properties["snapcast"] = _snapcast;
|
||||
|
||||
_shuffle = new ShuffleHandler();
|
||||
Current.Properties["shuffle"] = _shuffle;
|
||||
|
||||
_updater = new UpdateHandler();
|
||||
Current.Properties["updater"] = _updater;
|
||||
|
||||
Current.MainWindow = new MainWindow();
|
||||
|
||||
Systray = (TaskbarIcon)FindResource("SystrayTaskbar");
|
||||
Current.Properties["systray"] = Systray;
|
||||
_systray = (TaskbarIcon)FindResource("SystrayTaskbar");
|
||||
Current.Properties["systray"] = _systray;
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
Systray.Dispose();
|
||||
Snapcast.Stop();
|
||||
Hotkeys.RemoveHotKeys();
|
||||
_systray.Dispose();
|
||||
_snapcast.LaunchOrExit(true);
|
||||
_hotkeys.RemoveHotkeys();
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
|
82
CHANGELOG.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Changelog
|
||||
|
||||
## v1.5
|
||||
|
||||
*Released: 09/06/2025*
|
||||
|
||||
* Windows 11 compatibility
|
||||
* Shuffle button: add "clear queue" and "play queue" buttons
|
||||
* Hide windows when clicking corresponding buttons, if they were visible
|
||||
* Fix: add security to QueryStats response to avoid crashes
|
||||
* Fix: regression in Systray text display
|
||||
* Fix: clipboard copy crash
|
||||
* Update .NET (6.0 to 8.0)
|
||||
* Update Snapcast (0.26.0.1 to 0.31)
|
||||
* Update MpcNET (1.4.0 to 1.6.6)
|
||||
* Update RadioBrowser (0.6.1 to 0.7)
|
||||
* Update AutoUpdater.NET (1.7.6 to 1.9.2)
|
||||
* Update NotifyIcon.Wpf (1.1.0 to 2.0.1)
|
||||
|
||||
## v1.4
|
||||
|
||||
*Released: 09/12/2022*
|
||||
|
||||
* New feature: shuffle system
|
||||
* New feature: (un)installer script
|
||||
* New feature: update system
|
||||
* New feature: update MPD database button
|
||||
* Restore window position when relaunching the app
|
||||
* New settings organisation
|
||||
* Update Emoji.WPF from 0.3.3 to 0.3.4
|
||||
* Fix: cover images are displaying in all cases
|
||||
* Fix: querying a large list of radios could hang the app
|
||||
|
||||
## v1.3.1
|
||||
|
||||
*Released: 03/11/2022*
|
||||
|
||||
* Update .NET version from 5.0 to 6.0
|
||||
* Fix: simple patch to avoid a crash concerning GetAlbumCover
|
||||
* Fix: connection change now working
|
||||
|
||||
## v1.3
|
||||
|
||||
*Released: 18/04/2022*
|
||||
|
||||
* New feature: add support for readpicture, aka embedded cover art
|
||||
* New feature: add support for MPD password
|
||||
* Spanish translation
|
||||
* Trim album release date
|
||||
* Cover icon when a radio is playing
|
||||
* Update MpcNET package from 1.3 to 1.4
|
||||
* Fix: Snapcast not working with hostname
|
||||
* Fix: some radios crash
|
||||
* Fix: disable/enable radios and Snapcast with connection
|
||||
|
||||
## v1.2
|
||||
|
||||
*Released: 07/04/2022*
|
||||
|
||||
* New feature: support for custom shortcuts
|
||||
* New feature: MPD stats
|
||||
* Add GitHub repository link in settings
|
||||
* MpcNET NuGet package integration
|
||||
* Update Snapcast from v0.25 to v0.26
|
||||
* Fix: hostname supported for connection
|
||||
* Fix: crash when using invalid IP or Hostname
|
||||
* Fix: crash when no internet connection when using RadioBrowser
|
||||
|
||||
## v1.1
|
||||
|
||||
*Released: 04/10/2021*
|
||||
|
||||
* Radio browser
|
||||
* Mute shortcut
|
||||
* Enter key works in settings textboxes
|
||||
* Share current song by double-clicking
|
||||
|
||||
## v1.0
|
||||
|
||||
*Released: 03/09/2021*
|
||||
|
||||
* First release of unison
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
@ -16,29 +15,200 @@ namespace unison
|
||||
|
||||
private const int HOTKEY_ID = 9000;
|
||||
|
||||
// modifiers
|
||||
private const uint MOD_NONE = 0x0000;
|
||||
private const uint MOD_ALT = 0x0001;
|
||||
private const uint MOD_CONTROL = 0x0002;
|
||||
private const uint MOD_SHIFT = 0x0004;
|
||||
private const uint MOD_WIN = 0x0008;
|
||||
public enum MOD : int
|
||||
{
|
||||
None = 0x0000,
|
||||
Alt = 0x0001,
|
||||
Control = 0x0002,
|
||||
Shift = 0x0004,
|
||||
//Win = 0x0008
|
||||
};
|
||||
|
||||
// reference => https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
private const uint VK_MEDIA_PREV_TRACK = 0xB1;
|
||||
private const uint VK_MEDIA_NEXT_TRACK = 0xB0;
|
||||
private const uint VK_MEDIA_PLAY_PAUSE = 0xB3;
|
||||
private const uint VK_VOLUME_UP = 0xAF;
|
||||
private const uint VK_VOLUME_DOWN = 0xAE;
|
||||
private const uint VK_ENTER = 0x0D;
|
||||
public enum VK : int
|
||||
{
|
||||
None = 0x00,
|
||||
Back = 0x08,
|
||||
Tab = 0x09,
|
||||
Clear = 0x0c,
|
||||
Return = 0x0d,
|
||||
Menu = 0x12,
|
||||
Pause = 0x13,
|
||||
Capital = 0x14,
|
||||
Kana = 0x15,
|
||||
Hangul = 0x15,
|
||||
Junja = 0x17,
|
||||
Final = 0x18,
|
||||
Hanja = 0x19,
|
||||
Kanji = 0x19,
|
||||
Escape = 0x1b,
|
||||
Convert = 0x1c,
|
||||
NonConvert = 0x1d,
|
||||
Accept = 0x1e,
|
||||
ModeChange = 0x1f,
|
||||
Space = 0x20,
|
||||
Prior = 0x21,
|
||||
Next = 0x22,
|
||||
End = 0x23,
|
||||
Home = 0x24,
|
||||
Left = 0x25,
|
||||
Up = 0x26,
|
||||
Right = 0x27,
|
||||
Down = 0x28,
|
||||
Select = 0x29,
|
||||
Print = 0x2a,
|
||||
Execute = 0x2b,
|
||||
Snapshot = 0x2c,
|
||||
Insert = 0x2d,
|
||||
Delete = 0x2e,
|
||||
Help = 0x2f,
|
||||
NumRow0 = 0x30,
|
||||
NumRow1 = 0x31,
|
||||
NumRow2 = 0x32,
|
||||
NumRow3 = 0x33,
|
||||
NumRow4 = 0x34,
|
||||
NumRow5 = 0x35,
|
||||
NumRow6 = 0x36,
|
||||
NumRow7 = 0x37,
|
||||
NumRow8 = 0x38,
|
||||
NumRow9 = 0x39,
|
||||
A = 0x41,
|
||||
B = 0x42,
|
||||
C = 0x43,
|
||||
D = 0x44,
|
||||
E = 0x45,
|
||||
F = 0x46,
|
||||
G = 0x47,
|
||||
H = 0x48,
|
||||
I = 0x49,
|
||||
J = 0x4a,
|
||||
K = 0x4b,
|
||||
L = 0x4c,
|
||||
M = 0x4d,
|
||||
N = 0x4e,
|
||||
O = 0x4f,
|
||||
P = 0x50,
|
||||
Q = 0x51,
|
||||
R = 0x52,
|
||||
S = 0x53,
|
||||
T = 0x54,
|
||||
U = 0x55,
|
||||
V = 0x56,
|
||||
W = 0x57,
|
||||
X = 0x58,
|
||||
Y = 0x59,
|
||||
Z = 0x5a,
|
||||
Apps = 0x5d,
|
||||
Sleep = 0x5f,
|
||||
NumPad0 = 0x60,
|
||||
NumPad1 = 0x61,
|
||||
NumPad2 = 0x62,
|
||||
NumPad3 = 0x63,
|
||||
NumPad4 = 0x64,
|
||||
NumPad5 = 0x65,
|
||||
NumPad6 = 0x66,
|
||||
NumPad7 = 0x67,
|
||||
NumPad8 = 0x68,
|
||||
NumPad9 = 0x69,
|
||||
Multiply = 0x6a,
|
||||
Add = 0x6b,
|
||||
Separator = 0x6c,
|
||||
Subtract = 0x6d,
|
||||
Decimal = 0x6e,
|
||||
Divide = 0x6f,
|
||||
F1 = 0x70,
|
||||
F2 = 0x71,
|
||||
F3 = 0x72,
|
||||
F4 = 0x73,
|
||||
F5 = 0x74,
|
||||
F6 = 0x75,
|
||||
F7 = 0x76,
|
||||
F8 = 0x77,
|
||||
F9 = 0x78,
|
||||
F10 = 0x79,
|
||||
F11 = 0x7a,
|
||||
F12 = 0x7b,
|
||||
F13 = 0x7c,
|
||||
F14 = 0x7d,
|
||||
F15 = 0x7e,
|
||||
F16 = 0x7f,
|
||||
F17 = 0x80,
|
||||
F18 = 0x81,
|
||||
F19 = 0x82,
|
||||
F20 = 0x83,
|
||||
F21 = 0x84,
|
||||
F22 = 0x85,
|
||||
F23 = 0x86,
|
||||
F24 = 0x87,
|
||||
NumLock = 0x90,
|
||||
Scroll = 0x91,
|
||||
LMenu = 0xa4,
|
||||
RMenu = 0xa5,
|
||||
BrowserBack = 0xa6,
|
||||
BrowserForward = 0xa7,
|
||||
BrowserRefresh = 0xa8,
|
||||
BrowserStop = 0xa9,
|
||||
BrowserSearch = 0xaa,
|
||||
BrowserFavorites = 0xab,
|
||||
BrowserHome = 0xac,
|
||||
VolumeMute = 0xad,
|
||||
VolumeDown = 0xae,
|
||||
VolumeUp = 0xaf,
|
||||
MediaNextTrack = 0xb0,
|
||||
MediaPreviousTrack = 0xb1,
|
||||
MediaStop = 0xb2,
|
||||
MediaPlayPause = 0xb3,
|
||||
LaunchMail = 0xb4,
|
||||
LaunchMediaSelect = 0xb5,
|
||||
LaunchApp1 = 0xb6,
|
||||
LaunchApp2 = 0xb7,
|
||||
};
|
||||
|
||||
public struct HotkeyPair
|
||||
{
|
||||
public MOD mod;
|
||||
public VK vk;
|
||||
|
||||
public HotkeyPair(MOD _mod, VK _vk) { mod = _mod; vk = _vk; }
|
||||
|
||||
public uint GetMOD() { return (uint)mod; }
|
||||
public uint GetVK() { return (uint)vk; }
|
||||
|
||||
public void SetMOD(MOD modmod) { mod = modmod; }
|
||||
public void SetVK(VK vkvk) { vk = vkvk; }
|
||||
}
|
||||
|
||||
private MainWindow _appWindow;
|
||||
private readonly MPDHandler _mpd;
|
||||
|
||||
private IntPtr _windowHandle;
|
||||
private HwndSource _source;
|
||||
|
||||
private readonly MPDHandler mpd;
|
||||
public HotkeyPair _NextTrack;
|
||||
public HotkeyPair _PreviousTrack;
|
||||
public HotkeyPair _PlayPause;
|
||||
public HotkeyPair _VolumeUp;
|
||||
public HotkeyPair _VolumeDown;
|
||||
public HotkeyPair _VolumeMute;
|
||||
public HotkeyPair _ShowWindow;
|
||||
public HotkeyPair[] _Shortcuts;
|
||||
|
||||
public HotkeyHandler()
|
||||
{
|
||||
mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_NextTrack = new HotkeyPair((MOD)Properties.Settings.Default.nextTrack_mod, (VK)Properties.Settings.Default.nextTrack_vk);
|
||||
_PreviousTrack = new HotkeyPair((MOD)Properties.Settings.Default.previousTrack_mod, (VK)Properties.Settings.Default.previousTrack_vk);
|
||||
_PlayPause = new HotkeyPair((MOD)Properties.Settings.Default.playPause_mod, (VK)Properties.Settings.Default.playPause_vk);
|
||||
_VolumeUp = new HotkeyPair((MOD)Properties.Settings.Default.volumeUp_mod, (VK)Properties.Settings.Default.volumeUp_vk);
|
||||
_VolumeDown = new HotkeyPair((MOD)Properties.Settings.Default.volumeDown_mod, (VK)Properties.Settings.Default.volumeDown_vk);
|
||||
_VolumeMute = new HotkeyPair((MOD)Properties.Settings.Default.volumeMute_mod, (VK)Properties.Settings.Default.volumeMute_vk);
|
||||
_ShowWindow = new HotkeyPair((MOD)Properties.Settings.Default.showWindow_mod, (VK)Properties.Settings.Default.showWindow_vk);
|
||||
_Shortcuts = new HotkeyPair[] { _NextTrack, _PreviousTrack, _PlayPause, _VolumeUp, _VolumeDown, _VolumeMute, _ShowWindow };
|
||||
}
|
||||
|
||||
public void Activate(Window win)
|
||||
@ -47,17 +217,28 @@ namespace unison
|
||||
{
|
||||
_windowHandle = new WindowInteropHelper(win).Handle;
|
||||
_source = HwndSource.FromHwnd(_windowHandle);
|
||||
_source.AddHook(HwndHook);
|
||||
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_MEDIA_PREV_TRACK);
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_MEDIA_NEXT_TRACK);
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_MEDIA_PLAY_PAUSE);
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_VOLUME_UP);
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_VOLUME_DOWN);
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL | MOD_ALT, VK_ENTER);
|
||||
AddHotkeys();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddHotkeys()
|
||||
{
|
||||
_source.AddHook(HwndHook);
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _NextTrack.GetMOD(), _NextTrack.GetVK());
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _PreviousTrack.GetMOD(), _PreviousTrack.GetVK());
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _PlayPause.GetMOD(), _PlayPause.GetVK());
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _VolumeUp.GetMOD(), _VolumeUp.GetVK());
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _VolumeDown.GetMOD(), _VolumeDown.GetVK());
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _VolumeMute.GetMOD(), _VolumeMute.GetVK());
|
||||
RegisterHotKey(_windowHandle, HOTKEY_ID, _ShowWindow.GetMOD(), _ShowWindow.GetVK());
|
||||
}
|
||||
|
||||
public void RemoveHotkeys()
|
||||
{
|
||||
_source.RemoveHook(HwndHook);
|
||||
UnregisterHotKey(_windowHandle, HOTKEY_ID);
|
||||
}
|
||||
|
||||
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
const int WM_HOTKEY = 0x0312;
|
||||
@ -65,62 +246,48 @@ namespace unison
|
||||
if (msg == WM_HOTKEY && wParam.ToInt32() == HOTKEY_ID)
|
||||
{
|
||||
uint vkey = ((uint)lParam >> 16) & 0xFFFF;
|
||||
MainWindow AppWindow = (MainWindow)Application.Current.MainWindow;
|
||||
switch (vkey)
|
||||
|
||||
if (vkey == _NextTrack.GetVK())
|
||||
_mpd.Next();
|
||||
else if (vkey == _PreviousTrack.GetVK())
|
||||
_mpd.Prev();
|
||||
else if (vkey == _PlayPause.GetVK())
|
||||
_mpd.PlayPause();
|
||||
else if (vkey == _VolumeUp.GetVK())
|
||||
_mpd.VolumeUp();
|
||||
else if (vkey == _VolumeDown.GetVK())
|
||||
_mpd.VolumeDown();
|
||||
else if (vkey == _VolumeMute.GetVK())
|
||||
_mpd.VolumeMute();
|
||||
else if (vkey == _ShowWindow.GetVK())
|
||||
{
|
||||
case VK_MEDIA_NEXT_TRACK:
|
||||
mpd.Next();
|
||||
break;
|
||||
case VK_MEDIA_PREV_TRACK:
|
||||
mpd.Prev();
|
||||
break;
|
||||
case VK_VOLUME_DOWN:
|
||||
mpd._currentVolume -= Properties.Settings.Default.volume_offset;
|
||||
if (mpd._currentVolume < 0)
|
||||
mpd._currentVolume = 0;
|
||||
mpd.SetVolume(mpd._currentVolume);
|
||||
break;
|
||||
case VK_VOLUME_UP:
|
||||
mpd._currentVolume += Properties.Settings.Default.volume_offset;
|
||||
if (mpd._currentVolume > 100)
|
||||
mpd._currentVolume = 100;
|
||||
mpd.SetVolume(mpd._currentVolume);
|
||||
break;
|
||||
case VK_MEDIA_PLAY_PAUSE:
|
||||
mpd.PlayPause();
|
||||
break;
|
||||
case VK_ENTER:
|
||||
if (AppWindow.WindowState == WindowState.Minimized)
|
||||
if (_appWindow == null)
|
||||
_appWindow = (MainWindow)Application.Current.MainWindow;
|
||||
|
||||
if (_appWindow.WindowState == WindowState.Minimized)
|
||||
{
|
||||
_appWindow.Show();
|
||||
_appWindow.Activate();
|
||||
_appWindow.WindowState = WindowState.Normal;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_appWindow.IsActive)
|
||||
{
|
||||
AppWindow.Show();
|
||||
AppWindow.Activate();
|
||||
AppWindow.WindowState = WindowState.Normal;
|
||||
_appWindow.Hide();
|
||||
_appWindow.WindowState = WindowState.Minimized;
|
||||
}
|
||||
else
|
||||
else // not minimized but not in front
|
||||
{
|
||||
if (AppWindow.IsActive)
|
||||
{
|
||||
AppWindow.Hide();
|
||||
AppWindow.WindowState = WindowState.Minimized;
|
||||
}
|
||||
else // not minimized but not in front
|
||||
{
|
||||
AppWindow.Show();
|
||||
AppWindow.Activate();
|
||||
AppWindow.WindowState = WindowState.Normal;
|
||||
}
|
||||
_appWindow.Show();
|
||||
_appWindow.Activate();
|
||||
_appWindow.WindowState = WindowState.Normal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void RemoveHotKeys()
|
||||
{
|
||||
_source.RemoveHook(HwndHook);
|
||||
UnregisterHotKey(_windowHandle, HOTKEY_ID);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@ -12,6 +13,7 @@ using System.Windows.Threading;
|
||||
using MpcNET;
|
||||
using MpcNET.Commands.Database;
|
||||
using MpcNET.Commands.Playback;
|
||||
using MpcNET.Commands.Queue;
|
||||
using MpcNET.Commands.Reflection;
|
||||
using MpcNET.Commands.Status;
|
||||
using MpcNET.Message;
|
||||
@ -19,55 +21,66 @@ using MpcNET.Types;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public class Statistics
|
||||
{
|
||||
public int Songs { get; set; }
|
||||
public int Albums { get; set; }
|
||||
public int Artists { get; set; }
|
||||
public string TotalPlaytime { get; set; }
|
||||
public string Uptime { get; set; }
|
||||
public string TotalTimePlayed { get; set; }
|
||||
public string DatabaseUpdate { get; set; }
|
||||
}
|
||||
|
||||
public class MPDHandler
|
||||
{
|
||||
public bool _connected;
|
||||
public string _version;
|
||||
public int _currentVolume;
|
||||
public bool _currentRandom;
|
||||
public bool _currentRepeat;
|
||||
public bool _currentSingle;
|
||||
public bool _currentConsume;
|
||||
public double _currentTime;
|
||||
public double _totalTime;
|
||||
BitmapFrame _cover;
|
||||
|
||||
public event EventHandler ConnectionChanged;
|
||||
public event EventHandler StatusChanged;
|
||||
public event EventHandler SongChanged;
|
||||
public event EventHandler CoverChanged;
|
||||
|
||||
public static MpdStatus BOGUS_STATUS = new MpdStatus(0, false, false, false, false, -1, -1, -1, MpdState.Unknown, -1, -1, -1, -1, TimeSpan.Zero, TimeSpan.Zero, -1, -1, -1, -1, -1, "", "");
|
||||
public MpdStatus CurrentStatus { get; private set; } = BOGUS_STATUS;
|
||||
|
||||
IMpdFile CurrentSong { get; set; }
|
||||
private bool _connected;
|
||||
private string _version;
|
||||
private int _currentVolume;
|
||||
private int _previousVolume;
|
||||
private bool _currentRandom;
|
||||
private bool _currentRepeat;
|
||||
private bool _currentSingle;
|
||||
private bool _currentConsume;
|
||||
private double _currentTime;
|
||||
private double _totalTime;
|
||||
private IEnumerable<IMpdFile> _Playlist;
|
||||
|
||||
private MpdStatus _currentStatus;
|
||||
private IMpdFile _currentSong;
|
||||
private BitmapImage _cover;
|
||||
private readonly Statistics _stats;
|
||||
private readonly System.Timers.Timer _elapsedTimer;
|
||||
private async void ElapsedTimer(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if ((_currentTime < _totalTime) && (CurrentStatus.State == MpdState.Play))
|
||||
{
|
||||
_currentTime += 0.5;
|
||||
await Task.Delay(5);
|
||||
}
|
||||
else
|
||||
{
|
||||
_elapsedTimer.Stop();
|
||||
}
|
||||
}
|
||||
private readonly DispatcherTimer _retryTimer;
|
||||
|
||||
private bool _isUpdatingStatus = false;
|
||||
private bool _isUpdatingSong = false;
|
||||
|
||||
public IPAddress _ipAddress;
|
||||
|
||||
private event EventHandler ConnectionChanged;
|
||||
private event EventHandler StatusChanged;
|
||||
private event EventHandler SongChanged;
|
||||
private event EventHandler CoverChanged;
|
||||
|
||||
private MpcConnection _connection;
|
||||
private MpcConnection _commandConnection;
|
||||
|
||||
private IPEndPoint _mpdEndpoint;
|
||||
|
||||
private CancellationTokenSource cancelToken;
|
||||
public CancellationTokenSource _cancelCommand;
|
||||
private CancellationTokenSource _cancelConnect;
|
||||
|
||||
private bool UpdateStarted = false;
|
||||
|
||||
public MPDHandler()
|
||||
{
|
||||
cancelToken = new CancellationTokenSource();
|
||||
Startup(null, null);
|
||||
|
||||
Initialize();
|
||||
_stats = new Statistics();
|
||||
|
||||
_retryTimer = new DispatcherTimer();
|
||||
_retryTimer.Interval = TimeSpan.FromSeconds(5);
|
||||
_retryTimer.Tick += Startup;
|
||||
|
||||
_elapsedTimer = new System.Timers.Timer(500);
|
||||
_elapsedTimer.Elapsed += new System.Timers.ElapsedEventHandler(ElapsedTimer);
|
||||
@ -78,8 +91,21 @@ namespace unison
|
||||
CoverChanged += OnCoverChanged;
|
||||
}
|
||||
|
||||
static void OnConnectionChanged(object sender, EventArgs e)
|
||||
private void ElapsedTimer(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if ((_currentTime < _totalTime || _totalTime == -1) && (_currentStatus.State == MpdState.Play))
|
||||
_currentTime += 0.5;
|
||||
else
|
||||
_elapsedTimer.Stop();
|
||||
}
|
||||
|
||||
void OnConnectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!_connected)
|
||||
_retryTimer.Start();
|
||||
else
|
||||
_retryTimer.Stop();
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
@ -87,8 +113,7 @@ namespace unison
|
||||
|
||||
SnapcastHandler Snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
|
||||
Snapcast.OnConnectionChanged(sender, e);
|
||||
|
||||
}, DispatcherPriority.ContextIdle);
|
||||
});
|
||||
}
|
||||
|
||||
static void OnStatusChanged(object sender, EventArgs e)
|
||||
@ -97,7 +122,7 @@ namespace unison
|
||||
{
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.OnStatusChanged(sender, e);
|
||||
}, DispatcherPriority.ContextIdle);
|
||||
});
|
||||
}
|
||||
|
||||
static void OnSongChanged(object sender, EventArgs e)
|
||||
@ -106,7 +131,7 @@ namespace unison
|
||||
{
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.OnSongChanged(sender, e);
|
||||
}, DispatcherPriority.ContextIdle);
|
||||
});
|
||||
}
|
||||
|
||||
static void OnCoverChanged(object sender, EventArgs e)
|
||||
@ -115,44 +140,151 @@ namespace unison
|
||||
{
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.OnCoverChanged(sender, e);
|
||||
}, DispatcherPriority.ContextIdle);
|
||||
});
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
public void SendCommand<T>(IMpcCommand<T> command)
|
||||
{
|
||||
Connect();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await SafelySendCommandAsync(command);
|
||||
});
|
||||
}
|
||||
|
||||
public async void Connect()
|
||||
public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command)
|
||||
{
|
||||
var token = cancelToken.Token;
|
||||
if (_commandConnection == null || !IsConnected())
|
||||
{
|
||||
Trace.WriteLine("[SafelySendCommandAsync] no command connection");
|
||||
return default;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_connection = await ConnectInternal(token);
|
||||
_commandConnection = await ConnectInternal(token);
|
||||
IMpdMessage<T> response = await _commandConnection.SendAsync(command);
|
||||
if (!response.IsResponseValid)
|
||||
{
|
||||
string mpdError = response.Response?.Result?.MpdError;
|
||||
if (mpdError != null && mpdError != "")
|
||||
throw new Exception(mpdError);
|
||||
else
|
||||
throw new Exception($"Invalid server response: {response}.");
|
||||
}
|
||||
|
||||
return response.Response.Content;
|
||||
}
|
||||
catch(MpcNET.Exceptions.MpcConnectException exception)
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine("exception: " + exception);
|
||||
Trace.WriteLine($"Sending {command.GetType().Name} failed: {e.Message}");
|
||||
}
|
||||
if (_connection.IsConnected)
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async void Startup(object sender, EventArgs e)
|
||||
{
|
||||
await Initialize();
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
Trace.WriteLine("Initializing");
|
||||
|
||||
Disconnected();
|
||||
|
||||
if (!_connected)
|
||||
await Connect();
|
||||
}
|
||||
|
||||
public void Disconnected()
|
||||
{
|
||||
_connected = false;
|
||||
ConnectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
_commandConnection?.DisconnectAsync();
|
||||
_connection?.DisconnectAsync();
|
||||
|
||||
_cancelConnect?.Cancel();
|
||||
_cancelConnect = new CancellationTokenSource();
|
||||
|
||||
_cancelCommand?.Cancel();
|
||||
_cancelCommand = new CancellationTokenSource();
|
||||
|
||||
_connection = null;
|
||||
_commandConnection = null;
|
||||
|
||||
Trace.WriteLine("Disconnected");
|
||||
}
|
||||
|
||||
public async Task Connect()
|
||||
{
|
||||
Trace.WriteLine("Connecting");
|
||||
|
||||
if (_cancelCommand.IsCancellationRequested || _cancelConnect.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_connected = true;
|
||||
_version = _connection.Version;
|
||||
_connection = await ConnectInternal(_cancelConnect.Token);
|
||||
_commandConnection = await ConnectInternal(_cancelCommand.Token);
|
||||
}
|
||||
catch (MpcNET.Exceptions.MpcConnectException e)
|
||||
{
|
||||
_connected = false;
|
||||
Trace.WriteLine($"Error in connect: {e.Message}");
|
||||
ConnectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
if (_connection != null && _commandConnection != null)
|
||||
{
|
||||
if (_connection.IsConnected && _commandConnection.IsConnected)
|
||||
{
|
||||
_connected = true;
|
||||
_version = _connection.Version;
|
||||
ConnectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_connected = false;
|
||||
ConnectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
await UpdateStatusAsync();
|
||||
await UpdateSongAsync();
|
||||
|
||||
Loop(token);
|
||||
Loop(_cancelCommand.Token);
|
||||
}
|
||||
|
||||
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
|
||||
{
|
||||
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress);
|
||||
if (token.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
_mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port);
|
||||
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out _ipAddress);
|
||||
|
||||
if (_ipAddress == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPAddress[] addrList = Dns.GetHostAddresses(Properties.Settings.Default.mpd_host);
|
||||
if (addrList.Length > 0)
|
||||
{
|
||||
foreach (IPAddress addr in addrList)
|
||||
{
|
||||
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
_ipAddress = addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new MpcNET.Exceptions.MpcConnectException("No correct IP provided by user.");
|
||||
}
|
||||
}
|
||||
|
||||
_mpdEndpoint = new IPEndPoint(_ipAddress, Properties.Settings.Default.mpd_port);
|
||||
MpcConnection connection = new MpcConnection(_mpdEndpoint);
|
||||
await connection.ConnectAsync(token);
|
||||
|
||||
@ -162,6 +294,7 @@ namespace unison
|
||||
if (!result.IsResponseValid)
|
||||
{
|
||||
string mpdError = result.Response?.Result?.MpdError;
|
||||
Trace.WriteLine(mpdError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,75 +309,84 @@ namespace unison
|
||||
{
|
||||
try
|
||||
{
|
||||
if (token.IsCancellationRequested || _connection == null)
|
||||
if (token.IsCancellationRequested || _connection == null || !IsConnected())
|
||||
break;
|
||||
|
||||
Trace.WriteLine("loop");
|
||||
var idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options"));
|
||||
IMpdMessage<string> idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update"));
|
||||
|
||||
if (idleChanges.IsResponseValid)
|
||||
await HandleIdleResponseAsync(idleChanges.Response.Content);
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread (1): {idleChanges.Response?.Content}");
|
||||
throw new Exception(idleChanges.Response?.Content);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
if (token.IsCancellationRequested)
|
||||
Trace.WriteLine($"Idle connection cancelled.");
|
||||
else
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
await Initialize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task HandleIdleResponseAsync(string subsystems)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (subsystems.Contains("player") || subsystems.Contains("mixer") || subsystems.Contains("output") || subsystems.Contains("options"))
|
||||
if (subsystems.Contains("player") || subsystems.Contains("mixer") || subsystems.Contains("output") || subsystems.Contains("options") || subsystems.Contains("update"))
|
||||
{
|
||||
await UpdateStatusAsync();
|
||||
|
||||
if (subsystems.Contains("player"))
|
||||
{
|
||||
await UpdateSongAsync();
|
||||
}
|
||||
|
||||
if (subsystems.Contains("update"))
|
||||
UpdateDatabaseSync();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
await Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateStatusCommand()
|
||||
private void UpdateDatabaseSync()
|
||||
{
|
||||
if (_commandConnection == null) return;
|
||||
|
||||
try
|
||||
if (!UpdateStarted)
|
||||
{
|
||||
MpdStatus response = await SafelySendCommandAsync(new StatusCommand());
|
||||
|
||||
if (response != null)
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
CurrentStatus = response;
|
||||
UpdateStatus();
|
||||
}
|
||||
else
|
||||
throw new Exception();
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.GetSettings().MPDDatabaseUpdate_Start();
|
||||
});
|
||||
|
||||
UpdateStarted = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
Connect();
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.GetSettings().MPDDatabaseUpdate_Stop();
|
||||
MainWin.UpdateStats();
|
||||
});
|
||||
UpdateStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isUpdatingStatus = false;
|
||||
private async Task UpdateStatusAsync()
|
||||
{
|
||||
if (_connection == null) return;
|
||||
|
||||
if (_isUpdatingStatus) return;
|
||||
if (_connection == null || _isUpdatingStatus)
|
||||
return;
|
||||
|
||||
_isUpdatingStatus = true;
|
||||
|
||||
try
|
||||
@ -252,7 +394,7 @@ namespace unison
|
||||
IMpdMessage<MpdStatus> response = await _connection.SendAsync(new StatusCommand());
|
||||
if (response != null && response.IsResponseValid)
|
||||
{
|
||||
CurrentStatus = response.Response.Content;
|
||||
_currentStatus = response.Response.Content;
|
||||
UpdateStatus();
|
||||
}
|
||||
else
|
||||
@ -261,17 +403,19 @@ namespace unison
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
Connect();
|
||||
await Initialize();
|
||||
}
|
||||
|
||||
_isUpdatingStatus = false;
|
||||
}
|
||||
|
||||
bool _isUpdatingSong = false;
|
||||
private async Task UpdateSongAsync()
|
||||
{
|
||||
if (_connection == null) return;
|
||||
Trace.WriteLine("Updating song");
|
||||
|
||||
if (_connection == null || _isUpdatingSong)
|
||||
return;
|
||||
|
||||
if (_isUpdatingSong) return;
|
||||
_isUpdatingSong = true;
|
||||
|
||||
try
|
||||
@ -279,107 +423,128 @@ namespace unison
|
||||
IMpdMessage<IMpdFile> response = await _connection.SendAsync(new CurrentSongCommand());
|
||||
if (response != null && response.IsResponseValid)
|
||||
{
|
||||
CurrentSong = response.Response.Content;
|
||||
_currentSong = response.Response.Content;
|
||||
UpdateSong();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
Connect();
|
||||
await Initialize();
|
||||
}
|
||||
|
||||
_isUpdatingSong = false;
|
||||
|
||||
Trace.WriteLine("Updated song");
|
||||
}
|
||||
|
||||
public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command)
|
||||
private async void GetAlbumCover(string path, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _commandConnection.SendAsync(command);
|
||||
if (!response.IsResponseValid)
|
||||
{
|
||||
// If we have an MpdError string, only show that as the error to avoid extra noise
|
||||
var mpdError = response.Response?.Result?.MpdError;
|
||||
if (mpdError != null && mpdError != "")
|
||||
throw new Exception(mpdError);
|
||||
else
|
||||
throw new Exception($"Invalid server response: {response}.");
|
||||
}
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
return response.Response.Content;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Sending {command.GetType().Name} failed: {e.Message}");
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
private async void GetAlbumBitmap(string path, CancellationToken token = default)
|
||||
{
|
||||
List<byte> data = new List<byte>();
|
||||
try
|
||||
{
|
||||
if (_connection == null) // We got cancelled
|
||||
return;
|
||||
|
||||
bool ReadPictureFailed = true;
|
||||
long totalBinarySize = 9999;
|
||||
long currentSize = 0;
|
||||
|
||||
do
|
||||
{
|
||||
var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
|
||||
if (!albumReq.IsResponseValid) break;
|
||||
if (_connection == null)
|
||||
return;
|
||||
|
||||
var response = albumReq.Response.Content;
|
||||
if (response.Binary == 0) break; // MPD isn't giving us any more data, let's roll with what we have.
|
||||
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
|
||||
if (!albumReq.IsResponseValid)
|
||||
break;
|
||||
|
||||
MpdBinaryData response = albumReq.Response.Content;
|
||||
if (response == null || response.Binary == 0)
|
||||
break;
|
||||
|
||||
ReadPictureFailed = false;
|
||||
totalBinarySize = response.Size;
|
||||
currentSize += response.Binary;
|
||||
data.AddRange(response.Data);
|
||||
}
|
||||
while (currentSize < totalBinarySize && !token.IsCancellationRequested);
|
||||
|
||||
do
|
||||
{
|
||||
if (!ReadPictureFailed)
|
||||
break;
|
||||
if (_connection == null)
|
||||
return;
|
||||
|
||||
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new ReadPictureCommand(path, currentSize));
|
||||
if (!albumReq.IsResponseValid)
|
||||
break;
|
||||
|
||||
MpdBinaryData response = albumReq.Response.Content;
|
||||
if (response == null || response.Binary == 0)
|
||||
break;
|
||||
|
||||
totalBinarySize = response.Size;
|
||||
currentSize += response.Binary;
|
||||
data.AddRange(response.Data);
|
||||
//Debug.WriteLine($"Downloading albumart: {currentSize}/{totalBinarySize}");
|
||||
} while (currentSize < totalBinarySize && !token.IsCancellationRequested);
|
||||
}
|
||||
while (currentSize < totalBinarySize && !token.IsCancellationRequested);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine("Exception caught while getting albumart: " + e);
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
Trace.WriteLine("Exception caught while getting albumart: " + e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Count == 0)
|
||||
{
|
||||
Trace.WriteLine("empty cover");
|
||||
_cover = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
using MemoryStream stream = new MemoryStream(data.ToArray());
|
||||
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
||||
_cover = new BitmapImage();
|
||||
_cover.BeginInit();
|
||||
_cover.CacheOption = BitmapCacheOption.OnLoad;
|
||||
_cover.StreamSource = stream;
|
||||
_cover.EndInit();
|
||||
_cover.Freeze();
|
||||
}
|
||||
UpdateCover();
|
||||
}
|
||||
|
||||
public void UpdateSong()
|
||||
public void UpdateStatus()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
if (CurrentSong == null)
|
||||
if (!_connected || _currentStatus == null)
|
||||
return;
|
||||
|
||||
_currentTime = CurrentStatus.Elapsed.TotalSeconds;
|
||||
_totalTime = CurrentSong.Time;
|
||||
_currentRandom = _currentStatus.Random;
|
||||
_currentRepeat = _currentStatus.Repeat;
|
||||
_currentConsume = _currentStatus.Consume;
|
||||
_currentSingle = _currentStatus.Single;
|
||||
_currentVolume = _currentStatus.Volume;
|
||||
|
||||
StatusChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void UpdateSong()
|
||||
{
|
||||
if (!_connected || _currentSong == null)
|
||||
return;
|
||||
|
||||
_currentTime = _currentStatus.Elapsed.TotalSeconds;
|
||||
_totalTime = _currentSong.Time;
|
||||
if (!_elapsedTimer.Enabled)
|
||||
_elapsedTimer.Start();
|
||||
|
||||
SongChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
string uri = Regex.Escape(CurrentSong.Path);
|
||||
GetAlbumBitmap(uri);
|
||||
string uri = Regex.Escape(_currentSong.Path);
|
||||
GetAlbumCover(uri, _cancelCommand.Token);
|
||||
}
|
||||
|
||||
public void UpdateCover()
|
||||
@ -387,40 +552,151 @@ namespace unison
|
||||
CoverChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void UpdateStatus()
|
||||
public IMpdFile GetCurrentSong() => _currentSong;
|
||||
public MpdStatus GetStatus() => _currentStatus;
|
||||
public BitmapImage GetCover() => _cover;
|
||||
public string GetVersion() => _version;
|
||||
public Statistics GetStats() => _stats;
|
||||
public double GetCurrentTime() => _currentTime;
|
||||
public IEnumerable<IMpdFile> GetPlaylist() => _Playlist;
|
||||
|
||||
public bool IsConnected() => _connected;
|
||||
public bool IsPlaying() => _currentStatus?.State == MpdState.Play;
|
||||
|
||||
public bool CanPrevNext = true;
|
||||
|
||||
public void Prev()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
|
||||
if (CurrentStatus == null)
|
||||
return;
|
||||
|
||||
_currentRandom = CurrentStatus.Random;
|
||||
_currentRepeat = CurrentStatus.Repeat;
|
||||
_currentConsume = CurrentStatus.Consume;
|
||||
_currentSingle = CurrentStatus.Single;
|
||||
_currentVolume = CurrentStatus.Volume;
|
||||
|
||||
StatusChanged?.Invoke(this, EventArgs.Empty);
|
||||
if (CanPrevNext && !_isUpdatingSong)
|
||||
SendCommand(new PreviousCommand());
|
||||
}
|
||||
|
||||
public IMpdFile GetCurrentSong() => CurrentSong;
|
||||
public MpdStatus GetStatus() => CurrentStatus;
|
||||
public BitmapFrame GetCover() => _cover;
|
||||
public string GetVersion() => _version;
|
||||
public void Next()
|
||||
{
|
||||
if (CanPrevNext && !_isUpdatingSong)
|
||||
SendCommand(new NextCommand());
|
||||
}
|
||||
|
||||
public void PlayPause() => SendCommand(new PauseResumeCommand());
|
||||
public void Play(int pos) => SendCommand(new PlayCommand(pos));
|
||||
|
||||
public async void Prev() => await SafelySendCommandAsync(new PreviousCommand());
|
||||
public async void Next() => await SafelySendCommandAsync(new NextCommand());
|
||||
public async void PlayPause() =>await SafelySendCommandAsync(new PauseResumeCommand());
|
||||
public void Random() => SendCommand(new RandomCommand(!_currentRandom));
|
||||
public void Repeat() => SendCommand(new RepeatCommand(!_currentRepeat));
|
||||
public void Single() => SendCommand(new SingleCommand(!_currentSingle));
|
||||
public void Consume() => SendCommand(new ConsumeCommand(!_currentConsume));
|
||||
|
||||
public async void Random() => await SafelySendCommandAsync(new RandomCommand(!_currentRandom));
|
||||
public async void Repeat() => await SafelySendCommandAsync(new RepeatCommand(!_currentRepeat));
|
||||
public async void Single() => await SafelySendCommandAsync(new SingleCommand(!_currentSingle));
|
||||
public async void Consume() => await SafelySendCommandAsync(new ConsumeCommand(!_currentConsume));
|
||||
public void SetTime(double value) => SendCommand(new SeekCurCommand(value));
|
||||
public void SetVolume(int value) => SendCommand(new SetVolumeCommand((byte)value));
|
||||
|
||||
public void VolumeUp()
|
||||
{
|
||||
_currentVolume += Properties.Settings.Default.volume_offset;
|
||||
if (_currentVolume > 100)
|
||||
_currentVolume = 100;
|
||||
SetVolume(_currentVolume);
|
||||
}
|
||||
|
||||
public async void SetVolume(int value) => await SafelySendCommandAsync(new SetVolumeCommand((byte)value));
|
||||
public async void SetTime(double value) => await SafelySendCommandAsync(new SeekCurCommand(value));
|
||||
public void VolumeDown()
|
||||
{
|
||||
_currentVolume -= Properties.Settings.Default.volume_offset;
|
||||
if (_currentVolume < 0)
|
||||
_currentVolume = 0;
|
||||
SetVolume(_currentVolume);
|
||||
}
|
||||
|
||||
public bool IsPlaying() => CurrentStatus?.State == MpdState.Play;
|
||||
public void VolumeMute()
|
||||
{
|
||||
if (_currentVolume == 0)
|
||||
{
|
||||
_currentVolume = _previousVolume;
|
||||
_previousVolume = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_previousVolume = _currentVolume;
|
||||
_currentVolume = 0;
|
||||
}
|
||||
SetVolume(_currentVolume);
|
||||
}
|
||||
|
||||
public void ClearQueue() => SendCommand(new ClearCommand());
|
||||
public void PlayCommand() => SendCommand(new PlayCommand(0));
|
||||
|
||||
public void AddSong(string Uri)
|
||||
{
|
||||
SendCommand(new AddCommand(Uri));
|
||||
}
|
||||
|
||||
public void ClearAddAndPlay(string Uri)
|
||||
{
|
||||
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new ClearCommand(), new AddCommand(Uri), new PlayCommand(0) });
|
||||
SendCommand(commandList);
|
||||
}
|
||||
|
||||
public async Task QueryPlaylist() => _Playlist = await SafelySendCommandAsync(new PlaylistCommand());
|
||||
|
||||
public int GetPlaylistCount()
|
||||
{
|
||||
if (_Playlist == null)
|
||||
return 0;
|
||||
return _Playlist.ToArray().Length;
|
||||
}
|
||||
|
||||
public void UpdateDB() => SendCommand(new UpdateCommand());
|
||||
|
||||
private static string FormatTime(TimeSpan time)
|
||||
{
|
||||
string FormattedTime = "";
|
||||
|
||||
if (time.Days == 1)
|
||||
FormattedTime += $"{time.Days} {Resources.Resources.Day}, ";
|
||||
else if (time.Days > 1)
|
||||
FormattedTime += $"{time.Days} {Resources.Resources.Days}, ";
|
||||
|
||||
if (time.Hours == 1)
|
||||
FormattedTime += $"{time.Hours} {Resources.Resources.Hour}, ";
|
||||
else
|
||||
FormattedTime += $"{time.Hours} {Resources.Resources.Hours}, ";
|
||||
|
||||
if (time.Minutes == 1)
|
||||
FormattedTime += $"{time.Minutes} {Resources.Resources.Minute}, ";
|
||||
else
|
||||
FormattedTime += $"{time.Minutes} {Resources.Resources.Minutes}, ";
|
||||
|
||||
if (time.Seconds == 1)
|
||||
FormattedTime += $"{time.Seconds} {Resources.Resources.Second}";
|
||||
else
|
||||
FormattedTime += $"{time.Seconds} {Resources.Resources.Seconds}";
|
||||
|
||||
return FormattedTime;
|
||||
}
|
||||
|
||||
public async void QueryStats()
|
||||
{
|
||||
Dictionary<string, string> Response = await SafelySendCommandAsync(new StatsCommand());
|
||||
if (Response == null)
|
||||
return;
|
||||
|
||||
if (Response.ContainsKey("songs"))
|
||||
_stats.Songs = int.Parse(Response["songs"]);
|
||||
if (Response.ContainsKey("albums"))
|
||||
_stats.Albums = int.Parse(Response["albums"]);
|
||||
if (Response.ContainsKey("artists"))
|
||||
_stats.Artists = int.Parse(Response["artists"]);
|
||||
|
||||
if (Response.ContainsKey("uptime"))
|
||||
_stats.Uptime = FormatTime(TimeSpan.FromSeconds(int.Parse(Response["uptime"])));
|
||||
if (Response.ContainsKey("db_playtime"))
|
||||
_stats.TotalPlaytime = FormatTime(TimeSpan.FromSeconds(int.Parse(Response["db_playtime"])));
|
||||
if (Response.ContainsKey("playtime"))
|
||||
_stats.TotalTimePlayed = FormatTime(TimeSpan.FromSeconds(int.Parse(Response["playtime"])));
|
||||
|
||||
DateTime date = new DateTime(1970, 1, 1);
|
||||
if (Response.ContainsKey("db_update"))
|
||||
date = date.AddSeconds(int.Parse(Response["db_update"])).ToLocalTime();
|
||||
|
||||
string dayOfWeek = Resources.Resources.Culture.DateTimeFormat.GetDayName(date.DayOfWeek);
|
||||
_stats.DatabaseUpdate = dayOfWeek + " " + date.ToString("dd/MM/yyyy @ HH:mm");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
Handlers/RadioHandler.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using RadioBrowser;
|
||||
using RadioBrowser.Models;
|
||||
|
||||
namespace unison.Handlers
|
||||
{
|
||||
public class CountryListItem
|
||||
{
|
||||
public uint Count { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Name == "")
|
||||
return "None";
|
||||
return $"{Name} ({Count})";
|
||||
}
|
||||
}
|
||||
|
||||
public class StationListItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Codec { get; set; }
|
||||
public string Tags { get; set; }
|
||||
public int Bitrate { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
|
||||
private string _country;
|
||||
public string Country
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_country.Length == 0)
|
||||
return "🏴☠️";
|
||||
return string.Concat(_country.ToUpper().Select(x => char.ConvertFromUtf32(x + 0x1F1A5))); // return emoji
|
||||
}
|
||||
set
|
||||
{
|
||||
_country = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class RadioHandler
|
||||
{
|
||||
private readonly RadioBrowserClient _radioBrowser;
|
||||
private readonly bool _connected = true;
|
||||
|
||||
public bool IsConnected() => _connected;
|
||||
|
||||
|
||||
public RadioHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
_radioBrowser = new RadioBrowserClient();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine("Exception while connecting to RadioBrowser: " + e.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
public async Task<List<NameAndCount>> GetCountries()
|
||||
{
|
||||
return await _radioBrowser.Lists.GetCountriesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<StationInfo>> AdvancedSearch(AdvancedSearchOptions options)
|
||||
{
|
||||
return await _radioBrowser.Search.AdvancedAsync(options);
|
||||
}
|
||||
}
|
||||
}
|
112
Handlers/ShuffleHandler.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using MpcNET;
|
||||
using MpcNET.Commands.Database;
|
||||
using MpcNET.Commands.Playback;
|
||||
using MpcNET.Commands.Queue;
|
||||
using MpcNET.Commands.Reflection;
|
||||
using MpcNET.Tags;
|
||||
using MpcNET.Types;
|
||||
using MpcNET.Types.Filters;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
class ShuffleHandler
|
||||
{
|
||||
private readonly MPDHandler _mpd;
|
||||
|
||||
public int AddedSongs = 0;
|
||||
public List<string> SongList { get; }
|
||||
|
||||
public ShuffleHandler()
|
||||
{
|
||||
SongList = new();
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
}
|
||||
|
||||
public async Task GetSongsFromFilter(List<IFilter> filter, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
SongList.Clear();
|
||||
|
||||
int songTotal = _mpd.GetStats().Songs;
|
||||
|
||||
IEnumerable<IMpdFile> response = await _mpd.SafelySendCommandAsync(new SearchCommand(filter, 0, songTotal + 1));
|
||||
foreach (IMpdFile file in response)
|
||||
SongList.Add(file.Path);
|
||||
}
|
||||
|
||||
public async Task AddToQueueRandom(int SongNumber, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
int addedSongs = 0;
|
||||
|
||||
var commandList = new CommandList();
|
||||
int songTotal = _mpd.GetStats().Songs;
|
||||
|
||||
for (int i = 0; i < SongNumber; i++)
|
||||
{
|
||||
int song = new Random().Next(0, songTotal - 1);
|
||||
commandList.Add(new SearchAddCommand(new FilterTag(MpdTags.Title, "", FilterOperator.Contains), song, song + 1));
|
||||
addedSongs++;
|
||||
}
|
||||
|
||||
// play if stopped or unknown state (no queue managing at the moment, so mandatory)
|
||||
if ((_mpd.GetStatus().State != MpdState.Play && _mpd.GetStatus().State != MpdState.Pause))
|
||||
commandList.Add(new PlayCommand(0));
|
||||
|
||||
await _mpd.SafelySendCommandAsync(commandList);
|
||||
}
|
||||
|
||||
public async Task AddToQueueFilter(int SongNumber, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
int AddedSongs = 0;
|
||||
|
||||
// more (or equal) requested songs than available => add everything
|
||||
if (SongNumber >= SongList.Count)
|
||||
{
|
||||
var commandList = new CommandList();
|
||||
foreach (string path in SongList)
|
||||
{
|
||||
commandList.Add(new AddCommand(path));
|
||||
AddedSongs++;
|
||||
}
|
||||
|
||||
commandList.Add(new PlayCommand(0));
|
||||
await _mpd.SafelySendCommandAsync(commandList);
|
||||
}
|
||||
// more available songs than requested =>
|
||||
// we add unique indexes until we reach the requested amount
|
||||
else
|
||||
{
|
||||
HashSet<int> SongIndex = new();
|
||||
while (SongIndex.Count < SongNumber)
|
||||
{
|
||||
int MaxIndex = new Random().Next(0, SongList.Count - 1);
|
||||
SongIndex.Add(MaxIndex);
|
||||
}
|
||||
|
||||
var commandList = new CommandList();
|
||||
foreach (int index in SongIndex)
|
||||
{
|
||||
commandList.Add(new AddCommand(SongList[index]));
|
||||
AddedSongs++;
|
||||
}
|
||||
|
||||
commandList.Add(new PlayCommand(0));
|
||||
await _mpd.SafelySendCommandAsync(commandList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
|
||||
namespace unison
|
||||
@ -9,23 +8,30 @@ namespace unison
|
||||
public class SnapcastHandler
|
||||
{
|
||||
private readonly Process _snapcast = new();
|
||||
public bool Started { get; private set; }
|
||||
|
||||
public SnapcastHandler()
|
||||
{
|
||||
}
|
||||
public bool HasStarted { get; private set; }
|
||||
|
||||
public void OnConnectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (Properties.Settings.Default.snapcast_startup)
|
||||
{
|
||||
var mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
if (mpd._connected)
|
||||
Start();
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
if (mpd.IsConnected())
|
||||
LaunchOrExit();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateInterface()
|
||||
private void HandleExit(object sender, EventArgs e)
|
||||
{
|
||||
_snapcast.Kill();
|
||||
HasStarted = false;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateInterface();
|
||||
});
|
||||
}
|
||||
|
||||
public static void UpdateInterface()
|
||||
{
|
||||
TaskbarIcon Systray = (TaskbarIcon)Application.Current.Properties["systray"];
|
||||
SystrayViewModel DataContext = Systray.DataContext as SystrayViewModel;
|
||||
@ -35,46 +41,44 @@ namespace unison
|
||||
{
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.OnSnapcastChanged();
|
||||
}, DispatcherPriority.ContextIdle);
|
||||
});
|
||||
}
|
||||
|
||||
public void Start()
|
||||
public void LaunchOrExit(bool ForceExit = false)
|
||||
{
|
||||
if (!Started)
|
||||
if (!HasStarted && !ForceExit)
|
||||
{
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
|
||||
_snapcast.StartInfo.FileName = Properties.Settings.Default.snapcast_path + @"\snapclient.exe";
|
||||
_snapcast.StartInfo.Arguments = $"--host {Properties.Settings.Default.mpd_host}";
|
||||
_snapcast.StartInfo.Arguments = $"--host {mpd._ipAddress}";
|
||||
_snapcast.StartInfo.CreateNoWindow = !Properties.Settings.Default.snapcast_window;
|
||||
_snapcast.EnableRaisingEvents = true;
|
||||
_snapcast.Exited += new EventHandler(HandleExit);
|
||||
|
||||
try
|
||||
{
|
||||
_snapcast.Start();
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
MessageBox.Show($"[Snapcast error]\nInvalid path: {err.Message}\n\nCurrent path: {Properties.Settings.Default.snapcast_path}\nYou can reset it in the settings if needed.",
|
||||
MessageBox.Show($"[{Resources.Resources.Snapcast_Popup1}]\n" +
|
||||
$"{Resources.Resources.Snapcast_Popup2} {err.Message}\n\n" +
|
||||
$"{Resources.Resources.Snapcast_Popup3} {Properties.Settings.Default.snapcast_path}\n" +
|
||||
$"{Resources.Resources.Snapcast_Popup4}",
|
||||
"unison", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Trace.WriteLine(err.Message);
|
||||
return;
|
||||
}
|
||||
Started = true;
|
||||
UpdateInterface();
|
||||
HasStarted = true;
|
||||
}
|
||||
else
|
||||
else if (HasStarted)
|
||||
{
|
||||
_snapcast.Kill();
|
||||
Started = false;
|
||||
UpdateInterface();
|
||||
HasStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (Started)
|
||||
{
|
||||
_snapcast.Kill();
|
||||
Started = false;
|
||||
if (!ForceExit)
|
||||
UpdateInterface();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
Handlers/UpdateHandler.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System.Windows;
|
||||
using AutoUpdaterDotNET;
|
||||
|
||||
namespace unison.Handlers
|
||||
{
|
||||
internal class UpdateHandler
|
||||
{
|
||||
readonly string xmlFile = "https://raw.githubusercontent.com/ZetaKebab/unison/main/Installer/unison.xml";
|
||||
|
||||
private bool _UpdateAvailable = false;
|
||||
private bool _RequestedCheck = false;
|
||||
|
||||
public bool UpdateAvailable() => _UpdateAvailable;
|
||||
|
||||
public UpdateHandler()
|
||||
{
|
||||
AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
|
||||
Start();
|
||||
}
|
||||
|
||||
public void Start(bool RequestCheck = false)
|
||||
{
|
||||
_RequestedCheck = RequestCheck;
|
||||
AutoUpdater.Start(xmlFile);
|
||||
}
|
||||
|
||||
private static string CutVersionNumber(string number)
|
||||
{
|
||||
return number.Substring(0, number.LastIndexOf("."));
|
||||
}
|
||||
|
||||
private void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
|
||||
{
|
||||
if (args.Error == null)
|
||||
{
|
||||
if (args.IsUpdateAvailable)
|
||||
{
|
||||
_UpdateAvailable = true;
|
||||
string number = CutVersionNumber(args.CurrentVersion);
|
||||
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.UpdateUpdateStatus(number);
|
||||
|
||||
MessageBoxResult Result = MessageBox.Show($"{Resources.Resources.Update_Message1} {number}.\n{Resources.Resources.Update_Message2}",
|
||||
"unison", MessageBoxButton.YesNo, MessageBoxImage.Information);
|
||||
if (Result == MessageBoxResult.Yes)
|
||||
AutoUpdater.DownloadUpdate(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_RequestedCheck)
|
||||
MessageBox.Show($"{Resources.Resources.Update_NoUpdate}", "unison", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
Installer/unison.iss
Normal file
@ -0,0 +1,43 @@
|
||||
#define Name "unison"
|
||||
#define Version "1.5"
|
||||
#define Snapcast "snapcast_0.31"
|
||||
#define Publisher "Th<54>o Marchal"
|
||||
#define URL "https://github.com/ZetaKebab/unison"
|
||||
#define ExeName "unison.exe"
|
||||
|
||||
[Setup]
|
||||
AppName={#Name}
|
||||
AppVersion={#Version}
|
||||
AppVerName={#Name} v{#Version}
|
||||
AppPublisher={#Publisher}
|
||||
AppPublisherURL={#URL}
|
||||
AppSupportURL={#URL}
|
||||
AppUpdatesURL={#URL}
|
||||
DefaultDirName={autopf}\{#Name}
|
||||
DisableProgramGroupPage=yes
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
OutputBaseFilename="{#Name}-v{#Version}-setup"
|
||||
OutputDir=..\publish\installer
|
||||
SetupIconFile=..\Resources\icon-full.ico
|
||||
UninstallDisplayIcon = "{app}\{#Name}.exe"
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
|
||||
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
|
||||
|
||||
[Files]
|
||||
Source: "..\publish\{#ExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\publish\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\publish\unison.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\publish\{#Snapcast}\*"; DestDir: "{app}\{#Snapcast}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#Name}"; Filename: "{app}\{#ExeName}"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#Name}.exe"; Parameters: "-frominstaller"; Flags: nowait postinstall skipifsilent
|
||||
|
7
Installer/unison.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.5.0.0</version>
|
||||
<url>https://github.com/ZetaKebab/unison/releases/download/v1.5/unison-v1.5-setup.exe</url>
|
||||
<changelog>https://raw.githubusercontent.com/ZetaKebab/unison/main/CHANGELOG.md</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
2
LICENSE
@ -1,4 +1,4 @@
|
||||
MIT License Copyright (c) 2021 Théo Marchal
|
||||
MIT License Copyright (c) 2025 Théo Marchal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
@ -4,14 +4,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Configuration>Release-Stable</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<SelfContained>false</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishSingleFile>True</PublishSingleFile>
|
||||
<PublishReadyToRun>False</PublishReadyToRun>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
</Project>
|
276
Properties/Settings.Designer.cs
generated
@ -12,7 +12,7 @@ namespace unison.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.13.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
@ -73,7 +73,19 @@ namespace unison.Properties {
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("snapclient_0.25.0-1_win64")]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("False")]
|
||||
public bool snapcast_window {
|
||||
get {
|
||||
return ((bool)(this["snapcast_window"]));
|
||||
}
|
||||
set {
|
||||
this["snapcast_window"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("snapcast_0.31")]
|
||||
public string snapcast_path {
|
||||
get {
|
||||
return ((string)(this["snapcast_path"]));
|
||||
@ -109,13 +121,265 @@ namespace unison.Properties {
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("False")]
|
||||
public bool snapcast_window {
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("2")]
|
||||
public uint nextTrack_mod {
|
||||
get {
|
||||
return ((bool)(this["snapcast_window"]));
|
||||
return ((uint)(this["nextTrack_mod"]));
|
||||
}
|
||||
set {
|
||||
this["snapcast_window"] = value;
|
||||
this["nextTrack_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("176")]
|
||||
public uint nextTrack_vk {
|
||||
get {
|
||||
return ((uint)(this["nextTrack_vk"]));
|
||||
}
|
||||
set {
|
||||
this["nextTrack_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("2")]
|
||||
public uint previousTrack_mod {
|
||||
get {
|
||||
return ((uint)(this["previousTrack_mod"]));
|
||||
}
|
||||
set {
|
||||
this["previousTrack_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("177")]
|
||||
public uint previousTrack_vk {
|
||||
get {
|
||||
return ((uint)(this["previousTrack_vk"]));
|
||||
}
|
||||
set {
|
||||
this["previousTrack_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("2")]
|
||||
public uint playPause_mod {
|
||||
get {
|
||||
return ((uint)(this["playPause_mod"]));
|
||||
}
|
||||
set {
|
||||
this["playPause_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("179")]
|
||||
public uint playPause_vk {
|
||||
get {
|
||||
return ((uint)(this["playPause_vk"]));
|
||||
}
|
||||
set {
|
||||
this["playPause_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("2")]
|
||||
public uint volumeUp_mod {
|
||||
get {
|
||||
return ((uint)(this["volumeUp_mod"]));
|
||||
}
|
||||
set {
|
||||
this["volumeUp_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("175")]
|
||||
public uint volumeUp_vk {
|
||||
get {
|
||||
return ((uint)(this["volumeUp_vk"]));
|
||||
}
|
||||
set {
|
||||
this["volumeUp_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("2")]
|
||||
public uint volumeDown_mod {
|
||||
get {
|
||||
return ((uint)(this["volumeDown_mod"]));
|
||||
}
|
||||
set {
|
||||
this["volumeDown_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("174")]
|
||||
public uint volumeDown_vk {
|
||||
get {
|
||||
return ((uint)(this["volumeDown_vk"]));
|
||||
}
|
||||
set {
|
||||
this["volumeDown_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("2")]
|
||||
public uint volumeMute_mod {
|
||||
get {
|
||||
return ((uint)(this["volumeMute_mod"]));
|
||||
}
|
||||
set {
|
||||
this["volumeMute_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("173")]
|
||||
public uint volumeMute_vk {
|
||||
get {
|
||||
return ((uint)(this["volumeMute_vk"]));
|
||||
}
|
||||
set {
|
||||
this["volumeMute_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("3")]
|
||||
public uint showWindow_mod {
|
||||
get {
|
||||
return ((uint)(this["showWindow_mod"]));
|
||||
}
|
||||
set {
|
||||
this["showWindow_mod"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("13")]
|
||||
public uint showWindow_vk {
|
||||
get {
|
||||
return ((uint)(this["showWindow_vk"]));
|
||||
}
|
||||
set {
|
||||
this["showWindow_vk"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("10")]
|
||||
public double MainWindowTop {
|
||||
get {
|
||||
return ((double)(this["MainWindowTop"]));
|
||||
}
|
||||
set {
|
||||
this["MainWindowTop"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("10")]
|
||||
public double MainWindowLeft {
|
||||
get {
|
||||
return ((double)(this["MainWindowLeft"]));
|
||||
}
|
||||
set {
|
||||
this["MainWindowLeft"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("10")]
|
||||
public double RadiosWindowTop {
|
||||
get {
|
||||
return ((double)(this["RadiosWindowTop"]));
|
||||
}
|
||||
set {
|
||||
this["RadiosWindowTop"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("1000")]
|
||||
public double RadiosWindowLeft {
|
||||
get {
|
||||
return ((double)(this["RadiosWindowLeft"]));
|
||||
}
|
||||
set {
|
||||
this["RadiosWindowLeft"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("10")]
|
||||
public double ShuffleWindowTop {
|
||||
get {
|
||||
return ((double)(this["ShuffleWindowTop"]));
|
||||
}
|
||||
set {
|
||||
this["ShuffleWindowTop"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("330")]
|
||||
public double ShuffleWindowLeft {
|
||||
get {
|
||||
return ((double)(this["ShuffleWindowLeft"]));
|
||||
}
|
||||
set {
|
||||
this["ShuffleWindowLeft"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("330")]
|
||||
public double SettingsWindowTop {
|
||||
get {
|
||||
return ((double)(this["SettingsWindowTop"]));
|
||||
}
|
||||
set {
|
||||
this["SettingsWindowTop"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("600")]
|
||||
public double SettingsWindowLeft {
|
||||
get {
|
||||
return ((double)(this["SettingsWindowLeft"]));
|
||||
}
|
||||
set {
|
||||
this["SettingsWindowLeft"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
<Setting Name="snapcast_path" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)">snapclient_0.25.0-1_win64</Value>
|
||||
<Value Profile="(Default)">snapcast_0.31</Value>
|
||||
</Setting>
|
||||
<Setting Name="snapcast_port" Type="System.Int32" Scope="User">
|
||||
<Value Profile="(Default)">1704</Value>
|
||||
@ -26,5 +26,71 @@
|
||||
<Setting Name="volume_offset" Type="System.Int32" Scope="User">
|
||||
<Value Profile="(Default)">5</Value>
|
||||
</Setting>
|
||||
<Setting Name="nextTrack_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">2</Value>
|
||||
</Setting>
|
||||
<Setting Name="nextTrack_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">176</Value>
|
||||
</Setting>
|
||||
<Setting Name="previousTrack_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">2</Value>
|
||||
</Setting>
|
||||
<Setting Name="previousTrack_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">177</Value>
|
||||
</Setting>
|
||||
<Setting Name="playPause_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">2</Value>
|
||||
</Setting>
|
||||
<Setting Name="playPause_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">179</Value>
|
||||
</Setting>
|
||||
<Setting Name="volumeUp_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">2</Value>
|
||||
</Setting>
|
||||
<Setting Name="volumeUp_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">175</Value>
|
||||
</Setting>
|
||||
<Setting Name="volumeDown_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">2</Value>
|
||||
</Setting>
|
||||
<Setting Name="volumeDown_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">174</Value>
|
||||
</Setting>
|
||||
<Setting Name="volumeMute_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">2</Value>
|
||||
</Setting>
|
||||
<Setting Name="volumeMute_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">173</Value>
|
||||
</Setting>
|
||||
<Setting Name="showWindow_mod" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">3</Value>
|
||||
</Setting>
|
||||
<Setting Name="showWindow_vk" Type="System.UInt32" Scope="User">
|
||||
<Value Profile="(Default)">13</Value>
|
||||
</Setting>
|
||||
<Setting Name="MainWindowTop" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">10</Value>
|
||||
</Setting>
|
||||
<Setting Name="MainWindowLeft" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">10</Value>
|
||||
</Setting>
|
||||
<Setting Name="RadiosWindowTop" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">10</Value>
|
||||
</Setting>
|
||||
<Setting Name="RadiosWindowLeft" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">1000</Value>
|
||||
</Setting>
|
||||
<Setting Name="ShuffleWindowTop" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">10</Value>
|
||||
</Setting>
|
||||
<Setting Name="ShuffleWindowLeft" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">330</Value>
|
||||
</Setting>
|
||||
<Setting Name="SettingsWindowTop" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">330</Value>
|
||||
</Setting>
|
||||
<Setting Name="SettingsWindowLeft" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">600</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
52
README.md
@ -1,3 +1,53 @@
|
||||
# unison
|
||||
|
||||
mpd client
|
||||

|
||||
|
||||
**unison** is a very simple [Music Player Daemon (MPD)](https://www.musicpd.org/) daemon client with the following goals:
|
||||
|
||||
* lightweight window that can be toggled with shortcuts
|
||||
* music control through rebindable shortcuts
|
||||
* [Snapcast](https://github.com/badaix/snapcast) integration
|
||||
* radio stations
|
||||
* basic shuffle system
|
||||
|
||||
## Features
|
||||
|
||||
### Window
|
||||
|
||||
By default, unison works as a daemon in the taskbar system tray. You can display the main window when needed at any time with a shortcut.
|
||||
|
||||

|
||||
|
||||
### Shortcuts
|
||||
|
||||
You can control your music at anytime with the shortcuts. They are usable system-wide, even if the window is not visible. They are of course fully rebindable.
|
||||
|
||||

|
||||
|
||||
### Snapcast
|
||||
|
||||
Embedding a Snapcast client allows to listen to music on multiple devices. For example, if you music is on a distant server connected to speakers in your living room, you can still listen to it on your computer running unison with this integrated client.
|
||||
|
||||
### Radio stations
|
||||
|
||||
Through [Radio-Browser](https://www.radio-browser.info), a community database, you can play radio streams directly from unison. There are more than 28,000 stations recorded on this service, so it is a nice way to discover new music and cultures.
|
||||
|
||||

|
||||
|
||||
### Shuffle system
|
||||
|
||||
A shuffle system is available, allowing you to only use unison as a (very basic) music manager. You can use filters to have a tailored shuffle experience.
|
||||
|
||||

|
||||
|
||||
## Translations
|
||||
|
||||
unison is translated in English, French and Spanish. You can contribute if you want!
|
||||
|
||||
## The future
|
||||
|
||||
This project started as a simple daemon to control my music with shortcuts, and a Snapcast player. With time, I added a few useful features, while learning C# and WPF. I wanted to make a complete implementation of MPD features (library, playlist, queue management, etc). However, this will probably not happen. Indeed, I have been waiting a few years for Microsoft to settle on a modern implementation of its user interface framework. But it is still not there, and I do not want to spend time for a technology that might die too quickly. There's no coherence in Windows' interfaces, and it is not really motivating. I also want to be able to play Snapcast from iOS, so I think I will most likely invest (natively) my time in this platform for the foreseeable future.
|
||||
|
||||
If I were to continue, the first thing to do would be to make a single window interface, by making a real MVVP implementation, and not one depending on windows like I did here, by iterating on a single program that has become a bit too big to not separate logic from visual.
|
||||
|
||||
I still plan on maintaining this app in a working state, as its working great as is. But I do not intend, for the time being at least, to make dramatic changes as I would have liked to.
|
1062
Resources/Resources.Designer.cs
generated
Normal file
453
Resources/Resources.es-ES.resx
Normal file
@ -0,0 +1,453 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Clear_Queue" xml:space="preserve">
|
||||
<value>Borrar cola</value>
|
||||
</data>
|
||||
<data name="Database" xml:space="preserve">
|
||||
<value>Base de datos</value>
|
||||
</data>
|
||||
<data name="Day" xml:space="preserve">
|
||||
<value>día</value>
|
||||
</data>
|
||||
<data name="Days" xml:space="preserve">
|
||||
<value>días</value>
|
||||
</data>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Salir</value>
|
||||
</data>
|
||||
<data name="FilterType_Album" xml:space="preserve">
|
||||
<value>Album</value>
|
||||
</data>
|
||||
<data name="FilterType_Artist" xml:space="preserve">
|
||||
<value>Artista</value>
|
||||
</data>
|
||||
<data name="FilterType_Directory" xml:space="preserve">
|
||||
<value>Directorio</value>
|
||||
</data>
|
||||
<data name="FilterType_Genre" xml:space="preserve">
|
||||
<value>Género</value>
|
||||
</data>
|
||||
<data name="FilterType_Song" xml:space="preserve">
|
||||
<value>Canción</value>
|
||||
</data>
|
||||
<data name="FilterType_Year" xml:space="preserve">
|
||||
<value>Año</value>
|
||||
</data>
|
||||
<data name="Hour" xml:space="preserve">
|
||||
<value>hora</value>
|
||||
</data>
|
||||
<data name="Hours" xml:space="preserve">
|
||||
<value>horas</value>
|
||||
</data>
|
||||
<data name="Minute" xml:space="preserve">
|
||||
<value>minuto</value>
|
||||
</data>
|
||||
<data name="Minutes" xml:space="preserve">
|
||||
<value>minutos</value>
|
||||
</data>
|
||||
<data name="Operator_Contains" xml:space="preserve">
|
||||
<value>contiene</value>
|
||||
</data>
|
||||
<data name="Operator_Is" xml:space="preserve">
|
||||
<value>es</value>
|
||||
</data>
|
||||
<data name="Operator_IsNot" xml:space="preserve">
|
||||
<value>no es</value>
|
||||
</data>
|
||||
<data name="Play_Queue" xml:space="preserve">
|
||||
<value>Reproducir cola</value>
|
||||
</data>
|
||||
<data name="Queue_Management" xml:space="preserve">
|
||||
<value>Gestión de cola</value>
|
||||
</data>
|
||||
<data name="Radios" xml:space="preserve">
|
||||
<value>Radios</value>
|
||||
</data>
|
||||
<data name="Radio_Country" xml:space="preserve">
|
||||
<value>Países</value>
|
||||
</data>
|
||||
<data name="Radio_Loading" xml:space="preserve">
|
||||
<value>Cargando estaciones...</value>
|
||||
</data>
|
||||
<data name="Radio_Name" xml:space="preserve">
|
||||
<value>Nombre</value>
|
||||
</data>
|
||||
<data name="Radio_NotFound" xml:space="preserve">
|
||||
<value>¡No estaciones encontradas!</value>
|
||||
</data>
|
||||
<data name="Radio_Reset" xml:space="preserve">
|
||||
<value>Reinicializar</value>
|
||||
</data>
|
||||
<data name="Radio_Search" xml:space="preserve">
|
||||
<value>Búsqueda</value>
|
||||
</data>
|
||||
<data name="Radio_SearchStation" xml:space="preserve">
|
||||
<value>Buscar estación</value>
|
||||
</data>
|
||||
<data name="Radio_Tags" xml:space="preserve">
|
||||
<value>Tags</value>
|
||||
</data>
|
||||
<data name="Second" xml:space="preserve">
|
||||
<value>segundo</value>
|
||||
</data>
|
||||
<data name="Seconds" xml:space="preserve">
|
||||
<value>segundos</value>
|
||||
</data>
|
||||
<data name="Settings" xml:space="preserve">
|
||||
<value>Ajustes</value>
|
||||
</data>
|
||||
<data name="Settings_About" xml:space="preserve">
|
||||
<value>Acerca de</value>
|
||||
</data>
|
||||
<data name="Settings_AboutInfo" xml:space="preserve">
|
||||
<value>unison es un software libre. Es construido con las siguientes tecnologías:</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectButton" xml:space="preserve">
|
||||
<value>Conexión</value>
|
||||
</data>
|
||||
<data name="Settings_Connection" xml:space="preserve">
|
||||
<value>Conexión</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionPasswordInfo" xml:space="preserve">
|
||||
<value>Tenga en cuenta que, dado que las contraseñas de MPD no son seguras (se envían en texto sin formato al servidor), se guardan tal cual en el archivo de configuración.</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatus" xml:space="preserve">
|
||||
<value>Estado:</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
|
||||
<value>conectado a MPD</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
|
||||
<value>conectando...</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
|
||||
<value>no conectado.</value>
|
||||
</data>
|
||||
<data name="Settings_Host" xml:space="preserve">
|
||||
<value>Host</value>
|
||||
</data>
|
||||
<data name="Settings_License" xml:space="preserve">
|
||||
<value>Licencia</value>
|
||||
</data>
|
||||
<data name="Settings_MadeBy" xml:space="preserve">
|
||||
<value>Creado por</value>
|
||||
</data>
|
||||
<data name="Settings_NextTrack" xml:space="preserve">
|
||||
<value>Pista siguiente</value>
|
||||
</data>
|
||||
<data name="Settings_Password" xml:space="preserve">
|
||||
<value>Contraseña</value>
|
||||
</data>
|
||||
<data name="Settings_PlayPause" xml:space="preserve">
|
||||
<value>Jugar / pausa</value>
|
||||
</data>
|
||||
<data name="Settings_Port" xml:space="preserve">
|
||||
<value>Puerto</value>
|
||||
</data>
|
||||
<data name="Settings_PreviousTrack" xml:space="preserve">
|
||||
<value>Pista precedente</value>
|
||||
</data>
|
||||
<data name="Settings_Shortcuts" xml:space="preserve">
|
||||
<value>Atajos</value>
|
||||
</data>
|
||||
<data name="Settings_ShortcutsInfo" xml:space="preserve">
|
||||
<value>Tenga en cuenta que si no está reconocida la tecla, esto se debe a una limitación en el funcionamiento de las teclas virtuales.</value>
|
||||
</data>
|
||||
<data name="Settings_ShortcutsKey" xml:space="preserve">
|
||||
<value>Entre una tecla...</value>
|
||||
</data>
|
||||
<data name="Settings_ShowWindow" xml:space="preserve">
|
||||
<value>Mostrar ventana</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle1" xml:space="preserve">
|
||||
<value>La ventana Aleatorio permite agregar canciones aleatorias a la fila. La dos opciones tienen en cuenta el filtro.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle2" xml:space="preserve">
|
||||
<value>Si el filtro es vacío, la integralidad de la biblioteca musical se tiene en cuenta.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle3" xml:space="preserve">
|
||||
<value>El filtro es buscado cada vez que las opciones Agregar a la fila o Aleatorio continuo son usadas.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle4" xml:space="preserve">
|
||||
<value>Agrega un número dado de canciones a la fila. For razones tecnicas, es opción es limitada a 100 canciones aleatorias sin filtro, y a 1000 canciones con filtro.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle5" xml:space="preserve">
|
||||
<value>Activando esa opción, unison va a agregar automaticamente canciones a la fila para nunca llegar al cabo de canciones a escuchar.</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo1" xml:space="preserve">
|
||||
<value>Puede cambiar a su propia versión instalada localmente del cliente Snapcast con una ruta</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo2" xml:space="preserve">
|
||||
<value> absoluta</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo3" xml:space="preserve">
|
||||
<value>.</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastLauch" xml:space="preserve">
|
||||
<value>Lanzar al inicio</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastPath" xml:space="preserve">
|
||||
<value>Ruta del ejecutable</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastPort" xml:space="preserve">
|
||||
<value>Puerto</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastResetButton" xml:space="preserve">
|
||||
<value>Reinicializar</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastWindow" xml:space="preserve">
|
||||
<value>Mostrar ventana de Snapcast</value>
|
||||
</data>
|
||||
<data name="Settings_SourceCode1" xml:space="preserve">
|
||||
<value>Código fuente disponible gratuitamente</value>
|
||||
</data>
|
||||
<data name="Settings_SourceCode2" xml:space="preserve">
|
||||
<value>aquí</value>
|
||||
</data>
|
||||
<data name="Settings_UpdateDatabase" xml:space="preserve">
|
||||
<value>Actualizar base de datos</value>
|
||||
</data>
|
||||
<data name="Settings_Version" xml:space="preserve">
|
||||
<value>Versión:</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeDown" xml:space="preserve">
|
||||
<value>Bajar volumen</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeMute" xml:space="preserve">
|
||||
<value>Volumen mudo</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeOffset" xml:space="preserve">
|
||||
<value>Intervalo de volumen</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeUp" xml:space="preserve">
|
||||
<value>Subir volumen</value>
|
||||
</data>
|
||||
<data name="ShowWindow" xml:space="preserve">
|
||||
<value>Mostrar ventana</value>
|
||||
</data>
|
||||
<data name="Shuffle" xml:space="preserve">
|
||||
<value>Aleatorio</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage1" xml:space="preserve">
|
||||
<value>Agregando</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage2" xml:space="preserve">
|
||||
<value>canciones...</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage3" xml:space="preserve">
|
||||
<value>¡terminado!</value>
|
||||
</data>
|
||||
<data name="Shuffle_Continuous" xml:space="preserve">
|
||||
<value>Aleatorio continuo</value>
|
||||
</data>
|
||||
<data name="Shuffle_ContinuousEnable" xml:space="preserve">
|
||||
<value>Empezar aleatorio continuo</value>
|
||||
</data>
|
||||
<data name="Shuffle_Filter" xml:space="preserve">
|
||||
<value>Filtro</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterQuery" xml:space="preserve">
|
||||
<value>Búsqueda de filtro</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterReset" xml:space="preserve">
|
||||
<value>Reinicializar</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterSongNumber" xml:space="preserve">
|
||||
<value>Canciones en el filtro:</value>
|
||||
</data>
|
||||
<data name="Shuffle_Querying1" xml:space="preserve">
|
||||
<value>Búsqueda de filtro...</value>
|
||||
</data>
|
||||
<data name="Shuffle_Querying2" xml:space="preserve">
|
||||
<value>¡terminado!</value>
|
||||
</data>
|
||||
<data name="Shuffle_Queue" xml:space="preserve">
|
||||
<value>Agregar a la fila</value>
|
||||
</data>
|
||||
<data name="Shuffle_QueueSongs" xml:space="preserve">
|
||||
<value>Canciones para agregar</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup1" xml:space="preserve">
|
||||
<value>Error Snapcast</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup2" xml:space="preserve">
|
||||
<value>Ruta no válida:</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup3" xml:space="preserve">
|
||||
<value>Ruta actual:</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup4" xml:space="preserve">
|
||||
<value>Puede restablecerlo en la configuración si es necesario.</value>
|
||||
</data>
|
||||
<data name="StartSnapcast" xml:space="preserve">
|
||||
<value>Iniciar Snapcast</value>
|
||||
</data>
|
||||
<data name="Stats_Albums" xml:space="preserve">
|
||||
<value>Álbumes:</value>
|
||||
</data>
|
||||
<data name="Stats_Artists" xml:space="preserve">
|
||||
<value>Artistas:</value>
|
||||
</data>
|
||||
<data name="Stats_LastDatabaseUpdate" xml:space="preserve">
|
||||
<value>Última actualización de la base de datos:</value>
|
||||
</data>
|
||||
<data name="Stats_Songs" xml:space="preserve">
|
||||
<value>Canciones:</value>
|
||||
</data>
|
||||
<data name="Stats_TotalPlaytime" xml:space="preserve">
|
||||
<value>Tiempo de juego total:</value>
|
||||
</data>
|
||||
<data name="Stats_TotalTimePlayed" xml:space="preserve">
|
||||
<value>Tiempo total jugado:</value>
|
||||
</data>
|
||||
<data name="Stats_UpdateDBMessage1" xml:space="preserve">
|
||||
<value>Actualizando base de datos...</value>
|
||||
</data>
|
||||
<data name="Stats_UpdateDBMessage2" xml:space="preserve">
|
||||
<value> ¡terminado!</value>
|
||||
</data>
|
||||
<data name="Stats_Uptime" xml:space="preserve">
|
||||
<value>Tiempo de actividad de MPD:</value>
|
||||
</data>
|
||||
<data name="StopSnapcast" xml:space="preserve">
|
||||
<value>Parar Snapcast</value>
|
||||
</data>
|
||||
<data name="Update_ButtonCheck" xml:space="preserve">
|
||||
<value>Buscar actualización</value>
|
||||
</data>
|
||||
<data name="Update_ButtonStart" xml:space="preserve">
|
||||
<value>Actualizar</value>
|
||||
</data>
|
||||
<data name="Update_Message1" xml:space="preserve">
|
||||
<value>¡Actualización disponible! La nueva versión es la</value>
|
||||
</data>
|
||||
<data name="Update_Message2" xml:space="preserve">
|
||||
<value>¿Instalar ahora?</value>
|
||||
</data>
|
||||
<data name="Update_NoUpdate" xml:space="preserve">
|
||||
<value>No actualización disponible.</value>
|
||||
</data>
|
||||
<data name="Update_String1" xml:space="preserve">
|
||||
<value>¡Nueva versión</value>
|
||||
</data>
|
||||
<data name="Update_String2" xml:space="preserve">
|
||||
<value>disponible!</value>
|
||||
</data>
|
||||
</root>
|
453
Resources/Resources.fr-FR.resx
Normal file
@ -0,0 +1,453 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Clear_Queue" xml:space="preserve">
|
||||
<value>Effacer la file</value>
|
||||
</data>
|
||||
<data name="Database" xml:space="preserve">
|
||||
<value>Base de donnée</value>
|
||||
</data>
|
||||
<data name="Day" xml:space="preserve">
|
||||
<value>jour</value>
|
||||
</data>
|
||||
<data name="Days" xml:space="preserve">
|
||||
<value>jours</value>
|
||||
</data>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Quitter</value>
|
||||
</data>
|
||||
<data name="FilterType_Album" xml:space="preserve">
|
||||
<value>Album</value>
|
||||
</data>
|
||||
<data name="FilterType_Artist" xml:space="preserve">
|
||||
<value>Artiste</value>
|
||||
</data>
|
||||
<data name="FilterType_Directory" xml:space="preserve">
|
||||
<value>Dossier</value>
|
||||
</data>
|
||||
<data name="FilterType_Genre" xml:space="preserve">
|
||||
<value>Genre</value>
|
||||
</data>
|
||||
<data name="FilterType_Song" xml:space="preserve">
|
||||
<value>Chanson</value>
|
||||
</data>
|
||||
<data name="FilterType_Year" xml:space="preserve">
|
||||
<value>Année</value>
|
||||
</data>
|
||||
<data name="Hour" xml:space="preserve">
|
||||
<value>heure</value>
|
||||
</data>
|
||||
<data name="Hours" xml:space="preserve">
|
||||
<value>heures</value>
|
||||
</data>
|
||||
<data name="Minute" xml:space="preserve">
|
||||
<value>minute</value>
|
||||
</data>
|
||||
<data name="Minutes" xml:space="preserve">
|
||||
<value>minutes</value>
|
||||
</data>
|
||||
<data name="Operator_Contains" xml:space="preserve">
|
||||
<value>contient</value>
|
||||
</data>
|
||||
<data name="Operator_Is" xml:space="preserve">
|
||||
<value>est</value>
|
||||
</data>
|
||||
<data name="Operator_IsNot" xml:space="preserve">
|
||||
<value>n'est pas</value>
|
||||
</data>
|
||||
<data name="Play_Queue" xml:space="preserve">
|
||||
<value>Jouer la file</value>
|
||||
</data>
|
||||
<data name="Queue_Management" xml:space="preserve">
|
||||
<value>Gestion de file</value>
|
||||
</data>
|
||||
<data name="Radios" xml:space="preserve">
|
||||
<value>Radios</value>
|
||||
</data>
|
||||
<data name="Radio_Country" xml:space="preserve">
|
||||
<value>Pays</value>
|
||||
</data>
|
||||
<data name="Radio_Loading" xml:space="preserve">
|
||||
<value>Recherche de stations...</value>
|
||||
</data>
|
||||
<data name="Radio_Name" xml:space="preserve">
|
||||
<value>Nom</value>
|
||||
</data>
|
||||
<data name="Radio_NotFound" xml:space="preserve">
|
||||
<value>Aucune station trouvée !</value>
|
||||
</data>
|
||||
<data name="Radio_Reset" xml:space="preserve">
|
||||
<value>Réinitialiser</value>
|
||||
</data>
|
||||
<data name="Radio_Search" xml:space="preserve">
|
||||
<value>Chercher</value>
|
||||
</data>
|
||||
<data name="Radio_SearchStation" xml:space="preserve">
|
||||
<value>Recherche de station</value>
|
||||
</data>
|
||||
<data name="Radio_Tags" xml:space="preserve">
|
||||
<value>Tags</value>
|
||||
</data>
|
||||
<data name="Second" xml:space="preserve">
|
||||
<value>seconde</value>
|
||||
</data>
|
||||
<data name="Seconds" xml:space="preserve">
|
||||
<value>secondes</value>
|
||||
</data>
|
||||
<data name="Settings" xml:space="preserve">
|
||||
<value>Configuration</value>
|
||||
</data>
|
||||
<data name="Settings_About" xml:space="preserve">
|
||||
<value>À propos</value>
|
||||
</data>
|
||||
<data name="Settings_AboutInfo" xml:space="preserve">
|
||||
<value>unison est un logiciel libre. Il est développé avec les technologies suivantes :</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectButton" xml:space="preserve">
|
||||
<value>Connexion</value>
|
||||
</data>
|
||||
<data name="Settings_Connection" xml:space="preserve">
|
||||
<value>Connexion</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionPasswordInfo" xml:space="preserve">
|
||||
<value>Veuillez noter que comme les mots de passe de MPD ne sont pas sécurisés (ils sont envoyés en clair au serveur), ils sont sauvegardés tels quels.</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatus" xml:space="preserve">
|
||||
<value>Statut :</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
|
||||
<value>connecté à MPD</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
|
||||
<value>connexion en cours...</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
|
||||
<value>non connecté.</value>
|
||||
</data>
|
||||
<data name="Settings_Host" xml:space="preserve">
|
||||
<value>Hôte</value>
|
||||
</data>
|
||||
<data name="Settings_License" xml:space="preserve">
|
||||
<value>Licence</value>
|
||||
</data>
|
||||
<data name="Settings_MadeBy" xml:space="preserve">
|
||||
<value>Créé par</value>
|
||||
</data>
|
||||
<data name="Settings_NextTrack" xml:space="preserve">
|
||||
<value>Piste suivante</value>
|
||||
</data>
|
||||
<data name="Settings_Password" xml:space="preserve">
|
||||
<value>Mot de passe</value>
|
||||
</data>
|
||||
<data name="Settings_PlayPause" xml:space="preserve">
|
||||
<value>Jouer / pause</value>
|
||||
</data>
|
||||
<data name="Settings_Port" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Settings_PreviousTrack" xml:space="preserve">
|
||||
<value>Piste précédente</value>
|
||||
</data>
|
||||
<data name="Settings_Shortcuts" xml:space="preserve">
|
||||
<value>Raccourcis</value>
|
||||
</data>
|
||||
<data name="Settings_ShortcutsInfo" xml:space="preserve">
|
||||
<value>Veuillez noter que si votre touche n'est pas reconnue, cela est du à la manière dont les touches virtuelles sont gérées par Windows.</value>
|
||||
</data>
|
||||
<data name="Settings_ShortcutsKey" xml:space="preserve">
|
||||
<value>Appuyez sur une touche...</value>
|
||||
</data>
|
||||
<data name="Settings_ShowWindow" xml:space="preserve">
|
||||
<value>Afficher la fenêtre</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle1" xml:space="preserve">
|
||||
<value>La fenêtre aléatoire permet d'ajouter des chansons aléatoires à la file. Les deux options prennent en compte le filtre.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle2" xml:space="preserve">
|
||||
<value>Si le filtre est vide, l'intégralité de la bibliothèque est prise en compte.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle3" xml:space="preserve">
|
||||
<value>Le filtre est recherché à chaque fois que les options Ajouter à la file ou Aléatoire continu sont utilisées.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle4" xml:space="preserve">
|
||||
<value>Ajoute un nombre fixe de chansons à la file. Pour des raisons techniques, cette option est limitée à 100 chansons aléatoires sans filtre, et à 1000 chansons avec filtre.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle5" xml:space="preserve">
|
||||
<value>En activant cette option, unison va ajouter automatiquement des chansons à la file pour ne jamais arriver à cours de chansons à écouter.</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo1" xml:space="preserve">
|
||||
<value>Il est possible de mettre votre version localement installé de Snapcast avec un </value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo2" xml:space="preserve">
|
||||
<value>chemin absolu</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo3" xml:space="preserve">
|
||||
<value>.</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastLauch" xml:space="preserve">
|
||||
<value>Lancer au démarrage</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastPath" xml:space="preserve">
|
||||
<value>Chemin de l'exécutable</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastPort" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastResetButton" xml:space="preserve">
|
||||
<value>Réinitialiser</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastWindow" xml:space="preserve">
|
||||
<value>Afficher la fenêtre de Snapcast</value>
|
||||
</data>
|
||||
<data name="Settings_SourceCode1" xml:space="preserve">
|
||||
<value>Code source librement disponible</value>
|
||||
</data>
|
||||
<data name="Settings_SourceCode2" xml:space="preserve">
|
||||
<value>ici</value>
|
||||
</data>
|
||||
<data name="Settings_UpdateDatabase" xml:space="preserve">
|
||||
<value>Mettre à jour la base de donnée</value>
|
||||
</data>
|
||||
<data name="Settings_Version" xml:space="preserve">
|
||||
<value>Version :</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeDown" xml:space="preserve">
|
||||
<value>Baisse de volume</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeMute" xml:space="preserve">
|
||||
<value>Volume en sourdine</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeOffset" xml:space="preserve">
|
||||
<value>Écart de volume</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeUp" xml:space="preserve">
|
||||
<value>Augmentation de volume</value>
|
||||
</data>
|
||||
<data name="ShowWindow" xml:space="preserve">
|
||||
<value>Montrer la fenêtre</value>
|
||||
</data>
|
||||
<data name="Shuffle" xml:space="preserve">
|
||||
<value>Aléatoire</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage1" xml:space="preserve">
|
||||
<value>Ajout de</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage2" xml:space="preserve">
|
||||
<value>chansons...</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage3" xml:space="preserve">
|
||||
<value>fini !</value>
|
||||
</data>
|
||||
<data name="Shuffle_Continuous" xml:space="preserve">
|
||||
<value>Aléatoire continu</value>
|
||||
</data>
|
||||
<data name="Shuffle_ContinuousEnable" xml:space="preserve">
|
||||
<value>Activer le mode aléatoire continu</value>
|
||||
</data>
|
||||
<data name="Shuffle_Filter" xml:space="preserve">
|
||||
<value>Filtre</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterQuery" xml:space="preserve">
|
||||
<value>Recherche du filtre</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterReset" xml:space="preserve">
|
||||
<value>Réinitialiser</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterSongNumber" xml:space="preserve">
|
||||
<value>Nombre de chansons dans le filtre :</value>
|
||||
</data>
|
||||
<data name="Shuffle_Querying1" xml:space="preserve">
|
||||
<value>Recherche du filtre...</value>
|
||||
</data>
|
||||
<data name="Shuffle_Querying2" xml:space="preserve">
|
||||
<value>fini !</value>
|
||||
</data>
|
||||
<data name="Shuffle_Queue" xml:space="preserve">
|
||||
<value>Ajouter à la file</value>
|
||||
</data>
|
||||
<data name="Shuffle_QueueSongs" xml:space="preserve">
|
||||
<value>Chansons à ajouter</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup1" xml:space="preserve">
|
||||
<value>Erreur Snapcast</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup2" xml:space="preserve">
|
||||
<value>Chemin invalide :</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup3" xml:space="preserve">
|
||||
<value>Chemin actuel :</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup4" xml:space="preserve">
|
||||
<value>Vous pouvez le réinitialiser dans la configuration.</value>
|
||||
</data>
|
||||
<data name="StartSnapcast" xml:space="preserve">
|
||||
<value>Démarrer Snapcast</value>
|
||||
</data>
|
||||
<data name="Stats_Albums" xml:space="preserve">
|
||||
<value>Albums :</value>
|
||||
</data>
|
||||
<data name="Stats_Artists" xml:space="preserve">
|
||||
<value>Artistes :</value>
|
||||
</data>
|
||||
<data name="Stats_LastDatabaseUpdate" xml:space="preserve">
|
||||
<value>Mise à jour de la base de données :</value>
|
||||
</data>
|
||||
<data name="Stats_Songs" xml:space="preserve">
|
||||
<value>Morceaux :</value>
|
||||
</data>
|
||||
<data name="Stats_TotalPlaytime" xml:space="preserve">
|
||||
<value>Temps total :</value>
|
||||
</data>
|
||||
<data name="Stats_TotalTimePlayed" xml:space="preserve">
|
||||
<value>Temps d'écoute écoulé :</value>
|
||||
</data>
|
||||
<data name="Stats_UpdateDBMessage1" xml:space="preserve">
|
||||
<value>Mise à jour de la base de donnée...</value>
|
||||
</data>
|
||||
<data name="Stats_UpdateDBMessage2" xml:space="preserve">
|
||||
<value> fini !</value>
|
||||
</data>
|
||||
<data name="Stats_Uptime" xml:space="preserve">
|
||||
<value>MPD lancé depuis :</value>
|
||||
</data>
|
||||
<data name="StopSnapcast" xml:space="preserve">
|
||||
<value>Stopper Snapcast</value>
|
||||
</data>
|
||||
<data name="Update_ButtonCheck" xml:space="preserve">
|
||||
<value>Vérifier les mises à jour</value>
|
||||
</data>
|
||||
<data name="Update_ButtonStart" xml:space="preserve">
|
||||
<value>Mettre à jour</value>
|
||||
</data>
|
||||
<data name="Update_Message1" xml:space="preserve">
|
||||
<value>Mise à jour disponible ! La nouvelle version est la</value>
|
||||
</data>
|
||||
<data name="Update_Message2" xml:space="preserve">
|
||||
<value>Installer maintenant ?</value>
|
||||
</data>
|
||||
<data name="Update_NoUpdate" xml:space="preserve">
|
||||
<value>Pas de mise à jour disponible.</value>
|
||||
</data>
|
||||
<data name="Update_String1" xml:space="preserve">
|
||||
<value>Nouvelle version</value>
|
||||
</data>
|
||||
<data name="Update_String2" xml:space="preserve">
|
||||
<value>disponible !</value>
|
||||
</data>
|
||||
</root>
|
453
Resources/Resources.resx
Normal file
@ -0,0 +1,453 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Clear_Queue" xml:space="preserve">
|
||||
<value>Clear queue</value>
|
||||
</data>
|
||||
<data name="Database" xml:space="preserve">
|
||||
<value>Database</value>
|
||||
</data>
|
||||
<data name="Day" xml:space="preserve">
|
||||
<value>day</value>
|
||||
</data>
|
||||
<data name="Days" xml:space="preserve">
|
||||
<value>days</value>
|
||||
</data>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
</data>
|
||||
<data name="FilterType_Album" xml:space="preserve">
|
||||
<value>Album</value>
|
||||
</data>
|
||||
<data name="FilterType_Artist" xml:space="preserve">
|
||||
<value>Artist</value>
|
||||
</data>
|
||||
<data name="FilterType_Directory" xml:space="preserve">
|
||||
<value>Directory</value>
|
||||
</data>
|
||||
<data name="FilterType_Genre" xml:space="preserve">
|
||||
<value>Genre</value>
|
||||
</data>
|
||||
<data name="FilterType_Song" xml:space="preserve">
|
||||
<value>Song</value>
|
||||
</data>
|
||||
<data name="FilterType_Year" xml:space="preserve">
|
||||
<value>Year</value>
|
||||
</data>
|
||||
<data name="Hour" xml:space="preserve">
|
||||
<value>hour</value>
|
||||
</data>
|
||||
<data name="Hours" xml:space="preserve">
|
||||
<value>hours</value>
|
||||
</data>
|
||||
<data name="Minute" xml:space="preserve">
|
||||
<value>minute</value>
|
||||
</data>
|
||||
<data name="Minutes" xml:space="preserve">
|
||||
<value>minutes</value>
|
||||
</data>
|
||||
<data name="Operator_Contains" xml:space="preserve">
|
||||
<value>contains</value>
|
||||
</data>
|
||||
<data name="Operator_Is" xml:space="preserve">
|
||||
<value>is</value>
|
||||
</data>
|
||||
<data name="Operator_IsNot" xml:space="preserve">
|
||||
<value>is not</value>
|
||||
</data>
|
||||
<data name="Play_Queue" xml:space="preserve">
|
||||
<value>Play queue</value>
|
||||
</data>
|
||||
<data name="Queue_Management" xml:space="preserve">
|
||||
<value>Queue management</value>
|
||||
</data>
|
||||
<data name="Radios" xml:space="preserve">
|
||||
<value>Radios</value>
|
||||
</data>
|
||||
<data name="Radio_Country" xml:space="preserve">
|
||||
<value>Country</value>
|
||||
</data>
|
||||
<data name="Radio_Loading" xml:space="preserve">
|
||||
<value>Loading stations...</value>
|
||||
</data>
|
||||
<data name="Radio_Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="Radio_NotFound" xml:space="preserve">
|
||||
<value>No stations found!</value>
|
||||
</data>
|
||||
<data name="Radio_Reset" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
</data>
|
||||
<data name="Radio_Search" xml:space="preserve">
|
||||
<value>Search</value>
|
||||
</data>
|
||||
<data name="Radio_SearchStation" xml:space="preserve">
|
||||
<value>Search station</value>
|
||||
</data>
|
||||
<data name="Radio_Tags" xml:space="preserve">
|
||||
<value>Tags</value>
|
||||
</data>
|
||||
<data name="Second" xml:space="preserve">
|
||||
<value>second</value>
|
||||
</data>
|
||||
<data name="Seconds" xml:space="preserve">
|
||||
<value>seconds</value>
|
||||
</data>
|
||||
<data name="Settings" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="Settings_About" xml:space="preserve">
|
||||
<value>About</value>
|
||||
</data>
|
||||
<data name="Settings_AboutInfo" xml:space="preserve">
|
||||
<value>unison is a free software. It is built with the following technologies:</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectButton" xml:space="preserve">
|
||||
<value>Connect</value>
|
||||
</data>
|
||||
<data name="Settings_Connection" xml:space="preserve">
|
||||
<value>Connection</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionPasswordInfo" xml:space="preserve">
|
||||
<value>Please note that since MPD passwords are not secure (they are sent in plain text to the server), they are saved as is in the setting file.</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatus" xml:space="preserve">
|
||||
<value>Status:</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
|
||||
<value>connected to MPD</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
|
||||
<value>connecting...</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
|
||||
<value>not connected.</value>
|
||||
</data>
|
||||
<data name="Settings_Host" xml:space="preserve">
|
||||
<value>Host</value>
|
||||
</data>
|
||||
<data name="Settings_License" xml:space="preserve">
|
||||
<value>License</value>
|
||||
</data>
|
||||
<data name="Settings_MadeBy" xml:space="preserve">
|
||||
<value>Made by</value>
|
||||
</data>
|
||||
<data name="Settings_NextTrack" xml:space="preserve">
|
||||
<value>Next track</value>
|
||||
</data>
|
||||
<data name="Settings_Password" xml:space="preserve">
|
||||
<value>Password</value>
|
||||
</data>
|
||||
<data name="Settings_PlayPause" xml:space="preserve">
|
||||
<value>Play / pause</value>
|
||||
</data>
|
||||
<data name="Settings_Port" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Settings_PreviousTrack" xml:space="preserve">
|
||||
<value>Previous track</value>
|
||||
</data>
|
||||
<data name="Settings_Shortcuts" xml:space="preserve">
|
||||
<value>Shortcuts</value>
|
||||
</data>
|
||||
<data name="Settings_ShortcutsInfo" xml:space="preserve">
|
||||
<value>Please note that if the input key is not recognized, this is due to a limitation on how virtual keys work.</value>
|
||||
</data>
|
||||
<data name="Settings_ShortcutsKey" xml:space="preserve">
|
||||
<value>Enter a key...</value>
|
||||
</data>
|
||||
<data name="Settings_ShowWindow" xml:space="preserve">
|
||||
<value>Show window</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle1" xml:space="preserve">
|
||||
<value>The shuffle window allows to add random songs to your queue. Both options take into account the filter.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle2" xml:space="preserve">
|
||||
<value>If the filter is empty, the entire music library is taken into account.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle3" xml:space="preserve">
|
||||
<value>The filter is queried each time you use the Add to queue or Continuous shuffle options.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle4" xml:space="preserve">
|
||||
<value>Add a fixed number of songs to the queue. For technical reasons, it is limited to 100 random songs without a filter, and to 1000 songs with a filter.</value>
|
||||
</data>
|
||||
<data name="Settings_Shuffle5" xml:space="preserve">
|
||||
<value>By enabling this option, unison will automatically add songs to the queue so you never run out of songs to listen to.</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo1" xml:space="preserve">
|
||||
<value>You can change to your own locally installed version of the Snapcast client with an</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo2" xml:space="preserve">
|
||||
<value> absolute </value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastInfo3" xml:space="preserve">
|
||||
<value>path.</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastLauch" xml:space="preserve">
|
||||
<value>Launch at startup</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastPath" xml:space="preserve">
|
||||
<value>Executable path</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastPort" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastResetButton" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
</data>
|
||||
<data name="Settings_SnapcastWindow" xml:space="preserve">
|
||||
<value>Show Snapcast window</value>
|
||||
</data>
|
||||
<data name="Settings_SourceCode1" xml:space="preserve">
|
||||
<value>Source code freely available</value>
|
||||
</data>
|
||||
<data name="Settings_SourceCode2" xml:space="preserve">
|
||||
<value>here</value>
|
||||
</data>
|
||||
<data name="Settings_UpdateDatabase" xml:space="preserve">
|
||||
<value>Update database</value>
|
||||
</data>
|
||||
<data name="Settings_Version" xml:space="preserve">
|
||||
<value>Version:</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeDown" xml:space="preserve">
|
||||
<value>Volume down</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeMute" xml:space="preserve">
|
||||
<value>Volume mute</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeOffset" xml:space="preserve">
|
||||
<value>Volume offset</value>
|
||||
</data>
|
||||
<data name="Settings_VolumeUp" xml:space="preserve">
|
||||
<value>Volume up</value>
|
||||
</data>
|
||||
<data name="ShowWindow" xml:space="preserve">
|
||||
<value>Show window</value>
|
||||
</data>
|
||||
<data name="Shuffle" xml:space="preserve">
|
||||
<value>Shuffle</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage1" xml:space="preserve">
|
||||
<value>Adding</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage2" xml:space="preserve">
|
||||
<value>songs...</value>
|
||||
</data>
|
||||
<data name="Shuffle_ButtonMessage3" xml:space="preserve">
|
||||
<value>done!</value>
|
||||
</data>
|
||||
<data name="Shuffle_Continuous" xml:space="preserve">
|
||||
<value>Continuous shuffle</value>
|
||||
</data>
|
||||
<data name="Shuffle_ContinuousEnable" xml:space="preserve">
|
||||
<value>Enable continuous shuffle</value>
|
||||
</data>
|
||||
<data name="Shuffle_Filter" xml:space="preserve">
|
||||
<value>Filter</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterQuery" xml:space="preserve">
|
||||
<value>Query filter</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterReset" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
</data>
|
||||
<data name="Shuffle_FilterSongNumber" xml:space="preserve">
|
||||
<value>Number of songs in filter:</value>
|
||||
</data>
|
||||
<data name="Shuffle_Querying1" xml:space="preserve">
|
||||
<value>Querying filter...</value>
|
||||
</data>
|
||||
<data name="Shuffle_Querying2" xml:space="preserve">
|
||||
<value>done!</value>
|
||||
</data>
|
||||
<data name="Shuffle_Queue" xml:space="preserve">
|
||||
<value>Add to queue</value>
|
||||
</data>
|
||||
<data name="Shuffle_QueueSongs" xml:space="preserve">
|
||||
<value>Songs to add</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup1" xml:space="preserve">
|
||||
<value>Snapcast error</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup2" xml:space="preserve">
|
||||
<value>Invalid path:</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup3" xml:space="preserve">
|
||||
<value>Current path:</value>
|
||||
</data>
|
||||
<data name="Snapcast_Popup4" xml:space="preserve">
|
||||
<value>You can reset it in the settings if needed.</value>
|
||||
</data>
|
||||
<data name="StartSnapcast" xml:space="preserve">
|
||||
<value>Start Snapcast</value>
|
||||
</data>
|
||||
<data name="Stats_Albums" xml:space="preserve">
|
||||
<value>Albums:</value>
|
||||
</data>
|
||||
<data name="Stats_Artists" xml:space="preserve">
|
||||
<value>Artists:</value>
|
||||
</data>
|
||||
<data name="Stats_LastDatabaseUpdate" xml:space="preserve">
|
||||
<value>Last database update:</value>
|
||||
</data>
|
||||
<data name="Stats_Songs" xml:space="preserve">
|
||||
<value>Songs:</value>
|
||||
</data>
|
||||
<data name="Stats_TotalPlaytime" xml:space="preserve">
|
||||
<value>Total playtime:</value>
|
||||
</data>
|
||||
<data name="Stats_TotalTimePlayed" xml:space="preserve">
|
||||
<value>Total time played:</value>
|
||||
</data>
|
||||
<data name="Stats_UpdateDBMessage1" xml:space="preserve">
|
||||
<value>Updating database...</value>
|
||||
</data>
|
||||
<data name="Stats_UpdateDBMessage2" xml:space="preserve">
|
||||
<value>done!</value>
|
||||
</data>
|
||||
<data name="Stats_Uptime" xml:space="preserve">
|
||||
<value>MPD uptime:</value>
|
||||
</data>
|
||||
<data name="StopSnapcast" xml:space="preserve">
|
||||
<value>Stop Snapcast</value>
|
||||
</data>
|
||||
<data name="Update_ButtonCheck" xml:space="preserve">
|
||||
<value>Check for updates</value>
|
||||
</data>
|
||||
<data name="Update_ButtonStart" xml:space="preserve">
|
||||
<value>Start update</value>
|
||||
</data>
|
||||
<data name="Update_Message1" xml:space="preserve">
|
||||
<value>Update available! New version is</value>
|
||||
</data>
|
||||
<data name="Update_Message2" xml:space="preserve">
|
||||
<value>Install now?</value>
|
||||
</data>
|
||||
<data name="Update_NoUpdate" xml:space="preserve">
|
||||
<value>No update available.</value>
|
||||
</data>
|
||||
<data name="Update_String1" xml:space="preserve">
|
||||
<value>New version</value>
|
||||
</data>
|
||||
<data name="Update_String2" xml:space="preserve">
|
||||
<value>available!</value>
|
||||
</data>
|
||||
</root>
|
28
Resources/Resources.xaml
Normal file
@ -0,0 +1,28 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:sys="clr-namespace:System;assembly=System.Runtime">
|
||||
|
||||
<system:String x:Key="snapcastPath">snapcast_0.31</system:String>
|
||||
<system:String x:Key="snapcastPort">1704</system:String>
|
||||
|
||||
<system:String x:Key="playButton"></system:String>
|
||||
<system:String x:Key="pauseButton"></system:String>
|
||||
|
||||
<system:String x:Key="volume_offset">5</system:String>
|
||||
|
||||
<system:UInt32 x:Key="nextTrack_mod">2</system:UInt32>
|
||||
<system:UInt32 x:Key="nextTrack_vk">176</system:UInt32>
|
||||
<system:UInt32 x:Key="previousTrack_mod">2</system:UInt32>
|
||||
<system:UInt32 x:Key="previousTrack_vk">177</system:UInt32>
|
||||
<system:UInt32 x:Key="playPause_mod">2</system:UInt32>
|
||||
<system:UInt32 x:Key="playPause_vk">179</system:UInt32>
|
||||
<system:UInt32 x:Key="volumeUp_mod">2</system:UInt32>
|
||||
<system:UInt32 x:Key="volumeUp_vk">175</system:UInt32>
|
||||
<system:UInt32 x:Key="volumeDown_mod">2</system:UInt32>
|
||||
<system:UInt32 x:Key="volumeDown_vk">174</system:UInt32>
|
||||
<system:UInt32 x:Key="volumeMute_mod">2</system:UInt32>
|
||||
<system:UInt32 x:Key="volumeMute_vk">173</system:UInt32>
|
||||
<system:UInt32 x:Key="showWindow_mod">3</system:UInt32>
|
||||
<system:UInt32 x:Key="showWindow_vk">13</system:UInt32>
|
||||
|
||||
</ResourceDictionary>
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 294 KiB |
BIN
Resources/nocover.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Resources/nothing.png
Normal file
After Width: | Height: | Size: 369 B |
BIN
Resources/radio.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Screenshots/screen1.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
Screenshots/screen2.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
Screenshots/screen3.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
Screenshots/screen4.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
Screenshots/screen5.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
@ -4,13 +4,20 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
|
||||
xmlns:clr="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:local="clr-namespace:unison"
|
||||
xmlns:properties="clr-namespace:unison.Resources"
|
||||
mc:Ignorable="d"
|
||||
Title="unison"
|
||||
Closing="Window_Closing" Icon="/images/icon-full.ico" ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
|
||||
Title="unison" Closing="Window_Closing" LocationChanged="Window_LocationChanged" Icon="/Resources/icon-full.ico" ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
|
||||
|
||||
<Grid Background="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}" MinHeight="270">
|
||||
<Window.Resources>
|
||||
<Style TargetType="Border" x:Key="UnselectedButton">
|
||||
<Setter Property="BorderThickness" Value="0, 0, 0, 0"/>
|
||||
</Style>
|
||||
<Style TargetType="Border" x:Key="SelectedButton">
|
||||
<Setter Property="BorderThickness" Value="0, 0, 0, 2"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Background="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}" MinHeight="270" MinWidth="700" Margin="0,10,0,0">
|
||||
<Grid x:Name="TopLayout" Margin="10,0,10,0" VerticalAlignment="Stretch" Width="Auto" Height="Auto">
|
||||
<Grid x:Name="Display" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="225,0,0,0" Height="Auto" Width="Auto">
|
||||
<GroupBox Height="220" VerticalAlignment="Center">
|
||||
@ -21,14 +28,14 @@
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
<Grid x:Name="CurrentSong" Margin="10,0,10,0" VerticalAlignment="Top" MinHeight="80">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" MouseDown="MouseDownClipboard">
|
||||
<TextBlock x:Name="SongTitle" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" FontSize="20" Text="Title"/>
|
||||
<TextBlock x:Name="SongArtist" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Bold" FontSize="18" Text="Artist"/>
|
||||
<TextBlock x:Name="SongAlbum" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" FontSize="16" Text="Album"/>
|
||||
<TextBlock TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="0,2,0,0">
|
||||
<TextBlock x:Name="SongInfo" TextWrapping="Wrap" TextAlignment="Center" FontWeight="Normal" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="0,2,0,0">
|
||||
<Run x:Name="SongGenre"/>
|
||||
<Run> – </Run>
|
||||
<Run x:Name="Format"/>
|
||||
<Run x:Name="SongInfoDash"> – </Run>
|
||||
<Run x:Name="SongFormat"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@ -54,30 +61,30 @@
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<Grid VerticalAlignment="Stretch" Margin="0,18,0,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,2,0,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="10,2,10,0">
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="16"/>
|
||||
<Slider x:Name="VolumeSlider" Maximum="100" Value="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" MinWidth="180" FlowDirection="LeftToRight" PreviewMouseUp="VolumeSlider_DragCompleted" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="7.5,0,0,0" FontSize="16"/>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="27">
|
||||
<Border x:Name="BorderRandom" BorderThickness="0,0,0,0" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
|
||||
<Border x:Name="BorderRandom" Style="{StaticResource UnselectedButton}" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
|
||||
<Button x:Name="Random" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Background="{x:Null}" BorderBrush="{x:Null}" Click="Random_Clicked" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="" Margin="0" Padding="0, 0, 0, 2"/>
|
||||
</Button>
|
||||
</Border>
|
||||
<Border x:Name="BorderRepeat" BorderThickness="0, 0, 0, 0" BorderBrush="Black" VerticalAlignment="Top" Margin="10,0,0,0">
|
||||
<Border x:Name="BorderRepeat" Style="{StaticResource UnselectedButton}" BorderBrush="Black" VerticalAlignment="Top" Margin="10,0,0,0">
|
||||
<Button x:Name="Repeat" Background="{x:Null}" FontSize="18" BorderBrush="{x:Null}" Click="Repeat_Clicked" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="" Padding="0, 0, 0, 2"/>
|
||||
</Button>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="27">
|
||||
<Border x:Name="BorderSingle" BorderThickness="0,0,0,0" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True" Margin="0,0,10,0">
|
||||
<Border x:Name="BorderSingle" Style="{StaticResource UnselectedButton}" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True" Margin="0,0,10,0">
|
||||
<Button x:Name="Single" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="{x:Null}" Background="{x:Null}" Click="Single_Clicked" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="" Margin="0" Padding="0, 0, 0, 2"/>
|
||||
</Button>
|
||||
</Border>
|
||||
<Border x:Name="BorderConsume" BorderThickness="0,0,0,0" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
|
||||
<Border x:Name="BorderConsume" Style="{StaticResource UnselectedButton}" BorderBrush="Black" HorizontalAlignment="Stretch" VerticalAlignment="Top" SnapsToDevicePixels="True">
|
||||
<Button x:Name="Consume" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="{x:Null}" Background="{x:Null}" Click="Consume_Clicked" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="" Margin="0" Padding="0, 0, 0, 2"/>
|
||||
</Button>
|
||||
@ -96,31 +103,51 @@
|
||||
<StackPanel.OpacityMask>
|
||||
<VisualBrush Visual="{Binding ElementName=mask}"/>
|
||||
</StackPanel.OpacityMask>
|
||||
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/images/nocover.png" Visibility="Collapsed" />
|
||||
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/images/nocover.png" />
|
||||
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
|
||||
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
|
||||
<Image x:Name="RadioCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/radio.png" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40">
|
||||
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="10,0,0,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
|
||||
<TextBlock x:Name="SnapcastText" Text="Start Snapcast" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Center" TextAlignment="Center" MinWidth="350" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}"/>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
|
||||
<!--<Button x:Name="Shuffle" Padding="5, 2" HorizontalAlignment="Right" Margin="0,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0">
|
||||
<Button x:Name="Shuffle" Padding="5, 2" Click="Shuffle_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" Margin="0,0,10,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
|
||||
<TextBlock Text="🔁" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Shuffle}" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>-->
|
||||
</Button>
|
||||
|
||||
<Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="📻" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Radios}" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Grid x:Name="ConnectionOkIcon" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid x:Name="ConnectionFailIcon" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="5,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
|
||||
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="0,0,10,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="🔊" Padding="0,0,0,2"/>
|
||||
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="Settings" Margin="5, 0, 0, 0"/>
|
||||
<TextBlock Text="🛠️" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings}" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
@ -6,180 +6,310 @@ using System.Windows.Threading;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Diagnostics;
|
||||
using System.Data;
|
||||
using MpcNET.Commands.Playback;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class MainWindow : Window, INotifyPropertyChanged
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public readonly Settings SettingsWindow = new Settings();
|
||||
private readonly Settings _settingsWin;
|
||||
private readonly Radios _radiosWin;
|
||||
private readonly Shuffle _shuffleWin;
|
||||
|
||||
private readonly MPDHandler mpd;
|
||||
public Settings GetSettings() => _settingsWin;
|
||||
public Radios GetRadios() => _radiosWin;
|
||||
public Shuffle GetShuffle() => _shuffleWin;
|
||||
|
||||
DispatcherTimer timer = new DispatcherTimer();
|
||||
|
||||
Thickness SelectedThickness;
|
||||
Thickness BaseThickness;
|
||||
private readonly DispatcherTimer _timer;
|
||||
private readonly MPDHandler _mpd;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitHwnd();
|
||||
InitializeComponent();
|
||||
DefaultState(true);
|
||||
|
||||
WindowState = WindowState.Minimized;
|
||||
|
||||
mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
Top = Properties.Settings.Default.MainWindowTop;
|
||||
Left = Properties.Settings.Default.MainWindowLeft;
|
||||
|
||||
timer = new DispatcherTimer();
|
||||
timer.Interval = TimeSpan.FromSeconds(0.5);
|
||||
timer.Tick += Timer_Tick;
|
||||
timer.Start();
|
||||
_settingsWin = new Settings();
|
||||
_radiosWin = new Radios();
|
||||
_shuffleWin = new Shuffle();
|
||||
_timer = new DispatcherTimer();
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
|
||||
SelectedThickness.Left = SelectedThickness.Right = SelectedThickness.Top = 0.0f;
|
||||
SelectedThickness.Bottom = 2.0f;
|
||||
BaseThickness.Left = BaseThickness.Right = BaseThickness.Top = BaseThickness.Bottom = 0.0f;
|
||||
_timer.Interval = TimeSpan.FromSeconds(0.5);
|
||||
_timer.Tick += Timer_Tick;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void Timer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
if (mpd.GetCurrentSong() == null)
|
||||
if (_mpd.GetCurrentSong() == null)
|
||||
return;
|
||||
|
||||
CurrentTime.Text = FormatSeconds(mpd._currentTime);
|
||||
TimeSlider.Value = mpd._currentTime / mpd.GetCurrentSong().Time * 100;
|
||||
CurrentTime.Text = FormatSeconds(_mpd.GetCurrentTime());
|
||||
TimeSlider.Value = _mpd.GetCurrentTime() / _mpd.GetCurrentSong().Time * 100;
|
||||
}
|
||||
|
||||
public void UpdateButton(ref Border border, bool b)
|
||||
public void UpdateStats()
|
||||
{
|
||||
if (b)
|
||||
border.BorderThickness = SelectedThickness;
|
||||
else
|
||||
border.BorderThickness = BaseThickness;
|
||||
}
|
||||
|
||||
public string FormatSeconds(int time)
|
||||
{
|
||||
TimeSpan timespan = TimeSpan.FromSeconds(time);
|
||||
return timespan.ToString(@"mm\:ss");
|
||||
}
|
||||
public string FormatSeconds(double time)
|
||||
{
|
||||
TimeSpan timespan = TimeSpan.FromSeconds(time);
|
||||
return timespan.ToString(@"mm\:ss");
|
||||
_mpd.QueryStats();
|
||||
_settingsWin.UpdateStats();
|
||||
}
|
||||
|
||||
public void OnConnectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
Connection.Text = (mpd._connected ? "✔️" : "❌") + $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
|
||||
SettingsWindow.UpdateConnectionStatus();
|
||||
if (mpd._connected)
|
||||
if (_mpd.IsConnected())
|
||||
{
|
||||
UpdateStats();
|
||||
|
||||
ConnectionOkIcon.Visibility = Visibility.Visible;
|
||||
ConnectionFailIcon.Visibility = Visibility.Collapsed;
|
||||
|
||||
Snapcast.IsEnabled = true;
|
||||
if (_radiosWin.IsConnected())
|
||||
Radio.IsEnabled = true;
|
||||
|
||||
_shuffleWin.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
_timer.Stop();
|
||||
DefaultState(true);
|
||||
ConnectionOkIcon.Visibility = Visibility.Collapsed;
|
||||
ConnectionFailIcon.Visibility = Visibility.Visible;
|
||||
|
||||
Snapcast.IsEnabled = false;
|
||||
Radio.IsEnabled = false;
|
||||
}
|
||||
_settingsWin.UpdateConnectionStatus();
|
||||
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
|
||||
}
|
||||
|
||||
public void OnSongChanged(object sender, EventArgs e)
|
||||
public async void OnSongChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (mpd.GetCurrentSong() == null)
|
||||
if (_mpd.GetCurrentSong() == null)
|
||||
return;
|
||||
|
||||
if (!mpd.GetCurrentSong().HasName)
|
||||
SongTitle.Text = mpd.GetCurrentSong().Title;
|
||||
if (_mpd.GetCurrentSong().HasTitle && _mpd.GetCurrentSong().Title.Length > 0)
|
||||
SongTitle.Text = _mpd.GetCurrentSong().Title;
|
||||
else if (_mpd.GetCurrentSong().HasName && _mpd.GetCurrentSong().Name.Length > 0)
|
||||
SongTitle.Text = _mpd.GetCurrentSong().Name;
|
||||
else if (_mpd.GetCurrentSong().Path != null)
|
||||
{
|
||||
int start = _mpd.GetCurrentSong().Path.LastIndexOf("/") + 1;
|
||||
int end = _mpd.GetCurrentSong().Path.LastIndexOf(".");
|
||||
if (start > 0 && end > 0 && end > start)
|
||||
SongTitle.Text = _mpd.GetCurrentSong().Path.Substring(start, end - start);
|
||||
}
|
||||
|
||||
SongTitle.ToolTip = _mpd.GetCurrentSong().Path;
|
||||
SongArtist.Text = _mpd.GetCurrentSong().Artist;
|
||||
SongAlbum.Text = _mpd.GetCurrentSong().Album;
|
||||
|
||||
if (_mpd.GetCurrentSong().Date != null)
|
||||
SongAlbum.Text += $" ({_mpd.GetCurrentSong().Date.Split("-")[0]})";
|
||||
|
||||
SongGenre.Text = _mpd.GetCurrentSong().Genre;
|
||||
SongFormat.Text = _mpd.GetCurrentSong().Path.Substring(_mpd.GetCurrentSong().Path.LastIndexOf(".") + 1);
|
||||
if (SongGenre.Text.Length == 0 || SongFormat.Text.Length == 0)
|
||||
SongInfoDash.Text = "";
|
||||
else
|
||||
SongTitle.Text = mpd.GetCurrentSong().Title;
|
||||
SongTitle.ToolTip = mpd.GetCurrentSong().Path;
|
||||
SongArtist.Text = mpd.GetCurrentSong().Artist;
|
||||
SongAlbum.Text = mpd.GetCurrentSong().Album;
|
||||
SongGenre.Text = mpd.GetCurrentSong().Genre;
|
||||
SongInfoDash.Text = " – ";
|
||||
|
||||
if (mpd.GetCurrentSong().Date != null)
|
||||
SongAlbum.Text += $" ({ mpd.GetCurrentSong().Date})";
|
||||
Format.Text = mpd.GetCurrentSong().Path.Substring(mpd.GetCurrentSong().Path.LastIndexOf(".") + 1);
|
||||
TimeSlider.IsEnabled = true;
|
||||
if (_mpd.GetCurrentSong().Time == -1)
|
||||
{
|
||||
CurrentTime.Text = "";
|
||||
EndTime.Text = "";
|
||||
_timer.Stop();
|
||||
TimeSlider.Value = 50;
|
||||
TimeSlider.IsEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_timer.IsEnabled)
|
||||
_timer.Start();
|
||||
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
|
||||
}
|
||||
|
||||
EndTime.Text = FormatSeconds(mpd.GetCurrentSong().Time);
|
||||
Trace.WriteLine("Song changed called!");
|
||||
|
||||
if (_shuffleWin.GetContinuous())
|
||||
{
|
||||
// force consume: there's no other way to be sure
|
||||
// that we don't get to the end of the queue with nothing to play
|
||||
_mpd.SendCommand(new ConsumeCommand(true));
|
||||
|
||||
_mpd.CanPrevNext = false;
|
||||
await _shuffleWin.HandleContinuous();
|
||||
_mpd.CanPrevNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStatusChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (mpd.GetStatus() == null)
|
||||
if (_mpd.GetStatus() == null)
|
||||
return;
|
||||
|
||||
if (VolumeSlider.Value != mpd._currentVolume)
|
||||
if (VolumeSlider.Value != _mpd.GetStatus().Volume)
|
||||
{
|
||||
VolumeSlider.Value = mpd._currentVolume;
|
||||
VolumeSlider.ToolTip = mpd._currentVolume;
|
||||
VolumeSlider.Value = _mpd.GetStatus().Volume;
|
||||
VolumeSlider.ToolTip = _mpd.GetStatus().Volume;
|
||||
}
|
||||
|
||||
if (mpd.IsPlaying())
|
||||
PlayPause.Text = "\xedb4";
|
||||
else
|
||||
PlayPause.Text = "\xedb5";
|
||||
UpdateButton(ref BorderRandom, _mpd.GetStatus().Random);
|
||||
UpdateButton(ref BorderRepeat, _mpd.GetStatus().Repeat);
|
||||
UpdateButton(ref BorderSingle, _mpd.GetStatus().Single);
|
||||
UpdateButton(ref BorderConsume, _mpd.GetStatus().Consume);
|
||||
|
||||
UpdateButton(ref BorderRandom, mpd._currentRandom);
|
||||
UpdateButton(ref BorderRepeat, mpd._currentRepeat);
|
||||
UpdateButton(ref BorderSingle, mpd._currentSingle);
|
||||
UpdateButton(ref BorderConsume, mpd._currentConsume);
|
||||
if (_mpd.IsPlaying())
|
||||
PlayPause.Text = (string)Application.Current.FindResource("playButton");
|
||||
else
|
||||
{
|
||||
PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
|
||||
if (_mpd.GetStatus().State == MpcNET.MpdState.Stop)
|
||||
{
|
||||
DefaultState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DefaultState(bool LostConnection = false)
|
||||
{
|
||||
SongTitle.Text = "";
|
||||
SongArtist.Text = "";
|
||||
SongAlbum.Text = "";
|
||||
SongGenre.Text = "";
|
||||
SongInfoDash.Text = "";
|
||||
SongFormat.Text = "";
|
||||
CurrentTime.Text = "";
|
||||
EndTime.Text = "";
|
||||
PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
|
||||
TimeSlider.Value = 50;
|
||||
TimeSlider.IsEnabled = false;
|
||||
NoCover.Visibility = Visibility.Collapsed;
|
||||
Cover.Visibility = Visibility.Collapsed;
|
||||
RadioCover.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (LostConnection)
|
||||
{
|
||||
ConnectionOkIcon.Visibility = Visibility.Collapsed;
|
||||
ConnectionFailIcon.Visibility = Visibility.Visible;
|
||||
}
|
||||
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
|
||||
}
|
||||
|
||||
public void OnCoverChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (mpd.GetCover() == null)
|
||||
{
|
||||
NoCover.Visibility = Visibility.Collapsed;
|
||||
Cover.Visibility = Visibility.Collapsed;
|
||||
RadioCover.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (_mpd.GetCurrentSong().Time == -1)
|
||||
RadioCover.Visibility = Visibility.Visible;
|
||||
else if (_mpd.GetCover() == null)
|
||||
NoCover.Visibility = Visibility.Visible;
|
||||
Cover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else if (Cover.Source != mpd.GetCover())
|
||||
else if (Cover.Source != _mpd.GetCover())
|
||||
{
|
||||
Cover.Source = mpd.GetCover();
|
||||
Cover.Source = _mpd.GetCover();
|
||||
Cover.Visibility = Visibility.Visible;
|
||||
NoCover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSnapcastChanged()
|
||||
{
|
||||
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
|
||||
if (snapcast.Started)
|
||||
SnapcastText.Text = "Stop Snapcast";
|
||||
if (snapcast.HasStarted)
|
||||
SnapcastText.Text = unison.Resources.Resources.StopSnapcast;
|
||||
else
|
||||
SnapcastText.Text = "Start Snapcast";
|
||||
SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
|
||||
}
|
||||
|
||||
public void Pause_Clicked(object sender, RoutedEventArgs e) => mpd.PlayPause();
|
||||
public void Previous_Clicked(object sender, RoutedEventArgs e) => mpd.Prev();
|
||||
public void Next_Clicked(object sender, RoutedEventArgs e) => mpd.Next();
|
||||
public void UpdateButton(ref Border border, bool b)
|
||||
{
|
||||
border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
|
||||
}
|
||||
|
||||
public void Random_Clicked(object sender, RoutedEventArgs e) => mpd.Random();
|
||||
public void Repeat_Clicked(object sender, RoutedEventArgs e) => mpd.Repeat();
|
||||
public void Single_Clicked(object sender, RoutedEventArgs e) => mpd.Single();
|
||||
public void Consume_Clicked(object sender, RoutedEventArgs e) => mpd.Consume();
|
||||
public void ChangeVolume(int value) => mpd.SetVolume(value);
|
||||
public static string FormatSeconds(int time)
|
||||
{
|
||||
TimeSpan timespan = TimeSpan.FromSeconds(time);
|
||||
return timespan.ToString(@"mm\:ss");
|
||||
}
|
||||
public static string FormatSeconds(double time)
|
||||
{
|
||||
TimeSpan timespan = TimeSpan.FromSeconds(time);
|
||||
return timespan.ToString(@"mm\:ss");
|
||||
}
|
||||
|
||||
public void Pause_Clicked(object sender, RoutedEventArgs e) => _mpd.PlayPause();
|
||||
public void Previous_Clicked(object sender, RoutedEventArgs e) => _mpd.Prev();
|
||||
public void Next_Clicked(object sender, RoutedEventArgs e) => _mpd.Next();
|
||||
public void Random_Clicked(object sender, RoutedEventArgs e) => _mpd.Random();
|
||||
public void Repeat_Clicked(object sender, RoutedEventArgs e) => _mpd.Repeat();
|
||||
public void Single_Clicked(object sender, RoutedEventArgs e) => _mpd.Single();
|
||||
public void Consume_Clicked(object sender, RoutedEventArgs e) => _mpd.Consume();
|
||||
public void ChangeVolume(int value) => _mpd.SetVolume(value);
|
||||
|
||||
public void Snapcast_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
|
||||
if (!snapcast.Started)
|
||||
snapcast.Start();
|
||||
else
|
||||
snapcast.Stop();
|
||||
snapcast.LaunchOrExit();
|
||||
}
|
||||
|
||||
public void Radios_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_radiosWin.WindowState == WindowState.Minimized)
|
||||
{
|
||||
_radiosWin.Show();
|
||||
_radiosWin.Activate();
|
||||
_radiosWin.WindowState = WindowState.Normal;
|
||||
}
|
||||
else if (_radiosWin.WindowState == WindowState.Normal)
|
||||
{
|
||||
_radiosWin.Hide();
|
||||
_radiosWin.WindowState = WindowState.Minimized;
|
||||
}
|
||||
}
|
||||
|
||||
public void Shuffle_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_shuffleWin.WindowState == WindowState.Minimized)
|
||||
{
|
||||
_shuffleWin.Show();
|
||||
_shuffleWin.Activate();
|
||||
_shuffleWin.WindowState = WindowState.Normal;
|
||||
}
|
||||
else if (_shuffleWin.WindowState == WindowState.Normal)
|
||||
{
|
||||
_shuffleWin.Hide();
|
||||
_shuffleWin.WindowState = WindowState.Minimized;
|
||||
}
|
||||
}
|
||||
|
||||
public void Settings_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SettingsWindow.Show();
|
||||
SettingsWindow.Activate();
|
||||
|
||||
if (SettingsWindow.WindowState == WindowState.Minimized)
|
||||
SettingsWindow.WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
Hide();
|
||||
if (_settingsWin.WindowState == WindowState.Minimized)
|
||||
{
|
||||
_settingsWin.Show();
|
||||
_settingsWin.Activate();
|
||||
_settingsWin.WindowState = WindowState.Normal;
|
||||
}
|
||||
else if (_settingsWin.WindowState == WindowState.Normal)
|
||||
{
|
||||
_settingsWin.Hide();
|
||||
_settingsWin.WindowState = WindowState.Minimized;
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeSlider_DragStarted(object sender, DragStartedEventArgs e)
|
||||
{
|
||||
timer.Stop();
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
private void TimeSlider_DragCompleted(object sender, MouseButtonEventArgs e)
|
||||
@ -187,18 +317,23 @@ namespace unison
|
||||
Slider slider = (Slider)sender;
|
||||
|
||||
double SongPercentage = slider.Value;
|
||||
double SongTime = mpd._totalTime;
|
||||
double SongTime = _mpd.GetCurrentSong().Time;
|
||||
double SeekTime = SongPercentage / 100 * SongTime;
|
||||
|
||||
mpd.SetTime(SeekTime);
|
||||
timer.Start();
|
||||
_mpd.SetTime(SeekTime);
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void VolumeSlider_DragCompleted(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Slider slider = (Slider)sender;
|
||||
mpd.SetVolume((int)slider.Value);
|
||||
slider.ToolTip = mpd._currentVolume;
|
||||
_mpd.SetVolume((int)slider.Value);
|
||||
slider.ToolTip = (int)slider.Value;
|
||||
}
|
||||
|
||||
public void UpdateUpdateStatus(string version)
|
||||
{
|
||||
_settingsWin.UpdateUpdateStatus(version);
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
@ -208,17 +343,35 @@ namespace unison
|
||||
hk.Activate(this);
|
||||
}
|
||||
|
||||
private void MouseDownClipboard(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
string CopyText = SongTitle.Text + " - " + SongArtist.Text + "\n";
|
||||
CopyText += SongAlbum.Text + "\n";
|
||||
CopyText += SongTitle.ToolTip;
|
||||
Clipboard.SetDataObject(CopyText);
|
||||
}
|
||||
}
|
||||
|
||||
public void InitHwnd()
|
||||
{
|
||||
WindowInteropHelper helper = new(this);
|
||||
helper.EnsureHandle();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged(string propertyName)
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
e.Cancel = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
Properties.Settings.Default.MainWindowTop = Top;
|
||||
Properties.Settings.Default.MainWindowLeft = Left;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
}
|
74
Views/Radios.xaml
Normal file
@ -0,0 +1,74 @@
|
||||
<Window x:Class="unison.Radios"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
|
||||
xmlns:properties="clr-namespace:unison.Resources"
|
||||
mc:Ignorable="d"
|
||||
Title="Radios" Closing="Window_Closing" LocationChanged="Window_LocationChanged" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
|
||||
|
||||
<Grid Margin="0,5,0,0">
|
||||
<StackPanel>
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="5,0,5,0">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<Run Text="📻"/>
|
||||
<Run Text="{x:Static properties:Resources.Radio_SearchStation}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical" Margin="5,0,5,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="{x:Static properties:Resources.Radio_Name}" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="NameSearch" KeyDown="SearchHandler" Width="200" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Radio_Tags}" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="TagSearch" KeyDown="SearchHandler" Width="300" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Radio_Country}" Margin="0,0,0,5"/>
|
||||
<ComboBox x:Name="CountryList" SelectedIndex="0" KeyDown="SearchHandler" Width="240" ScrollViewer.CanContentScroll="False"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Button x:Name="SearchButton" Content="{x:Static properties:Resources.Radio_Search}" Click="Search_Clicked" Padding="5, 2"/>
|
||||
<Button Content="{x:Static properties:Resources.Radio_Reset}" Click="Reset_Clicked" Margin="10,0,0,0" Padding="5, 2"/>
|
||||
<TextBlock x:Name="SearchStatus" Margin="15,1,0,0" FontStyle="Italic" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Margin="5,10,5,5" MaxHeight="600" MinWidth="800" MaxWidth="800">
|
||||
<Grid.Resources>
|
||||
<DataTemplate x:Key="CountryTemplate">
|
||||
<emoji:TextBlock TextAlignment="Center" Text="{Binding Country}"/>
|
||||
</DataTemplate>
|
||||
</Grid.Resources>
|
||||
<DataGrid Name="RadioListGrid" MouseDoubleClick="Row_DoubleClick" CanUserAddRows="False" CanUserDeleteRows="False"
|
||||
CanUserReorderColumns="False" CanUserResizeRows="False" IsReadOnly="True" SelectionMode="Single"
|
||||
HeadersVisibility="Column" GridLinesVisibility="None" VirtualizingPanel.ScrollUnit="Pixel">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Header="🏳️" CellTemplate="{StaticResource CountryTemplate}" MinWidth="25" />
|
||||
<DataGridTextColumn Header="{x:Static properties:Resources.Radio_Name}" Binding="{Binding Name}" MinWidth="50"/>
|
||||
<DataGridTextColumn Header="Codec" Binding="{Binding Codec}" MinWidth="47"/>
|
||||
<DataGridTextColumn Header="Bitrate" Binding="{Binding Bitrate}" MinWidth="47"/>
|
||||
<DataGridTextColumn Header="Tags" Binding="{Binding Tags}" MinWidth="50"/>
|
||||
</DataGrid.Columns>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
186
Views/Radios.xaml.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Collections.Generic;
|
||||
using unison.Handlers;
|
||||
using RadioBrowser.Models;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class Radios : Window
|
||||
{
|
||||
private MPDHandler _mpd;
|
||||
private RadioHandler _radio;
|
||||
|
||||
public bool IsConnected() => _radio.IsConnected();
|
||||
|
||||
public Radios()
|
||||
{
|
||||
InitializeComponent();
|
||||
Initialize();
|
||||
InitHwnd();
|
||||
|
||||
WindowState = WindowState.Minimized;
|
||||
|
||||
Top = Properties.Settings.Default.RadiosWindowTop;
|
||||
Left = Properties.Settings.Default.RadiosWindowLeft;
|
||||
}
|
||||
|
||||
public async void Initialize()
|
||||
{
|
||||
_radio = new RadioHandler();
|
||||
|
||||
if (!_radio.IsConnected())
|
||||
SearchButton.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
CountryList.Items.Add(new CountryListItem { Name = "", Count = 0 });
|
||||
|
||||
List<NameAndCount> Countries = await _radio.GetCountries();
|
||||
foreach (NameAndCount Country in Countries)
|
||||
{
|
||||
CountryList.Items.Add(new CountryListItem
|
||||
{
|
||||
Name = Country.Name,
|
||||
Count = Country.Stationcount
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine("Exception while getting countries in RadioBrowser: " + e.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CleanString(string str)
|
||||
{
|
||||
return str.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");
|
||||
}
|
||||
|
||||
private void SearchHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
Search_Clicked(null, null);
|
||||
}
|
||||
|
||||
private async void Search_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
CountryListItem a = (CountryListItem)CountryList.SelectedItem;
|
||||
await SearchAdvanced(NameSearch.Text, a?.Name, TagSearch.Text);
|
||||
}
|
||||
catch (Exception except)
|
||||
{
|
||||
Trace.WriteLine("Error on RadioBrowser search: " + except.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SearchAdvanced(string name, string country, string tags)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchStatus.Text = unison.Resources.Resources.Radio_Loading;
|
||||
|
||||
List<StationInfo> advancedSearch = await Task.Run(async () =>
|
||||
{
|
||||
return await _radio.AdvancedSearch(new AdvancedSearchOptions
|
||||
{
|
||||
Name = name,
|
||||
Country = country,
|
||||
TagList = tags
|
||||
});
|
||||
});
|
||||
|
||||
RadioListGrid.Items.Clear();
|
||||
if (advancedSearch.Count > 0)
|
||||
{
|
||||
SearchStatus.Text = "";
|
||||
foreach (StationInfo station in advancedSearch)
|
||||
{
|
||||
RadioListGrid.Items.Add(new StationListItem
|
||||
{
|
||||
Name = CleanString(station.Name),
|
||||
Country = station.CountryCode,
|
||||
Codec = station.Codec,
|
||||
Bitrate = station.Bitrate,
|
||||
Url = station.Url,
|
||||
Tags = string.Join(", ", station.Tags)
|
||||
});
|
||||
}
|
||||
FitToContent();
|
||||
}
|
||||
else
|
||||
SearchStatus.Text = unison.Resources.Resources.Radio_NotFound;
|
||||
}
|
||||
catch (Exception except)
|
||||
{
|
||||
Trace.WriteLine("Error on RadioBrowser search advanced: " + except.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void FitToContent()
|
||||
{
|
||||
foreach (DataGridColumn column in RadioListGrid.Columns)
|
||||
column.Width = new DataGridLength(1.0, DataGridLengthUnitType.SizeToCells);
|
||||
}
|
||||
|
||||
private void Row_DoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
DataGrid grid = sender as DataGrid;
|
||||
StationListItem station;
|
||||
try
|
||||
{
|
||||
station = grid.Items[grid.SelectedIndex] as StationListItem;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
Trace.WriteLine("Error: Invalid index.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (station.Url == null)
|
||||
{
|
||||
Trace.WriteLine("Error: Invalid station.");
|
||||
return;
|
||||
}
|
||||
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
_mpd.ClearAddAndPlay(station.Url.AbsoluteUri);
|
||||
}
|
||||
|
||||
private void Reset_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
NameSearch.Text = "";
|
||||
TagSearch.Text = "";
|
||||
CountryList.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void InitHwnd()
|
||||
{
|
||||
WindowInteropHelper helper = new(this);
|
||||
helper.EnsureHandle();
|
||||
}
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
Properties.Settings.Default.RadiosWindowTop = Top;
|
||||
Properties.Settings.Default.RadiosWindowLeft = Left;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,92 +4,78 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
|
||||
xmlns:local="clr-namespace:unison"
|
||||
xmlns:properties="clr-namespace:unison.Resources" xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||
mc:Ignorable="d"
|
||||
Closing="Window_Closing" Title="Settings" ResizeMode="CanMinimize" Icon="/images/icon-full.ico" WindowStyle="ToolWindow" SizeToContent="WidthAndHeight">
|
||||
<Grid>
|
||||
Title="{x:Static properties:Resources.Settings}" Closing="Window_Closing" LocationChanged="Window_LocationChanged" ResizeMode="CanMinimize" Icon="/Resources/icon-full.ico" WindowStyle="ToolWindow" SizeToContent="WidthAndHeight">
|
||||
|
||||
<Window.Resources>
|
||||
<x:Array x:Key="ShortcutItems" Type="sys:String">
|
||||
<sys:String>None</sys:String>
|
||||
<sys:String>Alt</sys:String>
|
||||
<sys:String>Control</sys:String>
|
||||
<sys:String>Shift</sys:String>
|
||||
</x:Array>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid MinWidth="390">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TabControl Margin="10">
|
||||
<TabItem Header="MPD">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="📶 Connection"/>
|
||||
</StackPanel>
|
||||
<TextBlock>
|
||||
<Run Text="📶"/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_Connection}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="Host" TextWrapping="Wrap" Margin="5,0,0,0"/>
|
||||
<TextBox x:Name="MpdHost" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_Host}" TextWrapping="Wrap" Margin="5,0,0,0"/>
|
||||
<TextBox x:Name="MpdHost" KeyDown="ConnectHandler" TextChanged="MpdConnectTextBox" TextWrapping="Wrap" Margin="10,2,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5,0,0">
|
||||
<TextBlock Text="Port" TextWrapping="Wrap" Margin="5,0,0,0"/>
|
||||
<TextBox x:Name="MpdPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_Port}" TextWrapping="Wrap" Margin="5,0,0,0"/>
|
||||
<TextBox x:Name="MpdPort" KeyDown="ConnectHandler" PreviewTextInput="NumberValidationTextBox" MaxLength="5" TextWrapping="Wrap" Margin="10,2,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5,0,0">
|
||||
<TextBlock Text="Password" TextWrapping="Wrap" Margin="5,0,0,0"/>
|
||||
<TextBox x:Name="MpdPassword" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_Password}" TextWrapping="Wrap" Margin="5,0,0,0"/>
|
||||
<PasswordBox x:Name="MpdPassword" KeyDown="ConnectHandler" Margin="10,2,0,0"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_ConnectionPasswordInfo}" TextWrapping="Wrap" Margin="10,5,0,0" MaxWidth="390"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="ConnectionStatus" Text="Not connected." TextWrapping="Wrap" Margin="5,10,0,0"/>
|
||||
<Button x:Name="ConnectButton" Content="Connect" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
|
||||
<TextBlock TextWrapping="Wrap" Margin="5,10,0,0">
|
||||
<Run Text="{x:Static properties:Resources.Settings_ConnectionStatus}" FontWeight="Bold" />
|
||||
<Run x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}"/>
|
||||
</TextBlock>
|
||||
|
||||
<Button x:Name="ConnectButton" Content="{x:Static properties:Resources.Settings_ConnectButton}" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Snapcast">
|
||||
<TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🔊 Snapcast"/>
|
||||
</StackPanel>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
|
||||
<TextBlock Text="Launch at startup" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
|
||||
<TextBlock Text="Show Snapcast window" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<TextBlock Text="Port" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
|
||||
<TextBlock Text="Executable path" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
|
||||
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
|
||||
You can change to your own locally installed version of the Snapcast client with an <Run FontStyle="Italic" FontWeight="DemiBold">absolute</Run> path.
|
||||
</TextBlock>
|
||||
<Button Content="Reset" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Shortcuts">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="⌨️ Shortcuts"/>
|
||||
</StackPanel>
|
||||
<TextBlock>
|
||||
<Run Text="⌨️"/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_Shortcuts}"></Run>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Volume offset" TextWrapping="Wrap"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeOffset}" TextWrapping="Wrap" Margin="0,2,0,0"/>
|
||||
<TextBox x:Name="VolumeOffset" TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="8,2,0,0"/>
|
||||
</StackPanel>
|
||||
<Grid MinWidth="220" Margin="0,5,0,0">
|
||||
|
||||
<Grid Margin="0,5,0,0" x:Name="RebindKeyWrapper">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
@ -101,53 +87,212 @@
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="Next track" TextWrapping="Wrap" Grid.Column="0" Grid.Row="0" Margin="1"/>
|
||||
<TextBlock Text="Previous track" TextWrapping="Wrap" Grid.Column="0" Grid.Row="1" Margin="1"/>
|
||||
<TextBlock Text="Play / Pause" TextWrapping="Wrap" Grid.Column="0" Grid.Row="2" Margin="1"/>
|
||||
<TextBlock Text="Volume up" TextWrapping="Wrap" Grid.Column="0" Grid.Row="3" Margin="1"/>
|
||||
<TextBlock Text="Volume down" TextWrapping="Wrap" Grid.Column="0" Grid.Row="4" Margin="1"/>
|
||||
<TextBlock Text="Show window" TextWrapping="Wrap" Grid.Column="0" Grid.Row="5" Margin="1"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_NextTrack}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="0" Margin="1,1,1,1" Grid.RowSpan="2"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_PreviousTrack}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="2" Margin="1,1,1,1"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_PlayPause}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="3" Margin="1,1,1,1"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeUp}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="4" Margin="1,1,1,1"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeDown}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="5" Margin="1,1,1,1"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeMute}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="6" Margin="1,1,1,1"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_ShowWindow}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="7" Margin="1,1,1,1"/>
|
||||
|
||||
<TextBlock Text="ctrl + media_next" TextWrapping="Wrap" Grid.Column="1" Grid.Row="0" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
|
||||
<TextBlock Text="ctrl + media_prev" TextWrapping="Wrap" Grid.Column="1" Grid.Row="1" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
|
||||
<TextBlock Text="ctrl + media_play" TextWrapping="Wrap" Grid.Column="1" Grid.Row="2" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
|
||||
<TextBlock Text="ctrl + volume_up" TextWrapping="Wrap" Grid.Column="1" Grid.Row="3" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
|
||||
<TextBlock Text="ctrl + volume_down" TextWrapping="Wrap" Grid.Column="1" Grid.Row="4" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
|
||||
<TextBlock Text="ctrl + alt + enter" TextWrapping="Wrap" Grid.Column="1" Grid.Row="5" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
|
||||
<StackPanel x:Name="Shortcut_NextTrack" Orientation="Horizontal" Grid.Column="1" Grid.Row="0" Margin="10,0,0,2" Grid.RowSpan="2">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="Shortcut_PreviousTrack" Orientation="Horizontal" Grid.Column="1" Grid.Row="2" Margin="10,0,0,2">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="Shortcut_PlayPause" Orientation="Horizontal" Grid.Column="1" Grid.Row="3" Margin="10,0,0,2">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="Shortcut_VolumeUp" Orientation="Horizontal" Grid.Column="1" Grid.Row="4" Margin="10,0,0,2">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="Shortcut_VolumeDown" Orientation="Horizontal" Grid.Column="1" Grid.Row="5" Margin="10,0,0,2">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="Shortcut_VolumeMute" Orientation="Horizontal" Grid.Column="1" Grid.Row="6" Margin="10,0,0,2">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="Shortcut_ShowWindow" Orientation="Horizontal" Grid.Column="1" Grid.Row="7" Margin="10,0,0,0">
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_ShortcutsInfo}" TextWrapping="Wrap" MaxWidth="440" TextAlignment="Justify" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="ShortcutsReset_Clicked"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="About" Height="20" VerticalAlignment="Bottom">
|
||||
<TabItem Header="Snapcast">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<emoji:TextBlock Text="🎶 unison"/>
|
||||
<Run Text="🔊 Snapcast"/>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Margin="10,2,5,0"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPath}" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Margin="10,2,5,0"/>
|
||||
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="390">
|
||||
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
|
||||
</TextBlock>
|
||||
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Width="120" Click="SnapcastReset_Clicked" Margin="0,5,0,0" BorderThickness="1,1,1,1"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{x:Static properties:Resources.Shuffle}">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<Run Text="🔁"/>
|
||||
<Run Text="{x:Static properties:Resources.Shuffle}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<Grid MaxWidth="500">
|
||||
<StackPanel>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Text="{x:Static properties:Resources.Settings_Shuffle1}"></Run>
|
||||
<Run Text="{x:Static properties:Resources.Settings_Shuffle2}"></Run><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_Shuffle3}"></Run><LineBreak/><LineBreak/>
|
||||
|
||||
<Run FontWeight="Bold" Text="{x:Static properties:Resources.Shuffle_Queue}"></Run><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_Shuffle4}"></Run>
|
||||
<LineBreak/><LineBreak/>
|
||||
|
||||
<Run FontWeight="Bold" Text="{x:Static properties:Resources.Shuffle_Continuous}"></Run><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_Shuffle5}"></Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{x:Static properties:Resources.Database}" Height="20" VerticalAlignment="Bottom">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<Run Text="📊"/>
|
||||
<Run Text="{x:Static properties:Resources.Database}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel>
|
||||
|
||||
<Grid VerticalAlignment="Top">
|
||||
<TextBlock>
|
||||
<Run Text="{x:Static properties:Resources.Stats_Songs}"/><Run Text=" "/><Run x:Name="StatSong"/><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Stats_Albums}"/><Run Text=" "/><Run x:Name="StatAlbum"/><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Stats_Artists}"/><Run Text=" "/><Run x:Name="StatArtist"/><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Stats_TotalPlaytime}"/><Run Text=" "/><Run x:Name="StatTotalPlaytime"/><LineBreak/><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Stats_Uptime}"/><Run Text=" "/><Run x:Name="StatUptime"/><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Stats_TotalTimePlayed}"/><Run Text=" "/><Run x:Name="StatTotalTimePlayed"/><LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Stats_LastDatabaseUpdate}"/><Run Text=" "/><Run x:Name="StatDatabaseUpdate"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Button x:Name="UpdateDatabaseButton" Content="{x:Static properties:Resources.Settings_UpdateDatabase}" Click="MPDDatabaseUpdate_Clicked" MinWidth="120" Padding="3,1,3,1" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBlock x:Name="UpdateDBMessage" Text="{x:Static properties:Resources.Stats_UpdateDBMessage1}" Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed" />
|
||||
<TextBlock x:Name="UpdateDBMessage2" Text="{x:Static properties:Resources.Stats_UpdateDBMessage2}" Margin="3,3,0,0" FontStyle="Italic" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{x:Static properties:Resources.Settings_About}" Height="20" VerticalAlignment="Bottom">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<Run Text="🎶 unison"/>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock TextWrapping="Wrap" Margin="0,0,0,10" VerticalAlignment="Top">
|
||||
<Run Text="Version:"/>
|
||||
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
|
||||
<Run Text="{x:Static properties:Resources.Settings_Version}"/>
|
||||
<Run Text="{Binding GetVersion, Mode = OneWay}"/>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="UpdateText" TextWrapping="Wrap" VerticalAlignment="Top">
|
||||
<Run x:Name="UpdateText2" Text="New version X.X.X available!" FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<Button x:Name="UpdateButton" Content="{x:Static properties:Resources.Update_ButtonCheck}" Margin="0,3,0,10" Width="150" Click="CheckUpdates" HorizontalAlignment="Left"/>
|
||||
|
||||
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
|
||||
unison is free software. It is built with the following technologies:<LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/Difegue/Stylophone" RequestNavigate="Hyperlink_RequestNavigate">Stylophone</Hyperlink>'s updated MpcNET<LineBreak/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_AboutInfo}" /><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/Difegue/MpcNET" RequestNavigate="Hyperlink_RequestNavigate">MpcNET</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/hardcodet/wpf-notifyicon" RequestNavigate="Hyperlink_RequestNavigate">wpf-notifyicon</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/samhocevar/emoji.wpf" RequestNavigate="Hyperlink_RequestNavigate">Emoji.WPF</Hyperlink>
|
||||
※ <Hyperlink NavigateUri="https://github.com/samhocevar/emoji.wpf" RequestNavigate="Hyperlink_RequestNavigate">Emoji.WPF</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://git.sr.ht/~youkai/RadioBrowser.NET" RequestNavigate="Hyperlink_RequestNavigate">RadioBrowser</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/ravibpatel/AutoUpdater.NET" RequestNavigate="Hyperlink_RequestNavigate">AutoUpdater.NET</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/badaix/snapcast" RequestNavigate="Hyperlink_RequestNavigate">Snapcast</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,10,0,0">
|
||||
Source code freely available
|
||||
<Hyperlink NavigateUri="https://git.n700.ovh/keb/unison" RequestNavigate="Hyperlink_RequestNavigate">
|
||||
here
|
||||
<Run Text="{x:Static properties:Resources.Settings_SourceCode1}" />
|
||||
<Hyperlink NavigateUri="https://github.com/ZetaKebab/unison" RequestNavigate="Hyperlink_RequestNavigate">
|
||||
<Run Text="{x:Static properties:Resources.Settings_SourceCode2}" />
|
||||
</Hyperlink>.
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,10,0,0">
|
||||
Made by
|
||||
<Run Text="{x:Static properties:Resources.Settings_MadeBy}" />
|
||||
<Hyperlink NavigateUri="https://marchal.dev" RequestNavigate="Hyperlink_RequestNavigate">
|
||||
Théo Marchal
|
||||
</Hyperlink>.
|
||||
@ -155,10 +300,17 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{x:Static properties:Resources.Settings_License}" Height="20" VerticalAlignment="Bottom">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0" Margin="0,10,0,0">
|
||||
<GroupBox.Header>
|
||||
<emoji:TextBlock Text="📝 License"/>
|
||||
<TextBlock>
|
||||
<Run Text="📝"/>
|
||||
<Run Text="{x:Static properties:Resources.Settings_License}" />
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<TextBlock Text="{Binding GetLicense, Mode = OneWay}" TextWrapping="Wrap" Width="500" TextAlignment="Justify" />
|
||||
|
@ -1,21 +1,24 @@
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Threading;
|
||||
using unison.Handlers;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class Settings : Window
|
||||
{
|
||||
private string defaultSnapcastPath = "snapclient_0.25.0-1_win64";
|
||||
private string defaultSnapcastPort = "1704";
|
||||
|
||||
public static string GetVersion => Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
|
||||
|
||||
public static string GetLicense
|
||||
@ -36,22 +39,52 @@ namespace unison
|
||||
}
|
||||
}
|
||||
|
||||
readonly HotkeyHandler _hotkeys = (HotkeyHandler)Application.Current.Properties["hotkeys"];
|
||||
|
||||
public Settings()
|
||||
{
|
||||
InitHwnd();
|
||||
InitializeComponent();
|
||||
Initialize();
|
||||
DataContext = this;
|
||||
|
||||
WindowState = WindowState.Minimized;
|
||||
|
||||
Top = Properties.Settings.Default.SettingsWindowTop;
|
||||
Left = Properties.Settings.Default.SettingsWindowLeft;
|
||||
}
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
MpdHost.Text = Properties.Settings.Default.mpd_host;
|
||||
MpdPort.Text = Properties.Settings.Default.mpd_port.ToString();
|
||||
MpdPassword.Text = Properties.Settings.Default.mpd_password;
|
||||
MpdPassword.Password = Properties.Settings.Default.mpd_password;
|
||||
SnapcastStartup.IsChecked = Properties.Settings.Default.snapcast_startup;
|
||||
SnapcastWindow.IsChecked = Properties.Settings.Default.snapcast_window;
|
||||
SnapcastPath.Text = Properties.Settings.Default.snapcast_path;
|
||||
SnapcastPort.Text = Properties.Settings.Default.snapcast_port.ToString();
|
||||
VolumeOffset.Text = Properties.Settings.Default.volume_offset.ToString();
|
||||
|
||||
UpdateText.Visibility = Visibility.Collapsed;
|
||||
|
||||
InitializeShortcuts();
|
||||
}
|
||||
|
||||
public void UpdateConnectionStatus()
|
||||
{
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
if (mpd.IsConnected())
|
||||
{
|
||||
ConnectionStatus.Text = $"{unison.Resources.Resources.Settings_ConnectionStatusConnected} {mpd.GetVersion()}.";
|
||||
ConnectButton.IsEnabled = false;
|
||||
UpdateDatabaseButton.IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusOffline;
|
||||
ConnectButton.IsEnabled = true;
|
||||
UpdateDatabaseButton.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
|
||||
@ -60,48 +93,106 @@ namespace unison
|
||||
e.Handled = regex.IsMatch(e.Text);
|
||||
}
|
||||
|
||||
private void MpdConnectTextBox(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
TextBox textBox = (TextBox)sender;
|
||||
|
||||
if (textBox.Text == Properties.Settings.Default.mpd_host)
|
||||
ConnectButton.IsEnabled = false;
|
||||
else
|
||||
ConnectButton.IsEnabled = true;
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
|
||||
{
|
||||
ProcessStartInfo psi = new(e.Uri.AbsoluteUri);
|
||||
ProcessStartInfo psi = new ProcessStartInfo(e.Uri.AbsoluteUri);
|
||||
psi.UseShellExecute = true;
|
||||
Process.Start(psi);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
public void UpdateConnectionStatus()
|
||||
{
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
if (mpd._connected)
|
||||
ConnectionStatus.Text = "Connected to MPD " + mpd.GetVersion() + ".";
|
||||
else
|
||||
ConnectionStatus.Text = "Not connected.";
|
||||
}
|
||||
|
||||
private void MPDConnect_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ConnectButton.IsEnabled)
|
||||
return;
|
||||
|
||||
SaveSettings();
|
||||
ConnectionStatus.Text = "Connecting...";
|
||||
|
||||
ConnectButton.IsEnabled = false;
|
||||
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
|
||||
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
mpd.Connect();
|
||||
Task.Run(async () => { await mpd.Initialize(); });
|
||||
}
|
||||
|
||||
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SnapcastPath.Text = defaultSnapcastPath;
|
||||
SnapcastPort.Text = defaultSnapcastPort;
|
||||
SnapcastPath.Text = (string)Application.Current.FindResource("snapcastPath");
|
||||
SnapcastPort.Text = (string)Application.Current.FindResource("snapcastPort");
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
public void UpdateStats()
|
||||
{
|
||||
Properties.Settings.Default.mpd_host = MpdHost.Text;
|
||||
Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture);
|
||||
Properties.Settings.Default.mpd_password = MpdPassword.Text;
|
||||
Properties.Settings.Default.snapcast_startup = (bool)SnapcastStartup.IsChecked;
|
||||
Properties.Settings.Default.snapcast_window = (bool)SnapcastWindow.IsChecked;
|
||||
Properties.Settings.Default.snapcast_path = SnapcastPath.Text;
|
||||
Properties.Settings.Default.snapcast_port = int.Parse(SnapcastPort.Text, CultureInfo.InvariantCulture);
|
||||
Properties.Settings.Default.volume_offset = int.Parse(VolumeOffset.Text, CultureInfo.InvariantCulture);
|
||||
Properties.Settings.Default.Save();
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
StatSong.Text = mpd.GetStats().Songs.ToString();
|
||||
StatAlbum.Text = mpd.GetStats().Albums.ToString();
|
||||
StatArtist.Text = mpd.GetStats().Artists.ToString();
|
||||
StatTotalPlaytime.Text = mpd.GetStats().TotalPlaytime.ToString();
|
||||
StatUptime.Text = mpd.GetStats().Uptime.ToString();
|
||||
StatTotalTimePlayed.Text = mpd.GetStats().TotalTimePlayed.ToString();
|
||||
StatDatabaseUpdate.Text = mpd.GetStats().DatabaseUpdate.ToString();
|
||||
}
|
||||
|
||||
private void ConnectHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
MPDConnect_Clicked(null, null);
|
||||
}
|
||||
|
||||
private void MPDDatabaseUpdate_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
if (mpd.IsConnected())
|
||||
mpd.UpdateDB();
|
||||
}
|
||||
|
||||
private static void TimedText(TextBlock textBlock, int time)
|
||||
{
|
||||
DispatcherTimer Timer = new DispatcherTimer();
|
||||
Timer.Interval = TimeSpan.FromSeconds(time);
|
||||
Timer.Tick += (sender, args) =>
|
||||
{
|
||||
Timer.Stop();
|
||||
textBlock.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
Timer.Start();
|
||||
}
|
||||
|
||||
public void MPDDatabaseUpdate_Start()
|
||||
{
|
||||
UpdateDBMessage.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
public void MPDDatabaseUpdate_Stop()
|
||||
{
|
||||
UpdateDBMessage2.Visibility = Visibility.Visible;
|
||||
TimedText(UpdateDBMessage, 2);
|
||||
TimedText(UpdateDBMessage2, 2);
|
||||
}
|
||||
|
||||
private void CheckUpdates(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateHandler updater = (UpdateHandler)Application.Current.Properties["updater"];
|
||||
updater.Start(true);
|
||||
}
|
||||
|
||||
public void UpdateUpdateStatus(string version)
|
||||
{
|
||||
UpdateText.Visibility = Visibility.Visible;
|
||||
UpdateText2.Text = unison.Resources.Resources.Update_String1 + " " + version + " " + unison.Resources.Resources.Update_String2;
|
||||
UpdateButton.Content = unison.Resources.Resources.Update_ButtonStart;
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
@ -117,5 +208,241 @@ namespace unison
|
||||
WindowInteropHelper helper = new(this);
|
||||
helper.EnsureHandle();
|
||||
}
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
Properties.Settings.Default.SettingsWindowTop = Top;
|
||||
Properties.Settings.Default.SettingsWindowLeft = Left;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
Properties.Settings.Default.mpd_host = MpdHost.Text;
|
||||
Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture);
|
||||
Properties.Settings.Default.mpd_password = MpdPassword.Password;
|
||||
Properties.Settings.Default.snapcast_startup = (bool)SnapcastStartup.IsChecked;
|
||||
Properties.Settings.Default.snapcast_window = (bool)SnapcastWindow.IsChecked;
|
||||
Properties.Settings.Default.snapcast_path = SnapcastPath.Text;
|
||||
Properties.Settings.Default.snapcast_port = int.Parse(SnapcastPort.Text, CultureInfo.InvariantCulture);
|
||||
Properties.Settings.Default.volume_offset = int.Parse(VolumeOffset.Text, CultureInfo.InvariantCulture);
|
||||
|
||||
Properties.Settings.Default.nextTrack_mod = GetMod(Shortcut_NextTrack);
|
||||
Properties.Settings.Default.nextTrack_vk = GetVk(Shortcut_NextTrack);
|
||||
Properties.Settings.Default.previousTrack_mod = GetMod(Shortcut_PreviousTrack);
|
||||
Properties.Settings.Default.previousTrack_vk = GetVk(Shortcut_PreviousTrack);
|
||||
Properties.Settings.Default.playPause_mod = GetMod(Shortcut_PlayPause);
|
||||
Properties.Settings.Default.playPause_vk = GetVk(Shortcut_PlayPause);
|
||||
Properties.Settings.Default.volumeUp_mod = GetMod(Shortcut_VolumeUp);
|
||||
Properties.Settings.Default.volumeUp_vk = GetVk(Shortcut_VolumeUp);
|
||||
Properties.Settings.Default.volumeDown_mod = GetMod(Shortcut_VolumeDown);
|
||||
Properties.Settings.Default.volumeDown_vk = GetVk(Shortcut_VolumeDown);
|
||||
Properties.Settings.Default.volumeMute_mod = GetMod(Shortcut_VolumeMute);
|
||||
Properties.Settings.Default.volumeMute_vk = GetVk(Shortcut_VolumeMute);
|
||||
Properties.Settings.Default.showWindow_mod = GetMod(Shortcut_ShowWindow);
|
||||
Properties.Settings.Default.showWindow_vk = GetVk(Shortcut_ShowWindow);
|
||||
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
|
||||
|
||||
// Hotkeys
|
||||
|
||||
void InitializeShortcuts()
|
||||
{
|
||||
System.Collections.Generic.IEnumerable<StackPanel> stackPanelCollection = RebindKeyWrapper.Children.OfType<StackPanel>();
|
||||
StackPanel[] stackPanelList = stackPanelCollection.ToArray();
|
||||
|
||||
// Default state
|
||||
for (int i = 0; i < stackPanelList.Length; i++)
|
||||
{
|
||||
ComboBox[] comboBoxList = stackPanelList[i].Children.OfType<ComboBox>().ToArray();
|
||||
foreach (ComboBox comboBox in comboBoxList) // default status (for reset)
|
||||
{
|
||||
comboBox.FontWeight = FontWeights.Light;
|
||||
comboBox.SelectedItem = "None";
|
||||
}
|
||||
TextBlock textBlock = (TextBlock)stackPanelList[i].Children.OfType<Button>().FirstOrDefault().Content;
|
||||
textBlock.Text = "None";
|
||||
}
|
||||
|
||||
// Populate values
|
||||
for (int i = 0; i < stackPanelList.Length; i++)
|
||||
{
|
||||
// setup MOD
|
||||
HotkeyHandler.MOD mod = _hotkeys._Shortcuts[i].mod;
|
||||
|
||||
HotkeyHandler.MOD[] MODList = System.Enum.GetValues(typeof(HotkeyHandler.MOD))
|
||||
.OfType<HotkeyHandler.MOD>()
|
||||
.Select(x => x & _hotkeys._Shortcuts[i].mod)
|
||||
.Where(x => x != HotkeyHandler.MOD.None)
|
||||
.ToArray();
|
||||
|
||||
ComboBox[] comboBox = stackPanelList[i].Children.OfType<ComboBox>().ToArray();
|
||||
for (int j = 0; j < MODList.Length; j++)
|
||||
{
|
||||
comboBox[j].SelectedItem = MODList[j].ToString();
|
||||
if (comboBox[j].SelectedItem.ToString() != "None")
|
||||
comboBox[j].FontWeight = FontWeights.Bold;
|
||||
}
|
||||
|
||||
// setup VK
|
||||
TextBlock textBlock = (TextBlock)stackPanelList[i].Children.OfType<Button>().FirstOrDefault().Content;
|
||||
textBlock.Text = _hotkeys._Shortcuts[i].vk.ToString();
|
||||
if (textBlock.Text != "None")
|
||||
textBlock.FontWeight = FontWeights.Bold;
|
||||
}
|
||||
}
|
||||
|
||||
private void HotkeyChanged()
|
||||
{
|
||||
_hotkeys.RemoveHotkeys();
|
||||
_hotkeys.AddHotkeys();
|
||||
}
|
||||
|
||||
private ref HotkeyHandler.HotkeyPair GetHotkeyVariable(string Name)
|
||||
{
|
||||
if (Name == "Shortcut_NextTrack")
|
||||
return ref _hotkeys._NextTrack;
|
||||
if (Name == "Shortcut_PreviousTrack")
|
||||
return ref _hotkeys._PreviousTrack;
|
||||
if (Name == "Shortcut_PlayPause")
|
||||
return ref _hotkeys._PlayPause;
|
||||
if (Name == "Shortcut_VolumeUp")
|
||||
return ref _hotkeys._VolumeUp;
|
||||
if (Name == "Shortcut_VolumeDown")
|
||||
return ref _hotkeys._VolumeDown;
|
||||
if (Name == "Shortcut_VolumeMute")
|
||||
return ref _hotkeys._VolumeMute;
|
||||
if (Name == "Shortcut_ShowWindow")
|
||||
return ref _hotkeys._ShowWindow;
|
||||
return ref _hotkeys._NextTrack;
|
||||
}
|
||||
|
||||
private void UpdateHotkey_MOD(string Name, HotkeyHandler.MOD mod) => GetHotkeyVariable(Name).SetMOD(mod);
|
||||
private void UpdateHotkey_VK(string Name, HotkeyHandler.VK vk) => GetHotkeyVariable(Name).SetVK(vk);
|
||||
|
||||
private void MOD_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
|
||||
ComboBox comboBox = (ComboBox)sender;
|
||||
StackPanel stackPanel = (StackPanel)comboBox.Parent;
|
||||
System.Collections.Generic.IEnumerable<ComboBox> stackPanelCollection = stackPanel.Children.OfType<ComboBox>();
|
||||
|
||||
HotkeyHandler.MOD MOD1, MOD2;
|
||||
|
||||
// we need to do this because the element is modified -after- this function
|
||||
if (comboBox.Tag.ToString() == "MOD1")
|
||||
{
|
||||
MOD1 = GetMOD(e.AddedItems[0].ToString());
|
||||
MOD2 = GetMOD(stackPanelCollection.Last().Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
MOD1 = GetMOD(stackPanelCollection.First().Text);
|
||||
MOD2 = GetMOD(e.AddedItems[0].ToString());
|
||||
}
|
||||
|
||||
if (e.AddedItems[0].ToString() == "None")
|
||||
comboBox.FontWeight = FontWeights.Light;
|
||||
else
|
||||
comboBox.FontWeight = FontWeights.Bold;
|
||||
|
||||
HotkeyHandler.MOD ModKey = MOD1 | MOD2;
|
||||
|
||||
UpdateHotkey_MOD(stackPanel.Name, ModKey);
|
||||
HotkeyChanged();
|
||||
}
|
||||
|
||||
private void RemapKey_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Button button = (Button)sender;
|
||||
TextBlock textBlock = (TextBlock)button.Content;
|
||||
textBlock.Text = unison.Resources.Resources.Settings_ShortcutsKey;
|
||||
textBlock.FontWeight = FontWeights.Bold;
|
||||
button.PreviewKeyDown += DetectPressedKey;
|
||||
}
|
||||
|
||||
private void DetectPressedKey(object sender, KeyEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
Key pressedKey = e.Key;
|
||||
HotkeyHandler.VK VirtualKey = GetVirtualKey(pressedKey);
|
||||
|
||||
Button button = (Button)sender;
|
||||
TextBlock textBlock = (TextBlock)button.Content;
|
||||
StackPanel stackPanel = (StackPanel)button.Parent;
|
||||
|
||||
if (VirtualKey == HotkeyHandler.VK.None)
|
||||
{
|
||||
pressedKey = Key.None;
|
||||
textBlock.FontWeight = FontWeights.Light;
|
||||
}
|
||||
else
|
||||
textBlock.FontWeight = FontWeights.Bold;
|
||||
|
||||
textBlock.Text = pressedKey.ToString();
|
||||
button.PreviewKeyDown -= DetectPressedKey;
|
||||
|
||||
UpdateHotkey_VK(stackPanel.Name, VirtualKey);
|
||||
HotkeyChanged();
|
||||
}
|
||||
|
||||
private static HotkeyHandler.VK GetVirtualKey(Key key)
|
||||
{
|
||||
foreach (object value in System.Enum.GetValues(typeof(HotkeyHandler.VK)))
|
||||
{
|
||||
if (key.ToString().ToLower() == value.ToString().ToLower())
|
||||
return (HotkeyHandler.VK)value;
|
||||
}
|
||||
return HotkeyHandler.VK.None;
|
||||
}
|
||||
|
||||
private static HotkeyHandler.MOD GetMOD(string str)
|
||||
{
|
||||
foreach (object value in System.Enum.GetValues(typeof(HotkeyHandler.MOD)))
|
||||
{
|
||||
if (str.ToLower() == value.ToString().ToLower())
|
||||
return (HotkeyHandler.MOD)value;
|
||||
}
|
||||
return HotkeyHandler.MOD.None;
|
||||
}
|
||||
|
||||
private void ShortcutsReset_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Properties.Settings.Default.nextTrack_mod = (uint)Application.Current.FindResource("nextTrack_mod");
|
||||
Properties.Settings.Default.nextTrack_vk = (uint)Application.Current.FindResource("nextTrack_vk");
|
||||
Properties.Settings.Default.previousTrack_mod = (uint)Application.Current.FindResource("previousTrack_mod");
|
||||
Properties.Settings.Default.previousTrack_vk = (uint)Application.Current.FindResource("previousTrack_vk");
|
||||
Properties.Settings.Default.playPause_mod = (uint)Application.Current.FindResource("playPause_mod");
|
||||
Properties.Settings.Default.playPause_vk = (uint)Application.Current.FindResource("playPause_vk");
|
||||
Properties.Settings.Default.volumeUp_mod = (uint)Application.Current.FindResource("volumeUp_mod");
|
||||
Properties.Settings.Default.volumeUp_vk = (uint)Application.Current.FindResource("volumeUp_vk");
|
||||
Properties.Settings.Default.volumeDown_mod = (uint)Application.Current.FindResource("volumeDown_mod");
|
||||
Properties.Settings.Default.volumeDown_vk = (uint)Application.Current.FindResource("volumeDown_vk");
|
||||
Properties.Settings.Default.volumeMute_mod = (uint)Application.Current.FindResource("volumeMute_mod");
|
||||
Properties.Settings.Default.volumeMute_vk = (uint)Application.Current.FindResource("volumeMute_vk");
|
||||
Properties.Settings.Default.showWindow_mod = (uint)Application.Current.FindResource("showWindow_mod");
|
||||
Properties.Settings.Default.showWindow_vk = (uint)Application.Current.FindResource("showWindow_vk");
|
||||
|
||||
_hotkeys.Initialize();
|
||||
HotkeyChanged();
|
||||
InitializeShortcuts();
|
||||
}
|
||||
|
||||
private static uint GetMod(StackPanel stackPanel)
|
||||
{
|
||||
return (uint)(GetMOD(stackPanel.Children.OfType<ComboBox>().First().SelectedItem.ToString()) | GetMOD(stackPanel.Children.OfType<ComboBox>().Last().SelectedItem.ToString()));
|
||||
}
|
||||
|
||||
private static uint GetVk(StackPanel stackPanel)
|
||||
{
|
||||
Button button = stackPanel.Children.OfType<Button>().First();
|
||||
TextBlock textBlock = (TextBlock)button.Content;
|
||||
return (uint)(HotkeyHandler.VK)System.Enum.Parse(typeof(HotkeyHandler.VK), textBlock.Text, true);
|
||||
}
|
||||
}
|
||||
}
|
124
Views/Shuffle.xaml
Normal file
@ -0,0 +1,124 @@
|
||||
<Window x:Class="unison.Shuffle"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
|
||||
xmlns:properties="clr-namespace:unison.Resources" xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||
mc:Ignorable="d"
|
||||
Title="Shuffle" Closing="Window_Closing" LocationChanged="Window_LocationChanged" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
|
||||
|
||||
<Window.Resources>
|
||||
<x:Array x:Key="FilterType" Type="sys:String">
|
||||
<x:Static Member="properties:Resources.FilterType_Song"/>
|
||||
<x:Static Member="properties:Resources.FilterType_Artist"/>
|
||||
<x:Static Member="properties:Resources.FilterType_Album"/>
|
||||
<x:Static Member="properties:Resources.FilterType_Year"/>
|
||||
<x:Static Member="properties:Resources.FilterType_Genre"/>
|
||||
<x:Static Member="properties:Resources.FilterType_Directory"/>
|
||||
</x:Array>
|
||||
<x:Array x:Key="OperatorTypeA" Type="sys:String">
|
||||
<x:Static Member="properties:Resources.Operator_Contains"/>
|
||||
<x:Static Member="properties:Resources.Operator_Is"/>
|
||||
<x:Static Member="properties:Resources.Operator_IsNot"/>
|
||||
</x:Array>
|
||||
<x:Array x:Key="OperatorTypeB" Type="sys:String">
|
||||
<x:Static Member="properties:Resources.Operator_Is"/>
|
||||
<x:Static Member="properties:Resources.Operator_IsNot"/>
|
||||
</x:Array>
|
||||
<x:Array x:Key="OperatorTypeC" Type="sys:String">
|
||||
<x:Static Member="properties:Resources.Operator_Is"/>
|
||||
</x:Array>
|
||||
|
||||
<DataTemplate x:Key="FilterPanel">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<ComboBox x:Name="FilterType" SelectionChanged="FilterType_SelectionChanged" ItemsSource="{StaticResource FilterType}" SelectedIndex="0" Width="100" ScrollViewer.CanContentScroll="False" FocusVisualStyle="{x:Null}"/>
|
||||
<ComboBox x:Name="FilterOperator" SelectionChanged="OperatorType_SelectionChanged" ItemsSource="{StaticResource OperatorTypeA}" SelectedIndex="0" Width="80" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
|
||||
<ComboBox x:Name="FilterList" SelectedIndex="0" Width="240" Visibility="Collapsed" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBox x:Name="FilterValue" KeyUp="QueryFilterHandler" Width="240" Margin="5,0,0,0"/>
|
||||
<Button Content="-" Padding="5, 2" Click="RemoveFilter_Clicked" Width="20" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="5,0,0,0"/>
|
||||
<Button Content="+" Padding="5, 2" Click="AddFilter_Clicked" Width="20" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="0,5,0,0">
|
||||
<StackPanel>
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="5,0,5,5">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<Run Text="🔡"/>
|
||||
<Run Text="{x:Static properties:Resources.Shuffle_Filter}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical" Margin="5,0,5,0">
|
||||
|
||||
<StackPanel x:Name="FilterPanel">
|
||||
<ContentPresenter ContentTemplate="{StaticResource FilterPanel}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="SongFilterPanel" Margin="0,10,0,0">
|
||||
<TextBlock>
|
||||
<Run Text="{x:Static properties:Resources.Shuffle_FilterSongNumber}"/><Run Text=" "/><Run x:Name="SongFilterNumber" FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5,0,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
|
||||
<Button Content="{x:Static properties:Resources.Shuffle_FilterQuery}" Click="UpdateFilter_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="0,0,10,0"/>
|
||||
<Button Content="{x:Static properties:Resources.Shuffle_FilterReset}" Click="Reset_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBlock x:Name="QueryFilterText" Text="{x:Static properties:Resources.Shuffle_Querying1}" Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed" />
|
||||
<TextBlock x:Name="QueryFilterText2" Text="{x:Static properties:Resources.Shuffle_Querying2}" Margin="3,3,0,0" FontStyle="Italic" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<GroupBox DockPanel.Dock="Right" Padding="0,4,0,0" Width="300">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<Run Text="🧾"/>
|
||||
<Run Text="{x:Static properties:Resources.Queue_Management}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical" Margin="5,5,5,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{x:Static properties:Resources.Shuffle_QueueSongs}" Margin="0,0,5,5"/>
|
||||
<TextBox x:Name="SongNumber" KeyUp="AddToQueueHandler" PreviewTextInput="QueueValidationTextBox" MaxLength="4" Text="15" Width="35" HorizontalAlignment="Left" VerticalAlignment="Top"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<Button Content="{x:Static properties:Resources.Shuffle_Queue}" Click="AddToQueue_Clicked" Padding="5, 2" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBlock x:Name="SearchStatus" Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed">
|
||||
<Run Text="{x:Static properties:Resources.Shuffle_ButtonMessage1}"/><Run Text=" "/><Run x:Name="NumberAddedSongs"/><Run Text=" "/><Run Text="{x:Static properties:Resources.Shuffle_ButtonMessage2}"/>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="SearchStatus2" Text="{x:Static properties:Resources.Shuffle_ButtonMessage3}" Margin="3,3,0,0" FontStyle="Italic" Visibility="Collapsed"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,15,0,0">
|
||||
<Button Content="{x:Static properties:Resources.Clear_Queue}" Click="ClearQueue_Clicked" Padding="5, 2" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
|
||||
<Button Content="{x:Static properties:Resources.Play_Queue}" Click="StartQueue_Clicked" Padding="5, 2" Margin="5, 0" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox DockPanel.Dock="Left" Padding="0,4,0,0" Width="260" Margin="0,0,5,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<Run Text="♾️"/>
|
||||
<Run Text="{x:Static properties:Resources.Shuffle_Continuous}"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal" Margin="5,7,5,0">
|
||||
<CheckBox x:Name="ContinuousShuffle" Checked="ContinuousShuffle_Checked" Unchecked="ContinuousShuffle_Checked" FocusVisualStyle="{x:Null}" VerticalAlignment="Top">
|
||||
<TextBlock Text="{x:Static properties:Resources.Shuffle_ContinuousEnable}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
446
Views/Shuffle.xaml.cs
Normal file
@ -0,0 +1,446 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MpcNET.Commands.Database;
|
||||
using MpcNET.Tags;
|
||||
using MpcNET.Types;
|
||||
using MpcNET.Types.Filters;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class Shuffle : Window
|
||||
{
|
||||
private readonly MPDHandler _mpd;
|
||||
private readonly ShuffleHandler _shuffle;
|
||||
|
||||
List<string> GenreList { get; }
|
||||
List<string> FolderList { get; }
|
||||
List<IFilter> Filters { get; }
|
||||
|
||||
bool _continuous = false;
|
||||
|
||||
public Shuffle()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitHwnd();
|
||||
|
||||
GenreList = new();
|
||||
FolderList = new();
|
||||
Filters = new();
|
||||
SongFilterNumber.Text = "0";
|
||||
|
||||
WindowState = WindowState.Minimized;
|
||||
|
||||
Top = Properties.Settings.Default.ShuffleWindowTop;
|
||||
Left = Properties.Settings.Default.ShuffleWindowLeft;
|
||||
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
_shuffle = (ShuffleHandler)Application.Current.Properties["shuffle"];
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
ListGenre(_mpd._cancelCommand.Token);
|
||||
ListFolder(_mpd._cancelCommand.Token);
|
||||
}
|
||||
|
||||
private async void ListGenre(CancellationToken token)
|
||||
{
|
||||
if (GenreList.Count != 0)
|
||||
return;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
List<string> Response = await _mpd.SafelySendCommandAsync(new ListCommand(MpdTags.Genre, null, null, null));
|
||||
|
||||
if (Response == null)
|
||||
return;
|
||||
|
||||
foreach (string genre in Response)
|
||||
GenreList.Add(genre);
|
||||
}
|
||||
|
||||
private async void ListFolder(CancellationToken token)
|
||||
{
|
||||
if (FolderList.Count != 0)
|
||||
return;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
IEnumerable<IMpdFilePath> Response = await _mpd.SafelySendCommandAsync(new LsInfoCommand(""));
|
||||
|
||||
if (Response == null)
|
||||
return;
|
||||
|
||||
foreach (IMpdFilePath folder in Response)
|
||||
FolderList.Add(folder.Name);
|
||||
}
|
||||
|
||||
private bool IsFilterEmpty()
|
||||
{
|
||||
if (Filters.Count == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private T FindParent<T>(DependencyObject child) where T : DependencyObject
|
||||
{
|
||||
var parent = VisualTreeHelper.GetParent(child);
|
||||
|
||||
if (parent == null)
|
||||
return null;
|
||||
|
||||
if (parent is T)
|
||||
return parent as T;
|
||||
else
|
||||
return FindParent<T>(parent);
|
||||
}
|
||||
|
||||
private void AddFilter_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FilterPanel.Children.Add(new ContentPresenter { ContentTemplate = (DataTemplate)FindResource("FilterPanel") });
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private void RemoveFilter_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (FilterPanel.Children.Count > 1)
|
||||
FilterPanel.Children.Remove(FindParent<ContentPresenter>(sender as Button));
|
||||
else
|
||||
Reset_Clicked(null, null);
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private void Reset_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FilterPanel.Children.RemoveRange(0, FilterPanel.Children.Count);
|
||||
FilterPanel.Children.Add(new ContentPresenter { ContentTemplate = (DataTemplate)FindResource("FilterPanel") });
|
||||
SongFilterNumber.Text = "0";
|
||||
_shuffle.SongList.Clear();
|
||||
}
|
||||
|
||||
private static ITag FilterEquivalence_Type(string value)
|
||||
{
|
||||
if (value == unison.Resources.Resources.FilterType_Song)
|
||||
return MpdTags.Title;
|
||||
else if (value == unison.Resources.Resources.FilterType_Artist)
|
||||
return MpdTags.Artist;
|
||||
else if (value == unison.Resources.Resources.FilterType_Album)
|
||||
return MpdTags.Album;
|
||||
else if (value == unison.Resources.Resources.FilterType_Year)
|
||||
return MpdTags.Date;
|
||||
else if (value == unison.Resources.Resources.FilterType_Genre)
|
||||
return MpdTags.Genre;
|
||||
return MpdTags.Title;
|
||||
}
|
||||
|
||||
private static FilterOperator FilterEquivalence_Operator(string value)
|
||||
{
|
||||
if (value == unison.Resources.Resources.Operator_Contains)
|
||||
return FilterOperator.Contains;
|
||||
else if (value == unison.Resources.Resources.Operator_Is)
|
||||
return FilterOperator.Equal;
|
||||
else if (value == unison.Resources.Resources.Operator_IsNot)
|
||||
return FilterOperator.Different;
|
||||
return FilterOperator.Equal;
|
||||
}
|
||||
|
||||
private void FilterType_Change(object sender, string Operator, List<string> Listing)
|
||||
{
|
||||
ComboBox comboBox = sender as ComboBox;
|
||||
StackPanel stackPanel = comboBox.Parent as StackPanel;
|
||||
foreach (ComboBox child in stackPanel.Children.OfType<ComboBox>())
|
||||
{
|
||||
if (child.Name == "FilterOperator")
|
||||
{
|
||||
child.ItemsSource = (Array)FindResource(Operator);
|
||||
child.SelectedItem = child.Items[0];
|
||||
}
|
||||
|
||||
if (child.Name == "FilterList")
|
||||
{
|
||||
child.Visibility = Visibility.Visible;
|
||||
child.ItemsSource = Listing;
|
||||
child.SelectedItem = child.Items[0];
|
||||
}
|
||||
}
|
||||
foreach (TextBox child in stackPanel.Children.OfType<TextBox>())
|
||||
{
|
||||
if (child.Name == "FilterValue")
|
||||
child.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private void FilterType_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
string item = e.AddedItems[0].ToString();
|
||||
if (item == unison.Resources.Resources.FilterType_Genre)
|
||||
FilterType_Change(sender, "OperatorTypeB", GenreList);
|
||||
else if (item == unison.Resources.Resources.FilterType_Directory)
|
||||
FilterType_Change(sender, "OperatorTypeC", FolderList);
|
||||
else
|
||||
{
|
||||
ComboBox combobox = sender as ComboBox;
|
||||
StackPanel stackpanel = combobox.Parent as StackPanel;
|
||||
foreach (ComboBox child in stackpanel.Children.OfType<ComboBox>())
|
||||
{
|
||||
if (child.Name == "FilterOperator")
|
||||
{
|
||||
child.ItemsSource = (Array)FindResource("OperatorTypeA");
|
||||
child.SelectedItem = child.Items[0];
|
||||
}
|
||||
|
||||
if (child.Name == "FilterList")
|
||||
child.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
foreach (TextBox child in stackpanel.Children.OfType<TextBox>())
|
||||
{
|
||||
if (child.Name == "FilterValue")
|
||||
child.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private void OperatorType_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private async void UpdateFilter_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
QueryFilterText.Visibility = Visibility.Visible;
|
||||
await UpdateFilter();
|
||||
|
||||
QueryFilterText2.Visibility = Visibility.Visible;
|
||||
|
||||
TimedText(QueryFilterText, 1);
|
||||
TimedText(QueryFilterText2, 1);
|
||||
}
|
||||
|
||||
private static void TimedText(TextBlock textBlock, int time)
|
||||
{
|
||||
DispatcherTimer Timer = new DispatcherTimer();
|
||||
Timer.Interval = TimeSpan.FromSeconds(time);
|
||||
Timer.Tick += (sender, args) =>
|
||||
{
|
||||
Timer.Stop();
|
||||
textBlock.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
Timer.Start();
|
||||
}
|
||||
|
||||
private async Task UpdateFilter()
|
||||
{
|
||||
Filters.Clear();
|
||||
|
||||
foreach (ContentPresenter superChild in FilterPanel.Children)
|
||||
{
|
||||
ITag tag = MpdTags.Title;
|
||||
FilterOperator op = FilterOperator.None;
|
||||
string value = "";
|
||||
bool isDir = false;
|
||||
|
||||
StackPanel stackPanel = VisualTreeHelper.GetChild(superChild, 0) as StackPanel;
|
||||
foreach (TextBox child in stackPanel.Children.OfType<TextBox>())
|
||||
{
|
||||
if (child.Name == "FilterValue")
|
||||
value = child.Text;
|
||||
}
|
||||
|
||||
foreach (ComboBox child in stackPanel.Children.OfType<ComboBox>())
|
||||
{
|
||||
if (child.Name == "FilterType")
|
||||
{
|
||||
if (child.SelectedItem.ToString() == unison.Resources.Resources.FilterType_Directory)
|
||||
isDir = true;
|
||||
else
|
||||
tag = FilterEquivalence_Type(child.SelectedItem.ToString());
|
||||
}
|
||||
|
||||
if (child.Name == "FilterOperator")
|
||||
op = FilterEquivalence_Operator(child.SelectedItem.ToString());
|
||||
|
||||
if (child.Name == "FilterList" && child.Visibility == Visibility.Visible)
|
||||
value = child.SelectedItem.ToString();
|
||||
}
|
||||
|
||||
if (value != "")
|
||||
{
|
||||
if (!isDir)
|
||||
Filters.Add(new FilterTag(tag, value, op));
|
||||
else
|
||||
Filters.Add(new FilterBase(value, FilterOperator.None));
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _shuffle.GetSongsFromFilter(Filters, _mpd._cancelCommand.Token);
|
||||
});
|
||||
SongFilterPanel.Visibility = Visibility.Visible;
|
||||
SongFilterNumber.Text = _shuffle.SongList.Count.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void QueryFilterHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
UpdateFilter_Clicked(null, null);
|
||||
}
|
||||
|
||||
private void QueueValidationTextBox(object sender, TextCompositionEventArgs e)
|
||||
{
|
||||
Regex regex = new Regex("[^0-9]+");
|
||||
e.Handled = regex.IsMatch(e.Text);
|
||||
}
|
||||
|
||||
private void QueueValidationNumber()
|
||||
{
|
||||
int Number;
|
||||
try
|
||||
{
|
||||
Number = int.Parse(SongNumber.Text);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number < 1)
|
||||
SongNumber.Text = "1";
|
||||
if (IsFilterEmpty())
|
||||
{
|
||||
if (Number > 100)
|
||||
SongNumber.Text = "100";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Number > 1000)
|
||||
SongNumber.Text = "1000";
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddToQueue()
|
||||
{
|
||||
if (_mpd.GetStats() == null)
|
||||
return;
|
||||
|
||||
await UpdateFilter();
|
||||
QueueValidationNumber();
|
||||
|
||||
NumberAddedSongs.Text = SongNumber.Text;
|
||||
SearchStatus.Visibility = Visibility.Visible;
|
||||
|
||||
int Num = int.Parse(SongNumber.Text);
|
||||
await AddToQueue_Internal(Num);
|
||||
|
||||
SearchStatus2.Visibility = Visibility.Visible;
|
||||
|
||||
TimedText(SearchStatus, 2);
|
||||
TimedText(SearchStatus2, 2);
|
||||
}
|
||||
|
||||
private async Task AddToQueue_Internal(int Num)
|
||||
{
|
||||
if (IsFilterEmpty())
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _shuffle.AddToQueueRandom(Num, _mpd._cancelCommand.Token);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _shuffle.AddToQueueFilter(Num, _mpd._cancelCommand.Token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToQueueHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
AddToQueue();
|
||||
}
|
||||
|
||||
private void AddToQueue_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddToQueue();
|
||||
}
|
||||
|
||||
public void ClearQueue_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mpd.ClearQueue();
|
||||
}
|
||||
|
||||
public void StartQueue_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mpd.PlayCommand();
|
||||
}
|
||||
|
||||
public bool GetContinuous()
|
||||
{
|
||||
return _continuous;
|
||||
}
|
||||
|
||||
public async Task HandleContinuous()
|
||||
{
|
||||
if (!_continuous)
|
||||
return;
|
||||
|
||||
int PlaylistLength = _mpd.GetStatus().PlaylistLength;
|
||||
int Num = 50 - PlaylistLength;
|
||||
|
||||
if (PlaylistLength > 25)
|
||||
return;
|
||||
|
||||
await UpdateFilter();
|
||||
await AddToQueue_Internal(Num);
|
||||
}
|
||||
|
||||
private async void ContinuousShuffle_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ContinuousShuffle.IsChecked == true)
|
||||
_continuous = true;
|
||||
else
|
||||
_continuous = false;
|
||||
|
||||
if (_mpd.GetStatus().PlaylistLength < 10)
|
||||
await HandleContinuous();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void InitHwnd()
|
||||
{
|
||||
WindowInteropHelper helper = new(this);
|
||||
helper.EnsureHandle();
|
||||
}
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
Properties.Settings.Default.ShuffleWindowTop = Top;
|
||||
Properties.Settings.Default.ShuffleWindowLeft = Left;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,47 +2,53 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
|
||||
xmlns:properties="clr-namespace:unison.Resources"
|
||||
xmlns:local="clr-namespace:unison">
|
||||
|
||||
<ContextMenu x:Shared="false" x:Key="SystrayMenu">
|
||||
<MenuItem IsEnabled="False">
|
||||
<MenuItem.Icon>
|
||||
<Image Source="/images/icon-full.ico" Width="16" Height="16"/>
|
||||
<Image Source="/Resources/icon-full.ico" Width="16" Height="16"/>
|
||||
</MenuItem.Icon>
|
||||
<MenuItem.Header>
|
||||
<TextBlock Text="{Binding GetAppText}" />
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="Show window" Command="{Binding ShowWindowCommand}">
|
||||
<MenuItem Header="{x:Static properties:Resources.ShowWindow}" Command="{Binding ShowWindowCommand}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="▶️" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Name="test_snapcast_item" Header="{Binding SnapcastText}" Command="{Binding Snapcast}">
|
||||
<MenuItem Header="{Binding SnapcastText}" Command="{Binding Snapcast}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="🔊" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<!--<MenuItem Header="Shuffle" Command="{Binding Shuffle}">
|
||||
<MenuItem Header="{x:Static properties:Resources.Radios}" Command="{Binding Radios}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="📻" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Shuffle" Command="{Binding Shuffle}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="🔀" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>-->
|
||||
<MenuItem Header="Settings" Command="{Binding Settings}">
|
||||
</MenuItem>
|
||||
<MenuItem Header="{x:Static properties:Resources.Settings}" Command="{Binding Settings}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="🛠️" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}">
|
||||
<MenuItem Header="{x:Static properties:Resources.Exit}" Command="{Binding ExitApplicationCommand}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="❌" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
|
||||
<tb:TaskbarIcon x:Key="SystrayTaskbar" IconSource="/images/icon-mini.ico" ToolTipText="{Binding GetAppText}" DoubleClickCommand="{Binding ShowWindowCommand}" ContextMenu="{StaticResource SystrayMenu}">
|
||||
<tb:TaskbarIcon x:Key="SystrayTaskbar" IconSource="/Resources/icon-mini.ico" ToolTipText="{Binding GetAppText}" DoubleClickCommand="{Binding ShowWindowCommand}" ContextMenu="{StaticResource SystrayMenu}">
|
||||
<tb:TaskbarIcon.DataContext>
|
||||
<local:SystrayViewModel />
|
||||
</tb:TaskbarIcon.DataContext>
|
||||
|
@ -3,11 +3,14 @@ using System.Windows.Input;
|
||||
using System.Reflection;
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public class SystrayViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public static string GetAppText => "unison v" + Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
|
||||
|
||||
public static ICommand ShowWindowCommand => new DelegateCommand
|
||||
@ -26,10 +29,7 @@ namespace unison
|
||||
|
||||
public static ICommand ExitApplicationCommand => new DelegateCommand
|
||||
{
|
||||
CommandAction = () =>
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
},
|
||||
CommandAction = () => Application.Current.Shutdown(),
|
||||
CanExecuteFunc = () => true
|
||||
};
|
||||
|
||||
@ -38,7 +38,7 @@ namespace unison
|
||||
get
|
||||
{
|
||||
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
|
||||
return snapcast.Started ? "Stop Snapcast" : "Start Snapcast";
|
||||
return snapcast.HasStarted ? unison.Resources.Resources.StopSnapcast : unison.Resources.Resources.StartSnapcast;
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,30 +50,51 @@ namespace unison
|
||||
{
|
||||
CommandAction = () =>
|
||||
{
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() => OnPropertyChanged("SnapcastText")));
|
||||
((MainWindow)Application.Current.MainWindow).Snapcast_Clicked(null, null);
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() => OnPropertyChanged(SnapcastText)));
|
||||
|
||||
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
|
||||
snapcast.LaunchOrExit();
|
||||
},
|
||||
CanExecuteFunc = () => true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand Settings
|
||||
public static ICommand Radios
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DelegateCommand
|
||||
{
|
||||
CommandAction = () =>
|
||||
{
|
||||
((MainWindow)Application.Current.MainWindow).Settings_Clicked(null, null);
|
||||
},
|
||||
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Radios_Clicked(null, null),
|
||||
CanExecuteFunc = () => true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public static ICommand Shuffle
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DelegateCommand
|
||||
{
|
||||
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Shuffle_Clicked(null, null),
|
||||
CanExecuteFunc = () => true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static ICommand Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DelegateCommand
|
||||
{
|
||||
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Settings_Clicked(null, null),
|
||||
CanExecuteFunc = () => true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
|
Before Width: | Height: | Size: 14 KiB |
BIN
snapcast_0.31/FLAC.dll
Normal file
BIN
snapcast_0.31/opus.dll
Normal file
BIN
snapcast_0.31/snapclient.exe
Normal file
BIN
snapcast_0.31/vc_redist.x64.exe
Normal file
109
unison.csproj
@ -2,33 +2,29 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationIcon>images\icon-full.ico</ApplicationIcon>
|
||||
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
|
||||
<Win32Resource></Win32Resource>
|
||||
<StartupObject>unison.App</StartupObject>
|
||||
<Version>0.0.1</Version>
|
||||
<Version>1.5</Version>
|
||||
<Company />
|
||||
<Authors>Théo Marchal</Authors>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://git.n700.ovh/keb/unison</PackageProjectUrl>
|
||||
<RepositoryUrl>https://git.n700.ovh/keb/unison</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/ZetaKebab/unison</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/ZetaKebab/unison</RepositoryUrl>
|
||||
<Copyright>Théo Marchal</Copyright>
|
||||
<PackageIconUrl />
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="images\icon-full.ico" />
|
||||
<None Remove="images\icon-mini.ico" />
|
||||
<None Remove="images\nocover.png" />
|
||||
<None Remove="Resources\icon-full.ico" />
|
||||
<None Remove="Resources\icon-mini.ico" />
|
||||
<None Remove="Resources\nocover.png" />
|
||||
<None Remove="LICENSE" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\FLAC.dll" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\ogg.dll" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\opus.dll" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\README.txt" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\snapclient.exe" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\soxr.dll" />
|
||||
<None Remove="snapclient_0.25.0-1_win64\vorbis.dll" />
|
||||
<None Remove="Resources\nothing.png" />
|
||||
<None Remove="Resources\radio.png" />
|
||||
<None Include="LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
@ -36,48 +32,32 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="images\icon-full.ico">
|
||||
<Resource Include="Resources\icon-full.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="images\icon-mini.ico">
|
||||
<Resource Include="Resources\icon-mini.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="images\nocover.png">
|
||||
<Resource Include="Resources\nocover.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="Resources\nothing.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="Resources\radio.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Content Include="LICENSE">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\README.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\FLAC.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\ogg.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\opus.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\snapclient.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\soxr.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="snapclient_0.25.0-1_win64\vorbis.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Emoji.Wpf" Version="0.3.3" />
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MpcNET\MpcNET.csproj" />
|
||||
<PackageReference Include="Autoupdater.NET.Official" Version="1.9.2" />
|
||||
<PackageReference Include="Emoji.Wpf" Version="0.3.4" />
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
|
||||
<PackageReference Include="RadioBrowser" Version="0.7.0" />
|
||||
<PackageReference Include="MpcNET" Version="1.6.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -86,6 +66,24 @@
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Resources\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources\Resources.es-ES.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resources\Resources.fr-FR.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resources\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -93,6 +91,27 @@
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\FLAC.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\ogg.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\opus.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\README.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\snapclient.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\soxr.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="snapcast_0.31\vorbis.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
11
unison.sln
@ -4,11 +4,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
VisualStudioVersion = 16.0.31515.178
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unison", "unison.csproj", "{489048C4-3FCA-4573-B34C-943D03F94D04}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19} = {230556C6-5AC3-4FD8-8947-C9ABF1416D19}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MpcNET", "..\MpcNET\MpcNET.csproj", "{230556C6-5AC3-4FD8-8947-C9ABF1416D19}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -23,12 +18,6 @@ Global
|
||||
{489048C4-3FCA-4573-B34C-943D03F94D04}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{489048C4-3FCA-4573-B34C-943D03F94D04}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{489048C4-3FCA-4573-B34C-943D03F94D04}.Release-Stable|Any CPU.Build.0 = Release|Any CPU
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU
|
||||
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|