10 Commits

23 changed files with 795 additions and 118 deletions

View File

@ -1,19 +1,5 @@
# Changelog # Changelog
## 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 ## v1.2
*Released: 07/04/2022* *Released: 07/04/2022*

View File

@ -155,7 +155,9 @@ namespace unison
try try
{ {
Debug.WriteLine("SafelySendCommandAsync => before command");
IMpdMessage<T> response = await _commandConnection.SendAsync(command); IMpdMessage<T> response = await _commandConnection.SendAsync(command);
Debug.WriteLine("SafelySendCommandAsync => after command");
if (!response.IsResponseValid) if (!response.IsResponseValid)
{ {
string mpdError = response.Response?.Result?.MpdError; string mpdError = response.Response?.Result?.MpdError;
@ -498,6 +500,8 @@ namespace unison
public void Next() => SendCommand(new NextCommand()); public void Next() => SendCommand(new NextCommand());
public void PlayPause() => SendCommand(new PauseResumeCommand()); public void PlayPause() => SendCommand(new PauseResumeCommand());
public void Play(int pos) => SendCommand(new PlayCommand(pos));
public void Random() => SendCommand(new RandomCommand(!_currentRandom)); public void Random() => SendCommand(new RandomCommand(!_currentRandom));
public void Repeat() => SendCommand(new RepeatCommand(!_currentRepeat)); public void Repeat() => SendCommand(new RepeatCommand(!_currentRepeat));
public void Single() => SendCommand(new SingleCommand(!_currentSingle)); public void Single() => SendCommand(new SingleCommand(!_currentSingle));
@ -553,25 +557,24 @@ namespace unison
public async void QueryStats() 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.Songs = int.Parse(response["songs"]); _stats.Artists = int.Parse(Response["artists"]);
_stats.Albums = int.Parse(response["albums"]);
_stats.Artists = int.Parse(response["artists"]);
TimeSpan time; TimeSpan time;
time = TimeSpan.FromSeconds(int.Parse(response["uptime"])); time = TimeSpan.FromSeconds(int.Parse(Response["uptime"]));
_stats.Uptime = time.ToString(@"dd\:hh\:mm\:ss"); _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"); _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"); _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"); _stats.DatabaseUpdate = date.ToString("dd/MM/yyyy @ HH:mm");
}
} }
} }
} }

View File

@ -1,13 +1,14 @@
# unison # unison
![Main window](Screenshots/screen1.png) ![Main window](Screenshots/screen-mainwindow.png)
**unison** is a very simple [Music Player Daemon (MPD)](https://www.musicpd.org/) daemon client with the following goals: **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 * lightweight window that can be toggled with shortcuts
* music control through rebindable shortcuts * music control through rebindable shortcuts
* shuffle panel
* [Snapcast](https://mjaggard.github.io/snapcast/) integration * [Snapcast](https://mjaggard.github.io/snapcast/) integration
* Radio stations * radio stations
## Features ## Features
@ -15,13 +16,25 @@
By default, unison works as a daemon in the taskbar system tray. You can display the main window when needed at any time with a shortcut. By default, unison works as a daemon in the taskbar system tray. You can display the main window when needed at any time with a shortcut.
![Systray](Screenshots/screen2.png) ![Systray](Screenshots/screen-systray.png)
### Shortcuts ### 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. You can control your music at anytime with the shortcuts. They are usable system-wide, even if the window is not visible. They are of course fully rebindable.
![Settings => shortcuts](Screenshots/screen3.png) ![Settings => shortcuts](Screenshots/screen-shortcuts.png)
### Shuffle panel
One of unison's main feature is a complete shuffle system based on criterias, aka a smart playlist.
You have two options:
* **Add to queue** allows you to add a defined number of songs to the queue.
* **Continuous shuffle** allows you, as long as the program is running, to automatically add songs to the queue.
Each of these options work with filters, but if none are selected, it is based on the entire library.
![Shuffle panel](Screenshots/screen-shuffle.png)
### Snapcast ### Snapcast
@ -31,12 +44,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. Through [Radio-Browser](https://www.radio-browser.info), a community database, you can play radio streams directly from unison. There are more than 28,000 stations recorded on this service, so it is a nice way to discover new music and cultures.
![Radio stations](Screenshots/screen4.png) ![Radio stations](Screenshots/screen-radio.png)
## Planned features ## Planned features
* A complete shuffle system based on set criteria, aka a smart playlist. * Playlist, queue and library management
* Playlist, queue and library management. I use other software to do it, but I will implement them at some point. * More options for the shuffle panel
* Dark mode
## Translations ## Translations

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -104,21 +104,21 @@
<VisualBrush Visual="{Binding ElementName=mask}"/> <VisualBrush Visual="{Binding ElementName=mask}"/>
</StackPanel.OpacityMask> </StackPanel.OpacityMask>
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" /> <Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" /> <Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" />
<Image x:Name="RadioCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/radio.png" Visibility="Collapsed" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
<Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40"> <Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0"> <StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0">
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="10,0,0,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False"> <Button x:Name="Shuffle" Padding="5, 2" Click="Shuffle_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" Margin="0,0,10,0">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/> <emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/> <TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
</StackPanel> </StackPanel>
</Button> </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"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="📻" Padding="0,0,0,2"/> <emoji:TextBlock Text="📻" Padding="0,0,0,2"/>
<TextBlock Text="{x:Static properties:Resources.Radios}" Margin="5, 0, 0, 0"/> <TextBlock Text="{x:Static properties:Resources.Radios}" Margin="5, 0, 0, 0"/>
@ -136,12 +136,13 @@
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="5,0,0,0" /> <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>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0"> <StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
<!--<Button x:Name="Shuffle" Padding="5, 2" HorizontalAlignment="Right" Margin="0,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> <Button x:Name="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"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/> <emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/> <TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
</StackPanel> </StackPanel>
</Button>--> </Button>
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}"> <Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/> <emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>

View File

@ -6,6 +6,8 @@ using System.Windows.Threading;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using MpcNET.Commands.Queue;
using System.Diagnostics;
namespace unison namespace unison
{ {
@ -13,6 +15,7 @@ namespace unison
{ {
private readonly Settings _settingsWin; private readonly Settings _settingsWin;
private readonly Radios _radiosWin; private readonly Radios _radiosWin;
private readonly Shuffle _shuffleWin;
private readonly DispatcherTimer _timer; private readonly DispatcherTimer _timer;
private readonly MPDHandler _mpd; private readonly MPDHandler _mpd;
@ -25,6 +28,7 @@ namespace unison
_settingsWin = new Settings(); _settingsWin = new Settings();
_radiosWin = new Radios(); _radiosWin = new Radios();
_shuffleWin = new Shuffle();
_timer = new DispatcherTimer(); _timer = new DispatcherTimer();
_mpd = (MPDHandler)Application.Current.Properties["mpd"]; _mpd = (MPDHandler)Application.Current.Properties["mpd"];
@ -46,12 +50,13 @@ namespace unison
{ {
if (_mpd.IsConnected()) if (_mpd.IsConnected())
{ {
ConnectionOkIcon.Visibility = Visibility.Visible; _mpd.QueryStats();
ConnectionFailIcon.Visibility = Visibility.Collapsed; _settingsWin.UpdateStats();
Snapcast.IsEnabled = true; Snapcast.IsEnabled = true;
if (_radiosWin.IsConnected()) ConnectionOkIcon.Visibility = Visibility.Visible;
Radio.IsEnabled = true; ConnectionFailIcon.Visibility = Visibility.Collapsed;
_shuffleWin.Initialize();
} }
else else
{ {
@ -59,15 +64,12 @@ namespace unison
DefaultState(true); DefaultState(true);
ConnectionOkIcon.Visibility = Visibility.Collapsed; ConnectionOkIcon.Visibility = Visibility.Collapsed;
ConnectionFailIcon.Visibility = Visibility.Visible; ConnectionFailIcon.Visibility = Visibility.Visible;
Snapcast.IsEnabled = false;
Radio.IsEnabled = false;
} }
_settingsWin.UpdateConnectionStatus(); _settingsWin.UpdateConnectionStatus();
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}"; 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) if (_mpd.GetCurrentSong() == null)
return; return;
@ -113,6 +115,18 @@ namespace unison
_timer.Start(); _timer.Start();
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time); EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
} }
Debug.WriteLine("Song changed called!");
if (!_shuffleWin.GetContinuous())
return;
NextTrack.IsEnabled = false;
PreviousTrack.IsEnabled = false;
await _shuffleWin.HandleContinuous();
NextTrack.IsEnabled = true;
PreviousTrack.IsEnabled = true;
Debug.WriteLine("finished continuous");
} }
public void OnStatusChanged(object sender, EventArgs e) public void OnStatusChanged(object sender, EventArgs e)
@ -141,9 +155,6 @@ namespace unison
DefaultState(); DefaultState();
} }
} }
_mpd.QueryStats();
_settingsWin.UpdateStats();
} }
private void DefaultState(bool LostConnection = false) private void DefaultState(bool LostConnection = false)
@ -159,9 +170,8 @@ namespace unison
PlayPause.Text = (string)Application.Current.FindResource("pauseButton"); PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
TimeSlider.Value = 50; TimeSlider.Value = 50;
TimeSlider.IsEnabled = false; TimeSlider.IsEnabled = false;
NoCover.Visibility = Visibility.Collapsed; NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed; Cover.Visibility = Visibility.Collapsed;
RadioCover.Visibility = Visibility.Collapsed;
if (LostConnection) if (LostConnection)
{ {
@ -173,18 +183,16 @@ namespace unison
public void OnCoverChanged(object sender, EventArgs e) public void OnCoverChanged(object sender, EventArgs e)
{ {
NoCover.Visibility = Visibility.Collapsed; if (_mpd.GetCover() == null)
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; NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
}
else if (Cover.Source != _mpd.GetCover()) else if (Cover.Source != _mpd.GetCover())
{ {
Cover.Source = _mpd.GetCover(); Cover.Source = _mpd.GetCover();
Cover.Visibility = Visibility.Visible; Cover.Visibility = Visibility.Visible;
NoCover.Visibility = Visibility.Collapsed;
} }
} }
@ -197,6 +205,11 @@ namespace unison
SnapcastText.Text = unison.Resources.Resources.StartSnapcast; SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
} }
public void OnRadioBrowserConnected()
{
Radio.IsEnabled = true;
}
public void UpdateButton(ref Border border, bool b) public void UpdateButton(ref Border border, bool b)
{ {
border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"]; border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
@ -214,8 +227,18 @@ namespace unison
} }
public void Pause_Clicked(object sender, RoutedEventArgs e) => _mpd.PlayPause(); public void Pause_Clicked(object sender, RoutedEventArgs e) => _mpd.PlayPause();
public void Previous_Clicked(object sender, RoutedEventArgs e) => _mpd.Prev();
public void Next_Clicked(object sender, RoutedEventArgs e) => _mpd.Next(); public void Previous_Clicked(object sender, RoutedEventArgs e)
{
if (PreviousTrack.IsEnabled)
_mpd.Prev();
}
public void Next_Clicked(object sender, RoutedEventArgs e)
{
if (NextTrack.IsEnabled)
_mpd.Next();
}
public void Random_Clicked(object sender, RoutedEventArgs e) => _mpd.Random(); public void Random_Clicked(object sender, RoutedEventArgs e) => _mpd.Random();
public void Repeat_Clicked(object sender, RoutedEventArgs e) => _mpd.Repeat(); public void Repeat_Clicked(object sender, RoutedEventArgs e) => _mpd.Repeat();
@ -238,6 +261,15 @@ namespace unison
_radiosWin.WindowState = WindowState.Normal; _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) public void Settings_Clicked(object sender, RoutedEventArgs e)
{ {
_settingsWin.Show(); _settingsWin.Show();

View File

@ -54,9 +54,6 @@ namespace unison
{ {
private RadioBrowserClient _radioBrowser; private RadioBrowserClient _radioBrowser;
private MPDHandler _mpd; private MPDHandler _mpd;
private bool _connected = true;
public bool IsConnected() => _connected;
public Radios() public Radios()
{ {
@ -72,8 +69,11 @@ namespace unison
Debug.WriteLine("Exception while connecting to RadioBrowser: " + e.Message); Debug.WriteLine("Exception while connecting to RadioBrowser: " + e.Message);
return; return;
} }
Application.Current.Dispatcher.Invoke(() =>
_connected = true; {
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnRadioBrowserConnected();
});
} }
public async void Initialize() public async void Initialize()

View File

@ -43,7 +43,7 @@
<StackPanel Margin="0,5,0,0"> <StackPanel Margin="0,5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_Password}" TextWrapping="Wrap" Margin="5,0,0,0"/> <TextBlock Text="{x:Static properties:Resources.Settings_Password}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<PasswordBox x:Name="MpdPassword" KeyDown="ConnectHandler" Width="250" Margin="10,2,0,0"/> <PasswordBox x:Name="MpdPassword" Width="250" Margin="10,2,0,0"/>
<TextBlock Text="{x:Static properties:Resources.Settings_ConnectionPasswordInfo}" TextWrapping="Wrap" Margin="10,5,0,0" MaxWidth="250" HorizontalAlignment="Left"/> <TextBlock Text="{x:Static properties:Resources.Settings_ConnectionPasswordInfo}" TextWrapping="Wrap" Margin="10,5,0,0" MaxWidth="250" HorizontalAlignment="Left"/>
</StackPanel> </StackPanel>
@ -52,6 +52,7 @@
<Run x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}"/> <Run x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}"/>
</TextBlock> </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"/> <Button x:Name="ConnectButton" Content="{x:Static properties:Resources.Settings_ConnectButton}" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -59,36 +60,6 @@
</DockPanel> </DockPanel>
</TabItem> </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}"> <TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
<DockPanel Margin="8"> <DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0"> <GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
@ -190,7 +161,64 @@
</StackPanel> </StackPanel>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="ShortcutsReset_Clicked"/> <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> </StackPanel>
</Grid> </Grid>
</GroupBox> </GroupBox>

116
Views/Shuffle.xaml Normal file
View File

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

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

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@
<Company /> <Company />
<Authors>Théo Marchal</Authors> <Authors>Théo Marchal</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/ZetaKebab/unison</PackageProjectUrl> <PackageProjectUrl>https://git.n700.ovh/keb/unison</PackageProjectUrl>
<RepositoryUrl>https://github.com/ZetaKebab/unison</RepositoryUrl> <RepositoryUrl>https://git.n700.ovh/keb/unison</RepositoryUrl>
<Copyright>Théo Marchal</Copyright> <Copyright>Théo Marchal</Copyright>
<PackageIconUrl /> <PackageIconUrl />
</PropertyGroup> </PropertyGroup>
@ -22,8 +22,6 @@
<None Remove="Resources\icon-mini.ico" /> <None Remove="Resources\icon-mini.ico" />
<None Remove="Resources\nocover.png" /> <None Remove="Resources\nocover.png" />
<None Remove="LICENSE" /> <None Remove="LICENSE" />
<None Remove="Resources\nothing.png" />
<None Remove="Resources\radio.png" />
<None Include="LICENSE"> <None Include="LICENSE">
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath></PackagePath> <PackagePath></PackagePath>
@ -40,12 +38,6 @@
<Resource Include="Resources\nocover.png"> <Resource Include="Resources\nocover.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource> </Resource>
<Resource Include="Resources\nothing.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\radio.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Content Include="LICENSE"> <Content Include="LICENSE">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>