18 Commits

Author SHA1 Message Date
792437b839 Update readme and screenshots 2022-04-17 16:04:28 +02:00
5bfa7d3b5b Basic shuffle features, must fix deadlocks 2022-04-17 16:02:09 +02:00
c5e8534af7 Merge branch 'main' into shuffle 2022-04-14 01:08:49 +02:00
5d6e3b6d1e Update MpcNET from 1.3 to 1.4 to use filters 2022-04-14 01:06:33 +02:00
ded5908ca2 Spanish translation 2022-04-13 14:07:09 +02:00
519fe4968e MPD password support 2022-04-13 00:43:04 +02:00
f95b884d16 Fix Radios crash 2022-04-12 23:14:13 +02:00
4136c13d5b Add support for readpicture, aka embedded cover art 2022-04-11 12:37:12 +02:00
0ab1afc2f8 More resilient handle of disconnection and reconnection 2022-04-10 02:25:41 +02:00
3b59e51368 Fix Snapcast not working with hostname 2022-04-08 01:39:59 +02:00
c7a93c2d82 Add CHANGELOG file 2022-04-07 13:41:46 +02:00
43350aed36 Trim album release date 2022-04-07 13:41:18 +02:00
6ad4d9c813 Merge branch 'main' into shuffle 2022-04-07 00:59:00 +02:00
c93a9a326e Better working continuous shuffle 2021-10-07 22:14:09 +02:00
c055c59de7 Handle no response when querying stats 2021-10-07 22:13:18 +02:00
4c71d6a6e0 Rough working shuffle system 2021-10-06 22:54:55 +02:00
3685c369b4 Merge branch 'main' into shuffle 2021-10-05 19:20:00 +02:00
e0d640532c Shuffle window (only visual) 2021-10-05 14:23:23 +02:00
27 changed files with 1426 additions and 256 deletions

View File

@ -15,6 +15,8 @@ namespace unison
{
//debug language
//unison.Resources.Resources.Culture = CultureInfo.GetCultureInfo("fr-FR");
//unison.Resources.Resources.Culture = CultureInfo.GetCultureInfo("es-ES");
base.OnStartup(e);

29
CHANGELOG.md Normal file
View File

@ -0,0 +1,29 @@
# Changelog
## v1.2
*Released: 07/04/2022*
* New feature: support for custom shortcuts
* New feature: MPD stats
* Add GitHub repository link in settings
* MpcNET NuGet package integration
* Update Snapcast from v0.25 to v0.26
* Fix: hostname supported for connection
* Fix: crash when using invalid IP or Hostname
* Fix: crash when no internet connection when using RadioBrowser
## v1.1
*Released: 04/10/2021*
* Radio browser
* Mute shortcut
* Enter key works in settings textboxes
* Share current song by double-clicking
## v1.0
*Released: 03/09/2021*
* First release of unison

View File

@ -54,7 +54,7 @@ namespace unison
bool _isUpdatingStatus = false;
bool _isUpdatingSong = false;
bool _invalidIp = false;
public IPAddress _ipAddress;
private event EventHandler ConnectionChanged;
private event EventHandler StatusChanged;
@ -64,12 +64,10 @@ namespace unison
private MpcConnection _connection;
private MpcConnection _commandConnection;
private IPEndPoint _mpdEndpoint;
private CancellationTokenSource cancelToken;
private CancellationTokenSource _cancelToken;
public MPDHandler()
{
cancelToken = new CancellationTokenSource();
Initialize(null, null);
_stats = new Statistics();
@ -97,7 +95,7 @@ namespace unison
void OnConnectionChanged(object sender, EventArgs e)
{
if (!_connected && !_invalidIp)
if (!_connected)
_retryTimer.Start();
else
_retryTimer.Stop();
@ -157,10 +155,12 @@ namespace unison
try
{
Debug.WriteLine("SafelySendCommandAsync => before command");
IMpdMessage<T> response = await _commandConnection.SendAsync(command);
Debug.WriteLine("SafelySendCommandAsync => after command");
if (!response.IsResponseValid)
{
var mpdError = response.Response?.Result?.MpdError;
string mpdError = response.Response?.Result?.MpdError;
if (mpdError != null && mpdError != "")
throw new Exception(mpdError);
else
@ -185,19 +185,23 @@ namespace unison
public async void Connect()
{
CancellationToken token = cancelToken.Token;
_cancelToken = new CancellationTokenSource();
CancellationToken token = _cancelToken.Token;
try
{
_connection = await ConnectInternal(token);
_commandConnection = await ConnectInternal(token);
}
catch(MpcNET.Exceptions.MpcConnectException)
catch (MpcNET.Exceptions.MpcConnectException e)
{
_invalidIp = true;
_connected = false;
Trace.WriteLine($"Error in connect: {e.Message}");
ConnectionChanged?.Invoke(this, EventArgs.Empty);
return;
}
if (_connection != null && _commandConnection != null)
{
_invalidIp = false;
if (_connection.IsConnected && _commandConnection.IsConnected)
{
_connected = true;
@ -207,6 +211,7 @@ namespace unison
}
else
{
_connected = false;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
return;
}
@ -219,16 +224,21 @@ namespace unison
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
{
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress);
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out _ipAddress);
if (ipAddress == null)
if (_ipAddress == null)
{
IPAddress[] addrList;
try
{
addrList = Dns.GetHostAddresses(Properties.Settings.Default.mpd_host);
IPAddress[] addrList = Dns.GetHostAddresses(Properties.Settings.Default.mpd_host);
if (addrList.Length > 0)
ipAddress = addrList[0];
{
foreach (IPAddress addr in addrList)
{
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
_ipAddress = addr;
}
}
}
catch (Exception)
{
@ -236,11 +246,11 @@ namespace unison
}
}
_mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port);
_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))
if (!string.IsNullOrEmpty(Properties.Settings.Default.mpd_password))
{
IMpdMessage<string> result = await connection.SendAsync(new PasswordCommand(Properties.Settings.Default.mpd_password));
if (!result.IsResponseValid)
@ -248,19 +258,21 @@ namespace unison
string mpdError = result.Response?.Result?.MpdError;
Trace.WriteLine(mpdError);
}
}*/
}
return connection;
}
private void Disconnected()
{
public void Disconnected()
{
_connected = false;
_connection = null;
_commandConnection = null;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
if (_connection != null)
_connection = null;
if (_commandConnection != null)
_commandConnection = null;
}
private void Loop(CancellationToken token)
@ -271,24 +283,29 @@ namespace unison
{
try
{
token.ThrowIfCancellationRequested();
if (token.IsCancellationRequested || _connection == null)
break;
var idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options"));
IMpdMessage<string> idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update"));
if (idleChanges.IsResponseValid)
await HandleIdleResponseAsync(idleChanges.Response.Content);
else
{
Trace.WriteLine($"Error in Idle connection thread: {idleChanges.Response?.Content}");
throw new Exception(idleChanges.Response?.Content);
}
}
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
Disconnected();
break;
}
}
}).ConfigureAwait(false);
}, token).ConfigureAwait(false);
}
private async Task HandleIdleResponseAsync(string subsystems)
@ -366,6 +383,7 @@ namespace unison
List<byte> data = new List<byte>();
try
{
bool ReadPictureFailed = true;
long totalBinarySize = 9999;
long currentSize = 0;
@ -374,18 +392,41 @@ namespace unison
if (_connection == null)
return;
var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new ReadPictureCommand(path, currentSize));
if (!albumReq.IsResponseValid)
break;
var response = albumReq.Response.Content;
if (response.Binary == 0)
MpdBinaryData response = albumReq.Response.Content;
if (response == null || response.Binary == 0)
break;
ReadPictureFailed = false;
totalBinarySize = response.Size;
currentSize += response.Binary;
data.AddRange(response.Data);
}
while (currentSize < totalBinarySize && !token.IsCancellationRequested);
do
{
if (!ReadPictureFailed)
break;
if (_connection == null)
return;
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
if (!albumReq.IsResponseValid)
break;
MpdBinaryData response = albumReq.Response.Content;
if (response == null || response.Binary == 0)
break;
totalBinarySize = response.Size;
currentSize += response.Binary;
data.AddRange(response.Data);
} while (currentSize < totalBinarySize && !token.IsCancellationRequested);
}
while (currentSize < totalBinarySize && !token.IsCancellationRequested);
}
catch (Exception e)
{
@ -459,6 +500,8 @@ namespace unison
public void Next() => SendCommand(new NextCommand());
public void PlayPause() => SendCommand(new PauseResumeCommand());
public void Play(int pos) => SendCommand(new PlayCommand(pos));
public void Random() => SendCommand(new RandomCommand(!_currentRandom));
public void Repeat() => SendCommand(new RepeatCommand(!_currentRepeat));
public void Single() => SendCommand(new SingleCommand(!_currentSingle));
@ -503,7 +546,6 @@ namespace unison
public void AddSong(string Uri)
{
Debug.WriteLine("AddCommand path: " + Uri);
SendCommand(new AddCommand(Uri));
}
@ -515,21 +557,23 @@ namespace unison
public async void QueryStats()
{
Dictionary<string, string> response = await SafelySendCommandAsync(new StatsCommand());
Dictionary<string, string> Response = await SafelySendCommandAsync(new StatsCommand());
if (Response == null)
return;
_stats.Songs = int.Parse(response["songs"]);
_stats.Albums = int.Parse(response["albums"]);
_stats.Artists = int.Parse(response["artists"]);
_stats.Songs = int.Parse(Response["songs"]);
_stats.Albums = int.Parse(Response["albums"]);
_stats.Artists = int.Parse(Response["artists"]);
TimeSpan time;
time = TimeSpan.FromSeconds(int.Parse(response["uptime"]));
time = TimeSpan.FromSeconds(int.Parse(Response["uptime"]));
_stats.Uptime = time.ToString(@"dd\:hh\:mm\:ss");
time = TimeSpan.FromSeconds(int.Parse(response["db_playtime"]));
time = TimeSpan.FromSeconds(int.Parse(Response["db_playtime"]));
_stats.TotalPlaytime = time.ToString(@"dd\:hh\:mm\:ss");
time = TimeSpan.FromSeconds(int.Parse(response["playtime"]));
time = TimeSpan.FromSeconds(int.Parse(Response["playtime"]));
_stats.TotalTimePlayed = time.ToString(@"dd\:hh\:mm\:ss");
DateTime date = new DateTime(1970, 1, 1).AddSeconds(int.Parse(response["db_update"])).ToLocalTime();
DateTime date = new DateTime(1970, 1, 1).AddSeconds(int.Parse(Response["db_update"])).ToLocalTime();
_stats.DatabaseUpdate = date.ToString("dd/MM/yyyy @ HH:mm");
}
}

View File

@ -14,7 +14,7 @@ namespace unison
{
if (Properties.Settings.Default.snapcast_startup)
{
var mpd = (MPDHandler)Application.Current.Properties["mpd"];
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
LaunchOrExit();
}
@ -37,8 +37,10 @@ namespace unison
{
if (!HasStarted && !ForceExit)
{
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
_snapcast.StartInfo.FileName = Properties.Settings.Default.snapcast_path + @"\snapclient.exe";
_snapcast.StartInfo.Arguments = $"--host {Properties.Settings.Default.mpd_host}";
_snapcast.StartInfo.Arguments = $"--host {mpd._ipAddress}";
_snapcast.StartInfo.CreateNoWindow = !Properties.Settings.Default.snapcast_window;
try
{

View File

@ -1,13 +1,14 @@
# unison
![Main window](Screenshots/screen1.png)
![Main window](Screenshots/screen-mainwindow.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 rebindable shortcuts
* shuffle panel
* [Snapcast](https://mjaggard.github.io/snapcast/) integration
* Radio stations
* radio stations
## Features
@ -15,13 +16,25 @@
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)
![Systray](Screenshots/screen-systray.png)
### Shortcuts
You can control your music at anytime with the shortcuts. They are usable system-wide, even if the window is not visible. They are of course fully rebindable.
![Settings => shortcuts](Screenshots/screen3.png)
![Settings => shortcuts](Screenshots/screen-shortcuts.png)
### Shuffle panel
One of unison's main feature is a complete shuffle system based on criterias, aka a smart playlist.
You have two options:
* **Add to queue** allows you to add a defined number of songs to the queue.
* **Continuous shuffle** allows you, as long as the program is running, to automatically add songs to the queue.
Each of these options work with filters, but if none are selected, it is based on the entire library.
![Shuffle panel](Screenshots/screen-shuffle.png)
### Snapcast
@ -31,19 +44,14 @@ Embedding a Snapcast client allows to listen to music on multiple devices. For e
Through [Radio-Browser](https://www.radio-browser.info), a community database, you can play radio streams directly from unison. There are more than 28,000 stations recorded on this service, so it is a nice way to discover new music and cultures.
![Radio stations](Screenshots/screen4.png)
![Radio stations](Screenshots/screen-radio.png)
## Caveats
## Planned features
### Missing features
* MPD passwords: I don't really see the point, but if asked, I will integrate them.
### Planned features
* A complete shuffle system based on set criteria, aka a smart playlist.
* Playlist, queue and library management. I use other software to do it, but I will implement them at some point.
* Playlist, queue and library management
* More options for the shuffle panel
* Dark mode
## Translations
unison is translated in English and French. You can contribute if you want!
unison is translated in English, French and Spanish. You can contribute if you want!

View File

@ -196,7 +196,25 @@ namespace unison.Resources {
}
/// <summary>
/// Looks up a localized string similar to Connected to MPD.
/// Looks up a localized string similar to Please note that since MPD passwords are not secure (they are sent in plain text to the server), there are saved as is in the setting file..
/// </summary>
public static string Settings_ConnectionPasswordInfo {
get {
return ResourceManager.GetString("Settings_ConnectionPasswordInfo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Status:.
/// </summary>
public static string Settings_ConnectionStatus {
get {
return ResourceManager.GetString("Settings_ConnectionStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to connected to MPD.
/// </summary>
public static string Settings_ConnectionStatusConnected {
get {
@ -205,7 +223,7 @@ namespace unison.Resources {
}
/// <summary>
/// Looks up a localized string similar to Connecting....
/// Looks up a localized string similar to connecting....
/// </summary>
public static string Settings_ConnectionStatusConnecting {
get {
@ -214,7 +232,7 @@ namespace unison.Resources {
}
/// <summary>
/// Looks up a localized string similar to Not connected..
/// Looks up a localized string similar to not connected..
/// </summary>
public static string Settings_ConnectionStatusOffline {
get {
@ -258,6 +276,15 @@ namespace unison.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Password.
/// </summary>
public static string Settings_Password {
get {
return ResourceManager.GetString("Settings_Password", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Play / pause.
/// </summary>

View File

@ -0,0 +1,306 @@
<?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>Salir</value>
</data>
<data name="Radios" xml:space="preserve">
<value>Radios</value>
</data>
<data name="Radio_Country" xml:space="preserve">
<value>Países</value>
</data>
<data name="Radio_Loading" xml:space="preserve">
<value>Cargando estaciones...</value>
</data>
<data name="Radio_Name" xml:space="preserve">
<value>Nombre</value>
</data>
<data name="Radio_NotFound" xml:space="preserve">
<value>¡No estaciones encontradas!</value>
</data>
<data name="Radio_Reset" xml:space="preserve">
<value>Reinicializar</value>
</data>
<data name="Radio_Search" xml:space="preserve">
<value>Búsqueda</value>
</data>
<data name="Radio_SearchStation" xml:space="preserve">
<value>Buscar estación</value>
</data>
<data name="Radio_Tags" xml:space="preserve">
<value>Tags</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Ajustes</value>
</data>
<data name="Settings_About" xml:space="preserve">
<value>Acerca de</value>
</data>
<data name="Settings_AboutInfo" xml:space="preserve">
<value>unison es un software libre. Es construido con las siguientes tecnologías:</value>
</data>
<data name="Settings_ConnectButton" xml:space="preserve">
<value>Conexión</value>
</data>
<data name="Settings_Connection" xml:space="preserve">
<value>Conexión</value>
</data>
<data name="Settings_ConnectionPasswordInfo" xml:space="preserve">
<value>Tenga en cuenta que, dado que las contraseñas de MPD no son seguras (se envían en texto sin formato al servidor), se guardan tal cual en el archivo de configuración.</value>
</data>
<data name="Settings_ConnectionStatus" xml:space="preserve">
<value>Estado:</value>
</data>
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
<value>conectado a MPD</value>
</data>
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
<value>conectando...</value>
</data>
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
<value>no conectado.</value>
</data>
<data name="Settings_Host" xml:space="preserve">
<value>Host</value>
</data>
<data name="Settings_License" xml:space="preserve">
<value>Licencia</value>
</data>
<data name="Settings_MadeBy" xml:space="preserve">
<value>Creado por</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve">
<value>Pista siguiente</value>
</data>
<data name="Settings_Password" xml:space="preserve">
<value>Contraseña</value>
</data>
<data name="Settings_PlayPause" xml:space="preserve">
<value>Jugar / pausa</value>
</data>
<data name="Settings_Port" xml:space="preserve">
<value>Puerto</value>
</data>
<data name="Settings_PreviousTrack" xml:space="preserve">
<value>Pista precedente</value>
</data>
<data name="Settings_Shortcuts" xml:space="preserve">
<value>Atajos</value>
</data>
<data name="Settings_ShortcutsInfo" xml:space="preserve">
<value>Tenga en cuenta que si no está reconocida la tecla, esto se debe a una limitación en el funcionamiento de las teclas virtuales.</value>
</data>
<data name="Settings_ShortcutsKey" xml:space="preserve">
<value>Entre una tecla...</value>
</data>
<data name="Settings_ShowWindow" xml:space="preserve">
<value>Mostrar ventana</value>
</data>
<data name="Settings_SnapcastInfo1" xml:space="preserve">
<value>Puede cambiar a su propia versión instalada localmente del cliente Snapcast con una ruta</value>
</data>
<data name="Settings_SnapcastInfo2" xml:space="preserve">
<value> absoluta</value>
</data>
<data name="Settings_SnapcastInfo3" xml:space="preserve">
<value>.</value>
</data>
<data name="Settings_SnapcastLauch" xml:space="preserve">
<value>Lanzar al inicio</value>
</data>
<data name="Settings_SnapcastPath" xml:space="preserve">
<value>Ruta del ejecutable</value>
</data>
<data name="Settings_SnapcastPort" xml:space="preserve">
<value>Puerto</value>
</data>
<data name="Settings_SnapcastResetButton" xml:space="preserve">
<value>Reinicializar</value>
</data>
<data name="Settings_SnapcastWindow" xml:space="preserve">
<value>Mostrar ventana de Snapcast</value>
</data>
<data name="Settings_SourceCode1" xml:space="preserve">
<value>Código fuente disponible gratuitamente</value>
</data>
<data name="Settings_SourceCode2" xml:space="preserve">
<value>aquí</value>
</data>
<data name="Settings_Version" xml:space="preserve">
<value>Versión:</value>
</data>
<data name="Settings_VolumeDown" xml:space="preserve">
<value>Bajar volumen</value>
</data>
<data name="Settings_VolumeMute" xml:space="preserve">
<value>Volumen mudo</value>
</data>
<data name="Settings_VolumeOffset" xml:space="preserve">
<value>Intervalo de volumen</value>
</data>
<data name="Settings_VolumeUp" xml:space="preserve">
<value>Subir volumen</value>
</data>
<data name="ShowWindow" xml:space="preserve">
<value>Mostrar ventana</value>
</data>
<data name="Snapcast_Popup1" xml:space="preserve">
<value>Error Snapcast</value>
</data>
<data name="Snapcast_Popup2" xml:space="preserve">
<value>Ruta no válida:</value>
</data>
<data name="Snapcast_Popup3" xml:space="preserve">
<value>Ruta actual:</value>
</data>
<data name="Snapcast_Popup4" xml:space="preserve">
<value>Puede restablecerlo en la configuración si es necesario.</value>
</data>
<data name="StartSnapcast" xml:space="preserve">
<value>Iniciar Snapcast</value>
</data>
<data name="Stats" xml:space="preserve">
<value>Estadísticas</value>
</data>
<data name="Stats_Albums" xml:space="preserve">
<value>Álbumes:</value>
</data>
<data name="Stats_Artists" xml:space="preserve">
<value>Artistas:</value>
</data>
<data name="Stats_LastDatabaseUpdate" xml:space="preserve">
<value>Última actualización de la base de datos:</value>
</data>
<data name="Stats_Songs" xml:space="preserve">
<value>Canciones:</value>
</data>
<data name="Stats_TotalPlaytime" xml:space="preserve">
<value>Tiempo de juego total:</value>
</data>
<data name="Stats_TotalTimePlayed" xml:space="preserve">
<value>Tiempo total jugado:</value>
</data>
<data name="Stats_Uptime" xml:space="preserve">
<value>Tiempo de actividad de MPD:</value>
</data>
<data name="StopSnapcast" xml:space="preserve">
<value>Parar Snapcast</value>
</data>
</root>

View File

@ -133,7 +133,7 @@
<value>Nom</value>
</data>
<data name="Radio_NotFound" xml:space="preserve">
<value>Aucun station trouvée !</value>
<value>Aucune station trouvée !</value>
</data>
<data name="Radio_Reset" xml:space="preserve">
<value>Réinitialiser</value>
@ -162,14 +162,20 @@
<data name="Settings_Connection" xml:space="preserve">
<value>Connexion</value>
</data>
<data name="Settings_ConnectionPasswordInfo" xml:space="preserve">
<value>Veuillez noter que comme les mots de passe de MPD ne sont pas sécurisés (ils sont envoyés en clair au serveur), ils sont sauvegardés tels quels.</value>
</data>
<data name="Settings_ConnectionStatus" xml:space="preserve">
<value>Statut :</value>
</data>
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
<value>Connecté à MPD</value>
<value>connecté à MPD</value>
</data>
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
<value>Connexion en cours...</value>
<value>connexion en cours...</value>
</data>
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
<value>Non connecté.</value>
<value>non connecté.</value>
</data>
<data name="Settings_Host" xml:space="preserve">
<value>Hôte</value>
@ -183,6 +189,9 @@
<data name="Settings_NextTrack" xml:space="preserve">
<value>Piste suivante</value>
</data>
<data name="Settings_Password" xml:space="preserve">
<value>Mot de passe</value>
</data>
<data name="Settings_PlayPause" xml:space="preserve">
<value>Jouer / pause</value>
</data>

View File

@ -162,14 +162,20 @@
<data name="Settings_Connection" xml:space="preserve">
<value>Connection</value>
</data>
<data name="Settings_ConnectionPasswordInfo" xml:space="preserve">
<value>Please note that since MPD passwords are not secure (they are sent in plain text to the server), there are saved as is in the setting file.</value>
</data>
<data name="Settings_ConnectionStatus" xml:space="preserve">
<value>Status:</value>
</data>
<data name="Settings_ConnectionStatusConnected" xml:space="preserve">
<value>Connected to MPD</value>
<value>connected to MPD</value>
</data>
<data name="Settings_ConnectionStatusConnecting" xml:space="preserve">
<value>Connecting...</value>
<value>connecting...</value>
</data>
<data name="Settings_ConnectionStatusOffline" xml:space="preserve">
<value>Not connected.</value>
<value>not connected.</value>
</data>
<data name="Settings_Host" xml:space="preserve">
<value>Host</value>
@ -183,6 +189,9 @@
<data name="Settings_NextTrack" xml:space="preserve">
<value>Next track</value>
</data>
<data name="Settings_Password" xml:space="preserve">
<value>Password</value>
</data>
<data name="Settings_PlayPause" xml:space="preserve">
<value>Play / pause</value>
</data>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -110,14 +110,15 @@
</Border>
</Grid>
<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">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0">
<Button x:Name="Shuffle" Padding="5, 2" Click="Shuffle_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" Margin="0,0,10,0">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
</StackPanel>
</Button>
<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}" IsEnabled="False">
<Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="📻" Padding="0,0,0,2"/>
<TextBlock Text="{x:Static properties:Resources.Radios}" Margin="5, 0, 0, 0"/>
@ -135,12 +136,13 @@
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="5,0,0,0" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
<!--<Button x:Name="Shuffle" Padding="5, 2" HorizontalAlignment="Right" Margin="0,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="0,0,10,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
</StackPanel>
</Button>-->
</Button>
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>

View File

@ -6,6 +6,8 @@ using System.Windows.Threading;
using System.Windows.Interop;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
using MpcNET.Commands.Queue;
using System.Diagnostics;
namespace unison
{
@ -13,6 +15,7 @@ namespace unison
{
private readonly Settings _settingsWin;
private readonly Radios _radiosWin;
private readonly Shuffle _shuffleWin;
private readonly DispatcherTimer _timer;
private readonly MPDHandler _mpd;
@ -25,6 +28,7 @@ namespace unison
_settingsWin = new Settings();
_radiosWin = new Radios();
_shuffleWin = new Shuffle();
_timer = new DispatcherTimer();
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
@ -46,9 +50,13 @@ namespace unison
{
if (_mpd.IsConnected())
{
_mpd.QueryStats();
_settingsWin.UpdateStats();
Snapcast.IsEnabled = true;
ConnectionOkIcon.Visibility = Visibility.Visible;
ConnectionFailIcon.Visibility = Visibility.Collapsed;
_shuffleWin.Initialize();
}
else
{
@ -61,7 +69,7 @@ namespace unison
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
}
public void OnSongChanged(object sender, EventArgs e)
public async void OnSongChanged(object sender, EventArgs e)
{
if (_mpd.GetCurrentSong() == null)
return;
@ -83,7 +91,7 @@ namespace unison
SongAlbum.Text = _mpd.GetCurrentSong().Album;
if (_mpd.GetCurrentSong().Date != null)
SongAlbum.Text += $" ({ _mpd.GetCurrentSong().Date})";
SongAlbum.Text += $" ({_mpd.GetCurrentSong().Date.Split("-")[0]})";
SongGenre.Text = _mpd.GetCurrentSong().Genre;
SongFormat.Text = _mpd.GetCurrentSong().Path.Substring(_mpd.GetCurrentSong().Path.LastIndexOf(".") + 1);
@ -107,6 +115,18 @@ namespace unison
_timer.Start();
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
}
Debug.WriteLine("Song changed called!");
if (!_shuffleWin.GetContinuous())
return;
NextTrack.IsEnabled = false;
PreviousTrack.IsEnabled = false;
await _shuffleWin.HandleContinuous();
NextTrack.IsEnabled = true;
PreviousTrack.IsEnabled = true;
Debug.WriteLine("finished continuous");
}
public void OnStatusChanged(object sender, EventArgs e)
@ -135,9 +155,6 @@ namespace unison
DefaultState();
}
}
_mpd.QueryStats();
_settingsWin.UpdateStats();
}
private void DefaultState(bool LostConnection = false)
@ -210,8 +227,18 @@ namespace unison
}
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 Previous_Clicked(object sender, RoutedEventArgs e)
{
if (PreviousTrack.IsEnabled)
_mpd.Prev();
}
public void Next_Clicked(object sender, RoutedEventArgs e)
{
if (NextTrack.IsEnabled)
_mpd.Next();
}
public void Random_Clicked(object sender, RoutedEventArgs e) => _mpd.Random();
public void Repeat_Clicked(object sender, RoutedEventArgs e) => _mpd.Repeat();
@ -234,6 +261,15 @@ namespace unison
_radiosWin.WindowState = WindowState.Normal;
}
public void Shuffle_Clicked(object sender, RoutedEventArgs e)
{
_shuffleWin.Show();
_shuffleWin.Activate();
if (_shuffleWin.WindowState == WindowState.Minimized)
_shuffleWin.WindowState = WindowState.Normal;
}
public void Settings_Clicked(object sender, RoutedEventArgs e)
{
_settingsWin.Show();

View File

@ -78,16 +78,24 @@ namespace unison
public async void Initialize()
{
List<NameAndCount> Countries = await _radioBrowser.Lists.GetCountriesAsync();
CountryList.Items.Add(new CountryListItem { Name = "", Count = 0 });
foreach (NameAndCount Country in Countries)
try
{
CountryList.Items.Add(new CountryListItem
List<NameAndCount> Countries = await _radioBrowser.Lists.GetCountriesAsync();
CountryList.Items.Add(new CountryListItem { Name = "", Count = 0 });
foreach (NameAndCount Country in Countries)
{
Name = Country.Name,
Count = Country.Stationcount
});
CountryList.Items.Add(new CountryListItem
{
Name = Country.Name,
Count = Country.Stationcount
});
}
}
catch (Exception e)
{
Debug.WriteLine("Exception while getting countries in RadioBrowser: " + e.Message);
return;
}
}

View File

@ -33,20 +33,26 @@
<StackPanel>
<StackPanel>
<TextBlock Text="{x:Static properties:Resources.Settings_Host}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdHost" KeyDown="ConnectHandler" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
<TextBox x:Name="MpdHost" KeyDown="ConnectHandler" TextChanged="MpdConnectTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel>
<StackPanel Margin="0,5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_Port}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdPort" KeyDown="ConnectHandler" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
<TextBox x:Name="MpdPort" KeyDown="ConnectHandler" PreviewTextInput="NumberValidationTextBox" MaxLength="5" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel>
<!--<StackPanel Margin="0,5,0,0">
<TextBlock Text="Password" TextWrapping="Wrap" Margin="5,0,0,0"/>
<TextBox x:Name="MpdPassword" TextWrapping="Wrap" Width="250" Margin="10,2,0,0"/>
</StackPanel>-->
<StackPanel Margin="0,5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_Password}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<PasswordBox x:Name="MpdPassword" Width="250" Margin="10,2,0,0"/>
<TextBlock Text="{x:Static properties:Resources.Settings_ConnectionPasswordInfo}" TextWrapping="Wrap" Margin="10,5,0,0" MaxWidth="250" HorizontalAlignment="Left"/>
</StackPanel>
<TextBlock x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}" TextWrapping="Wrap" Margin="5,10,0,0"/>
<TextBlock TextWrapping="Wrap" Margin="5,10,0,0">
<Run Text="{x:Static properties:Resources.Settings_ConnectionStatus}" FontWeight="Bold" />
<Run x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}"/>
</TextBlock>
<!--<TextBlock x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}" TextWrapping="Wrap" Margin="5,10,0,0"/>-->
<Button x:Name="ConnectButton" Content="{x:Static properties:Resources.Settings_ConnectButton}" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
</StackPanel>
</Grid>
@ -54,60 +60,6 @@
</DockPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.Stats}">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="📊"/>
<Run Text="{x:Static properties:Resources.Stats}"/>
</TextBlock>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<TextBlock>
<Run Text="{x:Static properties:Resources.Stats_Songs}"/><Run Text=" "/><Run x:Name="StatSong"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_Albums}"/><Run Text=" "/><Run x:Name="StatAlbum"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_Artists}"/><Run Text=" "/><Run x:Name="StatArtist"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_TotalPlaytime}"/><Run Text=" "/><Run x:Name="StatTotalPlaytime"/><LineBreak/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_Uptime}"/><Run Text=" "/><Run x:Name="StatUptime"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_TotalTimePlayed}"/><Run Text=" "/><Run x:Name="StatTotalTimePlayed"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_LastDatabaseUpdate}"/><Run Text=" "/><Run x:Name="StatDatabaseUpdate"/>
</TextBlock>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="Snapcast">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<emoji:TextBlock Text="🔊 Snapcast"/>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<StackPanel>
<StackPanel>
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
</CheckBox>
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
</CheckBox>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
<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"/>
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
</TextBlock>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
</StackPanel>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
@ -205,17 +157,98 @@
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_ShortcutsInfo}" TextWrapping="Wrap" Margin="0,2,0,0" MaxWidth="420" />
<TextBlock Text="{x:Static properties:Resources.Settings_ShortcutsInfo}" TextWrapping="Wrap" MaxWidth="440" TextAlignment="Justify" />
</StackPanel>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="ShortcutsReset_Clicked"/>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="Snapcast">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<emoji:TextBlock Text="🔊 Snapcast"/>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<StackPanel>
<StackPanel>
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
</CheckBox>
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
</CheckBox>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
<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"/>
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
</TextBlock>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
</StackPanel>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="Shuffle">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="🔁 "/>
<Run Text="Shuffle"></Run>
</TextBlock>
</GroupBox.Header>
<Grid MaxWidth="500">
<StackPanel>
<TextBlock TextWrapping="Wrap">
<Run>The shuffle window allows to add random songs to your queue. Both options take into account the filter.</Run>
<Run>If the filter is empty, the entire music library is taken into account.</Run><LineBreak/><LineBreak/>
<Run FontWeight="Bold">Add to queue</Run><LineBreak/>
<Run>Add a fixed number of songs to the queue. It can take a long time to add more than 100 songs, so the option is limited to 1000 songs.</Run>
<LineBreak/><LineBreak/>
<Run FontWeight="Bold">Continuous shuffle</Run><LineBreak/>
<Run>By enabling this option, unison will automatically add songs to the queue so you never run out of songs to listen to.</Run>
</TextBlock>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.Stats}">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="📊"/>
<Run Text="{x:Static properties:Resources.Stats}"/>
</TextBlock>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<TextBlock>
<Run Text="{x:Static properties:Resources.Stats_Songs}"/><Run Text=" "/><Run x:Name="StatSong"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_Albums}"/><Run Text=" "/><Run x:Name="StatAlbum"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_Artists}"/><Run Text=" "/><Run x:Name="StatArtist"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_TotalPlaytime}"/><Run Text=" "/><Run x:Name="StatTotalPlaytime"/><LineBreak/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_Uptime}"/><Run Text=" "/><Run x:Name="StatUptime"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_TotalTimePlayed}"/><Run Text=" "/><Run x:Name="StatTotalTimePlayed"/><LineBreak/>
<Run Text="{x:Static properties:Resources.Stats_LastDatabaseUpdate}"/><Run Text=" "/><Run x:Name="StatDatabaseUpdate"/>
</TextBlock>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.Settings_About}" Height="20" VerticalAlignment="Bottom">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">

View File

@ -37,6 +37,7 @@ namespace unison
HotkeyHandler _hotkeys = (HotkeyHandler)Application.Current.Properties["hotkeys"];
public Settings()
{
InitHwnd();
@ -52,7 +53,7 @@ namespace unison
{
MpdHost.Text = Properties.Settings.Default.mpd_host;
MpdPort.Text = Properties.Settings.Default.mpd_port.ToString();
//MpdPassword.Text = Properties.Settings.Default.mpd_password;
MpdPassword.Password = Properties.Settings.Default.mpd_password;
SnapcastStartup.IsChecked = Properties.Settings.Default.snapcast_startup;
SnapcastWindow.IsChecked = Properties.Settings.Default.snapcast_window;
SnapcastPath.Text = Properties.Settings.Default.snapcast_path;
@ -62,6 +63,134 @@ namespace unison
InitializeShortcuts();
}
public void UpdateConnectionStatus()
{
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
{
ConnectionStatus.Text = $"{unison.Resources.Resources.Settings_ConnectionStatusConnected} {mpd.GetVersion()}.";
ConnectButton.IsEnabled = false;
}
else
{
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusOffline;
ConnectButton.IsEnabled = true;
}
}
private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
private void MpdConnectTextBox(object sender, TextChangedEventArgs e)
{
TextBox textBox = (TextBox)sender;
if (textBox.Text == Properties.Settings.Default.mpd_host)
ConnectButton.IsEnabled = false;
else
ConnectButton.IsEnabled = true;
e.Handled = true;
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
ProcessStartInfo psi = new(e.Uri.AbsoluteUri);
psi.UseShellExecute = true;
Process.Start(psi);
e.Handled = true;
}
private void MPDConnect_Clicked(object sender, RoutedEventArgs e)
{
if (!ConnectButton.IsEnabled)
return;
SaveSettings();
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
mpd = new MPDHandler();
ConnectButton.IsEnabled = false;
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
System.Threading.Tasks.Task.Run(() => { mpd.Connect(); });
}
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
{
SnapcastPath.Text = (string)Application.Current.FindResource("snapcastPath");
SnapcastPort.Text = (string)Application.Current.FindResource("snapcastPort");
}
public void UpdateStats()
{
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
StatSong.Text = mpd.GetStats().Songs.ToString();
StatAlbum.Text = mpd.GetStats().Albums.ToString();
StatArtist.Text = mpd.GetStats().Artists.ToString();
StatTotalPlaytime.Text = mpd.GetStats().TotalPlaytime.ToString();
StatUptime.Text = mpd.GetStats().Uptime.ToString();
StatTotalTimePlayed.Text = mpd.GetStats().TotalTimePlayed.ToString();
StatDatabaseUpdate.Text = mpd.GetStats().DatabaseUpdate.ToString();
}
private void ConnectHandler(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)
MPDConnect_Clicked(null, null);
}
private void Window_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
SaveSettings();
WindowState = WindowState.Minimized;
Hide();
}
public void InitHwnd()
{
WindowInteropHelper helper = new(this);
helper.EnsureHandle();
}
public void SaveSettings()
{
Properties.Settings.Default.mpd_host = MpdHost.Text;
Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.mpd_password = MpdPassword.Password;
Properties.Settings.Default.snapcast_startup = (bool)SnapcastStartup.IsChecked;
Properties.Settings.Default.snapcast_window = (bool)SnapcastWindow.IsChecked;
Properties.Settings.Default.snapcast_path = SnapcastPath.Text;
Properties.Settings.Default.snapcast_port = int.Parse(SnapcastPort.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.volume_offset = int.Parse(VolumeOffset.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.nextTrack_mod = GetMod(Shortcut_NextTrack);
Properties.Settings.Default.nextTrack_vk = GetVk(Shortcut_NextTrack);
Properties.Settings.Default.previousTrack_mod = GetMod(Shortcut_PreviousTrack);
Properties.Settings.Default.previousTrack_vk = GetVk(Shortcut_PreviousTrack);
Properties.Settings.Default.playPause_mod = GetMod(Shortcut_PlayPause);
Properties.Settings.Default.playPause_vk = GetVk(Shortcut_PlayPause);
Properties.Settings.Default.volumeUp_mod = GetMod(Shortcut_VolumeUp);
Properties.Settings.Default.volumeUp_vk = GetVk(Shortcut_VolumeUp);
Properties.Settings.Default.volumeDown_mod = GetMod(Shortcut_VolumeDown);
Properties.Settings.Default.volumeDown_vk = GetVk(Shortcut_VolumeDown);
Properties.Settings.Default.volumeMute_mod = GetMod(Shortcut_VolumeMute);
Properties.Settings.Default.volumeMute_vk = GetVk(Shortcut_VolumeMute);
Properties.Settings.Default.showWindow_mod = GetMod(Shortcut_ShowWindow);
Properties.Settings.Default.showWindow_vk = GetVk(Shortcut_ShowWindow);
Properties.Settings.Default.Save();
}
// Hotkeys
void InitializeShortcuts()
{
System.Collections.Generic.IEnumerable<StackPanel> stackPanelCollection = RebindKeyWrapper.Children.OfType<StackPanel>();
@ -108,55 +237,6 @@ namespace unison
}
}
private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
ProcessStartInfo psi = new(e.Uri.AbsoluteUri);
psi.UseShellExecute = true;
Process.Start(psi);
e.Handled = true;
}
public void UpdateConnectionStatus()
{
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
ConnectionStatus.Text = $"{unison.Resources.Resources.Settings_ConnectionStatusConnected} {mpd.GetVersion()}.";
else
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusOffline;
}
private void MPDConnect_Clicked(object sender, RoutedEventArgs e)
{
SaveSettings();
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
mpd.Connect();
}
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
{
SnapcastPath.Text = (string)Application.Current.FindResource("snapcastPath");
SnapcastPort.Text = (string)Application.Current.FindResource("snapcastPort");
}
public void UpdateStats()
{
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
StatSong.Text = mpd.GetStats().Songs.ToString();
StatAlbum.Text = mpd.GetStats().Albums.ToString();
StatArtist.Text = mpd.GetStats().Artists.ToString();
StatTotalPlaytime.Text = mpd.GetStats().TotalPlaytime.ToString();
StatUptime.Text = mpd.GetStats().Uptime.ToString();
StatTotalTimePlayed.Text = mpd.GetStats().TotalTimePlayed.ToString();
StatDatabaseUpdate.Text = mpd.GetStats().DatabaseUpdate.ToString();
}
private void HotkeyChanged()
{
_hotkeys.RemoveHotkeys();
@ -307,54 +387,5 @@ namespace unison
TextBlock textBlock = (TextBlock)button.Content;
return (uint)(HotkeyHandler.VK)System.Enum.Parse(typeof(HotkeyHandler.VK), textBlock.Text, true);
}
public void SaveSettings()
{
Properties.Settings.Default.mpd_host = MpdHost.Text;
Properties.Settings.Default.mpd_port = int.Parse(MpdPort.Text, CultureInfo.InvariantCulture);
//Properties.Settings.Default.mpd_password = MpdPassword.Text;
Properties.Settings.Default.snapcast_startup = (bool)SnapcastStartup.IsChecked;
Properties.Settings.Default.snapcast_window = (bool)SnapcastWindow.IsChecked;
Properties.Settings.Default.snapcast_path = SnapcastPath.Text;
Properties.Settings.Default.snapcast_port = int.Parse(SnapcastPort.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.volume_offset = int.Parse(VolumeOffset.Text, CultureInfo.InvariantCulture);
Properties.Settings.Default.nextTrack_mod = GetMod(Shortcut_NextTrack);
Properties.Settings.Default.nextTrack_vk = GetVk(Shortcut_NextTrack);
Properties.Settings.Default.previousTrack_mod = GetMod(Shortcut_PreviousTrack);
Properties.Settings.Default.previousTrack_vk = GetVk(Shortcut_PreviousTrack);
Properties.Settings.Default.playPause_mod = GetMod(Shortcut_PlayPause);
Properties.Settings.Default.playPause_vk = GetVk(Shortcut_PlayPause);
Properties.Settings.Default.volumeUp_mod = GetMod(Shortcut_VolumeUp);
Properties.Settings.Default.volumeUp_vk = GetVk(Shortcut_VolumeUp);
Properties.Settings.Default.volumeDown_mod = GetMod(Shortcut_VolumeDown);
Properties.Settings.Default.volumeDown_vk = GetVk(Shortcut_VolumeDown);
Properties.Settings.Default.volumeMute_mod = GetMod(Shortcut_VolumeMute);
Properties.Settings.Default.volumeMute_vk = GetVk(Shortcut_VolumeMute);
Properties.Settings.Default.showWindow_mod = GetMod(Shortcut_ShowWindow);
Properties.Settings.Default.showWindow_vk = GetVk(Shortcut_ShowWindow);
Properties.Settings.Default.Save();
}
private void ConnectHandler(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)
MPDConnect_Clicked(null, null);
}
private void Window_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
SaveSettings();
WindowState = WindowState.Minimized;
Hide();
}
public void InitHwnd()
{
WindowInteropHelper helper = new(this);
helper.EnsureHandle();
}
}
}

116
Views/Shuffle.xaml Normal file
View File

@ -0,0 +1,116 @@
<Window x:Class="unison.Shuffle"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
xmlns:local="clr-namespace:unison" xmlns:sys="clr-namespace:System;assembly=System.Runtime"
mc:Ignorable="d"
Title="Shuffle" Closing="Window_Closing" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
<Window.Resources>
<x:Array x:Key="FilterType" Type="sys:String">
<sys:String>Song</sys:String>
<sys:String>Artist</sys:String>
<sys:String>Album</sys:String>
<sys:String>Year</sys:String>
<sys:String>Genre</sys:String>
<sys:String>Directory</sys:String>
</x:Array>
<x:Array x:Key="OperatorTypeA" Type="sys:String">
<sys:String>contains</sys:String>
<sys:String>is</sys:String>
<sys:String>is not</sys:String>
</x:Array>
<x:Array x:Key="OperatorTypeB" Type="sys:String">
<sys:String>is</sys:String>
<sys:String>is not</sys:String>
</x:Array>
<x:Array x:Key="OperatorTypeC" Type="sys:String">
<sys:String>is</sys:String>
</x:Array>
<DataTemplate x:Key="FilterPanel">
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<ComboBox x:Name="FilterType" SelectionChanged="FilterType_SelectionChanged" ItemsSource="{StaticResource FilterType}" SelectedIndex="0" Width="100" ScrollViewer.CanContentScroll="False" FocusVisualStyle="{x:Null}"/>
<ComboBox x:Name="FilterOperator" SelectionChanged="OperatorType_SelectionChanged" ItemsSource="{StaticResource OperatorTypeA}" SelectedIndex="0" Width="80" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
<ComboBox x:Name="FilterList" SelectedIndex="0" Width="240" Visibility="Collapsed" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
<TextBox x:Name="FilterValue" Width="240" Margin="5,0,0,0"/>
<Button Content="-" Padding="5, 2" Click="RemoveFilter_Clicked" Width="20" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="5,0,0,0"/>
<Button Content="+" Padding="5, 2" Click="AddFilter_Clicked" Width="20" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="5,0,0,0"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="5,0,5,5">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="🔡"/>
<Run Text="Filter"/>
</TextBlock>
</GroupBox.Header>
<StackPanel Orientation="Vertical" Margin="5,0,5,0">
<StackPanel x:Name="FilterPanel">
<ContentPresenter ContentTemplate="{StaticResource FilterPanel}"/>
</StackPanel>
<StackPanel x:Name="SongFilterPanel" Margin="0,10,0,0">
<TextBlock>
<Run Text="Number of songs in filter: "/><Run x:Name="SongFilterNumber" FontWeight="Bold"/>
</TextBlock>
</StackPanel>
<StackPanel Margin="0,5,0,0">
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
<Button Content="Query filter" Click="UpdateFilter_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="0,0,10,0"/>
<Button Content="Reset" Click="Reset_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</GroupBox>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<GroupBox DockPanel.Dock="Right" Padding="0,4,0,0" Width="248">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text=""/>
<Run Text="Add to queue"/>
</TextBlock>
</GroupBox.Header>
<StackPanel Orientation="Vertical" Margin="5,5,5,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Text="Songs to add" Margin="0,0,5,5"/>
<TextBox x:Name="SongNumber" PreviewTextInput="QueueValidationTextBox" MaxLength="4" Text="100" Width="35" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<Button Content="Add to queue" Click="AddToQueue_Clicked" Padding="5, 2" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
<TextBlock x:Name="SearchStatus" Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed">
<Run Text="Added "/><Run x:Name="NumberAddedSongs"/><Run Text=" songs"/>
</TextBlock>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox DockPanel.Dock="Left" Padding="0,4,0,0" Width="248" Margin="0,0,5,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="♾️"/>
<Run Text="Continuous shuffle"/>
</TextBlock>
</GroupBox.Header>
<StackPanel Orientation="Horizontal" Margin="5,7,5,0">
<CheckBox x:Name="ContinuousShuffle" Checked="ContinuousShuffle_Checked" Unchecked="ContinuousShuffle_Checked" FocusVisualStyle="{x:Null}" VerticalAlignment="Top">
<TextBlock Text="Enable continuous shuffle" TextWrapping="Wrap"/>
</CheckBox>
</StackPanel>
</GroupBox>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</Window>

493
Views/Shuffle.xaml.cs Normal file
View File

@ -0,0 +1,493 @@
using MpcNET;
using MpcNET.Commands.Database;
using MpcNET.Commands.Reflection;
using MpcNET.Tags;
using MpcNET.Types;
using MpcNET.Types.Filters;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Linq;
using MpcNET.Commands.Queue;
using System.Windows.Input;
using System.Text.RegularExpressions;
namespace unison
{
public partial class Shuffle : Window
{
private MPDHandler _mpd;
bool _continuous = false;
List<string> _songList { get; }
List<string> _genreList { get; }
List<string> _folderList { get; }
List<IFilter> _filters { get; }
public Shuffle()
{
InitializeComponent();
_songList = new();
_genreList = new();
_folderList = new();
_filters = new();
SongFilterNumber.Text = "0";
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
}
public void Initialize()
{
ListGenre();
ListFolder();
}
public async void ListGenre()
{
if (_genreList.Count != 0)
return;
List<string> Response = await _mpd.SafelySendCommandAsync(new ListCommand(MpdTags.Genre, null, null));
if (Response == null)
return;
foreach (string genre in Response)
_genreList.Add(genre);
}
public async void ListFolder()
{
if (_folderList.Count != 0)
return;
IEnumerable<IMpdFilePath> Response = await _mpd.SafelySendCommandAsync(new LsInfoCommand(""));
if (Response == null)
return;
foreach (IMpdFilePath folder in Response)
_folderList.Add(folder.Name);
}
private bool IsFilterEmpty()
{
if (_filters.Count() == 0)
return true;
return false;
}
private void Window_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
WindowState = WindowState.Minimized;
Hide();
}
public void InitHwnd()
{
WindowInteropHelper helper = new(this);
helper.EnsureHandle();
}
private bool IsOnMainThread()
{
return App.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread;
}
private T FindParent<T>(DependencyObject child) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(child);
if (parent == null)
return null;
if (parent is T)
return parent as T;
else
return FindParent<T>(parent);
}
private void AddFilter_Clicked(object sender, RoutedEventArgs e)
{
FilterPanel.Children.Add(new ContentPresenter { ContentTemplate = (DataTemplate)FindResource("FilterPanel") });
SongFilterNumber.Text = "0";
}
private void RemoveFilter_Clicked(object sender, RoutedEventArgs e)
{
if (FilterPanel.Children.Count > 1)
FilterPanel.Children.Remove(FindParent<ContentPresenter>(sender as Button));
else
Reset_Clicked(null, null);
SongFilterNumber.Text = "0";
}
private void Reset_Clicked(object sender, RoutedEventArgs e)
{
FilterPanel.Children.RemoveRange(0, FilterPanel.Children.Count);
FilterPanel.Children.Add(new ContentPresenter { ContentTemplate = (DataTemplate)FindResource("FilterPanel") });
SongFilterNumber.Text = "0";
}
private ITag FilterEquivalence_Type(string value)
{
if (value == "Song")
return MpdTags.Title;
else if (value == "Artist")
return MpdTags.Artist;
else if (value == "Album")
return MpdTags.Album;
else if (value == "Year")
return MpdTags.Date;
else if (value == "Genre")
return MpdTags.Genre;
return MpdTags.Title;
}
private FilterOperator FilterEquivalence_Operator(string value)
{
if (value == "contains")
return FilterOperator.Contains;
else if (value == "is")
return FilterOperator.Equal;
else if (value == "is not")
return FilterOperator.Different;
return FilterOperator.Equal;
}
private async void UpdateFilter_Clicked(object sender, RoutedEventArgs e)
{
_filters.Clear();
Debug.WriteLine("is on main thread => " + IsOnMainThread());
foreach (ContentPresenter superChild in FilterPanel.Children)
{
ITag tag = MpdTags.Title;
FilterOperator op = FilterOperator.None;
string value = "";
bool isDir = false;
StackPanel stackPanel = VisualTreeHelper.GetChild(superChild, 0) as StackPanel;
foreach (TextBox child in stackPanel.Children.OfType<TextBox>())
{
if (child.Name == "FilterValue")
value = child.Text;
}
foreach (ComboBox child in stackPanel.Children.OfType<ComboBox>())
{
if (child.Name == "FilterType")
{
if (child.SelectedItem.ToString() == "Directory")
isDir = true;
else
tag = FilterEquivalence_Type(child.SelectedItem.ToString());
}
if (child.Name == "FilterOperator")
op = FilterEquivalence_Operator(child.SelectedItem.ToString());
if (child.Name == "FilterList" && child.Visibility == Visibility.Visible)
value = child.SelectedItem.ToString();
}
if (value != "")
{
if (!isDir)
_filters.Add(new FilterTag(tag, value, op));
else
_filters.Add(new FilterBase(value, FilterOperator.None));
await GetSongsFromFilter();
}
}
}
private async Task GetSongsFromFilter()
{
Debug.WriteLine("get songs from filter => start");
Debug.WriteLine("is on main thread => " + IsOnMainThread());
_songList.Clear();
SongFilterPanel.Visibility = Visibility.Visible;
int song = _mpd.GetStats().Songs;
Debug.WriteLine("get songs from filter => before command");
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new SearchCommand(_filters, 0, song + 1) });
string Response = await _mpd.SafelySendCommandAsync(commandList);
Debug.WriteLine("get songs from filter => after command");
// create a list of the file url
string[] value = Response.Split(", [file, ");
// there are no song in this filter
if (value[0] == "")
{
SongFilterNumber.Text = _songList.Count.ToString();
return;
}
Debug.WriteLine("get songs from filter => before adding to list");
foreach (string file in value)
{
int start = 0;
int end = file.IndexOf("],");
string filePath = file.Substring(start, end - start);
_songList.Add(filePath);
SongFilterNumber.Text = _songList.Count.ToString();
}
Debug.WriteLine("get songs from filter => after adding to list");
// remove characters from first file
_songList[0] = _songList[0].Substring(7, _songList[0].Length - 7);
SongFilterPanel.Visibility = Visibility.Visible;
SongFilterNumber.Text = _songList.Count.ToString();
Debug.WriteLine("get songs from filter => finish");
}
private void FilterType_Change(object sender, string Operator, List<string> Listing)
{
ComboBox comboBox = sender as ComboBox;
StackPanel stackPanel = comboBox.Parent as StackPanel;
foreach (ComboBox child in stackPanel.Children.OfType<ComboBox>())
{
if (child.Name == "FilterOperator")
{
child.ItemsSource = (Array)FindResource(Operator);
child.SelectedItem = child.Items[0];
}
if (child.Name == "FilterList")
{
child.Visibility = Visibility.Visible;
child.ItemsSource = Listing;
child.SelectedItem = child.Items[0];
}
}
foreach (TextBox child in stackPanel.Children.OfType<TextBox>())
{
if (child.Name == "FilterValue")
child.Visibility = Visibility.Collapsed;
}
SongFilterNumber.Text = "0";
}
private void FilterType_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
string item = e.AddedItems[0].ToString();
if (item == "Genre")
FilterType_Change(sender, "OperatorTypeB", _genreList);
else if (item == "Directory")
FilterType_Change(sender, "OperatorTypeC", _folderList);
else
{
ComboBox combobox = sender as ComboBox;
StackPanel stackpanel = combobox.Parent as StackPanel;
foreach (ComboBox child in stackpanel.Children.OfType<ComboBox>())
{
if (child.Name == "FilterOperator")
{
child.ItemsSource = (Array)FindResource("OperatorTypeA");
child.SelectedItem = child.Items[0];
}
if (child.Name == "FilterList")
child.Visibility = Visibility.Collapsed;
}
foreach (TextBox child in stackpanel.Children.OfType<TextBox>())
{
if (child.Name == "FilterValue")
child.Visibility = Visibility.Visible;
}
}
SongFilterNumber.Text = "0";
}
private void OperatorType_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.WriteLine("selection changed => operator type");
SongFilterNumber.Text = "0";
}
private void QueueValidationTextBox(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
private void QueueValidationNumber()
{
if (int.Parse(SongNumber.Text) < 1)
SongNumber.Text = "1";
if (int.Parse(SongNumber.Text) > 1000)
SongNumber.Text = "1000";
}
private async void AddToQueue_Clicked(object sender, RoutedEventArgs e)
{
QueueValidationNumber();
if (_mpd.GetStats() == null)
return;
if (IsFilterEmpty())
await AddToQueueRandom(int.Parse(SongNumber.Text)); // @TODO await or not???
else
{
UpdateFilter_Clicked(null, null);
await AddToQueueFilter(int.Parse(SongNumber.Text)); // @TODO await or not???
}
}
private async /*void*/ Task AddToQueueRandom(int SongNumber)
{
int AddedSongs = 0;
NumberAddedSongs.Text = AddedSongs.ToString();
SearchStatus.Visibility = Visibility.Visible;
Debug.WriteLine("song to add => " + SongNumber);
for (int i = 0; i < SongNumber; i++)
{
// generate random number
int song = new Random().Next(0, _mpd.GetStats().Songs - 1);
// query random song
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new SearchCommand(new FilterTag(MpdTags.Title, "", FilterOperator.Contains), song, song + 1) });
string Response = await _mpd.SafelySendCommandAsync(commandList);
await Task.Delay(1);
if (Response.Length > 0)
{
// parse song and add it to queue
int start = Response.IndexOf("[file, ");
int end = Response.IndexOf("],");
string filePath = Response.Substring(start + 7, end - (start + 7));
_mpd.AddSong(filePath);
Debug.WriteLine("song path => " + filePath);
AddedSongs++;
NumberAddedSongs.Text = AddedSongs.ToString();
}
}
// TODO make play at first position added, as soon as possible
if (!_mpd.IsPlaying())
_mpd.Play(0);
SearchStatus.Visibility = Visibility.Collapsed;
}
private async /*void*/ Task AddToQueueFilter(int SongNumber)
{
int AddedSongs = 0;
NumberAddedSongs.Text = AddedSongs.ToString();
SearchStatus.Visibility = Visibility.Visible;
Debug.WriteLine("song to add => " + SongNumber);
// more requested songs than available => add everything
if (SongNumber > _songList.Count)
{
foreach (string path in _songList)
{
await Task.Delay(1);
_mpd.AddSong(path);
Debug.WriteLine("song path => " + path);
AddedSongs++;
NumberAddedSongs.Text = AddedSongs.ToString();
}
}
// more available songs than requested =>
// we add unique indexes until we reach the requested amount
else
{
HashSet<int> SongIndex = new();
while (SongIndex.Count < SongNumber)
{
int MaxIndex = new Random().Next(0, _songList.Count - 1);
SongIndex.Add(MaxIndex);
}
foreach (int index in SongIndex)
_mpd.AddSong(_songList[index]);
}
SearchStatus.Visibility = Visibility.Collapsed;
}
public bool GetContinuous()
{
return _continuous;
}
public async Task HandleContinuous()
{
if (!GetContinuous())
return;
Debug.WriteLine("is on main thread => " + IsOnMainThread());
IEnumerable<IMpdFile> Playlist = await _mpd.SafelySendCommandAsync(new PlaylistCommand());
int queueSize = 0;
foreach (IMpdFile file in Playlist)
{
Debug.WriteLine(file.Path);
queueSize++;
}
Debug.WriteLine("queue size is: " + queueSize);
if (queueSize < 5)
{
if (_mpd.GetStats() == null)
return;
if (IsFilterEmpty())
await AddToQueueRandom(5); // @TODO await or not?
else
await AddToQueueFilter(5); // @TODO await or not?
}
}
private async void ContinuousShuffle_Checked(object sender, RoutedEventArgs e)
{
if (ContinuousShuffle.IsChecked == true)
{
_continuous = true;
_songList.Clear();
if (!IsFilterEmpty())
UpdateFilter_Clicked(null, null);
}
else
_continuous = false;
await HandleContinuous();
}
}
}

View File

@ -30,11 +30,11 @@
<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>
<Image Width="16" Height="16" emoji:Image.Source="🔀" />
</MenuItem.Icon>
</MenuItem>-->
</MenuItem>
<MenuItem Header="{x:Static properties:Resources.Settings}" Command="{Binding Settings}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🛠️" />

View File

@ -71,6 +71,18 @@ namespace unison
}
}
public ICommand Shuffle
{
get
{
return new DelegateCommand
{
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Shuffle_Clicked(null, null),
CanExecuteFunc = () => true
};
}
}
public ICommand Settings
{
get

View File

@ -7,7 +7,7 @@
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
<Win32Resource></Win32Resource>
<StartupObject>unison.App</StartupObject>
<Version>1.2</Version>
<Version>1.3</Version>
<Company />
<Authors>Théo Marchal</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
@ -47,7 +47,7 @@
<PackageReference Include="Emoji.Wpf" Version="0.3.3" />
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
<PackageReference Include="RadioBrowser" Version="0.6.1" />
<PackageReference Include="MpcNET" Version="1.3.0" />
<PackageReference Include="MpcNET" Version="1.4.0" />
</ItemGroup>
<ItemGroup>
@ -64,6 +64,9 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Resources.es-ES.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.fr-FR.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>