Compare commits
29 Commits
c5e8534af7
...
shuffle
Author | SHA1 | Date | |
---|---|---|---|
f6eb00759a | |||
468ec8be07 | |||
52b0a6fc85 | |||
5a43a1284e | |||
06207d9791 | |||
2f80af70ba | |||
aeb1b1d573 | |||
8451e684db | |||
9c8027428c | |||
8e04161a60 | |||
ccbb4525f3 | |||
529754934d | |||
0471ba88c1 | |||
3b55c79d30 | |||
b33ae3b9be | |||
67667c74da | |||
8033aebb23 | |||
0898d5cf11 | |||
cb741349fa | |||
cabfb165da | |||
5142477b5e | |||
a6b7ad9c1e | |||
b074a4e975 | |||
53d7668fd6 | |||
124b263499 | |||
3f74b1a4e7 | |||
1d3515a39d | |||
792437b839 | |||
5bfa7d3b5b |
16
App.xaml.cs
@ -1,6 +1,6 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using unison.Handlers;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
@ -9,13 +9,15 @@ namespace unison
|
||||
private TaskbarIcon _systray;
|
||||
private HotkeyHandler _hotkeys;
|
||||
private SnapcastHandler _snapcast;
|
||||
private ShuffleHandler _shuffle;
|
||||
private MPDHandler _mpd;
|
||||
private UpdateHandler _updater;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
//debug language
|
||||
//unison.Resources.Resources.Culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||
//unison.Resources.Resources.Culture = CultureInfo.GetCultureInfo("es-ES");
|
||||
//unison.Resources.Resources.Culture = System.Globalization.CultureInfo.GetCultureInfo("fr-FR");
|
||||
//unison.Resources.Resources.Culture = System.Globalization.CultureInfo.GetCultureInfo("es-ES");
|
||||
|
||||
|
||||
base.OnStartup(e);
|
||||
@ -29,6 +31,12 @@ namespace unison
|
||||
_snapcast = new SnapcastHandler();
|
||||
Current.Properties["snapcast"] = _snapcast;
|
||||
|
||||
_shuffle = new ShuffleHandler();
|
||||
Current.Properties["shuffle"] = _shuffle;
|
||||
|
||||
_updater = new UpdateHandler();
|
||||
Current.Properties["updater"] = _updater;
|
||||
|
||||
Current.MainWindow = new MainWindow();
|
||||
|
||||
_systray = (TaskbarIcon)FindResource("SystrayTaskbar");
|
||||
|
22
CHANGELOG.md
@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## v1.3.1
|
||||
|
||||
*Released: 03/11/2022*
|
||||
|
||||
* Update .NET version from 5.0 to 6.0
|
||||
* Fix: simple patch to avoid a crash concerning GetAlbumCover
|
||||
* Fix: connection change now working
|
||||
|
||||
## v1.3
|
||||
|
||||
*Released: 18/04/2022*
|
||||
|
||||
* New feature: add support for readpicture, aka embedded cover art
|
||||
* New feature: add support for MPD password
|
||||
* Spanish translation
|
||||
* Trim album release date
|
||||
* Cover icon when a radio is playing
|
||||
* Update MpcNET package from 1.3 to 1.4
|
||||
* Fix: Snapcast not working with hostname
|
||||
* Fix: some radios crash
|
||||
* Fix: disable/enable radios and Snapcast with connection
|
||||
|
||||
## v1.2
|
||||
|
||||
*Released: 07/04/2022*
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@ -34,7 +35,7 @@ namespace unison
|
||||
public class MPDHandler
|
||||
{
|
||||
private bool _connected;
|
||||
public string _version;
|
||||
private string _version;
|
||||
private int _currentVolume;
|
||||
private int _previousVolume;
|
||||
private bool _currentRandom;
|
||||
@ -43,10 +44,11 @@ namespace unison
|
||||
private bool _currentConsume;
|
||||
private double _currentTime;
|
||||
private double _totalTime;
|
||||
private IEnumerable<IMpdFile> _Playlist;
|
||||
|
||||
private MpdStatus _currentStatus;
|
||||
private IMpdFile _currentSong;
|
||||
private BitmapFrame _cover;
|
||||
private BitmapImage _cover;
|
||||
public Statistics _stats;
|
||||
private readonly System.Timers.Timer _elapsedTimer;
|
||||
private DispatcherTimer _retryTimer;
|
||||
@ -64,17 +66,19 @@ namespace unison
|
||||
private MpcConnection _connection;
|
||||
private MpcConnection _commandConnection;
|
||||
private IPEndPoint _mpdEndpoint;
|
||||
private CancellationTokenSource _cancelToken;
|
||||
|
||||
public CancellationTokenSource _cancelCommand;
|
||||
private CancellationTokenSource _cancelConnect;
|
||||
|
||||
public MPDHandler()
|
||||
{
|
||||
Initialize(null, null);
|
||||
Startup(null, null);
|
||||
|
||||
_stats = new Statistics();
|
||||
|
||||
_retryTimer = new DispatcherTimer();
|
||||
_retryTimer.Interval = TimeSpan.FromSeconds(5);
|
||||
_retryTimer.Tick += Initialize;
|
||||
_retryTimer.Tick += Startup;
|
||||
|
||||
_elapsedTimer = new System.Timers.Timer(500);
|
||||
_elapsedTimer.Elapsed += new System.Timers.ElapsedEventHandler(ElapsedTimer);
|
||||
@ -147,7 +151,7 @@ namespace unison
|
||||
|
||||
public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command)
|
||||
{
|
||||
if (_commandConnection == null)
|
||||
if (_commandConnection == null || !IsConnected())
|
||||
{
|
||||
Trace.WriteLine("[SafelySendCommandAsync] no command connection");
|
||||
return default(T);
|
||||
@ -175,21 +179,52 @@ namespace unison
|
||||
return default(T);
|
||||
}
|
||||
|
||||
private void Initialize(object sender, EventArgs e)
|
||||
public async void Startup(object sender, EventArgs e)
|
||||
{
|
||||
if (!_connected)
|
||||
Connect();
|
||||
await Initialize();
|
||||
}
|
||||
|
||||
public async void Connect()
|
||||
public async Task Initialize()
|
||||
{
|
||||
_cancelToken = new CancellationTokenSource();
|
||||
CancellationToken token = _cancelToken.Token;
|
||||
Trace.WriteLine("Initializing");
|
||||
|
||||
Disconnected();
|
||||
|
||||
if (!_connected)
|
||||
await Connect();
|
||||
}
|
||||
|
||||
public void Disconnected()
|
||||
{
|
||||
_connected = false;
|
||||
ConnectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
_commandConnection?.DisconnectAsync();
|
||||
_connection?.DisconnectAsync();
|
||||
|
||||
_cancelConnect?.Cancel();
|
||||
_cancelConnect = new CancellationTokenSource();
|
||||
|
||||
_cancelCommand?.Cancel();
|
||||
_cancelCommand = new CancellationTokenSource();
|
||||
|
||||
_connection = null;
|
||||
_commandConnection = null;
|
||||
|
||||
Trace.WriteLine("Disconnected");
|
||||
}
|
||||
|
||||
public async Task Connect()
|
||||
{
|
||||
Trace.WriteLine("Connecting");
|
||||
|
||||
if (_cancelCommand.IsCancellationRequested || _cancelConnect.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_connection = await ConnectInternal(token);
|
||||
_commandConnection = await ConnectInternal(token);
|
||||
_connection = await ConnectInternal(_cancelConnect.Token);
|
||||
_commandConnection = await ConnectInternal(_cancelCommand.Token);
|
||||
}
|
||||
catch (MpcNET.Exceptions.MpcConnectException e)
|
||||
{
|
||||
@ -217,11 +252,14 @@ namespace unison
|
||||
await UpdateStatusAsync();
|
||||
await UpdateSongAsync();
|
||||
|
||||
Loop(token);
|
||||
Loop(_cancelCommand.Token);
|
||||
}
|
||||
|
||||
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return null;
|
||||
|
||||
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out _ipAddress);
|
||||
|
||||
if (_ipAddress == null)
|
||||
@ -261,18 +299,6 @@ namespace unison
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void Disconnected()
|
||||
{
|
||||
_connected = false;
|
||||
ConnectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (_connection != null)
|
||||
_connection = null;
|
||||
|
||||
if (_commandConnection != null)
|
||||
_commandConnection = null;
|
||||
}
|
||||
|
||||
private void Loop(CancellationToken token)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
@ -281,8 +307,7 @@ namespace unison
|
||||
{
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (token.IsCancellationRequested || _connection == null)
|
||||
if (token.IsCancellationRequested || _connection == null || !IsConnected())
|
||||
break;
|
||||
|
||||
IMpdMessage<string> idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update"));
|
||||
@ -291,14 +316,17 @@ namespace unison
|
||||
await HandleIdleResponseAsync(idleChanges.Response.Content);
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {idleChanges.Response?.Content}");
|
||||
Trace.WriteLine($"Error in Idle connection thread (1): {idleChanges.Response?.Content}");
|
||||
throw new Exception(idleChanges.Response?.Content);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
Disconnected();
|
||||
if (token.IsCancellationRequested)
|
||||
Trace.WriteLine($"Idle connection cancelled.");
|
||||
else
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
await Initialize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -321,6 +349,7 @@ namespace unison
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
await Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,6 +374,7 @@ namespace unison
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
await Initialize();
|
||||
}
|
||||
|
||||
_isUpdatingStatus = false;
|
||||
@ -352,6 +382,8 @@ namespace unison
|
||||
|
||||
private async Task UpdateSongAsync()
|
||||
{
|
||||
Trace.WriteLine("Updating song");
|
||||
|
||||
if (_connection == null || _isUpdatingSong)
|
||||
return;
|
||||
|
||||
@ -371,13 +403,17 @@ namespace unison
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
|
||||
await Initialize();
|
||||
}
|
||||
|
||||
_isUpdatingSong = false;
|
||||
}
|
||||
|
||||
private async void GetAlbumCover(string path, CancellationToken token = default)
|
||||
private async void GetAlbumCover(string path, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
List<byte> data = new List<byte>();
|
||||
try
|
||||
{
|
||||
@ -390,7 +426,7 @@ namespace unison
|
||||
if (_connection == null)
|
||||
return;
|
||||
|
||||
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new ReadPictureCommand(path, currentSize));
|
||||
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
|
||||
if (!albumReq.IsResponseValid)
|
||||
break;
|
||||
|
||||
@ -412,7 +448,7 @@ namespace unison
|
||||
if (_connection == null)
|
||||
return;
|
||||
|
||||
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
|
||||
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new ReadPictureCommand(path, currentSize));
|
||||
if (!albumReq.IsResponseValid)
|
||||
break;
|
||||
|
||||
@ -428,6 +464,9 @@ namespace unison
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
Trace.WriteLine("Exception caught while getting albumart: " + e);
|
||||
return;
|
||||
}
|
||||
@ -437,14 +476,12 @@ namespace unison
|
||||
else
|
||||
{
|
||||
using MemoryStream stream = new MemoryStream(data.ToArray());
|
||||
try
|
||||
{
|
||||
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
||||
}
|
||||
catch (System.NotSupportedException)
|
||||
{
|
||||
_cover = null;
|
||||
}
|
||||
_cover = new BitmapImage();
|
||||
_cover.BeginInit();
|
||||
_cover.CacheOption = BitmapCacheOption.OnLoad;
|
||||
_cover.StreamSource = stream;
|
||||
_cover.EndInit();
|
||||
_cover.Freeze();
|
||||
}
|
||||
UpdateCover();
|
||||
}
|
||||
@ -476,7 +513,7 @@ namespace unison
|
||||
SongChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
string uri = Regex.Escape(_currentSong.Path);
|
||||
GetAlbumCover(uri);
|
||||
GetAlbumCover(uri, _cancelCommand.Token);
|
||||
}
|
||||
|
||||
public void UpdateCover()
|
||||
@ -486,17 +523,31 @@ namespace unison
|
||||
|
||||
public IMpdFile GetCurrentSong() => _currentSong;
|
||||
public MpdStatus GetStatus() => _currentStatus;
|
||||
public BitmapFrame GetCover() => _cover;
|
||||
public BitmapImage GetCover() => _cover;
|
||||
public string GetVersion() => _version;
|
||||
public Statistics GetStats() => _stats;
|
||||
public double GetCurrentTime() => _currentTime;
|
||||
public IEnumerable<IMpdFile> GetPlaylist() => _Playlist;
|
||||
|
||||
public bool IsConnected() => _connected;
|
||||
public bool IsPlaying() => _currentStatus?.State == MpdState.Play;
|
||||
|
||||
public void Prev() => SendCommand(new PreviousCommand());
|
||||
public void Next() => SendCommand(new NextCommand());
|
||||
public bool CanPrevNext = true;
|
||||
|
||||
public void Prev()
|
||||
{
|
||||
if (CanPrevNext)
|
||||
SendCommand(new PreviousCommand());
|
||||
}
|
||||
|
||||
public void Next()
|
||||
{
|
||||
if (CanPrevNext)
|
||||
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));
|
||||
@ -551,6 +602,15 @@ namespace unison
|
||||
SendCommand(commandList);
|
||||
}
|
||||
|
||||
public async Task QueryPlaylist() => _Playlist = await SafelySendCommandAsync(new PlaylistCommand());
|
||||
|
||||
public int GetPlaylistCount()
|
||||
{
|
||||
if (_Playlist == null)
|
||||
return 0;
|
||||
return _Playlist.ToArray().Count();
|
||||
}
|
||||
|
||||
public async void QueryStats()
|
||||
{
|
||||
Dictionary<string, string> Response = await SafelySendCommandAsync(new StatsCommand());
|
||||
|
81
Handlers/RadioHandler.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using RadioBrowser;
|
||||
using RadioBrowser.Models;
|
||||
|
||||
namespace unison.Handlers
|
||||
{
|
||||
public class CountryListItem
|
||||
{
|
||||
public uint Count { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Name == "")
|
||||
return "None";
|
||||
return $"{Name} ({Count})";
|
||||
}
|
||||
}
|
||||
|
||||
public class StationListItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Codec { get; set; }
|
||||
public string Tags { get; set; }
|
||||
public int Bitrate { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
|
||||
private string _country;
|
||||
public string Country
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_country.Length == 0)
|
||||
return "🏴☠️";
|
||||
return string.Concat(_country.ToUpper().Select(x => char.ConvertFromUtf32(x + 0x1F1A5))); // return emoji
|
||||
}
|
||||
set
|
||||
{
|
||||
_country = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class RadioHandler
|
||||
{
|
||||
private readonly RadioBrowserClient _radioBrowser;
|
||||
private readonly bool _connected = true;
|
||||
|
||||
public bool IsConnected() => _connected;
|
||||
|
||||
|
||||
public RadioHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
_radioBrowser = new RadioBrowserClient();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Trace.WriteLine("Exception while connecting to RadioBrowser: " + e.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
public async Task<List<NameAndCount>> GetCountries()
|
||||
{
|
||||
return await _radioBrowser.Lists.GetCountriesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<StationInfo>> AdvancedSearch(AdvancedSearchOptions options)
|
||||
{
|
||||
return await _radioBrowser.Search.AdvancedAsync(options);
|
||||
}
|
||||
}
|
||||
}
|
108
Handlers/ShuffleHandler.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using MpcNET;
|
||||
using MpcNET.Commands.Database;
|
||||
using MpcNET.Commands.Playback;
|
||||
using MpcNET.Commands.Queue;
|
||||
using MpcNET.Commands.Reflection;
|
||||
using MpcNET.Tags;
|
||||
using MpcNET.Types;
|
||||
using MpcNET.Types.Filters;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
class ShuffleHandler
|
||||
{
|
||||
private readonly MPDHandler _mpd;
|
||||
public int AddedSongs = 0;
|
||||
|
||||
public List<string> SongList { get; }
|
||||
|
||||
public ShuffleHandler()
|
||||
{
|
||||
SongList = new();
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
}
|
||||
|
||||
public async Task GetSongsFromFilter(List<IFilter> filter, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
SongList.Clear();
|
||||
|
||||
int song = _mpd.GetStats().Songs;
|
||||
IEnumerable<IMpdFile> response = await _mpd.SafelySendCommandAsync(new SearchCommand(filter, 0, song + 1));
|
||||
foreach (IMpdFile file in response)
|
||||
SongList.Add(file.Path);
|
||||
}
|
||||
|
||||
public async Task AddToQueueRandom(int SongNumber, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
int AddedSongs = 0;
|
||||
|
||||
var commandList = new CommandList();
|
||||
int songTotal = _mpd.GetStats().Songs;
|
||||
|
||||
for (int i = 0; i < SongNumber; i++)
|
||||
{
|
||||
int song = new Random().Next(0, songTotal - 1);
|
||||
commandList.Add(new SearchAddCommand(new FilterTag(MpdTags.Title, "", FilterOperator.Contains), song, song + 1));
|
||||
AddedSongs++;
|
||||
|
||||
// play if stopped or unknown state (no queue managing at the moment, so mandatory)
|
||||
if (i == 0 && (_mpd.GetStatus().State != MpdState.Play && _mpd.GetStatus().State != MpdState.Pause))
|
||||
commandList.Add(new PlayCommand(0));
|
||||
}
|
||||
|
||||
await _mpd.SafelySendCommandAsync(commandList);
|
||||
}
|
||||
|
||||
public async Task AddToQueueFilter(int SongNumber, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
int AddedSongs = 0;
|
||||
|
||||
// more (or equal) requested songs than available => add everything
|
||||
if (SongNumber >= SongList.Count)
|
||||
{
|
||||
var commandList = new CommandList();
|
||||
foreach (string path in SongList)
|
||||
{
|
||||
commandList.Add(new AddCommand(path));
|
||||
AddedSongs++;
|
||||
}
|
||||
|
||||
await _mpd.SafelySendCommandAsync(commandList);
|
||||
}
|
||||
// more available songs than requested =>
|
||||
// we add unique indexes until we reach the requested amount
|
||||
else
|
||||
{
|
||||
HashSet<int> SongIndex = new();
|
||||
while (SongIndex.Count < SongNumber)
|
||||
{
|
||||
int MaxIndex = new Random().Next(0, SongList.Count - 1);
|
||||
SongIndex.Add(MaxIndex);
|
||||
}
|
||||
|
||||
var commandList = new CommandList();
|
||||
foreach (int index in SongIndex)
|
||||
{
|
||||
commandList.Add(new AddCommand(SongList[index]));
|
||||
AddedSongs++;
|
||||
}
|
||||
|
||||
await _mpd.SafelySendCommandAsync(commandList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,16 @@ namespace unison
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleExit(object sender, EventArgs e)
|
||||
{
|
||||
_snapcast.Kill();
|
||||
HasStarted = false;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateInterface();
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateInterface()
|
||||
{
|
||||
TaskbarIcon Systray = (TaskbarIcon)Application.Current.Properties["systray"];
|
||||
@ -42,16 +52,19 @@ namespace unison
|
||||
_snapcast.StartInfo.FileName = Properties.Settings.Default.snapcast_path + @"\snapclient.exe";
|
||||
_snapcast.StartInfo.Arguments = $"--host {mpd._ipAddress}";
|
||||
_snapcast.StartInfo.CreateNoWindow = !Properties.Settings.Default.snapcast_window;
|
||||
_snapcast.EnableRaisingEvents = true;
|
||||
_snapcast.Exited += new EventHandler(HandleExit);
|
||||
|
||||
try
|
||||
{
|
||||
_snapcast.Start();
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
MessageBox.Show($"[{unison.Resources.Resources.Snapcast_Popup1}]\n" +
|
||||
$"{unison.Resources.Resources.Snapcast_Popup2} {err.Message}\n\n" +
|
||||
$"{unison.Resources.Resources.Snapcast_Popup3} {Properties.Settings.Default.snapcast_path}\n" +
|
||||
$"{unison.Resources.Resources.Snapcast_Popup4}",
|
||||
MessageBox.Show($"[{Resources.Resources.Snapcast_Popup1}]\n" +
|
||||
$"{Resources.Resources.Snapcast_Popup2} {err.Message}\n\n" +
|
||||
$"{Resources.Resources.Snapcast_Popup3} {Properties.Settings.Default.snapcast_path}\n" +
|
||||
$"{Resources.Resources.Snapcast_Popup4}",
|
||||
"unison", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
58
Handlers/UpdateHandler.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using AutoUpdaterDotNET;
|
||||
|
||||
namespace unison.Handlers
|
||||
{
|
||||
internal class UpdateHandler
|
||||
{
|
||||
readonly string xmlFile = "https://raw.githubusercontent.com/ZetaKebab/unison/main/Installer/unison.xml";
|
||||
|
||||
private bool _UpdateAvailable = false;
|
||||
public bool UpdateAvailable() => _UpdateAvailable;
|
||||
|
||||
private bool _RequestedCheck = false;
|
||||
|
||||
public UpdateHandler()
|
||||
{
|
||||
AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
|
||||
Start();
|
||||
}
|
||||
|
||||
public void Start(bool RequestCheck = false)
|
||||
{
|
||||
_RequestedCheck = RequestCheck;
|
||||
AutoUpdater.Start(xmlFile);
|
||||
}
|
||||
|
||||
private string CutVersionNumber(string number)
|
||||
{
|
||||
return number.Substring(0, number.LastIndexOf("."));
|
||||
}
|
||||
|
||||
private void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
|
||||
{
|
||||
if (args.Error == null)
|
||||
{
|
||||
if (args.IsUpdateAvailable)
|
||||
{
|
||||
_UpdateAvailable = true;
|
||||
string number = CutVersionNumber(args.CurrentVersion);
|
||||
|
||||
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
|
||||
MainWin.UpdateUpdateStatus(number);
|
||||
|
||||
MessageBoxResult Result = MessageBox.Show($"{Resources.Resources.Update_Message1} {number}.\n{Resources.Resources.Update_Message2}",
|
||||
"unison", MessageBoxButton.YesNo, MessageBoxImage.Information);
|
||||
if (Result == MessageBoxResult.Yes)
|
||||
AutoUpdater.DownloadUpdate(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_RequestedCheck)
|
||||
MessageBox.Show($"{Resources.Resources.Update_NoUpdate}", "unison", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
Installer/unison.iss
Normal file
@ -0,0 +1,43 @@
|
||||
#define Name "unison"
|
||||
#define Version "1.3.1"
|
||||
#define Snapcast "snapclient_0.26.0-1_win64"
|
||||
#define Publisher "Th<54>o Marchal"
|
||||
#define URL "https://github.com/ZetaKebab/unison"
|
||||
#define ExeName "unison.exe"
|
||||
|
||||
[Setup]
|
||||
AppName={#Name}
|
||||
AppVersion={#Version}
|
||||
AppVerName={#Name} v{#Version}
|
||||
AppPublisher={#Publisher}
|
||||
AppPublisherURL={#URL}
|
||||
AppSupportURL={#URL}
|
||||
AppUpdatesURL={#URL}
|
||||
DefaultDirName={autopf}\{#Name}
|
||||
DisableProgramGroupPage=yes
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
OutputBaseFilename="{#Name}-v{#Version}-setup"
|
||||
OutputDir=..\publish\installer
|
||||
SetupIconFile=..\Resources\icon-full.ico
|
||||
UninstallDisplayIcon = "{app}\{#Name}.exe"
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
|
||||
Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
|
||||
|
||||
[Files]
|
||||
Source: "..\publish\{#ExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\publish\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\publish\unison.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\publish\{#Snapcast}\*"; DestDir: "{app}\{#Snapcast}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#Name}"; Filename: "{app}\{#ExeName}"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#Name}.exe"; Parameters: "-frominstaller"; Flags: nowait postinstall skipifsilent
|
||||
|
7
Installer/unison.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.3.1.0</version>
|
||||
<url>https://github.com/ZetaKebab/unison/releases/download/v1.3.1/unison-v1.3.1.zip</url>
|
||||
<changelog>https://raw.githubusercontent.com/ZetaKebab/unison/main/CHANGELOG.md</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
</item>
|
@ -4,14 +4,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Configuration>Release-Stable</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<SelfContained>false</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishSingleFile>True</PublishSingleFile>
|
||||
<PublishReadyToRun>False</PublishReadyToRun>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
</Project>
|
27
README.md
@ -1,12 +1,12 @@
|
||||
# unison
|
||||
|
||||

|
||||

|
||||
|
||||
**unison** is a very simple [Music Player Daemon (MPD)](https://www.musicpd.org/) daemon client with the following goals:
|
||||
|
||||
* lightweight window that can be toggled with shortcuts
|
||||
* music control through rebindable shortcuts
|
||||
* [Snapcast](https://mjaggard.github.io/snapcast/) integration
|
||||
* [Snapcast](https://github.com/badaix/snapcast) integration
|
||||
* Radio stations
|
||||
|
||||
## Features
|
||||
@ -15,13 +15,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.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### Snapcast
|
||||
|
||||
@ -31,12 +43,13 @@ 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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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
|
||||
|
||||
|
67
Resources/Resources.Designer.cs
generated
@ -19,7 +19,7 @@ namespace unison.Resources {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@ -196,7 +196,7 @@ namespace unison.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// 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), they are saved as is in the setting file..
|
||||
/// </summary>
|
||||
public static string Settings_ConnectionPasswordInfo {
|
||||
get {
|
||||
@ -617,5 +617,68 @@ namespace unison.Resources {
|
||||
return ResourceManager.GetString("StopSnapcast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Check for updates.
|
||||
/// </summary>
|
||||
public static string Update_ButtonCheck {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_ButtonCheck", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Start update.
|
||||
/// </summary>
|
||||
public static string Update_ButtonStart {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_ButtonStart", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Update available! New version is.
|
||||
/// </summary>
|
||||
public static string Update_Message1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_Message1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Install now?.
|
||||
/// </summary>
|
||||
public static string Update_Message2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_Message2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No update available..
|
||||
/// </summary>
|
||||
public static string Update_NoUpdate {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_NoUpdate", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to New version.
|
||||
/// </summary>
|
||||
public static string Update_String1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_String1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to available!.
|
||||
/// </summary>
|
||||
public static string Update_String2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Update_String2", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,10 +112,10 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Salir</value>
|
||||
@ -303,4 +303,25 @@
|
||||
<data name="StopSnapcast" xml:space="preserve">
|
||||
<value>Parar Snapcast</value>
|
||||
</data>
|
||||
<data name="Update_ButtonCheck" xml:space="preserve">
|
||||
<value>Buscar actualización</value>
|
||||
</data>
|
||||
<data name="Update_ButtonStart" xml:space="preserve">
|
||||
<value>Actualizar</value>
|
||||
</data>
|
||||
<data name="Update_Message1" xml:space="preserve">
|
||||
<value>¡Actualización disponible! La nueva versión es la</value>
|
||||
</data>
|
||||
<data name="Update_Message2" xml:space="preserve">
|
||||
<value>¿Instalar ahora?</value>
|
||||
</data>
|
||||
<data name="Update_NoUpdate" xml:space="preserve">
|
||||
<value>No actualización disponible.</value>
|
||||
</data>
|
||||
<data name="Update_String1" xml:space="preserve">
|
||||
<value>¡Nueva versión</value>
|
||||
</data>
|
||||
<data name="Update_String2" xml:space="preserve">
|
||||
<value>disponible!</value>
|
||||
</data>
|
||||
</root>
|
@ -112,10 +112,10 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Quitter</value>
|
||||
@ -298,9 +298,30 @@
|
||||
<value>Temps d'écoute écoulé :</value>
|
||||
</data>
|
||||
<data name="Stats_Uptime" xml:space="preserve">
|
||||
<value>MPD lancé depuis : </value>
|
||||
<value>MPD lancé depuis :</value>
|
||||
</data>
|
||||
<data name="StopSnapcast" xml:space="preserve">
|
||||
<value>Stopper Snapcast</value>
|
||||
</data>
|
||||
<data name="Update_ButtonCheck" xml:space="preserve">
|
||||
<value>Vérifier les mises à jour</value>
|
||||
</data>
|
||||
<data name="Update_ButtonStart" xml:space="preserve">
|
||||
<value>Mettre à jour</value>
|
||||
</data>
|
||||
<data name="Update_Message1" xml:space="preserve">
|
||||
<value>Mise à jour disponible ! La nouvelle version est la</value>
|
||||
</data>
|
||||
<data name="Update_Message2" xml:space="preserve">
|
||||
<value>Installer maintenant ?</value>
|
||||
</data>
|
||||
<data name="Update_NoUpdate" xml:space="preserve">
|
||||
<value>Pas de mise à jour disponible.</value>
|
||||
</data>
|
||||
<data name="Update_String1" xml:space="preserve">
|
||||
<value>Nouvelle version</value>
|
||||
</data>
|
||||
<data name="Update_String2" xml:space="preserve">
|
||||
<value>disponible !</value>
|
||||
</data>
|
||||
</root>
|
@ -112,10 +112,10 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
@ -163,7 +163,7 @@
|
||||
<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>
|
||||
<value>Please note that since MPD passwords are not secure (they are sent in plain text to the server), they are saved as is in the setting file.</value>
|
||||
</data>
|
||||
<data name="Settings_ConnectionStatus" xml:space="preserve">
|
||||
<value>Status:</value>
|
||||
@ -303,4 +303,25 @@
|
||||
<data name="StopSnapcast" xml:space="preserve">
|
||||
<value>Stop Snapcast</value>
|
||||
</data>
|
||||
<data name="Update_ButtonCheck" xml:space="preserve">
|
||||
<value>Check for updates</value>
|
||||
</data>
|
||||
<data name="Update_ButtonStart" xml:space="preserve">
|
||||
<value>Start update</value>
|
||||
</data>
|
||||
<data name="Update_Message1" xml:space="preserve">
|
||||
<value>Update available! New version is</value>
|
||||
</data>
|
||||
<data name="Update_Message2" xml:space="preserve">
|
||||
<value>Install now?</value>
|
||||
</data>
|
||||
<data name="Update_NoUpdate" xml:space="preserve">
|
||||
<value>No update available.</value>
|
||||
</data>
|
||||
<data name="Update_String1" xml:space="preserve">
|
||||
<value>New version</value>
|
||||
</data>
|
||||
<data name="Update_String2" xml:space="preserve">
|
||||
<value>available!</value>
|
||||
</data>
|
||||
</root>
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
BIN
Resources/nothing.png
Normal file
After Width: | Height: | Size: 369 B |
BIN
Resources/radio.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Screenshots/screen-mainwindow.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
Screenshots/screen-shortcuts.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Screenshots/screen-shuffle.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
Screenshots/screen-systray.png
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -104,7 +104,8 @@
|
||||
<VisualBrush Visual="{Binding ElementName=mask}"/>
|
||||
</StackPanel.OpacityMask>
|
||||
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
|
||||
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" />
|
||||
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
|
||||
<Image x:Name="RadioCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/radio.png" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
@ -6,7 +6,6 @@ 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
|
||||
@ -50,12 +49,17 @@ namespace unison
|
||||
{
|
||||
if (_mpd.IsConnected())
|
||||
{
|
||||
Snapcast.IsEnabled = true;
|
||||
_mpd.QueryStats();
|
||||
_settingsWin.UpdateStats();
|
||||
|
||||
ConnectionOkIcon.Visibility = Visibility.Visible;
|
||||
ConnectionFailIcon.Visibility = Visibility.Collapsed;
|
||||
|
||||
_shuffleWin.ListGenre();
|
||||
_shuffleWin.ListFolder();
|
||||
Snapcast.IsEnabled = true;
|
||||
if (_radiosWin.IsConnected())
|
||||
Radio.IsEnabled = true;
|
||||
|
||||
_shuffleWin.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -63,6 +67,9 @@ namespace unison
|
||||
DefaultState(true);
|
||||
ConnectionOkIcon.Visibility = Visibility.Collapsed;
|
||||
ConnectionFailIcon.Visibility = Visibility.Visible;
|
||||
|
||||
Snapcast.IsEnabled = false;
|
||||
Radio.IsEnabled = false;
|
||||
}
|
||||
_settingsWin.UpdateConnectionStatus();
|
||||
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
|
||||
@ -115,30 +122,13 @@ namespace unison
|
||||
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
|
||||
}
|
||||
|
||||
Debug.WriteLine("Song changed called!");
|
||||
Trace.WriteLine("Song changed called!");
|
||||
|
||||
// handle continuous shuffle
|
||||
if (_shuffleWin.GetContinuous())
|
||||
{
|
||||
System.Collections.Generic.IEnumerable<MpcNET.Types.IMpdFile> a = await _mpd.SafelySendCommandAsync(new PlaylistCommand());
|
||||
int queueSize = 0;
|
||||
foreach (var i in a)
|
||||
{
|
||||
Debug.WriteLine(i.Path);
|
||||
queueSize++;
|
||||
}
|
||||
Debug.WriteLine("queue size is: " + queueSize);
|
||||
|
||||
if (queueSize < 5)
|
||||
{
|
||||
_shuffleWin.AddContinuousSongs();
|
||||
}
|
||||
|
||||
// query queue
|
||||
// if (queue.SongRemaining < 5)
|
||||
//{
|
||||
// // query shuffle songs
|
||||
//}
|
||||
_mpd.CanPrevNext = false;
|
||||
await _shuffleWin.HandleContinuous();
|
||||
_mpd.CanPrevNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,9 +158,6 @@ namespace unison
|
||||
DefaultState();
|
||||
}
|
||||
}
|
||||
|
||||
_mpd.QueryStats();
|
||||
_settingsWin.UpdateStats();
|
||||
}
|
||||
|
||||
private void DefaultState(bool LostConnection = false)
|
||||
@ -186,8 +173,9 @@ namespace unison
|
||||
PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
|
||||
TimeSlider.Value = 50;
|
||||
TimeSlider.IsEnabled = false;
|
||||
NoCover.Visibility = Visibility.Visible;
|
||||
NoCover.Visibility = Visibility.Collapsed;
|
||||
Cover.Visibility = Visibility.Collapsed;
|
||||
RadioCover.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (LostConnection)
|
||||
{
|
||||
@ -199,16 +187,18 @@ namespace unison
|
||||
|
||||
public void OnCoverChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (_mpd.GetCover() == null)
|
||||
{
|
||||
NoCover.Visibility = Visibility.Collapsed;
|
||||
Cover.Visibility = Visibility.Collapsed;
|
||||
RadioCover.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (_mpd.GetCurrentSong().Time == -1)
|
||||
RadioCover.Visibility = Visibility.Visible;
|
||||
else if (_mpd.GetCover() == null)
|
||||
NoCover.Visibility = Visibility.Visible;
|
||||
Cover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else if (Cover.Source != _mpd.GetCover())
|
||||
{
|
||||
Cover.Source = _mpd.GetCover();
|
||||
Cover.Visibility = Visibility.Visible;
|
||||
NoCover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,11 +211,6 @@ namespace unison
|
||||
SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
|
||||
}
|
||||
|
||||
public void OnRadioBrowserConnected()
|
||||
{
|
||||
Radio.IsEnabled = true;
|
||||
}
|
||||
|
||||
public void UpdateButton(ref Border border, bool b)
|
||||
{
|
||||
border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
|
||||
@ -245,7 +230,6 @@ 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 Random_Clicked(object sender, RoutedEventArgs e) => _mpd.Random();
|
||||
public void Repeat_Clicked(object sender, RoutedEventArgs e) => _mpd.Repeat();
|
||||
public void Single_Clicked(object sender, RoutedEventArgs e) => _mpd.Single();
|
||||
@ -309,6 +293,11 @@ namespace unison
|
||||
slider.ToolTip = (int)slider.Value;
|
||||
}
|
||||
|
||||
public void UpdateUpdateStatus(string version)
|
||||
{
|
||||
_settingsWin.UpdateUpdateStatus(version);
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
|
@ -37,7 +37,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Button Content="{x:Static properties:Resources.Radio_Search}" Click="Search_Clicked" Padding="5, 2"/>
|
||||
<Button x:Name="SearchButton" Content="{x:Static properties:Resources.Radio_Search}" Click="Search_Clicked" Padding="5, 2"/>
|
||||
<Button Content="{x:Static properties:Resources.Radio_Reset}" Click="Reset_Clicked" Margin="10,0,0,0" Padding="5, 2"/>
|
||||
<TextBlock x:Name="SearchStatus" Margin="15,1,0,0" FontStyle="Italic" />
|
||||
</StackPanel>
|
||||
|
@ -1,88 +1,42 @@
|
||||
using RadioBrowser;
|
||||
using RadioBrowser.Models;
|
||||
using System;
|
||||
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;
|
||||
using unison.Handlers;
|
||||
using RadioBrowser.Models;
|
||||
|
||||
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;
|
||||
RadioHandler _radio;
|
||||
|
||||
public bool IsConnected() => _radio.IsConnected();
|
||||
|
||||
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();
|
||||
});
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public async void Initialize()
|
||||
{
|
||||
_radio = new RadioHandler();
|
||||
|
||||
if (!_radio.IsConnected())
|
||||
SearchButton.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
List<NameAndCount> Countries = await _radioBrowser.Lists.GetCountriesAsync();
|
||||
CountryList.Items.Add(new CountryListItem { Name = "", Count = 0 });
|
||||
|
||||
List<NameAndCount> Countries = await _radio.GetCountries();
|
||||
foreach (NameAndCount Country in Countries)
|
||||
{
|
||||
CountryList.Items.Add(new CountryListItem
|
||||
@ -94,27 +48,49 @@ namespace unison
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine("Exception while getting countries in RadioBrowser: " + e.Message);
|
||||
Trace.WriteLine("Exception while getting countries in RadioBrowser: " + e.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private string CleanString(string str)
|
||||
private static string CleanString(string str)
|
||||
{
|
||||
return str.Replace("\r\n", "").Replace("\n", "").Replace("\r", "");
|
||||
}
|
||||
|
||||
private void SearchHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
Search_Clicked(null, null);
|
||||
}
|
||||
|
||||
private async void Search_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
CountryListItem a = (CountryListItem)CountryList.SelectedItem;
|
||||
await SearchAdvanced(NameSearch.Text, a?.Name, TagSearch.Text);
|
||||
}
|
||||
catch (Exception except)
|
||||
{
|
||||
Trace.WriteLine("Error on RadioBrowser search: " + except.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SearchAdvanced(string name, string country, string tags)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchStatus.Text = unison.Resources.Resources.Radio_Loading;
|
||||
|
||||
List<StationInfo> advancedSearch = await _radioBrowser.Search.AdvancedAsync(new AdvancedSearchOptions
|
||||
List<StationInfo> advancedSearch = await Task.Run(async () =>
|
||||
{
|
||||
Name = name,
|
||||
Country = country,
|
||||
TagList = tags
|
||||
return await _radio.AdvancedSearch(new AdvancedSearchOptions
|
||||
{
|
||||
Name = name,
|
||||
Country = country,
|
||||
TagList = tags
|
||||
});
|
||||
});
|
||||
|
||||
RadioListGrid.Items.Clear();
|
||||
@ -140,7 +116,7 @@ namespace unison
|
||||
}
|
||||
catch (Exception except)
|
||||
{
|
||||
Debug.WriteLine("Error on RadioBrowser search advanced: " + except.Message);
|
||||
Trace.WriteLine("Error on RadioBrowser search advanced: " + except.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,13 +136,13 @@ namespace unison
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
Debug.WriteLine("Error: Invalid index.");
|
||||
Trace.WriteLine("Error: Invalid index.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (station.Url == null)
|
||||
{
|
||||
Debug.WriteLine("Error: Invalid station.");
|
||||
Trace.WriteLine("Error: Invalid station.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,19 +150,6 @@ namespace unison
|
||||
_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 = "";
|
||||
@ -194,12 +157,6 @@ namespace unison
|
||||
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;
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
<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"/>
|
||||
<PasswordBox x:Name="MpdPassword" KeyDown="ConnectHandler" 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>
|
||||
|
||||
@ -52,7 +52,6 @@
|
||||
<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>
|
||||
@ -103,7 +102,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -111,7 +110,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -119,7 +118,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -127,7 +126,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -135,7 +134,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -143,7 +142,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -151,7 +150,7 @@
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD1" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<ComboBox ItemsSource="{StaticResource ShortcutItems}" Margin="0,0,5,0" MinWidth="70" SelectionChanged="MOD_SelectionChanged" Tag="MOD2" FontWeight="Light" SelectedIndex="0" BorderBrush="{x:Null}" FocusVisualStyle="{x:Null}"></ComboBox>
|
||||
<Button Click="RemapKey_Clicked" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
<TextBlock Text="None" TextAlignment="Center" TextWrapping="Wrap" MinWidth="150" Margin="5,1,5,1" HorizontalAlignment="Stretch" FontWeight="Light" VerticalAlignment="Stretch"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@ -208,19 +207,16 @@
|
||||
</GroupBox.Header>
|
||||
<Grid MaxWidth="500">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="0,2,0,0"/>
|
||||
<TextBlock Text="Prevent repetition rate (0-100%)" TextWrapping="Wrap" Margin="5,2,0,0"/>
|
||||
</StackPanel>
|
||||
<TextBlock TextWrapping="Wrap" Margin="0,10,0,0">
|
||||
<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">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>
|
||||
<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>
|
||||
@ -260,16 +256,24 @@
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock TextWrapping="Wrap" Margin="0,0,0,10" VerticalAlignment="Top">
|
||||
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
|
||||
<Run Text="{x:Static properties:Resources.Settings_Version}"/>
|
||||
<Run Text="{Binding GetVersion, Mode = OneWay}"/>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="UpdateText" TextWrapping="Wrap" VerticalAlignment="Top">
|
||||
<Run x:Name="UpdateText2" Text="New version X.X.X available!" FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
|
||||
<Button x:Name="UpdateButton" Content="{x:Static properties:Resources.Update_ButtonCheck}" Margin="0,3,0,10" Width="150" Click="CheckUpdates" HorizontalAlignment="Left"/>
|
||||
|
||||
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top">
|
||||
<Run Text="{x:Static properties:Resources.Settings_AboutInfo}" /><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/Difegue/MpcNET" RequestNavigate="Hyperlink_RequestNavigate">MpcNET</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/hardcodet/wpf-notifyicon" RequestNavigate="Hyperlink_RequestNavigate">wpf-notifyicon</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/samhocevar/emoji.wpf" RequestNavigate="Hyperlink_RequestNavigate">Emoji.WPF</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/tof4/RadioBrowser" RequestNavigate="Hyperlink_RequestNavigate">RadioBrowser</Hyperlink>
|
||||
※ <Hyperlink NavigateUri="https://github.com/tof4/RadioBrowser" RequestNavigate="Hyperlink_RequestNavigate">RadioBrowser</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/ravibpatel/AutoUpdater.NET" RequestNavigate="Hyperlink_RequestNavigate">AutoUpdater.NET</Hyperlink><LineBreak/>
|
||||
※ <Hyperlink NavigateUri="https://github.com/badaix/snapcast" RequestNavigate="Hyperlink_RequestNavigate">Snapcast</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,10,0,0">
|
||||
<Run Text="{x:Static properties:Resources.Settings_SourceCode1}" />
|
||||
@ -286,7 +290,11 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{x:Static properties:Resources.Settings_License}" Height="20" VerticalAlignment="Bottom">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0" Margin="0,10,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
|
@ -5,11 +5,13 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Navigation;
|
||||
using unison.Handlers;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
@ -60,6 +62,8 @@ namespace unison
|
||||
SnapcastPort.Text = Properties.Settings.Default.snapcast_port.ToString();
|
||||
VolumeOffset.Text = Properties.Settings.Default.volume_offset.ToString();
|
||||
|
||||
UpdateText.Visibility = Visibility.Collapsed;
|
||||
|
||||
InitializeShortcuts();
|
||||
}
|
||||
|
||||
@ -111,14 +115,11 @@ namespace unison
|
||||
|
||||
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(); });
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
Task.Run(async () => { await mpd.Initialize(); });
|
||||
}
|
||||
|
||||
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
|
||||
@ -145,6 +146,19 @@ namespace unison
|
||||
MPDConnect_Clicked(null, null);
|
||||
}
|
||||
|
||||
private void CheckUpdates(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateHandler updater = (UpdateHandler)Application.Current.Properties["updater"];
|
||||
updater.Start(true);
|
||||
}
|
||||
|
||||
public void UpdateUpdateStatus(string version)
|
||||
{
|
||||
UpdateText.Visibility = Visibility.Visible;
|
||||
UpdateText2.Text = unison.Resources.Resources.Update_String1 + " " + version + " " + unison.Resources.Resources.Update_String2;
|
||||
UpdateButton.Content = unison.Resources.Resources.Update_ButtonStart;
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
@ -376,12 +390,12 @@ namespace unison
|
||||
InitializeShortcuts();
|
||||
}
|
||||
|
||||
public uint GetMod(StackPanel stackPanel)
|
||||
private uint GetMod(StackPanel stackPanel)
|
||||
{
|
||||
return (uint)(GetMOD(stackPanel.Children.OfType<ComboBox>().First().SelectedItem.ToString()) | GetMOD(stackPanel.Children.OfType<ComboBox>().Last().SelectedItem.ToString()));
|
||||
}
|
||||
|
||||
public uint GetVk(StackPanel stackPanel)
|
||||
private uint GetVk(StackPanel stackPanel)
|
||||
{
|
||||
Button button = stackPanel.Children.OfType<Button>().First();
|
||||
TextBlock textBlock = (TextBlock)button.Content;
|
||||
|
@ -4,10 +4,44 @@
|
||||
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: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" KeyUp="QueryFilterHandler" Width="240" Margin="5,0,0,0"/>
|
||||
<Button Content="-" Padding="5, 2" Click="RemoveFilter_Clicked" Width="20" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="5,0,0,0"/>
|
||||
<Button Content="+" Padding="5, 2" Click="AddFilter_Clicked" Width="20" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<StackPanel>
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="5,0,5,5">
|
||||
@ -19,83 +53,64 @@
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical" Margin="5,0,5,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Song" Margin="0,0,0,2"/>
|
||||
<TextBox x:Name="Song" Width="240" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
|
||||
<TextBlock Text="Artist" Margin="0,0,0,2"/>
|
||||
<TextBox x:Name="Artist" Width="240" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="FilterPanel">
|
||||
<ContentPresenter ContentTemplate="{StaticResource FilterPanel}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Album" Margin="0,0,0,2"/>
|
||||
<TextBox x:Name="Album" Width="240" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
|
||||
<TextBlock Text="Year" Margin="0,0,0,2"/>
|
||||
<TextBox x:Name="Year" Width="240" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="Genre" Margin="0,0,0,2"/>
|
||||
<ComboBox x:Name="Genre" SelectedIndex="0" Width="240" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
|
||||
<TextBlock Text="Directory" Margin="0,0,0,2" TextDecorations="{x:Null}"/>
|
||||
<ComboBox x:Name="Directory" SelectedIndex="0" Width="240" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" IsEnabled="True"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
|
||||
<Button Content="Reset" Click="Reset_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="SongFilterPanel" Margin="0,5,0,0" Visibility="Collapsed">
|
||||
<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}"/>
|
||||
<TextBlock x:Name="QueryFilterText" Text="Querying filter..." Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Margin="0,5,0,0" HorizontalAlignment="Stretch">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<emoji:EmojiInline Text="♾️"/>
|
||||
<Run Text="Continuous shuffle"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal" Margin="5,8,0,0">
|
||||
<CheckBox x:Name="ContinuousShuffle" Checked="ContinuousShuffle_Checked" Unchecked="ContinuousShuffle_Checked" FocusVisualStyle="{x:Null}">
|
||||
<TextBlock Text="Enable continuous shuffle" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox x:Name="AddToQueueGroup" Margin="0,5,0,0" HorizontalAlignment="Stretch">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<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 Margin="5,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,10,0,0">
|
||||
<TextBox x:Name="SongNumber" Text="100" Width="35" HorizontalAlignment="Left" VerticalAlignment="Top"/>
|
||||
<TextBlock Text="Number of songs to add" Margin="5,0,0,5"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,2,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 to the 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" KeyUp="AddToQueueHandler" PreviewTextInput="QueueValidationTextBox" MaxLength="4" Text="15" Width="35" HorizontalAlignment="Left" VerticalAlignment="Top"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<Button Content="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="Adding "/><Run x:Name="NumberAddedSongs"/><Run Text=" songs..."/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</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>
|
||||
|
@ -1,99 +1,406 @@
|
||||
using MpcNET;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MpcNET.Commands.Database;
|
||||
using MpcNET.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.Interop;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class Shuffle : Window
|
||||
{
|
||||
private MPDHandler _mpd;
|
||||
private readonly MPDHandler _mpd;
|
||||
private readonly ShuffleHandler _shuffle;
|
||||
|
||||
List<string> GenreList { get; }
|
||||
List<string> FolderList { get; }
|
||||
List<IFilter> Filters { get; }
|
||||
|
||||
bool _continuous = false;
|
||||
List<string> _songList { get; }
|
||||
|
||||
public Shuffle()
|
||||
{
|
||||
InitializeComponent();
|
||||
_songList = new();
|
||||
GenreList = new();
|
||||
FolderList = new();
|
||||
Filters = new();
|
||||
SongFilterNumber.Text = "0";
|
||||
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
_shuffle = (ShuffleHandler)Application.Current.Properties["shuffle"];
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
ListGenre(_mpd._cancelCommand.Token);
|
||||
ListFolder(_mpd._cancelCommand.Token);
|
||||
}
|
||||
|
||||
public async void ListGenre(CancellationToken token)
|
||||
{
|
||||
if (GenreList.Count != 0)
|
||||
return;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
List<string> Response = await _mpd.SafelySendCommandAsync(new ListCommand(MpdTags.Genre, null, null));
|
||||
|
||||
if (Response == null)
|
||||
return;
|
||||
|
||||
foreach (string genre in Response)
|
||||
GenreList.Add(genre);
|
||||
}
|
||||
|
||||
public async void ListFolder(CancellationToken token)
|
||||
{
|
||||
if (FolderList.Count != 0)
|
||||
return;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
IEnumerable<IMpdFilePath> Response = await _mpd.SafelySendCommandAsync(new LsInfoCommand(""));
|
||||
|
||||
if (Response == null)
|
||||
return;
|
||||
|
||||
foreach (IMpdFilePath folder in Response)
|
||||
FolderList.Add(folder.Name);
|
||||
}
|
||||
|
||||
private bool IsFilterEmpty()
|
||||
{
|
||||
if (Song.Text.Length == 0 && Artist.Text.Length == 0 && Album.Text.Length == 0 && Year.Text.Length == 0 && Genre.SelectedIndex == 0 && Directory.Text.Length == 0)
|
||||
if (Filters.Count() == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private T FindParent<T>(DependencyObject child) where T : DependencyObject
|
||||
{
|
||||
var parent = VisualTreeHelper.GetParent(child);
|
||||
|
||||
if (parent == null)
|
||||
return null;
|
||||
|
||||
if (parent is T)
|
||||
return parent as T;
|
||||
else
|
||||
return FindParent<T>(parent);
|
||||
}
|
||||
|
||||
private void AddFilter_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FilterPanel.Children.Add(new ContentPresenter { ContentTemplate = (DataTemplate)FindResource("FilterPanel") });
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private void RemoveFilter_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (FilterPanel.Children.Count > 1)
|
||||
FilterPanel.Children.Remove(FindParent<ContentPresenter>(sender as Button));
|
||||
else
|
||||
Reset_Clicked(null, null);
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private void Reset_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FilterPanel.Children.RemoveRange(0, FilterPanel.Children.Count);
|
||||
FilterPanel.Children.Add(new ContentPresenter { ContentTemplate = (DataTemplate)FindResource("FilterPanel") });
|
||||
SongFilterNumber.Text = "0";
|
||||
_shuffle.SongList.Clear();
|
||||
}
|
||||
|
||||
private 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 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)
|
||||
{
|
||||
SongFilterNumber.Text = "0";
|
||||
}
|
||||
|
||||
private async void UpdateFilter_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
QueryFilterText.Visibility = Visibility.Visible;
|
||||
await UpdateFilter();
|
||||
TimedText(QueryFilterText, 1);
|
||||
}
|
||||
|
||||
private void TimedText(TextBlock textBlock, int time)
|
||||
{
|
||||
DispatcherTimer Timer = new DispatcherTimer();
|
||||
Timer.Interval = TimeSpan.FromSeconds(time);
|
||||
Timer.Tick += (sender, args) =>
|
||||
{
|
||||
Timer.Stop();
|
||||
textBlock.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
Timer.Start();
|
||||
}
|
||||
|
||||
private async Task UpdateFilter()
|
||||
{
|
||||
Filters.Clear();
|
||||
|
||||
foreach (ContentPresenter superChild in FilterPanel.Children)
|
||||
{
|
||||
ITag tag = MpdTags.Title;
|
||||
FilterOperator op = FilterOperator.None;
|
||||
string value = "";
|
||||
bool isDir = false;
|
||||
|
||||
StackPanel stackPanel = VisualTreeHelper.GetChild(superChild, 0) as StackPanel;
|
||||
foreach (TextBox child in stackPanel.Children.OfType<TextBox>())
|
||||
{
|
||||
if (child.Name == "FilterValue")
|
||||
value = child.Text;
|
||||
}
|
||||
|
||||
foreach (ComboBox child in stackPanel.Children.OfType<ComboBox>())
|
||||
{
|
||||
if (child.Name == "FilterType")
|
||||
{
|
||||
if (child.SelectedItem.ToString() == "Directory")
|
||||
isDir = true;
|
||||
else
|
||||
tag = FilterEquivalence_Type(child.SelectedItem.ToString());
|
||||
}
|
||||
|
||||
if (child.Name == "FilterOperator")
|
||||
op = FilterEquivalence_Operator(child.SelectedItem.ToString());
|
||||
|
||||
if (child.Name == "FilterList" && child.Visibility == Visibility.Visible)
|
||||
value = child.SelectedItem.ToString();
|
||||
}
|
||||
|
||||
if (value != "")
|
||||
{
|
||||
if (!isDir)
|
||||
Filters.Add(new FilterTag(tag, value, op));
|
||||
else
|
||||
Filters.Add(new FilterBase(value, FilterOperator.None));
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _shuffle.GetSongsFromFilter(Filters, _mpd._cancelCommand.Token);
|
||||
});
|
||||
SongFilterPanel.Visibility = Visibility.Visible;
|
||||
SongFilterNumber.Text = _shuffle.SongList.Count.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void QueryFilterHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
UpdateFilter_Clicked(null, null);
|
||||
}
|
||||
|
||||
private void QueueValidationTextBox(object sender, TextCompositionEventArgs e)
|
||||
{
|
||||
Regex regex = new Regex("[^0-9]+");
|
||||
e.Handled = regex.IsMatch(e.Text);
|
||||
}
|
||||
|
||||
private void QueueValidationNumber()
|
||||
{
|
||||
int Number;
|
||||
try
|
||||
{
|
||||
Number = int.Parse(SongNumber.Text);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number < 1)
|
||||
SongNumber.Text = "1";
|
||||
if (IsFilterEmpty())
|
||||
{
|
||||
if (Number > 100)
|
||||
SongNumber.Text = "100";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Number > 1000)
|
||||
SongNumber.Text = "1000";
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddToQueue()
|
||||
{
|
||||
if (_mpd.GetStats() == null)
|
||||
return;
|
||||
|
||||
await UpdateFilter();
|
||||
QueueValidationNumber();
|
||||
|
||||
// TODO
|
||||
// Added => Adding songs...
|
||||
// to
|
||||
// Added X songs! (display for 5 seconds)
|
||||
|
||||
NumberAddedSongs.Text = SongNumber.Text;
|
||||
SearchStatus.Visibility = Visibility.Visible;
|
||||
|
||||
int Num = int.Parse(SongNumber.Text);
|
||||
await AddToQueue_Internal(Num);
|
||||
|
||||
TimedText(SearchStatus, 2);
|
||||
}
|
||||
|
||||
private async Task AddToQueue_Internal(int Num)
|
||||
{
|
||||
if (IsFilterEmpty())
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _shuffle.AddToQueueRandom(Num, _mpd._cancelCommand.Token);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await _shuffle.AddToQueueFilter(Num, _mpd._cancelCommand.Token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToQueueHandler(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Return)
|
||||
AddToQueue();
|
||||
}
|
||||
|
||||
private void AddToQueue_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddToQueue();
|
||||
}
|
||||
|
||||
public bool GetContinuous()
|
||||
{
|
||||
return _continuous;
|
||||
}
|
||||
|
||||
public void AddContinuousSongs()
|
||||
public async Task HandleContinuous()
|
||||
{
|
||||
if (IsFilterEmpty())
|
||||
{
|
||||
// add a completely random song
|
||||
ContinuousShuffle_AddToQueueRandom();
|
||||
if (!_continuous)
|
||||
return;
|
||||
}
|
||||
|
||||
int AddedSongs = 0;
|
||||
NumberAddedSongs.Text = AddedSongs.ToString();
|
||||
SearchStatus.Visibility = Visibility.Visible;
|
||||
int PlaylistLength = _mpd.GetStatus().PlaylistLength;
|
||||
int Num = 10 - PlaylistLength;
|
||||
if (Num < 1)
|
||||
return;
|
||||
|
||||
HashSet<int> SongIndex = new();
|
||||
while (SongIndex.Count < 2)
|
||||
{
|
||||
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;
|
||||
await UpdateFilter();
|
||||
await AddToQueue_Internal(Num);
|
||||
}
|
||||
|
||||
public async void ListGenre()
|
||||
private async void ContinuousShuffle_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Genre.Items.Count == 0)
|
||||
{
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
List<string> Response = await _mpd.SafelySendCommandAsync(new ListCommand(MpdTags.Genre, null, null));
|
||||
if (ContinuousShuffle.IsChecked == true)
|
||||
_continuous = true;
|
||||
else
|
||||
_continuous = false;
|
||||
|
||||
if (Response.Count > 0)
|
||||
{
|
||||
Genre.Items.Add("");
|
||||
foreach (var genre in Response)
|
||||
Genre.Items.Add(genre);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void ListFolder()
|
||||
{
|
||||
if (Directory.Items.Count == 0)
|
||||
{
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
IEnumerable<IMpdFilePath> Response = await _mpd.SafelySendCommandAsync(new LsInfoCommand(""));
|
||||
|
||||
if (Response != null)
|
||||
{
|
||||
Directory.Items.Add("");
|
||||
foreach (var directory in Response)
|
||||
Directory.Items.Add(directory.Name);
|
||||
}
|
||||
}
|
||||
if (_mpd.GetStatus().PlaylistLength < 10)
|
||||
await HandleContinuous();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
@ -108,200 +415,5 @@ namespace unison
|
||||
WindowInteropHelper helper = new(this);
|
||||
helper.EnsureHandle();
|
||||
}
|
||||
|
||||
private void Reset_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Song.Text = "";
|
||||
Artist.Text = "";
|
||||
Album.Text = "";
|
||||
Year.Text = "";
|
||||
Genre.SelectedIndex = 0;
|
||||
Directory.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private async void ContinuousShuffle_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ContinuousShuffle.IsChecked == true)
|
||||
{
|
||||
AddToQueueGroup.IsEnabled = false;
|
||||
_continuous = true;
|
||||
_songList.Clear();
|
||||
if (!IsFilterEmpty())
|
||||
{
|
||||
/*await*/
|
||||
GetSongsFromFilter();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddToQueueGroup.IsEnabled = true;
|
||||
_continuous = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ContinuousShuffle_AddToQueueRandom()
|
||||
{
|
||||
for (int i = 0; i < 2; 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(MpdTags.Title, "", 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);
|
||||
}
|
||||
}
|
||||
|
||||
SearchStatus.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async void AddToQueueRandom()
|
||||
{
|
||||
int AddedSongs = 0;
|
||||
NumberAddedSongs.Text = AddedSongs.ToString();
|
||||
SearchStatus.Visibility = Visibility.Visible;
|
||||
|
||||
for (int i = 0; i < int.Parse(SongNumber.Text); 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(MpdTags.Title, "", 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);
|
||||
|
||||
AddedSongs++;
|
||||
NumberAddedSongs.Text = AddedSongs.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
SearchStatus.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async Task GetSongsFromFilter()
|
||||
{
|
||||
_songList.Clear();
|
||||
SongFilterPanel.Visibility = Visibility.Visible;
|
||||
|
||||
int song = _mpd.GetStats().Songs;
|
||||
|
||||
List<IFilter> filtersA = new();
|
||||
if (Song.Text != "")
|
||||
filtersA.Add(new FilterTag(MpdTags.Title, Song.Text, FilterOperator.Contains));
|
||||
if (Artist.Text != "")
|
||||
filtersA.Add(new FilterTag(MpdTags.Artist, Artist.Text, FilterOperator.Contains));
|
||||
if (Album.Text != "")
|
||||
filtersA.Add(new FilterTag(MpdTags.Album, Album.Text, FilterOperator.Contains));
|
||||
if (Year.Text != "")
|
||||
filtersA.Add(new FilterTag(MpdTags.Date, Year.Text, FilterOperator.Contains));
|
||||
if (Genre.Text != "")
|
||||
filtersA.Add(new FilterTag(MpdTags.Genre, Genre.Text, FilterOperator.Contains));
|
||||
if (Directory.Text != "")
|
||||
filtersA.Add(new FilterBase(Directory.Text, FilterOperator.None));
|
||||
|
||||
Debug.WriteLine(Directory.Text);
|
||||
|
||||
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new SearchCommand(filtersA, 0, song + 1) });
|
||||
string Response = await _mpd.SafelySendCommandAsync(commandList);
|
||||
|
||||
Debug.WriteLine(Response);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
foreach (string file in value)
|
||||
{
|
||||
int start = 0;
|
||||
int end = file.IndexOf("],");
|
||||
string filePath = file.Substring(start, end - start);
|
||||
|
||||
Debug.WriteLine(filePath);
|
||||
_songList.Add(filePath);
|
||||
|
||||
SongFilterNumber.Text = _songList.Count.ToString();
|
||||
}
|
||||
|
||||
// remove characters from first file
|
||||
_songList[0] = _songList[0].Substring(7, _songList[0].Length - 7);
|
||||
|
||||
SongFilterPanel.Visibility = Visibility.Visible;
|
||||
SongFilterNumber.Text = _songList.Count.ToString();
|
||||
}
|
||||
|
||||
private async void AddToQueueFilter()
|
||||
{
|
||||
await GetSongsFromFilter();
|
||||
|
||||
int AddedSongs = 0;
|
||||
NumberAddedSongs.Text = AddedSongs.ToString();
|
||||
SearchStatus.Visibility = Visibility.Visible;
|
||||
|
||||
// more requested songs than available => add everything
|
||||
if (int.Parse(SongNumber.Text) > _songList.Count)
|
||||
{
|
||||
foreach (string path in _songList)
|
||||
{
|
||||
await Task.Delay(1);
|
||||
_mpd.AddSong(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 < int.Parse(SongNumber.Text))
|
||||
{
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
private void AddToQueue_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
if (_mpd.GetStats() == null)
|
||||
return;
|
||||
|
||||
if (IsFilterEmpty())
|
||||
AddToQueueRandom();
|
||||
else
|
||||
AddToQueueFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,17 +2,17 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
|
||||
<Win32Resource></Win32Resource>
|
||||
<StartupObject>unison.App</StartupObject>
|
||||
<Version>1.3</Version>
|
||||
<Version>1.4</Version>
|
||||
<Company />
|
||||
<Authors>Théo Marchal</Authors>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://git.n700.ovh/keb/unison</PackageProjectUrl>
|
||||
<RepositoryUrl>https://git.n700.ovh/keb/unison</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/ZetaKebab/unison</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/ZetaKebab/unison</RepositoryUrl>
|
||||
<Copyright>Théo Marchal</Copyright>
|
||||
<PackageIconUrl />
|
||||
</PropertyGroup>
|
||||
@ -22,6 +22,8 @@
|
||||
<None Remove="Resources\icon-mini.ico" />
|
||||
<None Remove="Resources\nocover.png" />
|
||||
<None Remove="LICENSE" />
|
||||
<None Remove="Resources\nothing.png" />
|
||||
<None Remove="Resources\radio.png" />
|
||||
<None Include="LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
@ -38,12 +40,19 @@
|
||||
<Resource Include="Resources\nocover.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="Resources\nothing.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="Resources\radio.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Content Include="LICENSE">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.6" />
|
||||
<PackageReference Include="Emoji.Wpf" Version="0.3.3" />
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
|
||||
<PackageReference Include="RadioBrowser" Version="0.6.1" />
|
||||
|