22 Commits

Author SHA1 Message Date
15db118dd6 Add GitHub link in Settings 2022-04-05 00:36:14 +02:00
67315f90b0 Fix RadioBrowser crashes if no internet connection 2022-04-05 00:23:55 +02:00
7fdf349c28 Fix crash when the IP is invalid, also supports hostnames 2022-04-01 14:07:58 +02:00
9a5f686dde Fix exception on connection 2022-03-31 00:27:33 +02:00
b209cb2556 Update Snapcast: v0.25 to v0.26 2022-03-30 13:04:10 +02:00
6e1f43bed0 Switch to new MpcNET package 2022-03-30 02:05:13 +02:00
9bd088fac8 Stats translation and resources 2021-10-05 18:54:49 +02:00
0be28ab205 Stats panel in settings 2021-10-05 14:18:58 +02:00
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
31 changed files with 839 additions and 94 deletions

View File

@ -28,6 +28,7 @@ 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 MainWindow _appWindow;
@ -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);
} }
} }
@ -79,6 +81,9 @@ namespace unison
case VK_VOLUME_UP: case VK_VOLUME_UP:
_mpd.VolumeUp(); _mpd.VolumeUp();
break; break;
case VK_VOLUME_MUTE:
_mpd.VolumeMute();
break;
case VK_MEDIA_PLAY_PAUSE: case VK_MEDIA_PLAY_PAUSE:
_mpd.PlayPause(); _mpd.PlayPause();
break; break;

View File

@ -12,17 +12,31 @@ 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.Status; using MpcNET.Commands.Status;
using MpcNET.Message; using MpcNET.Message;
using MpcNET.Types; using MpcNET.Types;
namespace unison namespace unison
{ {
public class Statistics
{
public int Songs { get; set; }
public int Albums { get; set; }
public int Artists { get; set; }
public string TotalPlaytime { get; set; }
public string Uptime { get; set; }
public string TotalTimePlayed { get; set; }
public string DatabaseUpdate { get; set; }
}
public class MPDHandler public class MPDHandler
{ {
private bool _connected; private bool _connected;
public string _version; public string _version;
private int _currentVolume; private int _currentVolume;
private int _previousVolume;
private bool _currentRandom; private bool _currentRandom;
private bool _currentRepeat; private bool _currentRepeat;
private bool _currentSingle; private bool _currentSingle;
@ -33,12 +47,15 @@ namespace unison
private MpdStatus _currentStatus; private MpdStatus _currentStatus;
private IMpdFile _currentSong; private IMpdFile _currentSong;
private BitmapFrame _cover; private BitmapFrame _cover;
public Statistics _stats;
private readonly System.Timers.Timer _elapsedTimer; private readonly System.Timers.Timer _elapsedTimer;
private DispatcherTimer _retryTimer; private DispatcherTimer _retryTimer;
bool _isUpdatingStatus = false; bool _isUpdatingStatus = false;
bool _isUpdatingSong = false; bool _isUpdatingSong = false;
bool _invalidIp = false;
private event EventHandler ConnectionChanged; private event EventHandler ConnectionChanged;
private event EventHandler StatusChanged; private event EventHandler StatusChanged;
private event EventHandler SongChanged; private event EventHandler SongChanged;
@ -55,6 +72,8 @@ namespace unison
Initialize(null, null); Initialize(null, null);
_stats = new Statistics();
_retryTimer = new DispatcherTimer(); _retryTimer = new DispatcherTimer();
_retryTimer.Interval = TimeSpan.FromSeconds(5); _retryTimer.Interval = TimeSpan.FromSeconds(5);
_retryTimer.Tick += Initialize; _retryTimer.Tick += Initialize;
@ -78,7 +97,7 @@ namespace unison
void OnConnectionChanged(object sender, EventArgs e) void OnConnectionChanged(object sender, EventArgs e)
{ {
if (!_connected) if (!_connected && !_invalidIp)
_retryTimer.Start(); _retryTimer.Start();
else else
_retryTimer.Stop(); _retryTimer.Stop();
@ -172,12 +191,13 @@ namespace unison
_connection = await ConnectInternal(token); _connection = await ConnectInternal(token);
_commandConnection = await ConnectInternal(token); _commandConnection = await ConnectInternal(token);
} }
catch(MpcNET.Exceptions.MpcConnectException exception) catch(MpcNET.Exceptions.MpcConnectException)
{ {
Trace.WriteLine("exception: " + exception); _invalidIp = true;
} }
if (_connection != null && _commandConnection != null) if (_connection != null && _commandConnection != null)
{ {
_invalidIp = false;
if (_connection.IsConnected && _commandConnection.IsConnected) if (_connection.IsConnected && _commandConnection.IsConnected)
{ {
_connected = true; _connected = true;
@ -201,6 +221,21 @@ namespace unison
{ {
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress); IPAddress.TryParse(Properties.Settings.Default.mpd_host, out IPAddress ipAddress);
if (ipAddress == null)
{
IPAddress[] addrList;
try
{
addrList = Dns.GetHostAddresses(Properties.Settings.Default.mpd_host);
if (addrList.Length > 0)
ipAddress = addrList[0];
}
catch (Exception)
{
throw new MpcNET.Exceptions.MpcConnectException("No correct IP provided by user.");
}
}
_mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port); _mpdEndpoint = new IPEndPoint(ipAddress, Properties.Settings.Default.mpd_port);
MpcConnection connection = new MpcConnection(_mpdEndpoint); MpcConnection connection = new MpcConnection(_mpdEndpoint);
await connection.ConnectAsync(token); await connection.ConnectAsync(token);
@ -331,14 +366,14 @@ namespace unison
List<byte> data = new List<byte>(); List<byte> data = new List<byte>();
try try
{ {
if (_connection == null)
return;
long totalBinarySize = 9999; long totalBinarySize = 9999;
long currentSize = 0; long currentSize = 0;
do do
{ {
if (_connection == null)
return;
var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize)); var albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
if (!albumReq.IsResponseValid) if (!albumReq.IsResponseValid)
break; break;
@ -363,7 +398,14 @@ namespace unison
else else
{ {
using MemoryStream stream = new MemoryStream(data.ToArray()); using MemoryStream stream = new MemoryStream(data.ToArray());
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); try
{
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
catch (System.NotSupportedException)
{
_cover = null;
}
} }
UpdateCover(); UpdateCover();
} }
@ -407,6 +449,7 @@ namespace unison
public MpdStatus GetStatus() => _currentStatus; public MpdStatus GetStatus() => _currentStatus;
public BitmapFrame GetCover() => _cover; public BitmapFrame GetCover() => _cover;
public string GetVersion() => _version; public string GetVersion() => _version;
public Statistics GetStats() => _stats;
public double GetCurrentTime() => _currentTime; public double GetCurrentTime() => _currentTime;
public bool IsConnected() => _connected; public bool IsConnected() => _connected;
@ -439,5 +482,55 @@ namespace unison
_currentVolume = 0; _currentVolume = 0;
SetVolume(_currentVolume); 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);
}
public async void QueryStats()
{
Dictionary<string, string> response = await SafelySendCommandAsync(new StatsCommand());
_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"]));
_stats.Uptime = time.ToString(@"dd\:hh\:mm\:ss");
time = TimeSpan.FromSeconds(int.Parse(response["db_playtime"]));
_stats.TotalPlaytime = time.ToString(@"dd\:hh\:mm\:ss");
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();
_stats.DatabaseUpdate = date.ToString("dd/MM/yyyy @ HH:mm");
}
} }
} }

View File

@ -1,4 +1,4 @@
MIT License Copyright (c) 2021 Théo Marchal MIT License Copyright (c) 2022 Théo Marchal
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -73,7 +73,19 @@ namespace unison.Properties {
[global::System.Configuration.UserScopedSettingAttribute()] [global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("snapclient_0.25.0-1_win64")] [global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool snapcast_window {
get {
return ((bool)(this["snapcast_window"]));
}
set {
this["snapcast_window"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("snapclient_0.26.0-1_win64")]
public string snapcast_path { public string snapcast_path {
get { get {
return ((string)(this["snapcast_path"])); return ((string)(this["snapcast_path"]));
@ -106,17 +118,5 @@ 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

@ -18,7 +18,7 @@
<Value Profile="(Default)">False</Value> <Value Profile="(Default)">False</Value>
</Setting> </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.26.0-1_win64</Value>
</Setting> </Setting>
<Setting Name="snapcast_port" Type="System.Int32" Scope="User"> <Setting Name="snapcast_port" Type="System.Int32" Scope="User">
<Value Profile="(Default)">1704</Value> <Value Profile="(Default)">1704</Value>

View File

@ -7,6 +7,7 @@
* lightweight window that can be toggled with shortcuts * lightweight window that can be toggled with shortcuts
* music control through shortcuts * music control through shortcuts
* [Snapcast](https://mjaggard.github.io/snapcast/) integration * [Snapcast](https://mjaggard.github.io/snapcast/) integration
* Radio stations
## Features ## Features
@ -26,6 +27,12 @@ You can control your music at anytime with the shortcuts. They can of course be
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. 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 ## Caveats
### Missing features ### Missing features
@ -37,7 +44,6 @@ The main goal of embedding Snapcast is the ability to listen locally to music wh
### Wanted features ### Wanted features
* A complete shuffle system based on set criteria, aka a smart playlist. * A complete shuffle system based on set criteria, aka a smart playlist.
* Radio integration.
## Translations ## Translations

View File

@ -69,6 +69,87 @@ namespace unison.Resources {
} }
} }
/// <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> /// <summary>
/// Looks up a localized string similar to Settings. /// Looks up a localized string similar to Settings.
/// </summary> /// </summary>
@ -168,15 +249,6 @@ namespace unison.Resources {
} }
} }
/// <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> /// <summary>
/// Looks up a localized string similar to Next track. /// Looks up a localized string similar to Next track.
/// </summary> /// </summary>
@ -339,6 +411,15 @@ namespace unison.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Volume mute.
/// </summary>
public static string Settings_VolumeMute {
get {
return ResourceManager.GetString("Settings_VolumeMute", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Volume offset. /// Looks up a localized string similar to Volume offset.
/// </summary> /// </summary>
@ -411,6 +492,78 @@ namespace unison.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Stats.
/// </summary>
public static string Stats {
get {
return ResourceManager.GetString("Stats", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Albums:.
/// </summary>
public static string Stats_Albums {
get {
return ResourceManager.GetString("Stats_Albums", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Artists:.
/// </summary>
public static string Stats_Artists {
get {
return ResourceManager.GetString("Stats_Artists", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Last database update:.
/// </summary>
public static string Stats_LastDatabaseUpdate {
get {
return ResourceManager.GetString("Stats_LastDatabaseUpdate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Songs:.
/// </summary>
public static string Stats_Songs {
get {
return ResourceManager.GetString("Stats_Songs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Total playtime:.
/// </summary>
public static string Stats_TotalPlaytime {
get {
return ResourceManager.GetString("Stats_TotalPlaytime", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Total time played:.
/// </summary>
public static string Stats_TotalTimePlayed {
get {
return ResourceManager.GetString("Stats_TotalTimePlayed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to MPD uptime:.
/// </summary>
public static string Stats_Uptime {
get {
return ResourceManager.GetString("Stats_Uptime", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Stop Snapcast. /// Looks up a localized string similar to Stop Snapcast.
/// </summary> /// </summary>

View File

@ -120,6 +120,33 @@
<data name="Exit" xml:space="preserve"> <data name="Exit" xml:space="preserve">
<value>Quitter</value> <value>Quitter</value>
</data> </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"> <data name="Settings" xml:space="preserve">
<value>Configuration</value> <value>Configuration</value>
</data> </data>
@ -153,9 +180,6 @@
<data name="Settings_MadeBy" xml:space="preserve"> <data name="Settings_MadeBy" xml:space="preserve">
<value>Créé par</value> <value>Créé par</value>
</data> </data>
<data name="Settings_MpcNET" xml:space="preserve">
<value> et sa version de MpcNET</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve"> <data name="Settings_NextTrack" xml:space="preserve">
<value>Piste suivante</value> <value>Piste suivante</value>
</data> </data>
@ -210,6 +234,9 @@
<data name="Settings_VolumeDown" xml:space="preserve"> <data name="Settings_VolumeDown" xml:space="preserve">
<value>Baisse de volume</value> <value>Baisse de volume</value>
</data> </data>
<data name="Settings_VolumeMute" xml:space="preserve">
<value>Volume en sourdine</value>
</data>
<data name="Settings_VolumeOffset" xml:space="preserve"> <data name="Settings_VolumeOffset" xml:space="preserve">
<value>Écart de volume</value> <value>Écart de volume</value>
</data> </data>
@ -234,6 +261,30 @@
<data name="StartSnapcast" xml:space="preserve"> <data name="StartSnapcast" xml:space="preserve">
<value>Démarrer Snapcast</value> <value>Démarrer Snapcast</value>
</data> </data>
<data name="Stats" xml:space="preserve">
<value>Stats</value>
</data>
<data name="Stats_Albums" xml:space="preserve">
<value>Albums :</value>
</data>
<data name="Stats_Artists" xml:space="preserve">
<value>Artistes :</value>
</data>
<data name="Stats_LastDatabaseUpdate" xml:space="preserve">
<value>Mise à jour de la base de données :</value>
</data>
<data name="Stats_Songs" xml:space="preserve">
<value>Morceaux :</value>
</data>
<data name="Stats_TotalPlaytime" xml:space="preserve">
<value>Temps total :</value>
</data>
<data name="Stats_TotalTimePlayed" xml:space="preserve">
<value>Temps d'écoute écoulé :</value>
</data>
<data name="Stats_Uptime" xml:space="preserve">
<value>MPD lancé depuis : </value>
</data>
<data name="StopSnapcast" xml:space="preserve"> <data name="StopSnapcast" xml:space="preserve">
<value>Stopper Snapcast</value> <value>Stopper Snapcast</value>
</data> </data>

View File

@ -120,6 +120,33 @@
<data name="Exit" xml:space="preserve"> <data name="Exit" xml:space="preserve">
<value>Exit</value> <value>Exit</value>
</data> </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"> <data name="Settings" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
@ -153,9 +180,6 @@
<data name="Settings_MadeBy" xml:space="preserve"> <data name="Settings_MadeBy" xml:space="preserve">
<value>Made by</value> <value>Made by</value>
</data> </data>
<data name="Settings_MpcNET" xml:space="preserve">
<value>'s updated MpcNET</value>
</data>
<data name="Settings_NextTrack" xml:space="preserve"> <data name="Settings_NextTrack" xml:space="preserve">
<value>Next track</value> <value>Next track</value>
</data> </data>
@ -210,6 +234,9 @@
<data name="Settings_VolumeDown" xml:space="preserve"> <data name="Settings_VolumeDown" xml:space="preserve">
<value>Volume down</value> <value>Volume down</value>
</data> </data>
<data name="Settings_VolumeMute" xml:space="preserve">
<value>Volume mute</value>
</data>
<data name="Settings_VolumeOffset" xml:space="preserve"> <data name="Settings_VolumeOffset" xml:space="preserve">
<value>Volume offset</value> <value>Volume offset</value>
</data> </data>
@ -234,6 +261,30 @@
<data name="StartSnapcast" xml:space="preserve"> <data name="StartSnapcast" xml:space="preserve">
<value>Start Snapcast</value> <value>Start Snapcast</value>
</data> </data>
<data name="Stats" xml:space="preserve">
<value>Stats</value>
</data>
<data name="Stats_Albums" xml:space="preserve">
<value>Albums:</value>
</data>
<data name="Stats_Artists" xml:space="preserve">
<value>Artists:</value>
</data>
<data name="Stats_LastDatabaseUpdate" xml:space="preserve">
<value>Last database update:</value>
</data>
<data name="Stats_Songs" xml:space="preserve">
<value>Songs:</value>
</data>
<data name="Stats_TotalPlaytime" xml:space="preserve">
<value>Total playtime:</value>
</data>
<data name="Stats_TotalTimePlayed" xml:space="preserve">
<value>Total time played:</value>
</data>
<data name="Stats_Uptime" xml:space="preserve">
<value>MPD uptime:</value>
</data>
<data name="StopSnapcast" xml:space="preserve"> <data name="StopSnapcast" xml:space="preserve">
<value>Stop Snapcast</value> <value>Stop Snapcast</value>
</data> </data>

View File

@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"> xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="snapcastPath">snapclient_0.25.0-1_win64</system:String> <system:String x:Key="snapcastPath">snapclient_0.26.0-1_win64</system:String>
<system:String x:Key="snapcastPort">1704</system:String> <system:String x:Key="snapcastPort">1704</system:String>
<system:String x:Key="connectionOk1">&#xf385;</system:String> <system:String x:Key="connectionOk1">&#xf385;</system:String>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 50 KiB

BIN
Screenshots/screen4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -28,7 +28,7 @@
</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"/>
@ -110,12 +110,20 @@
</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">
<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="0,0,10,0">
<StackPanel Orientation="Horizontal"> <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">
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/> <StackPanel Orientation="Horizontal">
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/> <emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
</StackPanel> <TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
</Button> </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">
<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"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid x:Name="ConnectionOkIcon" 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="&#xf385;" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" VerticalAlignment="Center" HorizontalAlignment="Center" />

View File

@ -12,6 +12,7 @@ namespace unison
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly Settings _settingsWin; private readonly Settings _settingsWin;
private readonly Radios _radiosWin;
private readonly DispatcherTimer _timer; private readonly DispatcherTimer _timer;
private readonly MPDHandler _mpd; private readonly MPDHandler _mpd;
@ -23,6 +24,7 @@ namespace unison
WindowState = WindowState.Minimized; WindowState = WindowState.Minimized;
_settingsWin = new Settings(); _settingsWin = new Settings();
_radiosWin = new Radios();
_timer = new DispatcherTimer(); _timer = new DispatcherTimer();
_mpd = (MPDHandler)Application.Current.Properties["mpd"]; _mpd = (MPDHandler)Application.Current.Properties["mpd"];
@ -68,11 +70,12 @@ namespace unison
SongTitle.Text = _mpd.GetCurrentSong().Title; SongTitle.Text = _mpd.GetCurrentSong().Title;
else if (_mpd.GetCurrentSong().HasName && _mpd.GetCurrentSong().Name.Length > 0) else if (_mpd.GetCurrentSong().HasName && _mpd.GetCurrentSong().Name.Length > 0)
SongTitle.Text = _mpd.GetCurrentSong().Name; SongTitle.Text = _mpd.GetCurrentSong().Name;
else else if (_mpd.GetCurrentSong().Path != null)
{ {
int start = _mpd.GetCurrentSong().Path.LastIndexOf("/") + 1; int start = _mpd.GetCurrentSong().Path.LastIndexOf("/") + 1;
int end = _mpd.GetCurrentSong().Path.LastIndexOf("."); int end = _mpd.GetCurrentSong().Path.LastIndexOf(".");
SongTitle.Text = _mpd.GetCurrentSong().Path.Substring(start, end - start); if (start > 0 && end > 0 && end > start)
SongTitle.Text = _mpd.GetCurrentSong().Path.Substring(start, end - start);
} }
SongTitle.ToolTip = _mpd.GetCurrentSong().Path; SongTitle.ToolTip = _mpd.GetCurrentSong().Path;
@ -132,6 +135,9 @@ namespace unison
DefaultState(); DefaultState();
} }
} }
_mpd.QueryStats();
_settingsWin.UpdateStats();
} }
private void DefaultState(bool LostConnection = false) private void DefaultState(bool LostConnection = false)
@ -182,6 +188,11 @@ namespace unison
SnapcastText.Text = unison.Resources.Resources.StartSnapcast; SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
} }
public void OnRadioBrowserConnected()
{
Radio.IsEnabled = true;
}
public void UpdateButton(ref Border border, bool b) public void UpdateButton(ref Border border, bool b)
{ {
border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"]; border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
@ -214,6 +225,15 @@ namespace unison
snapcast.LaunchOrExit(); snapcast.LaunchOrExit();
} }
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)
{ {
_settingsWin.Show(); _settingsWin.Show();
@ -254,6 +274,17 @@ 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);

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>

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

@ -0,0 +1,208 @@
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();
try
{
_radioBrowser = new RadioBrowserClient();
Initialize();
}
catch (Exception e)
{
Debug.WriteLine("Exception while connecting to RadioBrowser: " + e.Message);
return;
}
Application.Current.Dispatcher.Invoke(() =>
{
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnRadioBrowserConnected();
});
}
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)
{
try
{
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;
}
catch (Exception except)
{
Debug.WriteLine("Error on RadioBrowser search advanced: " + except.Message);
}
}
private void FitToContent()
{
foreach (DataGridColumn column in RadioListGrid.Columns)
column.Width = new DataGridLength(1.0, DataGridLengthUnitType.SizeToCells);
}
private void Row_DoubleClick(object sender, MouseButtonEventArgs e)
{
DataGrid grid = sender as DataGrid;
StationListItem station;
try
{
station = grid.Items[grid.SelectedIndex] as StationListItem;
}
catch (ArgumentOutOfRangeException)
{
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)
{
try
{
CountryListItem a = (CountryListItem)CountryList.SelectedItem;
await SearchAdvanced(NameSearch.Text, a?.Name, TagSearch.Text);
}
catch (Exception except)
{
Debug.WriteLine("Error on RadioBrowser search: " + except.Message);
}
}
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

@ -14,23 +14,21 @@
<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"> <TextBlock>
<TextBlock> <emoji:EmojiInline Text="📶"/>
<emoji:EmojiInline Text="📶"/> <Run Text="{x:Static properties:Resources.Settings_Connection}"/>
<Run Text="{x:Static properties:Resources.Settings_Connection}"/> </TextBlock>
</TextBlock>
</StackPanel>
</GroupBox.Header> </GroupBox.Header>
<Grid VerticalAlignment="Top"> <Grid VerticalAlignment="Top">
<StackPanel> <StackPanel>
<StackPanel> <StackPanel>
<TextBlock Text="{x:Static properties:Resources.Settings_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="{x:Static properties:Resources.Settings_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">
@ -46,13 +44,35 @@
</DockPanel> </DockPanel>
</TabItem> </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"> <TabItem Header="Snapcast">
<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"> <emoji:TextBlock Text="🔊 Snapcast"/>
<emoji:TextBlock Text="🔊 Snapcast"/>
</StackPanel>
</GroupBox.Header> </GroupBox.Header>
<Grid VerticalAlignment="Top"> <Grid VerticalAlignment="Top">
<StackPanel> <StackPanel>
@ -82,17 +102,15 @@
<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"> <TextBlock>
<TextBlock> <emoji:EmojiInline Text="⌨️ "/>
<emoji:EmojiInline Text="⌨️ "/> <Run Text="{x:Static properties:Resources.Settings_Shortcuts}"></Run>
<Run Text="{x:Static properties:Resources.Settings_Shortcuts}"></Run> </TextBlock>
</TextBlock>
</StackPanel>
</GroupBox.Header> </GroupBox.Header>
<Grid> <Grid>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Static properties:Resources.Settings_VolumeOffset}" TextWrapping="Wrap"/> <TextBlock Text="{x:Static properties:Resources.Settings_VolumeOffset}" TextWrapping="Wrap" Margin="0,2,0,0"/>
<TextBox x:Name="VolumeOffset" TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="8,2,0,0"/> <TextBox x:Name="VolumeOffset" TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="8,2,0,0"/>
</StackPanel> </StackPanel>
<Grid MinWidth="300" Margin="0,5,0,0"> <Grid MinWidth="300" Margin="0,5,0,0">
@ -107,20 +125,23 @@
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="{x:Static properties:Resources.Settings_NextTrack}" 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="{x:Static properties:Resources.Settings_PreviousTrack}" 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="{x:Static properties:Resources.Settings_PlayPause}" 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="{x:Static properties:Resources.Settings_VolumeUp}" 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="{x:Static properties:Resources.Settings_VolumeDown}" 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="{x:Static properties:Resources.Settings_ShowWindow}" 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>
@ -142,13 +163,14 @@
</TextBlock> </TextBlock>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top"> <TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
<Run Text="{x:Static properties:Resources.Settings_AboutInfo}" /><LineBreak/> <Run Text="{x:Static properties:Resources.Settings_AboutInfo}" /><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/Difegue/MpcNET" RequestNavigate="Hyperlink_RequestNavigate">MpcNET</Hyperlink><LineBreak/>
※ <Hyperlink NavigateUri="https://github.com/hardcodet/wpf-notifyicon" RequestNavigate="Hyperlink_RequestNavigate">wpf-notifyicon</Hyperlink><LineBreak/> ※ <Hyperlink NavigateUri="https://github.com/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">
<Run Text="{x:Static properties:Resources.Settings_SourceCode1}" /> <Run Text="{x:Static properties:Resources.Settings_SourceCode1}" />
<Hyperlink NavigateUri="https://git.n700.ovh/keb/unison" RequestNavigate="Hyperlink_RequestNavigate"> <Hyperlink NavigateUri="https://github.com/ZetaKebab/unison" RequestNavigate="Hyperlink_RequestNavigate">
<Run Text="{x:Static properties:Resources.Settings_SourceCode2}" /> <Run Text="{x:Static properties:Resources.Settings_SourceCode2}" />
</Hyperlink>. </Hyperlink>.
</TextBlock> </TextBlock>

View File

@ -88,6 +88,18 @@ namespace unison
SnapcastPort.Text = (string)Application.Current.FindResource("snapcastPort"); 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();
}
public void SaveSettings() public void SaveSettings()
{ {
Properties.Settings.Default.mpd_host = MpdHost.Text; Properties.Settings.Default.mpd_host = MpdHost.Text;
@ -101,6 +113,12 @@ namespace unison
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

@ -25,6 +25,11 @@
<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="🔀" />

View File

@ -59,6 +59,18 @@ namespace unison
} }
} }
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

Binary file not shown.

View File

@ -7,7 +7,7 @@
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon> <ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
<Win32Resource></Win32Resource> <Win32Resource></Win32Resource>
<StartupObject>unison.App</StartupObject> <StartupObject>unison.App</StartupObject>
<Version>1.0</Version> <Version>1.1</Version>
<Company /> <Company />
<Authors>Théo Marchal</Authors> <Authors>Théo Marchal</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
@ -49,35 +49,33 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\README.txt"> <Content Include="snapclient_0.25.0-1_win64\README.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\FLAC.dll"> <Content Include="snapclient_0.25.0-1_win64\FLAC.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\ogg.dll"> <Content Include="snapclient_0.25.0-1_win64\ogg.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\opus.dll"> <Content Include="snapclient_0.25.0-1_win64\opus.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\snapclient.exe"> <Content Include="snapclient_0.25.0-1_win64\snapclient.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\soxr.dll"> <Content Include="snapclient_0.25.0-1_win64\soxr.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
<Content Include="snapclient_0.25.0-1_win64\vorbis.dll"> <Content Include="snapclient_0.25.0-1_win64\vorbis.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<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" />
</ItemGroup> <PackageReference Include="RadioBrowser" Version="0.6.1" />
<PackageReference Include="MpcNET" Version="1.3.0" />
<ItemGroup>
<ProjectReference Include="..\MpcNET\MpcNET.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -108,6 +106,27 @@
<Generator>SettingsSingleFileGenerator</Generator> <Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput> <LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None> </None>
<None Update="snapclient_0.26.0-1_win64\FLAC.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="snapclient_0.26.0-1_win64\ogg.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="snapclient_0.26.0-1_win64\opus.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="snapclient_0.26.0-1_win64\README.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="snapclient_0.26.0-1_win64\snapclient.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="snapclient_0.26.0-1_win64\soxr.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="snapclient_0.26.0-1_win64\vorbis.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,11 +4,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
VisualStudioVersion = 16.0.31515.178 VisualStudioVersion = 16.0.31515.178
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unison", "unison.csproj", "{489048C4-3FCA-4573-B34C-943D03F94D04}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unison", "unison.csproj", "{489048C4-3FCA-4573-B34C-943D03F94D04}"
ProjectSection(ProjectDependencies) = postProject
{230556C6-5AC3-4FD8-8947-C9ABF1416D19} = {230556C6-5AC3-4FD8-8947-C9ABF1416D19}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MpcNET", "..\MpcNET\MpcNET.csproj", "{230556C6-5AC3-4FD8-8947-C9ABF1416D19}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -23,12 +18,6 @@ Global
{489048C4-3FCA-4573-B34C-943D03F94D04}.Release|Any CPU.Build.0 = Release|Any CPU {489048C4-3FCA-4573-B34C-943D03F94D04}.Release|Any CPU.Build.0 = Release|Any CPU
{489048C4-3FCA-4573-B34C-943D03F94D04}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU {489048C4-3FCA-4573-B34C-943D03F94D04}.Release-Stable|Any CPU.ActiveCfg = Release|Any CPU
{489048C4-3FCA-4573-B34C-943D03F94D04}.Release-Stable|Any CPU.Build.0 = Release|Any CPU {489048C4-3FCA-4573-B34C-943D03F94D04}.Release-Stable|Any CPU.Build.0 = Release|Any CPU
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release|Any CPU.Build.0 = Release|Any CPU
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU
{230556C6-5AC3-4FD8-8947-C9ABF1416D19}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE