23 Commits

Author SHA1 Message Date
62a3220f7f Share current song by double click 2021-10-04 20:01:50 +02:00
6e4ed82211 Mute shortcut 2021-10-04 19:20:41 +02:00
7b2a7bae21 Focus style on Radios button 2021-10-04 19:14:47 +02:00
72d751db71 Allow enter key in settings textbox to connect 2021-10-04 19:13:25 +02:00
e4b63073d8 Update readme and screenshots for radios feature 2021-10-04 14:07:28 +02:00
d49f3ab030 Forgot a resource file 2021-10-04 14:07:12 +02:00
62835065c0 Add Radios to the systray 2021-10-04 14:06:20 +02:00
196b93c7f3 Fix gray line in the middle of the radio datagrid 2021-10-04 13:29:55 +02:00
f9a14ee3c0 Remove last parasite line in case of long list 2021-10-03 20:08:10 +02:00
7aafa935e1 Fix mpd bug execution with radios 2021-10-03 17:20:14 +02:00
2960afd9bd Radio text in resources and translation 2021-10-03 15:39:39 +02:00
23098e0ebb Progress on radio implementation 2021-10-03 13:57:45 +02:00
32d3610b07 Progress on radio implementation 2021-10-03 13:54:54 +02:00
fbb65a039a First POC for a radio browser 2021-10-01 02:24:18 +02:00
4d180871ca Readme updated 2021-09-03 01:25:56 +02:00
b87bff54c4 Little code organization tweaks 2021-09-03 00:21:15 +02:00
be8cef35d3 Resources file management, french translation and a bit of design 2021-09-02 19:47:55 +02:00
c568a957f7 MPD handles disconnection and reconnection 2021-09-02 00:14:28 +02:00
2c8696155a Handle no title for song and stop state 2021-09-01 10:21:52 +02:00
48b5bc5d28 Pass of code standardisation 2021-09-01 02:35:37 +02:00
984d2056de Button border thickness declared in XAML and not in code 2021-08-31 02:25:42 +02:00
86a652c170 Improvement on seek time slider 2021-08-31 01:53:52 +02:00
0aa80ed8b5 Option to show Snapcast window 2021-08-31 01:42:00 +02:00
28 changed files with 2146 additions and 557 deletions

View File

@ -8,6 +8,7 @@
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/Systray.xaml" /> <ResourceDictionary Source="Views/Systray.xaml" />
<ResourceDictionary Source="Resources/Resources.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}"> <Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}">

View File

@ -1,39 +1,43 @@
using System.Windows; using System.Globalization;
using System.Windows;
using Hardcodet.Wpf.TaskbarNotification; using Hardcodet.Wpf.TaskbarNotification;
namespace unison namespace unison
{ {
public partial class App : Application public partial class App : Application
{ {
private TaskbarIcon Systray; private TaskbarIcon _systray;
private HotkeyHandler Hotkeys; private HotkeyHandler _hotkeys;
private SnapcastHandler Snapcast; private SnapcastHandler _snapcast;
private MPDHandler MPD; private MPDHandler _mpd;
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
//debug language
//unison.Resources.Resources.Culture = CultureInfo.GetCultureInfo("fr-FR");
base.OnStartup(e); base.OnStartup(e);
MPD = new MPDHandler(); _mpd = new MPDHandler();
Current.Properties["mpd"] = MPD; Current.Properties["mpd"] = _mpd;
Hotkeys = new HotkeyHandler(); _hotkeys = new HotkeyHandler();
Current.Properties["hotkeys"] = Hotkeys; Current.Properties["hotkeys"] = _hotkeys;
Snapcast = new SnapcastHandler(); _snapcast = new SnapcastHandler();
Current.Properties["snapcast"] = Snapcast; Current.Properties["snapcast"] = _snapcast;
Current.MainWindow = new MainWindow(); Current.MainWindow = new MainWindow();
Systray = (TaskbarIcon)FindResource("SystrayTaskbar"); _systray = (TaskbarIcon)FindResource("SystrayTaskbar");
Current.Properties["systray"] = Systray; Current.Properties["systray"] = _systray;
} }
protected override void OnExit(ExitEventArgs e) protected override void OnExit(ExitEventArgs e)
{ {
Systray.Dispose(); _systray.Dispose();
Snapcast.Stop(); _snapcast.LaunchOrExit(true);
Hotkeys.RemoveHotKeys(); _hotkeys.RemoveHotKeys();
base.OnExit(e); base.OnExit(e);
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
@ -29,16 +28,18 @@ namespace unison
private const uint VK_MEDIA_PLAY_PAUSE = 0xB3; private const uint VK_MEDIA_PLAY_PAUSE = 0xB3;
private const uint VK_VOLUME_UP = 0xAF; private const uint VK_VOLUME_UP = 0xAF;
private const uint VK_VOLUME_DOWN = 0xAE; private const uint VK_VOLUME_DOWN = 0xAE;
private const uint VK_VOLUME_MUTE = 0xAD;
private const uint VK_ENTER = 0x0D; private const uint VK_ENTER = 0x0D;
private MainWindow _appWindow;
private readonly MPDHandler _mpd;
private IntPtr _windowHandle; private IntPtr _windowHandle;
private HwndSource _source; private HwndSource _source;
private readonly MPDHandler mpd;
public HotkeyHandler() public HotkeyHandler()
{ {
mpd = (MPDHandler)Application.Current.Properties["mpd"]; _mpd = (MPDHandler)Application.Current.Properties["mpd"];
} }
public void Activate(Window win) public void Activate(Window win)
@ -54,6 +55,7 @@ namespace unison
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_MEDIA_PLAY_PAUSE); 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_UP);
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_VOLUME_DOWN); RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_VOLUME_DOWN);
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL, VK_VOLUME_MUTE);
RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL | MOD_ALT, VK_ENTER); RegisterHotKey(_windowHandle, HOTKEY_ID, MOD_CONTROL | MOD_ALT, VK_ENTER);
} }
} }
@ -65,49 +67,48 @@ namespace unison
if (msg == WM_HOTKEY && wParam.ToInt32() == HOTKEY_ID) if (msg == WM_HOTKEY && wParam.ToInt32() == HOTKEY_ID)
{ {
uint vkey = ((uint)lParam >> 16) & 0xFFFF; uint vkey = ((uint)lParam >> 16) & 0xFFFF;
MainWindow AppWindow = (MainWindow)Application.Current.MainWindow;
switch (vkey) switch (vkey)
{ {
case VK_MEDIA_NEXT_TRACK: case VK_MEDIA_NEXT_TRACK:
mpd.Next(); _mpd.Next();
break; break;
case VK_MEDIA_PREV_TRACK: case VK_MEDIA_PREV_TRACK:
mpd.Prev(); _mpd.Prev();
break; break;
case VK_VOLUME_DOWN: case VK_VOLUME_DOWN:
mpd._currentVolume -= Properties.Settings.Default.volume_offset; _mpd.VolumeDown();
if (mpd._currentVolume < 0)
mpd._currentVolume = 0;
mpd.SetVolume(mpd._currentVolume);
break; break;
case VK_VOLUME_UP: case VK_VOLUME_UP:
mpd._currentVolume += Properties.Settings.Default.volume_offset; _mpd.VolumeUp();
if (mpd._currentVolume > 100) break;
mpd._currentVolume = 100; case VK_VOLUME_MUTE:
mpd.SetVolume(mpd._currentVolume); _mpd.VolumeMute();
break; break;
case VK_MEDIA_PLAY_PAUSE: case VK_MEDIA_PLAY_PAUSE:
mpd.PlayPause(); _mpd.PlayPause();
break; break;
case VK_ENTER: case VK_ENTER:
if (AppWindow.WindowState == WindowState.Minimized) if (_appWindow == null)
_appWindow = (MainWindow)Application.Current.MainWindow;
if (_appWindow.WindowState == WindowState.Minimized)
{ {
AppWindow.Show(); _appWindow.Show();
AppWindow.Activate(); _appWindow.Activate();
AppWindow.WindowState = WindowState.Normal; _appWindow.WindowState = WindowState.Normal;
} }
else else
{ {
if (AppWindow.IsActive) if (_appWindow.IsActive)
{ {
AppWindow.Hide(); _appWindow.Hide();
AppWindow.WindowState = WindowState.Minimized; _appWindow.WindowState = WindowState.Minimized;
} }
else // not minimized but not in front else // not minimized but not in front
{ {
AppWindow.Show(); _appWindow.Show();
AppWindow.Activate(); _appWindow.Activate();
AppWindow.WindowState = WindowState.Normal; _appWindow.WindowState = WindowState.Normal;
} }
} }
break; break;

View File

@ -12,6 +12,7 @@ using System.Windows.Threading;
using MpcNET; using MpcNET;
using MpcNET.Commands.Database; using MpcNET.Commands.Database;
using MpcNET.Commands.Playback; using MpcNET.Commands.Playback;
using MpcNET.Commands.Queue;
using MpcNET.Commands.Reflection; using MpcNET.Commands.Reflection;
using MpcNET.Commands.Status; using MpcNET.Commands.Status;
using MpcNET.Message; using MpcNET.Message;
@ -21,53 +22,45 @@ namespace unison
{ {
public class MPDHandler public class MPDHandler
{ {
public bool _connected; private bool _connected;
public string _version; public string _version;
public int _currentVolume; private int _currentVolume;
public bool _currentRandom; private int _previousVolume;
public bool _currentRepeat; private bool _currentRandom;
public bool _currentSingle; private bool _currentRepeat;
public bool _currentConsume; private bool _currentSingle;
public double _currentTime; private bool _currentConsume;
public double _totalTime; private double _currentTime;
BitmapFrame _cover; private double _totalTime;
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 MpdStatus _currentStatus;
private IMpdFile _currentSong;
private BitmapFrame _cover;
private readonly System.Timers.Timer _elapsedTimer; private readonly System.Timers.Timer _elapsedTimer;
private async void ElapsedTimer(object sender, System.Timers.ElapsedEventArgs e) private DispatcherTimer _retryTimer;
{
if ((_currentTime < _totalTime) && (CurrentStatus.State == MpdState.Play)) bool _isUpdatingStatus = false;
{ bool _isUpdatingSong = false;
_currentTime += 0.5;
await Task.Delay(5); private event EventHandler ConnectionChanged;
} private event EventHandler StatusChanged;
else private event EventHandler SongChanged;
{ private event EventHandler CoverChanged;
_elapsedTimer.Stop();
}
}
private MpcConnection _connection; private MpcConnection _connection;
private MpcConnection _commandConnection; private MpcConnection _commandConnection;
private IPEndPoint _mpdEndpoint; private IPEndPoint _mpdEndpoint;
private CancellationTokenSource cancelToken; private CancellationTokenSource cancelToken;
public MPDHandler() public MPDHandler()
{ {
cancelToken = new CancellationTokenSource(); cancelToken = new CancellationTokenSource();
Initialize(); Initialize(null, null);
_retryTimer = new DispatcherTimer();
_retryTimer.Interval = TimeSpan.FromSeconds(5);
_retryTimer.Tick += Initialize;
_elapsedTimer = new System.Timers.Timer(500); _elapsedTimer = new System.Timers.Timer(500);
_elapsedTimer.Elapsed += new System.Timers.ElapsedEventHandler(ElapsedTimer); _elapsedTimer.Elapsed += new System.Timers.ElapsedEventHandler(ElapsedTimer);
@ -78,8 +71,21 @@ namespace unison
CoverChanged += OnCoverChanged; 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(() => Application.Current.Dispatcher.Invoke(() =>
{ {
MainWindow MainWin = (MainWindow)Application.Current.MainWindow; MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
@ -87,8 +93,7 @@ namespace unison
SnapcastHandler Snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"]; SnapcastHandler Snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
Snapcast.OnConnectionChanged(sender, e); Snapcast.OnConnectionChanged(sender, e);
});
}, DispatcherPriority.ContextIdle);
} }
static void OnStatusChanged(object sender, EventArgs e) static void OnStatusChanged(object sender, EventArgs e)
@ -97,7 +102,7 @@ namespace unison
{ {
MainWindow MainWin = (MainWindow)Application.Current.MainWindow; MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnStatusChanged(sender, e); MainWin.OnStatusChanged(sender, e);
}, DispatcherPriority.ContextIdle); });
} }
static void OnSongChanged(object sender, EventArgs e) static void OnSongChanged(object sender, EventArgs e)
@ -106,7 +111,7 @@ namespace unison
{ {
MainWindow MainWin = (MainWindow)Application.Current.MainWindow; MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnSongChanged(sender, e); MainWin.OnSongChanged(sender, e);
}, DispatcherPriority.ContextIdle); });
} }
static void OnCoverChanged(object sender, EventArgs e) static void OnCoverChanged(object sender, EventArgs e)
@ -115,194 +120,30 @@ namespace unison
{ {
MainWindow MainWin = (MainWindow)Application.Current.MainWindow; MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnCoverChanged(sender, e); MainWin.OnCoverChanged(sender, e);
}, DispatcherPriority.ContextIdle); });
} }
private void Initialize() public void SendCommand<T>(IMpcCommand<T> command)
{
Connect();
}
public async void Connect()
{
var token = cancelToken.Token;
try
{
_connection = await ConnectInternal(token);
_commandConnection = await ConnectInternal(token);
}
catch(MpcNET.Exceptions.MpcConnectException exception)
{
Trace.WriteLine("exception: " + exception);
}
if (_connection.IsConnected)
{
_connected = true;
_version = _connection.Version;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
await UpdateStatusAsync();
await UpdateSongAsync();
Loop(token);
}
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
{
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress);
_mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port);
MpcConnection connection = new MpcConnection(_mpdEndpoint);
await connection.ConnectAsync(token);
if (!string.IsNullOrEmpty(Properties.Settings.Default.mpd_password))
{
IMpdMessage<string> result = await connection.SendAsync(new PasswordCommand(Properties.Settings.Default.mpd_password));
if (!result.IsResponseValid)
{
string mpdError = result.Response?.Result?.MpdError;
}
}
return connection;
}
private void Loop(CancellationToken token)
{ {
Task.Run(async () => Task.Run(async () =>
{ {
while (true) await SafelySendCommandAsync(command);
{ });
try
{
if (token.IsCancellationRequested || _connection == null)
break;
Trace.WriteLine("loop");
var idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options"));
if (idleChanges.IsResponseValid)
await HandleIdleResponseAsync(idleChanges.Response.Content);
else
throw new Exception(idleChanges.Response?.Content);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine($"Error in Idle connection thread: {e.Message}");
}
}
}).ConfigureAwait(false);
}
private async Task HandleIdleResponseAsync(string subsystems)
{
try
{
if (subsystems.Contains("player") || subsystems.Contains("mixer") || subsystems.Contains("output") || subsystems.Contains("options"))
{
await UpdateStatusAsync();
if (subsystems.Contains("player"))
{
await UpdateSongAsync();
}
}
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
}
private async Task UpdateStatusCommand()
{
if (_commandConnection == null) return;
try
{
MpdStatus response = await SafelySendCommandAsync(new StatusCommand());
if (response != null)
{
CurrentStatus = response;
UpdateStatus();
}
else
throw new Exception();
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
Connect();
}
}
bool _isUpdatingStatus = false;
private async Task UpdateStatusAsync()
{
if (_connection == null) return;
if (_isUpdatingStatus) return;
_isUpdatingStatus = true;
try
{
IMpdMessage<MpdStatus> response = await _connection.SendAsync(new StatusCommand());
if (response != null && response.IsResponseValid)
{
CurrentStatus = response.Response.Content;
UpdateStatus();
}
else
throw new Exception();
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
Connect();
}
_isUpdatingStatus = false;
}
bool _isUpdatingSong = false;
private async Task UpdateSongAsync()
{
if (_connection == null) return;
if (_isUpdatingSong) return;
_isUpdatingSong = true;
try
{
IMpdMessage<IMpdFile> response = await _connection.SendAsync(new CurrentSongCommand());
if (response != null && response.IsResponseValid)
{
CurrentSong = response.Response.Content;
UpdateSong();
}
else
{
throw new Exception();
}
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
Connect();
}
_isUpdatingSong = false;
} }
public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command) public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command)
{ {
if (_commandConnection == null)
{
Trace.WriteLine("[SafelySendCommandAsync] no command connection");
return default(T);
}
try try
{ {
var response = await _commandConnection.SendAsync(command); IMpdMessage<T> response = await _commandConnection.SendAsync(command);
if (!response.IsResponseValid) 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; var mpdError = response.Response?.Result?.MpdError;
if (mpdError != null && mpdError != "") if (mpdError != null && mpdError != "")
throw new Exception(mpdError); throw new Exception(mpdError);
@ -320,12 +161,180 @@ namespace unison
return default(T); return default(T);
} }
private async void GetAlbumBitmap(string path, CancellationToken token = default) private void Initialize(object sender, EventArgs e)
{
if (!_connected)
Connect();
}
public async void Connect()
{
CancellationToken token = cancelToken.Token;
try
{
_connection = await ConnectInternal(token);
_commandConnection = await ConnectInternal(token);
}
catch(MpcNET.Exceptions.MpcConnectException exception)
{
Trace.WriteLine("exception: " + exception);
}
if (_connection != null && _commandConnection != null)
{
if (_connection.IsConnected && _commandConnection.IsConnected)
{
_connected = true;
_version = _connection.Version;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
}
else
{
ConnectionChanged?.Invoke(this, EventArgs.Empty);
return;
}
await UpdateStatusAsync();
await UpdateSongAsync();
Loop(token);
}
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
{
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress);
_mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port);
MpcConnection connection = new MpcConnection(_mpdEndpoint);
await connection.ConnectAsync(token);
/*if (!string.IsNullOrEmpty(Properties.Settings.Default.mpd_password))
{
IMpdMessage<string> result = await connection.SendAsync(new PasswordCommand(Properties.Settings.Default.mpd_password));
if (!result.IsResponseValid)
{
string mpdError = result.Response?.Result?.MpdError;
Trace.WriteLine(mpdError);
}
}*/
return connection;
}
private void Disconnected()
{
_connected = false;
_connection = null;
_commandConnection = null;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
private void Loop(CancellationToken token)
{
Task.Run(async () =>
{
while (true)
{
try
{
if (token.IsCancellationRequested || _connection == null)
break;
var idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options"));
if (idleChanges.IsResponseValid)
await HandleIdleResponseAsync(idleChanges.Response.Content);
else
throw new Exception(idleChanges.Response?.Content);
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
Disconnected();
}
}
}).ConfigureAwait(false);
}
private async Task HandleIdleResponseAsync(string subsystems)
{
try
{
if (subsystems.Contains("player") || subsystems.Contains("mixer") || subsystems.Contains("output") || subsystems.Contains("options"))
{
await UpdateStatusAsync();
if (subsystems.Contains("player"))
await UpdateSongAsync();
}
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
}
private async Task UpdateStatusAsync()
{
if (_connection == null || _isUpdatingStatus)
return;
_isUpdatingStatus = true;
try
{
IMpdMessage<MpdStatus> response = await _connection.SendAsync(new StatusCommand());
if (response != null && response.IsResponseValid)
{
_currentStatus = response.Response.Content;
UpdateStatus();
}
else
throw new Exception();
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
_isUpdatingStatus = false;
}
private async Task UpdateSongAsync()
{
if (_connection == null || _isUpdatingSong)
return;
_isUpdatingSong = true;
try
{
IMpdMessage<IMpdFile> response = await _connection.SendAsync(new CurrentSongCommand());
if (response != null && response.IsResponseValid)
{
_currentSong = response.Response.Content;
UpdateSong();
}
else
throw new Exception();
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
}
_isUpdatingSong = false;
}
private async void GetAlbumCover(string path, CancellationToken token = default)
{ {
List<byte> data = new List<byte>(); List<byte> data = new List<byte>();
try try
{ {
if (_connection == null) // We got cancelled if (_connection == null)
return; return;
long totalBinarySize = 9999; long totalBinarySize = 9999;
@ -334,52 +343,69 @@ namespace unison
do do
{ {
var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize)); var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
if (!albumReq.IsResponseValid) break; if (!albumReq.IsResponseValid)
break;
var response = albumReq.Response.Content; 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. if (response.Binary == 0)
break;
totalBinarySize = response.Size; totalBinarySize = response.Size;
currentSize += response.Binary; currentSize += response.Binary;
data.AddRange(response.Data); data.AddRange(response.Data);
//Debug.WriteLine($"Downloading albumart: {currentSize}/{totalBinarySize}");
} while (currentSize < totalBinarySize && !token.IsCancellationRequested); } while (currentSize < totalBinarySize && !token.IsCancellationRequested);
} }
catch (Exception e) catch (Exception e)
{ {
Debug.WriteLine("Exception caught while getting albumart: " + e); Trace.WriteLine("Exception caught while getting albumart: " + e);
return; return;
} }
if (data.Count == 0) if (data.Count == 0)
{
Trace.WriteLine("empty cover");
_cover = null; _cover = null;
}
else else
{ {
using MemoryStream stream = new MemoryStream(data.ToArray()); using MemoryStream stream = new MemoryStream(data.ToArray());
try
{
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); _cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
} }
catch (System.NotSupportedException)
{
_cover = null;
}
}
UpdateCover(); UpdateCover();
} }
public void UpdateSong() public void UpdateStatus()
{ {
if (!_connected) if (!_connected || _currentStatus == null)
return;
if (CurrentSong == null)
return; return;
_currentTime = CurrentStatus.Elapsed.TotalSeconds; _currentRandom = _currentStatus.Random;
_totalTime = CurrentSong.Time; _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) if (!_elapsedTimer.Enabled)
_elapsedTimer.Start(); _elapsedTimer.Start();
SongChanged?.Invoke(this, EventArgs.Empty); SongChanged?.Invoke(this, EventArgs.Empty);
string uri = Regex.Escape(CurrentSong.Path); string uri = Regex.Escape(_currentSong.Path);
GetAlbumBitmap(uri); GetAlbumCover(uri);
} }
public void UpdateCover() public void UpdateCover()
@ -387,40 +413,71 @@ namespace unison
CoverChanged?.Invoke(this, EventArgs.Empty); CoverChanged?.Invoke(this, EventArgs.Empty);
} }
public void UpdateStatus() public IMpdFile GetCurrentSong() => _currentSong;
{ public MpdStatus GetStatus() => _currentStatus;
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);
}
public IMpdFile GetCurrentSong() => CurrentSong;
public MpdStatus GetStatus() => CurrentStatus;
public BitmapFrame GetCover() => _cover; public BitmapFrame GetCover() => _cover;
public string GetVersion() => _version; public string GetVersion() => _version;
public double GetCurrentTime() => _currentTime;
public async void Prev() => await SafelySendCommandAsync(new PreviousCommand()); public bool IsConnected() => _connected;
public async void Next() => await SafelySendCommandAsync(new NextCommand()); public bool IsPlaying() => _currentStatus?.State == MpdState.Play;
public async void PlayPause() =>await SafelySendCommandAsync(new PauseResumeCommand());
public async void Random() => await SafelySendCommandAsync(new RandomCommand(!_currentRandom)); public void Prev() => SendCommand(new PreviousCommand());
public async void Repeat() => await SafelySendCommandAsync(new RepeatCommand(!_currentRepeat)); public void Next() => SendCommand(new NextCommand());
public async void Single() => await SafelySendCommandAsync(new SingleCommand(!_currentSingle)); public void PlayPause() => SendCommand(new PauseResumeCommand());
public async void Consume() => await SafelySendCommandAsync(new ConsumeCommand(!_currentConsume));
public async void SetVolume(int value) => await SafelySendCommandAsync(new SetVolumeCommand((byte)value)); public void Random() => SendCommand(new RandomCommand(!_currentRandom));
public async void SetTime(double value) => await SafelySendCommandAsync(new SeekCurCommand(value)); public void Repeat() => SendCommand(new RepeatCommand(!_currentRepeat));
public void Single() => SendCommand(new SingleCommand(!_currentSingle));
public void Consume() => SendCommand(new ConsumeCommand(!_currentConsume));
public bool IsPlaying() => CurrentStatus?.State == MpdState.Play; 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 void VolumeDown()
{
_currentVolume -= Properties.Settings.Default.volume_offset;
if (_currentVolume < 0)
_currentVolume = 0;
SetVolume(_currentVolume);
}
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)
{
Debug.WriteLine("AddCommand path: " + 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);
}
} }
} }

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Windows; using System.Windows;
using System.Windows.Threading;
using Hardcodet.Wpf.TaskbarNotification; using Hardcodet.Wpf.TaskbarNotification;
namespace unison namespace unison
@ -9,19 +8,15 @@ namespace unison
public class SnapcastHandler public class SnapcastHandler
{ {
private readonly Process _snapcast = new(); private readonly Process _snapcast = new();
public bool Started { get; private set; } public bool HasStarted { get; private set; }
public SnapcastHandler()
{
}
public void OnConnectionChanged(object sender, EventArgs e) public void OnConnectionChanged(object sender, EventArgs e)
{ {
if (Properties.Settings.Default.snapcast_startup) if (Properties.Settings.Default.snapcast_startup)
{ {
var mpd = (MPDHandler)Application.Current.Properties["mpd"]; var mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd._connected) if (mpd.IsConnected())
Start(); LaunchOrExit();
} }
} }
@ -35,46 +30,39 @@ namespace unison
{ {
MainWindow MainWin = (MainWindow)Application.Current.MainWindow; MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnSnapcastChanged(); MainWin.OnSnapcastChanged();
}, DispatcherPriority.ContextIdle); });
} }
public void Start() public void LaunchOrExit(bool ForceExit = false)
{ {
if (!Started) if (!HasStarted && !ForceExit)
{ {
_snapcast.StartInfo.FileName = Properties.Settings.Default.snapcast_path + @"\snapclient.exe"; _snapcast.StartInfo.FileName = Properties.Settings.Default.snapcast_path + @"\snapclient.exe";
_snapcast.StartInfo.Arguments = $"--host {Properties.Settings.Default.mpd_host}"; _snapcast.StartInfo.Arguments = $"--host {Properties.Settings.Default.mpd_host}";
_snapcast.StartInfo.CreateNoWindow = true; _snapcast.StartInfo.CreateNoWindow = !Properties.Settings.Default.snapcast_window;
try try
{ {
_snapcast.Start(); _snapcast.Start();
} }
catch (Exception err) 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($"[{unison.Resources.Resources.Snapcast_Popup1}]\n" +
$"{unison.Resources.Resources.Snapcast_Popup2} {err.Message}\n\n" +
$"{unison.Resources.Resources.Snapcast_Popup3} {Properties.Settings.Default.snapcast_path}\n" +
$"{unison.Resources.Resources.Snapcast_Popup4}",
"unison", MessageBoxButton.OK, MessageBoxImage.Error); "unison", MessageBoxButton.OK, MessageBoxImage.Error);
Trace.WriteLine(err.Message);
return; return;
} }
Started = true; HasStarted = true;
UpdateInterface();
} }
else else if (HasStarted)
{ {
_snapcast.Kill(); _snapcast.Kill();
Started = false; HasStarted = false;
UpdateInterface();
}
} }
public void Stop() if (!ForceExit)
{
if (Started)
{
_snapcast.Kill();
Started = false;
UpdateInterface(); UpdateInterface();
} }
} }
}
} }

View File

@ -106,5 +106,17 @@ namespace unison.Properties {
this["volume_offset"] = value; this["volume_offset"] = value;
} }
} }
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool snapcast_window {
get {
return ((bool)(this["snapcast_window"]));
}
set {
this["snapcast_window"] = value;
}
}
} }
} }

View File

@ -14,6 +14,9 @@
<Setting Name="snapcast_startup" Type="System.Boolean" Scope="User"> <Setting Name="snapcast_startup" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value> <Value Profile="(Default)">False</Value>
</Setting> </Setting>
<Setting Name="snapcast_window" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="snapcast_path" Type="System.String" Scope="User"> <Setting Name="snapcast_path" Type="System.String" Scope="User">
<Value Profile="(Default)">snapclient_0.25.0-1_win64</Value> <Value Profile="(Default)">snapclient_0.25.0-1_win64</Value>
</Setting> </Setting>

View File

@ -1,3 +1,50 @@
# unison # unison
mpd client ![Main window](Screenshots/screen1.png)
**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 shortcuts
* [Snapcast](https://mjaggard.github.io/snapcast/) integration
* Radio stations
## 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.
![Systray](Screenshots/screen2.png)
### Shortcuts
You can control your music at anytime with the shortcuts. They can of course be used if the window is not visible.
![Settings => shortcuts](Screenshots/screen3.png)
### Snapcast
The main goal of embedding Snapcast is the ability to listen locally to music when I'm not using my main audio system. The computer running unison can then play music easily.
### 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 should be a nice way to discover new music and cultures.
![Radio stations](Screenshots/screen4.png)
## Caveats
### Missing features
* Custom shortcuts.
* MPD passwords: I don't really see the point, but if asked, I will integrate them.
* Any sort of playlist and queue management. I use other software to do it, but I might implement them at some point.
### Wanted features
* A complete shuffle system based on set criteria, aka a smart playlist.
## Translations
unison is translated in English and French. You can contribute if you want!

513
Resources/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,513 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace unison.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("unison.Resources.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Exit.
/// </summary>
public static string Exit {
get {
return ResourceManager.GetString("Exit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Country.
/// </summary>
public static string Radio_Country {
get {
return ResourceManager.GetString("Radio_Country", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loading stations....
/// </summary>
public static string Radio_Loading {
get {
return ResourceManager.GetString("Radio_Loading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string Radio_Name {
get {
return ResourceManager.GetString("Radio_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No stations found!.
/// </summary>
public static string Radio_NotFound {
get {
return ResourceManager.GetString("Radio_NotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
public static string Radio_Reset {
get {
return ResourceManager.GetString("Radio_Reset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>
public static string Radio_Search {
get {
return ResourceManager.GetString("Radio_Search", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search station.
/// </summary>
public static string Radio_SearchStation {
get {
return ResourceManager.GetString("Radio_SearchStation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tags.
/// </summary>
public static string Radio_Tags {
get {
return ResourceManager.GetString("Radio_Tags", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Radios.
/// </summary>
public static string Radios {
get {
return ResourceManager.GetString("Radios", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>
public static string Settings {
get {
return ResourceManager.GetString("Settings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to About.
/// </summary>
public static string Settings_About {
get {
return ResourceManager.GetString("Settings_About", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to unison is a free software. It is built with the following technologies:.
/// </summary>
public static string Settings_AboutInfo {
get {
return ResourceManager.GetString("Settings_AboutInfo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connect.
/// </summary>
public static string Settings_ConnectButton {
get {
return ResourceManager.GetString("Settings_ConnectButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connection.
/// </summary>
public static string Settings_Connection {
get {
return ResourceManager.GetString("Settings_Connection", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connected to MPD.
/// </summary>
public static string Settings_ConnectionStatusConnected {
get {
return ResourceManager.GetString("Settings_ConnectionStatusConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connecting....
/// </summary>
public static string Settings_ConnectionStatusConnecting {
get {
return ResourceManager.GetString("Settings_ConnectionStatusConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not connected..
/// </summary>
public static string Settings_ConnectionStatusOffline {
get {
return ResourceManager.GetString("Settings_ConnectionStatusOffline", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Host.
/// </summary>
public static string Settings_Host {
get {
return ResourceManager.GetString("Settings_Host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to License.
/// </summary>
public static string Settings_License {
get {
return ResourceManager.GetString("Settings_License", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Made by.
/// </summary>
public static string Settings_MadeBy {
get {
return ResourceManager.GetString("Settings_MadeBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;s updated MpcNET.
/// </summary>
public static string Settings_MpcNET {
get {
return ResourceManager.GetString("Settings_MpcNET", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Next track.
/// </summary>
public static string Settings_NextTrack {
get {
return ResourceManager.GetString("Settings_NextTrack", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Play / pause.
/// </summary>
public static string Settings_PlayPause {
get {
return ResourceManager.GetString("Settings_PlayPause", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Port.
/// </summary>
public static string Settings_Port {
get {
return ResourceManager.GetString("Settings_Port", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Previous track.
/// </summary>
public static string Settings_PreviousTrack {
get {
return ResourceManager.GetString("Settings_PreviousTrack", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shortcuts.
/// </summary>
public static string Settings_Shortcuts {
get {
return ResourceManager.GetString("Settings_Shortcuts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show window.
/// </summary>
public static string Settings_ShowWindow {
get {
return ResourceManager.GetString("Settings_ShowWindow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You can change to your own locally installed version of the Snapcast client with an.
/// </summary>
public static string Settings_SnapcastInfo1 {
get {
return ResourceManager.GetString("Settings_SnapcastInfo1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to absolute .
/// </summary>
public static string Settings_SnapcastInfo2 {
get {
return ResourceManager.GetString("Settings_SnapcastInfo2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to path..
/// </summary>
public static string Settings_SnapcastInfo3 {
get {
return ResourceManager.GetString("Settings_SnapcastInfo3", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch at startup.
/// </summary>
public static string Settings_SnapcastLauch {
get {
return ResourceManager.GetString("Settings_SnapcastLauch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Executable path.
/// </summary>
public static string Settings_SnapcastPath {
get {
return ResourceManager.GetString("Settings_SnapcastPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Port.
/// </summary>
public static string Settings_SnapcastPort {
get {
return ResourceManager.GetString("Settings_SnapcastPort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
public static string Settings_SnapcastResetButton {
get {
return ResourceManager.GetString("Settings_SnapcastResetButton", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show Snapcast window.
/// </summary>
public static string Settings_SnapcastWindow {
get {
return ResourceManager.GetString("Settings_SnapcastWindow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Source code freely available.
/// </summary>
public static string Settings_SourceCode1 {
get {
return ResourceManager.GetString("Settings_SourceCode1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to here.
/// </summary>
public static string Settings_SourceCode2 {
get {
return ResourceManager.GetString("Settings_SourceCode2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version:.
/// </summary>
public static string Settings_Version {
get {
return ResourceManager.GetString("Settings_Version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume down.
/// </summary>
public static string Settings_VolumeDown {
get {
return ResourceManager.GetString("Settings_VolumeDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume mute.
/// </summary>
public static string Settings_VolumeMute {
get {
return ResourceManager.GetString("Settings_VolumeMute", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume offset.
/// </summary>
public static string Settings_VolumeOffset {
get {
return ResourceManager.GetString("Settings_VolumeOffset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume up.
/// </summary>
public static string Settings_VolumeUp {
get {
return ResourceManager.GetString("Settings_VolumeUp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show window.
/// </summary>
public static string ShowWindow {
get {
return ResourceManager.GetString("ShowWindow", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Snapcast error.
/// </summary>
public static string Snapcast_Popup1 {
get {
return ResourceManager.GetString("Snapcast_Popup1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid path:.
/// </summary>
public static string Snapcast_Popup2 {
get {
return ResourceManager.GetString("Snapcast_Popup2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Current path:.
/// </summary>
public static string Snapcast_Popup3 {
get {
return ResourceManager.GetString("Snapcast_Popup3", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You can reset it in the settings if needed..
/// </summary>
public static string Snapcast_Popup4 {
get {
return ResourceManager.GetString("Snapcast_Popup4", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Start Snapcast.
/// </summary>
public static string StartSnapcast {
get {
return ResourceManager.GetString("StartSnapcast", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Stop Snapcast.
/// </summary>
public static string StopSnapcast {
get {
return ResourceManager.GetString("StopSnapcast", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,270 @@
<?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=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Exit" xml:space="preserve">
<value>Quitter</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>Aucun 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="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_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_MpcNET" xml:space="preserve">
<value> et sa version de MpcNET</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve">
<value>Piste suivante</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_ShowWindow" xml:space="preserve">
<value>Afficher la fenêtre</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_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="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="StopSnapcast" xml:space="preserve">
<value>Stopper Snapcast</value>
</data>
</root>

270
Resources/Resources.resx Normal file
View File

@ -0,0 +1,270 @@
<?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=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Exit" xml:space="preserve">
<value>Exit</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="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_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_MpcNET" xml:space="preserve">
<value>'s updated MpcNET</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve">
<value>Next track</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_ShowWindow" xml:space="preserve">
<value>Show window</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_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="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="StopSnapcast" xml:space="preserve">
<value>Stop Snapcast</value>
</data>
</root>

15
Resources/Resources.xaml Normal file
View File

@ -0,0 +1,15 @@
<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">
<system:String x:Key="snapcastPath">snapclient_0.25.0-1_win64</system:String>
<system:String x:Key="snapcastPort">1704</system:String>
<system:String x:Key="connectionOk1">&#xf385;</system:String>
<system:String x:Key="connectionOk2">&#xf386;</system:String>
<system:String x:Key="connectionFail">&#xf384;</system:String>
<system:String x:Key="playButton">&#xedb4;</system:String>
<system:String x:Key="pauseButton">&#xedb5;</system:String>
</ResourceDictionary>

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 294 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
Screenshots/screen1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
Screenshots/screen2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
Screenshots/screen3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
Screenshots/screen4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -4,13 +4,20 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf" xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
xmlns:clr="clr-namespace:System;assembly=mscorlib" xmlns:properties="clr-namespace:unison.Resources"
xmlns:local="clr-namespace:unison"
mc:Ignorable="d" mc:Ignorable="d"
Title="unison" Title="unison" Closing="Window_Closing" Icon="/Resources/icon-full.ico" ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
Closing="Window_Closing" Icon="/images/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="650">
<Grid x:Name="TopLayout" Margin="10,0,10,0" VerticalAlignment="Stretch" Width="Auto" Height="Auto"> <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"> <Grid x:Name="Display" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="225,0,0,0" Height="Auto" Width="Auto">
<GroupBox Height="220" VerticalAlignment="Center"> <GroupBox Height="220" VerticalAlignment="Center">
@ -21,14 +28,14 @@
</GroupBox.Header> </GroupBox.Header>
<Grid> <Grid>
<Grid x:Name="CurrentSong" Margin="10,0,10,0" VerticalAlignment="Top" MinHeight="80"> <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="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="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 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 x:Name="SongGenre"/>
<Run> </Run> <Run x:Name="SongInfoDash"> </Run>
<Run x:Name="Format"/> <Run x:Name="SongFormat"/>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -37,7 +44,7 @@
<Grid HorizontalAlignment="Center" VerticalAlignment="Top"> <Grid HorizontalAlignment="Center" VerticalAlignment="Top">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock x:Name="CurrentTime" Text="0:00" TextWrapping="Wrap" HorizontalAlignment="Left"/> <TextBlock x:Name="CurrentTime" Text="0:00" TextWrapping="Wrap" HorizontalAlignment="Left"/>
<Slider Style="{DynamicResource SliderTime}" x:Name="TimeSlider" MinWidth="320" Margin="5,0,5,0" HorizontalAlignment="Center" Maximum="100" PreviewMouseUp="TimeSlider_DragCompleted" FocusVisualStyle="{x:Null}"/> <Slider Style="{DynamicResource SliderTime}" x:Name="TimeSlider" MinWidth="320" Margin="5,0,5,0" HorizontalAlignment="Center" Maximum="100" PreviewMouseUp="TimeSlider_DragCompleted" Thumb.DragStarted="TimeSlider_DragStarted" FocusVisualStyle="{x:Null}"/>
<TextBlock x:Name="EndTime" Text="0:00" TextWrapping="Wrap" Height="18" HorizontalAlignment="Right"/> <TextBlock x:Name="EndTime" Text="0:00" TextWrapping="Wrap" Height="18" HorizontalAlignment="Right"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -54,30 +61,30 @@
</Button> </Button>
</StackPanel> </StackPanel>
<Grid VerticalAlignment="Stretch" Margin="0,18,0,0"> <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="&#xe992;" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="16"/> <TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xe992;" 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}"/> <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="&#xe995;" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="7.5,0,0,0" FontSize="16"/> <TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xe995;" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="7.5,0,0,0" FontSize="16"/>
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="27"> <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}"> <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="&#xe8b1;" Margin="0" Padding="0, 0, 0, 2"/> <TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe8b1;" Margin="0" Padding="0, 0, 0, 2"/>
</Button> </Button>
</Border> </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}"> <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="&#xe8ee;" Padding="0, 0, 0, 2"/> <TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe8ee;" Padding="0, 0, 0, 2"/>
</Button> </Button>
</Border> </Border>
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Bottom" MinHeight="27"> <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}"> <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="&#xe8ed;" Margin="0" Padding="0, 0, 0, 2"/> <TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe8ed;" Margin="0" Padding="0, 0, 0, 2"/>
</Button> </Button>
</Border> </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}"> <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="&#xe75c;" Margin="0" Padding="0, 0, 0, 2"/> <TextBlock FontSize="18" FontFamily="Segoe MDL2 Assets" Text="&#xe75c;" Margin="0" Padding="0, 0, 0, 2"/>
</Button> </Button>
@ -96,20 +103,37 @@
<StackPanel.OpacityMask> <StackPanel.OpacityMask>
<VisualBrush Visual="{Binding ElementName=mask}"/> <VisualBrush Visual="{Binding ElementName=mask}"/>
</StackPanel.OpacityMask> </StackPanel.OpacityMask>
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/images/nocover.png" Visibility="Collapsed" /> <Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/images/nocover.png" /> <Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
<Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40"> <Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
<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"> <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"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/> <emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
<TextBlock x:Name="SnapcastText" Text="Start Snapcast" Margin="5, 0, 0, 0"/> <TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
</StackPanel> </StackPanel>
</Button> </Button>
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Center" TextAlignment="Center" MinWidth="350" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}"/> <Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Margin="5,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
<StackPanel Orientation="Horizontal">
<emoji: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="&#xf385;" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xf386;" 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="&#xf384;" 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"> <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}}"> <!--<Button x:Name="Shuffle" Padding="5, 2" HorizontalAlignment="Right" Margin="0,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@ -120,7 +144,7 @@
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}"> <Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/> <emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>
<TextBlock Text="Settings" Margin="5, 0, 0, 0"/> <TextBlock Text="{x:Static properties:Resources.Settings}" Margin="5, 0, 0, 0"/>
</StackPanel> </StackPanel>
</Button> </Button>
</StackPanel> </StackPanel>

View File

@ -5,52 +5,189 @@ using System.Windows.Controls;
using System.Windows.Threading; using System.Windows.Threading;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Controls.Primitives;
namespace unison 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 MPDHandler mpd; private readonly DispatcherTimer _timer;
private readonly MPDHandler _mpd;
Thickness SelectedThickness;
Thickness BaseThickness;
public MainWindow() public MainWindow()
{ {
InitHwnd(); InitHwnd();
InitializeComponent(); InitializeComponent();
DefaultState(true);
WindowState = WindowState.Minimized; WindowState = WindowState.Minimized;
mpd = (MPDHandler)Application.Current.Properties["mpd"]; _settingsWin = new Settings();
_radiosWin = new Radios();
_timer = new DispatcherTimer();
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
DispatcherTimer timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromSeconds(0.5);
timer.Interval = TimeSpan.FromSeconds(0.5); _timer.Tick += Timer_Tick;
timer.Tick += Timer_Tick; _timer.Start();
timer.Start();
SelectedThickness.Left = SelectedThickness.Right = SelectedThickness.Top = 0.0f;
SelectedThickness.Bottom = 2.0f;
BaseThickness.Left = BaseThickness.Right = BaseThickness.Top = BaseThickness.Bottom = 0.0f;
} }
private void Timer_Tick(object sender, EventArgs e) private void Timer_Tick(object sender, EventArgs e)
{ {
if (mpd.GetCurrentSong() == null) if (_mpd.GetCurrentSong() == null)
return; return;
CurrentTime.Text = FormatSeconds(mpd._currentTime); CurrentTime.Text = FormatSeconds(_mpd.GetCurrentTime());
TimeSlider.Value = mpd._currentTime / mpd.GetCurrentSong().Time * 100; TimeSlider.Value = _mpd.GetCurrentTime() / _mpd.GetCurrentSong().Time * 100;
}
public void OnConnectionChanged(object sender, EventArgs e)
{
if (_mpd.IsConnected())
{
Snapcast.IsEnabled = true;
ConnectionOkIcon.Visibility = Visibility.Visible;
ConnectionFailIcon.Visibility = Visibility.Collapsed;
}
else
{
_timer.Stop();
DefaultState(true);
ConnectionOkIcon.Visibility = Visibility.Collapsed;
ConnectionFailIcon.Visibility = Visibility.Visible;
}
_settingsWin.UpdateConnectionStatus();
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
}
public void OnSongChanged(object sender, EventArgs e)
{
if (_mpd.GetCurrentSong() == null)
return;
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})";
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
SongInfoDash.Text = " ";
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);
}
}
public void OnStatusChanged(object sender, EventArgs e)
{
if (_mpd.GetStatus() == null)
return;
if (VolumeSlider.Value != _mpd.GetStatus().Volume)
{
VolumeSlider.Value = _mpd.GetStatus().Volume;
VolumeSlider.ToolTip = _mpd.GetStatus().Volume;
}
UpdateButton(ref BorderRandom, _mpd.GetStatus().Random);
UpdateButton(ref BorderRepeat, _mpd.GetStatus().Repeat);
UpdateButton(ref BorderSingle, _mpd.GetStatus().Single);
UpdateButton(ref BorderConsume, _mpd.GetStatus().Consume);
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.Visible;
Cover.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.Visible;
Cover.Visibility = Visibility.Collapsed;
}
else if (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.HasStarted)
SnapcastText.Text = unison.Resources.Resources.StopSnapcast;
else
SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
} }
public void UpdateButton(ref Border border, bool b) public void UpdateButton(ref Border border, bool b)
{ {
if (b) border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
border.BorderThickness = SelectedThickness;
else
border.BorderThickness = BaseThickness;
} }
public string FormatSeconds(int time) public string FormatSeconds(int time)
@ -64,114 +201,43 @@ namespace unison
return timespan.ToString(@"mm\:ss"); return timespan.ToString(@"mm\:ss");
} }
public void OnConnectionChanged(object sender, EventArgs e) public void Pause_Clicked(object sender, RoutedEventArgs e) => _mpd.PlayPause();
{ public void Previous_Clicked(object sender, RoutedEventArgs e) => _mpd.Prev();
Connection.Text = (mpd._connected ? "✔️" : "❌") + $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}"; public void Next_Clicked(object sender, RoutedEventArgs e) => _mpd.Next();
SettingsWindow.UpdateConnectionStatus();
if (mpd._connected)
Snapcast.IsEnabled = true;
}
public void OnSongChanged(object sender, EventArgs e) public void Random_Clicked(object sender, RoutedEventArgs e) => _mpd.Random();
{ public void Repeat_Clicked(object sender, RoutedEventArgs e) => _mpd.Repeat();
if (mpd.GetCurrentSong() == null) public void Single_Clicked(object sender, RoutedEventArgs e) => _mpd.Single();
return; public void Consume_Clicked(object sender, RoutedEventArgs e) => _mpd.Consume();
public void ChangeVolume(int value) => _mpd.SetVolume(value);
if (!mpd.GetCurrentSong().HasName)
SongTitle.Text = mpd.GetCurrentSong().Title;
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;
if (mpd.GetCurrentSong().Date != null)
SongAlbum.Text += $" ({ mpd.GetCurrentSong().Date})";
Format.Text = mpd.GetCurrentSong().Path.Substring(mpd.GetCurrentSong().Path.LastIndexOf(".") + 1);
EndTime.Text = FormatSeconds(mpd.GetCurrentSong().Time);
}
public void OnStatusChanged(object sender, EventArgs e)
{
if (mpd.GetStatus() == null)
return;
if (VolumeSlider.Value != mpd._currentVolume)
{
VolumeSlider.Value = mpd._currentVolume;
VolumeSlider.ToolTip = mpd._currentVolume;
}
if (mpd.IsPlaying())
PlayPause.Text = "\xedb4";
else
PlayPause.Text = "\xedb5";
UpdateButton(ref BorderRandom, mpd._currentRandom);
UpdateButton(ref BorderRepeat, mpd._currentRepeat);
UpdateButton(ref BorderSingle, mpd._currentSingle);
UpdateButton(ref BorderConsume, mpd._currentConsume);
}
public void OnCoverChanged(object sender, EventArgs e)
{
if (mpd.GetCover() == null)
{
NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
}
else if (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";
else
SnapcastText.Text = "Start Snapcast";
}
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) public void Snapcast_Clicked(object sender, RoutedEventArgs e)
{ {
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"]; SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
if (!snapcast.Started) snapcast.LaunchOrExit();
snapcast.Start(); }
else
snapcast.Stop(); public void Radios_Clicked(object sender, RoutedEventArgs e)
{
_radiosWin.Show();
_radiosWin.Activate();
if (_radiosWin.WindowState == WindowState.Minimized)
_radiosWin.WindowState = WindowState.Normal;
} }
public void Settings_Clicked(object sender, RoutedEventArgs e) public void Settings_Clicked(object sender, RoutedEventArgs e)
{ {
SettingsWindow.Show(); _settingsWin.Show();
SettingsWindow.Activate(); _settingsWin.Activate();
if (SettingsWindow.WindowState == WindowState.Minimized) if (_settingsWin.WindowState == WindowState.Minimized)
SettingsWindow.WindowState = WindowState.Normal; _settingsWin.WindowState = WindowState.Normal;
} }
private void Window_Closing(object sender, CancelEventArgs e) private void TimeSlider_DragStarted(object sender, DragStartedEventArgs e)
{ {
e.Cancel = true; _timer.Stop();
WindowState = WindowState.Minimized;
Hide();
} }
private void TimeSlider_DragCompleted(object sender, MouseButtonEventArgs e) private void TimeSlider_DragCompleted(object sender, MouseButtonEventArgs e)
@ -179,17 +245,18 @@ namespace unison
Slider slider = (Slider)sender; Slider slider = (Slider)sender;
double SongPercentage = slider.Value; double SongPercentage = slider.Value;
double SongTime = mpd._totalTime; double SongTime = _mpd.GetCurrentSong().Time;
double SeekTime = SongPercentage / 100 * SongTime; double SeekTime = SongPercentage / 100 * SongTime;
mpd.SetTime(SeekTime); _mpd.SetTime(SeekTime);
_timer.Start();
} }
private void VolumeSlider_DragCompleted(object sender, MouseButtonEventArgs e) private void VolumeSlider_DragCompleted(object sender, MouseButtonEventArgs e)
{ {
Slider slider = (Slider)sender; Slider slider = (Slider)sender;
mpd.SetVolume((int)slider.Value); _mpd.SetVolume((int)slider.Value);
slider.ToolTip = mpd._currentVolume; slider.ToolTip = (int)slider.Value;
} }
protected override void OnSourceInitialized(EventArgs e) protected override void OnSourceInitialized(EventArgs e)
@ -199,17 +266,28 @@ namespace unison
hk.Activate(this); 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.SetText(CopyText);
}
}
public void InitHwnd() public void InitHwnd()
{ {
WindowInteropHelper helper = new(this); WindowInteropHelper helper = new(this);
helper.EnsureHandle(); helper.EnsureHandle();
} }
public event PropertyChangedEventHandler PropertyChanged; private void Window_Closing(object sender, CancelEventArgs e)
protected virtual void OnPropertyChanged(string propertyName)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); e.Cancel = true;
WindowState = WindowState.Minimized;
Hide();
} }
} }
} }

74
Views/Radios.xaml Normal file
View 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" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
<Grid>
<StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="5,0,5,0">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline 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 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>

181
Views/Radios.xaml.cs Normal file
View File

@ -0,0 +1,181 @@
using RadioBrowser;
using RadioBrowser.Models;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System;
using System.ComponentModel;
using System.Windows.Interop;
using System.Windows.Controls;
using System.Windows.Input;
using System.Collections.Generic;
namespace unison
{
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;
}
}
}
public partial class Radios : Window
{
private RadioBrowserClient _radioBrowser;
private MPDHandler _mpd;
public Radios()
{
InitializeComponent();
_radioBrowser = new RadioBrowserClient();
Initialize();
}
public async void Initialize()
{
List<NameAndCount> Countries = await _radioBrowser.Lists.GetCountriesAsync();
CountryList.Items.Add(new CountryListItem { Name = "", Count = 0 });
foreach (NameAndCount Country in Countries)
{
CountryList.Items.Add(new CountryListItem
{
Name = Country.Name,
Count = Country.Stationcount
});
}
}
private string CleanString(string str)
{
return str.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");
}
public async Task SearchAdvanced(string name, string country, string tags)
{
SearchStatus.Text = unison.Resources.Resources.Radio_Loading;
List<StationInfo> advancedSearch = await _radioBrowser.Search.AdvancedAsync(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;
}
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)
{
Debug.WriteLine("Error: Invalid index.");
return;
}
if (station.Url == null)
{
Debug.WriteLine("Error: Invalid station.");
return;
}
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
_mpd.ClearAddAndPlay(station.Url.AbsoluteUri);
}
private async void Search_Clicked(object sender, RoutedEventArgs e)
{
CountryListItem a = (CountryListItem)CountryList.SelectedItem;
await SearchAdvanced(NameSearch.Text, a?.Name, TagSearch.Text);
}
private void Reset_Clicked(object sender, RoutedEventArgs e)
{
NameSearch.Text = "";
TagSearch.Text = "";
CountryList.SelectedIndex = 0;
}
private void SearchHandler(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)
Search_Clicked(null, null);
}
private void Window_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
WindowState = WindowState.Minimized;
Hide();
}
public void InitHwnd()
{
WindowInteropHelper helper = new(this);
helper.EnsureHandle();
}
}
}

View File

@ -4,9 +4,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf" xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
xmlns:local="clr-namespace:unison" xmlns:properties="clr-namespace:unison.Resources"
mc:Ignorable="d" mc:Ignorable="d"
Closing="Window_Closing" Title="Settings" ResizeMode="CanMinimize" Icon="/images/icon-full.ico" WindowStyle="ToolWindow" SizeToContent="WidthAndHeight"> Closing="Window_Closing" Title="{x:Static properties:Resources.Settings}" ResizeMode="CanMinimize" Icon="/Resources/icon-full.ico" WindowStyle="ToolWindow" SizeToContent="WidthAndHeight">
<Grid> <Grid>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TabControl Margin="10"> <TabControl Margin="10">
@ -15,28 +15,31 @@
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0"> <GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header> <GroupBox.Header>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="📶 Connection"/> <TextBlock>
<emoji:EmojiInline Text="📶"/>
<Run Text="{x:Static properties:Resources.Settings_Connection}"/>
</TextBlock>
</StackPanel> </StackPanel>
</GroupBox.Header> </GroupBox.Header>
<Grid VerticalAlignment="Top"> <Grid VerticalAlignment="Top">
<StackPanel> <StackPanel>
<StackPanel> <StackPanel>
<TextBlock Text="Host" TextWrapping="Wrap" Margin="5,0,0,0"/> <TextBlock Text="{x:Static properties:Resources.Settings_Host}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdHost" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/> <TextBox x:Name="MpdHost" KeyDown="ConnectHandler" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel> </StackPanel>
<StackPanel Margin="0,5,0,0"> <StackPanel Margin="0,5,0,0">
<TextBlock Text="Port" TextWrapping="Wrap" Margin="5,0,0,0"/> <TextBlock Text="{x:Static properties:Resources.Settings_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"/> <TextBox x:Name="MpdPort" KeyDown="ConnectHandler" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel> </StackPanel>
<StackPanel Margin="0,5,0,0"> <!--<StackPanel Margin="0,5,0,0">
<TextBlock Text="Password" TextWrapping="Wrap" Margin="5,0,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"/> <TextBox x:Name="MpdPassword" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel> </StackPanel>-->
<TextBlock x:Name="ConnectionStatus" Text="Not connected." TextWrapping="Wrap" Margin="5,10,0,0"/> <TextBlock x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}" TextWrapping="Wrap" Margin="5,10,0,0"/>
<Button x:Name="ConnectButton" Content="Connect" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/> <Button x:Name="ConnectButton" Content="{x:Static properties:Resources.Settings_ConnectButton}" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</GroupBox> </GroupBox>
@ -55,16 +58,19 @@
<StackPanel> <StackPanel>
<StackPanel> <StackPanel>
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0"> <CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
<TextBlock Text="Launch at startup" TextWrapping="Wrap"/> <TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
</CheckBox> </CheckBox>
<TextBlock Text="Port" TextWrapping="Wrap" Margin="5,5,0,0"/> <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" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/> <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"/> <TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPath}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/> <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"> <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. <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> </TextBlock>
<Button Content="Reset" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/> <Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -72,21 +78,24 @@
</DockPanel> </DockPanel>
</TabItem> </TabItem>
<TabItem Header="Shortcuts"> <TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
<DockPanel Margin="8"> <DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0"> <GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header> <GroupBox.Header>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="⌨️ Shortcuts"/> <TextBlock>
<emoji:EmojiInline Text="⌨️ "/>
<Run Text="{x:Static properties:Resources.Settings_Shortcuts}"></Run>
</TextBlock>
</StackPanel> </StackPanel>
</GroupBox.Header> </GroupBox.Header>
<Grid> <Grid>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="Volume offset" TextWrapping="Wrap"/> <TextBlock Text="{x:Static properties:Resources.Settings_VolumeOffset}" TextWrapping="Wrap"/>
<TextBox x:Name="VolumeOffset" TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="8,2,0,0"/> <TextBox x:Name="VolumeOffset" TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="8,2,0,0"/>
</StackPanel> </StackPanel>
<Grid MinWidth="220" Margin="0,5,0,0"> <Grid MinWidth="300" Margin="0,5,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition/> <ColumnDefinition/>
<ColumnDefinition/> <ColumnDefinition/>
@ -98,20 +107,23 @@
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="Next track" TextWrapping="Wrap" Grid.Column="0" Grid.Row="0" Margin="1"/> <TextBlock Text="{x:Static properties:Resources.Settings_NextTrack}" 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="{x:Static properties:Resources.Settings_PreviousTrack}" 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="{x:Static properties:Resources.Settings_PlayPause}" 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="{x:Static properties:Resources.Settings_VolumeUp}" 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="{x:Static properties:Resources.Settings_VolumeDown}" 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_VolumeMute}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="5" Margin="1"/>
<TextBlock Text="{x:Static properties:Resources.Settings_ShowWindow}" TextWrapping="Wrap" Grid.Column="0" Grid.Row="6" Margin="1"/>
<TextBlock Text="ctrl + media_next" TextWrapping="Wrap" Grid.Column="1" Grid.Row="0" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/> <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_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 + 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_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 + 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"/> <TextBlock Text="ctrl + volume_mute" TextWrapping="Wrap" Grid.Column="1" Grid.Row="5" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
<TextBlock Text="ctrl + alt + enter" TextWrapping="Wrap" Grid.Column="1" Grid.Row="6" Margin="1" HorizontalAlignment="Right" FontWeight="Bold"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -119,7 +131,7 @@
</DockPanel> </DockPanel>
</TabItem> </TabItem>
<TabItem Header="About" Height="20" VerticalAlignment="Bottom"> <TabItem Header="{x:Static properties:Resources.Settings_About}" Height="20" VerticalAlignment="Bottom">
<DockPanel Margin="8"> <DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0"> <GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header> <GroupBox.Header>
@ -128,23 +140,24 @@
<Grid VerticalAlignment="Top"> <Grid VerticalAlignment="Top">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock TextWrapping="Wrap" Margin="0,0,0,10" VerticalAlignment="Top"> <TextBlock TextWrapping="Wrap" Margin="0,0,0,10" VerticalAlignment="Top">
<Run Text="Version:"/> <Run Text="{x:Static properties:Resources.Settings_Version}"/>
<Run Text="{Binding GetVersion, Mode = OneWay}"/> <Run Text="{Binding GetVersion, Mode = OneWay}"/>
</TextBlock> </TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top"> <TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
unison is free software. It is built with the following technologies:<LineBreak/> <Run Text="{x:Static properties:Resources.Settings_AboutInfo}" /><LineBreak/>
※ <Hyperlink NavigateUri="https://github.com/Difegue/Stylophone" RequestNavigate="Hyperlink_RequestNavigate">Stylophone</Hyperlink>'s updated MpcNET<LineBreak/> ※ <Hyperlink NavigateUri="https://github.com/Difegue/Stylophone" RequestNavigate="Hyperlink_RequestNavigate">Stylophone</Hyperlink><Run Text="{x:Static properties:Resources.Settings_MpcNET}" /><LineBreak/>
※ <Hyperlink NavigateUri="https://github.com/hardcodet/wpf-notifyicon" RequestNavigate="Hyperlink_RequestNavigate">wpf-notifyicon</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://github.com/tof4/RadioBrowser" RequestNavigate="Hyperlink_RequestNavigate">RadioBrowser</Hyperlink>
</TextBlock> </TextBlock>
<TextBlock Margin="0,10,0,0"> <TextBlock Margin="0,10,0,0">
Source code freely available <Run Text="{x:Static properties:Resources.Settings_SourceCode1}" />
<Hyperlink NavigateUri="https://git.n700.ovh/keb/unison" RequestNavigate="Hyperlink_RequestNavigate"> <Hyperlink NavigateUri="https://git.n700.ovh/keb/unison" RequestNavigate="Hyperlink_RequestNavigate">
here <Run Text="{x:Static properties:Resources.Settings_SourceCode2}" />
</Hyperlink>. </Hyperlink>.
</TextBlock> </TextBlock>
<TextBlock Margin="0,10,0,0"> <TextBlock Margin="0,10,0,0">
Made by <Run Text="{x:Static properties:Resources.Settings_MadeBy}" />
<Hyperlink NavigateUri="https://marchal.dev" RequestNavigate="Hyperlink_RequestNavigate"> <Hyperlink NavigateUri="https://marchal.dev" RequestNavigate="Hyperlink_RequestNavigate">
Théo Marchal Théo Marchal
</Hyperlink>. </Hyperlink>.
@ -155,7 +168,10 @@
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0" Margin="0,10,0,0"> <GroupBox DockPanel.Dock="Top" Padding="0,4,0,0" Margin="0,10,0,0">
<GroupBox.Header> <GroupBox.Header>
<emoji:TextBlock Text="📝 License"/> <TextBlock>
<emoji:EmojiInline Text="📝 "/>
<Run Text="{x:Static properties:Resources.Settings_License}" />
</TextBlock>
</GroupBox.Header> </GroupBox.Header>
<Grid VerticalAlignment="Top"> <Grid VerticalAlignment="Top">
<TextBlock Text="{Binding GetLicense, Mode = OneWay}" TextWrapping="Wrap" Width="500" TextAlignment="Justify" /> <TextBlock Text="{Binding GetLicense, Mode = OneWay}" TextWrapping="Wrap" Width="500" TextAlignment="Justify" />

View File

@ -13,9 +13,6 @@ namespace unison
{ {
public partial class Settings : Window 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 GetVersion => Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
public static string GetLicense public static string GetLicense
@ -46,8 +43,9 @@ namespace unison
MpdHost.Text = Properties.Settings.Default.mpd_host; MpdHost.Text = Properties.Settings.Default.mpd_host;
MpdPort.Text = Properties.Settings.Default.mpd_port.ToString(); MpdPort.Text = Properties.Settings.Default.mpd_port.ToString();
MpdPassword.Text = Properties.Settings.Default.mpd_password; //MpdPassword.Text = Properties.Settings.Default.mpd_password;
SnapcastStartup.IsChecked = Properties.Settings.Default.snapcast_startup; SnapcastStartup.IsChecked = Properties.Settings.Default.snapcast_startup;
SnapcastWindow.IsChecked = Properties.Settings.Default.snapcast_window;
SnapcastPath.Text = Properties.Settings.Default.snapcast_path; SnapcastPath.Text = Properties.Settings.Default.snapcast_path;
SnapcastPort.Text = Properties.Settings.Default.snapcast_port.ToString(); SnapcastPort.Text = Properties.Settings.Default.snapcast_port.ToString();
VolumeOffset.Text = Properties.Settings.Default.volume_offset.ToString(); VolumeOffset.Text = Properties.Settings.Default.volume_offset.ToString();
@ -70,38 +68,45 @@ namespace unison
public void UpdateConnectionStatus() public void UpdateConnectionStatus()
{ {
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"]; MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd._connected) if (mpd.IsConnected())
ConnectionStatus.Text = "Connected to MPD " + mpd.GetVersion() + "."; ConnectionStatus.Text = $"{unison.Resources.Resources.Settings_ConnectionStatusConnected} {mpd.GetVersion()}.";
else else
ConnectionStatus.Text = "Not connected."; ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusOffline;
} }
private void MPDConnect_Clicked(object sender, RoutedEventArgs e) private void MPDConnect_Clicked(object sender, RoutedEventArgs e)
{ {
SaveSettings(); SaveSettings();
ConnectionStatus.Text = "Connecting..."; ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"]; MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
mpd.Connect(); mpd.Connect();
} }
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e) private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
{ {
SnapcastPath.Text = defaultSnapcastPath; SnapcastPath.Text = (string)Application.Current.FindResource("snapcastPath");
SnapcastPort.Text = defaultSnapcastPort; SnapcastPort.Text = (string)Application.Current.FindResource("snapcastPort");
} }
public void SaveSettings() public void SaveSettings()
{ {
Properties.Settings.Default.mpd_host = MpdHost.Text; Properties.Settings.Default.mpd_host = MpdHost.Text;
Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture); Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.mpd_password = MpdPassword.Text; //Properties.Settings.Default.mpd_password = MpdPassword.Text;
Properties.Settings.Default.snapcast_startup = (bool)SnapcastStartup.IsChecked; 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_path = SnapcastPath.Text;
Properties.Settings.Default.snapcast_port = int.Parse(SnapcastPort.Text, CultureInfo.InvariantCulture); 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.volume_offset = int.Parse(VolumeOffset.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.Save(); Properties.Settings.Default.Save();
} }
private void ConnectHandler(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)
MPDConnect_Clicked(null, null);
}
private void Window_Closing(object sender, CancelEventArgs e) private void Window_Closing(object sender, CancelEventArgs e)
{ {
e.Cancel = true; e.Cancel = true;

View File

@ -2,47 +2,53 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tb="http://www.hardcodet.net/taskbar" xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf" xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
xmlns:properties="clr-namespace:unison.Resources"
xmlns:local="clr-namespace:unison"> xmlns:local="clr-namespace:unison">
<ContextMenu x:Shared="false" x:Key="SystrayMenu"> <ContextMenu x:Shared="false" x:Key="SystrayMenu">
<MenuItem IsEnabled="False"> <MenuItem IsEnabled="False">
<MenuItem.Icon> <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.Icon>
<MenuItem.Header> <MenuItem.Header>
<TextBlock Text="{Binding GetAppText}" /> <TextBlock Text="{Binding GetAppText}" />
</MenuItem.Header> </MenuItem.Header>
</MenuItem> </MenuItem>
<Separator /> <Separator />
<MenuItem Header="Show window" Command="{Binding ShowWindowCommand}"> <MenuItem Header="{x:Static properties:Resources.ShowWindow}" Command="{Binding ShowWindowCommand}">
<MenuItem.Icon> <MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="▶️" /> <Image Width="16" Height="16" emoji:Image.Source="▶️" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Name="test_snapcast_item" Header="{Binding SnapcastText}" Command="{Binding Snapcast}"> <MenuItem Header="{Binding SnapcastText}" Command="{Binding Snapcast}">
<MenuItem.Icon> <MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🔊" /> <Image Width="16" Height="16" emoji:Image.Source="🔊" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<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 Header="Shuffle" Command="{Binding Shuffle}">
<MenuItem.Icon> <MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🔀" /> <Image Width="16" Height="16" emoji:Image.Source="🔀" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem>--> </MenuItem>-->
<MenuItem Header="Settings" Command="{Binding Settings}"> <MenuItem Header="{x:Static properties:Resources.Settings}" Command="{Binding Settings}">
<MenuItem.Icon> <MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🛠️" /> <Image Width="16" Height="16" emoji:Image.Source="🛠️" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<Separator /> <Separator />
<MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}"> <MenuItem Header="{x:Static properties:Resources.Exit}" Command="{Binding ExitApplicationCommand}">
<MenuItem.Icon> <MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="❌" /> <Image Width="16" Height="16" emoji:Image.Source="❌" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
</ContextMenu> </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> <tb:TaskbarIcon.DataContext>
<local:SystrayViewModel /> <local:SystrayViewModel />
</tb:TaskbarIcon.DataContext> </tb:TaskbarIcon.DataContext>

View File

@ -8,6 +8,8 @@ namespace unison
{ {
public class SystrayViewModel : INotifyPropertyChanged public class SystrayViewModel : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler PropertyChanged;
public static string GetAppText => "unison v" + Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion; public static string GetAppText => "unison v" + Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
public static ICommand ShowWindowCommand => new DelegateCommand public static ICommand ShowWindowCommand => new DelegateCommand
@ -26,10 +28,7 @@ namespace unison
public static ICommand ExitApplicationCommand => new DelegateCommand public static ICommand ExitApplicationCommand => new DelegateCommand
{ {
CommandAction = () => CommandAction = () => Application.Current.Shutdown(),
{
Application.Current.Shutdown();
},
CanExecuteFunc = () => true CanExecuteFunc = () => true
}; };
@ -38,7 +37,7 @@ namespace unison
get get
{ {
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"]; 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;
} }
} }
@ -51,30 +50,39 @@ namespace unison
CommandAction = () => CommandAction = () =>
{ {
Application.Current.Dispatcher.BeginInvoke(new Action(() => OnPropertyChanged("SnapcastText"))); Application.Current.Dispatcher.BeginInvoke(new Action(() => OnPropertyChanged("SnapcastText")));
((MainWindow)Application.Current.MainWindow).Snapcast_Clicked(null, null);
SnapcastHandler snapcast = (SnapcastHandler)Application.Current.Properties["snapcast"];
snapcast.LaunchOrExit();
}, },
CanExecuteFunc = () => true CanExecuteFunc = () => true
}; };
} }
} }
public ICommand Radios
{
get
{
return new DelegateCommand
{
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Radios_Clicked(null, null),
CanExecuteFunc = () => true
};
}
}
public ICommand Settings public ICommand Settings
{ {
get get
{ {
return new DelegateCommand return new DelegateCommand
{ {
CommandAction = () => CommandAction = () => ((MainWindow)Application.Current.MainWindow).Settings_Clicked(null, null),
{
((MainWindow)Application.Current.MainWindow).Settings_Clicked(null, null);
},
CanExecuteFunc = () => true CanExecuteFunc = () => true
}; };
} }
} }
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName) public virtual void OnPropertyChanged(string propertyName)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

View File

@ -4,10 +4,10 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework> <TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ApplicationIcon>images\icon-full.ico</ApplicationIcon> <ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
<Win32Resource></Win32Resource> <Win32Resource></Win32Resource>
<StartupObject>unison.App</StartupObject> <StartupObject>unison.App</StartupObject>
<Version>0.0.1</Version> <Version>1.1</Version>
<Company /> <Company />
<Authors>Théo Marchal</Authors> <Authors>Théo Marchal</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
@ -18,9 +18,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="images\icon-full.ico" /> <None Remove="Resources\icon-full.ico" />
<None Remove="images\icon-mini.ico" /> <None Remove="Resources\icon-mini.ico" />
<None Remove="images\nocover.png" /> <None Remove="Resources\nocover.png" />
<None Remove="LICENSE" /> <None Remove="LICENSE" />
<None Remove="snapclient_0.25.0-1_win64\FLAC.dll" /> <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\ogg.dll" />
@ -36,13 +36,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Resource Include="images\icon-full.ico"> <Resource Include="Resources\icon-full.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource> </Resource>
<Resource Include="images\icon-mini.ico"> <Resource Include="Resources\icon-mini.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource> </Resource>
<Resource Include="images\nocover.png"> <Resource Include="Resources\nocover.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource> </Resource>
<Content Include="LICENSE"> <Content Include="LICENSE">
@ -74,6 +74,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Emoji.Wpf" Version="0.3.3" /> <PackageReference Include="Emoji.Wpf" Version="0.3.3" />
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" /> <PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
<PackageReference Include="RadioBrowser" Version="0.6.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -86,6 +87,21 @@
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon> <DependentUpon>Settings.settings</DependentUpon>
</Compile> </Compile>
<Compile Update="Resources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<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>
<ItemGroup> <ItemGroup>