Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
f6eb00759a | |||
468ec8be07 | |||
52b0a6fc85 | |||
5a43a1284e | |||
06207d9791 | |||
2f80af70ba | |||
aeb1b1d573 | |||
8451e684db | |||
9c8027428c | |||
8e04161a60 | |||
ccbb4525f3 | |||
529754934d | |||
0471ba88c1 | |||
3b55c79d30 | |||
b33ae3b9be | |||
5142477b5e | |||
a6b7ad9c1e | |||
1d3515a39d | |||
792437b839 | |||
5bfa7d3b5b | |||
c5e8534af7 | |||
5d6e3b6d1e | |||
6ad4d9c813 | |||
c93a9a326e | |||
c055c59de7 | |||
4c71d6a6e0 | |||
3685c369b4 | |||
e0d640532c |
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");
|
||||
|
@ -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;
|
||||
@ -65,7 +67,7 @@ namespace unison
|
||||
private MpcConnection _commandConnection;
|
||||
private IPEndPoint _mpdEndpoint;
|
||||
|
||||
private CancellationTokenSource _cancelCommand;
|
||||
public CancellationTokenSource _cancelCommand;
|
||||
private CancellationTokenSource _cancelConnect;
|
||||
|
||||
public MPDHandler()
|
||||
@ -474,14 +476,12 @@ namespace unison
|
||||
else
|
||||
{
|
||||
using MemoryStream stream = new MemoryStream(data.ToArray());
|
||||
try
|
||||
{
|
||||
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_cover = null;
|
||||
}
|
||||
_cover = new BitmapImage();
|
||||
_cover.BeginInit();
|
||||
_cover.CacheOption = BitmapCacheOption.OnLoad;
|
||||
_cover.StreamSource = stream;
|
||||
_cover.EndInit();
|
||||
_cover.Freeze();
|
||||
}
|
||||
UpdateCover();
|
||||
}
|
||||
@ -523,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));
|
||||
@ -588,27 +602,35 @@ 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());
|
||||
Dictionary<string, string> Response = await SafelySendCommandAsync(new StatsCommand());
|
||||
if (Response == null)
|
||||
return;
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
_stats.Songs = int.Parse(response["songs"]);
|
||||
_stats.Albums = int.Parse(response["albums"]);
|
||||
_stats.Artists = int.Parse(response["artists"]);
|
||||
_stats.Songs = int.Parse(Response["songs"]);
|
||||
_stats.Albums = int.Parse(Response["albums"]);
|
||||
_stats.Artists = int.Parse(Response["artists"]);
|
||||
|
||||
TimeSpan time;
|
||||
time = TimeSpan.FromSeconds(int.Parse(response["uptime"]));
|
||||
time = TimeSpan.FromSeconds(int.Parse(Response["uptime"]));
|
||||
_stats.Uptime = time.ToString(@"dd\:hh\:mm\:ss");
|
||||
time = TimeSpan.FromSeconds(int.Parse(response["db_playtime"]));
|
||||
time = TimeSpan.FromSeconds(int.Parse(Response["db_playtime"]));
|
||||
_stats.TotalPlaytime = time.ToString(@"dd\:hh\:mm\:ss");
|
||||
time = TimeSpan.FromSeconds(int.Parse(response["playtime"]));
|
||||
time = TimeSpan.FromSeconds(int.Parse(Response["playtime"]));
|
||||
_stats.TotalTimePlayed = time.ToString(@"dd\:hh\:mm\:ss");
|
||||
|
||||
DateTime date = new DateTime(1970, 1, 1).AddSeconds(int.Parse(response["db_update"])).ToLocalTime();
|
||||
DateTime date = new DateTime(1970, 1, 1).AddSeconds(int.Parse(Response["db_update"])).ToLocalTime();
|
||||
_stats.DatabaseUpdate = date.ToString("dd/MM/yyyy @ HH:mm");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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>
|
||||
@ -303,4 +303,25 @@
|
||||
<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>
|
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 |
@ -111,14 +111,15 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
<Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
|
||||
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="10,0,0,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0">
|
||||
<Button x:Name="Shuffle" Padding="5, 2" Click="Shuffle_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" Margin="0,0,10,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
|
||||
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
|
||||
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Margin="5,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
|
||||
<Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="📻" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Radios}" Margin="5, 0, 0, 0"/>
|
||||
@ -136,12 +137,13 @@
|
||||
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="5,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
|
||||
<!--<Button x:Name="Shuffle" Padding="5, 2" HorizontalAlignment="Right" Margin="0,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
|
||||
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="0,0,10,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
|
||||
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
|
||||
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
|
||||
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
|
||||
</StackPanel>
|
||||
</Button>-->
|
||||
</Button>
|
||||
|
||||
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>
|
||||
|
@ -6,6 +6,7 @@ using System.Windows.Threading;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
@ -13,6 +14,7 @@ namespace unison
|
||||
{
|
||||
private readonly Settings _settingsWin;
|
||||
private readonly Radios _radiosWin;
|
||||
private readonly Shuffle _shuffleWin;
|
||||
private readonly DispatcherTimer _timer;
|
||||
private readonly MPDHandler _mpd;
|
||||
|
||||
@ -25,6 +27,7 @@ namespace unison
|
||||
|
||||
_settingsWin = new Settings();
|
||||
_radiosWin = new Radios();
|
||||
_shuffleWin = new Shuffle();
|
||||
_timer = new DispatcherTimer();
|
||||
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
|
||||
@ -46,12 +49,17 @@ namespace unison
|
||||
{
|
||||
if (_mpd.IsConnected())
|
||||
{
|
||||
_mpd.QueryStats();
|
||||
_settingsWin.UpdateStats();
|
||||
|
||||
ConnectionOkIcon.Visibility = Visibility.Visible;
|
||||
ConnectionFailIcon.Visibility = Visibility.Collapsed;
|
||||
|
||||
Snapcast.IsEnabled = true;
|
||||
if (_radiosWin.IsConnected())
|
||||
Radio.IsEnabled = true;
|
||||
|
||||
_shuffleWin.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -67,7 +75,7 @@ namespace unison
|
||||
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
|
||||
}
|
||||
|
||||
public void OnSongChanged(object sender, EventArgs e)
|
||||
public async void OnSongChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (_mpd.GetCurrentSong() == null)
|
||||
return;
|
||||
@ -113,6 +121,15 @@ namespace unison
|
||||
_timer.Start();
|
||||
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
|
||||
}
|
||||
|
||||
Trace.WriteLine("Song changed called!");
|
||||
|
||||
if (_shuffleWin.GetContinuous())
|
||||
{
|
||||
_mpd.CanPrevNext = false;
|
||||
await _shuffleWin.HandleContinuous();
|
||||
_mpd.CanPrevNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStatusChanged(object sender, EventArgs e)
|
||||
@ -141,9 +158,6 @@ namespace unison
|
||||
DefaultState();
|
||||
}
|
||||
}
|
||||
|
||||
_mpd.QueryStats();
|
||||
_settingsWin.UpdateStats();
|
||||
}
|
||||
|
||||
private void DefaultState(bool LostConnection = false)
|
||||
@ -216,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();
|
||||
@ -238,6 +251,15 @@ namespace unison
|
||||
_radiosWin.WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
public void Shuffle_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_shuffleWin.Show();
|
||||
_shuffleWin.Activate();
|
||||
|
||||
if (_shuffleWin.WindowState == WindowState.Minimized)
|
||||
_shuffleWin.WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
public void Settings_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_settingsWin.Show();
|
||||
@ -271,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;
|
||||
private bool _connected = true;
|
||||
RadioHandler _radio;
|
||||
|
||||
public bool IsConnected() => _connected;
|
||||
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;
|
||||
}
|
||||
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
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,28 +48,50 @@ 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 () =>
|
||||
{
|
||||
return await _radio.AdvancedSearch(new AdvancedSearchOptions
|
||||
{
|
||||
Name = name,
|
||||
Country = country,
|
||||
TagList = tags
|
||||
});
|
||||
});
|
||||
|
||||
RadioListGrid.Items.Clear();
|
||||
if (advancedSearch.Count > 0)
|
||||
@ -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;
|
||||
|
@ -59,36 +59,6 @@
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Snapcast">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<emoji:TextBlock Text="🔊 Snapcast"/>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPath}" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
|
||||
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
|
||||
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
|
||||
</TextBlock>
|
||||
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
@ -190,7 +160,64 @@
|
||||
</StackPanel>
|
||||
|
||||
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="ShortcutsReset_Clicked"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Snapcast">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<emoji:TextBlock Text="🔊 Snapcast"/>
|
||||
</GroupBox.Header>
|
||||
<Grid VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
|
||||
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPath}" TextWrapping="Wrap" Margin="5,5,0,0"/>
|
||||
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
|
||||
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
|
||||
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
|
||||
</TextBlock>
|
||||
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Shuffle">
|
||||
<DockPanel Margin="8">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<emoji:EmojiInline Text="🔁 "/>
|
||||
<Run Text="Shuffle"></Run>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<Grid MaxWidth="500">
|
||||
<StackPanel>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run>The shuffle window allows to add random songs to your queue. Both options take into account the filter.</Run>
|
||||
<Run>If the filter is empty, the entire music library is taken into account.</Run><LineBreak/><LineBreak/>
|
||||
|
||||
<Run FontWeight="Bold">Add to queue</Run><LineBreak/>
|
||||
<Run>Add a fixed number of songs to the queue. It can take a long time to add more than 100 songs, so the option is limited to 1000 songs.</Run>
|
||||
<LineBreak/><LineBreak/>
|
||||
|
||||
<Run FontWeight="Bold">Continuous shuffle</Run><LineBreak/>
|
||||
<Run>By enabling this option, unison will automatically add songs to the queue so you never run out of songs to listen to.</Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
@ -229,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}" />
|
||||
@ -255,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();
|
||||
}
|
||||
|
||||
@ -115,7 +119,7 @@ namespace unison
|
||||
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
|
||||
|
||||
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
|
||||
System.Threading.Tasks.Task.Run(async () => { await mpd.Initialize(); });
|
||||
Task.Run(async () => { await mpd.Initialize(); });
|
||||
}
|
||||
|
||||
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)
|
||||
@ -142,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;
|
||||
@ -373,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;
|
||||
|
117
Views/Shuffle.xaml
Normal file
@ -0,0 +1,117 @@
|
||||
<Window x:Class="unison.Shuffle"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
|
||||
xmlns:local="clr-namespace:unison" xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||
mc:Ignorable="d"
|
||||
Title="Shuffle" Closing="Window_Closing" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
|
||||
|
||||
<Window.Resources>
|
||||
<x:Array x:Key="FilterType" Type="sys:String">
|
||||
<sys:String>Song</sys:String>
|
||||
<sys:String>Artist</sys:String>
|
||||
<sys:String>Album</sys:String>
|
||||
<sys:String>Year</sys:String>
|
||||
<sys:String>Genre</sys:String>
|
||||
<sys:String>Directory</sys:String>
|
||||
</x:Array>
|
||||
<x:Array x:Key="OperatorTypeA" Type="sys:String">
|
||||
<sys:String>contains</sys:String>
|
||||
<sys:String>is</sys:String>
|
||||
<sys:String>is not</sys:String>
|
||||
</x:Array>
|
||||
<x:Array x:Key="OperatorTypeB" Type="sys:String">
|
||||
<sys:String>is</sys:String>
|
||||
<sys:String>is not</sys:String>
|
||||
</x:Array>
|
||||
<x:Array x:Key="OperatorTypeC" Type="sys:String">
|
||||
<sys:String>is</sys:String>
|
||||
</x:Array>
|
||||
|
||||
<DataTemplate x:Key="FilterPanel">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<ComboBox x:Name="FilterType" SelectionChanged="FilterType_SelectionChanged" ItemsSource="{StaticResource FilterType}" SelectedIndex="0" Width="100" ScrollViewer.CanContentScroll="False" FocusVisualStyle="{x:Null}"/>
|
||||
<ComboBox x:Name="FilterOperator" SelectionChanged="OperatorType_SelectionChanged" ItemsSource="{StaticResource OperatorTypeA}" SelectedIndex="0" Width="80" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
|
||||
<ComboBox x:Name="FilterList" SelectedIndex="0" Width="240" Visibility="Collapsed" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBox x:Name="FilterValue" 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">
|
||||
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<emoji:EmojiInline Text="🔡"/>
|
||||
<Run Text="Filter"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical" Margin="5,0,5,0">
|
||||
|
||||
<StackPanel x:Name="FilterPanel">
|
||||
<ContentPresenter ContentTemplate="{StaticResource FilterPanel}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="SongFilterPanel" Margin="0,10,0,0">
|
||||
<TextBlock>
|
||||
<Run Text="Number of songs in filter: "/><Run x:Name="SongFilterNumber" FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,5,0,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
|
||||
<Button Content="Query filter" Click="UpdateFilter_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}" Margin="0,0,10,0"/>
|
||||
<Button Content="Reset" Click="Reset_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
|
||||
<TextBlock x:Name="QueryFilterText" Text="Querying filter..." Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<GroupBox DockPanel.Dock="Right" Padding="0,4,0,0" Width="248">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<emoji:EmojiInline Text="➕"/>
|
||||
<Run Text="Add to queue"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Vertical" Margin="5,5,5,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<TextBlock Text="Songs to add" Margin="0,0,5,5"/>
|
||||
<TextBox x:Name="SongNumber" 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>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox DockPanel.Dock="Left" Padding="0,4,0,0" Width="248" Margin="0,0,5,0">
|
||||
<GroupBox.Header>
|
||||
<TextBlock>
|
||||
<emoji:EmojiInline Text="♾️"/>
|
||||
<Run Text="Continuous shuffle"/>
|
||||
</TextBlock>
|
||||
</GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal" Margin="5,7,5,0">
|
||||
<CheckBox x:Name="ContinuousShuffle" Checked="ContinuousShuffle_Checked" Unchecked="ContinuousShuffle_Checked" FocusVisualStyle="{x:Null}" VerticalAlignment="Top">
|
||||
<TextBlock Text="Enable continuous shuffle" TextWrapping="Wrap"/>
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
419
Views/Shuffle.xaml.cs
Normal file
@ -0,0 +1,419 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MpcNET.Commands.Database;
|
||||
using MpcNET.Tags;
|
||||
using MpcNET.Types;
|
||||
using MpcNET.Types.Filters;
|
||||
|
||||
namespace unison
|
||||
{
|
||||
public partial class Shuffle : Window
|
||||
{
|
||||
private readonly MPDHandler _mpd;
|
||||
private readonly ShuffleHandler _shuffle;
|
||||
|
||||
List<string> GenreList { get; }
|
||||
List<string> FolderList { get; }
|
||||
List<IFilter> Filters { get; }
|
||||
|
||||
bool _continuous = false;
|
||||
|
||||
public Shuffle()
|
||||
{
|
||||
InitializeComponent();
|
||||
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 (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 async Task HandleContinuous()
|
||||
{
|
||||
if (!_continuous)
|
||||
return;
|
||||
|
||||
int PlaylistLength = _mpd.GetStatus().PlaylistLength;
|
||||
int Num = 10 - PlaylistLength;
|
||||
if (Num < 1)
|
||||
return;
|
||||
|
||||
await UpdateFilter();
|
||||
await AddToQueue_Internal(Num);
|
||||
}
|
||||
|
||||
private async void ContinuousShuffle_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ContinuousShuffle.IsChecked == true)
|
||||
_continuous = true;
|
||||
else
|
||||
_continuous = false;
|
||||
|
||||
if (_mpd.GetStatus().PlaylistLength < 10)
|
||||
await HandleContinuous();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void InitHwnd()
|
||||
{
|
||||
WindowInteropHelper helper = new(this);
|
||||
helper.EnsureHandle();
|
||||
}
|
||||
}
|
||||
}
|
@ -30,11 +30,11 @@
|
||||
<Image Width="16" Height="16" emoji:Image.Source="📻" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<!--<MenuItem Header="Shuffle" Command="{Binding Shuffle}">
|
||||
<MenuItem Header="Shuffle" Command="{Binding Shuffle}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="🔀" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>-->
|
||||
</MenuItem>
|
||||
<MenuItem Header="{x:Static properties:Resources.Settings}" Command="{Binding Settings}">
|
||||
<MenuItem.Icon>
|
||||
<Image Width="16" Height="16" emoji:Image.Source="🛠️" />
|
||||
|
@ -71,6 +71,18 @@ namespace unison
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand Shuffle
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DelegateCommand
|
||||
{
|
||||
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Shuffle_Clicked(null, null),
|
||||
CanExecuteFunc = () => true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand Settings
|
||||
{
|
||||
get
|
||||
|
@ -7,7 +7,7 @@
|
||||
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
|
||||
<Win32Resource></Win32Resource>
|
||||
<StartupObject>unison.App</StartupObject>
|
||||
<Version>1.3.1</Version>
|
||||
<Version>1.4</Version>
|
||||
<Company />
|
||||
<Authors>Théo Marchal</Authors>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
@ -52,6 +52,7 @@
|
||||
</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" />
|
||||
|